package workflow import ( "context" "os" "path/filepath" "strings" "testing" "github.com/ajhahnde/eeco/internal/ai" "github.com/ajhahnde/eeco/internal/config" ) // stubProvider is a deterministic ai.Provider for workflow tests. type stubProvider struct { calls int text string } func (s *stubProvider) Name() string { return "stub" } func (s *stubProvider) Run(context.Context, ai.Request) (ai.Response, error) { s.calls++ return ai.Response{Text: s.text}, nil } func gateWith(t *testing.T, cfg *config.Config, p ai.Provider, consent bool) *ai.Gate { t.Helper() return &ai.Gate{ Provider: p, Consent: consent, Budget: 1, StateDir: filepath.Join(cfg.Workspace, "state"), Project: "proj", } } func readLedger(t *testing.T, cfg *config.Config) string { t.Helper() b, err := os.ReadFile(filepath.Join(cfg.Workspace, "state", bugLedgerName)) if err != nil { t.Fatalf("ledger: %v", err) } return string(b) } func TestBugSweep_FindsMarkersAndAppends(t *testing.T) { cfg := newCfg(t) writeRepoFile(t, cfg.RepoRoot, "src/a.go", "package a\n// TODO: wire this up\nfunc A() {}\n") writeRepoFile(t, cfg.RepoRoot, "src/b.txt", "nothing here\n") res, err := bugSweep{}.Run(Env{Config: cfg}) if err != nil { t.Fatal(err) } if res.Code != CodeFinding { t.Fatalf("code = %d, want %d (finding)", res.Code, CodeFinding) } if len(res.Findings) != 1 || !strings.Contains(res.Findings[0].Msg, "TODO") { t.Fatalf("findings = %+v", res.Findings) } if l := readLedger(t, cfg); !strings.Contains(l, "TODO: wire this up") { t.Errorf("ledger missing marker:\n%s", l) } } func TestBugSweep_LedgerIsAppendOnly(t *testing.T) { cfg := newCfg(t) writeRepoFile(t, cfg.RepoRoot, "a.go", "// FIXME: one\n") if _, err := (bugSweep{}).Run(Env{Config: cfg}); err != nil { t.Fatal(err) } first := readLedger(t, cfg) if _, err := (bugSweep{}).Run(Env{Config: cfg}); err != nil { t.Fatal(err) } second := readLedger(t, cfg) if !strings.HasPrefix(second, first) { t.Error("second run rewrote earlier ledger content; must be append-only") } if strings.Count(second, "— static") != 2 { t.Errorf("want 2 static sections, got:\n%s", second) } } func TestBugSweep_CleanNoGateIsClean(t *testing.T) { cfg := newCfg(t) writeRepoFile(t, cfg.RepoRoot, "ok.go", "package ok\nfunc Fine() {}\n") res, err := bugSweep{}.Run(Env{Config: cfg}) if err != nil { t.Fatal(err) } if res.Code != CodeClean { t.Fatalf("code = %d, want %d (clean)", res.Code, CodeClean) } } func TestBugSweep_NoMarkersNoConsentDefersAI(t *testing.T) { cfg := newCfg(t) writeRepoFile(t, cfg.RepoRoot, "ok.go", "package ok\n") sp := &stubProvider{text: "analysis"} g := gateWith(t, cfg, sp, false) res, err := bugSweep{}.Run(Env{Config: cfg, Gate: g}) if err != nil { t.Fatal(err) } if res.Code != CodeAIDeferred { t.Fatalf("code = %d, want %d (AI deferred)", res.Code, CodeAIDeferred) } if sp.calls != 0 { t.Errorf("provider spent without consent: calls=%d", sp.calls) } q, _ := os.ReadFile(filepath.Join(cfg.Workspace, "state", "queue.md")) if !strings.Contains(string(q), "ai-parked") { t.Errorf("parked AI pass not queued:\n%s", q) } } func TestBugSweep_ConsentRunsAIAndAppends(t *testing.T) { cfg := newCfg(t) writeRepoFile(t, cfg.RepoRoot, "ok.go", "package ok\n") sp := &stubProvider{text: "AI: looks fine, no high-risk items"} g := gateWith(t, cfg, sp, true) res, err := bugSweep{}.Run(Env{Config: cfg, Gate: g}) if err != nil { t.Fatal(err) } if res.Code != CodeClean { t.Fatalf("code = %d, want %d (clean)", res.Code, CodeClean) } if sp.calls != 1 { t.Errorf("provider calls = %d, want 1", sp.calls) } l := readLedger(t, cfg) if !strings.Contains(l, "— ai") || !strings.Contains(l, "looks fine") { t.Errorf("ledger missing AI section:\n%s", l) } q, _ := os.ReadFile(filepath.Join(cfg.Workspace, "state", "queue.md")) if !strings.Contains(string(q), "bug-sweep") { t.Errorf("AI findings not queued for review:\n%s", q) } } // End-to-end proof the pre-write filter closes the gap: a provider response // carrying an attribution fingerprint is blocked at the gate, bug-sweep takes // the same deferred branch a parked pass takes, and the attribution text never // reaches the bug ledger (the gitignored workspace file leak-guard never scans). func TestBugSweep_FilterBlocksAttributionResponse(t *testing.T) { cfg := newCfg(t) writeRepoFile(t, cfg.RepoRoot, "ok.go", "package ok\n") // no static markers -> AI pass sp := &stubProvider{text: fragCoAB + ": A Bot \nlooks fine\n"} g := gateWith(t, cfg, sp, true) det, _ := NewDetector(nil) g.Scanner = det.ScanResponse res, err := bugSweep{}.Run(Env{Config: cfg, Gate: g}) if err != nil { t.Fatal(err) } if sp.calls != 1 { t.Errorf("provider should have run once before the block; calls=%d", sp.calls) } if res.Code != CodeAIDeferred { t.Fatalf("a blocked AI pass must defer like a parked pass; code=%d want %d", res.Code, CodeAIDeferred) } if b, err := os.ReadFile(filepath.Join(cfg.Workspace, "state", bugLedgerName)); err == nil { if strings.Contains(string(b), fragCoAB) { t.Errorf("blocked attribution text leaked into the bug ledger:\n%s", b) } } }