11import { beforeEach , describe , expect , test } from 'bun:test'
22import type { OpencodeClient } from '@opencode-ai/sdk'
33import type {
4+ ModePreset ,
45 ModeSwitcherConfig ,
56 OhMyOpencodeConfig ,
67 OpencodeConfig ,
78} from '../config/types.ts'
89import { createMockOpencodeClient , sampleConfigs } from '../test-utils/mocks.ts'
910
11+ /**
12+ * Creates a deep copy of the sample plugin config for isolated test use.
13+ */
14+ function clonePluginConfig ( ) : ModeSwitcherConfig {
15+ return JSON . parse (
16+ JSON . stringify ( sampleConfigs . pluginConfig )
17+ ) as ModeSwitcherConfig
18+ }
19+
1020/**
1121 * Mock implementation of ModeManager for testing purposes.
1222 *
@@ -23,6 +33,9 @@ class MockModeManager {
2333 private ohMyConfig : OhMyOpencodeConfig | null = null
2434 private client : OpencodeClient
2535
36+ /** Tracks whether a drift-toast was shown during initialize */
37+ lastDriftToast : string | null = null
38+
2639 constructor ( client : OpencodeClient ) {
2740 this . client = client
2841 }
@@ -41,11 +54,79 @@ class MockModeManager {
4154
4255 async initialize ( ) : Promise < void > {
4356 if ( ! this . config ) {
44- // Deep copy to avoid state sharing between tests
45- this . config = JSON . parse (
46- JSON . stringify ( sampleConfigs . pluginConfig )
47- ) as ModeSwitcherConfig
57+ this . config = clonePluginConfig ( )
58+ }
59+ await this . applyCurrentModeIfNeeded ( )
60+ }
61+
62+ private async applyCurrentModeIfNeeded ( ) : Promise < void > {
63+ if ( ! this . config ) {
64+ return
65+ }
66+
67+ const preset = this . config . presets [ this . config . currentMode ]
68+ if ( ! preset ) {
69+ return
4870 }
71+
72+ const drifted = this . hasConfigDrift ( preset )
73+ if ( ! drifted ) {
74+ return
75+ }
76+
77+ // Apply preset to in-memory configs
78+ if ( this . opencodeConfig ) {
79+ if ( preset . model ) {
80+ this . opencodeConfig . model = preset . model
81+ }
82+ this . opencodeConfig . agent = this . opencodeConfig . agent || { }
83+ for ( const [ name , p ] of Object . entries ( preset . opencode ) ) {
84+ this . opencodeConfig . agent [ name ] = {
85+ ...this . opencodeConfig . agent [ name ] ,
86+ model : p . model ,
87+ }
88+ }
89+ }
90+
91+ if ( this . ohMyConfig ) {
92+ this . ohMyConfig . agents = this . ohMyConfig . agents || { }
93+ for ( const [ name , p ] of Object . entries ( preset [ 'oh-my-opencode' ] ) ) {
94+ this . ohMyConfig . agents [ name ] = { model : p . model }
95+ }
96+ }
97+
98+ this . lastDriftToast = `Applied "${ this . config . currentMode } " mode. Restart opencode to take effect.`
99+ }
100+
101+ private hasConfigDrift ( preset : ModePreset ) : boolean {
102+ // Check global model
103+ if ( preset . model && this . opencodeConfig ) {
104+ if ( this . opencodeConfig . model !== preset . model ) {
105+ return true
106+ }
107+ }
108+
109+ // Check opencode agent models
110+ if ( this . opencodeConfig ?. agent ) {
111+ for ( const [ name , p ] of Object . entries ( preset . opencode ) ) {
112+ const actual = this . opencodeConfig . agent [ name ]
113+ if ( actual ?. model !== p . model ) {
114+ return true
115+ }
116+ }
117+ }
118+
119+ // Check oh-my-opencode agent models
120+ if ( this . ohMyConfig ?. agents ) {
121+ for ( const [ name , p ] of Object . entries ( preset [ 'oh-my-opencode' ] ) ) {
122+ const actual = this . ohMyConfig . agents [ name ]
123+ if ( actual ?. model !== p . model ) {
124+ return true
125+ }
126+ }
127+ }
128+
129+ return false
49130 }
50131
51132 private async ensureConfig ( ) : Promise < ModeSwitcherConfig > {
@@ -384,4 +465,114 @@ describe('ModeManager', () => {
384465 expect ( result ) . toBe ( false )
385466 } )
386467 } )
468+
469+ describe ( 'applyCurrentModeIfNeeded' , ( ) => {
470+ test ( 'applies preset when opencode.json has drifted' , async ( ) => {
471+ // Set currentMode to economy but opencode.json has performance models
472+ const config = clonePluginConfig ( )
473+ config . currentMode = 'economy'
474+ manager . setConfig ( config )
475+ manager . setOpencodeConfig ( {
476+ model : 'anthropic/claude-sonnet-4' ,
477+ agent : {
478+ build : { model : 'anthropic/claude-sonnet-4' } ,
479+ plan : { model : 'anthropic/claude-sonnet-4' } ,
480+ } ,
481+ } )
482+
483+ await manager . initialize ( )
484+
485+ expect ( manager . lastDriftToast ) . toContain ( 'economy' )
486+ expect ( manager . lastDriftToast ) . toContain ( 'Restart opencode' )
487+ } )
488+
489+ test ( 'applies preset when oh-my-opencode.json has drifted' , async ( ) => {
490+ const config = clonePluginConfig ( )
491+ config . currentMode = 'economy'
492+ manager . setConfig ( config )
493+ manager . setOhMyConfig ( {
494+ agents : {
495+ coder : { model : 'anthropic/claude-sonnet-4' } ,
496+ } ,
497+ } )
498+
499+ await manager . initialize ( )
500+
501+ expect ( manager . lastDriftToast ) . toContain ( 'economy' )
502+ } )
503+
504+ test ( 'does not apply when configs match preset' , async ( ) => {
505+ // Set currentMode to economy and configs already match
506+ const config = clonePluginConfig ( )
507+ config . currentMode = 'economy'
508+ manager . setConfig ( config )
509+ manager . setOpencodeConfig ( {
510+ model : 'opencode/glm-4.7-free' ,
511+ agent : {
512+ build : { model : 'opencode/glm-4.7-free' } ,
513+ plan : { model : 'opencode/glm-4.7-free' } ,
514+ } ,
515+ } )
516+ manager . setOhMyConfig ( {
517+ agents : {
518+ coder : { model : 'opencode/glm-4.7-free' } ,
519+ } ,
520+ } )
521+
522+ await manager . initialize ( )
523+
524+ expect ( manager . lastDriftToast ) . toBeNull ( )
525+ } )
526+
527+ test ( 'does nothing when preset is not found' , async ( ) => {
528+ const config : ModeSwitcherConfig = {
529+ currentMode : 'nonexistent' ,
530+ showToastOnStartup : true ,
531+ presets : {
532+ performance : {
533+ description : 'Test' ,
534+ opencode : { } ,
535+ 'oh-my-opencode' : { } ,
536+ } ,
537+ } ,
538+ }
539+ manager . setConfig ( config )
540+ manager . setOpencodeConfig ( {
541+ model : 'anthropic/claude-sonnet-4' ,
542+ agent : { build : { model : 'anthropic/claude-sonnet-4' } } ,
543+ } )
544+
545+ await manager . initialize ( )
546+
547+ expect ( manager . lastDriftToast ) . toBeNull ( )
548+ } )
549+
550+ test ( 'does nothing when no config files exist' , async ( ) => {
551+ const config = clonePluginConfig ( )
552+ config . currentMode = 'economy'
553+ manager . setConfig ( config )
554+ // Don't set opencodeConfig or ohMyConfig
555+
556+ await manager . initialize ( )
557+
558+ expect ( manager . lastDriftToast ) . toBeNull ( )
559+ } )
560+
561+ test ( 'detects drift on global model mismatch' , async ( ) => {
562+ const config = clonePluginConfig ( )
563+ config . currentMode = 'economy'
564+ manager . setConfig ( config )
565+ manager . setOpencodeConfig ( {
566+ model : 'anthropic/claude-sonnet-4' , // Mismatch
567+ agent : {
568+ build : { model : 'opencode/glm-4.7-free' } ,
569+ plan : { model : 'opencode/glm-4.7-free' } ,
570+ } ,
571+ } )
572+
573+ await manager . initialize ( )
574+
575+ expect ( manager . lastDriftToast ) . not . toBeNull ( )
576+ } )
577+ } )
387578} )
0 commit comments