Skip to content

Commit a248d14

Browse files
committed
refactor(review): split cmd_test.go into smaller files
1 parent 6f74464 commit a248d14

2 files changed

Lines changed: 318 additions & 305 deletions

File tree

cli/review/cmd_2_test.go

Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
package review_test
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"strings"
7+
"testing"
8+
9+
"github.com/GrayCodeAI/trace/cli/agent/types"
10+
"github.com/GrayCodeAI/trace/cli/review"
11+
reviewtypes "github.com/GrayCodeAI/trace/cli/review/types"
12+
"github.com/GrayCodeAI/trace/cli/settings"
13+
)
14+
15+
// TestComposeMultiAgentSinks exercises the sink-composition helper directly
16+
// with explicit isTTY/canPrompt values, so we get real coverage of the TTY
17+
// branch without depending on os.Stdout being a terminal during `go test`.
18+
func TestComposeMultiAgentSinks(t *testing.T) {
19+
t.Parallel()
20+
21+
provider := &stubCmdSynthesisProvider{}
22+
noopCancel := func() {}
23+
24+
tests := []struct {
25+
name string
26+
isTTY bool
27+
canPrompt bool
28+
provider review.SynthesisProvider
29+
wantTUI bool
30+
wantDump bool
31+
wantSynth bool
32+
wantTotal int
33+
}{
34+
{
35+
name: "non-tty omits tui and synth",
36+
isTTY: false,
37+
canPrompt: false,
38+
provider: provider,
39+
wantDump: true,
40+
wantTotal: 1,
41+
},
42+
{
43+
name: "tty with provider and prompt appends synth",
44+
isTTY: true,
45+
canPrompt: true,
46+
provider: provider,
47+
wantTUI: true,
48+
wantDump: true,
49+
wantSynth: true,
50+
wantTotal: 3,
51+
},
52+
{
53+
name: "tty without provider skips synth",
54+
isTTY: true,
55+
canPrompt: true,
56+
provider: nil,
57+
wantTUI: true,
58+
wantDump: true,
59+
wantTotal: 2,
60+
},
61+
{
62+
name: "tty without prompt skips synth even with provider",
63+
isTTY: true,
64+
canPrompt: false,
65+
provider: provider,
66+
wantTUI: true,
67+
wantDump: true,
68+
wantTotal: 2,
69+
},
70+
}
71+
72+
for _, tt := range tests {
73+
t.Run(tt.name, func(t *testing.T) {
74+
t.Parallel()
75+
sinks := review.ExposedComposeMultiAgentSinks(review.SinkComposeInputs{
76+
Out: &bytes.Buffer{},
77+
IsTTY: tt.isTTY,
78+
CanPrompt: tt.canPrompt,
79+
AgentNames: []string{"a", "b"},
80+
CancelRun: noopCancel,
81+
SynthesisProvider: tt.provider,
82+
})
83+
if got := len(sinks); got != tt.wantTotal {
84+
t.Fatalf("len(sinks)=%d, want %d", got, tt.wantTotal)
85+
}
86+
_, hasTUI := review.ExposedFindTUISink(sinks)
87+
if hasTUI != tt.wantTUI {
88+
t.Errorf("findTUISink found=%v, want %v", hasTUI, tt.wantTUI)
89+
}
90+
var hasDump, hasSynth bool
91+
for _, s := range sinks {
92+
switch s.(type) {
93+
case review.DumpSink:
94+
hasDump = true
95+
case review.SynthesisSink:
96+
hasSynth = true
97+
}
98+
}
99+
if hasDump != tt.wantDump {
100+
t.Errorf("DumpSink present=%v, want %v", hasDump, tt.wantDump)
101+
}
102+
if hasSynth != tt.wantSynth {
103+
t.Errorf("SynthesisSink present=%v, want %v", hasSynth, tt.wantSynth)
104+
}
105+
})
106+
}
107+
}
108+
109+
func TestComposeSingleAgentSinks(t *testing.T) {
110+
t.Parallel()
111+
112+
noopCancel := func() {}
113+
114+
tests := []struct {
115+
name string
116+
isTTY bool
117+
canPrompt bool
118+
wantTUI bool
119+
wantDump bool
120+
wantTotal int
121+
wantOutput string
122+
}{
123+
{
124+
name: "non-tty prints running line and uses dump only",
125+
wantDump: true,
126+
wantTotal: 1,
127+
wantOutput: "Running review with agent-a...",
128+
},
129+
{
130+
name: "tty uses tui and dump",
131+
isTTY: true,
132+
canPrompt: true,
133+
wantTUI: true,
134+
wantDump: true,
135+
wantTotal: 2,
136+
},
137+
{
138+
name: "tty without prompt falls back to running line",
139+
isTTY: true,
140+
canPrompt: false,
141+
wantDump: true,
142+
wantTotal: 1,
143+
wantOutput: "Running review with agent-a...",
144+
},
145+
}
146+
147+
for _, tt := range tests {
148+
t.Run(tt.name, func(t *testing.T) {
149+
t.Parallel()
150+
151+
out := &bytes.Buffer{}
152+
sinks := review.ExposedComposeSingleAgentSinks(review.SingleAgentSinkComposeInputs{
153+
Out: out,
154+
IsTTY: tt.isTTY,
155+
CanPrompt: tt.canPrompt,
156+
AgentName: "agent-a",
157+
CancelRun: noopCancel,
158+
})
159+
if got := len(sinks); got != tt.wantTotal {
160+
t.Fatalf("len(sinks)=%d, want %d", got, tt.wantTotal)
161+
}
162+
_, hasTUI := review.ExposedFindTUISink(sinks)
163+
if hasTUI != tt.wantTUI {
164+
t.Errorf("findTUISink found=%v, want %v", hasTUI, tt.wantTUI)
165+
}
166+
var hasDump, hasSynth bool
167+
for _, s := range sinks {
168+
switch s.(type) {
169+
case review.DumpSink:
170+
hasDump = true
171+
case review.SynthesisSink:
172+
hasSynth = true
173+
}
174+
}
175+
if hasDump != tt.wantDump {
176+
t.Errorf("DumpSink present=%v, want %v", hasDump, tt.wantDump)
177+
}
178+
if hasSynth {
179+
t.Error("SynthesisSink should not be present for single-agent reviews")
180+
}
181+
if tt.wantOutput != "" && !strings.Contains(out.String(), tt.wantOutput) {
182+
t.Errorf("output missing %q:\n%s", tt.wantOutput, out.String())
183+
}
184+
if tt.wantOutput == "" && out.Len() != 0 {
185+
t.Errorf("expected no pre-run output, got:\n%s", out.String())
186+
}
187+
})
188+
}
189+
}
190+
191+
func TestComposeSinks_TUIWritersRunBeforePostRunWriters(t *testing.T) {
192+
t.Parallel()
193+
provider := &stubSynthesisProvider{}
194+
195+
multi := review.ExposedComposeMultiAgentSinks(review.SinkComposeInputs{
196+
Out: &bytes.Buffer{},
197+
IsTTY: true,
198+
CanPrompt: true,
199+
AgentNames: []string{"a", "b"},
200+
CancelRun: func() {},
201+
SynthesisProvider: provider,
202+
})
203+
if len(multi) != 3 {
204+
t.Fatalf("multi sinks len = %d, want 3", len(multi))
205+
}
206+
if _, ok := multi[0].(*review.TUISink); !ok {
207+
t.Fatalf("multi sink[0] = %T, want *TUISink", multi[0])
208+
}
209+
if _, ok := multi[1].(review.DumpSink); !ok {
210+
t.Fatalf("multi sink[1] = %T, want DumpSink", multi[1])
211+
}
212+
if _, ok := multi[2].(review.SynthesisSink); !ok {
213+
t.Fatalf("multi sink[2] = %T, want SynthesisSink", multi[2])
214+
}
215+
216+
single := review.ExposedComposeSingleAgentSinks(review.SingleAgentSinkComposeInputs{
217+
Out: &bytes.Buffer{},
218+
IsTTY: true,
219+
CanPrompt: true,
220+
AgentName: "a",
221+
CancelRun: func() {},
222+
})
223+
if len(single) != 2 {
224+
t.Fatalf("single sinks len = %d, want 2", len(single))
225+
}
226+
if _, ok := single[0].(*review.TUISink); !ok {
227+
t.Fatalf("single sink[0] = %T, want *TUISink", single[0])
228+
}
229+
if _, ok := single[1].(review.DumpSink); !ok {
230+
t.Fatalf("single sink[1] = %T, want DumpSink", single[1])
231+
}
232+
}
233+
234+
// TestFindTUISink_NoTUIInSlice covers the not-found path so the caller's
235+
// `if tuiSink, ok := findTUISink(sinks); ok` branch is exercised in both
236+
// directions.
237+
func TestFindTUISink_NoTUIInSlice(t *testing.T) {
238+
t.Parallel()
239+
sinks := []reviewtypes.Sink{review.DumpSink{W: &bytes.Buffer{}}}
240+
if tui, ok := review.ExposedFindTUISink(sinks); ok || tui != nil {
241+
t.Errorf("findTUISink on dump-only slice returned (%v, %v); want (nil, false)", tui, ok)
242+
}
243+
}
244+
245+
// TestDispatchFork_SynthesisSinkNilProviderNoComposition verifies that when
246+
// deps.SynthesisProvider is nil, the command runs without panicking and does
247+
// not attempt to synthesize (no synthesis output appears).
248+
func TestDispatchFork_SynthesisSinkNilProviderNoComposition(t *testing.T) {
249+
setupCmdTestRepo(t)
250+
251+
if err := review.SaveReviewConfig(context.Background(), map[string]settings.ReviewConfig{
252+
"agent-a": {Prompt: "review"},
253+
"agent-b": {Prompt: "review"},
254+
}); err != nil {
255+
t.Fatal(err)
256+
}
257+
258+
multiPickerFn := func(_ context.Context, eligible []review.AgentChoice) (review.PickedAgents, error) {
259+
names := make([]string, 0, len(eligible))
260+
for _, e := range eligible {
261+
names = append(names, e.Name)
262+
}
263+
return review.PickedAgents{Names: names, PerRun: ""}, nil
264+
}
265+
266+
installed := []types.AgentName{"agent-a", "agent-b"}
267+
deps := newDispatchTestDeps(t, installed, []string{"agent-a", "agent-b"}, multiPickerFn, nil)
268+
deps.SynthesisProvider = nil // explicitly nil — synthesis unavailable
269+
270+
buf := &bytes.Buffer{}
271+
cmd := review.NewCommand(deps)
272+
cmd.SetOut(buf)
273+
cmd.SetErr(&bytes.Buffer{})
274+
cmd.SetArgs([]string{})
275+
276+
if err := cmd.Execute(); err != nil {
277+
t.Fatalf("unexpected error: %v", err)
278+
}
279+
// No synthesis output expected.
280+
if strings.Contains(buf.String(), "synthesis") {
281+
t.Errorf("no synthesis output expected when provider is nil, got: %s", buf.String())
282+
}
283+
}
284+
285+
// TestDispatchFork_SingleAgentNoSynthesis verifies that the single-agent path
286+
// never invokes synthesis (synthesis is multi-agent only). We set a provider
287+
// but use a single launchable agent; the command should complete without
288+
// calling the synthesis provider.
289+
func TestDispatchFork_SingleAgentNoSynthesis(t *testing.T) {
290+
setupCmdTestRepo(t)
291+
installHooksForCmdTest(t, "cursor")
292+
293+
if err := review.SaveReviewConfig(context.Background(), map[string]settings.ReviewConfig{
294+
"cursor": {Prompt: "review"},
295+
}); err != nil {
296+
t.Fatal(err)
297+
}
298+
299+
provider := &stubCmdSynthesisProvider{}
300+
301+
// cursor is installed but not launchable (ReviewerFor returns nil).
302+
installed := []types.AgentName{"cursor"}
303+
deps := newDispatchTestDeps(t, installed, nil /* no launchable */, nil, nil)
304+
deps.SynthesisProvider = provider
305+
306+
buf := &bytes.Buffer{}
307+
cmd := review.NewCommand(deps)
308+
cmd.SetOut(buf)
309+
cmd.SetErr(&bytes.Buffer{})
310+
cmd.SetArgs([]string{})
311+
312+
if err := cmd.Execute(); err != nil {
313+
t.Fatalf("unexpected error: %v", err)
314+
}
315+
if provider.called {
316+
t.Error("synthesis provider should NOT be called on single-agent path")
317+
}
318+
}

0 commit comments

Comments
 (0)