@@ -19,6 +19,7 @@ const {
1919 subagentModelChoices,
2020 subagentModelOrphanPin,
2121 subagentModelPin,
22+ subagentModelPinParts,
2223 subagentModelSelectValue,
2324} = await import ( "../src/components/settings/subagent-models.ts" ) ;
2425
@@ -55,6 +56,46 @@ test("a custom endpoint uses its display name instead of the generic vendor key"
5556 subagentModelPin ( { vendorKey : "custom" , name : "My Gateway" } , "local-model" ) ,
5657 "My Gateway/local-model" ,
5758 ) ;
59+
60+ test ( "every option the picker offers is a pin the editor will save" , ( ) => {
61+ // A custom endpoint's display name contains spaces, and the picker offers it.
62+ // The editor's draft check must accept exactly what the picker produces, or
63+ // the user can pick an option and then never save the sheet.
64+ const choices = subagentModelChoices ( [
65+ provider ( ) ,
66+ provider ( { id : "p2" , name : "My Gateway" , vendorKey : "custom" } ) ,
67+ provider ( { id : "p3" , name : "My Ollama" , vendorKey : "" } ) ,
68+ ] ) ;
69+ assert . ok ( choices . length > 0 ) ;
70+ for ( const choice of choices ) {
71+ assert . notEqual (
72+ subagentModelPinParts ( choice . value ) ,
73+ null ,
74+ `${ choice . value } is offered but not saveable` ,
75+ ) ;
76+ }
77+ } ) ;
78+
79+ test ( "a pin needs a provider half, a model half, and keeps its own slashes" , ( ) => {
80+ assert . deepEqual ( subagentModelPinParts ( "anthropic/claude-haiku-4-5" ) , {
81+ providerPart : "anthropic" ,
82+ modelId : "claude-haiku-4-5" ,
83+ } ) ;
84+ assert . deepEqual ( subagentModelPinParts ( " My Gateway/local-model " ) , {
85+ providerPart : "My Gateway" ,
86+ modelId : "local-model" ,
87+ } ) ;
88+ // An openrouter-style model id carries its own slashes; only the first one
89+ // separates the provider.
90+ assert . deepEqual ( subagentModelPinParts ( "openrouter/deepseek/deepseek-chat" ) , {
91+ providerPart : "openrouter" ,
92+ modelId : "deepseek/deepseek-chat" ,
93+ } ) ;
94+ // A bare id has no provider to look up, so it is not a pin.
95+ assert . equal ( subagentModelPinParts ( "claude-haiku-4-5" ) , null ) ;
96+ assert . equal ( subagentModelPinParts ( "/claude-haiku-4-5" ) , null ) ;
97+ assert . equal ( subagentModelPinParts ( "anthropic/" ) , null ) ;
98+ } ) ;
5899} ) ;
59100
60101test ( "the sheet lists configured models from enabled, credentialed providers" , ( ) => {
@@ -89,7 +130,10 @@ test("the sheet lists configured models from enabled, credentialed providers", (
89130 ) ;
90131} ) ;
91132
92- test ( "the sheet only lists bindings explicitly enabled for subagents" , ( ) => {
133+ test ( "the sheet lists every configured binding regardless of the delegation flag" , ( ) => {
134+ // The editor no longer consults `availableForSubagents`: a model the user
135+ // configured in Settings is selectable, so the list cannot be empty while a
136+ // runnable provider exists.
93137 const choices = subagentModelChoices ( [
94138 provider ( {
95139 models : [
@@ -104,7 +148,7 @@ test("the sheet only lists bindings explicitly enabled for subagents", () => {
104148 ] ) ;
105149 assert . deepEqual (
106150 choices . map ( ( choice ) => choice . modelId ) ,
107- [ "claude-haiku-4-5" ] ,
151+ [ "claude-haiku-4-5" , "claude-sonnet-4-6" , "claude-opus-4-6" ] ,
108152 ) ;
109153} ) ;
110154
@@ -222,7 +266,7 @@ test("a pin that is no longer configured stays selectable", () => {
222266 assert . equal ( subagentModelOrphanPin ( "openai/gpt-5" , choices ) , "openai/gpt-5" ) ;
223267} ) ;
224268
225- test ( "the editor model field offers configured, custom, and empty-list paths " , async ( ) => {
269+ test ( "the editor model field is a configured-only select with an empty-state action " , async ( ) => {
226270 const source = await readFile (
227271 new URL ( "../src/components/settings/SubagentEditorSheet.tsx" , import . meta. url ) ,
228272 "utf8" ,
@@ -234,11 +278,13 @@ test("the editor model field offers configured, custom, and empty-list paths", a
234278 assert . match ( modelField , / < S e l e c t / ) ;
235279 assert . match ( modelField , / < o p t g r o u p / ) ;
236280 assert . match ( modelField , / e x t e n s i o n s \. s u b a g e n t s \. m o d e l I n h e r i t / ) ;
237- assert . match ( modelField , / e x t e n s i o n s \. s u b a g e n t s \. m o d e l P i c k C u s t o m / ) ;
238281 assert . match ( modelField , / e x t e n s i o n s \. s u b a g e n t s \. m o d e l P i c k E m p t y / ) ;
282+ assert . match ( modelField , / e x t e n s i o n s \. s u b a g e n t s \. m o d e l P i c k E m p t y A c t i o n / ) ;
283+ assert . match ( modelField , / s e t S e t t i n g s T a b \( " a g e n t " \) / ) ;
284+ // No hand-typed model id: the field never renders a free-text input.
285+ assert . doesNotMatch ( modelField , / < I n p u t / ) ;
286+ assert . doesNotMatch ( modelField , / m o d e l P i c k C u s t o m / ) ;
239287 assert . match ( source , / s u b a g e n t M o d e l C h o i c e s \( p r o v i d e r s \) / ) ;
240- assert . match ( source , / C U S T O M _ S U B A G E N T _ M O D E L _ V A L U E / ) ;
241- assert . match ( source , / e x t e n s i o n s \. s u b a g e n t s \. m o d e l P i c k C u s t o m H i n t / ) ;
242288 assert . match ( source , / r e s e t S u b a g e n t T e m p l a t e \( d r a f t \) / ) ;
243289} ) ;
244290
0 commit comments