@@ -9,13 +9,15 @@ import { Logger } from "../src/logger.ts"
99import {
1010 archivePlan ,
1111 calculateBackoff ,
12+ getCycleElapsedTime ,
13+ isCycleTimedOut ,
1214 isShutdownRequested ,
1315 logStartupInfo ,
1416 requestShutdown ,
1517 resetShutdownFlags ,
1618 sleep ,
1719} from "../src/loop.ts"
18- import type { Config , Paths } from "../src/types.ts"
20+ import type { Config , Paths , RuntimeState } from "../src/types.ts"
1921
2022const TEST_DIR = "/tmp/opencoder-test-loop"
2123
@@ -30,6 +32,7 @@ function createTestPaths(): Paths {
3032 alertsFile : join ( TEST_DIR , "alerts.log" ) ,
3133 historyDir : join ( TEST_DIR , "history" ) ,
3234 ideasDir : join ( TEST_DIR , "ideas" ) ,
35+ ideasHistoryDir : join ( TEST_DIR , "ideas" , "history" ) ,
3336 configFile : join ( TEST_DIR , "config.json" ) ,
3437 }
3538}
@@ -48,6 +51,22 @@ function createTestConfig(overrides?: Partial<Config>): Config {
4851 autoCommit : true ,
4952 autoPush : true ,
5053 commitSignoff : false ,
54+ cycleTimeoutMinutes : 60 ,
55+ ...overrides ,
56+ }
57+ }
58+
59+ /** Create a test RuntimeState object */
60+ function createTestState ( overrides ?: Partial < RuntimeState > ) : RuntimeState {
61+ return {
62+ cycle : 1 ,
63+ phase : "plan" ,
64+ taskIndex : 0 ,
65+ lastUpdate : new Date ( ) . toISOString ( ) ,
66+ retryCount : 0 ,
67+ totalTasks : 0 ,
68+ currentTaskNum : 0 ,
69+ currentTaskDesc : "" ,
5170 ...overrides ,
5271 }
5372}
@@ -458,4 +477,93 @@ Some notes with Unicode: 日本語 🎉`
458477 expect ( archivedContent ) . toBe ( planContent )
459478 } )
460479 } )
480+
481+ describe ( "isCycleTimedOut" , ( ) => {
482+ test ( "returns false when timeout is disabled (0)" , ( ) => {
483+ const state = createTestState ( {
484+ cycleStartTime : new Date ( Date . now ( ) - 120 * 60 * 1000 ) . toISOString ( ) , // 2 hours ago
485+ } )
486+ const config = createTestConfig ( { cycleTimeoutMinutes : 0 } )
487+
488+ expect ( isCycleTimedOut ( state , config ) ) . toBe ( false )
489+ } )
490+
491+ test ( "returns false when no start time is set" , ( ) => {
492+ const state = createTestState ( { cycleStartTime : undefined } )
493+ const config = createTestConfig ( { cycleTimeoutMinutes : 60 } )
494+
495+ expect ( isCycleTimedOut ( state , config ) ) . toBe ( false )
496+ } )
497+
498+ test ( "returns false when within timeout" , ( ) => {
499+ const state = createTestState ( {
500+ cycleStartTime : new Date ( Date . now ( ) - 30 * 60 * 1000 ) . toISOString ( ) , // 30 minutes ago
501+ } )
502+ const config = createTestConfig ( { cycleTimeoutMinutes : 60 } )
503+
504+ expect ( isCycleTimedOut ( state , config ) ) . toBe ( false )
505+ } )
506+
507+ test ( "returns true when timeout exceeded" , ( ) => {
508+ const state = createTestState ( {
509+ cycleStartTime : new Date ( Date . now ( ) - 65 * 60 * 1000 ) . toISOString ( ) , // 65 minutes ago
510+ } )
511+ const config = createTestConfig ( { cycleTimeoutMinutes : 60 } )
512+
513+ expect ( isCycleTimedOut ( state , config ) ) . toBe ( true )
514+ } )
515+
516+ test ( "returns true exactly at timeout" , ( ) => {
517+ const state = createTestState ( {
518+ cycleStartTime : new Date ( Date . now ( ) - 60 * 60 * 1000 ) . toISOString ( ) , // Exactly 60 minutes ago
519+ } )
520+ const config = createTestConfig ( { cycleTimeoutMinutes : 60 } )
521+
522+ expect ( isCycleTimedOut ( state , config ) ) . toBe ( true )
523+ } )
524+
525+ test ( "works with short timeout values" , ( ) => {
526+ const state = createTestState ( {
527+ cycleStartTime : new Date ( Date . now ( ) - 2 * 60 * 1000 ) . toISOString ( ) , // 2 minutes ago
528+ } )
529+ const config = createTestConfig ( { cycleTimeoutMinutes : 1 } )
530+
531+ expect ( isCycleTimedOut ( state , config ) ) . toBe ( true )
532+ } )
533+ } )
534+
535+ describe ( "getCycleElapsedTime" , ( ) => {
536+ test ( "returns empty string when no start time" , ( ) => {
537+ const state = createTestState ( { cycleStartTime : undefined } )
538+
539+ expect ( getCycleElapsedTime ( state ) ) . toBe ( "" )
540+ } )
541+
542+ test ( "returns seconds only for short durations" , ( ) => {
543+ const state = createTestState ( {
544+ cycleStartTime : new Date ( Date . now ( ) - 30 * 1000 ) . toISOString ( ) , // 30 seconds ago
545+ } )
546+
547+ const elapsed = getCycleElapsedTime ( state )
548+ expect ( elapsed ) . toMatch ( / ^ \d + s $ / )
549+ } )
550+
551+ test ( "returns minutes and seconds for longer durations" , ( ) => {
552+ const state = createTestState ( {
553+ cycleStartTime : new Date ( Date . now ( ) - ( 5 * 60 + 30 ) * 1000 ) . toISOString ( ) , // 5m 30s ago
554+ } )
555+
556+ const elapsed = getCycleElapsedTime ( state )
557+ expect ( elapsed ) . toMatch ( / ^ \d + m \d + s $ / )
558+ } )
559+
560+ test ( "handles hours worth of minutes" , ( ) => {
561+ const state = createTestState ( {
562+ cycleStartTime : new Date ( Date . now ( ) - 90 * 60 * 1000 ) . toISOString ( ) , // 90 minutes ago
563+ } )
564+
565+ const elapsed = getCycleElapsedTime ( state )
566+ expect ( elapsed ) . toContain ( "90m" )
567+ } )
568+ } )
461569} )
0 commit comments