@@ -22,6 +22,7 @@ pub const Executor = struct {
2222 log : * Logger ,
2323 allocator : Allocator ,
2424 session_id : ? []const u8 ,
25+ opencode_cmd : []const u8 ,
2526
2627 /// Initialize executor
2728 pub fn init (cfg : * const config.Config , log : * Logger , allocator : Allocator ) Executor {
@@ -30,6 +31,18 @@ pub const Executor = struct {
3031 .log = log ,
3132 .allocator = allocator ,
3233 .session_id = null ,
34+ .opencode_cmd = "opencode" ,
35+ };
36+ }
37+
38+ /// Initialize executor with custom opencode command (for testing)
39+ pub fn initWithCmd (cfg : * const config.Config , log : * Logger , allocator : Allocator , opencode_cmd : []const u8 ) Executor {
40+ return Executor {
41+ .cfg = cfg ,
42+ .log = log ,
43+ .allocator = allocator ,
44+ .session_id = null ,
45+ .opencode_cmd = opencode_cmd ,
3346 };
3447 }
3548
@@ -166,7 +179,7 @@ pub const Executor = struct {
166179 var args = std .ArrayListUnmanaged ([]const u8 ){};
167180 defer args .deinit (self .allocator );
168181
169- try args .append (self .allocator , "opencode" );
182+ try args .append (self .allocator , self . opencode_cmd );
170183 try args .append (self .allocator , "run" );
171184 try args .append (self .allocator , "--model" );
172185 try args .append (self .allocator , model );
@@ -195,14 +208,16 @@ pub const Executor = struct {
195208
196209 // Read stdout
197210 var stdout_list = std .ArrayListUnmanaged (u8 ){};
198- errdefer stdout_list .deinit (self .allocator );
199211
200212 if (child .stdout ) | stdout | {
201213 var buf : [4096 ]u8 = undefined ;
202214 while (true ) {
203215 const n = stdout .read (& buf ) catch break ;
204216 if (n == 0 ) break ;
205- try stdout_list .appendSlice (self .allocator , buf [0.. n ]);
217+ stdout_list .appendSlice (self .allocator , buf [0.. n ]) catch | err | {
218+ stdout_list .deinit (self .allocator );
219+ return err ;
220+ };
206221 }
207222 }
208223
@@ -235,14 +250,178 @@ pub const Executor = struct {
235250// Tests
236251// ============================================================================
237252
253+ // Test helper: Create a minimal mock logger for testing
254+ fn createTestLogger (allocator : Allocator ) ! * Logger {
255+ // Create a temp directory for logging
256+ const temp_dir = try std .fmt .allocPrint (allocator , "/tmp/opencoder_test_{d}" , .{std .time .milliTimestamp ()});
257+ defer allocator .free (temp_dir );
258+
259+ try std .fs .cwd ().makePath (temp_dir );
260+
261+ const cycle_log_dir = try std .fs .path .join (allocator , &.{ temp_dir , "cycles" });
262+ const alerts_file = try std .fs .path .join (allocator , &.{ temp_dir , "alerts.log" });
263+
264+ try std .fs .cwd ().makePath (cycle_log_dir );
265+
266+ const logger_ptr = try allocator .create (Logger );
267+ logger_ptr .* = Logger {
268+ .main_log = null ,
269+ .cycle_log_dir = cycle_log_dir ,
270+ .alerts_file = alerts_file ,
271+ .cycle = 0 ,
272+ .verbose = false ,
273+ .allocator = allocator ,
274+ };
275+ return logger_ptr ;
276+ }
277+
278+ fn destroyTestLogger (logger_ptr : * Logger , allocator : Allocator ) void {
279+ // Clean up temp directory
280+ const temp_base = std .fs .path .dirname (logger_ptr .cycle_log_dir ) orelse "/tmp" ;
281+ std .fs .cwd ().deleteTree (temp_base ) catch {};
282+
283+ allocator .free (logger_ptr .cycle_log_dir );
284+ allocator .free (logger_ptr .alerts_file );
285+ allocator .destroy (logger_ptr );
286+ }
287+
238288test "Executor.init creates executor" {
239- // We can't fully test without a Logger, but we can test initialization structure
240- // This is more of a compile-time check
241289 const allocator = std .testing .allocator ;
242- _ = allocator ;
290+ const test_logger = try createTestLogger (allocator );
291+ defer destroyTestLogger (test_logger , allocator );
292+
293+ const test_cfg = config.Config {
294+ .planning_model = "test/model" ,
295+ .execution_model = "test/model" ,
296+ .project_dir = "/tmp" ,
297+ .verbose = false ,
298+ .user_hint = null ,
299+ .max_retries = 3 ,
300+ .backoff_base = 1 ,
301+ .log_retention = 30 ,
302+ };
303+
304+ const executor = Executor .init (& test_cfg , test_logger , allocator );
305+ try std .testing .expectEqualStrings ("opencode" , executor .opencode_cmd );
306+ try std .testing .expect (executor .session_id == null );
307+ }
308+
309+ test "Executor.initWithCmd creates executor with custom command" {
310+ const allocator = std .testing .allocator ;
311+ const test_logger = try createTestLogger (allocator );
312+ defer destroyTestLogger (test_logger , allocator );
313+
314+ const test_cfg = config.Config {
315+ .planning_model = "test/model" ,
316+ .execution_model = "test/model" ,
317+ .project_dir = "/tmp" ,
318+ .verbose = false ,
319+ .user_hint = null ,
320+ .max_retries = 3 ,
321+ .backoff_base = 1 ,
322+ .log_retention = 30 ,
323+ };
324+
325+ const executor = Executor .initWithCmd (& test_cfg , test_logger , allocator , "./test_helpers/mock_opencode.sh" );
326+ try std .testing .expectEqualStrings ("./test_helpers/mock_opencode.sh" , executor .opencode_cmd );
243327}
244328
245329test "ExecutionResult enum values" {
246330 try std .testing .expectEqual (ExecutionResult .success , ExecutionResult .success );
247331 try std .testing .expectEqual (ExecutionResult .failure , ExecutionResult .failure );
248332}
333+
334+ test "runOpencode handles successful execution" {
335+ const allocator = std .testing .allocator ;
336+ const test_logger = try createTestLogger (allocator );
337+ defer destroyTestLogger (test_logger , allocator );
338+
339+ const test_cfg = config.Config {
340+ .planning_model = "test/model" ,
341+ .execution_model = "test/model" ,
342+ .project_dir = "/tmp" ,
343+ .verbose = false ,
344+ .user_hint = null ,
345+ .max_retries = 3 ,
346+ .backoff_base = 1 ,
347+ .log_retention = 30 ,
348+ };
349+
350+ // Get absolute path to mock script
351+ var cwd_buf : [std .fs .max_path_bytes ]u8 = undefined ;
352+ const cwd = try std .fs .cwd ().realpath ("." , & cwd_buf );
353+ const mock_path = try std .fs .path .join (allocator , &.{ cwd , "test_helpers/mock_opencode_success.sh" });
354+ defer allocator .free (mock_path );
355+
356+ var executor = Executor .initWithCmd (& test_cfg , test_logger , allocator , mock_path );
357+ defer executor .deinit ();
358+
359+ const result = try executor .runOpencode ("test/model" , "Test" , "test prompt" , false );
360+ defer allocator .free (result );
361+
362+ try std .testing .expect (result .len > 0 );
363+ try std .testing .expect (std .mem .indexOf (u8 , result , "Mock opencode output" ) != null );
364+ }
365+
366+ test "runOpencode handles process failure" {
367+ const allocator = std .testing .allocator ;
368+ const test_logger = try createTestLogger (allocator );
369+ defer destroyTestLogger (test_logger , allocator );
370+
371+ const test_cfg = config.Config {
372+ .planning_model = "test/model" ,
373+ .execution_model = "test/model" ,
374+ .project_dir = "/tmp" ,
375+ .verbose = false ,
376+ .user_hint = null ,
377+ .max_retries = 3 ,
378+ .backoff_base = 1 ,
379+ .log_retention = 30 ,
380+ };
381+
382+ // Get absolute path to mock script
383+ var cwd_buf : [std .fs .max_path_bytes ]u8 = undefined ;
384+ const cwd = try std .fs .cwd ().realpath ("." , & cwd_buf );
385+ const mock_path = try std .fs .path .join (allocator , &.{ cwd , "test_helpers/mock_opencode_failure.sh" });
386+ defer allocator .free (mock_path );
387+
388+ var executor = Executor .initWithCmd (& test_cfg , test_logger , allocator , mock_path );
389+ defer executor .deinit ();
390+
391+ const result = executor .runOpencode ("test/model" , "Test" , "test prompt" , false );
392+ try std .testing .expectError (error .OpencodeFailed , result );
393+ }
394+
395+ test "runOpencode passes session ID when continuing" {
396+ const allocator = std .testing .allocator ;
397+ const test_logger = try createTestLogger (allocator );
398+ defer destroyTestLogger (test_logger , allocator );
399+
400+ const test_cfg = config.Config {
401+ .planning_model = "test/model" ,
402+ .execution_model = "test/model" ,
403+ .project_dir = "/tmp" ,
404+ .verbose = false ,
405+ .user_hint = null ,
406+ .max_retries = 3 ,
407+ .backoff_base = 1 ,
408+ .log_retention = 30 ,
409+ };
410+
411+ // Get absolute path to mock script
412+ var cwd_buf : [std .fs .max_path_bytes ]u8 = undefined ;
413+ const cwd = try std .fs .cwd ().realpath ("." , & cwd_buf );
414+ const mock_path = try std .fs .path .join (allocator , &.{ cwd , "test_helpers/mock_opencode_success.sh" });
415+ defer allocator .free (mock_path );
416+
417+ var executor = Executor .initWithCmd (& test_cfg , test_logger , allocator , mock_path );
418+ defer executor .deinit ();
419+
420+ // Set a session ID
421+ executor .session_id = try std .fmt .allocPrint (allocator , "test_session_123" , .{});
422+
423+ const result = try executor .runOpencode ("test/model" , "Test" , "test prompt" , true );
424+ defer allocator .free (result );
425+
426+ try std .testing .expect (result .len > 0 );
427+ }
0 commit comments