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