package web import ( "strings" "testing" "unicode/utf8" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" ) // ── R-294 / R-202 — THE ORPHAN CARD STOPS PROMISING WHAT IT CANNOT KNOW ───────────────────────── // // The card told a customer, at the moment they had just lost their off-site history, that the old // copies "may be restorable later with their recovery code". The discriminator is // host_escrow_superseded.identity_blob and it lives on the HUB; the box caches only // HubEscrowIdentityPresent, which describes the CURRENT escrow, and no report or ACK field carries // superseded-blob retention. The renderer therefore could not evaluate the condition it stated. // // For everything set aside before hub v0.93.0 (in force 2026-08-04 ~11:11Z) the promise is false and // unfixable, and on 2026-08-10 it was being made to a real machine in exactly that state. // // Implements documentation/design/SPEC-orphan-card-copy-2026-08-10.md §5. Render tests per branch of // the gate, because a template gate without one is the seam-wiring lesson. // orphanCardData renders the backups_remote page with the offbox repo in the given state. Only // RepoState varies between the branches below, so what each test proves is attributable. func orphanCardData(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 return d } // ── Branch 1: the card IS shown ───────────────────────────────────────────────────────────────── // SPEC §5.1 — the regression guard. The promise must not return in any form. // // RED-PROOF: restore the old sentence („…és a hozzá tartozó helyreállítási kóddal később // visszaállítható lehet.") in backups_remote.html and this fails on the first assertion, with the // promise quoted back in the failure message. func TestOrphanCard_DoesNotPromiseRestorability(t *testing.T) { html := renderBackupPage(t, "backups_remote", orphanCardData("orphaned")) if !strings.Contains(html, "offbox-orphan-card") { t.Fatal("the orphan card did not render at all — this test would then pass vacuously, " + "which is the way a copy guard silently stops guarding") } // R-299: the guard matches the STEM, not one inflection. The first version of this test asserted // „visszaállítható lehet" (singular) and the card carried „visszaállíthatók lehetnek" (plural) one // paragraph above it — the same claim, invisible to the guard. A guard matching one inflection of a // Hungarian verb guards one SENTENCE, not the claim. `visszaállíthat` is the potential stem // ("can be restored"); the plain forms the rest of the UI uses („visszaállítás", „visszaállítani") // do not contain it, so this does not over-match. // // It asserts on the RENDERED bytes, which is why the explanatory {{/* */}} comment in the template // may quote the retired wording: html/template strips it. An HTML comment would SHIP and // would make this test unfailable — that is the R-253 trap, and it is why the comment form matters. if idx := strings.Index(html, "visszaállíthat"); idx >= 0 { // Slice on RUNE boundaries. Go string indexing is by byte, and cutting Hungarian text at a // byte offset splits a multi-byte character — the failure message then shows a replacement // char and reads like an encoding bug in the product rather than in this message. start, end := idx-80, idx+90 if start < 0 { start = 0 } if end > len(html) { end = len(html) } for start < len(html) && !utf8.RuneStart(html[start]) { start++ } for end < len(html) && !utf8.RuneStart(html[end]) { end++ } t.Errorf("R-294/R-299: the card still promises the set-aside copies may be restorable. The box "+ "cannot evaluate that — the discriminator (superseded identity_blob) is on the hub and no "+ "wire field carries it — and for everything set aside before 2026-08-04 it is false.\n"+ " found: …%s…", html[start:end]) } } // SPEC §5.2 — a refusal that names no route is a defect in this project. Pin the route, not only the // absence of the promise. // // RED-PROOF: delete the „írj nekünk" sentence and this fails — the customer is told we cannot promise // anything and given nowhere to go. func TestOrphanCard_NamesARouteAfterDeclining(t *testing.T) { html := renderBackupPage(t, "backups_remote", orphanCardData("orphaned")) if !strings.Contains(html, "nem tudjuk megígérni") { t.Error("R-294: the card no longer DECLINES the claim — stating nothing is not the same as " + "saying plainly that we cannot promise it") } if !strings.Contains(html, "írj nekünk") { t.Error("R-294: the card declines the promise but names no route the customer can take") } // It must still say the copy is kept — otherwise "we cannot promise" reads as "it is gone". if !strings.Contains(html, "nem töröljük") { t.Error("R-294: the card no longer says the set-aside copy is kept; without that, declining " + "the promise reads to the customer as data loss") } } // The reason the card exists — the orphan EXPLANATION — is accurate and must survive the copy change. func TestOrphanCard_KeepsTheExplanation(t *testing.T) { html := renderBackupPage(t, "backups_remote", orphanCardData("orphaned")) if !strings.Contains(html, "korábbi, már nem elérhető kulccsal") { t.Error("the explanation of WHY the store is orphaned was lost — the customer is then shown a " + "refusal with no cause") } } // ── Branch 2: the card is NOT shown ───────────────────────────────────────────────────────────── // SPEC §5.3 — per branch of the gate. A healthy store must not see any of this copy: R-215's shape is // a screen about a situation the customer is not in. func TestOrphanCard_HealthyStoreSeesNoneOfIt(t *testing.T) { html := renderBackupPage(t, "backups_remote", orphanCardData("ok")) if strings.Contains(html, "offbox-orphan-card") { t.Fatal("the orphan card rendered for a healthy store") } for _, s := range []string{"nem tudjuk megígérni", "írj nekünk", "félretéve marad"} { if strings.Contains(html, s) { t.Errorf("orphan copy %q leaked onto a healthy box's page", s) } } } // R-299 — the EXPLANATION paragraph keeps its two accurate halves and drops the promise. // // This is the always-visible half of the card; the paragraph fixed yesterday only appears after the // customer clicks through to the confirm block. So on first view this is the ONLY text they read, // and until now it was the one still making the promise. func TestOrphanCard_ExplanationKeepsWhatIsTrueAndDropsThePromise(t *testing.T) { html := renderBackupPage(t, "backups_remote", orphanCardData("orphaned")) // The two halves that ARE knowable from the box must survive. for _, keep := range []string{ "nem elérhető kulccsal", // why the store is orphaned "nem írható a tárolóba", // the consequence right now "nem sérültek", // the backups are intact — knowable, and reassuring for a reason } { if !strings.Contains(html, keep) { t.Errorf("R-299: the explanation lost an accurate statement (%q). Declining the promise must "+ "not turn into telling the customer less than we know", keep) } } // …and the part the box cannot evaluate must be declined, with a route. if !strings.Contains(html, "nem tudja megállapítani") { t.Error("R-299: the explanation no longer says the machine cannot determine this — silence is " + "not the same as declining a claim") } if !strings.Contains(html, "írj nekünk") { t.Error("R-299: the explanation declines the claim but names no route") } }