package api import ( "go/ast" "go/parser" "go/token" "strings" "testing" ) // TestDeployStackWiresTheLifecycleGate — the seam-discipline test (§9 rule 6). // // The lifecycle predicate is unit-tested in internal/stacks, and a test there passes whether or not // deployStack ever calls it. Three inert-seam defects shipped fully-green in three days (controller // v0.154.0, agent v0.91.0, agent v0.92.0's missing sudoers grant), all this exact shape: correct // component, absent caller. So the CALLER is asserted here, from source. // // It walks the AST rather than doing strings.Contains on the file, because a commented-out call // still contains the string — the lesson recorded in PROMPT-TEMPLATE §10. // // It also asserts ORDER: the gate must precede the DeployStack call, or it is not fail-closed. func TestDeployStackWiresTheLifecycleGate(t *testing.T) { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "router.go", nil, 0) // comments dropped: a commented call is not a call if err != nil { t.Fatalf("parse router.go: %v", err) } var fn *ast.FuncDecl ast.Inspect(f, func(n ast.Node) bool { if d, ok := n.(*ast.FuncDecl); ok && d.Name.Name == "deployStack" { fn = d return false } return true }) if fn == nil { t.Fatal("deployStack not found in router.go — did it move? the gate's wiring is now unasserted") } canInstallPos, deployPos := -1, -1 ast.Inspect(fn, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true } sel, ok := call.Fun.(*ast.SelectorExpr) if !ok { return true } off := fset.Position(call.Pos()).Offset switch sel.Sel.Name { case "CanInstall": if canInstallPos == -1 { canInstallPos = off } case "DeployStack": if deployPos == -1 { deployPos = off } } return true }) if canInstallPos == -1 { t.Fatal("deployStack never calls Meta.CanInstall() — the lifecycle gate is INERT: " + "a withdrawn app is hidden from the catalog page but still installable by direct POST") } if deployPos == -1 { t.Fatal("deployStack no longer calls DeployStack — this test's ordering assertion is meaningless") } if canInstallPos > deployPos { t.Fatalf("the lifecycle gate (offset %d) runs AFTER DeployStack (offset %d) — a gate that "+ "fires after the mutation is not fail-closed", canInstallPos, deployPos) } } // TestLifecycleRefusalMessageIsCustomerFacingHungarian: the refusal text reaches the customer via // showAlert(), so it must be the sentence the spec ruled, not a Go error string. func TestLifecycleRefusalMessageIsCustomerFacingHungarian(t *testing.T) { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "router.go", nil, 0) if err != nil { t.Fatal(err) } const want = "Ez az alkalmazás jelenleg nem telepíthető." found := false ast.Inspect(f, func(n ast.Node) bool { if lit, ok := n.(*ast.BasicLit); ok && lit.Kind == token.STRING && strings.Contains(lit.Value, want) { found = true } return true }) if !found { t.Fatalf("the ruled refusal message %q is not present in router.go", want) } }