@@ -17,6 +17,7 @@ vi.mock('@/blocks/registry', () => ({
1717 subBlocks : [
1818 { id : 'systemPrompt' , type : 'long-input' } ,
1919 { id : 'model' , type : 'combobox' } ,
20+ { id : 'customModelConfig' , type : 'code' } ,
2021 ] ,
2122 } ,
2223 {
@@ -41,6 +42,7 @@ vi.mock('@/blocks/registry', () => ({
4142 subBlocks : [
4243 { id : 'systemPrompt' , type : 'long-input' } ,
4344 { id : 'model' , type : 'combobox' } ,
45+ { id : 'customModelConfig' , type : 'code' } ,
4446 ] ,
4547 } ,
4648 function : {
@@ -557,3 +559,126 @@ describe('forward-reference connections (pending resolution)', () => {
557559 expect ( state . blocks [ BLOCK_A ] . data ?. pendingConnections ) . toBeUndefined ( )
558560 } )
559561} )
562+
563+ describe ( 'custom model credential round trips' , ( ) => {
564+ const existingSecret = 'sk-existing-custom-secret'
565+
566+ function makeCustomAgentWorkflow ( includeConfig = true ) {
567+ const customModelConfig = JSON . stringify ( {
568+ provider : 'openai' ,
569+ model : 'gpt-5.6-terra' ,
570+ parameters : { reasoningEffort : 'medium' , temperature : 0.2 } ,
571+ credentials : { mode : 'explicit' , apiKey : existingSecret } ,
572+ providerOptions : { } ,
573+ } )
574+
575+ return {
576+ blocks : {
577+ 'agent-1' : {
578+ id : 'agent-1' ,
579+ type : 'agent' ,
580+ name : 'Agent 1' ,
581+ position : { x : 0 , y : 0 } ,
582+ enabled : true ,
583+ subBlocks : {
584+ model : { id : 'model' , type : 'combobox' , value : 'sim-custom' } ,
585+ ...( includeConfig
586+ ? {
587+ customModelConfig : {
588+ id : 'customModelConfig' ,
589+ type : 'code' ,
590+ value : customModelConfig ,
591+ } ,
592+ }
593+ : { } ) ,
594+ } ,
595+ outputs : { } ,
596+ data : { } ,
597+ } ,
598+ } ,
599+ edges : [ ] as any [ ] ,
600+ loops : { } ,
601+ parallels : { } ,
602+ }
603+ }
604+
605+ it ( 'restores the existing literal key when an edit round-trips the VFS placeholder' , ( ) => {
606+ const result = applyOperationsToWorkflowState ( makeCustomAgentWorkflow ( ) , [
607+ {
608+ operation_type : 'edit' ,
609+ block_id : 'agent-1' ,
610+ params : {
611+ inputs : {
612+ customModelConfig : {
613+ provider : 'openai' ,
614+ model : 'gpt-5.6-sol' ,
615+ parameters : { reasoningEffort : 'high' , temperature : 0.1 } ,
616+ credentials : { mode : 'explicit' , apiKey : '<redacted>' } ,
617+ providerOptions : { } ,
618+ } ,
619+ } ,
620+ } ,
621+ } ,
622+ ] )
623+
624+ expect ( result . validationErrors ) . toHaveLength ( 0 )
625+ const stored = JSON . parse (
626+ result . state . blocks [ 'agent-1' ] . subBlocks . customModelConfig . value as string
627+ )
628+ expect ( stored ) . toMatchObject ( {
629+ provider : 'openai' ,
630+ model : 'gpt-5.6-sol' ,
631+ parameters : { reasoningEffort : 'high' , temperature : 0.1 } ,
632+ credentials : { mode : 'explicit' , apiKey : existingSecret } ,
633+ } )
634+ } )
635+
636+ it ( 'rejects the placeholder on a provider change without overwriting the stored config' , ( ) => {
637+ const workflow = makeCustomAgentWorkflow ( )
638+ const original = workflow . blocks [ 'agent-1' ] . subBlocks . customModelConfig . value
639+ const result = applyOperationsToWorkflowState ( workflow , [
640+ {
641+ operation_type : 'edit' ,
642+ block_id : 'agent-1' ,
643+ params : {
644+ inputs : {
645+ customModelConfig : {
646+ provider : 'xai' ,
647+ model : 'grok-4.5' ,
648+ credentials : { mode : 'explicit' , apiKey : '<redacted>' } ,
649+ providerOptions : { } ,
650+ } ,
651+ } ,
652+ } ,
653+ } ,
654+ ] )
655+
656+ expect ( result . validationErrors ) . toHaveLength ( 1 )
657+ expect ( result . validationErrors [ 0 ] ?. error ) . toContain ( 'when changing providers' )
658+ expect ( result . state . blocks [ 'agent-1' ] . subBlocks . customModelConfig . value ) . toBe ( original )
659+ expect ( JSON . stringify ( result . validationErrors ) ) . not . toContain ( existingSecret )
660+ } )
661+
662+ it ( 'rejects the placeholder when the existing block has no key to preserve' , ( ) => {
663+ const result = applyOperationsToWorkflowState ( makeCustomAgentWorkflow ( false ) , [
664+ {
665+ operation_type : 'edit' ,
666+ block_id : 'agent-1' ,
667+ params : {
668+ inputs : {
669+ customModelConfig : {
670+ provider : 'openai' ,
671+ model : 'gpt-5.6-terra' ,
672+ credentials : { mode : 'explicit' , apiKey : '<redacted>' } ,
673+ providerOptions : { } ,
674+ } ,
675+ } ,
676+ } ,
677+ } ,
678+ ] )
679+
680+ expect ( result . validationErrors ) . toHaveLength ( 1 )
681+ expect ( result . validationErrors [ 0 ] ?. error ) . toContain ( 'without an existing stored key' )
682+ expect ( result . state . blocks [ 'agent-1' ] . subBlocks ) . not . toHaveProperty ( 'customModelConfig' )
683+ } )
684+ } )
0 commit comments