206b0357d1
gates / gates (push) Successful in 27s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
125 lines
3.9 KiB
Go
125 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
|
)
|
|
|
|
// R-475 — the update reads EVERY backup tier, through the one adapter.
|
|
|
|
func TestR475_TierConstantsAgree(t *testing.T) {
|
|
if stacks.UpdateTierLocal != backup.UpdateTierLocal ||
|
|
stacks.UpdateTierSecondDrive != backup.UpdateTierSecondDrive ||
|
|
stacks.UpdateTierOffsite != backup.UpdateTierOffsite {
|
|
t.Fatal("stacks and backup number the tiers differently — a hold would name the wrong copy")
|
|
}
|
|
}
|
|
|
|
// adapterMethodSelectors returns the selector names used in updateGuardsAdapter.<name>'s body.
|
|
func adapterMethodSelectors(t *testing.T, name string) string {
|
|
t.Helper()
|
|
fset := token.NewFileSet()
|
|
f, err := parser.ParseFile(fset, "main.go", nil, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, d := range f.Decls {
|
|
fn, ok := d.(*ast.FuncDecl)
|
|
if !ok || fn.Recv == nil || fn.Name.Name != name || fn.Body == nil {
|
|
continue
|
|
}
|
|
star, ok := fn.Recv.List[0].Type.(*ast.StarExpr)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if id, ok := star.X.(*ast.Ident); !ok || id.Name != "updateGuardsAdapter" {
|
|
continue
|
|
}
|
|
var names []string
|
|
ast.Inspect(fn.Body, func(n ast.Node) bool {
|
|
if sel, ok := n.(*ast.SelectorExpr); ok {
|
|
names = append(names, sel.Sel.Name)
|
|
}
|
|
return true
|
|
})
|
|
return " " + strings.Join(names, " ") + " "
|
|
}
|
|
t.Fatalf("updateGuardsAdapter.%s not found in main.go", name)
|
|
return ""
|
|
}
|
|
|
|
func TestR475_AdapterReadsEveryTier(t *testing.T) {
|
|
rp := adapterMethodSelectors(t, "RestorePoints")
|
|
if !strings.Contains(rp, " UpdateRestorePoints ") {
|
|
t.Errorf("the adapter must read every tier through backup.UpdateRestorePoints; selectors:%s", rp)
|
|
}
|
|
if strings.Contains(rp, " Tier2UnitRestorePoint ") {
|
|
t.Error("the Tier-2-only predicate must not come back into the update path (R-475)")
|
|
}
|
|
if cb := adapterMethodSelectors(t, "CanBackUp"); !strings.Contains(cb, " CanBackUpApp ") {
|
|
t.Errorf("CanBackUp must ask backup.CanBackUpApp; selectors:%s", cb)
|
|
}
|
|
// R-659 (v0.268.0) CHANGED THIS ON PURPOSE: the hold no longer names the precondition copy the
|
|
// adapter is handed (round 11: that copy was one the restore refuses). The tier, and R-479's
|
|
// "what the copy holds", are now chosen inside backup.HoldAfterFailedUpdateWhole from every tier —
|
|
// pinned there by TestR659_TheHoldNamesOnlyAWholeCopy (which asserts the holds-phrase too).
|
|
if h := adapterMethodSelectors(t, "HoldAfterFailedUpdate"); !strings.Contains(h, " HoldAfterFailedUpdateWhole ") {
|
|
t.Errorf("the hold must be recorded through HoldAfterFailedUpdateWhole (R-659); selectors:%s", h)
|
|
}
|
|
}
|
|
|
|
// R-490 — /api/system/info reaches the API router; the web layer's /api/system/ prefix used to eat it.
|
|
//
|
|
// COMPANION RED-PROOF (REPORT.md): delete the exact-path mount — this fails.
|
|
func TestR490_SystemInfoIsMountedAheadOfTheWebPrefix(t *testing.T) {
|
|
src, err := parser.ParseFile(token.NewFileSet(), "main.go", nil, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var exact, prefix bool
|
|
ast.Inspect(src, func(n ast.Node) bool {
|
|
call, ok := n.(*ast.CallExpr)
|
|
if !ok || len(call.Args) < 2 {
|
|
return true
|
|
}
|
|
sel, ok := call.Fun.(*ast.SelectorExpr)
|
|
if !ok || sel.Sel.Name != "Handle" {
|
|
return true
|
|
}
|
|
lit, ok := call.Args[0].(*ast.BasicLit)
|
|
if !ok {
|
|
return true
|
|
}
|
|
switch lit.Value {
|
|
case `"/api/system/info"`:
|
|
exact = strings.Contains(fmtNode(call.Args[1]), "apiRouter")
|
|
case `"/api/system/"`:
|
|
prefix = true
|
|
}
|
|
return true
|
|
})
|
|
if !prefix {
|
|
t.Fatal("the /api/system/ prefix mount is gone — this test's premise moved")
|
|
}
|
|
if !exact {
|
|
t.Error("/api/system/info must be mounted on the API router ahead of the web layer's /api/system/ prefix (R-490)")
|
|
}
|
|
}
|
|
|
|
func fmtNode(n ast.Node) string {
|
|
var names []string
|
|
ast.Inspect(n, func(x ast.Node) bool {
|
|
if id, ok := x.(*ast.Ident); ok {
|
|
names = append(names, id.Name)
|
|
}
|
|
return true
|
|
})
|
|
return strings.Join(names, " ")
|
|
}
|