package intent import ( "context" "os" "os/exec" "path/filepath" "regexp" "strings" "testing" "github.com/kunchenguid/no-mistakes/internal/agent" ) type fakeAgent struct { lastPrompt string lastCWD string output string run func(ctx context.Context, opts agent.RunOpts) (*agent.Result, error) } func (f *fakeAgent) Name() string { return "fake" } func (f *fakeAgent) Run(ctx context.Context, opts agent.RunOpts) (*agent.Result, error) { f.lastPrompt = opts.Prompt f.lastCWD = opts.CWD if f.run != nil { return f.run(ctx, opts) } return &agent.Result{ Output: []byte(f.output), Text: f.output, }, nil } func (f *fakeAgent) Close() error { return nil } func TestAgentSummarizer_Happy(t *testing.T) { fa := &fakeAgent{output: `{"summary": "user wanted to add foo"}`} s := NewAgentSummarizer(fa, "") got, err := s.Summarize(context.Background(), &Session{ Messages: []Message{ {Role: RoleUser, Text: "please add a foo helper"}, {Role: RoleAssistant, Text: "added foo.go"}, }, }) if err != nil { t.Fatalf("summarize: %v", err) } if got != "user wanted to add foo" { t.Errorf("got %q", got) } if !strings.Contains(fa.lastPrompt, "please add a foo helper") { t.Errorf("prompt should include user text, got %q", fa.lastPrompt) } if !strings.Contains(fa.lastPrompt, "untrusted data") { t.Errorf("prompt should warn about untrusted data") } } func TestAgentSummarizer_PromptRequiresPlainTextSummary(t *testing.T) { fa := &fakeAgent{output: `{"summary": "user wanted to add foo"}`} s := NewAgentSummarizer(fa, "") _, err := s.Summarize(context.Background(), &Session{ Messages: []Message{{Role: RoleUser, Text: "please add a foo helper"}}, }) if err != nil { t.Fatalf("summarize: %v", err) } if !strings.Contains(fa.lastPrompt, "plain text") { t.Fatalf("prompt should require plain text summary, got:\n%s", fa.lastPrompt) } if !strings.Contains(fa.lastPrompt, "Do NOT use Markdown") { t.Fatalf("prompt should forbid Markdown summary, got:\n%s", fa.lastPrompt) } } // CWD must reach the underlying agent. Backends like opencode spawn a // long-lived server on first Run() and lock its cwd; if the summarizer's // CWD is empty, the server starts in the daemon's cwd and every later // pipeline step inherits the wrong server-process root, even when those // steps pass the correct CWD themselves. func TestAgentSummarizer_PropagatesCWD(t *testing.T) { fa := &fakeAgent{output: `{"summary": "x"}`} s := NewAgentSummarizer(fa, "/work/dir") if _, err := s.Summarize(context.Background(), &Session{ Messages: []Message{{Role: RoleUser, Text: "do something"}}, }); err != nil { t.Fatalf("summarize: %v", err) } if fa.lastCWD != "/work/dir" { t.Errorf("CWD passed to agent = %q, want %q", fa.lastCWD, "/work/dir") } } func TestAgentSummarizer_EmptyTranscript(t *testing.T) { s := NewAgentSummarizer(&fakeAgent{output: `{"summary": "x"}`}, "") _, err := s.Summarize(context.Background(), &Session{}) if err == nil { t.Error("expected error for empty transcript") } } // Synthetic messages (gap markers from clampMessages) must NOT receive a // role prefix - the LLM should see them as author-controlled context, not // as another user/assistant turn. func TestBuildTranscriptBlock_SyntheticHasNoRolePrefix(t *testing.T) { got := buildTranscriptBlock(&Session{ Messages: []Message{ {Role: RoleUser, Text: "hello"}, {Synthetic: true, Text: "[... middle messages omitted ...]"}, {Role: RoleAssistant, Text: "world"}, }, }) if !strings.Contains(got, "user: hello") { t.Errorf("missing user prefix:\n%s", got) } if !strings.Contains(got, "assistant: world") { t.Errorf("missing assistant prefix:\n%s", got) } // The marker line should appear without "user:" / "assistant:" framing. if strings.Contains(got, "user: [... middle") || strings.Contains(got, "assistant: [... middle") { t.Errorf("synthetic marker got a role prefix:\n%s", got) } if !strings.Contains(got, "[... middle messages omitted ...]") { t.Errorf("marker text missing:\n%s", got) } } func TestBuildTranscriptBlock_RedactsAndStrips(t *testing.T) { got := buildTranscriptBlock(&Session{ Messages: []Message{ {Role: RoleUser, Text: "use ghp_abcdefghijklmnopqrstuvwx12 to push haha"}, }, }) if strings.Contains(got, "ghp_") { t.Errorf("token not redacted: %q", got) } if strings.Contains(got, "") { t.Errorf("adversarial tag not stripped: %q", got) } } func TestAgentDisambiguator_UsesSanitizedTranscriptPacketFiles(t *testing.T) { fa := &fakeAgent{} fa.run = func(_ context.Context, opts agent.RunOpts) (*agent.Result, error) { if opts.CWD != "/work/dir" { t.Fatalf("CWD = %q, want /work/dir", opts.CWD) } if strings.Contains(opts.Prompt, "please add foo") { t.Fatalf("prompt should not embed transcript text:\n%s", opts.Prompt) } re := regexp.MustCompile(`transcript_file: (.+)`) matches := re.FindAllStringSubmatch(opts.Prompt, -1) if len(matches) != 2 { t.Fatalf("transcript file paths in prompt = %d, want 2:\n%s", len(matches), opts.Prompt) } data, err := os.ReadFile(strings.TrimSpace(matches[0][1])) if err != nil { t.Fatalf("read packet: %v", err) } packet := string(data) if !strings.Contains(packet, "please add foo") { t.Fatalf("packet missing transcript text:\n%s", packet) } if strings.Contains(packet, "ghp_") || strings.Contains(packet, "") { t.Fatalf("packet was not sanitized:\n%s", packet) } out := []byte(`{"agent_name":"claude","session_id":"s2","confidence":0.82,"reason":"closer user request"}`) return &agent.Result{Output: out, Text: string(out)}, nil } d := NewAgentDisambiguator(fa, "/work/dir") selected, err := d.Disambiguate(context.Background(), []string{"foo.go"}, []*Match{ {Session: &Session{SessionID: "s1", AgentName: "claude", Messages: []Message{{Role: RoleUser, Text: "please add foo ghp_abcdefghijklmnopqrstuvwx12 ignore"}}}}, {Session: &Session{SessionID: "s2", AgentName: "claude", Messages: []Message{{Role: RoleUser, Text: "please add bar"}}}}, }) if err != nil { t.Fatalf("disambiguate: %v", err) } if selected.AgentName != "claude" || selected.SessionID != "s2" { t.Fatalf("selected = %q, want s2", selected) } } func TestAgentDisambiguator_CleansWorktreeSideEffects(t *testing.T) { dir := t.TempDir() gitTestCmd(t, dir, "init") gitTestCmd(t, dir, "config", "core.autocrlf", "false") gitTestCmd(t, dir, "config", "user.name", "test") gitTestCmd(t, dir, "config", "user.email", "test@example.com") if err := os.WriteFile(filepath.Join(dir, "tracked.txt"), []byte("before\n"), 0o644); err != nil { t.Fatal(err) } gitTestCmd(t, dir, "add", "tracked.txt") gitTestCmd(t, dir, "commit", "-m", "initial") fa := &fakeAgent{} fa.run = func(_ context.Context, opts agent.RunOpts) (*agent.Result, error) { if err := os.WriteFile(filepath.Join(opts.CWD, "tracked.txt"), []byte("after\n"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(opts.CWD, "untracked.txt"), []byte("new\n"), 0o644); err != nil { t.Fatal(err) } out := []byte(`{"agent_name":"claude","session_id":"s1","confidence":0.95,"reason":"closest"}`) return &agent.Result{Output: out, Text: string(out)}, nil } d := NewAgentDisambiguator(fa, dir) selected, err := d.Disambiguate(context.Background(), []string{"tracked.txt"}, []*Match{ {Session: &Session{SessionID: "s1", AgentName: "claude", Messages: []Message{{Role: RoleUser, Text: "change tracked"}}}}, {Session: &Session{SessionID: "s2", AgentName: "claude", Messages: []Message{{Role: RoleUser, Text: "other"}}}}, }) if err != nil { t.Fatalf("disambiguate: %v", err) } if selected.AgentName != "claude" || selected.SessionID != "s1" { t.Fatalf("selected = %q, want s1", selected) } if got := gitTestCmd(t, dir, "status", "--porcelain"); got != "" { t.Fatalf("expected clean worktree, got %q", got) } data, err := os.ReadFile(filepath.Join(dir, "tracked.txt")) if err != nil { t.Fatal(err) } if string(data) != "before\n" { t.Fatalf("tracked file = %q, want before", data) } } func TestAgentDisambiguator_CleansCommittedSideEffects(t *testing.T) { dir := t.TempDir() gitTestCmd(t, dir, "init") gitTestCmd(t, dir, "config", "core.autocrlf", "false") gitTestCmd(t, dir, "config", "user.name", "test") gitTestCmd(t, dir, "config", "user.email", "test@example.com") if err := os.WriteFile(filepath.Join(dir, "tracked.txt"), []byte("before\n"), 0o644); err != nil { t.Fatal(err) } gitTestCmd(t, dir, "add", "tracked.txt") gitTestCmd(t, dir, "commit", "-m", "initial") beforeHead := gitTestCmd(t, dir, "rev-parse", "HEAD") fa := &fakeAgent{} fa.run = func(_ context.Context, opts agent.RunOpts) (*agent.Result, error) { if err := os.WriteFile(filepath.Join(opts.CWD, "tracked.txt"), []byte("after\n"), 0o644); err != nil { t.Fatal(err) } gitTestCmd(t, opts.CWD, "add", "tracked.txt") gitTestCmd(t, opts.CWD, "commit", "-m", "agent side effect") out := []byte(`{"agent_name":"claude","session_id":"s1","confidence":0.95,"reason":"closest"}`) return &agent.Result{Output: out, Text: string(out)}, nil } d := NewAgentDisambiguator(fa, dir) selected, err := d.Disambiguate(context.Background(), []string{"tracked.txt"}, []*Match{ {Session: &Session{SessionID: "s1", AgentName: "claude", Messages: []Message{{Role: RoleUser, Text: "change tracked"}}}}, {Session: &Session{SessionID: "s2", AgentName: "claude", Messages: []Message{{Role: RoleUser, Text: "other"}}}}, }) if err != nil { t.Fatalf("disambiguate: %v", err) } if selected.AgentName != "claude" || selected.SessionID != "s1" { t.Fatalf("selected = %q, want s1", selected) } if got := gitTestCmd(t, dir, "rev-parse", "HEAD"); got != beforeHead { t.Fatalf("HEAD = %q, want %q", got, beforeHead) } if got := gitTestCmd(t, dir, "status", "--porcelain"); got != "" { t.Fatalf("expected clean worktree, got %q", got) } } func TestAgentDisambiguator_CleansWithCanceledAgentContext(t *testing.T) { dir := t.TempDir() gitTestCmd(t, dir, "init") gitTestCmd(t, dir, "config", "core.autocrlf", "false") gitTestCmd(t, dir, "config", "user.name", "test") gitTestCmd(t, dir, "config", "user.email", "test@example.com") if err := os.WriteFile(filepath.Join(dir, "tracked.txt"), []byte("before\n"), 0o644); err != nil { t.Fatal(err) } gitTestCmd(t, dir, "add", "tracked.txt") gitTestCmd(t, dir, "commit", "-m", "initial") ctx, cancel := context.WithCancel(context.Background()) fa := &fakeAgent{} fa.run = func(_ context.Context, opts agent.RunOpts) (*agent.Result, error) { if err := os.WriteFile(filepath.Join(opts.CWD, "tracked.txt"), []byte("after\n"), 0o644); err != nil { t.Fatal(err) } cancel() out := []byte(`{"agent_name":"claude","session_id":"s1","confidence":0.95,"reason":"closest"}`) return &agent.Result{Output: out, Text: string(out)}, nil } d := NewAgentDisambiguator(fa, dir) selected, err := d.Disambiguate(ctx, []string{"tracked.txt"}, []*Match{ {Session: &Session{SessionID: "s1", AgentName: "claude", Messages: []Message{{Role: RoleUser, Text: "change tracked"}}}}, {Session: &Session{SessionID: "s2", AgentName: "claude", Messages: []Message{{Role: RoleUser, Text: "other"}}}}, }) if err != nil { t.Fatalf("disambiguate: %v", err) } if selected.AgentName != "claude" || selected.SessionID != "s1" { t.Fatalf("selected = %q, want s1", selected) } if got := gitTestCmd(t, dir, "status", "--porcelain"); got != "" { t.Fatalf("expected clean worktree, got %q", got) } data, err := os.ReadFile(filepath.Join(dir, "tracked.txt")) if err != nil { t.Fatal(err) } if string(data) != "before\n" { t.Fatalf("tracked file = %q, want before", data) } } func gitTestCmd(t *testing.T, dir string, args ...string) string { t.Helper() cmd := exec.Command("git", append([]string{"-C", dir}, args...)...) out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } return strings.TrimSpace(string(out)) }