v0.275.0: a backup's data and its version travel together (R-696, 07 §6.6, D4 option A); R-695, R-691, R-694
gates / gates (push) Successful in 23s

The unit's data files are stamped with the versions that wrote them; the capture keeps the
definition the data belongs to; a restore never starts data under another version's
definition (unit restores refuse a mismatch; the off-site restore writes the snapshot's
definition); every tier's time is its data's; the conversion-copy release needs a dump on
the new engine. File-browser sync single-flight + no empty kept folder (R-695); the kept
view joins the folder's owning group, language switch resyncs (R-691); a restore-generated
login is not shown as the password (R-694). Red-proofs in
felhom.eu/documentation/audits/version-travel-2026-09-26/.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-26 10:35:22 +02:00
parent fb2bcdd5d6
commit b6810f14ff
47 changed files with 3771 additions and 135 deletions
@@ -85,7 +85,7 @@ func newUnitNotifyManager(t *testing.T, stacks []string, fail map[string]bool) (
func TestCaptureAll_FailureNotifiesOnceWithTheSpaceFigures(t *testing.T) {
m, got := newUnitNotifyManager(t, []string{"immich"}, map[string]bool{"immich": true})
m.captureAllRecoveryUnits()
m.captureAllRecoveryUnits(false)
if len(*got) != 1 {
t.Fatalf("got %d unit-failure events, want exactly 1 — a per-app Tier-1 capture failure "+
@@ -117,7 +117,7 @@ func TestCaptureAll_OneFailureDoesNotAbortOrDuplicate(t *testing.T) {
[]string{"homebox", "immich", "nextcloud"},
map[string]bool{"immich": true})
m.captureAllRecoveryUnits()
m.captureAllRecoveryUnits(false)
if len(*got) != 1 {
t.Fatalf("got %d events, want exactly 1 — either the loop ABORTED on the middle app "+
@@ -134,7 +134,7 @@ func TestCaptureAll_OneFailureDoesNotAbortOrDuplicate(t *testing.T) {
m2, got2 := newUnitNotifyManager(t,
[]string{"homebox", "immich", "nextcloud"},
map[string]bool{"immich": true, "nextcloud": true})
m2.captureAllRecoveryUnits()
m2.captureAllRecoveryUnits(false)
if len(*got2) != 2 {
t.Fatalf("got %d events, want 2 — the app AFTER the first failure was never reached, so the "+
"loop is aborting rather than continuing: %+v", len(*got2), *got2)
@@ -148,7 +148,7 @@ func TestCaptureAll_OneFailureDoesNotAbortOrDuplicate(t *testing.T) {
// to ignore.
func TestCaptureAll_SuccessIsSilent(t *testing.T) {
m, got := newUnitNotifyManager(t, []string{"homebox"}, nil)
m.captureAllRecoveryUnits()
m.captureAllRecoveryUnits(false)
if len(*got) != 0 {
t.Fatalf("a successful capture fired %d event(s): %+v", len(*got), *got)
}
@@ -163,7 +163,7 @@ func TestCaptureAll_UnwiredNotifyDoesNotPanic(t *testing.T) {
systemDataPath: dir,
stackProvider: &unitFailProvider{stacks: []string{"immich"}, fail: map[string]bool{"immich": true}, dir: dir},
}
m.captureAllRecoveryUnits() // no SetUnitNotify — must not panic
m.captureAllRecoveryUnits(false) // no SetUnitNotify — must not panic
}
// §8.4 in the failure direction: an unreadable target filesystem is reported as UNKNOWN, never as