@@ -16,8 +16,18 @@ import bufferPostFixture from '@/tools/buffer/__fixtures__/post.json'
1616import { bufferCreatePostTool } from '@/tools/buffer/create_post'
1717import { bufferEditPostTool } from '@/tools/buffer/edit_post'
1818import type { BufferCreatePostParams , BufferEditPostParams } from '@/tools/buffer/types'
19+ import { flowCreateBranchTool } from '@/tools/flow/create-branch'
20+ import { flowListCommentsTool } from '@/tools/flow/list-comments'
21+ import { flowListEntitiesTool } from '@/tools/flow/list-entities'
1922import { flowSetValueTool } from '@/tools/flow/set-value'
20- import type { FlowSetValueParams } from '@/tools/flow/types'
23+ import type {
24+ FlowCreateBranchParams ,
25+ FlowListCommentsParams ,
26+ FlowListEntitiesParams ,
27+ FlowSetValueParams ,
28+ FlowUpdateCommentParams ,
29+ } from '@/tools/flow/types'
30+ import { flowUpdateCommentTool } from '@/tools/flow/update-comment'
2131import { mcpRunOperationTool } from '@/tools/mcp/run-operation'
2232import type { ToolConfig } from '@/tools/types'
2333import { getTool } from '@/tools/utils'
@@ -92,6 +102,8 @@ describe('GenericBlockHandler', () => {
92102 writes . length = 0
93103 mockGetBlock . mockReturnValue ( FlowBlock )
94104 mockGetTool . mockReturnValue ( flowSetValueTool )
105+ mockBlock . metadata = { id : 'flow' , name : 'Flow' }
106+ mockBlock . config = { tool : 'flow_set_value' , params : { } }
95107 const { validateRequiredParametersAfterMerge } =
96108 await vi . importActual < typeof import ( '@/tools/utils' ) > ( '@/tools/utils' )
97109 mockExecuteTool . mockImplementation ( async ( toolId : string , params : FlowSetValueParams ) => {
@@ -105,21 +117,13 @@ describe('GenericBlockHandler', () => {
105117 it . each ( values . map ( ( value ) => ( { value, encoded : JSON . stringify ( value ) } ) ) ) (
106118 'preserves the JSON value $encoded on the provider wire' ,
107119 async ( { value, encoded } ) => {
108- await handler . execute (
109- mockContext ,
110- {
111- ...mockBlock ,
112- metadata : { id : 'flow' , name : 'Flow' } ,
113- config : { tool : 'flow_set_value' , params : { } } ,
114- } ,
115- {
116- operation : 'set_value' ,
117- projectId : 'project' ,
118- entityId : 'entity' ,
119- fieldKey : 'name' ,
120- value : encoded ,
121- }
122- )
120+ await handler . execute ( mockContext , mockBlock , {
121+ operation : 'set_value' ,
122+ projectId : 'project' ,
123+ entityId : 'entity' ,
124+ fieldKey : 'name' ,
125+ value : encoded ,
126+ } )
123127 expect ( writes ) . toEqual ( [ { key : 'name' , value } ] )
124128 }
125129 )
@@ -128,27 +132,140 @@ describe('GenericBlockHandler', () => {
128132 'rejects omitted or malformed JSON before sending a field write (%s)' ,
129133 async ( value ) => {
130134 await expect (
131- handler . execute (
132- mockContext ,
133- {
134- ...mockBlock ,
135- metadata : { id : 'flow' , name : 'Flow' } ,
136- config : { tool : 'flow_set_value' , params : { } } ,
137- } ,
138- {
139- operation : 'set_value' ,
140- projectId : 'project' ,
141- entityId : 'entity' ,
142- fieldKey : 'name' ,
143- value,
144- }
145- )
135+ handler . execute ( mockContext , mockBlock , {
136+ operation : 'set_value' ,
137+ projectId : 'project' ,
138+ entityId : 'entity' ,
139+ fieldKey : 'name' ,
140+ value,
141+ } )
146142 ) . rejects . toThrow ( )
147143 expect ( writes ) . toEqual ( [ ] )
148144 }
149145 )
146+
147+ it ( 'rejects a whitespace-only field key before sending a write' , async ( ) => {
148+ await expect (
149+ handler . execute ( mockContext , mockBlock , {
150+ operation : 'set_value' ,
151+ projectId : 'project' ,
152+ entityId : 'entity' ,
153+ fieldKey : ' \t ' ,
154+ value : '"updated"' ,
155+ } )
156+ ) . rejects . toThrow ( 'Field key is required' )
157+ expect ( writes ) . toEqual ( [ ] )
158+ } )
159+ } )
160+
161+ describe ( 'Flow list limits' , ( ) => {
162+ const requests : string [ ] = [ ]
163+
164+ beforeEach ( ( ) => {
165+ requests . length = 0
166+ mockGetBlock . mockReturnValue ( FlowBlock )
167+ mockGetTool . mockReturnValue ( flowListEntitiesTool )
168+ mockExecuteTool . mockImplementation (
169+ async ( _toolId : string , params : FlowListEntitiesParams ) => {
170+ const url = flowListEntitiesTool . request . url
171+ requests . push ( typeof url === 'function' ? url ( params ) : url )
172+ return { success : true , output : { } }
173+ }
174+ )
175+ mockBlock . metadata = { id : 'flow' , name : 'Flow' }
176+ mockBlock . config = { tool : 'flow_list_entities' , params : { } }
177+ } )
178+
179+ it . each ( [
180+ { limit : null , expected : '50' } ,
181+ { limit : '' , expected : '50' } ,
182+ { limit : ' ' , expected : '50' } ,
183+ { limit : '25' , expected : '25' } ,
184+ ] ) ( 'sends page size $expected when the Limit input is $limit' , async ( { limit, expected } ) => {
185+ await handler . execute ( mockContext , mockBlock , {
186+ operation : 'list_entities' ,
187+ domain : 'https://api.example.com' ,
188+ projectId : 'project' ,
189+ limit,
190+ } )
191+ expect ( requests . map ( ( url ) => new URL ( url ) . searchParams . get ( 'limit' ) ) ) . toEqual ( [ expected ] )
192+ } )
193+
194+ it ( 'rejects an explicit zero instead of silently replacing it with the default' , async ( ) => {
195+ await expect (
196+ handler . execute ( mockContext , mockBlock , {
197+ operation : 'list_entities' ,
198+ domain : 'https://api.example.com' ,
199+ projectId : 'project' ,
200+ limit : '0' ,
201+ } )
202+ ) . rejects . toThrow ( 'Limit must be an integer between 1 and 1000' )
203+ expect ( requests ) . toEqual ( [ ] )
204+ } )
150205 } )
151206
207+ it . each ( [
208+ { selection : 'untouched' , value : null , expected : undefined } ,
209+ { selection : 'cleared' , value : '' , expected : undefined } ,
210+ { selection : 'No' , value : 'false' , expected : false } ,
211+ { selection : 'Yes' , value : true , expected : true } ,
212+ ] ) (
213+ 'sends optional Flow booleans correctly for $selection dropdowns' ,
214+ async ( { value, expected } ) => {
215+ const requests : { includeResolved : string | null ; body : unknown } [ ] = [ ]
216+ mockGetBlock . mockReturnValue ( FlowBlock )
217+ mockBlock . metadata = { id : 'flow' , name : 'Flow' }
218+
219+ for ( const tool of [ flowListCommentsTool , flowUpdateCommentTool , flowCreateBranchTool ] ) {
220+ mockGetTool . mockReturnValue ( tool )
221+ mockBlock . config = { tool : tool . id , params : { } }
222+ mockExecuteTool . mockImplementation (
223+ async (
224+ _toolId : string ,
225+ params : FlowListCommentsParams & FlowUpdateCommentParams & FlowCreateBranchParams
226+ ) => {
227+ const url = tool . request . url
228+ requests . push ( {
229+ includeResolved : new URL (
230+ typeof url === 'function' ? url ( params ) : url
231+ ) . searchParams . get ( 'include_resolved' ) ,
232+ body : await Response . json ( tool . request . body ?.( params ) ?? null ) . json ( ) ,
233+ } )
234+ return { success : true , output : { } }
235+ }
236+ )
237+ await handler . execute ( mockContext , mockBlock , {
238+ operation : tool . id . replace ( 'flow_' , '' ) ,
239+ domain : 'https://api.example.com' ,
240+ projectId : 'project' ,
241+ entityId : 'entity' ,
242+ commentId : 'comment' ,
243+ text : 'Updated comment' ,
244+ branchName : 'Pilot' ,
245+ includeResolved : value ,
246+ resolved : value ,
247+ isVariant : value ,
248+ } )
249+ }
250+
251+ expect ( requests ) . toEqual ( [
252+ { includeResolved : expected === undefined ? null : String ( expected ) , body : null } ,
253+ {
254+ includeResolved : null ,
255+ body :
256+ expected === undefined
257+ ? { text : 'Updated comment' }
258+ : { text : 'Updated comment' , resolved : expected } ,
259+ } ,
260+ {
261+ includeResolved : null ,
262+ body :
263+ expected === undefined ? { name : 'Pilot' } : { name : 'Pilot' , is_variant : expected } ,
264+ } ,
265+ ] )
266+ }
267+ )
268+
152269 it . each ( [
153270 {
154271 selection : 'keep-current scheduling' ,
0 commit comments