44import { beforeEach , describe , expect , it , vi } from 'vitest'
55import { normalizeConditionRouterIds } from './builders'
66
7- const { mockValidateSelectorIds } = vi . hoisted ( ( ) => ( {
7+ const { mockValidateSelectorIds, mockGetModelOptions } = vi . hoisted ( ( ) => ( {
88 mockValidateSelectorIds : vi . fn ( ) ,
9+ mockGetModelOptions : vi . fn ( ( ) => [ ] ) ,
910} ) )
1011
1112const conditionBlockConfig = {
@@ -26,7 +27,24 @@ const routerBlockConfig = {
2627 type : 'router_v2' ,
2728 name : 'Router' ,
2829 outputs : { } ,
29- subBlocks : [ { id : 'routes' , type : 'router-input' } ] ,
30+ subBlocks : [
31+ { id : 'routes' , type : 'router-input' } ,
32+ { id : 'model' , type : 'combobox' , options : mockGetModelOptions } ,
33+ ] ,
34+ }
35+
36+ const agentBlockConfig = {
37+ type : 'agent' ,
38+ name : 'Agent' ,
39+ outputs : { } ,
40+ subBlocks : [ { id : 'model' , type : 'combobox' , options : mockGetModelOptions } ] ,
41+ }
42+
43+ const huggingfaceBlockConfig = {
44+ type : 'huggingface' ,
45+ name : 'HuggingFace' ,
46+ outputs : { } ,
47+ subBlocks : [ { id : 'model' , type : 'short-input' } ] ,
3048}
3149
3250vi . mock ( '@/blocks/registry' , ( ) => ( {
@@ -37,7 +55,15 @@ vi.mock('@/blocks/registry', () => ({
3755 ? oauthBlockConfig
3856 : type === 'router_v2'
3957 ? routerBlockConfig
40- : undefined ,
58+ : type === 'agent'
59+ ? agentBlockConfig
60+ : type === 'huggingface'
61+ ? huggingfaceBlockConfig
62+ : undefined ,
63+ } ) )
64+
65+ vi . mock ( '@/blocks/utils' , ( ) => ( {
66+ getModelOptions : mockGetModelOptions ,
4167} ) )
4268
4369vi . mock ( '@/lib/copilot/validation/selector-validator' , ( ) => ( {
@@ -83,6 +109,95 @@ describe('validateInputsForBlock', () => {
83109 expect ( result . errors ) . toHaveLength ( 1 )
84110 expect ( result . errors [ 0 ] ?. error ) . toContain ( 'expected a JSON array' )
85111 } )
112+
113+ it ( 'accepts known agent model ids' , ( ) => {
114+ const result = validateInputsForBlock ( 'agent' , { model : 'claude-sonnet-4-6' } , 'agent-1' )
115+
116+ expect ( result . errors ) . toHaveLength ( 0 )
117+ expect ( result . validInputs . model ) . toBe ( 'claude-sonnet-4-6' )
118+ } )
119+
120+ it ( 'rejects hallucinated agent model ids that match a static provider pattern' , ( ) => {
121+ const result = validateInputsForBlock ( 'agent' , { model : 'claude-sonnet-4.6' } , 'agent-1' )
122+
123+ expect ( result . validInputs . model ) . toBeUndefined ( )
124+ expect ( result . errors ) . toHaveLength ( 1 )
125+ expect ( result . errors [ 0 ] ?. field ) . toBe ( 'model' )
126+ expect ( result . errors [ 0 ] ?. error ) . toContain ( 'Unknown model id' )
127+ expect ( result . errors [ 0 ] ?. error ) . toContain ( 'claude-sonnet-4-6' )
128+ } )
129+
130+ it ( 'rejects legacy claude-4.5-haiku style ids' , ( ) => {
131+ const result = validateInputsForBlock ( 'agent' , { model : 'claude-4.5-haiku' } , 'agent-1' )
132+
133+ expect ( result . errors ) . toHaveLength ( 1 )
134+ expect ( result . errors [ 0 ] ?. error ) . toContain ( 'Unknown model id' )
135+ } )
136+
137+ it ( 'allows empty model values' , ( ) => {
138+ const result = validateInputsForBlock ( 'agent' , { model : '' } , 'agent-1' )
139+
140+ expect ( result . errors ) . toHaveLength ( 0 )
141+ expect ( result . validInputs . model ) . toBe ( '' )
142+ } )
143+
144+ it ( 'allows custom ollama-prefixed model ids' , ( ) => {
145+ const result = validateInputsForBlock ( 'agent' , { model : 'ollama/my-private-model' } , 'agent-1' )
146+
147+ expect ( result . errors ) . toHaveLength ( 0 )
148+ expect ( result . validInputs . model ) . toBe ( 'ollama/my-private-model' )
149+ } )
150+
151+ it ( 'validates the model field on router_v2 blocks too' , ( ) => {
152+ const valid = validateInputsForBlock ( 'router_v2' , { model : 'claude-sonnet-4-6' } , 'router-1' )
153+ expect ( valid . errors ) . toHaveLength ( 0 )
154+ expect ( valid . validInputs . model ) . toBe ( 'claude-sonnet-4-6' )
155+
156+ const invalid = validateInputsForBlock ( 'router_v2' , { model : 'claude-sonnet-4.6' } , 'router-1' )
157+ expect ( invalid . validInputs . model ) . toBeUndefined ( )
158+ expect ( invalid . errors ) . toHaveLength ( 1 )
159+ expect ( invalid . errors [ 0 ] ?. blockType ) . toBe ( 'router_v2' )
160+ expect ( invalid . errors [ 0 ] ?. field ) . toBe ( 'model' )
161+ expect ( invalid . errors [ 0 ] ?. error ) . toContain ( 'Unknown model id' )
162+ } )
163+
164+ it ( "does not apply model validation to blocks whose model field is not Sim's catalog" , ( ) => {
165+ const result = validateInputsForBlock (
166+ 'huggingface' ,
167+ { model : 'mistralai/Mistral-7B-Instruct-v0.3' } ,
168+ 'hf-1'
169+ )
170+
171+ expect ( result . errors ) . toHaveLength ( 0 )
172+ expect ( result . validInputs . model ) . toBe ( 'mistralai/Mistral-7B-Instruct-v0.3' )
173+ } )
174+
175+ it ( 'accepts date-suffixed variants of known Anthropic ids' , ( ) => {
176+ const result = validateInputsForBlock (
177+ 'agent' ,
178+ { model : 'claude-sonnet-4-5-20250929' } ,
179+ 'agent-1'
180+ )
181+
182+ expect ( result . errors ) . toHaveLength ( 0 )
183+ expect ( result . validInputs . model ) . toBe ( 'claude-sonnet-4-5-20250929' )
184+ } )
185+
186+ it ( 'trims whitespace around catalog model ids and stores the trimmed value' , ( ) => {
187+ const result = validateInputsForBlock ( 'agent' , { model : ' gpt-5.4 ' } , 'agent-1' )
188+
189+ expect ( result . errors ) . toHaveLength ( 0 )
190+ expect ( result . validInputs . model ) . toBe ( 'gpt-5.4' )
191+ } )
192+
193+ it ( 'rejects a pattern-matching but uncataloged id even with surrounding whitespace' , ( ) => {
194+ const result = validateInputsForBlock ( 'agent' , { model : ' gpt-100-ultra ' } , 'agent-1' )
195+
196+ expect ( result . validInputs . model ) . toBeUndefined ( )
197+ expect ( result . errors ) . toHaveLength ( 1 )
198+ expect ( result . errors [ 0 ] ?. error ) . toContain ( 'gpt-100-ultra' )
199+ expect ( result . errors [ 0 ] ?. error ) . not . toMatch ( / \s { 2 , } / )
200+ } )
86201} )
87202
88203describe ( 'normalizeConditionRouterIds' , ( ) => {
0 commit comments