Operator actions in the report reply (D1, R-314/R-279/R-177, decision 185)
The hub may ask the running controller for a CLOSED list of actions, carried in the report ACK (operator_actions) and answered on the next report (operator_action_results): offsite_backup_now, abandon_stop, abandon_extend (1-30 days), run_job (fill-watch, offsite-integrity, offsite-proof, disk-health-check). Unknown action/job/argument -> refused, nothing called. Once per id (in memory; every action is safe to repeat). - internal/report/opactions.go: the executor; results re-sent until the hub stops listing the id. - scheduler.RunNow: refuses unknown / already-running jobs; OnDemand(ctx) makes an operator's offsite-integrity run even when not due. - ExtendAbandon never shortens the countdown and refuses in the hub phase; StopAbandon reports failure when the hub cancel failed (it said success). - Report ACK read cap 4 KiB -> 64 KiB (an ACK over the cap dropped every field in it). 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:
@@ -0,0 +1,85 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
)
|
||||
|
||||
// `09` §3 decision 185 (D1): the operator's abandon_extend and abandon_stop now arrive from the hub,
|
||||
// with no shell and no human reading the CLI's printout. Three guarantees the CLI lever did not give,
|
||||
// each pinned here because the hub action relies on it:
|
||||
// 1. an „extend" never moves the deletion EARLIER (decision 185: no action may shorten a countdown);
|
||||
// 2. an „extend" is refused while the deletion is the HUB's (decision 74) — the box's own date would
|
||||
// change on the page while the hub still deleted on its date;
|
||||
// 3. a „stop" that could not cancel the hub's pending deletion reports a failure, never success.
|
||||
|
||||
func TestD1_ExtendNeverShortensTheCountdown(t *testing.T) {
|
||||
start := time.Date(2026, 8, 7, 12, 0, 0, 0, time.UTC)
|
||||
m, sett, _ := abandonFixture(t, start)
|
||||
if err := m.ResetOrphanedRepo(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before := m.AbandonStatus().DueAt // start + 14 days
|
||||
// Day 1: „extend by 3 days" would mean day 4 — ten days EARLIER than today's due date.
|
||||
m.SetOffboxClock(func() time.Time { return start.AddDate(0, 0, 1) })
|
||||
if _, err := m.ExtendAbandon(3); err == nil {
|
||||
t.Fatal("an extension that moves the deletion earlier was accepted")
|
||||
}
|
||||
if got := m.AbandonStatus().DueAt; !got.Equal(before) {
|
||||
t.Fatalf("a refused extension changed the due date: %v -> %v", before, got)
|
||||
}
|
||||
if got := sett.GetOffboxTarget().AbandonAt; got != before.Format(time.RFC3339) {
|
||||
t.Fatalf("settings.json due date moved: %q", got)
|
||||
}
|
||||
// Control: a real extension (later than today's due date) is accepted.
|
||||
if due, err := m.ExtendAbandon(20); err != nil || !due.After(before) {
|
||||
t.Fatalf("a real extension: due=%v err=%v", due, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestD1_ExtendRefusedWhileTheDeletionIsTheHubs(t *testing.T) {
|
||||
m, sett := newOffboxManager(t)
|
||||
pinTarget(t, sett)
|
||||
hubDue := time.Now().Add(5 * 24 * time.Hour).UTC()
|
||||
if err := sett.UpdateOffboxStatus(func(o *settings.OffboxTarget) {
|
||||
o.AbandonRepoPath = "/home/felhom-repo.orphaned-20260901"
|
||||
o.AbandonHubDueAt = hubDue.Format(time.RFC3339)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !m.AbandonStatus().HubPending {
|
||||
t.Fatal("fixture: expected a hub-phase deletion")
|
||||
}
|
||||
if _, err := m.ExtendAbandon(30); err == nil {
|
||||
t.Fatal("an extension during the hub phase was accepted — the hub would still delete on its own date")
|
||||
}
|
||||
if got := sett.GetOffboxTarget().AbandonAt; got != "" {
|
||||
t.Fatalf("the refused extension wrote a box-side date %q the page would show", got)
|
||||
}
|
||||
}
|
||||
|
||||
type failingCancelAbandon struct{ fakeAbandon }
|
||||
|
||||
func (f *failingCancelAbandon) Cancel(context.Context) error { return errors.New("hub unreachable") }
|
||||
|
||||
func TestD1_StopReportsFailureWhenTheHubCancelFails(t *testing.T) {
|
||||
m, sett := newOffboxManager(t)
|
||||
pinTarget(t, sett)
|
||||
if err := sett.UpdateOffboxStatus(func(o *settings.OffboxTarget) {
|
||||
o.AbandonRepoPath = "/home/felhom-repo.orphaned-20260901"
|
||||
o.AbandonHubDueAt = time.Now().Add(5 * 24 * time.Hour).UTC().Format(time.RFC3339)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m.SetOffsiteAbandonClient(&failingCancelAbandon{})
|
||||
if err := m.StopAbandon(); err == nil {
|
||||
t.Fatal("StopAbandon returned success while the hub's deletion is still pending")
|
||||
}
|
||||
if !m.AbandonStatus().Active {
|
||||
t.Fatal("fixture: the countdown must still be running after a failed cancel")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user