package logscanner import ( "errors" "strings" "testing" "github.com/alexei/tinyforge/internal/store" ) // fakeRuleSource lets us inject rules into ReloadRules without // standing up SQLite. Mirrors the fakeTriggerSource pattern from // the events package. type fakeRuleSource struct { rows []store.LogScanRule err error } func (f *fakeRuleSource) ListLogScanRules() ([]store.LogScanRule, error) { if f.err != nil { return nil, f.err } return f.rows, nil } func TestManagerStats_CapturesCompileErrors(t *testing.T) { rs := &fakeRuleSource{rows: []store.LogScanRule{ {ID: 1, Name: "valid", Pattern: `panic`, Severity: "warn", Streams: "all", Enabled: true}, {ID: 2, Name: "broken", Pattern: `([unclosed`, Severity: "warn", Streams: "all", Enabled: true}, {ID: 3, Name: "also-broken", Pattern: `[`, Severity: "warn", Streams: "all", Enabled: true}, }} m := NewManager(Config{Rules: rs}) if err := m.ReloadRules(); err != nil { t.Fatalf("ReloadRules: %v", err) } stats := m.Stats() if len(stats.LastCompileErrors) != 2 { t.Fatalf("expected 2 compile errors, got %d: %+v", len(stats.LastCompileErrors), stats.LastCompileErrors) } // The error messages should mention the rule id/name from the // BuildSnapshot format so operators can find which rule broke. joined := strings.Join(stats.LastCompileErrors, "|") if !strings.Contains(joined, "broken") { t.Errorf("error messages should reference rule name 'broken': %s", joined) } } func TestManagerStats_CompileErrorsReplacedOnReload(t *testing.T) { // A broken rule then a reload with all-valid rules should // clear the error list — operators expect the panel to flip // from "2 errors" to "all clean" after they fix things. rs := &fakeRuleSource{rows: []store.LogScanRule{ {ID: 1, Name: "broken", Pattern: `([`, Severity: "warn", Streams: "all", Enabled: true}, }} m := NewManager(Config{Rules: rs}) _ = m.ReloadRules() if len(m.Stats().LastCompileErrors) != 1 { t.Fatal("expected one compile error before fix") } rs.rows = []store.LogScanRule{ {ID: 1, Name: "fixed", Pattern: `panic`, Severity: "warn", Streams: "all", Enabled: true}, } _ = m.ReloadRules() if len(m.Stats().LastCompileErrors) != 0 { t.Errorf("expected zero compile errors after reload, got %d", len(m.Stats().LastCompileErrors)) } } func TestManagerStats_ReloadErrorPropagates(t *testing.T) { rs := &fakeRuleSource{err: errors.New("db down")} m := NewManager(Config{Rules: rs}) if err := m.ReloadRules(); err == nil { t.Fatal("expected ReloadRules to propagate the source error") } } func TestManagerStats_ActiveTailsDefaultsZero(t *testing.T) { // Without Start() and without a docker dependency we can't // run real tails, but the counter should be a stable 0 read // rather than panic/uninitialized. rs := &fakeRuleSource{} m := NewManager(Config{Rules: rs}) if got := m.Stats().ActiveTails; got != 0 { t.Errorf("ActiveTails on fresh manager = %d, want 0", got) } }