@@ -9,6 +9,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
99import { SyncDescriptor } from '#/_base/di/descriptors' ;
1010import { DisposableStore } from '#/_base/di/lifecycle' ;
1111import { TestInstantiationService } from '#/_base/di/test' ;
12+ import { resetUnexpectedErrorHandler , setUnexpectedErrorHandler } from '#/_base/errors/unexpectedError' ;
1213import { Event } from '#/_base/event' ;
1314import { IEventBus } from '#/app/event/eventBus' ;
1415import { EventBusService } from '#/app/event/eventBusService' ;
@@ -264,4 +265,152 @@ describe('AgentGoalService (wire-backed)', () => {
264265 } ) ,
265266 ] ) ;
266267 } ) ;
268+
269+ it ( 'restores goal records with omitted optional fields from older journals' , async ( ) => {
270+ await restoreTestAgentWire ( wire , log , testWireScope ( SCOPE , KEY ) , [
271+ { type : 'goal.create' , goalId : 'goal-1' , objective : 'work' } ,
272+ { type : 'goal.update' } ,
273+ ] ) ;
274+
275+ expect ( modelOf ( wire ) ) . toMatchObject ( {
276+ goalId : 'goal-1' ,
277+ status : 'paused' ,
278+ budgetLimits : { } ,
279+ } ) ;
280+ } ) ;
281+
282+ it ( 'restores legacy goal create audit fields without changing normalized state' , async ( ) => {
283+ await restoreTestAgentWire ( wire , log , testWireScope ( SCOPE , KEY ) , [
284+ {
285+ type : 'goal.create' ,
286+ goalId : 'goal-1' ,
287+ objective : 'work' ,
288+ status : 'active' ,
289+ actor : 'user' ,
290+ budgetLimits : { } ,
291+ } ,
292+ ] ) ;
293+
294+ expect ( modelOf ( wire ) ) . toMatchObject ( {
295+ goalId : 'goal-1' ,
296+ status : 'paused' ,
297+ budgetLimits : { } ,
298+ } ) ;
299+ } ) ;
300+
301+ it ( 'restores a legacy goal update identity without changing state selection' , async ( ) => {
302+ await restoreTestAgentWire ( wire , log , testWireScope ( SCOPE , KEY ) , [
303+ { type : 'goal.create' , goalId : 'goal-1' , objective : 'work' } ,
304+ { type : 'goal.update' , goalId : 'goal-1' , status : 'blocked' , reason : 'waiting' } ,
305+ ] ) ;
306+
307+ expect ( modelOf ( wire ) ) . toMatchObject ( {
308+ goalId : 'goal-1' ,
309+ status : 'blocked' ,
310+ terminalReason : 'waiting' ,
311+ } ) ;
312+ } ) ;
313+
314+ it ( 'strips forward-compatible goal fields during restore' , async ( ) => {
315+ await restoreTestAgentWire ( wire , log , testWireScope ( SCOPE , KEY ) , [
316+ {
317+ type : 'goal.create' ,
318+ goalId : 'goal-1' ,
319+ objective : 'work' ,
320+ futureField : true ,
321+ } ,
322+ ] ) ;
323+
324+ expect ( modelOf ( wire ) ) . toMatchObject ( { goalId : 'goal-1' , objective : 'work' } ) ;
325+ } ) ;
326+
327+ it ( 'skips a goal update with an invalid status during restore' , async ( ) => {
328+ const unexpected : unknown [ ] = [ ] ;
329+ setUnexpectedErrorHandler ( ( error ) => unexpected . push ( error ) ) ;
330+ try {
331+ await restoreTestAgentWire ( wire , log , testWireScope ( SCOPE , KEY ) , [
332+ { type : 'goal.create' , goalId : 'goal-1' , objective : 'work' } ,
333+ { type : 'goal.update' , status : 'cancelled' } ,
334+ ] ) ;
335+
336+ expect ( modelOf ( wire ) ) . toMatchObject ( { status : 'paused' } ) ;
337+ expect ( unexpected ) . toContainEqual (
338+ expect . objectContaining ( { code : 'wire.unknown_record' , details : { type : 'goal.update' , index : 1 } } ) ,
339+ ) ;
340+ } finally {
341+ resetUnexpectedErrorHandler ( ) ;
342+ }
343+ } ) ;
344+
345+ it ( 'skips a goal update with an invalid actor during restore' , async ( ) => {
346+ const unexpected : unknown [ ] = [ ] ;
347+ setUnexpectedErrorHandler ( ( error ) => unexpected . push ( error ) ) ;
348+ try {
349+ await restoreTestAgentWire ( wire , log , testWireScope ( SCOPE , KEY ) , [
350+ { type : 'goal.create' , goalId : 'goal-1' , objective : 'work' } ,
351+ { type : 'goal.update' , actor : 'assistant' } ,
352+ ] ) ;
353+
354+ expect ( modelOf ( wire ) ) . toMatchObject ( { status : 'paused' } ) ;
355+ expect ( unexpected ) . toContainEqual (
356+ expect . objectContaining ( { code : 'wire.unknown_record' , details : { type : 'goal.update' , index : 1 } } ) ,
357+ ) ;
358+ } finally {
359+ resetUnexpectedErrorHandler ( ) ;
360+ }
361+ } ) ;
362+
363+ it ( 'skips negative and non-finite goal counters and budgets during restore' , async ( ) => {
364+ const unexpected : unknown [ ] = [ ] ;
365+ setUnexpectedErrorHandler ( ( error ) => unexpected . push ( error ) ) ;
366+ try {
367+ await restoreTestAgentWire ( wire , log , testWireScope ( SCOPE , KEY ) , [
368+ { type : 'goal.create' , goalId : 'goal-1' , objective : 'work' } ,
369+ { type : 'goal.update' , turnsUsed : - 1 } ,
370+ { type : 'goal.update' , tokensUsed : Number . POSITIVE_INFINITY } ,
371+ { type : 'goal.update' , wallClockMs : Number . NaN } ,
372+ { type : 'goal.update' , wallClockResumedAt : Number . NaN } ,
373+ { type : 'goal.update' , budgetLimits : { turnBudget : - 1 } } ,
374+ { type : 'goal.update' , budgetLimits : { tokenBudget : Number . POSITIVE_INFINITY } } ,
375+ { type : 'goal.update' , budgetLimits : { wallClockBudgetMs : Number . NaN } } ,
376+ ] ) ;
377+
378+ expect ( modelOf ( wire ) ) . toMatchObject ( {
379+ turnsUsed : 0 ,
380+ tokensUsed : 0 ,
381+ wallClockMs : 0 ,
382+ budgetLimits : { } ,
383+ } ) ;
384+ expect ( unexpected ) . toHaveLength ( 7 ) ;
385+ } finally {
386+ resetUnexpectedErrorHandler ( ) ;
387+ }
388+ } ) ;
389+
390+ it ( 'skips null, arrays, and malformed nested goal records during restore' , async ( ) => {
391+ const unexpected : unknown [ ] = [ ] ;
392+ setUnexpectedErrorHandler ( ( error ) => unexpected . push ( error ) ) ;
393+ try {
394+ await restoreTestAgentWire (
395+ wire ,
396+ log ,
397+ testWireScope ( SCOPE , KEY ) ,
398+ [
399+ null ,
400+ [ ] ,
401+ {
402+ type : 'goal.create' ,
403+ goalId : 'goal-1' ,
404+ objective : 'work' ,
405+ budgetLimits : { unexpected : true } ,
406+ } ,
407+ ] as unknown as WireRecord [ ] ,
408+ ) ;
409+
410+ expect ( modelOf ( wire ) ) . toBeNull ( ) ;
411+ expect ( unexpected ) . toHaveLength ( 3 ) ;
412+ } finally {
413+ resetUnexpectedErrorHandler ( ) ;
414+ }
415+ } ) ;
267416} ) ;
0 commit comments