@@ -2,14 +2,20 @@ import { describe, expect, it } from 'vitest';
22
33import { DisposableStore } from '#/_base/di/lifecycle' ;
44import { createServices } from '#/_base/di/test' ;
5- import type { AgentContext } from '#/agent/agentContext/agentContext' ;
5+ import { IAgentLoopService } from '#/agent/loop/loop' ;
6+ import { IAgentPromptService } from '#/agent/prompt/prompt' ;
7+ import type { DurableAgentRuntimeParticipant } from '#/agent/runtime/agentRuntime' ;
68import { AgentRuntimeSet } from '#/agent/runtime/agentRuntimeSet' ;
79import { IAgentToolRegistryService } from '#/agent/toolRegistry/toolRegistry' ;
810import { IConfigService } from '#/app/config/config' ;
11+ import { ITelemetryService } from '#/app/telemetry/telemetry' ;
912import { DEFAULT_CRON_CONFIG } from '#/features/cron/configSection' ;
1013import { AgentCron , cronAgentRuntimeProvider } from '#/features/cron/cronAgentRuntime' ;
11- import { CronCursor } from '#/features/cron/cronOps' ;
14+ import { CronCursor , type CronModelState } from '#/features/cron/cronOps' ;
15+ import { IEventDispatcher } from '#/state/eventDispatcher' ;
1216
17+ import { stubAgentContext } from '../../agent/agentContext/stubs' ;
18+ import { stubLoopWithHooks } from '../../agent/loop/stubs' ;
1319import {
1420 createTestAgent ,
1521 InMemoryWireRecordPersistence ,
@@ -28,48 +34,80 @@ async function bootCronContext(options: TestAgentOptions = {}): Promise<TestAgen
2834
2935describe ( 'session cron wire persistence' , ( ) => {
3036 it ( 'settles an in-flight tick without reading services after close' , async ( ) => {
31- let releaseReady ! : ( ) => void ;
32- const ready = new Promise < void > ( ( resolve ) => { releaseReady = resolve ; } ) ;
33- let markTickStarted ! : ( ) => void ;
34- const tickStarted = new Promise < void > ( ( resolve ) => { markTickStarted = resolve ; } ) ;
35- let readyReads = 0 ;
37+ let releaseInject ! : ( ) => void ;
38+ const injection = new Promise < undefined > ( ( resolve ) => { releaseInject = ( ) => { resolve ( undefined ) ; } ; } ) ;
39+ let markInjectStarted ! : ( ) => void ;
40+ const injectStarted = new Promise < void > ( ( resolve ) => { markInjectStarted = resolve ; } ) ;
3641 let closed = false ;
42+ let postCloseReads = 0 ;
43+ const recordRead = ( ) : void => {
44+ if ( closed ) postCloseReads += 1 ;
45+ } ;
3746 const disposables = new DisposableStore ( ) ;
3847 const services = createServices ( disposables , {
3948 additionalServices : ( reg ) => {
4049 reg . definePartialInstance ( IConfigService , {
41- get ready ( ) {
42- readyReads += 1 ;
43- if ( readyReads === 2 ) markTickStarted ( ) ;
44- return ready ;
45- } ,
50+ ready : Promise . resolve ( ) ,
4651 get : < T > ( ) => {
47- if ( closed ) throw new Error ( 'config read after close' ) ;
48- return DEFAULT_CRON_CONFIG as T ;
52+ recordRead ( ) ;
53+ return { ...DEFAULT_CRON_CONFIG , noJitter : true , manualTick : true } as T ;
54+ } ,
55+ } ) ;
56+ reg . defineInstance ( IAgentLoopService , stubLoopWithHooks ( ) ) ;
57+ reg . definePartialInstance ( IAgentPromptService , {
58+ inject : ( ) => {
59+ markInjectStarted ( ) ;
60+ return injection ;
4961 } ,
5062 } ) ;
63+ reg . definePartialInstance ( IEventDispatcher , {
64+ dispatch : async ( ) => { recordRead ( ) ; } ,
65+ } ) ;
66+ reg . definePartialInstance ( ITelemetryService , {
67+ track2 : ( ) => { recordRead ( ) ; } ,
68+ } ) ;
5169 } ,
5270 } ) ;
53- const agent = { agentId : 'main' , generation : 1 , space : { } } as AgentContext ;
71+ const agent = stubAgentContext ( 'main' ) ;
5472 const runtimes = new AgentRuntimeSet ( agent , services ) ;
5573 runtimes . apply ( {
5674 definition : AgentCron ,
5775 provider : cronAgentRuntimeProvider ,
5876 generation : 1 ,
5977 active : true ,
6078 } ) ;
61- runtimes . attachDurable ( { attach : ( ) => ( { dispose : ( ) => { } } ) } ) ;
62-
63- const restoring = runtimes . restore ( ) ;
64- const ticking = runtimes . resolve ( AgentCron ) . tick ( ) ;
65- await tickStarted ;
66- await runtimes . close ( ) ;
67- closed = true ;
68- disposables . dispose ( ) ;
69- releaseReady ( ) ;
70-
71- await expect ( ticking ) . resolves . toBeUndefined ( ) ;
72- await expect ( restoring ) . resolves . toBeUndefined ( ) ;
79+ let participant : DurableAgentRuntimeParticipant < CronModelState > | undefined ;
80+ runtimes . attachDurable ( {
81+ attach : ( attached ) => {
82+ participant = attached ;
83+ return { dispose : ( ) => { } } ;
84+ } ,
85+ } ) ;
86+
87+ try {
88+ await runtimes . restore ( ) ;
89+ if ( participant === undefined ) throw new Error ( 'Cron runtime was not attached' ) ;
90+ const now = Date . now ( ) ;
91+ participant . commit ( new Map ( [ [ 'deadbeef' , {
92+ id : 'deadbeef' ,
93+ cron : '* * * * *' ,
94+ prompt : 'fire after wait' ,
95+ recurring : true ,
96+ createdAt : now - 120_000 ,
97+ } ] ] ) ) ;
98+
99+ const ticking = runtimes . resolve ( AgentCron ) . tick ( ) ;
100+ await injectStarted ;
101+ await runtimes . close ( ) ;
102+ closed = true ;
103+ releaseInject ( ) ;
104+
105+ await expect ( ticking ) . resolves . toBeUndefined ( ) ;
106+ expect ( postCloseReads ) . toBe ( 0 ) ;
107+ } finally {
108+ await runtimes . close ( ) ;
109+ disposables . dispose ( ) ;
110+ }
73111 } ) ;
74112
75113 it ( 'writes cron ops as durable wire records and rebuilds the task table on replay' , async ( ) => {
0 commit comments