package app import ( "testing" cfgpkg "github.com/jan/fm-rds-tx/internal/config" "github.com/jan/fm-rds-tx/internal/output" "github.com/jan/fm-rds-tx/internal/platform" ) func TestEngineRuntimeStateReporting(t *testing.T) { e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil)) if got := e.Stats().State; got != string(RuntimeStateIdle) { t.Fatalf("expected initial state idle, got %s", got) } e.setRuntimeState(RuntimeStatePrebuffering) if got := e.Stats().State; got != string(RuntimeStatePrebuffering) { t.Fatalf("expected prebuffering, got %s", got) } e.setRuntimeState(RuntimeStateRunning) if got := e.currentRuntimeState(); got != RuntimeStateRunning { t.Fatalf("currentRuntimeState mismatch: %s", got) } } func TestEngineRuntimeStateTransitions(t *testing.T) { e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil)) e.setRuntimeState(RuntimeStatePrebuffering) queue := output.QueueStats{Depth: 1, FillLevel: 0.75, Health: output.QueueHealthNormal} e.evaluateRuntimeState(queue, false) if got := e.currentRuntimeState(); got != RuntimeStateRunning { t.Fatalf("expected running after full buffer, got %s", got) } queue.Health = output.QueueHealthCritical for i := 0; i < queueCriticalStreakThreshold; i++ { e.evaluateRuntimeState(queue, false) } if got := e.currentRuntimeState(); got != RuntimeStateDegraded { t.Fatalf("expected degraded on queue critical streak, got %s", got) } queue.Health = output.QueueHealthNormal e.evaluateRuntimeState(queue, false) if got := e.currentRuntimeState(); got != RuntimeStateRunning { t.Fatalf("expected running once queue healthy, got %s", got) } e.evaluateRuntimeState(queue, true) if got := e.currentRuntimeState(); got != RuntimeStateDegraded { t.Fatalf("expected degraded when late buffers seen, got %s", got) } } func TestEngineRuntimeStateMuteOnPersistentQueueCritical(t *testing.T) { e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil)) e.setRuntimeState(RuntimeStateRunning) queue := output.QueueStats{Depth: 1, Health: output.QueueHealthCritical} for i := 0; i < queueMutedStreakThreshold; i++ { e.evaluateRuntimeState(queue, false) } if got := e.currentRuntimeState(); got != RuntimeStateMuted { t.Fatalf("expected muted after prolonged queue critical, got %s", got) } muteFault := e.LastFault() if muteFault == nil { t.Fatal("expected fault recorded for the mute transition") } if muteFault.Reason != FaultReasonQueueCritical { t.Fatalf("expected queue critical reason, got %s", muteFault.Reason) } if muteFault.Severity != FaultSeverityMuted { t.Fatalf("expected muted severity, got %s", muteFault.Severity) } queue.Health = output.QueueHealthNormal for i := 0; i < queueMutedRecoveryThreshold-1; i++ { e.evaluateRuntimeState(queue, false) if got := e.currentRuntimeState(); got != RuntimeStateMuted { t.Fatalf("expected still muted while recovery window builds, got %s", got) } } e.evaluateRuntimeState(queue, false) if got := e.currentRuntimeState(); got != RuntimeStateDegraded { t.Fatalf("expected degrade once mute recovery threshold reached, got %s", got) } recoveryFault := e.LastFault() if recoveryFault == nil { t.Fatal("expected recovery fault entry after leaving mute") } if recoveryFault.Severity != FaultSeverityDegraded { t.Fatalf("expected degraded severity for recovery event, got %s", recoveryFault.Severity) } if recoveryFault.Reason != FaultReasonQueueCritical { t.Fatalf("expected queue critical reason for recovery event, got %s", recoveryFault.Reason) } e.evaluateRuntimeState(queue, false) if got := e.currentRuntimeState(); got != RuntimeStateRunning { t.Fatalf("expected running after recovery, got %s", got) } } func TestEngineFaultsAfterMutedCriticalStreak(t *testing.T) { e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil)) e.setRuntimeState(RuntimeStateRunning) queue := output.QueueStats{Depth: 1, Health: output.QueueHealthCritical} for i := 0; i < queueMutedStreakThreshold; i++ { e.evaluateRuntimeState(queue, false) } if got := e.currentRuntimeState(); got != RuntimeStateMuted { t.Fatalf("expected muted after draining critical streak, got %s", got) } triggered := false for i := 0; i < queueFaultedStreakThreshold; i++ { e.evaluateRuntimeState(queue, false) if e.currentRuntimeState() == RuntimeStateFaulted { triggered = true break } } if !triggered { t.Fatalf("expected faulted after %d extra critical checks", queueFaultedStreakThreshold) } if got := e.currentRuntimeState(); got != RuntimeStateFaulted { t.Fatalf("expected faulted state, got %s", got) } fault := e.LastFault() if fault == nil { t.Fatal("expected recorded fault") } if fault.Severity != FaultSeverityFaulted { t.Fatalf("expected faulted severity, got %s", fault.Severity) } if fault.Reason != FaultReasonQueueCritical { t.Fatalf("expected queue critical reason, got %s", fault.Reason) } } func TestRuntimeTransitionCounters(t *testing.T) { e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil)) if got := e.Stats().DegradedTransitions; got != 0 { t.Fatalf("expected zero transitions initially, got %d", got) } if got := e.Stats().FaultCount; got != 0 { t.Fatalf("expected zero faults initially, got %d", got) } e.setRuntimeState(RuntimeStateDegraded) if got := e.Stats().DegradedTransitions; got != 1 { t.Fatalf("expected one degraded transition, got %d", got) } e.setRuntimeState(RuntimeStateMuted) if got := e.Stats().MutedTransitions; got != 1 { t.Fatalf("expected one mute transition, got %d", got) } e.setRuntimeState(RuntimeStateFaulted) if got := e.Stats().FaultedTransitions; got != 1 { t.Fatalf("expected one faulted transition, got %d", got) } e.recordFault(FaultReasonQueueCritical, FaultSeverityWarn, "audit") if got := e.Stats().FaultCount; got != 1 { t.Fatalf("expected one recorded fault, got %d", got) } } func TestEngineTransitionHistory(t *testing.T) { e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil)) e.setRuntimeState(RuntimeStateRunning) e.setRuntimeState(RuntimeStateDegraded) e.setRuntimeState(RuntimeStateMuted) history := e.Stats().TransitionHistory if len(history) != 3 { t.Fatalf("expected 3 transitions recorded, got %d", len(history)) } if history[0].From != RuntimeStateIdle || history[0].To != RuntimeStateRunning { t.Fatalf("unexpected first transition: %+v", history[0]) } if history[0].Severity != "ok" { t.Fatalf("expected ok severity for running transition, got %s", history[0].Severity) } if history[1].To != RuntimeStateDegraded || history[1].Severity != "warn" { t.Fatalf("expected degraded transition with warn severity, got %+v", history[1]) } if history[2].To != RuntimeStateMuted || history[2].Severity != "warn" { t.Fatalf("expected muted transition with warn severity, got %+v", history[2]) } } func TestEngineResetFaultRequiresFaultedState(t *testing.T) { e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil)) if err := e.ResetFault(); err == nil { t.Fatal("expected error when resetting non-faulted state") } } func TestEngineResetFaultTransitionsToDegraded(t *testing.T) { e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil)) e.criticalStreak.Store(7) e.mutedRecoveryStreak.Store(3) e.mutedFaultStreak.Store(1) e.setRuntimeState(RuntimeStateFaulted) if err := e.ResetFault(); err != nil { t.Fatalf("reset fault failed: %v", err) } if got := e.currentRuntimeState(); got != RuntimeStateDegraded { t.Fatalf("expected degraded after reset, got %s", got) } if e.criticalStreak.Load() != 0 { t.Fatalf("expected critical streak reset, got %d", e.criticalStreak.Load()) } if e.mutedRecoveryStreak.Load() != 0 { t.Fatalf("expected mute recovery streak reset, got %d", e.mutedRecoveryStreak.Load()) } if e.mutedFaultStreak.Load() != 0 { t.Fatalf("expected mute fault streak reset, got %d", e.mutedFaultStreak.Load()) } if err := e.ResetFault(); err == nil { t.Fatal("expected error when resetting after recovery") } }