89712563a0
gates / gates (push) Successful in 10s
The retrieval clause rendered unconditionally on every page and is false on a reachable state - the same screen where the orphan card says we cannot tell. The condition is a fingerprint PINNED at the decision, not a comparison against the current key. The obvious proxy asks about the wrong key: the set-aside copies were written under an older key the box no longer has, so on a twice-rebuilt box the proxy promises about copies nothing can open. Demonstrated - under the proxy, the replaced-package and legacy cases both flip back to promising. The pin is a recorded assumption and says so: nothing on the box records which key wrote those copies. Empty is not a match. A countdown started before this carries no pin and takes the cautious branch, not a backfill. A sweep of all 36 templates found a fourth instance (backups page, same condition applied) and a fifth (the confirmation screen, correctly left alone - true at the moment of the decision). New retrieval_promise_gate registers each claim with a reason rather than banning a verb: a string ban failed twice, and the honest replacement copy contains the stem.
118 lines
5.4 KiB
Go
118 lines
5.4 KiB
Go
package web
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
)
|
|
|
|
// ── R-302 — RENDERED, at the boundary the defect lives at ───────────────────────────────────────
|
|
//
|
|
// The condition is unit-tested in internal/backup; these assert the SENTENCES, because the defect was
|
|
// always copy that disagreed with what the box could see, and only the rendered bytes show that.
|
|
|
|
// abandonBannerData renders a page carrying the countdown strip. `offered` is the R-302 verdict.
|
|
func abandonBannerData(offered bool, repoState string) map[string]interface{} {
|
|
d := splitTestData()
|
|
d["Offbox"] = &settings.OffboxTarget{
|
|
Enabled: true, Host: "nas.local", User: "felhom", RepoPath: "/srv/repo",
|
|
EscrowState: "escrowed", RepoState: repoState, QuotaGB: 50, StatsKnown: true,
|
|
}
|
|
d["OffboxQuotaPct"] = 0
|
|
d["RecoveryBanner"] = true
|
|
d["RecoveryAbandonDays"] = 3
|
|
d["RecoveryAbandonDate"] = "2026-08-26"
|
|
d["RecoveryAbandonRetrievalOffered"] = offered
|
|
return d
|
|
}
|
|
|
|
const (
|
|
promiseClause = "visszaszerezheted őket a helyreállítási kóddal"
|
|
cautiousClause = "nem tudjuk megállapítani"
|
|
deletionClause = "véglegesen töröljük"
|
|
writeToUs = "írj nekünk a törlés előtt"
|
|
)
|
|
|
|
// ── SCENARIO A — package unchanged → the clause stands, byte-identical in meaning to before ─────
|
|
func TestR302_Render_A_PromiseKeptWhenStillTrue(t *testing.T) {
|
|
html := renderBackupPage(t, "backups_remote", abandonBannerData(true, "ok"))
|
|
|
|
if !strings.Contains(html, deletionClause) {
|
|
t.Fatal("the deletion sentence is missing — that half is certain and must always render")
|
|
}
|
|
if !strings.Contains(html, promiseClause) {
|
|
t.Error("R-302: a customer who can genuinely still change their mind lost the retrieval clause. " +
|
|
"The grace period is explicitly NOT decorative; hedging a true sentence is its own dishonesty")
|
|
}
|
|
}
|
|
|
|
// ── SCENARIO B/D/E (rendered) — cautious branch says what is true and names a route ─────────────
|
|
//
|
|
// RED-PROOF: remove the `{{if .RecoveryAbandonRetrievalOffered}}` conditional from layout.html and
|
|
// this fails on the first assertion — the false promise returns.
|
|
func TestR302_Render_B_CautiousBranchWhenNotKnowable(t *testing.T) {
|
|
html := renderBackupPage(t, "backups_remote", abandonBannerData(false, "ok"))
|
|
|
|
if strings.Contains(html, promiseClause) {
|
|
t.Error("R-302: the banner still promises retrieval when the box cannot see that it is true — " +
|
|
"this is the sentence a customer reads after giving up their history")
|
|
}
|
|
if !strings.Contains(html, cautiousClause) {
|
|
t.Error("R-302: the cautious branch does not say we cannot determine it — silence is not the " +
|
|
"same as declining a claim")
|
|
}
|
|
if !strings.Contains(html, writeToUs) {
|
|
t.Error("R-302: the cautious branch names no route, and it is time-bounded — the customer must " +
|
|
"be told to write in BEFORE the deletion date")
|
|
}
|
|
// The certain half is unconditional.
|
|
if !strings.Contains(html, deletionClause) {
|
|
t.Error("R-302: the deletion sentence was lost with the promise — it is the part we DO know")
|
|
}
|
|
}
|
|
|
|
// ── SCENARIO C — THE CO-RENDER. The card and the banner must not contradict each other ──────────
|
|
//
|
|
// Reachable per yesterday's reading: ResetOrphanedRepo clears RepoState then starts the countdown, but
|
|
// markOrphaned (offbox.go:804) has NO guard against an active countdown, so a later run finding the
|
|
// FRESH store unopenable re-raises the card while the countdown runs.
|
|
//
|
|
// RED-PROOF — THE ONE THAT MATTERS: replace the condition with the rejected proxy (hub fingerprint vs
|
|
// the CURRENT key, compared at render). In this state those differ, so the proxy answers "promise it"
|
|
// and the false promise returns on the very screen the card is declining it. That is why the pin was
|
|
// chosen over the obvious condition.
|
|
func TestR302_Render_C_CoRenderDoesNotContradictItself(t *testing.T) {
|
|
html := renderBackupPage(t, "backups_remote", abandonBannerData(false, "orphaned"))
|
|
|
|
if !strings.Contains(html, "offbox-orphan-card") {
|
|
t.Fatal("the orphan card did not render — this test would then prove nothing about the co-render")
|
|
}
|
|
if !strings.Contains(html, deletionClause) {
|
|
t.Fatal("the banner did not render — likewise")
|
|
}
|
|
// The card says we cannot tell. The banner must not say the opposite one strip above it.
|
|
if strings.Contains(html, promiseClause) {
|
|
t.Error("R-302 CO-RENDER: the orphan card says we cannot determine whether the set-aside copies " +
|
|
"can be opened, and the banner above it tells the customer they can still retrieve them. " +
|
|
"One page, two answers, and the confident one is the wrong one")
|
|
}
|
|
}
|
|
|
|
// ── SCENARIO F — no countdown → the undecided reminder ladder is untouched ──────────────────────
|
|
func TestR302_Render_F_NoCountdownLeavesTheLadderAlone(t *testing.T) {
|
|
d := splitTestData()
|
|
d["RecoveryBanner"] = true
|
|
d["RecoveryReminderTier"] = 14 // an undecided box, two weeks waiting
|
|
html := renderBackupPage(t, "backups_remote", d)
|
|
|
|
for _, s := range []string{deletionClause, promiseClause, cautiousClause} {
|
|
if strings.Contains(html, s) {
|
|
t.Errorf("abandonment copy %q leaked onto a box with no countdown running", s)
|
|
}
|
|
}
|
|
if !strings.Contains(html, "Két hete") {
|
|
t.Error("the undecided reminder ladder changed — it is not in scope and must be byte-identical")
|
|
}
|
|
}
|