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) } }) } }