@@ -157,4 +157,95 @@ describe('openclawCronLegacyMigration', () => {
157157
158158 expect ( result ) . toEqual ( { status : 'failed' , code : 2 } ) ;
159159 } ) ;
160+
161+ test ( 'archives legacy cron files after successful doctor run' , async ( ) => {
162+ const storePath = resolveLegacyCronStorePath ( stateDir ) ;
163+ const statePath = path . join ( stateDir , 'cron' , 'jobs-state.json' ) ;
164+ const runLogPath = path . join ( stateDir , 'cron' , 'runs' , 'job-1.jsonl' ) ;
165+ writeFile ( storePath , '{"version":1,"jobs":[{"id":"job-1"}]}\n' ) ;
166+ writeFile ( statePath , '{"job-1":{}}\n' ) ;
167+ writeFile ( runLogPath , '{"status":"ok"}\n' ) ;
168+ const runner = vi . fn < LegacyCronMigrationRunner > ( ) . mockResolvedValue ( {
169+ code : 0 ,
170+ stdout : '' ,
171+ stderr : '' ,
172+ } ) ;
173+
174+ const result = await migrateLegacyCronStorageWithDoctor ( {
175+ stateDir,
176+ runtimeRoot,
177+ electronNodeRuntimePath,
178+ env : { } ,
179+ runner,
180+ } ) ;
181+
182+ expect ( result ) . toEqual ( { status : 'migrated' , code : 0 } ) ;
183+ expect ( fs . existsSync ( storePath ) ) . toBe ( false ) ;
184+ expect ( fs . existsSync ( statePath ) ) . toBe ( false ) ;
185+ expect ( fs . existsSync ( runLogPath ) ) . toBe ( false ) ;
186+ expect ( fs . readFileSync ( `${ storePath } .migrated` , 'utf8' ) ) . toBe ( '{"version":1,"jobs":[{"id":"job-1"}]}\n' ) ;
187+ expect ( fs . readFileSync ( `${ statePath } .migrated` , 'utf8' ) ) . toBe ( '{"job-1":{}}\n' ) ;
188+ expect ( fs . readFileSync ( `${ runLogPath } .migrated` , 'utf8' ) ) . toBe ( '{"status":"ok"}\n' ) ;
189+ expect ( hasLegacyCronStorage ( stateDir ) ) . toBe ( false ) ;
190+
191+ const rerun = await migrateLegacyCronStorageWithDoctor ( {
192+ stateDir,
193+ runtimeRoot,
194+ electronNodeRuntimePath,
195+ env : { } ,
196+ runner,
197+ } ) ;
198+
199+ expect ( rerun ) . toEqual ( { status : 'skipped' , reason : 'no-legacy-cron-files' } ) ;
200+ expect ( runner ) . toHaveBeenCalledTimes ( 1 ) ;
201+ } ) ;
202+
203+ test ( 'keeps legacy cron files when doctor fails' , async ( ) => {
204+ const storePath = resolveLegacyCronStorePath ( stateDir ) ;
205+ writeFile ( storePath , '{"version":1,"jobs":[]}\n' ) ;
206+ const runner = vi . fn < LegacyCronMigrationRunner > ( ) . mockResolvedValue ( {
207+ code : 2 ,
208+ stdout : '' ,
209+ stderr : 'failed' ,
210+ } ) ;
211+
212+ await migrateLegacyCronStorageWithDoctor ( {
213+ stateDir,
214+ runtimeRoot,
215+ electronNodeRuntimePath,
216+ env : { } ,
217+ runner,
218+ } ) ;
219+
220+ expect ( fs . existsSync ( storePath ) ) . toBe ( true ) ;
221+ expect ( hasLegacyCronStorage ( stateDir ) ) . toBe ( true ) ;
222+ } ) ;
223+
224+ test ( 'does not clobber an existing archive from a previous migration' , async ( ) => {
225+ const storePath = resolveLegacyCronStorePath ( stateDir ) ;
226+ writeFile ( `${ storePath } .migrated` , 'earlier archive\n' ) ;
227+ writeFile ( storePath , '{"version":1,"jobs":[{"id":"job-2"}]}\n' ) ;
228+ const runner = vi . fn < LegacyCronMigrationRunner > ( ) . mockResolvedValue ( {
229+ code : 0 ,
230+ stdout : '' ,
231+ stderr : '' ,
232+ } ) ;
233+
234+ const result = await migrateLegacyCronStorageWithDoctor ( {
235+ stateDir,
236+ runtimeRoot,
237+ electronNodeRuntimePath,
238+ env : { } ,
239+ runner,
240+ } ) ;
241+
242+ expect ( result ) . toEqual ( { status : 'migrated' , code : 0 } ) ;
243+ expect ( fs . existsSync ( storePath ) ) . toBe ( false ) ;
244+ expect ( fs . readFileSync ( `${ storePath } .migrated` , 'utf8' ) ) . toBe ( 'earlier archive\n' ) ;
245+ const fallbackArchives = fs . readdirSync ( path . join ( stateDir , 'cron' ) )
246+ . filter ( ( name ) => name . startsWith ( 'jobs.json.migrated-' ) ) ;
247+ expect ( fallbackArchives ) . toHaveLength ( 1 ) ;
248+ expect ( fs . readFileSync ( path . join ( stateDir , 'cron' , fallbackArchives [ 0 ] ) , 'utf8' ) )
249+ . toBe ( '{"version":1,"jobs":[{"id":"job-2"}]}\n' ) ;
250+ } ) ;
160251} ) ;
0 commit comments