package aftercrash import ( "context" "testing" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/crashboot" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" ) // R-1007 (decision 236): the repair runs ONLY after an unclean host boot that THIS start followed, only after the // crash grace, and only once. A normal boot and an unknown fact never run it. The verdict is crashboot's own (the // real Grace with a fake probe), so these tests assert the consequence: was the app's block run? var start = time.Date(2026, 10, 14, 12, 8, 0, 0, time.UTC) func probe(f crashboot.Fact, err error) crashboot.Probe { return func(context.Context) (crashboot.Fact, error) { return f, err } } func runner(p crashboot.Probe) (*Runner, *[]string) { var ran []string g := crashboot.New(start, p, nil) r := New(g, func() []string { return []string{"immich"} }, func(name string, _ time.Time) stacks.AfterUncleanBootResult { ran = append(ran, name) return stacks.AfterUncleanBootResult{} }, func(string, time.Time, stacks.AfterUncleanBootResult) {}, nil) return r, &ran } // RED-PROOF: drop the `now.Sub(r.Start) < wait` check → the run happens at +1 min → fails. // RED-PROOF: drop `r.done = true` before the run → a second tick runs it again → fails. func TestAfterCrash_RunsOnceAfterTheGraceOnAnUncleanBootThisStartFollowed(t *testing.T) { r, ran := runner(probe(crashboot.Fact{Known: true, Unclean: true, BootAt: start.Add(-40 * time.Second)}, nil)) for _, at := range []time.Duration{0, time.Minute, 14 * time.Minute} { r.Tick(start.Add(at)) } if len(*ran) != 0 { t.Fatalf("ran inside the crash grace: %v", *ran) } r.Tick(start.Add(crashboot.CrashGrace + time.Second)) r.Tick(start.Add(crashboot.CrashGrace + time.Minute)) if len(*ran) != 1 { t.Fatalf("ran %d time(s), want exactly once after the grace", len(*ran)) } } // RED-PROOF: in Tick, treat `!crash` like a crash (drop the early return) → a normal boot runs the repair → fails. func TestAfterCrash_NeverOnANormalAnUnknownOrAnOldUncleanBoot(t *testing.T) { cases := map[string]crashboot.Probe{ "normal boot": probe(crashboot.Fact{Known: true, Unclean: false, BootAt: start.Add(-time.Minute)}, nil), "unknown (no crash guard)": probe(crashboot.Fact{}, nil), "unknown (agent unreachable)": probe(crashboot.Fact{}, context.DeadlineExceeded), "no agent at all": nil, "an unclean boot days ago": probe(crashboot.Fact{Known: true, Unclean: true, BootAt: start.Add(-72 * time.Hour)}, nil), "an unclean boot with no time": probe(crashboot.Fact{Known: true, Unclean: true}, nil), } for name, p := range cases { r, ran := runner(p) for at := time.Duration(0); at <= 2*crashboot.CrashGrace; at += 30 * time.Second { r.Tick(start.Add(at)) } if len(*ran) != 0 { t.Errorf("%s: the repair ran", name) } if !r.Done() { t.Errorf("%s: the decision was never taken", name) } } }