v0.254.0 — the saved notes follow the language, and the switch becomes a globe (R-557 slice 2 release C; SLICE 2 CLOSED)
gates / gates (push) Successful in 23s
gates / gates (push) Successful in 23s
The notes a background run SAVES — last night's backup line, the last error, the proof result, the restore outcome — are written in the BOX's language at the moment they are written. A household that switches sees the previous run's note in the old language until the next run rewrites it: the operator's §16 option 1, stated rather than hidden. EndRestoreOp no longer receives a Hungarian literal from anywhere. The language switch is a globe. Two text links wrapped in the sidebar footer and asked the reader to recognise "Magyar"/"English" as links; a globe is the one symbol every web user already reads as "language", so nobody has to read Hungarian to escape Hungarian. It is <details>/<summary> — a menu with no script, drawn inline because the icon sprite lives only in layout.html and the visitor pages have their own shell. Those visitor pages get the same globe, and a visitor's choice stays theirs: a display-only felhom_lang cookie that langFor reads ONLY when there is no session. A signed-in household can never inherit a language a previous visitor picked in the same browser. POST /lang is CSRF-exempt for a narrow reason written at the exemption — its only achievable effect is the language of the page the victim's own browser shows them — and safeBackPath refuses //evil.example as well as https://, because "starts with /" alone is not the test. §16 taken: a successful claim carries the cookie into the household's setting. TWO PARITY EXCEPTIONS, MEASURED: 106 fixtures compared with a real diff — exactly two change shapes (the dashboard footer, the globe in the shells) and 5 byte-identical, which are the three pages that must not change. I INTRODUCED A DEADLOCK AND THE SUITE CAUGHT IT BY HANGING. UpdateOffboxStatus holds the settings write lock while running its callback; boxLang() wants the read lock; sync.RWMutex is not reentrant. On a real box an off-site run would have hung forever HOLDING the settings lock. Fixed by resolving the language before the callback, and guarded by a test that names the file and line in a second instead of hanging for 25 minutes. MinAgent: 0.131.0 (unchanged). No hub release needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -32,7 +32,7 @@ import (
|
||||
|
||||
// r103Row is a Tier-2-configured row with a successful copy — the shape the template needs before it
|
||||
// will render the actions block at all.
|
||||
func r103Row(unitRestorable bool, date string, proven bool) AppBackupRow {
|
||||
func r103Row(t *testing.T, unitRestorable bool, date string, proven bool) AppBackupRow {
|
||||
row := AppBackupRow{
|
||||
StackName: "docmost", DisplayName: "Docmost",
|
||||
Tier2Configured: true, Tier2Dest: "flash", Tier2Schedule: "Naponta",
|
||||
@@ -42,7 +42,7 @@ func r103Row(unitRestorable bool, date string, proven bool) AppBackupRow {
|
||||
}
|
||||
if unitRestorable {
|
||||
row.Tier2CopyDate, row.Tier2CopyDateProven = date, proven
|
||||
row.Tier2UnitConfirm = tier2UnitConfirmMsg(date, proven)
|
||||
row.Tier2UnitConfirm = noteServer(t).tier2UnitConfirmMsg(date, proven)
|
||||
}
|
||||
return row
|
||||
}
|
||||
@@ -50,13 +50,13 @@ func r103Row(unitRestorable bool, date string, proven bool) AppBackupRow {
|
||||
// D1 — TestR103_UnitActionOfferedWhenTheMirrorExists.
|
||||
func TestR103_UnitActionOfferedWhenTheMirrorExists(t *testing.T) {
|
||||
html := renderBackupPage(t, "backups_apps", baseBackupData([]AppBackupRow{
|
||||
r103Row(true, "2026-08-25T03:30:00Z", true),
|
||||
r103Row(t, true, "2026-08-25T03:30:00Z", true),
|
||||
}))
|
||||
if !strings.Contains(html, `action="/backup/tier2/unit-restore"`) {
|
||||
t.Error("the unit-restore form is not on the page — the refusal is still a dead end")
|
||||
}
|
||||
if !strings.Contains(html, tier2UnitActionLabel) {
|
||||
t.Errorf("the action is not labelled %q", tier2UnitActionLabel)
|
||||
if !strings.Contains(html, noteHU(t, tier2UnitActionLabelKey)) {
|
||||
t.Errorf("the action is not labelled %q", noteHU(t, tier2UnitActionLabelKey))
|
||||
}
|
||||
// The additive action must still be there, separately. Merging them is forbidden: they are
|
||||
// different promises (one adds, one overwrites).
|
||||
@@ -74,12 +74,12 @@ func TestR103_UnitActionOfferedWhenTheMirrorExists(t *testing.T) {
|
||||
// button, because it is a promise withdrawn at the moment of use.
|
||||
func TestR103_UnitActionAbsentWhenItDoesNot(t *testing.T) {
|
||||
html := renderBackupPage(t, "backups_apps", baseBackupData([]AppBackupRow{
|
||||
r103Row(false, "2026-08-25T03:30:00Z", true),
|
||||
r103Row(t, false, "2026-08-25T03:30:00Z", true),
|
||||
}))
|
||||
if strings.Contains(html, "/backup/tier2/unit-restore") {
|
||||
t.Error("the unit action was offered for a copy with no openable unit")
|
||||
}
|
||||
if strings.Contains(html, tier2UnitActionLabel) {
|
||||
if strings.Contains(html, noteHU(t, tier2UnitActionLabelKey)) {
|
||||
t.Error("the unit action's label leaked onto a row that must not offer it")
|
||||
}
|
||||
// NEGATIVE CONTROL for D1's assertions: the row itself did render, so D1's positives were not
|
||||
@@ -93,18 +93,18 @@ func TestR103_UnitActionAbsentWhenItDoesNot(t *testing.T) {
|
||||
// requires: a destructive operation reached from a non-destructive surface says so, and says how it
|
||||
// differs from the action beside it.
|
||||
func TestR103_ConfirmStatesTheOverwrite(t *testing.T) {
|
||||
confirm := tier2UnitConfirmMsg("2026-08-25T03:30:00Z", true)
|
||||
confirm := noteServer(t).tier2UnitConfirmMsg("2026-08-25T03:30:00Z", true)
|
||||
|
||||
if !strings.Contains(confirm, tier2UnitConfirmBase) {
|
||||
if !strings.Contains(confirm, noteHU(t, tier2UnitConfirmBaseKey)) {
|
||||
t.Error("the confirm does not state that the operation OVERWRITES live data")
|
||||
}
|
||||
if !strings.Contains(confirm, tier2UnitConfirmContrast) {
|
||||
if !strings.Contains(confirm, noteHU(t, tier2UnitConfirmContrastKey)) {
|
||||
t.Error("the confirm does not state how it differs from the additive restore beside it")
|
||||
}
|
||||
// NEGATIVE CONTROL: the additive restore's own confirm must NOT have acquired this language.
|
||||
// Without it, a test that passed because both buttons warn about overwriting would look green.
|
||||
html := renderBackupPage(t, "backups_apps", baseBackupData([]AppBackupRow{
|
||||
r103Row(true, "2026-08-25T03:30:00Z", true),
|
||||
r103Row(t, true, "2026-08-25T03:30:00Z", true),
|
||||
}))
|
||||
fileConfirmStart := strings.Index(html, "Visszaállítja a hiányzó fájlokat")
|
||||
if fileConfirmStart < 0 {
|
||||
@@ -136,11 +136,11 @@ func TestR103_ConfirmNamesTheCopyDate(t *testing.T) {
|
||||
t.Fatal("the stamp did not render as a local date — the assertion below would be vacuous")
|
||||
}
|
||||
|
||||
proven := tier2UnitConfirmMsg(stamp, true)
|
||||
proven := noteServer(t).tier2UnitConfirmMsg(stamp, true)
|
||||
if !strings.Contains(proven, rendered) {
|
||||
t.Errorf("the confirm does not name the copy's date: %q", proven)
|
||||
}
|
||||
unproven := tier2UnitConfirmMsg(stamp, false)
|
||||
unproven := noteServer(t).tier2UnitConfirmMsg(stamp, false)
|
||||
if !strings.Contains(unproven, rendered) {
|
||||
t.Errorf("the unproven confirm does not name the date: %q", unproven)
|
||||
}
|
||||
@@ -148,13 +148,13 @@ func TestR103_ConfirmNamesTheCopyDate(t *testing.T) {
|
||||
t.Error("a proven copy and a bare attempt got the SAME sentence — R-101 exactly")
|
||||
}
|
||||
// A copy with no recorded date still gets the warning; it just cannot name one.
|
||||
none := tier2UnitConfirmMsg("", false)
|
||||
if !strings.Contains(none, tier2UnitConfirmBase) || !strings.Contains(none, tier2UnitConfirmContrast) {
|
||||
none := noteServer(t).tier2UnitConfirmMsg("", false)
|
||||
if !strings.Contains(none, noteHU(t, tier2UnitConfirmBaseKey)) || !strings.Contains(none, noteHU(t, tier2UnitConfirmContrastKey)) {
|
||||
t.Errorf("a dateless copy lost its confirm entirely: %q", none)
|
||||
}
|
||||
|
||||
// And it is on the page, not merely constructible.
|
||||
html := renderBackupPage(t, "backups_apps", baseBackupData([]AppBackupRow{r103Row(true, stamp, true)}))
|
||||
html := renderBackupPage(t, "backups_apps", baseBackupData([]AppBackupRow{r103Row(t, true, stamp, true)}))
|
||||
if !strings.Contains(html, rendered) {
|
||||
t.Errorf("the copy's date %q is not in the rendered row", rendered)
|
||||
}
|
||||
@@ -163,23 +163,25 @@ func TestR103_ConfirmNamesTheCopyDate(t *testing.T) {
|
||||
// TestR103_AvailableMsgNamesTheButtonByItsLabel — the refusal that now points AT the new action must
|
||||
// name it by the label the button actually carries, or it points at nothing.
|
||||
func TestR103_AvailableMsgNamesTheButtonByItsLabel(t *testing.T) {
|
||||
if !strings.Contains(tier2UnitAvailableMsg, tier2UnitActionLabel) {
|
||||
t.Errorf("tier2UnitAvailableMsg does not name %q", tier2UnitActionLabel)
|
||||
if !strings.Contains(noteHU(t, tier2UnitAvailableKey), noteHU(t, tier2UnitActionLabelKey)) {
|
||||
t.Errorf("noteHU(t, tier2UnitAvailableKey) does not name %q", noteHU(t, tier2UnitActionLabelKey))
|
||||
}
|
||||
// It must NOT send anyone to another page any more; that is the R-103 defect it replaces.
|
||||
if strings.Contains(tier2UnitAvailableMsg, "oldalon") {
|
||||
if strings.Contains(noteHU(t, tier2UnitAvailableKey), "oldalon") {
|
||||
t.Error("the message still routes the customer to another page for a copy restorable here")
|
||||
}
|
||||
// The surviving no-coverage message still names the route that works.
|
||||
if !strings.Contains(tier2NoCoverageMsg, "oldalon") {
|
||||
t.Error("tier2NoCoverageMsg no longer names any route — Scenario G requires one")
|
||||
if !strings.Contains(noteHU(t, tier2NoCoverageKey), "oldalon") {
|
||||
t.Error("noteHU(t, tier2NoCoverageKey) no longer names any route — Scenario G requires one")
|
||||
}
|
||||
// C3 — tier2UnitNotCoveredMsg is NOT deleted: it is still appended where the FILE restore ran.
|
||||
if tier2UnitNotCoveredMsg == "" {
|
||||
t.Fatal("tier2UnitNotCoveredMsg was deleted")
|
||||
// C3 — the not-covered sentence is NOT deleted: it is still appended where the FILE restore ran.
|
||||
// Since v0.254.0 it is a SAVED note written in the box's language, so the key is what the source
|
||||
// names and the bundle is what carries the sentence; both halves are checked.
|
||||
if noteHU(t, tier2UnitNotCoveredKey) == "" {
|
||||
t.Fatal("the not-covered sentence was deleted from the bundle")
|
||||
}
|
||||
if !strings.Contains(readSourceFile(t, "handlers.go"), `msg += " " + tier2UnitNotCoveredMsg`) {
|
||||
t.Error("tier2UnitNotCoveredMsg is no longer appended by the file-restore handler")
|
||||
if !strings.Contains(readSourceFile(t, "handlers.go"), `msg += " " + s.note(tier2UnitNotCoveredKey)`) {
|
||||
t.Error("the not-covered sentence is no longer appended by the file-restore handler")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,8 +347,8 @@ func TestR103_HandlerPublishesTheOutcome(t *testing.T) {
|
||||
}
|
||||
// The sentence is yesterday's honest outcome PLUS the clause naming which copy overwrote the live
|
||||
// data (Scenario E). Asserted as an exact composition so neither half can silently vanish.
|
||||
wantOutcome := unitRestoreOutcomeMsg("app", backup.UnitRestoreResult{})
|
||||
wantSource := tier2UnitSourceMsg(backup.Tier2Coverage{CopyLastSuccess: r103CopyStamp})
|
||||
wantOutcome := noteServer(t).unitRestoreOutcomeMsg("app", backup.UnitRestoreResult{})
|
||||
wantSource := noteServer(t).tier2UnitSourceMsg(backup.Tier2Coverage{CopyLastSuccess: r103CopyStamp})
|
||||
if wantSource == "" {
|
||||
t.Fatal("the fixture recorded no copy date — the assertion below would be vacuous")
|
||||
}
|
||||
@@ -369,8 +371,10 @@ func TestR103_UnitRestoreRefusesAnUnopenableMirrorWithoutStopping(t *testing.T)
|
||||
if !strings.Contains(loc, "flash_error") {
|
||||
t.Fatalf("the restore was accepted over an unopenable mirror: %q", loc)
|
||||
}
|
||||
if !strings.Contains(loc, url.QueryEscape(tier2NoCoverageMsg)) {
|
||||
t.Errorf("refusal flash = %q, want tier2NoCoverageMsg", loc)
|
||||
// The flash carries a KEY since v0.252.0; the sentence is what this test is about, so it is
|
||||
// resolved the way the page resolves it.
|
||||
if got := flashSentence(t, loc); got != noteHU(t, tier2NoCoverageKey) {
|
||||
t.Errorf("refusal flash = %q, want the no-coverage sentence", got)
|
||||
}
|
||||
if st := m.RestoreStatus(); st.Running || st.Last != nil {
|
||||
t.Errorf("an operation was begun despite the refusal: %+v", st)
|
||||
@@ -404,10 +408,10 @@ func TestR103_FileRestoreRefusalPointsAtTheActionThatWorks(t *testing.T) {
|
||||
s, _ := newR103Server(t, true)
|
||||
rec := postTier2Restore(t, s, "app")
|
||||
loc := rec.Header().Get("Location")
|
||||
if !strings.Contains(loc, url.QueryEscape(tier2UnitAvailableMsg)) {
|
||||
t.Errorf("refusal flash = %q, want tier2UnitAvailableMsg", loc)
|
||||
if !strings.Contains(loc, url.QueryEscape(noteHU(t, tier2UnitAvailableKey))) {
|
||||
t.Errorf("refusal flash = %q, want noteHU(t, tier2UnitAvailableKey)", loc)
|
||||
}
|
||||
if strings.Contains(loc, url.QueryEscape(tier2NoCoverageMsg)) {
|
||||
if strings.Contains(loc, url.QueryEscape(noteHU(t, tier2NoCoverageKey))) {
|
||||
t.Error("the old dead-end message is still shown for a copy restorable here")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user