@@ -124,7 +124,7 @@ vi.mock('../access', () => ({
124124
125125import { applyCreateWorkflowOutputToContext } from '@/lib/copilot/request/tools/workflow-context'
126126import { performUpdateWorkflow } from '@/lib/workflows/orchestration'
127- import { verifyFolderWorkspace } from '@/lib/workflows/utils'
127+ import { listFolders , verifyFolderWorkspace } from '@/lib/workflows/utils'
128128import {
129129 executeCreateWorkflow ,
130130 executeMoveWorkflow ,
@@ -136,6 +136,7 @@ import {
136136} from './mutations'
137137
138138const performUpdateWorkflowMock = vi . mocked ( performUpdateWorkflow )
139+ const listFoldersMock = vi . mocked ( listFolders )
139140const verifyFolderWorkspaceMock = vi . mocked ( verifyFolderWorkspace )
140141const billingAttribution : BillingAttributionSnapshot = {
141142 actorUserId : 'user-1' ,
@@ -291,6 +292,7 @@ describe('executeCreateWorkflow billing attribution', () => {
291292 payerUsage : { currentUsage : 1 , limit : 10 } ,
292293 } )
293294 reserveExecutionSlotMock . mockResolvedValue ( { reserved : true , created : true } )
295+ listFoldersMock . mockResolvedValue ( [ ] )
294296 } )
295297
296298 it ( 'ignores legacy description input instead of persisting it' , async ( ) => {
@@ -320,6 +322,65 @@ describe('executeCreateWorkflow billing attribution', () => {
320322 } )
321323 } )
322324
325+ it ( 'canonicalizes a workflow-folder VFS path and resolves its internal ID' , async ( ) => {
326+ listFoldersMock . mockResolvedValue ( [
327+ { folderId : 'folder-dream' , folderName : 'Dream' , parentId : null } ,
328+ {
329+ folderId : 'folder-launch-plans' ,
330+ folderName : 'Launch Plans' ,
331+ parentId : 'folder-dream' ,
332+ } ,
333+ ] )
334+ performCreateWorkflowMock . mockResolvedValue ( {
335+ success : true ,
336+ workflow : {
337+ id : 'created-workflow' ,
338+ name : 'Created Workflow' ,
339+ workspaceId : 'workspace-1' ,
340+ folderId : 'folder-launch-plans' ,
341+ } ,
342+ } )
343+
344+ const result = await executeCreateWorkflow (
345+ {
346+ name : 'Created Workflow' ,
347+ workspaceId : 'workspace-1' ,
348+ folderPath : 'workflows/Dream/Launch%20Plans' ,
349+ } ,
350+ executionContext
351+ )
352+
353+ expect ( result . success ) . toBe ( true )
354+ expect ( performCreateWorkflowMock ) . toHaveBeenCalledWith ( {
355+ userId : 'user-1' ,
356+ workspaceId : 'workspace-1' ,
357+ name : 'Created Workflow' ,
358+ folderId : 'folder-launch-plans' ,
359+ } )
360+ expect ( workflowAuthzMockFns . mockAssertFolderMutable ) . toHaveBeenCalledWith ( 'folder-launch-plans' )
361+ } )
362+
363+ it ( 'fails clearly when a workflow-folder VFS path does not exist' , async ( ) => {
364+ listFoldersMock . mockResolvedValue ( [
365+ { folderId : 'folder-existing' , folderName : 'Existing' , parentId : null } ,
366+ ] )
367+
368+ const result = await executeCreateWorkflow (
369+ {
370+ name : 'Created Workflow' ,
371+ workspaceId : 'workspace-1' ,
372+ folderPath : 'workflows/Dream' ,
373+ } ,
374+ executionContext
375+ )
376+
377+ expect ( result ) . toEqual ( {
378+ success : false ,
379+ error : 'Folder not found at workflows/Dream' ,
380+ } )
381+ expect ( performCreateWorkflowMock ) . not . toHaveBeenCalled ( )
382+ } )
383+
323384 it ( 'keeps same-workspace creation and subsequent execution on the immutable payer' , async ( ) => {
324385 const context : ExecutionContext = { ...executionContext , workflowId : '' }
325386 performCreateWorkflowMock . mockResolvedValue ( {
0 commit comments