Files
felhom-controller/controller/internal/web/escrow_wait_state_test.go
T
admin 120332103a feat(escrow): v0.138.0 — "awaiting hub confirmation" waiting state
After a completed escrow ceremony the Távoli mentés page showed the yellow
"Helyreállítási kód szükséges" card for ~15 min until the next hub-report ACK
flipped pending→escrowed. Phase-0 diagnosis (read-only) = verdict A (report-cycle
lag), already resolved on the demo box (escrow_state:"escrowed"); hub Hypothesis B
verified false (SaveHostEscrow ON CONFLICT already clears stale_at on upload) → no
hub change.

- settings.OffboxTarget.CeremonyCompletedAt: stamped on the recovery-code claim,
  zeroed on the auto-confirmer Flip + the deprecated manual confirm; persisted.
- web/handlers.go: offboxCeremonyWaitState + escrowCeremonyGraceWindow (35m).
- backups_remote.html: info "megerősítésre vár, legfeljebb 15 perc" card → warn
  "a megerősítés nem érkezett meg" past the window. Existing branches untouched.
- backups_escrow.html: "Mi történik ezután?" note on the wizard's final step.
- Test web/escrow_wait_state_test.go (truth table + red-proof recorded in REPORT).

No scheduler/agent/hub/endpoint changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qDiBqKKQ5vPB5fXBqu7Kp
2026-07-16 18:44:13 +02:00

55 lines
3.0 KiB
Go

package web
import (
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)
// TestOffboxCeremonyWaitState — truth table for the post-ceremony escrow card pick (v0.138.0).
// The awaiting card bridges the report-cycle gap between a completed ceremony and the hub-verified
// pending→escrowed flip; it must degrade to a warning after the grace window and must NEVER show
// once escrowed, unstamped, or on an unparseable stamp (those fall back to the plain pending CTA).
//
// COMPANION red-proof (run → fail → revert, recorded in REPORT): change the escrowed guard so it
// no longer short-circuits (e.g. drop `t.EscrowState == "escrowed"` from the early return) → the
// "escrowed clears the wait" case below FAILS (it would report awaiting on an already-confirmed
// target, resurfacing the interim card after the healthy state). Alternatively flip `>=` to `>` at
// the boundary and the exact-boundary case FAILS.
func TestOffboxCeremonyWaitState(t *testing.T) {
now := time.Now()
within := now.Add(-10 * time.Minute).Format(time.RFC3339) // inside the 35m grace window
past := now.Add(-40 * time.Minute).Format(time.RFC3339) // past the grace window
boundary := now.Add(-escrowCeremonyGraceWindow).Format(time.RFC3339) // exactly at the window → timed out (>=)
cases := []struct {
name string
target *settings.OffboxTarget
wantAwaiting bool
wantTimedOut bool
}{
{"nil target", nil, false, false},
{"pending, no stamp (plain CTA)", &settings.OffboxTarget{EscrowState: "pending"}, false, false},
{"pending, stamped within window → awaiting", &settings.OffboxTarget{EscrowState: "pending", CeremonyCompletedAt: within}, true, false},
{"pending, stamped past window → timed out", &settings.OffboxTarget{EscrowState: "pending", CeremonyCompletedAt: past}, false, true},
{"pending, stamped at boundary → timed out", &settings.OffboxTarget{EscrowState: "pending", CeremonyCompletedAt: boundary}, false, true},
{"escrowed clears the wait (stamp ignored)", &settings.OffboxTarget{EscrowState: "escrowed", CeremonyCompletedAt: within}, false, false},
{"empty state, stamped within window → awaiting", &settings.OffboxTarget{EscrowState: "", CeremonyCompletedAt: within}, true, false},
{"pending, unparseable stamp → plain CTA", &settings.OffboxTarget{EscrowState: "pending", CeremonyCompletedAt: "not-a-timestamp"}, false, false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
gotAwaiting, gotTimedOut := offboxCeremonyWaitState(c.target)
if gotAwaiting != c.wantAwaiting || gotTimedOut != c.wantTimedOut {
t.Errorf("offboxCeremonyWaitState() = (awaiting=%v, timedOut=%v), want (awaiting=%v, timedOut=%v)",
gotAwaiting, gotTimedOut, c.wantAwaiting, c.wantTimedOut)
}
// Mutual exclusion invariant — the two card branches must never both fire.
if gotAwaiting && gotTimedOut {
t.Errorf("%s: both awaiting and timedOut true — the card branches are not mutually exclusive", c.name)
}
})
}
}