68f3e12398
gates / gates (push) Successful in 14s
The explanation paragraph - the always-visible half of the card - still ended 'a hozzajuk tartozo helyreallitasi koddal kesobb visszaallithatok lehetnek', the same unevaluable claim v0.211.0 removed from the confirm block below it. It survived because the spec called that line accurate, and because the regression guard asserted the SINGULAR form while the card carried the plural, which does not contain that substring. The guard now matches the stem, so any conjugation fails it. The two accurate halves are kept. Also: the guard's failure message sliced rendered HTML at a byte offset and cut Hungarian mid-character; it now slices on rune boundaries.
162 lines
8.0 KiB
Go
162 lines
8.0 KiB
Go
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")
|
|
}
|
|
}
|