@@ -9,6 +9,7 @@ const mockUpdateOpenCodeConfig = vi.fn()
99const mockDeleteOpenCodeConfig = vi . fn ( )
1010const mockGetOpenCodeConfigByName = vi . fn ( )
1111const mockSetDefaultOpenCodeConfig = vi . fn ( )
12+ const mockGetDefaultOpenCodeConfig = vi . fn ( )
1213
1314vi . mock ( 'fs' , ( ) => ( {
1415 existsSync : vi . fn ( ( ) => false ) ,
@@ -53,6 +54,7 @@ vi.mock('../../src/services/settings', () => ({
5354 deleteOpenCodeConfig : mockDeleteOpenCodeConfig ,
5455 getOpenCodeConfigByName : mockGetOpenCodeConfigByName ,
5556 setDefaultOpenCodeConfig : mockSetDefaultOpenCodeConfig ,
57+ getDefaultOpenCodeConfig : mockGetDefaultOpenCodeConfig ,
5658 } ) ) ,
5759} ) )
5860
@@ -188,6 +190,7 @@ describe('Settings Routes - OpenCode Upgrade', () => {
188190 mockRelinkReposFromSessionDirectories . mockReset ( )
189191 mockWriteFileContent . mockReset ( )
190192 mockPatchOpenCodeConfig . mockReset ( )
193+ mockGetDefaultOpenCodeConfig . mockReset ( )
191194
192195 testDb = { } as any
193196 settingsApp = createSettingsRoutes ( testDb , { getGitEnvironment : vi . fn ( ) . mockReturnValue ( { } ) } as any )
@@ -215,6 +218,7 @@ describe('Settings Routes - OpenCode Upgrade', () => {
215218 duplicatePathCount : 0 ,
216219 errors : [ ] ,
217220 } )
221+ mockGetDefaultOpenCodeConfig . mockReturnValue ( null )
218222 } )
219223
220224 describe ( 'OpenCode config routes' , ( ) => {
@@ -410,6 +414,306 @@ describe('Settings Routes - OpenCode Upgrade', () => {
410414 )
411415 expect ( json . removedFields ) . toEqual ( [ 'command.review' ] )
412416 } )
417+
418+ it ( 'should restart when agents changed on set-default' , async ( ) => {
419+ mockGetDefaultOpenCodeConfig . mockReturnValue ( {
420+ id : 1 ,
421+ name : 'old-default' ,
422+ content : { agent : { primary : { model : 'a' } } } ,
423+ rawContent : '{"agent":{"primary":{"model":"a"}}}' ,
424+ isValid : true ,
425+ isDefault : true ,
426+ createdAt : 1 ,
427+ updatedAt : 1 ,
428+ } )
429+ mockGetOpenCodeConfigByName . mockReturnValue ( {
430+ id : 2 ,
431+ name : 'new-config' ,
432+ content : { agent : { primary : { model : 'b' } } } ,
433+ rawContent : '{"agent":{"primary":{"model":"b"}}}' ,
434+ isValid : true ,
435+ isDefault : false ,
436+ createdAt : 2 ,
437+ updatedAt : 2 ,
438+ } )
439+ mockPatchOpenCodeConfig . mockResolvedValueOnce ( {
440+ success : true ,
441+ appliedConfig : { agent : { primary : { model : 'b' } } } ,
442+ } )
443+ mockUpdateOpenCodeConfig . mockReturnValue ( {
444+ id : 2 ,
445+ name : 'new-config' ,
446+ content : { agent : { primary : { model : 'b' } } } ,
447+ rawContent : '{"agent":{"primary":{"model":"b"}}}' ,
448+ isValid : true ,
449+ isDefault : false ,
450+ createdAt : 2 ,
451+ updatedAt : 3 ,
452+ } )
453+ mockSetDefaultOpenCodeConfig . mockReturnValue ( {
454+ id : 2 ,
455+ name : 'new-config' ,
456+ content : { agent : { primary : { model : 'b' } } } ,
457+ rawContent : '{"agent":{"primary":{"model":"b"}}}' ,
458+ isValid : true ,
459+ isDefault : true ,
460+ createdAt : 2 ,
461+ updatedAt : 4 ,
462+ } )
463+
464+ const req = new Request ( 'http://localhost/opencode-configs/new-config/set-default' , {
465+ method : 'POST' ,
466+ } )
467+ const res = await settingsApp . fetch ( req )
468+ const json = await res . json ( ) as Record < string , unknown >
469+
470+ expect ( res . status ) . toBe ( 200 )
471+ expect ( mockRestart ) . toHaveBeenCalledTimes ( 1 )
472+ expect ( mockReloadConfig ) . not . toHaveBeenCalled ( )
473+ expect ( json . restarted ) . toBe ( true )
474+ expect ( json . reloaded ) . toBe ( false )
475+ expect ( json . reloadError ) . toBeUndefined ( )
476+ } )
477+
478+ it ( 'should reload when plugins/skills/providers changed on set-default' , async ( ) => {
479+ mockGetDefaultOpenCodeConfig . mockReturnValue ( {
480+ id : 1 ,
481+ name : 'old-default' ,
482+ content : { plugin : [ 'old' ] } ,
483+ rawContent : '{"plugin":["old"]}' ,
484+ isValid : true ,
485+ isDefault : true ,
486+ createdAt : 1 ,
487+ updatedAt : 1 ,
488+ } )
489+ mockGetOpenCodeConfigByName . mockReturnValue ( {
490+ id : 2 ,
491+ name : 'new-config' ,
492+ content : { plugin : [ 'old' , 'new' ] } ,
493+ rawContent : '{"plugin":["old","new"]}' ,
494+ isValid : true ,
495+ isDefault : false ,
496+ createdAt : 2 ,
497+ updatedAt : 2 ,
498+ } )
499+ mockPatchOpenCodeConfig . mockResolvedValueOnce ( {
500+ success : true ,
501+ appliedConfig : { plugin : [ 'old' , 'new' ] } ,
502+ } )
503+ mockUpdateOpenCodeConfig . mockReturnValue ( {
504+ id : 2 ,
505+ name : 'new-config' ,
506+ content : { plugin : [ 'old' , 'new' ] } ,
507+ rawContent : '{"plugin":["old","new"]}' ,
508+ isValid : true ,
509+ isDefault : false ,
510+ createdAt : 2 ,
511+ updatedAt : 3 ,
512+ } )
513+ mockSetDefaultOpenCodeConfig . mockReturnValue ( {
514+ id : 2 ,
515+ name : 'new-config' ,
516+ content : { plugin : [ 'old' , 'new' ] } ,
517+ rawContent : '{"plugin":["old","new"]}' ,
518+ isValid : true ,
519+ isDefault : true ,
520+ createdAt : 2 ,
521+ updatedAt : 4 ,
522+ } )
523+
524+ const req = new Request ( 'http://localhost/opencode-configs/new-config/set-default' , {
525+ method : 'POST' ,
526+ } )
527+ const res = await settingsApp . fetch ( req )
528+ const json = await res . json ( ) as Record < string , unknown >
529+
530+ expect ( res . status ) . toBe ( 200 )
531+ expect ( mockReloadConfig ) . toHaveBeenCalledTimes ( 1 )
532+ expect ( mockRestart ) . not . toHaveBeenCalled ( )
533+ expect ( json . reloaded ) . toBe ( true )
534+ expect ( json . restarted ) . toBe ( false )
535+ } )
536+
537+ it ( 'should not reload or restart when no runtime fields changed' , async ( ) => {
538+ mockGetDefaultOpenCodeConfig . mockReturnValue ( {
539+ id : 1 ,
540+ name : 'old-default' ,
541+ content : { theme : 'dark' , agent : { } } ,
542+ rawContent : '{"theme":"dark","agent":{}}' ,
543+ isValid : true ,
544+ isDefault : true ,
545+ createdAt : 1 ,
546+ updatedAt : 1 ,
547+ } )
548+ mockGetOpenCodeConfigByName . mockReturnValue ( {
549+ id : 2 ,
550+ name : 'new-config' ,
551+ content : { theme : 'light' , agent : { } } ,
552+ rawContent : '{"theme":"light","agent":{}}' ,
553+ isValid : true ,
554+ isDefault : false ,
555+ createdAt : 2 ,
556+ updatedAt : 2 ,
557+ } )
558+ mockPatchOpenCodeConfig . mockResolvedValueOnce ( {
559+ success : true ,
560+ appliedConfig : { theme : 'light' , agent : { } } ,
561+ } )
562+ mockUpdateOpenCodeConfig . mockReturnValue ( {
563+ id : 2 ,
564+ name : 'new-config' ,
565+ content : { theme : 'light' , agent : { } } ,
566+ rawContent : '{"theme":"light","agent":{}}' ,
567+ isValid : true ,
568+ isDefault : false ,
569+ createdAt : 2 ,
570+ updatedAt : 3 ,
571+ } )
572+ mockSetDefaultOpenCodeConfig . mockReturnValue ( {
573+ id : 2 ,
574+ name : 'new-config' ,
575+ content : { theme : 'light' , agent : { } } ,
576+ rawContent : '{"theme":"light","agent":{}}' ,
577+ isValid : true ,
578+ isDefault : true ,
579+ createdAt : 2 ,
580+ updatedAt : 4 ,
581+ } )
582+
583+ const req = new Request ( 'http://localhost/opencode-configs/new-config/set-default' , {
584+ method : 'POST' ,
585+ } )
586+ const res = await settingsApp . fetch ( req )
587+ const json = await res . json ( ) as Record < string , unknown >
588+
589+ expect ( res . status ) . toBe ( 200 )
590+ expect ( mockRestart ) . not . toHaveBeenCalled ( )
591+ expect ( mockReloadConfig ) . not . toHaveBeenCalled ( )
592+ expect ( json . restarted ) . toBe ( false )
593+ expect ( json . reloaded ) . toBe ( false )
594+ } )
595+
596+ it ( 'should return reloadError in response when restart fails' , async ( ) => {
597+ mockGetDefaultOpenCodeConfig . mockReturnValue ( {
598+ id : 1 ,
599+ name : 'old-default' ,
600+ content : { agent : { primary : { model : 'a' } } } ,
601+ rawContent : '{"agent":{"primary":{"model":"a"}}}' ,
602+ isValid : true ,
603+ isDefault : true ,
604+ createdAt : 1 ,
605+ updatedAt : 1 ,
606+ } )
607+ mockGetOpenCodeConfigByName . mockReturnValue ( {
608+ id : 2 ,
609+ name : 'new-config' ,
610+ content : { agent : { primary : { model : 'b' } } } ,
611+ rawContent : '{"agent":{"primary":{"model":"b"}}}' ,
612+ isValid : true ,
613+ isDefault : false ,
614+ createdAt : 2 ,
615+ updatedAt : 2 ,
616+ } )
617+ mockPatchOpenCodeConfig . mockResolvedValueOnce ( {
618+ success : true ,
619+ appliedConfig : { agent : { primary : { model : 'b' } } } ,
620+ } )
621+ mockUpdateOpenCodeConfig . mockReturnValue ( {
622+ id : 2 ,
623+ name : 'new-config' ,
624+ content : { agent : { primary : { model : 'b' } } } ,
625+ rawContent : '{"agent":{"primary":{"model":"b"}}}' ,
626+ isValid : true ,
627+ isDefault : false ,
628+ createdAt : 2 ,
629+ updatedAt : 3 ,
630+ } )
631+ mockSetDefaultOpenCodeConfig . mockReturnValue ( {
632+ id : 2 ,
633+ name : 'new-config' ,
634+ content : { agent : { primary : { model : 'b' } } } ,
635+ rawContent : '{"agent":{"primary":{"model":"b"}}}' ,
636+ isValid : true ,
637+ isDefault : true ,
638+ createdAt : 2 ,
639+ updatedAt : 4 ,
640+ } )
641+ mockRestart . mockRejectedValueOnce ( new Error ( 'boom' ) )
642+
643+ const req = new Request ( 'http://localhost/opencode-configs/new-config/set-default' , {
644+ method : 'POST' ,
645+ } )
646+ const res = await settingsApp . fetch ( req )
647+ const json = await res . json ( ) as Record < string , unknown >
648+
649+ expect ( res . status ) . toBe ( 200 )
650+ expect ( mockRestart ) . toHaveBeenCalledTimes ( 1 )
651+ expect ( json . reloadError ) . toBe ( 'boom' )
652+ expect ( json . restarted ) . toBe ( false )
653+ expect ( json . id ) . toBeDefined ( )
654+ expect ( json . name ) . toBe ( 'new-config' )
655+ expect ( json . isDefault ) . toBe ( true )
656+ } )
657+
658+ it ( 'should preserve removedFields alongside new flags' , async ( ) => {
659+ mockGetDefaultOpenCodeConfig . mockReturnValue ( {
660+ id : 1 ,
661+ name : 'old-default' ,
662+ content : { plugin : [ 'old' ] } ,
663+ rawContent : '{"plugin":["old"]}' ,
664+ isValid : true ,
665+ isDefault : true ,
666+ createdAt : 1 ,
667+ updatedAt : 1 ,
668+ } )
669+ mockGetOpenCodeConfigByName . mockReturnValue ( {
670+ id : 2 ,
671+ name : 'new-config' ,
672+ content : { plugin : [ 'old' , 'new' ] , command : { review : true } } ,
673+ rawContent : '{"plugin":["old","new"],"command":{"review":true}}' ,
674+ isValid : true ,
675+ isDefault : false ,
676+ createdAt : 2 ,
677+ updatedAt : 2 ,
678+ } )
679+ mockPatchOpenCodeConfig . mockResolvedValueOnce ( {
680+ success : true ,
681+ appliedConfig : { plugin : [ 'old' , 'new' ] } ,
682+ removedFields : [ 'command.review' ] ,
683+ details : [ { path : 'command.review' , message : 'Invalid field' } ] ,
684+ } )
685+ mockUpdateOpenCodeConfig . mockReturnValue ( {
686+ id : 2 ,
687+ name : 'new-config' ,
688+ content : { plugin : [ 'old' , 'new' ] } ,
689+ rawContent : '{"plugin":["old","new"]}' ,
690+ isValid : true ,
691+ isDefault : false ,
692+ createdAt : 2 ,
693+ updatedAt : 3 ,
694+ } )
695+ mockSetDefaultOpenCodeConfig . mockReturnValue ( {
696+ id : 2 ,
697+ name : 'new-config' ,
698+ content : { plugin : [ 'old' , 'new' ] } ,
699+ rawContent : '{"plugin":["old","new"]}' ,
700+ isValid : true ,
701+ isDefault : true ,
702+ createdAt : 2 ,
703+ updatedAt : 4 ,
704+ } )
705+
706+ const req = new Request ( 'http://localhost/opencode-configs/new-config/set-default' , {
707+ method : 'POST' ,
708+ } )
709+ const res = await settingsApp . fetch ( req )
710+ const json = await res . json ( ) as Record < string , unknown >
711+
712+ expect ( res . status ) . toBe ( 200 )
713+ expect ( mockReloadConfig ) . toHaveBeenCalledTimes ( 1 )
714+ expect ( json . reloaded ) . toBe ( true )
715+ expect ( json . removedFields ) . toEqual ( [ 'command.review' ] )
716+ } )
413717 } )
414718
415719 describe ( 'OpenCode import routes' , ( ) => {
0 commit comments