ea0d3f1764
The catalog knew only 'present' or 'gone', and 'gone' orphans every customer already running the app. lifecycle: in .felhom.yml withdraws an app from new installs without touching anyone running it. Deploy gate is server-side and fail-closed, before any mutation, with the ruled Hungarian refusal - hiding a button is not a gate. Unknown values fail OPEN (available + one WARN), deliberately opposite, so a typo or a newer catalog cannot pull a working app out of every customer's list. Orphan detection never sees the field - a red-proof adds that filter and shows the abandoned app immediately reading as an orphan. Badge plumbing is generic (MetaBadge + meta_badge partial) so R-56's difficulty labels drop in with no new markup.
98 lines
3.0 KiB
Go
98 lines
3.0 KiB
Go
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)
|
|
}
|
|
}
|