55 "encoding/json"
66 "fmt"
77 "os"
8+ "os/exec"
89 "path/filepath"
910 "strings"
1011 "sync/atomic"
@@ -14,6 +15,44 @@ import (
1415 "github.com/minicodemonkey/chief/internal/prd"
1516)
1617
18+ // mockProvider implements Provider for tests without importing agent (avoids import cycle).
19+ type mockProvider struct {
20+ cliPath string // if set, used as CLI path; otherwise "claude"
21+ }
22+
23+ func (m * mockProvider ) Name () string { return "Test" }
24+ func (m * mockProvider ) CLIPath () string { return m .path () }
25+ func (m * mockProvider ) InteractiveCommand (_ , _ string ) * exec.Cmd { return exec .Command ("true" ) }
26+ func (m * mockProvider ) ParseLine (line string ) * Event { return ParseLine (line ) }
27+ func (m * mockProvider ) LogFileName () string { return "claude.log" }
28+
29+ func (m * mockProvider ) ConvertCommand (_ , _ string ) (* exec.Cmd , OutputMode , string , error ) {
30+ return exec .Command ("true" ), OutputStdout , "" , nil
31+ }
32+
33+ func (m * mockProvider ) FixJSONCommand (_ string ) (* exec.Cmd , OutputMode , string , error ) {
34+ return exec .Command ("true" ), OutputStdout , "" , nil
35+ }
36+
37+ func (m * mockProvider ) path () string {
38+ if m .cliPath != "" {
39+ return m .cliPath
40+ }
41+ return "claude"
42+ }
43+
44+ func (m * mockProvider ) LoopCommand (ctx context.Context , _ , workDir string ) * exec.Cmd {
45+ p := m .path ()
46+ cmd := exec .CommandContext (ctx , p )
47+ cmd .Dir = workDir
48+ return cmd
49+ }
50+
51+ func (m * mockProvider ) CleanOutput (output string ) string { return output }
52+
53+ // testProvider is used by loop tests so they don't need to run a real CLI.
54+ var testProvider Provider = & mockProvider {}
55+
1756// createMockClaudeScript creates a shell script that outputs predefined stream-json.
1857func createMockClaudeScript (t * testing.T , dir string , output []string ) string {
1958 t .Helper ()
@@ -59,7 +98,7 @@ func createTestPRD(t *testing.T, dir string, allComplete bool) string {
5998}
6099
61100func TestNewLoop (t * testing.T ) {
62- l := NewLoop ("/path/to/prd.json" , "test prompt" , 5 )
101+ l := NewLoop ("/path/to/prd.json" , "test prompt" , 5 , testProvider )
63102
64103 if l .prdPath != "/path/to/prd.json" {
65104 t .Errorf ("Expected prdPath %q, got %q" , "/path/to/prd.json" , l .prdPath )
@@ -76,7 +115,7 @@ func TestNewLoop(t *testing.T) {
76115}
77116
78117func TestNewLoopWithWorkDir (t * testing.T ) {
79- l := NewLoopWithWorkDir ("/path/to/prd.json" , "/work/dir" , "test prompt" , 5 )
118+ l := NewLoopWithWorkDir ("/path/to/prd.json" , "/work/dir" , "test prompt" , 5 , testProvider )
80119
81120 if l .prdPath != "/path/to/prd.json" {
82121 t .Errorf ("Expected prdPath %q, got %q" , "/path/to/prd.json" , l .prdPath )
@@ -96,15 +135,15 @@ func TestNewLoopWithWorkDir(t *testing.T) {
96135}
97136
98137func TestNewLoopWithWorkDir_EmptyWorkDir (t * testing.T ) {
99- l := NewLoopWithWorkDir ("/path/to/prd.json" , "" , "test prompt" , 5 )
138+ l := NewLoopWithWorkDir ("/path/to/prd.json" , "" , "test prompt" , 5 , testProvider )
100139
101140 if l .workDir != "" {
102141 t .Errorf ("Expected empty workDir, got %q" , l .workDir )
103142 }
104143}
105144
106145func TestLoop_Events (t * testing.T ) {
107- l := NewLoop ("/path/to/prd.json" , "test prompt" , 5 )
146+ l := NewLoop ("/path/to/prd.json" , "test prompt" , 5 , testProvider )
108147 events := l .Events ()
109148
110149 if events == nil {
@@ -113,7 +152,7 @@ func TestLoop_Events(t *testing.T) {
113152}
114153
115154func TestLoop_Iteration (t * testing.T ) {
116- l := NewLoop ("/path/to/prd.json" , "test prompt" , 5 )
155+ l := NewLoop ("/path/to/prd.json" , "test prompt" , 5 , testProvider )
117156
118157 if l .Iteration () != 0 {
119158 t .Errorf ("Expected initial iteration to be 0, got %d" , l .Iteration ())
@@ -126,7 +165,7 @@ func TestLoop_Iteration(t *testing.T) {
126165}
127166
128167func TestLoop_Stop (t * testing.T ) {
129- l := NewLoop ("/path/to/prd.json" , "test prompt" , 5 )
168+ l := NewLoop ("/path/to/prd.json" , "test prompt" , 5 , testProvider )
130169
131170 l .Stop ()
132171
@@ -162,7 +201,7 @@ func TestLoop_RunWithMockClaude(t *testing.T) {
162201
163202 // Create a prompt that invokes our mock script instead of real Claude
164203 // For the actual test, we'll test the internal methods
165- l := NewLoop (prdPath , "test prompt" , 1 )
204+ l := NewLoop (prdPath , "test prompt" , 1 , testProvider )
166205
167206 // Override the command for testing - we'll test processOutput directly
168207 ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
@@ -244,7 +283,7 @@ func TestLoop_MaxIterations(t *testing.T) {
244283 tmpDir := t .TempDir ()
245284 prdPath := createTestPRD (t , tmpDir , false ) // Not complete
246285
247- l := NewLoop (prdPath , "test prompt" , 2 )
286+ l := NewLoop (prdPath , "test prompt" , 2 , testProvider )
248287
249288 // Simulate reaching max iterations by manually incrementing
250289 l .iteration = 2
@@ -290,7 +329,7 @@ func TestLoop_LogFile(t *testing.T) {
290329 t .Fatalf ("Failed to create log file: %v" , err )
291330 }
292331
293- l := NewLoop (filepath .Join (tmpDir , "prd.json" ), "test" , 1 )
332+ l := NewLoop (filepath .Join (tmpDir , "prd.json" ), "test" , 1 , testProvider )
294333 l .logFile = logFile
295334
296335 l .logLine ("test log line" )
@@ -309,7 +348,7 @@ func TestLoop_LogFile(t *testing.T) {
309348
310349// TestLoop_ChiefCompleteEvent tests detection of <chief-complete/> event.
311350func TestLoop_ChiefCompleteEvent (t * testing.T ) {
312- l := NewLoop ("/test/prd.json" , "test" , 5 )
351+ l := NewLoop ("/test/prd.json" , "test" , 5 , testProvider )
313352 l .iteration = 1
314353
315354 done := make (chan bool )
@@ -350,7 +389,7 @@ func TestLoop_ChiefCompleteEvent(t *testing.T) {
350389
351390// TestLoop_SetMaxIterations tests setting max iterations at runtime.
352391func TestLoop_SetMaxIterations (t * testing.T ) {
353- l := NewLoop ("/test/prd.json" , "test" , 5 )
392+ l := NewLoop ("/test/prd.json" , "test" , 5 , testProvider )
354393
355394 if l .MaxIterations () != 5 {
356395 t .Errorf ("Expected initial maxIter 5, got %d" , l .MaxIterations ())
@@ -380,7 +419,7 @@ func TestDefaultRetryConfig(t *testing.T) {
380419
381420// TestLoop_SetRetryConfig tests setting retry config.
382421func TestLoop_SetRetryConfig (t * testing.T ) {
383- l := NewLoop ("/test/prd.json" , "test" , 5 )
422+ l := NewLoop ("/test/prd.json" , "test" , 5 , testProvider )
384423
385424 // Check default
386425 if ! l .retryConfig .Enabled {
@@ -408,7 +447,7 @@ func TestLoop_SetRetryConfig(t *testing.T) {
408447
409448// TestLoop_WatchdogDefaultTimeout tests that the default watchdog timeout is set.
410449func TestLoop_WatchdogDefaultTimeout (t * testing.T ) {
411- l := NewLoop ("/test/prd.json" , "test" , 5 )
450+ l := NewLoop ("/test/prd.json" , "test" , 5 , testProvider )
412451
413452 if l .WatchdogTimeout () != DefaultWatchdogTimeout {
414453 t .Errorf ("Expected default watchdog timeout %v, got %v" , DefaultWatchdogTimeout , l .WatchdogTimeout ())
@@ -417,7 +456,7 @@ func TestLoop_WatchdogDefaultTimeout(t *testing.T) {
417456
418457// TestLoop_SetWatchdogTimeout tests setting the watchdog timeout.
419458func TestLoop_SetWatchdogTimeout (t * testing.T ) {
420- l := NewLoop ("/test/prd.json" , "test" , 5 )
459+ l := NewLoop ("/test/prd.json" , "test" , 5 , testProvider )
421460
422461 l .SetWatchdogTimeout (10 * time .Minute )
423462 if l .WatchdogTimeout () != 10 * time .Minute {
@@ -433,7 +472,7 @@ func TestLoop_SetWatchdogTimeout(t *testing.T) {
433472
434473// TestLoop_WatchdogKillsHungProcess tests that a hung process is killed after timeout.
435474func TestLoop_WatchdogKillsHungProcess (t * testing.T ) {
436- l := NewLoop ("/test/prd.json" , "test" , 5 )
475+ l := NewLoop ("/test/prd.json" , "test" , 5 , testProvider )
437476 l .iteration = 1
438477
439478 // Use a very short timeout for testing
@@ -496,7 +535,7 @@ func TestLoop_WatchdogKillsHungProcess(t *testing.T) {
496535
497536// TestLoop_WatchdogDoesNotFireForActiveProcess tests that an active process doesn't trigger the watchdog.
498537func TestLoop_WatchdogDoesNotFireForActiveProcess (t * testing.T ) {
499- l := NewLoop ("/test/prd.json" , "test" , 5 )
538+ l := NewLoop ("/test/prd.json" , "test" , 5 , testProvider )
500539 l .iteration = 1
501540
502541 // Use a timeout that's longer than our test
@@ -551,7 +590,7 @@ func TestLoop_WatchdogDoesNotFireForActiveProcess(t *testing.T) {
551590
552591// TestLoop_WatchdogDisabledWithZeroTimeout tests that watchdog is disabled when timeout is 0.
553592func TestLoop_WatchdogDisabledWithZeroTimeout (t * testing.T ) {
554- l := NewLoop ("/test/prd.json" , "test" , 5 )
593+ l := NewLoop ("/test/prd.json" , "test" , 5 , testProvider )
555594 l .SetWatchdogTimeout (0 )
556595
557596 if l .WatchdogTimeout () != 0 {
@@ -561,7 +600,7 @@ func TestLoop_WatchdogDisabledWithZeroTimeout(t *testing.T) {
561600 // Verify that runIteration would not start a watchdog
562601 // (tested indirectly: timeout == 0 means the if-block in runIteration is skipped)
563602 // We test this by verifying the constructor behavior and setter
564- l2 := NewLoop ("/test/prd.json" , "test" , 5 )
603+ l2 := NewLoop ("/test/prd.json" , "test" , 5 , testProvider )
565604 l2 .SetWatchdogTimeout (0 )
566605
567606 l2 .mu .Lock ()
@@ -575,7 +614,7 @@ func TestLoop_WatchdogDisabledWithZeroTimeout(t *testing.T) {
575614
576615// TestLoop_LastOutputTimeUpdated tests that lastOutputTime is updated on each scanner output.
577616func TestLoop_LastOutputTimeUpdated (t * testing.T ) {
578- l := NewLoop ("/test/prd.json" , "test" , 5 )
617+ l := NewLoop ("/test/prd.json" , "test" , 5 , testProvider )
579618 l .iteration = 1
580619
581620 // Drain events to avoid blocking
@@ -616,7 +655,7 @@ func TestLoop_LastOutputTimeUpdated(t *testing.T) {
616655// that feeds into retry logic.
617656func TestLoop_WatchdogReturnsError (t * testing.T ) {
618657 // This test verifies the error message format that runIterationWithRetry will see
619- l := NewLoop ("/test/prd.json" , "test" , 5 )
658+ l := NewLoop ("/test/prd.json" , "test" , 5 , testProvider )
620659 l .SetWatchdogTimeout (100 * time .Millisecond )
621660
622661 // The watchdog error message should contain "watchdog timeout"
@@ -630,7 +669,7 @@ func TestLoop_WatchdogReturnsError(t *testing.T) {
630669
631670// TestLoop_WatchdogWithWorkDir tests that watchdog works with NewLoopWithWorkDir too.
632671func TestLoop_WatchdogWithWorkDir (t * testing.T ) {
633- l := NewLoopWithWorkDir ("/test/prd.json" , "/work" , "test" , 5 )
672+ l := NewLoopWithWorkDir ("/test/prd.json" , "/work" , "test" , 5 , testProvider )
634673
635674 if l .WatchdogTimeout () != DefaultWatchdogTimeout {
636675 t .Errorf ("Expected default watchdog timeout for NewLoopWithWorkDir, got %v" , l .WatchdogTimeout ())
0 commit comments