@@ -274,6 +274,69 @@ pub fn isShutdownRequested() bool {
274274// Tests
275275// ============================================================================
276276
277+ // Test helper: Create mock logger
278+ fn createTestLogger (allocator : Allocator ) ! * Logger {
279+ const temp_dir = try std .fmt .allocPrint (allocator , "/tmp/loop_test_{d}" , .{std .time .milliTimestamp ()});
280+ defer allocator .free (temp_dir );
281+
282+ try std .fs .cwd ().makePath (temp_dir );
283+
284+ const cycle_log_dir = try std .fs .path .join (allocator , &.{ temp_dir , "cycles" });
285+ const alerts_file = try std .fs .path .join (allocator , &.{ temp_dir , "alerts.log" });
286+
287+ try std .fs .cwd ().makePath (cycle_log_dir );
288+
289+ const logger_ptr = try allocator .create (Logger );
290+ logger_ptr .* = Logger {
291+ .main_log = null ,
292+ .cycle_log_dir = cycle_log_dir ,
293+ .alerts_file = alerts_file ,
294+ .cycle = 0 ,
295+ .verbose = false ,
296+ .allocator = allocator ,
297+ };
298+ return logger_ptr ;
299+ }
300+
301+ fn destroyTestLogger (logger_ptr : * Logger , allocator : Allocator ) void {
302+ const temp_base = std .fs .path .dirname (logger_ptr .cycle_log_dir ) orelse "/tmp" ;
303+ std .fs .cwd ().deleteTree (temp_base ) catch {};
304+
305+ allocator .free (logger_ptr .cycle_log_dir );
306+ allocator .free (logger_ptr .alerts_file );
307+ allocator .destroy (logger_ptr );
308+ }
309+
310+ // Test helper: Create mock paths
311+ fn createTestPaths (allocator : Allocator ) ! fsutil.Paths {
312+ const temp_dir = try std .fmt .allocPrint (allocator , "/tmp/loop_paths_{d}" , .{std .time .milliTimestamp ()});
313+ defer allocator .free (temp_dir );
314+
315+ try std .fs .cwd ().makePath (temp_dir );
316+
317+ const opencoder_dir = try allocator .dupe (u8 , temp_dir );
318+ const state_file = try std .fs .path .join (allocator , &.{ temp_dir , "state.json" });
319+ const current_plan = try std .fs .path .join (allocator , &.{ temp_dir , "current_plan.md" });
320+ const main_log = try std .fs .path .join (allocator , &.{ temp_dir , "main.log" });
321+ const cycle_log_dir = try std .fs .path .join (allocator , &.{ temp_dir , "cycles" });
322+ const alerts_file = try std .fs .path .join (allocator , &.{ temp_dir , "alerts.log" });
323+ const history_dir = try std .fs .path .join (allocator , &.{ temp_dir , "history" });
324+
325+ try std .fs .cwd ().makePath (cycle_log_dir );
326+ try std .fs .cwd ().makePath (history_dir );
327+
328+ return fsutil.Paths {
329+ .opencoder_dir = opencoder_dir ,
330+ .state_file = state_file ,
331+ .current_plan = current_plan ,
332+ .main_log = main_log ,
333+ .cycle_log_dir = cycle_log_dir ,
334+ .alerts_file = alerts_file ,
335+ .history_dir = history_dir ,
336+ .allocator = allocator ,
337+ };
338+ }
339+
277340test "shutdown flag management" {
278341 // Reset state
279342 shutdown_requested = false ;
@@ -287,3 +350,261 @@ test "shutdown flag management" {
287350 // Reset for other tests
288351 shutdown_requested = false ;
289352}
353+
354+ test "Loop.init creates loop with correct fields" {
355+ const allocator = std .testing .allocator ;
356+
357+ // Create test dependencies
358+ const test_logger = try createTestLogger (allocator );
359+ defer destroyTestLogger (test_logger , allocator );
360+
361+ var test_paths = try createTestPaths (allocator );
362+ defer test_paths .deinit ();
363+
364+ const test_cfg = config.Config {
365+ .planning_model = "test/model" ,
366+ .execution_model = "test/model" ,
367+ .project_dir = "/tmp" ,
368+ .verbose = false ,
369+ .user_hint = null ,
370+ .max_retries = 3 ,
371+ .backoff_base = 5 ,
372+ .log_retention = 30 ,
373+ };
374+
375+ var test_state = state .State .default ();
376+ defer test_state .deinit (allocator );
377+
378+ var test_executor = Executor .init (& test_cfg , test_logger , allocator );
379+ defer test_executor .deinit ();
380+
381+ // Create loop
382+ const loop = Loop .init (
383+ & test_cfg ,
384+ & test_state ,
385+ & test_paths ,
386+ test_logger ,
387+ & test_executor ,
388+ allocator ,
389+ );
390+
391+ // Verify fields
392+ try std .testing .expectEqual (& test_cfg , loop .cfg );
393+ try std .testing .expectEqual (& test_state , loop .st );
394+ try std .testing .expectEqual (& test_paths , loop .paths );
395+ try std .testing .expectEqual (test_logger , loop .log );
396+ try std .testing .expectEqual (& test_executor , loop .executor );
397+ }
398+
399+ test "backoffSleep calculates correct sleep time" {
400+ const allocator = std .testing .allocator ;
401+
402+ // Create test dependencies with backoff_base = 10
403+ const test_logger = try createTestLogger (allocator );
404+ defer destroyTestLogger (test_logger , allocator );
405+
406+ var test_paths = try createTestPaths (allocator );
407+ defer test_paths .deinit ();
408+
409+ const test_cfg = config.Config {
410+ .planning_model = "test/model" ,
411+ .execution_model = "test/model" ,
412+ .project_dir = "/tmp" ,
413+ .verbose = false ,
414+ .user_hint = null ,
415+ .max_retries = 3 ,
416+ .backoff_base = 10 ,
417+ .log_retention = 30 ,
418+ };
419+
420+ var test_state = state .State .default ();
421+ defer test_state .deinit (allocator );
422+
423+ var test_executor = Executor .init (& test_cfg , test_logger , allocator );
424+ defer test_executor .deinit ();
425+
426+ var loop = Loop .init (
427+ & test_cfg ,
428+ & test_state ,
429+ & test_paths ,
430+ test_logger ,
431+ & test_executor ,
432+ allocator ,
433+ );
434+
435+ // Note: We can't easily test the actual sleep without waiting,
436+ // but we can verify the calculation: backoff_base * 2 = 10 * 2 = 20 seconds
437+ // The backoffSleep function uses: self.cfg.backoff_base * 2
438+ try std .testing .expectEqual (@as (u32 , 10 ), loop .cfg .backoff_base );
439+
440+ // Call backoffSleep - it should sleep for 20 seconds, but we can't verify timing in tests
441+ // Just ensure it doesn't crash
442+ const start = std .time .nanoTimestamp ();
443+ loop .backoffSleep ();
444+ const elapsed = std .time .nanoTimestamp () - start ;
445+
446+ // Verify it actually slept (at least 19 seconds to account for scheduling)
447+ try std .testing .expect (elapsed >= 19 * std .time .ns_per_s );
448+ }
449+
450+ test "Loop state transitions between phases" {
451+ const allocator = std .testing .allocator ;
452+
453+ const test_logger = try createTestLogger (allocator );
454+ defer destroyTestLogger (test_logger , allocator );
455+
456+ var test_paths = try createTestPaths (allocator );
457+ defer test_paths .deinit ();
458+
459+ const test_cfg = config.Config {
460+ .planning_model = "test/model" ,
461+ .execution_model = "test/model" ,
462+ .project_dir = "/tmp" ,
463+ .verbose = false ,
464+ .user_hint = null ,
465+ .max_retries = 3 ,
466+ .backoff_base = 1 ,
467+ .log_retention = 30 ,
468+ };
469+
470+ var test_state = state .State .default ();
471+ defer test_state .deinit (allocator );
472+
473+ var test_executor = Executor .init (& test_cfg , test_logger , allocator );
474+ defer test_executor .deinit ();
475+
476+ _ = Loop .init (
477+ & test_cfg ,
478+ & test_state ,
479+ & test_paths ,
480+ test_logger ,
481+ & test_executor ,
482+ allocator ,
483+ );
484+
485+ // Verify initial state
486+ try std .testing .expectEqual (state .Phase .planning , test_state .phase );
487+ try std .testing .expectEqual (@as (u32 , 1 ), test_state .cycle );
488+ try std .testing .expectEqual (@as (u32 , 0 ), test_state .task_index );
489+
490+ // Simulate phase transitions
491+ test_state .phase = .execution ;
492+ try std .testing .expectEqual (state .Phase .execution , test_state .phase );
493+
494+ test_state .phase = .evaluation ;
495+ try std .testing .expectEqual (state .Phase .evaluation , test_state .phase );
496+
497+ // Simulate cycle completion
498+ test_state .cycle += 1 ;
499+ test_state .phase = .planning ;
500+ try std .testing .expectEqual (@as (u32 , 2 ), test_state .cycle );
501+ try std .testing .expectEqual (state .Phase .planning , test_state .phase );
502+ }
503+
504+ test "Loop handles task counter increments" {
505+ const allocator = std .testing .allocator ;
506+
507+ const test_logger = try createTestLogger (allocator );
508+ defer destroyTestLogger (test_logger , allocator );
509+
510+ var test_paths = try createTestPaths (allocator );
511+ defer test_paths .deinit ();
512+
513+ const test_cfg = config.Config {
514+ .planning_model = "test/model" ,
515+ .execution_model = "test/model" ,
516+ .project_dir = "/tmp" ,
517+ .verbose = false ,
518+ .user_hint = null ,
519+ .max_retries = 3 ,
520+ .backoff_base = 1 ,
521+ .log_retention = 30 ,
522+ };
523+
524+ var test_state = state .State .default ();
525+ defer test_state .deinit (allocator );
526+
527+ var test_executor = Executor .init (& test_cfg , test_logger , allocator );
528+ defer test_executor .deinit ();
529+
530+ _ = Loop .init (
531+ & test_cfg ,
532+ & test_state ,
533+ & test_paths ,
534+ test_logger ,
535+ & test_executor ,
536+ allocator ,
537+ );
538+
539+ // Set total tasks
540+ test_state .total_tasks = 5 ;
541+
542+ // Simulate task execution
543+ try std .testing .expectEqual (@as (u32 , 0 ), test_state .current_task_num );
544+
545+ test_state .current_task_num += 1 ;
546+ test_state .task_index += 1 ;
547+ try std .testing .expectEqual (@as (u32 , 1 ), test_state .current_task_num );
548+ try std .testing .expectEqual (@as (u32 , 1 ), test_state .task_index );
549+
550+ test_state .current_task_num += 1 ;
551+ test_state .task_index += 1 ;
552+ try std .testing .expectEqual (@as (u32 , 2 ), test_state .current_task_num );
553+ try std .testing .expectEqual (@as (u32 , 2 ), test_state .task_index );
554+ }
555+
556+ test "Loop cycle reset on new cycle" {
557+ const allocator = std .testing .allocator ;
558+
559+ const test_logger = try createTestLogger (allocator );
560+ defer destroyTestLogger (test_logger , allocator );
561+
562+ var test_paths = try createTestPaths (allocator );
563+ defer test_paths .deinit ();
564+
565+ const test_cfg = config.Config {
566+ .planning_model = "test/model" ,
567+ .execution_model = "test/model" ,
568+ .project_dir = "/tmp" ,
569+ .verbose = false ,
570+ .user_hint = null ,
571+ .max_retries = 3 ,
572+ .backoff_base = 1 ,
573+ .log_retention = 30 ,
574+ };
575+
576+ var test_state = state.State {
577+ .cycle = 5 ,
578+ .phase = .evaluation ,
579+ .task_index = 10 ,
580+ .current_task_num = 8 ,
581+ .total_tasks = 10 ,
582+ };
583+ defer test_state .deinit (allocator );
584+
585+ var test_executor = Executor .init (& test_cfg , test_logger , allocator );
586+ defer test_executor .deinit ();
587+
588+ _ = Loop .init (
589+ & test_cfg ,
590+ & test_state ,
591+ & test_paths ,
592+ test_logger ,
593+ & test_executor ,
594+ allocator ,
595+ );
596+
597+ // Simulate starting new cycle (as done in loop.zig:201-206)
598+ test_state .cycle += 1 ;
599+ test_state .phase = .planning ;
600+ test_state .task_index = 0 ;
601+ test_state .current_task_num = 0 ;
602+ test_state .total_tasks = 0 ;
603+
604+ // Verify reset
605+ try std .testing .expectEqual (@as (u32 , 6 ), test_state .cycle );
606+ try std .testing .expectEqual (state .Phase .planning , test_state .phase );
607+ try std .testing .expectEqual (@as (u32 , 0 ), test_state .task_index );
608+ try std .testing .expectEqual (@as (u32 , 0 ), test_state .current_task_num );
609+ try std .testing .expectEqual (@as (u32 , 0 ), test_state .total_tasks );
610+ }
0 commit comments