11import { describe , expect , it } from "bun:test"
2- import type { AgentSideConnection , LoadSessionResponse , NewSessionResponse } from "@agentclientprotocol/sdk"
2+ import type {
3+ AgentSideConnection ,
4+ LoadSessionResponse ,
5+ NewSessionResponse ,
6+ SessionConfigOption ,
7+ SessionConfigSelectOption ,
8+ SetSessionConfigOptionResponse ,
9+ } from "@agentclientprotocol/sdk"
310import type { OpencodeClient } from "@opencode-ai/sdk/v2"
411import { Effect } from "effect"
512import * as ACPNextService from "@/acp-next/service"
@@ -10,6 +17,7 @@ import type { Provider } from "@/provider/provider"
1017const providerID = ProviderID . make ( "test" )
1118const modelID = ModelID . make ( "test-model" )
1219const configuredModelID = ModelID . make ( "configured-model" )
20+ const secondModelID = ModelID . make ( "second-model" )
1321
1422const provider : Provider . Info = {
1523 id : providerID ,
@@ -88,6 +96,43 @@ const provider: Provider.Info = {
8896 headers : { } ,
8997 release_date : "2026-01-01" ,
9098 } ,
99+ [ secondModelID ] : {
100+ id : secondModelID ,
101+ providerID,
102+ api : {
103+ id : secondModelID ,
104+ url : "https://example.com" ,
105+ npm : "@ai-sdk/openai-compatible" ,
106+ } ,
107+ name : "Second Model" ,
108+ family : "test" ,
109+ capabilities : {
110+ temperature : true ,
111+ reasoning : true ,
112+ attachment : false ,
113+ toolcall : true ,
114+ input : { text : true , audio : false , image : false , video : false , pdf : false } ,
115+ output : { text : true , audio : false , image : false , video : false , pdf : false } ,
116+ interleaved : false ,
117+ } ,
118+ cost : {
119+ input : 0 ,
120+ output : 0 ,
121+ cache : { read : 0 , write : 0 } ,
122+ } ,
123+ limit : {
124+ context : 128000 ,
125+ output : 4096 ,
126+ } ,
127+ status : "active" ,
128+ options : { } ,
129+ headers : { } ,
130+ release_date : "2026-01-01" ,
131+ variants : {
132+ low : { reasoningEffort : "low" } ,
133+ medium : { reasoningEffort : "medium" } ,
134+ } ,
135+ } ,
91136 } ,
92137}
93138
@@ -359,8 +404,131 @@ describe("ACP next service sessions", () => {
359404 expect ( result . sessionId ) . toBe ( "configured-model" )
360405 expect ( result . configOptions ?. find ( ( option ) => option . id === "model" ) ?. currentValue ) . toBe ( "test/configured-model" )
361406 } )
407+
408+ it ( "switches model and returns updated model and effort options" , async ( ) => {
409+ const { service } = makeService ( )
410+ const session = await Effect . runPromise ( service . newSession ( { cwd : "/workspace" , mcpServers : [ ] } ) )
411+ const updated = await Effect . runPromise (
412+ service . setSessionConfigOption ( {
413+ sessionId : session . sessionId ,
414+ configId : "model" ,
415+ value : "test/second-model" ,
416+ } ) ,
417+ )
418+
419+ expect ( select ( updated , "model" ) ?. currentValue ) . toBe ( "test/second-model" )
420+ expect ( select ( updated , "effort" ) ?. currentValue ) . toBe ( "low" )
421+ expect ( flattenSelectOptions ( select ( updated , "effort" ) ) . map ( ( option ) => option . value ) ) . toEqual ( [ "low" , "medium" ] )
422+ } )
423+
424+ it ( "switches effort and returns the updated effort current value" , async ( ) => {
425+ const { service } = makeService ( )
426+ const session = await Effect . runPromise ( service . newSession ( { cwd : "/workspace" , mcpServers : [ ] } ) )
427+ const updated = await Effect . runPromise (
428+ service . setSessionConfigOption ( {
429+ sessionId : session . sessionId ,
430+ configId : "effort" ,
431+ value : "high" ,
432+ } ) ,
433+ )
434+
435+ expect ( select ( updated , "effort" ) ?. currentValue ) . toBe ( "high" )
436+ } )
437+
438+ it ( "switches mode and returns the updated mode current value" , async ( ) => {
439+ const { service } = makeService ( )
440+ const session = await Effect . runPromise ( service . newSession ( { cwd : "/workspace" , mcpServers : [ ] } ) )
441+ const updated = await Effect . runPromise (
442+ service . setSessionConfigOption ( {
443+ sessionId : session . sessionId ,
444+ configId : "mode" ,
445+ value : "plan" ,
446+ } ) ,
447+ )
448+
449+ expect ( select ( updated , "mode" ) ?. currentValue ) . toBe ( "plan" )
450+ } )
451+
452+ it ( "maps invalid model effort mode and config id to invalid params" , async ( ) => {
453+ const { service } = makeService ( )
454+ const session = await Effect . runPromise ( service . newSession ( { cwd : "/workspace" , mcpServers : [ ] } ) )
455+
456+ const results = await Promise . all (
457+ [
458+ { configId : "model" , value : "test/missing-model" } ,
459+ { configId : "effort" , value : "max" } ,
460+ { configId : "mode" , value : "missing-mode" } ,
461+ { configId : "missing" , value : "value" } ,
462+ ] . map ( ( input ) =>
463+ Effect . runPromise (
464+ service
465+ . setSessionConfigOption ( { sessionId : session . sessionId , ...input } )
466+ . pipe ( Effect . mapError ( ACPNextError . toRequestError ) , Effect . flip ) ,
467+ ) ,
468+ ) ,
469+ )
470+ expect ( results . map ( ( error ) => error . code ) ) . toEqual ( [ - 32602 , - 32602 , - 32602 , - 32602 ] )
471+ } )
472+
473+ it ( "does not reload providers or commands when switching effort from a warm snapshot" , async ( ) => {
474+ let providersCalls = 0
475+ let commandCalls = 0
476+ const sdk = {
477+ config : {
478+ providers : ( ) => {
479+ providersCalls ++
480+ return Promise . resolve ( { data : { providers : [ provider ] , default : { test : modelID } } } )
481+ } ,
482+ get : ( ) => Promise . resolve ( { data : { } } ) ,
483+ } ,
484+ app : {
485+ agents : ( ) => Promise . resolve ( { data : [ { name : "build" , mode : "primary" , permission : [ ] , options : { } } ] } ) ,
486+ skills : ( ) => Promise . resolve ( { data : [ ] } ) ,
487+ } ,
488+ command : {
489+ list : ( ) => {
490+ commandCalls ++
491+ return Promise . resolve ( { data : [ ] } )
492+ } ,
493+ } ,
494+ session : {
495+ create : ( ) => Promise . resolve ( { data : { id : "ses_fast" } } ) ,
496+ list : ( ) => Promise . resolve ( { data : [ ] } ) ,
497+ } ,
498+ mcp : {
499+ add : ( ) => Promise . resolve ( { data : { } } ) ,
500+ } ,
501+ } as unknown as OpencodeClient
502+ const service = ACPNextService . make ( { sdk } )
503+ const session = await Effect . runPromise ( service . newSession ( { cwd : "/workspace" , mcpServers : [ ] } ) )
504+
505+ expect ( providersCalls ) . toBe ( 1 )
506+ expect ( commandCalls ) . toBe ( 1 )
507+
508+ await Effect . runPromise (
509+ service . setSessionConfigOption ( {
510+ sessionId : session . sessionId ,
511+ configId : "effort" ,
512+ value : "high" ,
513+ } ) ,
514+ )
515+
516+ expect ( providersCalls ) . toBe ( 1 )
517+ expect ( commandCalls ) . toBe ( 1 )
518+ } )
362519} )
363520
364521function categories ( result : NewSessionResponse | LoadSessionResponse ) {
365522 return result . configOptions ?. map ( ( option ) => option . category ) ?? [ ]
366523}
524+
525+ function select ( result : SetSessionConfigOptionResponse , id : string ) {
526+ return result . configOptions . find (
527+ ( option ) : option is Extract < SessionConfigOption , { type : "select" } > =>
528+ option . id === id && option . type === "select" ,
529+ )
530+ }
531+
532+ function flattenSelectOptions ( option : Extract < SessionConfigOption , { type : "select" } > | undefined ) {
533+ return option ?. options . flatMap ( ( item ) : SessionConfigSelectOption [ ] => ( "value" in item ? [ item ] : item . options ) ) ?? [ ]
534+ }
0 commit comments