diff --git a/packages/@n8n/agents/src/__tests__/workspace/run-javascript.test.ts b/packages/@n8n/agents/src/__tests__/workspace/run-javascript.test.ts index 69c7f3fbd84..bebd815de0c 100644 --- a/packages/@n8n/agents/src/__tests__/workspace/run-javascript.test.ts +++ b/packages/@n8n/agents/src/__tests__/workspace/run-javascript.test.ts @@ -51,24 +51,19 @@ function makeFakeSandbox(result: Partial = {}): WorkspaceSandbox } describe('runJavascriptInputSchema', () => { - it('accepts exactly one of path or code', () => { + it('accepts path, code, or both', () => { expect(runJavascriptInputSchema.safeParse({ path: 'plot.js' }).success).toBe(true); expect(runJavascriptInputSchema.safeParse({ code: 'console.log(1)' }).success).toBe(true); - }); - - it('rejects both path and code', () => { - const parsed = runJavascriptInputSchema.safeParse({ path: 'plot.js', code: 'console.log(1)' }); - expect(parsed.success).toBe(false); - if (!parsed.success) { - expect(parsed.error.message).toContain('Provide exactly one of path or code'); - } + expect( + runJavascriptInputSchema.safeParse({ path: 'tmp/unused', code: 'console.log(1)' }).success, + ).toBe(true); }); it('rejects neither path nor code', () => { const parsed = runJavascriptInputSchema.safeParse({}); expect(parsed.success).toBe(false); if (!parsed.success) { - expect(parsed.error.message).toContain('Provide exactly one of path or code'); + expect(parsed.error.message).toContain('Provide path or code'); } }); }); @@ -104,6 +99,22 @@ describe('createRunJavascriptTool', () => { }); }); + it('uses code when both path and code are set', async () => { + const filesystem = makeFakeFilesystem(); + const sandbox = makeFakeSandbox(); + const tool = createRunJavascriptTool(sandbox, filesystem); + + await tool.handler!({ path: 'tmp/unused', code: 'console.log(1)' }, {} as never); + + expect(filesystem.writeFile).toHaveBeenCalledTimes(1); + expect(vi.mocked(filesystem.writeFile).mock.calls[0]?.[1]).toBe('console.log(1)'); + const scriptPath = vi.mocked(filesystem.writeFile).mock.calls[0]?.[0]; + expect(sandbox.executeCommand).toHaveBeenCalledWith('node', [scriptPath], { + timeout: 60_000, + abortSignal: undefined, + }); + }); + it('runs an existing path with node and does not write a snippet', async () => { const filesystem = makeFakeFilesystem(); const sandbox = makeFakeSandbox(); diff --git a/packages/@n8n/agents/src/workspace/tools/run-javascript.ts b/packages/@n8n/agents/src/workspace/tools/run-javascript.ts index b11e95f1f5a..7ba77d14ef4 100644 --- a/packages/@n8n/agents/src/workspace/tools/run-javascript.ts +++ b/packages/@n8n/agents/src/workspace/tools/run-javascript.ts @@ -18,10 +18,16 @@ export const runJavascriptInputSchema = z .describe( 'Path of the JavaScript file, relative to the workspace root (absolute paths must be under it)', ), - code: z.string().min(1).optional().describe('JavaScript source to run as a Scratch File'), + code: z + .string() + .min(1) + .optional() + .describe( + 'JavaScript source to run as a Scratch File. If both path and code are set, code is used.', + ), }) - .refine((value) => Boolean(value.path) !== Boolean(value.code), { - message: 'Provide exactly one of path or code', + .refine((value) => Boolean(value.path) || Boolean(value.code), { + message: 'Provide path or code', }); const runJavascriptOutputSchema = z.object({ @@ -41,7 +47,7 @@ export function createRunJavascriptTool( ): BuiltTool { return new Tool('workspace_run_javascript') .description( - 'Run a JavaScript file in the sandbox with node. Provide exactly one of path or code. Read Attachments from `$N8N_UPLOADS_DIR` or `$N8N_UPLOADS_MANIFEST`. Write files the user should retrieve only under `$N8N_OUTPUTS_DIR`. This tool has a 60 second limit. Use `workspace_execute_command` for longer jobs.', + 'Run a JavaScript file in the sandbox with node. Provide path or code. If both are set, code is used. Read Attachments from `$N8N_UPLOADS_DIR` or `$N8N_UPLOADS_MANIFEST`. Write files the user should retrieve only under `$N8N_OUTPUTS_DIR`. This tool has a 60 second limit. Use `workspace_execute_command` for longer jobs.', ) .input(runJavascriptInputSchema) .output(runJavascriptOutputSchema) @@ -64,7 +70,7 @@ export function createRunJavascriptTool( } else if (input.path) { scriptPath = input.path; } else { - throw new Error('Provide exactly one of path or code'); + throw new Error('Provide path or code'); } const result = await sandbox.executeCommand('node', [scriptPath], {