11import { beforeEach , describe , expect , it , vi } from "vitest" ;
22
33const mocks = vi . hoisted ( ( ) => {
4+ const calls : string [ ] = [ ] ;
5+ const activeRunIds = [ "run-active" ] ;
6+ const fakeRecoverCrashedAgentRuns = Symbol ( "recoverCrashedAgentRuns" ) ;
47 const fakeApp = {
58 all : vi . fn ( ) ,
69 } ;
@@ -72,14 +75,21 @@ const mocks = vi.hoisted(() => {
7275 const fakeCleanupHandle = { stop : vi . fn ( ) } ;
7376 const fakePluginLoader = { kind : "plugin-loader" } ;
7477 const defaultPluginIds = [ "password-auth-provider" , "json-file-handler" ] ;
78+ const messageGatewayStart = vi . fn ( ) ;
79+ const serverError = vi . fn ( ) ;
80+ const serverInfo = vi . fn ( ) ;
81+ const serverWarn = vi . fn ( ) ;
7582
7683 return {
84+ activeRunIds,
7785 assertSearchRuntimeHealth : vi . fn ( ) . mockResolvedValue ( fakeDatabaseSummary ) ,
86+ calls,
7887 createAppPluginLoader : vi . fn ( ( ) => fakePluginLoader ) ,
7988 createDefaultGraphRuntime : vi . fn ( ) ,
8089 createRuntimeBackends : vi . fn ( ) . mockResolvedValue ( fakeBackends ) ,
8190 ensureDB : vi . fn ( ) . mockResolvedValue ( undefined ) ,
8291 ensureRootUser : vi . fn ( ) . mockResolvedValue ( undefined ) ,
92+ executeCommand : vi . fn ( ) ,
8393 executeQuery : vi . fn ( ) ,
8494 fakeApp,
8595 fakeBackends,
@@ -91,6 +101,7 @@ const mocks = vi.hoisted(() => {
91101 fakePluginLoader,
92102 fakePluginManager,
93103 fakeProfile,
104+ fakeRecoverCrashedAgentRuns,
94105 fakeRouteRegistry,
95106 getCurrentRedisHandle : vi . fn ( ) . mockReturnValue ( undefined ) ,
96107 getDbHandle : vi . fn ( ) . mockResolvedValue ( fakeDrizzleDB ) ,
@@ -99,6 +110,9 @@ const mocks = vi.hoisted(() => {
99110 appMethodRegistry : { id : "registry" } ,
100111 } ) ) ,
101112 getFirstRegisteredUser : Symbol ( "getFirstRegisteredUser" ) ,
113+ getGlobalGraphRuntimeOrNull : vi . fn ( ( ) => ( {
114+ scheduler : { getActiveRunIds : vi . fn ( ( ) => activeRunIds ) } ,
115+ } ) ) ,
102116 getSetting : Symbol ( "getSetting" ) ,
103117 grantFirstUserSuperadmin : vi . fn ( ) . mockResolvedValue ( undefined ) ,
104118 initCacheStore : vi . fn ( ) ,
@@ -107,23 +121,29 @@ const mocks = vi.hoisted(() => {
107121 initSessionStore : vi . fn ( ) ,
108122 initAllVectorStorage : vi . fn ( ) . mockResolvedValue ( undefined ) ,
109123 MessageGateway : class {
110- public start = vi . fn ( ) ;
124+ public start = messageGatewayStart ;
111125 } ,
126+ messageGatewayStart,
112127 pluginManagerGet : vi . fn ( ( ) => fakePluginManager ) ,
113128 pluginManagerClear : vi . fn ( ) ,
114129 pluginManagerInstallDefaults : vi . fn ( ) . mockResolvedValue ( undefined ) ,
115130 registerAuditHandler : vi . fn ( ) ,
116131 registerBuiltinAgents : vi . fn ( ) . mockResolvedValue ( undefined ) ,
117132 registerDomainEventHandlers : vi . fn ( ) ,
118133 registerVectorizationConsumer : vi . fn ( ) . mockResolvedValue ( undefined ) ,
134+ recoverCrashedAgentRuns : fakeRecoverCrashedAgentRuns ,
119135 resolveRuntimeProfile : vi . fn ( ( ) => fakeProfile ) ,
120136 seedSystemRoles : vi . fn ( ) . mockResolvedValue ( undefined ) ,
121137 serverLogger : {
122138 withSituation : ( ) => ( {
123- error : vi . fn ( ) ,
124- info : vi . fn ( ) ,
139+ error : serverError ,
140+ info : serverInfo ,
141+ warn : serverWarn ,
125142 } ) ,
126143 } ,
144+ serverError,
145+ serverInfo,
146+ serverWarn,
127147 setVectorizationQueue : vi . fn ( ) ,
128148 startPostgresRuntimeCleanup : vi . fn ( ( ) => fakeCleanupHandle ) ,
129149 wireEntityStateFetchers : vi . fn ( ) ,
@@ -144,6 +164,7 @@ vi.mock("@cat/db", () => ({
144164} ) ) ;
145165
146166vi . mock ( "@cat/domain" , ( ) => ( {
167+ executeCommand : mocks . executeCommand ,
147168 executeQuery : mocks . executeQuery ,
148169 getCurrentRedisHandle : mocks . getCurrentRedisHandle ,
149170 getDbHandle : mocks . getDbHandle ,
@@ -152,6 +173,7 @@ vi.mock("@cat/domain", () => ({
152173 initCacheStore : mocks . initCacheStore ,
153174 initRuntimeState : mocks . initRuntimeState ,
154175 initSessionStore : mocks . initSessionStore ,
176+ recoverCrashedAgentRuns : mocks . recoverCrashedAgentRuns ,
155177 resolveRuntimeProfile : mocks . resolveRuntimeProfile ,
156178} ) ) ;
157179
@@ -200,6 +222,7 @@ vi.mock("@cat/vcs", () => ({
200222
201223vi . mock ( "@cat/workflow" , ( ) => ( {
202224 createDefaultGraphRuntime : mocks . createDefaultGraphRuntime ,
225+ getGlobalGraphRuntimeOrNull : mocks . getGlobalGraphRuntimeOrNull ,
203226} ) ) ;
204227
205228vi . mock ( "./default-plugins/catalog" , ( ) => ( {
@@ -224,13 +247,31 @@ import { initializeApp } from "./initialize";
224247describe ( "initializeApp" , ( ) => {
225248 beforeEach ( ( ) => {
226249 vi . clearAllMocks ( ) ;
250+ mocks . calls . length = 0 ;
227251 mocks . executeQuery
228252 . mockResolvedValueOnce ( null )
229253 . mockResolvedValueOnce ( "CAT" )
230254 . mockResolvedValueOnce ( "http://localhost:3000/" ) ;
255+ mocks . executeCommand . mockImplementation ( async ( _ctx , command ) => {
256+ if ( command === mocks . recoverCrashedAgentRuns ) {
257+ mocks . calls . push ( "recover-runs" ) ;
258+ return { recoveredRunIds : [ "run-1" ] } ;
259+ }
260+ return undefined ;
261+ } ) ;
262+ mocks . registerDomainEventHandlers . mockImplementation ( ( ) => {
263+ mocks . calls . push ( "domain-handlers" ) ;
264+ } ) ;
265+ mocks . registerVectorizationConsumer . mockImplementation ( async ( ) => {
266+ mocks . calls . push ( "vectorization-consumer" ) ;
267+ } ) ;
268+ mocks . createDefaultGraphRuntime . mockImplementation ( ( ) => {
269+ mocks . calls . push ( "graph-runtime" ) ;
270+ } ) ;
231271 Reflect . deleteProperty ( globalThis , "app" ) ;
232272 globalThis . inited = false ;
233273 globalThis . redis = undefined ;
274+ Reflect . deleteProperty ( globalThis , "messageGateway" ) ;
234275 Reflect . deleteProperty ( globalThis , "pluginManager" ) ;
235276 globalThis . runtimeCleanup = undefined ;
236277 } ) ;
@@ -285,6 +326,21 @@ describe("initializeApp", () => {
285326 mocks . fakePluginManager ,
286327 mocks . getDefaultPluginIds ( ) ,
287328 ) ;
329+ expect ( mocks . calls ) . toEqual ( [
330+ "domain-handlers" ,
331+ "recover-runs" ,
332+ "vectorization-consumer" ,
333+ "graph-runtime" ,
334+ ] ) ;
335+ expect ( mocks . executeCommand ) . toHaveBeenCalledWith (
336+ { db : mocks . fakeDrizzleClient } ,
337+ mocks . recoverCrashedAgentRuns ,
338+ { activeRunIds : mocks . activeRunIds } ,
339+ ) ;
340+ expect ( mocks . serverWarn ) . toHaveBeenCalledWith (
341+ { recoveredRunIds : [ "run-1" ] } ,
342+ "Recovered crashed workflow runs" ,
343+ ) ;
288344 expect ( mocks . initAllVectorStorage ) . not . toHaveBeenCalled ( ) ;
289345 expect ( mocks . registerVectorizationConsumer ) . toHaveBeenCalledWith (
290346 mocks . fakeBackends . vectorizationQueue ,
@@ -299,4 +355,49 @@ describe("initializeApp", () => {
299355
300356 exitSpy . mockRestore ( ) ;
301357 } ) ;
358+
359+ it ( "exits before readiness when run recovery fails" , async ( ) => {
360+ const exitError = new Error ( "process.exit called" ) ;
361+ const exitSpy = vi
362+ . spyOn ( process , "exit" )
363+ . mockImplementation (
364+ ( _code ?: Parameters < typeof process . exit > [ 0 ] ) : never => {
365+ throw exitError ;
366+ } ,
367+ ) ;
368+ mocks . executeCommand . mockRejectedValueOnce ( new Error ( "db recovery failed" ) ) ;
369+
370+ await expect ( initializeApp ( ) ) . rejects . toBe ( exitError ) ;
371+
372+ expect ( exitSpy ) . toHaveBeenCalledWith ( 1 ) ;
373+ expect ( globalThis . inited ) . not . toBe ( true ) ;
374+ expect ( mocks . registerVectorizationConsumer ) . not . toHaveBeenCalled ( ) ;
375+ expect ( mocks . createDefaultGraphRuntime ) . not . toHaveBeenCalled ( ) ;
376+ expect ( mocks . messageGatewayStart ) . not . toHaveBeenCalled ( ) ;
377+
378+ exitSpy . mockRestore ( ) ;
379+ } ) ;
380+
381+ it ( "exits before readiness when queue recovery fails" , async ( ) => {
382+ const exitError = new Error ( "process.exit called" ) ;
383+ const exitSpy = vi
384+ . spyOn ( process , "exit" )
385+ . mockImplementation (
386+ ( _code ?: Parameters < typeof process . exit > [ 0 ] ) : never => {
387+ throw exitError ;
388+ } ,
389+ ) ;
390+ mocks . registerVectorizationConsumer . mockRejectedValueOnce (
391+ new Error ( "redis recovery failed" ) ,
392+ ) ;
393+
394+ await expect ( initializeApp ( ) ) . rejects . toBe ( exitError ) ;
395+
396+ expect ( exitSpy ) . toHaveBeenCalledWith ( 1 ) ;
397+ expect ( globalThis . inited ) . not . toBe ( true ) ;
398+ expect ( mocks . createDefaultGraphRuntime ) . not . toHaveBeenCalled ( ) ;
399+ expect ( mocks . messageGatewayStart ) . not . toHaveBeenCalled ( ) ;
400+
401+ exitSpy . mockRestore ( ) ;
402+ } ) ;
302403} ) ;
0 commit comments