Files
felhom-controller/controller/cmd/controller/r856_wiring_test.go
T
admin 6ed223886d
gates / gates (push) Successful in 46s
R-1112: open the family store when the controller starts
Found on demo-hp with 0.325.0-rc.1: the store (and so the conversion of an old family.json) opened only on the
first family request; a box nobody visits through a family gate kept the session values. OpenFamilyStore from
main, one INFO line with the count. TestR1112_StartConvertsTheFamilyFile, TestR1112_MainOpensTheFamilyStore,
red-proved.

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-17 09:56:12 +02:00

86 lines
2.7 KiB
Go

package main
import (
"go/ast"
"go/parser"
"go/token"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/crashboot"
)
// R-856 (`09` §3 decision 143): the dead-app check — the source of app_start_failed and
// app_stopped_unhealthy — waits on the crash-aware boot grace, not on the fixed 90 s constant.
//
// Red-proof: put `time.Since(startTime) < deadAppBootGrace` back as the check's gate — this test fails.
func TestR856_DeadAppCheckWaitsOnTheCrashAwareGrace(t *testing.T) {
f, err := parser.ParseFile(token.NewFileSet(), "main.go", nil, 0)
if err != nil {
t.Fatal(err)
}
var body *ast.BlockStmt
ast.Inspect(f, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok || len(call.Args) < 3 {
return true
}
if sel, ok := call.Fun.(*ast.SelectorExpr); !ok || sel.Sel.Name != "Every" {
return true
}
if lit, ok := call.Args[0].(*ast.BasicLit); ok && lit.Value == `"deadapp-check"` {
if fl, ok := call.Args[2].(*ast.FuncLit); ok {
body = fl.Body
}
}
return true
})
if body == nil {
t.Fatal("the deadapp-check schedule is gone from main.go")
}
within, constant := false, false
ast.Inspect(body, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.SelectorExpr:
if id, ok := x.X.(*ast.Ident); ok && id.Name == "bootGrace" && x.Sel.Name == "Within" {
within = true
}
case *ast.Ident:
if x.Name == "deadAppBootGrace" {
constant = true
}
}
return true
})
if !within || constant {
t.Errorf("the dead-app check must gate on bootGrace.Within (found=%v) and not on deadAppBootGrace (found=%v)", within, constant)
}
}
// The normal grace is today's 90 s: a normal boot must keep it exactly (decision 143's other half).
func TestR856_NormalGraceIsTheDeadAppBootGrace(t *testing.T) {
if crashboot.NormalGrace != deadAppBootGrace {
t.Errorf("crashboot.NormalGrace = %s, deadAppBootGrace = %s — a normal boot must keep today's grace", crashboot.NormalGrace, deadAppBootGrace)
}
}
// R-1112: main opens the family store at start (webServer.OpenFamilyStore), so an old family.json is converted on every
// box — not only on one where someone passes a family gate. Red-proof: remove the call from main.go → this fails.
func TestR1112_MainOpensTheFamilyStore(t *testing.T) {
f, err := parser.ParseFile(token.NewFileSet(), "main.go", nil, 0)
if err != nil {
t.Fatal(err)
}
found := false
ast.Inspect(f, func(n ast.Node) bool {
if call, ok := n.(*ast.CallExpr); ok {
if sel, ok := call.Fun.(*ast.SelectorExpr); ok && sel.Sel.Name == "OpenFamilyStore" {
found = true
}
}
return true
})
if !found {
t.Fatal("main.go does not call OpenFamilyStore — an old family.json keeps its session values until a family request")
}
}