package backup import ( "go/ast" "go/parser" "go/token" "os" "path/filepath" "strings" "testing" ) // R-383 — the double-failure message claimed the undo copy EXISTED without ever asking the disk. // // THE DEFECT. The branch where BOTH the replay and the rollback failed ended with // „a korábbi állapot mentése megvan: ". The filename came from the path `writeSafetyDump` // returned. One of the two ways `rollbackSafetyDump` fails is that the file is NOT THERE — so the // sentence was most likely to be false in precisely the case it was printed. Measured twice live, on // v0.220.2 and v0.221.1. // // THE LAYER. The guard sits on the phrase builder, because that is where the claim is MADE. A test // at the reconstitute level would need a whole failed off-site restore to reach one sentence, and the // AST test below is what pins that the sentence is still wired to this builder. // // RED-PROOF (observed, see REPORT.md): replace undoCopyPhrase's body with the pre-fix shape // // return "a korábbi állapot mentése megvan: " + filepath.Base(set.First()) // // and TestR383_AbsentUndoCopyIsNotClaimedToExist fails with the message asserting the file exists. func TestR383_AbsentUndoCopyIsNotClaimedToExist(t *testing.T) { dir := t.TempDir() real := filepath.Join(dir, "pre-restore-20260823T120000Z-app-postgres.sql") if err := os.WriteFile(real, []byte("-- a real dump\n"), 0o600); err != nil { t.Fatal(err) } gone := filepath.Join(dir, "pre-restore-20260823T120000Z-app-mariadb.sql") empty := filepath.Join(dir, "pre-restore-20260823T120000Z-app-empty.sql") if err := os.WriteFile(empty, nil, 0o600); err != nil { t.Fatal(err) } set := func(paths ...string) safetyDumpSet { s := safetyDumpSet{Stamp: "20260823T120000Z"} for _, p := range paths { s.Files = append(s.Files, safetyDumpFile{Path: p}) } return s } cases := []struct { name string set safetyDumpSet mustContain []string mustNotHave []string }{ { name: "present — the operator still gets the filename", set: set(real), mustContain: []string{"megvan", filepath.Base(real)}, }, { // THE DEFECT ITSELF: the file is gone and the sentence used to say it was there. name: "absent — must NOT claim it exists, must still name where it should be", set: set(gone), mustContain: []string{"NEM találjuk", filepath.Base(gone)}, mustNotHave: []string{"mentése megvan"}, }, { // A 0-byte dump restores nothing. Calling it present is the same false reassurance. name: "zero-length — counts as missing", set: set(empty), mustContain: []string{"NEM találjuk", filepath.Base(empty)}, mustNotHave: []string{"mentése megvan"}, }, { name: "partial — both halves named, neither hidden", set: set(real, gone), mustContain: []string{"RÉSZBEN", filepath.Base(real), "HIÁNYZIK", filepath.Base(gone)}, }, { name: "no undo was ever written — said plainly, not silently", set: set(), mustContain: []string{"nem készült"}, mustNotHave: []string{"megvan"}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := newNoteManager(t).undoCopyPhrase(tc.set) for _, want := range tc.mustContain { if !strings.Contains(got, want) { t.Errorf("phrase %q does not contain %q", got, want) } } for _, bad := range tc.mustNotHave { if strings.Contains(got, bad) { t.Errorf("phrase %q contains %q — it asserts a file that is not on disk", got, bad) } } }) } } // The seam, through production wiring (§10). A correct phrase builder nobody calls is R-106's defect // — "seam built but never wired", four instances in this project. This walks the AST rather than // grepping for a substring, so a commented-out call or a similarly-named local cannot satisfy it. func TestR383_TheDoubleFailureMessageIsWiredToTheBuilder(t *testing.T) { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "offbox_reconstitute.go", nil, 0) if err != nil { t.Fatalf("parse: %v", err) } var holdCalled, phraseCalled bool ast.Inspect(f, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true } switch fn := call.Fun.(type) { case *ast.SelectorExpr: // v0.254.0 (R-557 release C): undoCopyPhrase became a METHOD, because a saved note is // rendered in the box's language and that needs the Manager. A method call is a // SelectorExpr, not an Ident — so it is matched here as well as below, and the test keeps // asserting what it always asserted rather than passing because the shape moved. if fn.Sel.Name == "holdAppAfterFailedRollback" { holdCalled = true } if fn.Sel.Name == "undoCopyPhrase" { phraseCalled = true } case *ast.Ident: if fn.Name == "undoCopyPhrase" { phraseCalled = true } } return true }) if !holdCalled { t.Fatal("holdAppAfterFailedRollback is no longer called — the double-failure branch moved; " + "re-point this test before trusting it") } if !phraseCalled { t.Fatal("undoCopyPhrase is never called from offbox_reconstitute.go — the message is back to " + "asserting a file it did not check (R-383)") } // And the claim must not be re-typed OUTSIDE the builder. Inside undoCopyPhrase it is the // verified branch and belongs there; anywhere else it is unconditional again, which is R-383. // The builder's extent comes from the AST, so this cannot be defeated by moving the function. var lo, hi token.Pos for _, d := range f.Decls { if fd, ok := d.(*ast.FuncDecl); ok && fd.Name.Name == "undoCopyPhrase" { lo, hi = fd.Pos(), fd.End() } } if lo == token.NoPos { t.Fatal("undoCopyPhrase is not declared in offbox_reconstitute.go") } ast.Inspect(f, func(n ast.Node) bool { lit, ok := n.(*ast.BasicLit) if !ok || lit.Kind != token.STRING { return true } if lit.Pos() >= lo && lit.End() <= hi { return true // inside the builder — the verified branch } if strings.Contains(lit.Value, "állapot mentése megvan") { t.Errorf("%s: the unconditional claim is back outside undoCopyPhrase: %s", fset.Position(lit.Pos()), lit.Value) } return true }) }