3c6b49b31c
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
91 lines
3.4 KiB
Go
91 lines
3.4 KiB
Go
package main
|
|
|
|
import (
|
|
"go/ast"
|
|
"io"
|
|
"log"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
|
)
|
|
|
|
// `09` §3 decision 28 (v0.269.0) — the box stops a crash loop / OOM storm; never inside a backup's,
|
|
// a quiesce's or an update's own stop, never twice, never an app already held.
|
|
func TestD28_StopHoldTellInOrder_AndTheSkips(t *testing.T) {
|
|
var calls []string
|
|
d := unhealthyDeps{
|
|
verdicts: func(time.Time, []stacks.OOMContainer) []stacks.UnhealthyVerdict {
|
|
return []stacks.UnhealthyVerdict{
|
|
{Stack: "gokapi", Kind: stacks.UnhealthyCrashLoop, Count: 7, Window: 10 * time.Minute},
|
|
{Stack: "quiesced", Kind: stacks.UnhealthyCrashLoop, Count: 7},
|
|
{Stack: "busy", Kind: stacks.UnhealthyCrashLoop, Count: 7},
|
|
{Stack: "held", Kind: stacks.UnhealthyOOMStorm, Count: 20},
|
|
}
|
|
},
|
|
suppressed: func() map[string]bool { return map[string]bool{"quiesced": true} },
|
|
busy: func(n string) (bool, string) { return n == "busy", "a restore is running" },
|
|
holdKind: func(n string) string {
|
|
if n == "held" {
|
|
return "update_failed"
|
|
}
|
|
return ""
|
|
},
|
|
stop: func(n string) error { calls = append(calls, "stop:"+n); return nil },
|
|
hold: func(n, k string, _ time.Time) (int, error) { calls = append(calls, "hold:"+n+":"+k); return 1, nil },
|
|
notify: func(v stacks.UnhealthyVerdict, trip int) { calls = append(calls, "notify:"+v.Stack) },
|
|
}
|
|
stopped := stopUnhealthyApps(log.New(io.Discard, "", 0), d, nil, time.Now())
|
|
if strings.Join(calls, ",") != "stop:gokapi,hold:gokapi:crash_loop,notify:gokapi" {
|
|
t.Fatalf("calls = %v — want gokapi stopped, held, told; the quiesced, busy and held apps untouched", calls)
|
|
}
|
|
if len(stopped) != 1 || stopped[0] != "gokapi" {
|
|
t.Fatalf("stopped = %v", stopped)
|
|
}
|
|
}
|
|
|
|
// A failed stop is neither held nor announced (the household would read "we stopped it" over a running app).
|
|
func TestD28_AFailedStopIsNotAnnounced(t *testing.T) {
|
|
var calls []string
|
|
d := unhealthyDeps{
|
|
verdicts: func(time.Time, []stacks.OOMContainer) []stacks.UnhealthyVerdict {
|
|
return []stacks.UnhealthyVerdict{{Stack: "x", Kind: "crash_loop"}}
|
|
},
|
|
suppressed: func() map[string]bool { return nil },
|
|
busy: func(string) (bool, string) { return false, "" },
|
|
holdKind: func(string) string { return "" },
|
|
stop: func(string) error { return io.ErrUnexpectedEOF },
|
|
hold: func(string, string, time.Time) (int, error) { calls = append(calls, "hold"); return 1, nil },
|
|
notify: func(stacks.UnhealthyVerdict, int) { calls = append(calls, "notify") },
|
|
}
|
|
stopUnhealthyApps(log.New(io.Discard, "", 0), d, nil, time.Now())
|
|
if len(calls) != 0 {
|
|
t.Fatalf("calls = %v after a failed stop", calls)
|
|
}
|
|
}
|
|
|
|
// The wiring: main's dead-app loop calls stopUnhealthyApps with the detector.
|
|
func TestD28_TheLoopStopsUnhealthyApps(t *testing.T) {
|
|
found, withDetector := false, false
|
|
ast.Inspect(mainBody(t), func(n ast.Node) bool {
|
|
call, ok := n.(*ast.CallExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
if id, ok := call.Fun.(*ast.Ident); ok && id.Name == "stopUnhealthyApps" {
|
|
found = true
|
|
ast.Inspect(call, func(m ast.Node) bool {
|
|
if sel, ok := m.(*ast.SelectorExpr); ok && sel.Sel.Name == "ObserveUnhealthy" {
|
|
withDetector = true
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
return true
|
|
})
|
|
if !found || !withDetector {
|
|
t.Fatalf("main calls stopUnhealthyApps=%v with ObserveUnhealthy=%v — decision 28 is built and never run", found, withDetector)
|
|
}
|
|
}
|