Go 173 lines
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 <b@x>\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)
}
}
}