v0.290.0: the clean-up guard skips same-day superseded young snapshots instead of refusing (R-824), refuses above the weekly cap; a due set-aside deletion is handed to the hub's 7-day wait (decision 74, R-823)
gates / gates (push) Successful in 27s

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-10-04 07:27:38 +02:00
parent c4bf730637
commit c1a73b24b3
10 changed files with 309 additions and 68 deletions
+110 -33
View File
@@ -169,8 +169,8 @@ func TestOffsiteGuard_RecentRemovalRefused(t *testing.T) {
}
}
// Honest retention inside a window: the oldest first, at most MaxRemove, and the forget names ids.
func TestOffsiteGuard_HonestPlanPrunesOldestFirstCapped(t *testing.T) {
// Honest retention inside a window: oldest first, the forget names ids.
func TestOffsiteGuard_HonestPlanPrunesOldestFirst(t *testing.T) {
m, sett := newOffboxManager(t)
pinTarget(t, sett)
now := time.Now()
@@ -178,21 +178,60 @@ func TestOffsiteGuard_HonestPlanPrunesOldestFirstCapped(t *testing.T) {
all := append([]guardSnap{snap("keep", now.Add(-time.Hour))}, plan...)
wr := &windowRunner{snaps: all, plan: plan}
m.SetOffboxRunner(wr.run)
fw := &fakeWindow{grant: OffsiteWindow{Granted: true, ID: 9, NewestAllowed: now, MaxRemove: 2}}
fw := &fakeWindow{grant: OffsiteWindow{Granted: true, ID: 9, NewestAllowed: now, MaxRemove: 5}}
m.SetOffsiteWindowClient(fw)
m.offsiteWindowRetention(context.Background(), nil, nil, "after-run")
if len(wr.forgets) != 1 {
t.Fatalf("forgets = %v", wr.forgets)
}
got := strings.Join(wr.forgets[0], " ")
if !strings.Contains(got, "forget a-full b-full --prune") || strings.Contains(got, "c-full") {
t.Fatalf("forget = %q (want the two OLDEST, capped)", got)
if len(wr.forgets) != 1 || !strings.Contains(strings.Join(wr.forgets[0], " "), "forget a-full b-full c-full --prune") {
t.Fatalf("forget = %v", wr.forgets)
}
if len(fw.closed) != 1 || fw.closed[0].Outcome != "pruned" || fw.closed[0].ID != 9 {
t.Fatalf("close = %+v", fw.closed)
}
}
// A plan larger than one week's removal REFUSES (the 2026-10-04 brief), nothing removed.
func TestOffsiteGuard_AboveWeeklyCapRefused(t *testing.T) {
now := time.Now()
var plan []guardSnap
for i := 0; i < 6; i++ {
plan = append(plan, snap(fmt.Sprintf("o%d", i), now.Add(-time.Duration(30+i)*24*time.Hour)))
}
ids, why := offsiteGuard(plan, plan, now, now, 5)
if ids != nil || !strings.Contains(why, "more than one week") {
t.Fatalf("ids=%v why=%q", ids, why)
}
}
// R-824, THE MEASURED SHAPE (demo-hp window 1, 2026-10-03): the night run's snapshots of each app were
// superseded the SAME DAY by a manual run, so the honest policy removes them while they are young.
// v0.289 refused the whole window; now they are EXCLUDED (kept for later) and the old removal goes ahead.
func TestOffsiteGuard_SameDaySupersededYoungExcluded(t *testing.T) {
m, sett := newOffboxManager(t)
pinTarget(t, sett)
now := time.Now()
day := now.Add(-26 * time.Hour).Truncate(24 * time.Hour)
night := guardSnap{ID: "night-full", ShortID: "night", Time: day.Add(2 * time.Hour), Hostname: "demo-hp", Tags: []string{"opengist"}}
manual := guardSnap{ID: "manual-full", ShortID: "manual", Time: day.Add(15 * time.Hour), Hostname: "demo-hp", Tags: []string{"opengist"}}
old := guardSnap{ID: "old-full", ShortID: "old", Time: now.Add(-40 * 24 * time.Hour), Hostname: "demo-hp", Tags: []string{"opengist"}}
wr := &windowRunner{snaps: []guardSnap{old, night, manual}, plan: []guardSnap{night, old}}
m.SetOffboxRunner(wr.run)
fw := &fakeWindow{grant: OffsiteWindow{Granted: true, ID: 2, NewestAllowed: now, MaxRemove: 5}}
m.SetOffsiteWindowClient(fw)
m.offsiteWindowRetention(context.Background(), nil, nil, "after-run")
if len(fw.closed) != 1 || fw.closed[0].Outcome != "pruned" {
t.Fatalf("the window must run, not refuse: %+v", fw.closed)
}
got := strings.Join(wr.forgets[0], " ")
if !strings.Contains(got, "old-full") || strings.Contains(got, "night-full") {
t.Fatalf("forget = %q (the young superseded copy must be KEPT for now)", got)
}
// A young removal with NO same-day successor in its group is still the poisoning signature.
lone := guardSnap{ID: "lone-full", ShortID: "lone", Time: now.Add(-50 * time.Hour), Hostname: "demo-hp", Tags: []string{"bookstack"}}
if _, why := offsiteGuard([]guardSnap{lone, manual}, []guardSnap{lone}, now, now, 5); !strings.Contains(why, "not superseded the same day") {
t.Fatalf("why = %q", why)
}
}
// The orphan reset on the pinned tier asks the HUB; no ssh `mv` from the box.
func TestResetOrphaned_PinnedAsksTheHub(t *testing.T) {
m, sett := newOffboxManager(t)
@@ -212,30 +251,6 @@ func TestResetOrphaned_PinnedAsksTheHub(t *testing.T) {
}
}
// Abandonment on the pinned tier: due → nothing deleted, the operator is told, the sweep goes quiet.
func TestAbandon_PinnedDefersToOperator(t *testing.T) {
m, sett := newOffboxManager(t)
pinTarget(t, sett)
sett.UpdateOffboxStatus(func(o *settings.OffboxTarget) {
o.AbandonRepoPath = "/home/felhom-repo.orphaned-20260901"
o.AbandonStartedAt = time.Now().Add(-20 * 24 * time.Hour).UTC().Format(time.RFC3339)
o.AbandonAt = time.Now().Add(-time.Hour).UTC().Format(time.RFC3339)
})
m.SetOffboxSSH(func(context.Context, string, string, int, string, string, string) ([]byte, error) {
t.Fatal("the box tried to delete on the pinned tier")
return nil, nil
})
var evs []string
m.SetOffboxOrphanEvent(func(e, _ string) { evs = append(evs, e) })
deleted, err := m.AbandonSweep(context.Background())
if deleted || err != nil || len(evs) != 1 || evs[0] != "offbox_abandon_deferred" {
t.Fatalf("deleted=%v err=%v events=%v", deleted, err, evs)
}
if again, _ := m.AbandonSweep(context.Background()); again {
t.Fatal("second sweep deleted")
}
}
// The provider's rclone notice must not reach a JSON parser — measured live on demo-felhom (v0.289.0).
func TestStripRcloneNotice(t *testing.T) {
in := "rclone: 2026/10/03 15:05:42 NOTICE: Config file \"/home/.config/rclone/rclone.conf\" not found - using defaults\n[{\"id\":\"s1\"}]\n"
@@ -266,3 +281,65 @@ func TestRunOffbox_UnreadableCountIsNotZero(t *testing.T) {
t.Fatalf("an unreadable count was recorded as a measured zero: %+v", got.SnapshotCount)
}
}
type fakeAbandon struct {
requested []string
state string
cancels int
due time.Time
}
func (f *fakeAbandon) Request(_ context.Context, p string) (time.Time, error) {
f.requested = append(f.requested, p)
f.state = "pending"
return f.due, nil
}
func (f *fakeAbandon) Status(context.Context) (string, error) { return f.state, nil }
func (f *fakeAbandon) Cancel(context.Context) error { f.cancels++; f.state = "cancelled"; return nil }
// Decision 74 (R-823), the box side: a due abandonment on the pinned tier is HANDED to the hub (nothing
// deleted by the box), the page keeps a dated, cancellable deletion, a recovery cancels it at the hub, and
// a "deleted" from the hub completes the two-phase commit.
func TestAbandon_PinnedHandsToHubAndFollows(t *testing.T) {
m, sett := newOffboxManager(t)
pinTarget(t, sett)
setDue := func() {
sett.UpdateOffboxStatus(func(o *settings.OffboxTarget) {
o.AbandonRepoPath = "/home/felhom-repo.orphaned-20260901"
o.AbandonStartedAt = time.Now().Add(-20 * 24 * time.Hour).UTC().Format(time.RFC3339)
o.AbandonAt = time.Now().Add(-time.Hour).UTC().Format(time.RFC3339)
})
}
setDue()
m.SetOffboxSSH(func(context.Context, string, string, int, string, string, string) ([]byte, error) {
t.Fatal("the box tried to delete on the pinned tier")
return nil, nil
})
fa := &fakeAbandon{due: time.Now().Add(7 * 24 * time.Hour)}
m.SetOffsiteAbandonClient(fa)
if del, err := m.AbandonSweep(context.Background()); del || err != nil || len(fa.requested) != 1 {
t.Fatalf("del=%v err=%v requested=%v", del, err, fa.requested)
}
st := m.AbandonStatus()
if !st.Active || !st.HubPending || st.DaysLeft < 6 {
t.Fatalf("the page would lose the dated deletion: %+v", st)
}
// Still pending: nothing happens, nothing re-requested.
if del, _ := m.AbandonSweep(context.Background()); del || len(fa.requested) != 1 {
t.Fatal("re-requested or deleted while pending")
}
// The household recovers → cancelled at the hub, countdown gone.
m.CancelAbandon("recovery succeeded")
if fa.cancels != 1 || m.AbandonStatus().Active {
t.Fatalf("cancels=%d status=%+v", fa.cancels, m.AbandonStatus())
}
// Again, and this time the hub deletes.
setDue()
_, _ = m.AbandonSweep(context.Background())
fa.state = "deleted"
var evs []string
m.SetOffboxOrphanEvent(func(e, _ string) { evs = append(evs, e) })
if del, err := m.AbandonSweep(context.Background()); !del || err != nil || !m.AbandonStatus().PurgeRequested || len(evs) != 1 || evs[0] != "offbox_abandon_completed" {
t.Fatalf("del=%v err=%v status=%+v evs=%v", del, err, m.AbandonStatus(), evs)
}
}