v0.164.0: deliberately stopped apps no longer alarm (banner + email)
A UI stop (Leallitas -> compose down -> StateStopped) is the user's own
action, not a fault, and must not raise the deadapp banner OR the
app_start_failed event. Filter at the single fix-3 derivation point:
extract scanDeployedAppRunStates's pure core to classifyRunStates and
change the down predicate to IsDownState(st.State) && st.State !=
StateStopped. Suppresses StateStopped from both the banner dead-list and
the notifier Down-set at once.
Rests on two invariants (recorded at the seam, README, CONTEXT):
I1 StopStack = compose down => zero containers => StateStopped
I2 P2 census: all catalog services unless-stopped => faults never rest
at stopped (they surface as exited/degraded).
IsDownState unchanged; out-of-band 'compose stop' (containers remain ->
exited) still alerts. Tests +4 (notify 3->4, main 4->7), both red-proofs
verified. No template/funcmap/notifier/counter/copy change.
This commit is contained in:
@@ -56,6 +56,42 @@ func TestNotifyAppStartFailures_FirstSeenDownFires(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Group C (Scenario C, v0.164.0) — stop → start → crash stays correct. The stop is reported as
|
||||
// Down=false (classifyRunStates suppresses StateStopped — proven in package main's
|
||||
// TestClassifyRunStates_StoppedIsSuppressed), so it fires NOTHING and leaves the tracker clean; the
|
||||
// later crash (StateExited → Down=true) is then a clean false→true transition → exactly ONE event.
|
||||
//
|
||||
// COMPANION red-proof: flip the stop cycle to Down=true (as bare IsDownState would report before the
|
||||
// suppression) → the stop becomes a running→down transition and fires here, so the "zero after the
|
||||
// stop" assertion below fails. (Verified by hand-editing the fixture.)
|
||||
func TestNotifyAppStartFailures_StopStartCrashSequence(t *testing.T) {
|
||||
n := New("http://hub", "key", "cust", nil, log.New(io.Discard, "", 0), false)
|
||||
var count int
|
||||
n.pushFn = func(eventType, _, _ string, _ interface{}) {
|
||||
if eventType == "app_start_failed" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
app := "immich"
|
||||
stopped := []AppRunState{{Name: app, DisplayName: "Immich", Down: false}} // user stop: StateStopped ⇒ Down=false
|
||||
running := []AppRunState{{Name: app, DisplayName: "Immich", Down: false}} // started again
|
||||
exited := []AppRunState{{Name: app, DisplayName: "Immich", Down: true}} // then it crashes: StateExited
|
||||
|
||||
// The deliberate stop must be SILENT — the whole point of the suppression.
|
||||
n.NotifyAppStartFailures(stopped)
|
||||
if count != 0 {
|
||||
t.Fatalf("a deliberate stop must fire no event, got %d", count)
|
||||
}
|
||||
|
||||
// Start, then crash → exactly ONE app_start_failed for the final false→true transition.
|
||||
n.NotifyAppStartFailures(running)
|
||||
n.NotifyAppStartFailures(exited)
|
||||
if count != 1 {
|
||||
t.Fatalf("stop→start→crash must fire exactly one event for the crash, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
// A deployed app that is up never fires.
|
||||
func TestNotifyAppStartFailures_HealthyNeverFires(t *testing.T) {
|
||||
n := New("http://hub", "key", "cust", nil, log.New(io.Discard, "", 0), false)
|
||||
|
||||
Reference in New Issue
Block a user