From a7661cb4f45ee2d9a989f8a61fe1bea5597a0ddf Mon Sep 17 00:00:00 2001 From: Jaakko Husso Date: Thu, 27 Aug 2026 12:47:15 +0000 Subject: [PATCH] test(core): Support tool-result cleanup in the eval stub workspace (no-changelog) (#37202) Co-authored-by: Claude Opus 5 (1M context) --- .../__tests__/stub-workspace.test.ts | 29 +++++++++++++++++++ .../evaluations/harness/stub-workspace.ts | 25 ++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/packages/@n8n/instance-ai/evaluations/__tests__/stub-workspace.test.ts b/packages/@n8n/instance-ai/evaluations/__tests__/stub-workspace.test.ts index 78a810fdb5d..70d19deddf5 100644 --- a/packages/@n8n/instance-ai/evaluations/__tests__/stub-workspace.test.ts +++ b/packages/@n8n/instance-ai/evaluations/__tests__/stub-workspace.test.ts @@ -28,6 +28,35 @@ describe('createStubWorkspace', () => { await expect(filesystem().readFile('missing.ts')).rejects.toThrow('No such file'); }); + // The runtime offloads oversized tool results to `tool-results/runs//` + // and clears that directory at the end of every run, calling the filesystem + // directly rather than through the exposed tools. + describe('tool-result cleanup', () => { + const runDirectory = 'tool-results/runs/run-hash'; + + it('reports a directory as existing once a file sits under it', async () => { + const fs = filesystem(); + await fs.writeFile(`${runDirectory}/call.result.json`, '{}'); + + await expect(fs.exists(runDirectory)).resolves.toBe(true); + }); + + it('reports a directory that was never written as missing', async () => { + await expect(filesystem().exists(runDirectory)).resolves.toBe(false); + }); + + it('removes every file under the directory it is given, and nothing else', async () => { + const fs = filesystem(); + await fs.writeFile(`${runDirectory}/call.result.json`, '{}'); + await fs.writeFile('tool-results/runs/other-hash/call.result.json', '{}'); + + await fs.rmdir(runDirectory); + + await expect(fs.exists(runDirectory)).resolves.toBe(false); + await expect(fs.exists('tool-results/runs/other-hash')).resolves.toBe(true); + }); + }); + it('offers only the tools production exposes', () => { expect( createStubWorkspace() diff --git a/packages/@n8n/instance-ai/evaluations/harness/stub-workspace.ts b/packages/@n8n/instance-ai/evaluations/harness/stub-workspace.ts index 1df3ef8d61f..49a859d511e 100644 --- a/packages/@n8n/instance-ai/evaluations/harness/stub-workspace.ts +++ b/packages/@n8n/instance-ai/evaluations/harness/stub-workspace.ts @@ -4,6 +4,11 @@ // Without a workspace, `build-workflow` fails its source read with a // `code_fixable` remediation telling the agent to write the file with // `workspace_write_file` — a tool that only exists when a workspace is attached. +// +// Tool exposure is filtered to CORE_WORKSPACE_TOOL_NAMES, but the runtime also +// calls the filesystem directly for its own bookkeeping — offloading oversized +// tool results and clearing them at the end of every run — so those operations +// are implemented rather than rejected. // --------------------------------------------------------------------------- import { @@ -58,13 +63,29 @@ class InMemoryWorkspaceFilesystem implements WorkspaceFilesystem { await Promise.resolve(); } + async exists(path: string): Promise { + const key = relativePath(path); + if (this.files.has(key)) return await Promise.resolve(true); + const prefix = `${key}/`; + for (const file of this.files.keys()) { + if (file.startsWith(prefix)) return await Promise.resolve(true); + } + return await Promise.resolve(false); + } + + async rmdir(path: string): Promise { + const prefix = `${relativePath(path)}/`; + for (const file of this.files.keys()) { + if (file.startsWith(prefix)) this.files.delete(file); + } + await Promise.resolve(); + } + appendFile = unreachable; deleteFile = unreachable; copyFile = unreachable; moveFile = unreachable; - rmdir = unreachable; readdir = unreachable; - exists = unreachable; stat = unreachable; }