@@ -56,6 +56,11 @@ vi.mock('@/lib/workflows/deployment-status', () => ({
5656import { OrchestrationError } from '@/lib/core/orchestration/types'
5757import { replaceWorkflowState } from '@/lib/workflows/application/replace-workflow-state'
5858import { REFERENCES_UNCHECKED_NOTE } from '@/lib/workflows/editing/lint-report'
59+ import { validateInputsForBlock } from '@/lib/workflows/editing/validation'
60+ import { ExaBlock } from '@/blocks/blocks/exa'
61+ import { getBlock } from '@/blocks/registry'
62+
63+ const defaultGetBlock = vi . mocked ( getBlock ) . getMockImplementation ( )
5964
6065const BLOCK = {
6166 id : 'block-1' ,
@@ -87,6 +92,9 @@ const input = { workflowId: 'workflow-1', blocks: { 'block-1': BLOCK }, edges: [
8792describe ( 'replaceWorkflowState' , ( ) => {
8893 beforeEach ( ( ) => {
8994 vi . clearAllMocks ( )
95+ vi . mocked ( getBlock ) . mockImplementation ( ( type ) =>
96+ type === 'exa' ? ExaBlock : defaultGetBlock ?.( type )
97+ )
9098 mocks . resolveContext . mockResolvedValue ( context )
9199 mocks . resolvePermission . mockResolvedValue ( 'write' )
92100 workflowAuthzMockFns . mockAssertWorkflowMutable . mockResolvedValue ( undefined )
@@ -434,6 +442,66 @@ describe('replaceWorkflowState', () => {
434442 } )
435443 } )
436444
445+ describe ( 'registry input validation' , ( ) => {
446+ for ( const dryRun of [ true , false ] ) {
447+ it . each ( [ 'type' , 'category' , 'text' , 'highlights' , 'summary' ] ) (
448+ `rejects the same dynamic %s value as operations apply (dryRun=${ dryRun } )` ,
449+ async ( field ) => {
450+ const value = `<start.${ field } >`
451+ const { errors } = validateInputsForBlock ( 'exa' , { [ field ] : value } , BLOCK . id )
452+ expect ( errors ) . toHaveLength ( 1 )
453+ await expect (
454+ replaceWorkflowState . execute ( {
455+ principal : sessionPrincipal ,
456+ input : {
457+ ...input ,
458+ dryRun,
459+ blocks : {
460+ [ BLOCK . id ] : {
461+ ...BLOCK ,
462+ type : 'exa' ,
463+ advancedMode : true ,
464+ subBlocks : { [ field ] : { id : field , type : 'short-input' , value } } ,
465+ } ,
466+ } ,
467+ } ,
468+ } )
469+ ) . rejects . toThrow ( errors [ 0 ] . error )
470+ expect ( mocks . replace ) . not . toHaveBeenCalled ( )
471+ expect ( mocks . notify ) . not . toHaveBeenCalled ( )
472+ expect ( mocks . recordAudit ) . not . toHaveBeenCalled ( )
473+ }
474+ )
475+ }
476+
477+ it ( 'accepts literal choices alongside text references without enabling advanced mode' , async ( ) => {
478+ const block = {
479+ ...BLOCK ,
480+ type : 'exa' ,
481+ advancedMode : false ,
482+ subBlocks : {
483+ operation : { id : 'operation' , type : 'dropdown' as const , value : 'exa_search' } ,
484+ type : { id : 'type' , type : 'dropdown' as const , value : 'auto' } ,
485+ query : { id : 'query' , type : 'long-input' as const , value : '<start.query>' } ,
486+ text : { id : 'text' , type : 'switch' as const , value : true } ,
487+ highlights : { id : 'highlights' , type : 'switch' as const , value : false } ,
488+ summary : { id : 'summary' , type : 'switch' as const , value : null } ,
489+ } ,
490+ }
491+ await expect (
492+ replaceWorkflowState . execute ( {
493+ principal : sessionPrincipal ,
494+ input : { ...input , blocks : { [ BLOCK . id ] : block } } ,
495+ } )
496+ ) . resolves . toMatchObject ( { dryRun : false } )
497+ expect ( mocks . replace ) . toHaveBeenCalledWith (
498+ expect . objectContaining ( {
499+ state : { blocks : { [ BLOCK . id ] : block } , edges : [ ] , variables : undefined } ,
500+ } )
501+ )
502+ } )
503+ } )
504+
437505 /**
438506 * A replace stores blocks and their tool wiring wholesale, and the policies
439507 * deciding which of those a member may add take a human subject. A workspace
0 commit comments