diff --git a/package.json b/package.json index 8815a69a7..0e1f2aee1 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test:mcp-conformance": "pnpm build && AGENT_BUNDLE_MCP_CONFORMANCE=1 rstest --config rstest.mcp-conformance.config.ts", "test:native-host": "pnpm build && AGENT_BUNDLE_NATIVE_HOST_CONTRACTS=1 rstest --config rstest.native-host.config.ts", "test:watch": "rstest --config rstest.config.ts --watch", - "lint": "rslint .", + "lint": "rslint . && node scripts/check-test-remove-tree.mjs", "bench:hook-cold-start": "node scripts/measure-hook-cold-start.mjs", "record:claude-hooks-fixtures": "node scripts/record-claude-hooks-schema-fixtures.mjs", "typecheck": "node scripts/check-dist-fresh.mjs && tsc --noEmit && tsc --project packages/workbench/tsconfig.json && tsc --project packages/create-agent-bundle/tsconfig.json && tsc --project packages/rsc-markdown-stream/tsconfig.json && tsc --project packages/agent-bundle/tsconfig.web-host.json && pnpm --filter @agent-bundle/docs typecheck", diff --git a/packages/agent-bundle/tests/adapter-contract.test.ts b/packages/agent-bundle/tests/adapter-contract.test.ts index dad182af9..6029197a2 100644 --- a/packages/agent-bundle/tests/adapter-contract.test.ts +++ b/packages/agent-bundle/tests/adapter-contract.test.ts @@ -1,5 +1,5 @@ import { supportedCapabilities } from './support/adapter-capabilities.ts'; -import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -11,6 +11,7 @@ import type { TargetAdapter } from '../src/adapters/types.ts'; import { normalizeProject } from '../src/config/normalize.ts'; import type { LoadedConfig } from '../src/config/load.ts'; import { validateModel } from '../src/config/validate.ts'; +import { removeTree } from './support/remove-tree.ts'; const metadata = Object.freeze({ adapterRevision: 'test', @@ -87,7 +88,7 @@ it('delegates selected native hook sources through registered adapters', async ( target: 'example', }]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -180,7 +181,7 @@ it('normalizes malformed native hook source values into diagnostics without skip target: 'invalid', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -216,6 +217,6 @@ it('normalizes thrown native hook sources into diagnostics', async () => { target: 'throws', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/amp-adapter.test.ts b/packages/agent-bundle/tests/amp-adapter.test.ts index 9ae76e0b4..4d70ab0d1 100644 --- a/packages/agent-bundle/tests/amp-adapter.test.ts +++ b/packages/agent-bundle/tests/amp-adapter.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, rename, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -16,6 +16,7 @@ import { projectEventDocument } from '../src/events/projection.ts'; import { compileRouteGraph, emptyCompiledRouteGraph } from '../src/routes/graph.ts'; import { build } from './support/build.ts'; import { runNodeScript } from './support/run-node-script.ts'; +import { removeTree } from './support/remove-tree.ts'; const configPath = '/workspace/agent-bundle.config.ts'; const skillSource = '/workspace/src/skills/review/SKILL.md'; @@ -200,7 +201,7 @@ it('emits compilable private factory names for punctuation and reserved bindings expect(typeof loaded.default).toBe('function'); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -540,7 +541,7 @@ it('registers inline documented callbacks and maps every native result exactly', } finally { if (previousBun === undefined) Reflect.deleteProperty(globalThis, 'Bun'); else Reflect.set(globalThis, 'Bun', previousBun); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -683,7 +684,7 @@ it('builds a relocatable self-contained Amp artifact with manifest and evidence }); expect(registrations).toEqual(['skills/review']); } finally { - await rm(projectRoot, { force: true, recursive: true }); + await removeTree(projectRoot); } }); @@ -747,7 +748,7 @@ it('compiles a nested Amp hook wrapper that returns the documented tool.call dec stdout: '{"action":"reject-and-continue","message":"blocked"}', }); } finally { - await rm(projectRoot, { force: true, recursive: true }); + await removeTree(projectRoot); } }); @@ -832,6 +833,6 @@ it('runs a relocated standalone event route with its worker inside the Amp plugi stdout: '{"action":"reject-and-continue","message":"blocked"}', }); } finally { - await rm(projectRoot, { force: true, recursive: true }); + await removeTree(projectRoot); } }); diff --git a/packages/agent-bundle/tests/amp-install.test.ts b/packages/agent-bundle/tests/amp-install.test.ts index 80f3c1412..d5e1ccb84 100644 --- a/packages/agent-bundle/tests/amp-install.test.ts +++ b/packages/agent-bundle/tests/amp-install.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, readdir, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -9,6 +9,7 @@ import { installBundle, type InstallCommandRunner } from '../src/install/install import { readInstallReceipt } from '../src/install/receipt.ts'; import { uninstallBundle } from '../src/install/uninstall.ts'; import { writeInstallFixtureManifest } from './support/install-fixture.ts'; +import { removeTree } from './support/remove-tree.ts'; const isolatedEnvironment: Readonly = {}; const pluginName = 'amp-install-fixture'; @@ -138,7 +139,7 @@ it('installs, replaces, and uninstalls only the receipt-owned Amp directory', as await expect(readFile(join(destination, 'disabled-state.json'), 'utf8')).resolves.toBe('{"disabled":true}\n'); await expect(readFile(settings, 'utf8')).resolves.toBe('{"amp.plugins.disabled":["amp-install-fixture"]}\n'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -185,7 +186,7 @@ it('uses the documented XDG system and project plugin roots without touching Amp expect(uninstalled.state).toBe('uninstalled'); await expect(readFile(join(projectRoot, '.amp', 'settings.json'), 'utf8')).resolves.toBe('{"trusted":false}\n'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -208,7 +209,7 @@ it('installs a mixed-case portable plugin name accepted by the Amp planner', asy }); expect(installed.destination).toBe(join(home, '.config', 'amp', 'plugins', name)); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -236,7 +237,7 @@ it('rejects an Amp manifest name that could escape the plugin root', async () => })).rejects.toThrow('not a safe local plugin name'); await expect(readFile(join(home, '.config', 'escape', 'index.js'), 'utf8')).rejects.toThrow(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -261,7 +262,7 @@ it('refuses to replace a foreign Amp directory even with --replace', async () => })).rejects.toThrow('foreign install'); await expect(readFile(join(destination, 'index.js'), 'utf8')).resolves.toContain('foreign'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -287,7 +288,7 @@ it('refuses a symlinked Amp plugin ancestor before writing outside the host root })).rejects.toThrow('unsupported filesystem entry'); expect(await readdir(outside)).toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -309,6 +310,6 @@ it('refuses modified or unlisted files inside the generated Amp directory', asyn scope: 'user', })).rejects.toThrow('does not match its manifest-owned directory'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/api.test.ts b/packages/agent-bundle/tests/api.test.ts index 07d78c87b..a76eb2376 100644 --- a/packages/agent-bundle/tests/api.test.ts +++ b/packages/agent-bundle/tests/api.test.ts @@ -1,5 +1,5 @@ import { supportedCapabilities } from './support/adapter-capabilities.ts'; -import { chmod, mkdtemp, mkdir, readFile, rm, stat, symlink, writeFile } from 'node:fs/promises'; +import { chmod, mkdtemp, mkdir, readFile, stat, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -26,6 +26,7 @@ import { resolveTargetRelativeStdioArgument, } from '../src/services/mcp-runtime.ts'; import { createMcpPathTokenResolver, standardMcpPathTokens } from '../src/services/mcp-path-tokens.ts'; +import { removeTree } from './support/remove-tree.ts'; const createProject = async (): Promise => { const parent = await mkdtemp(join(tmpdir(), 'agent-bundle-api-parent-')); @@ -179,7 +180,7 @@ it('prepares and inspects a target owned only by the supplied advanced registry' expect(result.plans[0]?.selected).toEqual([]); expect(registry.names()).toEqual(['synthetic']); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -222,7 +223,7 @@ it('projects only the capability contract fields of adapter-owned rows into insp ]); expect(() => JSON.stringify(result.plans)).not.toThrow(); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -261,7 +262,7 @@ it('accepts claude.userConfig through the public inspection and build APIs', asy ) as Record; expect(manifest).toHaveProperty('userConfig.api_token.sensitive', true); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -288,7 +289,7 @@ it('returns a frozen invalid inspection for opaque source failures', async () => expect(Object.isFrozen(result.diagnostics)).toBe(true); expect(Object.isFrozen(result.plans)).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -311,7 +312,7 @@ it('attaches a specific recovery to every invalid inspection diagnostic', async }), ])); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -341,7 +342,7 @@ it('accepts the public claude.dependencies config surface and plans its manifest { marketplace: 'acme-shared', name: 'policy-kit', version: '^2.0' }, ]); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -369,7 +370,7 @@ it('reports one modern-MCP source diagnostic for a legacy SSE declaration', asyn expect(Object.isFrozen(result.diagnostics)).toBe(true); expect(Object.isFrozen(diagnostics[0])).toBe(true); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -408,7 +409,7 @@ it('resolves artifact output with CLI, config, and default precedence', async () expect(defaults.build.outputRoot).toBe(join(root, 'dist')); expect((await stat(join(root, 'dist'))).isDirectory()).toBe(true); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }, 30_000); @@ -470,7 +471,7 @@ it('build runs the Claude developer validator and load check over built claude t expect(strict.hostValidation?.[0]?.status).toBe('failed'); expect(strict.diagnostics.filter((entry) => entry.code === 'AB6020').every((entry) => entry.severity === 'error')).toBe(true); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }, 30_000); @@ -503,7 +504,7 @@ it('build surfaces a Claude load refusal as AB7325 even when plugin validate --s target: 'claude', })]); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }, 30_000); @@ -535,7 +536,7 @@ it('build reports one informational AB6019 skip for all Claude-validated targets ]); expect(result.diagnostics.some((entry) => entry.severity === 'error')).toBe(false); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }, 30_000); @@ -617,7 +618,7 @@ it('deduplicates identical adapter diagnostics without collapsing distinct stabl expect(Object.isFrozen(prepared.diagnostics)).toBe(true); expect(prepared.diagnostics.every((entry) => Object.isFrozen(entry))).toBe(true); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -664,7 +665,7 @@ it('contains hostile source getters as reusable preparation diagnostics', async expect(recovered.source.state).toBe('ready'); expect(recovered.model?.targets).toEqual([expect.objectContaining({ name: 'codex' })]); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -701,7 +702,7 @@ it('fails closed when routes getter throws during inspection', async () => { expect(Object.isFrozen(result)).toBe(true); expect(Object.isFrozen(result.diagnostics)).toBe(true); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -760,7 +761,7 @@ it('contains a throwing adapter plan as a reusable preparation diagnostic', asyn expect(recovered.source.state).toBe('ready'); expect(recovered.model?.targets).toEqual([expect.objectContaining({ name: syntheticTarget })]); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -803,7 +804,7 @@ it('contains an adapter planner that fails after preparation during inspect', as expect(Object.isFrozen(result)).toBe(true); expect(Object.isFrozen(result.diagnostics)).toBe(true); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -832,7 +833,7 @@ it('returns an invalid inspection for selected targets outside the normalized pr expect(Object.isFrozen(invalid.plans)).toBe(true); } } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -939,7 +940,7 @@ it('reports skipped target/component pairs against each target emission surface' expect(planFor('cursor')?.skipped.some((component) => component.kind === 'rule')).toBe(false); expect(Object.isFrozen(planFor('portable')?.skipped)).toBe(true); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -1069,7 +1070,7 @@ it('accounts lsp servers and event routes as distinct canonical kinds with a per }); expect(planFor('portable').kinds.find((report) => report.kind === 'event-route')).toEqual({ kind: 'event-route', selected: 0, skipped: 1 }); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -1125,7 +1126,7 @@ it('accounts an admitted degraded event route as selected, matching the validati expect(plan.skipped.some((component) => component.kind === 'event-route')).toBe(false); expect(plan.kinds.find((report) => report.kind === 'event-route')).toEqual({ kind: 'event-route', selected: 1, skipped: 0 }); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -1179,7 +1180,7 @@ it('judges event-route admission and lsp emission by the component-emission over ])); expect(result.model.lspServers).toEqual([expect.objectContaining({ declaredBy: 'synthetic', targets: [syntheticTarget] })]); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -1210,7 +1211,7 @@ it('never reports an lsp component as selected when the declaring planner reject expect.objectContaining({ code: 'claude.lsp.extension.conflict', severity: 'error' }), ])); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -1285,7 +1286,7 @@ it('reports omitted component features per target from the host feature rows (#1 ]); expect(validated.diagnostics.some((diagnostic) => diagnostic.code === 'AB4927' || diagnostic.code === 'AB4907' || diagnostic.code === 'AB4908')).toBe(false); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -1355,7 +1356,7 @@ it('never counts an opaque third-party lspServers declaration as emitted by a ho expect(claudePlan.selected.some((component) => component.kind === 'lsp')).toBe(false); expect(claudePlan.entries.some((entry) => entry.relativePath === '.lsp.json')).toBe(false); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -1378,7 +1379,7 @@ it('reports target exclusion before unsupported capability when both omit a comp expect(skippedHook).toMatchObject({ reason: 'excluded-by-targets' }); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -1407,7 +1408,7 @@ it('surfaces the computed native matcher on inspected hook entries', async () => expect(sessionStart).toBeDefined(); expect(sessionStart?.nativeMatcher).toBeUndefined(); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }); @@ -1498,7 +1499,7 @@ it('keeps one supplied registry through advanced artifact, hook, and MCP operati }); expect(registry.names()).toEqual([syntheticTarget]); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }, 60_000); @@ -1535,7 +1536,7 @@ it('prepares a factory-configured project into a frozen inspection and build res } await expect(validate({ artifact: hookArtifact, root })).resolves.toEqual({ diagnostics: [] }); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }, 30_000); @@ -1562,8 +1563,8 @@ it('returns an output-independent project context without absolute project paths expect(Object.isFrozen(left.projectContext)).toBe(true); } finally { await Promise.all([ - rm(join(leftRoot, '..'), { force: true, recursive: true }), - rm(join(rightRoot, '..'), { force: true, recursive: true }), + removeTree(join(leftRoot, '..')), + removeTree(join(rightRoot, '..')), ]); } }, 30_000); @@ -1643,8 +1644,8 @@ it('keeps rule and command model digests root-independent and sensitive to conte expect(changedCommand.projectContext.modelDigest).not.toBe(left.projectContext.modelDigest); } finally { await Promise.all([ - rm(join(leftRoot, '..'), { force: true, recursive: true }), - rm(join(rightRoot, '..'), { force: true, recursive: true }), + removeTree(join(leftRoot, '..')), + removeTree(join(rightRoot, '..')), ]); } }); @@ -1672,7 +1673,7 @@ it('rejects an output beneath an escaping symlink before loading source or writi await expect(readFile(marker, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); await expect(stat(join(external, 'artifact'))).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }, 30_000); @@ -1696,7 +1697,7 @@ it('rejects a dangling output symlink before loading source', async () => { }); await expect(readFile(marker, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }, 30_000); @@ -1720,7 +1721,7 @@ it('rejects an output symlink to the project root before loading source', async }); await expect(readFile(marker, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }, 30_000); @@ -1734,7 +1735,7 @@ it('allows an output below a symlink to the project root', async () => { expect(result.build.outputRoot).toBe(join(root, 'alias', 'artifact')); expect((await stat(join(root, 'artifact'))).isDirectory()).toBe(true); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }, 30_000); @@ -1767,8 +1768,8 @@ it('excludes a contained symlinked output tree from project context identity', a expect(JSON.stringify(right.projectContext)).not.toContain('actual-output'); } finally { await Promise.all([ - rm(join(leftRoot, '..'), { force: true, recursive: true }), - rm(join(rightRoot, '..'), { force: true, recursive: true }), + removeTree(join(leftRoot, '..')), + removeTree(join(rightRoot, '..')), ]); } }, 30_000); @@ -1830,7 +1831,7 @@ it('normalizes named top-level scripts with stable IDs, modes, and sorted target }, ]); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); @@ -1885,7 +1886,7 @@ it('builds conventional src/scripts modules beside explicit entries', async () = await expect(readFile(join(output, 'scripts', 'greet.mjs'), 'utf8')).resolves.toContain('hello from convention'); await expect(stat(join(output, 'scripts', 'claimed.mjs'))).resolves.toBeDefined(); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); @@ -1925,7 +1926,7 @@ it('refuses unshippable conventional script routes with actionable diagnostics', expect(diagnostic.sourcePath).toContain(join('src', 'scripts')); } } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); @@ -2027,7 +2028,7 @@ it('copies every supported top-level script output suffix byte-for-byte with sou diagnostics: [{ code: 'AB6004', generatedPath: 'agent-bundle.manifest.json' }], }); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }, 30_000); @@ -2072,7 +2073,7 @@ it('canonicalizes copied script extensions in emitted artifact paths', async () ])); await expect(validate({ artifact: output, root })).resolves.toEqual({ diagnostics: [] }); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); @@ -2111,7 +2112,7 @@ it('documents a versioned MCP App resource URI accepted by source validation', a expect(result.diagnostics.map((diagnostic) => diagnostic.code)).not.toContain('AB4329'); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); @@ -2154,7 +2155,7 @@ it('rejects unsafe, unsupported, missing, non-file, and unknown-target named scr 'AB4406', ])); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); @@ -2170,7 +2171,7 @@ it('lists hooks across artifact targets and rejects an explicit unknown target', ]); await expect(listHooks({ artifact, root, target: 'unsupported' })).rejects.toThrow('Unknown target'); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }, 30_000); @@ -2186,6 +2187,6 @@ it('validates an explicit artifact without loading its project source', async () expect(result).toEqual({ diagnostics: [] }); expect(Object.isFrozen(result.diagnostics)).toBe(true); } finally { - await rm(join(root, '..'), { force: true, recursive: true }); + await removeTree(join(root, '..')); } }, 30_000); diff --git a/packages/agent-bundle/tests/artifact-cli-bin.test.ts b/packages/agent-bundle/tests/artifact-cli-bin.test.ts index 9c3c705b5..8913f5858 100644 --- a/packages/agent-bundle/tests/artifact-cli-bin.test.ts +++ b/packages/agent-bundle/tests/artifact-cli-bin.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { chmod, mkdir, mkdtemp, readFile, rm, stat, symlink, writeFile } from 'node:fs/promises'; +import { chmod, mkdir, mkdtemp, readFile, stat, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { promisify } from 'node:util'; @@ -11,12 +11,13 @@ import type { TargetAdapter } from '../src/adapters/types.ts'; import { build, inspect } from '../src/api.ts'; import { validateArtifact } from '../src/build/validate-artifact.ts'; import type { NormalizedPlugin } from '../src/core/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const writeProjectFile = async (root: string, path: string, contents: string): Promise => { @@ -405,7 +406,7 @@ it('lets a skill reach the artifact bin through the plugin-root token, and the b it('emits a self-contained routed bin for a web-only plugin', { retry: 1, timeout: 240_000 }, async () => { const root = await createFixture({ targets: ['portable'], web: true }); - await rm(join(root, 'src', 'cli'), { force: true, recursive: true }); + await removeTree(join(root, 'src', 'cli')); const result = await build({ output: 'artifact', root }); const binPath = join(root, 'artifact', 'bin', `${pluginName}.mjs`); diff --git a/packages/agent-bundle/tests/artifact-inspection-service.test.ts b/packages/agent-bundle/tests/artifact-inspection-service.test.ts index e533ed1ea..5387f983f 100644 --- a/packages/agent-bundle/tests/artifact-inspection-service.test.ts +++ b/packages/agent-bundle/tests/artifact-inspection-service.test.ts @@ -22,6 +22,7 @@ import type { ArtifactEpoch } from '../src/dev/types.ts'; import { agentSkillsSchemaRevision } from '../src/schemas/agent-skills/contract.ts'; import { createTargetMcpRuntime, type TargetMcpRuntimeContract } from '../src/services/mcp-runtime.ts'; import { sha256Hex } from '../src/core/digest.ts'; +import { removeTree } from './support/remove-tree.ts'; interface FixtureFile { readonly contents: string; @@ -553,7 +554,7 @@ it('inspects one validated epoch as sorted, source-free artifact facts', async ( ]); expect(JSON.stringify(inspection)).not.toContain('do-not-expose'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -575,7 +576,7 @@ it('never publishes a manifested MCP host without its projection MCP document', })).rejects.toThrow(`projections["${fixtureTarget}"].documents.mcp is absent, but the target's MCP manifest is "mcp.json".`); await expect(store.listEpochs()).resolves.toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -600,7 +601,7 @@ it('revalidates an epoch on each inspection so post-publication corruption is vi code: 'ARTIFACT_INSPECTION_INVALID', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -666,7 +667,7 @@ it.each(provenanceTamperCases)('refuses to inspect an epoch whose provenance is }); expect(store).toMatchObject({ acquired: 1, closed: 1 }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -699,8 +700,8 @@ it('inspects identical root-relative provenance after the published epochs are r expect(serialized).not.toContain(relocated); expect(store).toMatchObject({ acquired: 1, closed: 1 }); } finally { - await rm(root, { force: true, recursive: true }); - await rm(relocated, { force: true, recursive: true }); + await removeTree(root); + await removeTree(relocated); } }); @@ -732,7 +733,7 @@ it('returns deeply frozen detached inspection records', async () => { expect((await service.inspect('epoch-immutable')).project.sourceInputs[0]!.path).toBe(configPath); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -760,7 +761,7 @@ it('uses callback facts captured during validation and excludes unmanifested mut }]); expect(JSON.stringify(inspection.runtime)).not.toContain('not-manifested.mjs'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -789,7 +790,7 @@ it('preserves the supplied runtime resolver call sequence while inspecting valid name: 'runner', })]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -810,7 +811,7 @@ it('accepts an exact registry with a non-configurable own method', async () => { epochId: 'epoch-registry-identity', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -849,7 +850,7 @@ it('retains immutable inspection evidence when manifest and hook bytes are repla expect(Object.isFrozen(result.snapshot!.manifest.files)).toBe(true); expect(Object.isFrozen(result.snapshot!.runtime.hooks)).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -866,7 +867,7 @@ it('fails closed without an inspection when an acquired artifact file cannot be }); expect(store).toMatchObject({ acquired: 1, closed: 1 }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -883,7 +884,7 @@ it('surfaces release failure after inspecting and closes every acquired referenc }); expect(store).toMatchObject({ acquired: 1, closed: 1 }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -906,7 +907,7 @@ it('releases references after successful and invalid artifact inspections', asyn }); expect(store).toMatchObject({ acquired: 2, closed: 2 }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -925,7 +926,7 @@ it('uses the exact supplied registry and fails closed with the default registry' epochId: 'epoch-registry', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -977,7 +978,7 @@ it('diffs exact epochs by artifact facts with stable lexical records', async () ]); expect(store).toMatchObject({ acquired: 4, closed: 4 }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -992,7 +993,7 @@ it('releases an acquired base reference when candidate acquisition fails', async .rejects.toMatchObject({ code: 'EPOCH_NOT_FOUND' }); expect(store).toMatchObject({ acquired: 1, closed: 1 }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1010,7 +1011,7 @@ it('surfaces release failure when diff closes a partially acquired reference', a }); expect(store).toMatchObject({ acquired: 1, closed: 1 }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1047,6 +1048,6 @@ it('compares canonical file source-input paths rather than project input hashes' 'scripts/source.mjs', ]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/artifact-validator.test.ts b/packages/agent-bundle/tests/artifact-validator.test.ts index 5cbf5307e..de7cda7f9 100644 --- a/packages/agent-bundle/tests/artifact-validator.test.ts +++ b/packages/agent-bundle/tests/artifact-validator.test.ts @@ -40,6 +40,7 @@ import { agentSkillsSchemaRevision } from '../src/schemas/agent-skills/contract. import { createMcpPathTokenResolver } from '../src/services/mcp-path-tokens.ts'; import { createTargetMcpRuntime } from '../src/services/mcp-runtime.ts'; import type { NormalizedPlugin } from '../src/core/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const hash = (value: string): string => sha256Hex(value); @@ -187,7 +188,7 @@ it('accepts compile evidence that covers a matching bundle', async () => { try { expect((await validateArtifact({ artifactRoot: root })).filter((diagnostic) => diagnostic.code === 'AB6039')).toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -203,7 +204,7 @@ it('reports compile evidence for different bundle bytes', async () => { expect.objectContaining({ code: 'AB6039', message: expect.stringContaining('describes different bytes') }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -216,7 +217,7 @@ it('reports compile evidence that does not cover a bundle', async () => { expect.objectContaining({ code: 'AB6039', message: expect.stringContaining('does not cover') }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -232,7 +233,7 @@ it('reports compile evidence that names a copy file', async () => { expect.objectContaining({ code: 'AB6039', message: expect.stringContaining('does not list as a compiled file') }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -255,7 +256,7 @@ it('reports a non-builtin external in compile evidence', async () => { expect.objectContaining({ code: 'AB6039', message: expect.stringContaining('is not one') }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -279,7 +280,7 @@ it('reports a missing artifact-relative external target in compile evidence', as expect.objectContaining({ code: 'AB6039', message: expect.stringContaining('does not contain') }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -295,7 +296,7 @@ it('reports compile evidence from a different policy revision', async () => { expect.objectContaining({ code: 'AB6039', message: expect.stringContaining('was judged under policy') }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -311,7 +312,7 @@ it('reports malformed compile evidence as a non-strict record', async () => { }), ]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -484,8 +485,8 @@ it('validates and owns every concrete document matched by an optional schema fam ); } finally { await Promise.all([ - rm(validRoot, { force: true, recursive: true }), - rm(invalidRoot, { force: true, recursive: true }), + removeTree(validRoot), + removeTree(invalidRoot), ]); } }); @@ -531,8 +532,8 @@ it('admits only direct .mdc files in a declared rules layout', async () => { ); } finally { await Promise.all([ - rm(validRoot, { force: true, recursive: true }), - rm(invalidRoot, { force: true, recursive: true }), + removeTree(validRoot), + removeTree(invalidRoot), ]); } }); @@ -559,8 +560,8 @@ it('admits only direct .md files in a declared commands layout', async () => { ); } finally { await Promise.all([ - rm(validRoot, { force: true, recursive: true }), - rm(invalidRoot, { force: true, recursive: true }), + removeTree(validRoot), + removeTree(invalidRoot), ]); } }); @@ -760,7 +761,7 @@ it('validates an emitted Skill and copied resources from the artifact only', asy try { expect(await validateArtifact({ artifactRoot: root, registry: customRegistry() })).toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -796,7 +797,7 @@ it('returns frozen validated evidence without changing the diagnostics-only vali expect(Object.isFrozen(result.snapshot!.manifest.compiler.validation.projections[0]!)).toBe(true); expect(await validateArtifact({ artifactRoot: root, registry: customRegistry() })).toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -816,7 +817,7 @@ it('rejects a rehashed top-level artifact file outside declared target namespace expect.objectContaining({ code: 'AB6004' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -836,7 +837,7 @@ it('rejects a rehashed file outside a declared target emitted layout', async () expect.objectContaining({ code: 'AB6004' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -853,7 +854,7 @@ it('accepts a manifested target asset emitted by the core build', async () => { try { await expect(validateArtifact({ artifactRoot: root, registry: customRegistry() })).resolves.toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -882,8 +883,8 @@ it('rejects malformed and unmanifested target asset paths', async () => { expect.objectContaining({ code: 'AB6004' }), ])); } finally { - await rm(malformedRoot, { force: true, recursive: true }); - await rm(unmanifestedRoot, { force: true, recursive: true }); + await removeTree(malformedRoot); + await removeTree(unmanifestedRoot); } }); @@ -902,7 +903,7 @@ it('rejects an artifact symlink even when the manifest remains self-consistent', expect.objectContaining({ code: 'AB6004' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -925,8 +926,8 @@ it('rejects a special manifest without following its symlink target', async () = expect.objectContaining({ code: 'AB6001' }), ])); } finally { - await rm(root, { force: true, recursive: true }); - await rm(outside, { force: true, recursive: true }); + await removeTree(root); + await removeTree(outside); } }); @@ -945,7 +946,7 @@ it('rejects a canonical manifest whose runtime is below the generated floor', as expect.objectContaining({ code: 'AB6001', generatedPath: 'agent-bundle.manifest.json' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -967,7 +968,7 @@ it('settles promptly when the artifact manifest is a FIFO', async () => { expect.objectContaining({ code: 'AB6013', generatedPath: 'agent-bundle.manifest.json' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -996,8 +997,8 @@ it('rejects empty declared and undeclared target directories independently of ma expect.objectContaining({ code: 'AB6004' }), ])); } finally { - await rm(emptyRoot, { force: true, recursive: true }); - await rm(declaredRoot, { force: true, recursive: true }); + await removeTree(emptyRoot); + await removeTree(declaredRoot); } }); @@ -1014,7 +1015,7 @@ it('rejects a nested empty directory under an otherwise valid target namespace', ]), ); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1038,7 +1039,7 @@ it('rejects forged hook output for a target without a hook contract', async () = expect.objectContaining({ code: 'AB6014', generatedPath: 'hooks/junk.txt' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1064,7 +1065,7 @@ it('rejects a canonically rehashed script with an unsupported extension', async expect.objectContaining({ code: 'AB6004' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1117,7 +1118,7 @@ it('fails ordinary artifact validation when an emitted portable tree breaks the .toEqual(normative.map((entry) => entry.message)); expect(hostValidated.diagnostics.some((entry) => entry.code === 'AB6038')).toBe(false); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1149,8 +1150,8 @@ it('does not follow a symlinked portable document into the byte lane once the in // The forged content was never read: no schema or normative finding from behind the link. expect(diagnostics.filter((entry) => ['AB6035', 'AB6036', 'AB6037'].includes(entry.code))).toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); - await rm(outside, { force: true, recursive: true }); + await removeTree(root); + await removeTree(outside); } }); @@ -1177,7 +1178,7 @@ it('leaves an advanced registry adapter that reuses the portable name to its own // identity, so a custom adapter under the portable name owes neither (#592). expect(diagnostics).toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1199,7 +1200,7 @@ it('admits nested project assets in the target-owned recursive asset namespace', try { await expect(validateArtifact({ artifactRoot: root, registry })).resolves.toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1214,7 +1215,7 @@ it('admits executable commands and nested support files in a recursive bin names try { await expect(validateArtifact({ artifactRoot: root, registry: customRegistry() })).resolves.toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1237,7 +1238,7 @@ it.each([ expect.objectContaining({ code: 'AB6004' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1255,7 +1256,7 @@ it('rejects emitted Skill Markdown without instruction body content', async () = ]), ); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1283,7 +1284,7 @@ it('validates emitted Skill frontmatter against the pinned contract and director expect.objectContaining({ code: 'AB6004' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1305,7 +1306,7 @@ it('rejects noncanonical and duplicate-key manifests as strict parse failures', ]); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1319,7 +1320,7 @@ it('matches a canonical nested manifest file table by path instead of directory try { expect(await validateArtifact({ artifactRoot: root, registry: customRegistry() })).toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1333,7 +1334,7 @@ it('rejects a canonical manifest that omits an executable file mode', async () = expect.objectContaining({ code: 'AB6004', generatedPath: 'agent-bundle.manifest.json' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1369,7 +1370,7 @@ it('preserves structural artifact diagnostics after a strict manifest passes', a expect.objectContaining({ code: 'AB6004', generatedPath: 'agent-bundle.manifest.json' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1396,7 +1397,7 @@ it('reports an orphan compiler MCP output after the artifact is rehashed', async expect.objectContaining({ code: 'AB6004' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1444,7 +1445,7 @@ it('does not attribute compiler MCP outputs to an equal-length sibling target', expect.objectContaining({ code: 'AB6017' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1469,7 +1470,7 @@ it.each([ const matching = diagnostics.some((entry) => entry.code === 'AB6017' && entry.generatedPath === 'native/servers.json'); expect(matching).toBe(expectsDiagnostic); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1497,7 +1498,7 @@ it('rejects a target-local file URL argument that is absent from the artifact', expect.objectContaining({ code: 'AB6017', generatedPath: nativePath, target: coherenceTarget }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1524,7 +1525,7 @@ it.each([ expect.objectContaining({ code: 'AB6017', generatedPath: 'mcp/mcp-server-deadbeef.mjs', target: coherenceTarget }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1608,7 +1609,7 @@ it('rejects host document launches that disagree with the manifest', async () => expect(agreementDiagnostics(await validateArtifact({ artifactRoot: root, registry: coherenceRegistry() }))).toEqual(cases[index]!.expected); } } finally { - await Promise.all(roots.map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.map((root) => removeTree(root))); } }); @@ -1629,7 +1630,7 @@ it('rejects duplicate keys in a canonically manifested native MCP document', asy expect.objectContaining({ code: 'AB6004' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1650,7 +1651,7 @@ it('requires a manifest hook row when native hook metadata is present', async () expect.objectContaining({ code: 'AB6018', generatedPath: 'hooks/hooks.json', target: hookCoherenceTarget }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1676,7 +1677,7 @@ it('reports a compiler-pattern native hook command that is not indexed', async ( expect.objectContaining({ code: 'AB6004' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1701,7 +1702,7 @@ it.each([ ]), ); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1716,7 +1717,7 @@ it('accepts inert top-level throws, rejections, and never-settling awaits', asyn try { await expect(validateArtifact({ artifactRoot: root, registry: customRegistry() })).resolves.toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1741,7 +1742,7 @@ it.each([ ]), ); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1765,7 +1766,7 @@ it('allows Node builtins and manifest-listed JSON terminal imports', async () => try { await expect(validateArtifact({ artifactRoot: root, registry: customRegistry() })).resolves.toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1786,7 +1787,7 @@ it('rejects non-literal dynamic imports', async () => { ]), ); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1799,7 +1800,7 @@ it('imports a self-contained generated module at a path with spaces', async () = try { await expect(validateArtifact({ artifactRoot: root, registry: customRegistry() })).resolves.toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1828,8 +1829,8 @@ it('rejects generated JavaScript that resolves a dependency outside the artifact ); } finally { await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(outside, { force: true, recursive: true }), + removeTree(root), + removeTree(outside), ]); } }); @@ -1847,7 +1848,7 @@ it('rejects an existing JavaScript dependency omitted from the manifest', async expect.objectContaining({ code: 'AB6004', generatedPath: 'agent-bundle.manifest.json' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1861,7 +1862,7 @@ it('accepts deterministic cycles between manifested JavaScript modules', async ( try { await expect(validateArtifact({ artifactRoot: root, registry: customRegistry() })).resolves.toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1953,7 +1954,7 @@ it('does not execute artifact JavaScript while validating deferred imports', asy expect(requests).toBe(0); } finally { await new Promise((resolvePromise, reject) => server.close((error) => error === undefined ? resolvePromise() : reject(error))); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1977,7 +1978,7 @@ it('reports one structural change for a file mutation during validation', async expect(diagnostics.filter((entry) => entry.code === 'AB6004' && entry.generatedPath === 'scripts/mutable.mjs')).toHaveLength(1); expect(mutated).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1997,7 +1998,7 @@ it('rejects a special entry added during validation without returning a snapshot expect(result.diagnostics.filter((entry) => entry.code === 'AB6013' && entry.generatedPath === 'late-link.json')).toHaveLength(1); expect(result.snapshot).toBeUndefined(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2017,7 +2018,7 @@ it('rejects an empty directory added during validation without returning a snaps expect(result.diagnostics.filter((entry) => entry.code === 'AB6014' && entry.generatedPath === 'late-empty')).toHaveLength(1); expect(result.snapshot).toBeUndefined(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2044,7 +2045,7 @@ it('does not re-enter artifact validation after taking final evidence snapshots' expect(result.diagnostics).toEqual([]); expect(result.snapshot).toBeDefined(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2070,7 +2071,7 @@ it('does not allow a late registry re-entry to create an unvalidated empty direc expect(result.diagnostics).toEqual([]); expect(result.snapshot).toBeDefined(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2096,7 +2097,7 @@ it.each([ const diagnostics = await validateArtifact({ artifactRoot: root, registry }); expect(diagnostics.filter((entry) => entry.code === 'AB6001' && entry.generatedPath === 'agent-bundle.manifest.json')).toHaveLength(1); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2117,7 +2118,7 @@ it('does not repeat JavaScript diagnostics after a validation-side mutation', as expect(diagnostics.filter((entry) => entry.code === 'AB6005' && entry.generatedPath === 'scripts/mutable.mjs')).toHaveLength(1); expect(diagnostics.filter((entry) => entry.code === 'AB6004' && entry.generatedPath === 'scripts/mutable.mjs')).toHaveLength(1); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2183,7 +2184,7 @@ it('lexes compiled modules the evidence record proves and walks every other modu .filter((entry) => entry.code === 'AB6005' || entry.code === 'AB6039') .map((entry) => [entry.code, entry.generatedPath, entry.message]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; const walkedInFull = [ @@ -2238,7 +2239,7 @@ it('does not import copied non-JavaScript resources', async () => { try { await expect(validateArtifact({ artifactRoot: root, registry: customRegistry() })).resolves.toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2262,7 +2263,7 @@ it('fails closed when Agent Skills provenance does not equal the pinned contract ])); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2294,7 +2295,7 @@ it('requires manifest target metadata to match the supplied registry exactly', a ])); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2312,7 +2313,7 @@ it('reports AB6010 when a projection records another built-in adapter identity', }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2334,7 +2335,7 @@ it('requires registered target-native documents and validates their pinned schem expect.objectContaining({ code: 'AB6012', generatedPath: 'document.json', target: customTarget }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2379,7 +2380,7 @@ it.each([ expect.objectContaining({ code: 'AB6012', generatedPath: mcpPath, target }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2452,7 +2453,7 @@ it('validates Claude plugin artifacts carrying the pinned userConfig contract', expect.objectContaining({ code: 'AB6012', generatedPath: pluginPath, target: 'claude' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2521,7 +2522,7 @@ it('validates an enriched Claude marketplace against the full closed pinned cont expect.objectContaining({ code: 'AB6012', generatedPath: marketplacePath, target: 'claude' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2587,7 +2588,7 @@ it('validates a canonically rehashed Codex marketplace at its emitted path', asy }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2653,7 +2654,7 @@ it.each(malformedValidatorCases)('reports $0 through the stable schema diagnosti }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2679,7 +2680,7 @@ it('documents recovery for every stable artifact diagnostic code', async () => { diagnostic.recovery !== undefined && diagnostic.recovery.trim().length > 0, )).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2731,7 +2732,7 @@ it.each(['amp', 'claude', 'codex', 'cursor', 'portable'] as const)( }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, ); @@ -2748,7 +2749,7 @@ it.each(['cursor', 'portable'] as const)( }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, ); @@ -2760,7 +2761,7 @@ it.each(['claude', 'codex'] as const)( try { await expect(validateArtifact({ artifactRoot: root })).resolves.toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, ); @@ -2795,7 +2796,7 @@ it('accepts an emitted Claude settings document against its pinned schema', asyn try { await expect(validateArtifact({ artifactRoot: root })).resolves.toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2809,7 +2810,7 @@ it('rejects a rehashed Claude settings document that carries an unsupported key' target: 'claude', })]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2844,7 +2845,7 @@ it('accepts a Claude plugin manifest carrying valid dependencies', async () => { try { await expect(validateArtifact({ artifactRoot: root })).resolves.toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2858,7 +2859,7 @@ it('rejects a rehashed Claude plugin manifest carrying invalid dependencies', as target: 'claude', })]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2893,7 +2894,7 @@ it('fails artifact validation when a Cursor manifest logo is missing from the de }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2918,6 +2919,6 @@ it('accepts a Cursor manifest logo that resolves inside the artifact', async () const diagnostics = await validateArtifact({ artifactRoot: root }); expect(diagnostics.filter((diagnostic) => diagnostic.code === 'AB6025')).toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/browser-stdio-bridge-spike.test.ts b/packages/agent-bundle/tests/browser-stdio-bridge-spike.test.ts index 04b0e4be5..2f40fed8c 100644 --- a/packages/agent-bundle/tests/browser-stdio-bridge-spike.test.ts +++ b/packages/agent-bundle/tests/browser-stdio-bridge-spike.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; @@ -15,6 +15,7 @@ import { normalizeProject } from '../src/config/normalize.ts'; import { resolveMcpPathTokens } from '../src/services/mcp-path-tokens.ts'; import { readTargetMcpServer } from '../src/services/mcp-runtime.ts'; +import { removeTree } from './support/remove-tree.ts'; interface BridgeFixture { readonly binding: { readonly serverName: string; readonly sessionId: string; readonly target: string }; @@ -281,6 +282,6 @@ it('bridges a browser-bound session to a generated stdio artifact without exposi expect(stderr.join('')).toContain(fixture.stderr); expect(frames).toEqual(fixture.frames); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); diff --git a/packages/agent-bundle/tests/build-compose.test.ts b/packages/agent-bundle/tests/build-compose.test.ts index c634a1908..1f7c6880d 100644 --- a/packages/agent-bundle/tests/build-compose.test.ts +++ b/packages/agent-bundle/tests/build-compose.test.ts @@ -1,6 +1,6 @@ import type { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; -import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readdir, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, relative } from 'node:path'; @@ -23,6 +23,7 @@ import { type TargetMcpRuntimeContract, } from '../src/services/mcp-runtime.ts'; import { supportedCapabilities } from './support/adapter-capabilities.ts'; +import { removeTree } from './support/remove-tree.ts'; /** * Acceptance tests for the composite plugin root (#555, Wave 1): every @@ -32,7 +33,7 @@ import { supportedCapabilities } from './support/adapter-capabilities.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const writeProjectFile = async (root: string, path: string, content: string): Promise => { diff --git a/packages/agent-bundle/tests/build-reproducibility.test.ts b/packages/agent-bundle/tests/build-reproducibility.test.ts index c3ed6529a..905036851 100644 --- a/packages/agent-bundle/tests/build-reproducibility.test.ts +++ b/packages/agent-bundle/tests/build-reproducibility.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readdir, readFile, rename, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readdir, readFile, rename, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, relative } from 'node:path'; @@ -7,11 +7,12 @@ import { afterEach, expect, it } from '@rstest/core'; import { build } from '../src/api.ts'; import { parseArtifactManifest } from '../src/build/manifest.ts'; import { sha256Hex } from '../src/core/digest.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const writeProjectFile = async (root: string, path: string, contents: string): Promise => { diff --git a/packages/agent-bundle/tests/build-source-publication.test.ts b/packages/agent-bundle/tests/build-source-publication.test.ts index c242887b9..78331811b 100644 --- a/packages/agent-bundle/tests/build-source-publication.test.ts +++ b/packages/agent-bundle/tests/build-source-publication.test.ts @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto'; -import { access, mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { access, mkdir, mkdtemp, readdir, readFile, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -7,6 +7,7 @@ import { expect, it } from '@rstest/core'; import { build } from '../src/api.ts'; import { DiagnosticError } from '../src/core/diagnostics.ts'; +import { removeTree } from './support/remove-tree.ts'; const mutateStageEnv = 'AB7101_MUTATE_STAGE'; @@ -138,7 +139,7 @@ it('rejects a first artifact build with AB7101 before publishing any output', as expect(await readFile(join(root, 'src', 'mcp', 'echoer.ts'), 'utf8')).toBe(changedEchoer); } finally { delete process.env[mutateStageEnv]; - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 60_000); @@ -156,7 +157,7 @@ it('rejects a later artifact build with AB7101 and leaves the previous artifact expect(await compilerOwnedTempEntries(root)).toEqual([]); } finally { delete process.env[mutateStageEnv]; - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 60_000); @@ -177,6 +178,6 @@ it('rejects a package-stage race with AB7101 before replacing the previous packa expect(await readFile(join(root, 'src', 'library.ts'), 'utf8')).toBe('export const value = 2;\n'); } finally { delete process.env[mutateStageEnv]; - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 60_000); diff --git a/packages/agent-bundle/tests/build.test.ts b/packages/agent-bundle/tests/build.test.ts index 5b04fe8f4..84dff8faa 100644 --- a/packages/agent-bundle/tests/build.test.ts +++ b/packages/agent-bundle/tests/build.test.ts @@ -27,6 +27,7 @@ import { createProjectContext } from '../src/core/project-context.ts'; import type { NormalizedPlugin } from '../src/core/types.ts'; import { sha256Hex } from '../src/core/digest.ts'; import { emptyCompiledRouteGraph } from '../src/routes/graph.ts'; +import { removeTree } from './support/remove-tree.ts'; const testMeta: AgentBundleMeta = Object.freeze({ name: 'reserved-probe-plugin', @@ -319,7 +320,7 @@ const runModule = async (modulePath: string, cwd: string): Promise<{ readonly co }); const cleanupProject = async (project: TestProject): Promise => { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); }; it('low-level build writes and returns the exact canonical manifest for a configured Skill script', async () => { @@ -1131,7 +1132,7 @@ it('restores the existing artifact when publication fails after backup', async ( await expect(readFile(join(outputRoot, 'artifact.txt'), 'utf8')).resolves.toBe('previous\n'); expect((await readdir(root)).sort()).toEqual(['dist']); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1205,7 +1206,7 @@ it('inlines reserved specifiers through exact-match aliases and virtual generate sourceInputs: [join(root, 'src', 'entry.ts'), join(root, 'src', 'shell.ts')], }]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -1240,7 +1241,7 @@ it('lowers every bundler config and builds under NODE_ENV=development, leaving N } finally { if (previousNodeEnv === undefined) delete process.env.NODE_ENV; else process.env.NODE_ENV = previousNodeEnv; - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -1263,7 +1264,7 @@ it('leaves NODE_ENV unset when a failing inspection had set it', async () => { } finally { if (previousNodeEnv === undefined) delete process.env.NODE_ENV; else process.env.NODE_ENV = previousNodeEnv; - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1314,7 +1315,7 @@ it('leaves filesystem URL and worker expressions in the emitted bundle untouched await expect(readdir(join(root, 'dist'))).resolves.toEqual(['scripts']); await expect(readdir(join(root, 'dist', 'scripts'))).resolves.toEqual(['references.mjs']); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -1405,7 +1406,7 @@ const buildLinkedWorkspaceProject = async ( const bundle = await readFile(join(root, 'dist', 'scripts', 'linked.mjs'), 'utf8'); return { bundle, evidence, root }; } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }; @@ -1555,7 +1556,7 @@ it('validates a relocated artifact from its record alone and still reports an ig }); const artifactRoot = join(relocated, 'artifact'); await rename(project.outputRoot, artifactRoot); - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); expect(await validateArtifact({ artifactRoot })).toEqual([]); const manifestPath = join(artifactRoot, 'agent-bundle.manifest.json'); @@ -1583,7 +1584,7 @@ it('validates a relocated artifact from its record alone and still reports an ig expect(diagnostics.filter((diagnostic) => diagnostic.code === 'AB6039')).toEqual([]); expect(diagnostics.filter((diagnostic) => diagnostic.code === 'AB6005')).toEqual(ignoredImportDiagnostics('scripts/greeting.mjs')); } finally { - await rm(relocated, { force: true, recursive: true }); + await removeTree(relocated); await cleanupProject(project); } }, 20_000); @@ -1689,7 +1690,7 @@ it('keeps sibling staged outputs alive under a tools hatch that asks to clean th await expect(readFile(join(root, 'dist', 'sibling.mjs'), 'utf8')) .resolves.toContain('already-emitted-sibling'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -1715,7 +1716,7 @@ it('overrides a tools hatch that strips plugins and repoints the entry away from expect(bundle).toContain('generated-wrapper-marker'); expect(bundle).toContain('generated-registry'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -1730,7 +1731,7 @@ it('rejects a tools hatch that externalizes a reserved specifier statically', as tools: { rspack: { externals: { 'agent-bundle/mcp-entry': 'module agent-bundle/mcp-entry' } } }, })).rejects.toThrow(/must not externalize the reserved specifier "agent-bundle\/mcp-entry"/u); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -1761,7 +1762,7 @@ it('rejects a tools hatch that externalizes a reserved specifier through functio }, })).rejects.toThrow(/must not externalize the reserved specifier "agent-bundle\/mcp-apps"/u); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -1779,6 +1780,6 @@ it('rejects a tools hatch alias that shadows a reserved specifier', async () => tools: { rspack: { resolve: { alias: { 'agent-bundle/mcp-apps': join(root, 'src', 'evil.ts') } } } }, })).rejects.toThrow(/must not alias the reserved specifier/u); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); diff --git a/packages/agent-bundle/tests/check-declaration-imports.test.ts b/packages/agent-bundle/tests/check-declaration-imports.test.ts index 014d35f39..108713125 100644 --- a/packages/agent-bundle/tests/check-declaration-imports.test.ts +++ b/packages/agent-bundle/tests/check-declaration-imports.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -13,6 +13,7 @@ import { runCheckDeclarationImports, type DeclarationManifest, } from '../../../scripts/check-declaration-imports.mjs'; +import { removeTree } from './support/remove-tree.ts'; const manifest: DeclarationManifest = { name: 'fixture-package', @@ -439,7 +440,7 @@ describe('the packed-declaration gate', () => { + '(reachable from exports["."])', ]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -468,7 +469,7 @@ describe('the packed-declaration gate', () => { expect(lines[0]).toBe('good-fixture: 2 packed declarations, 1 reachable from 1 export entries; 1 errors, 0 warnings'); expect(lines[1]).toContain(' error dist/internal.d.ts:1 imports "typescript-5"'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/check-test-remove-tree.test.ts b/packages/agent-bundle/tests/check-test-remove-tree.test.ts new file mode 100644 index 000000000..2b6458b8e --- /dev/null +++ b/packages/agent-bundle/tests/check-test-remove-tree.test.ts @@ -0,0 +1,242 @@ +import { expect, it } from '@rstest/core'; + +import { + bareRecursiveRmFailures, + recursiveRmCalls, + removalBindings, +} from '../../../scripts/check-test-remove-tree.mjs'; + +/** Assemble sample source at runtime so the lint gate does not scan the examples. */ +const sample = (lines: readonly string[]): string => lines.join('\n'); +const recursiveTrue = ['recurs', 'ive: true'].join(''); + +it('tracks named, aliased, and namespace Node fs removal bindings', () => { + expect(removalBindings(`import { rm } from 'node:fs/promises';`)).toEqual({ + bareNames: new Set(['rm']), + namespaceNames: new Set(), + }); + expect(removalBindings(`import { rm as remove } from 'node:fs/promises';`)).toEqual({ + bareNames: new Set(['remove']), + namespaceNames: new Set(), + }); + expect(removalBindings(`import * as fs from 'node:fs/promises';`)).toEqual({ + bareNames: new Set(), + namespaceNames: new Set(['fs']), + }); + expect(removalBindings(`import fs from 'node:fs';`)).toEqual({ + bareNames: new Set(), + namespaceNames: new Set(['fs']), + }); +}); + +it('flags bare, aliased, and namespace recursive removals without maxRetries', () => { + const bare = recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + `await rm(root, { force: true, ${recursiveTrue} });`, + ])); + expect(bare).toEqual([expect.objectContaining({ hasRetries: false, line: 2 })]); + + const aliased = recursiveRmCalls(sample([ + "import { rm as remove } from 'node:fs/promises';", + `await remove(root, { ${recursiveTrue} });`, + ])); + expect(aliased).toEqual([expect.objectContaining({ hasRetries: false, line: 2 })]); + + const namespaced = recursiveRmCalls(sample([ + "import * as fs from 'node:fs/promises';", + `await fs.rm(root, { ${recursiveTrue} });`, + ])); + expect(namespaced).toEqual([expect.objectContaining({ hasRetries: false, line: 2 })]); +}); + +it('allows nonrecursive removals, explicit maxRetries, and unrelated .rm calls', () => { + expect(recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + 'await rm(file);', + `await rm(root, { force: true, ${recursiveTrue}, maxRetries: 5 });`, + `await other.rm(root, { ${recursiveTrue} });`, + ]))).toEqual([expect.objectContaining({ hasRetries: true, line: 3 })]); +}); + +it('exempts the canonical removeTree helper and formats lint failures', () => { + const helper = sample([ + "import { rm as removeDirectory } from 'node:fs/promises';", + `await fs.rm(path, { force: true, ${recursiveTrue} });`, + ]); + expect(bareRecursiveRmFailures('packages/agent-bundle/tests/support/remove-tree.ts', helper)).toEqual([]); + + const escaped = sample([ + "import * as fs from 'node:fs/promises';", + `await fs.rm(root, { ${recursiveTrue} });`, + ]); + expect(bareRecursiveRmFailures('packages/agent-bundle/tests/example.test.ts', escaped)).toEqual([ + 'packages/agent-bundle/tests/example.test.ts:2 bare recursive rm. Use removeTree.', + ]); +}); + +it('ignores recursive rm text inside comments and string literals', () => { + expect(recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + `const sample = 'rm(root, { ${recursiveTrue} })'`, + ]))).toEqual([]); + + expect(recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + `/* rm(root, { ${recursiveTrue} }); */`, + ]))).toEqual([]); +}); + +it('still flags calls after string urls and rejects commented-out maxRetries', () => { + const afterUrl = recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + `const url = "https://example.test"; rm(root, { ${recursiveTrue} });`, + ])); + expect(afterUrl).toEqual([expect.objectContaining({ hasRetries: false, line: 2 })]); + + const commentedRetries = recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + `rm(root, { ${recursiveTrue} /* maxRetries: 5 */ });`, + ])); + expect(commentedRetries).toEqual([expect.objectContaining({ hasRetries: false, line: 2 })]); +}); + +it('matches $-suffixed removal aliases literally', () => { + const dollars = recursiveRmCalls(sample([ + "import { rm as remove$ } from 'node:fs/promises';", + `await remove$(root, { ${recursiveTrue} })`, + ])); + expect(dollars).toEqual([expect.objectContaining({ hasRetries: false, line: 2 })]); + expect(bareRecursiveRmFailures('packages/agent-bundle/tests/example.test.ts', sample([ + "import { rm as remove$ } from 'node:fs/promises';", + `await remove$(root, { ${recursiveTrue} })`, + ]))).toEqual([ + 'packages/agent-bundle/tests/example.test.ts:2 bare recursive rm. Use removeTree.', + ]); +}); + +it('reads quoted options keys and only the second call argument', () => { + expect(recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + 'rm(root, { "recursive": true });', + ]))).toEqual([expect.objectContaining({ hasRetries: false, line: 2 })]); + + expect(recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + `rm(root, { ${recursiveTrue}, "maxRetries": 5 });`, + ]))).toEqual([expect.objectContaining({ hasRetries: true, line: 2 })]); + + expect(recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + `rm(makeRoot({ maxRetries: 5 }), { ${recursiveTrue} });`, + ]))).toEqual([expect.objectContaining({ hasRetries: false, line: 2 })]); + + expect(recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + `rm(makeRoot({ ${recursiveTrue} }));`, + ]))).toEqual([]); +}); + +it('keeps regex literals, template substitutions, and spaced member calls correct', () => { + expect(recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + String.raw`const re = /['"]/; rm(root, { ${recursiveTrue} });`, + ]))).toEqual([expect.objectContaining({ hasRetries: false, line: 2 })]); + + expect(recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + 'const result = `${await rm(root, { ' + recursiveTrue + ' })}`;', + ]))).toEqual([expect.objectContaining({ hasRetries: false, line: 2 })]); + + expect(recursiveRmCalls(sample([ + "import { rm } from 'node:fs/promises';", + `other. rm(root, { ${recursiveTrue} });`, + ]))).toEqual([]); +}); + +it('only counts real AST Node fs import bindings', () => { + // Local identifier named rm is not a Node binding. + expect(recursiveRmCalls(sample([ + 'const rm = async () => undefined;', + `await rm(root, { ${recursiveTrue} });`, + ]))).toEqual([]); + + // Non-fs module named rm must not count as Node removal binding. + expect(removalBindings("import { rm } from 'some-rm-lib';")).toEqual({ + bareNames: new Set(), + namespaceNames: new Set(), + }); + expect(recursiveRmCalls(sample([ + "import { rm } from 'some-rm-lib';", + `await rm(root, { ${recursiveTrue} });`, + ]))).toEqual([]); + + // Commented-out import must not invent a bare Node binding (false-positive). + expect(recursiveRmCalls(sample([ + "// import { rm } from 'node:fs/promises';", + `await rm(root, { ${recursiveTrue} });`, + ]))).toEqual([]); + expect(recursiveRmCalls(sample([ + "// import { rm as remove } from 'node:fs/promises';", + `await remove(root, { ${recursiveTrue} });`, + ]))).toEqual([]); + + // Comments inside the named import still bind. + expect(recursiveRmCalls(sample([ + "import { rm /* teardown */ as remove } from 'node:fs/promises';", + `await remove(root, { ${recursiveTrue} });`, + ]))).toEqual([expect.objectContaining({ hasRetries: false, line: 2 })]); + + // Default + named form registers both namespace and bare alias. + expect(removalBindings(`import fs, { rm as remove } from 'node:fs/promises';`)).toEqual({ + bareNames: new Set(['remove']), + namespaceNames: new Set(['fs']), + }); + expect(recursiveRmCalls(sample([ + "import fs, { rm as remove } from 'node:fs/promises';", + `await remove(root, { ${recursiveTrue} });`, + `await fs.rm(root, { ${recursiveTrue} });`, + ]))).toEqual([ + expect.objectContaining({ hasRetries: false, line: 2 }), + expect.objectContaining({ hasRetries: false, line: 3 }), + ]); +}); + +it('still gates aliased and namespace Node fs.rm without maxRetries', () => { + const aliasedBare = recursiveRmCalls(sample([ + "import { rm as remove } from 'node:fs/promises';", + `await remove(path, { ${recursiveTrue} });`, + ])); + expect(aliasedBare).toEqual([expect.objectContaining({ hasRetries: false, line: 2 })]); + expect(bareRecursiveRmFailures('packages/agent-bundle/tests/example.test.ts', sample([ + "import { rm as remove } from 'node:fs/promises';", + `await remove(path, { ${recursiveTrue} });`, + ]))).toEqual([ + 'packages/agent-bundle/tests/example.test.ts:2 bare recursive rm. Use removeTree.', + ]); + + const aliasedRetried = recursiveRmCalls(sample([ + "import { rm as remove } from 'node:fs/promises';", + `await remove(path, { ${recursiveTrue}, maxRetries: 5 });`, + ])); + expect(aliasedRetried).toEqual([expect.objectContaining({ hasRetries: true, line: 2 })]); + expect(bareRecursiveRmFailures('packages/agent-bundle/tests/example.test.ts', sample([ + "import { rm as remove } from 'node:fs/promises';", + `await remove(path, { ${recursiveTrue}, maxRetries: 5 });`, + ]))).toEqual([]); + + const namespacedBare = recursiveRmCalls(sample([ + "import * as fs from 'node:fs/promises';", + `await fs.rm(path, { ${recursiveTrue} });`, + ])); + expect(namespacedBare).toEqual([expect.objectContaining({ hasRetries: false, line: 2 })]); + + const namespacedRetried = recursiveRmCalls(sample([ + "import * as fs from 'node:fs/promises';", + `await fs.rm(path, { ${recursiveTrue}, maxRetries: 5 });`, + ])); + expect(namespacedRetried).toEqual([expect.objectContaining({ hasRetries: true, line: 2 })]); + expect(bareRecursiveRmFailures('packages/agent-bundle/tests/example.test.ts', sample([ + "import * as fs from 'node:fs/promises';", + `await fs.rm(path, { ${recursiveTrue}, maxRetries: 5 });`, + ]))).toEqual([]); +}); diff --git a/packages/agent-bundle/tests/classify-docs-only.test.ts b/packages/agent-bundle/tests/classify-docs-only.test.ts index 08ff35947..8b34f6e02 100644 --- a/packages/agent-bundle/tests/classify-docs-only.test.ts +++ b/packages/agent-bundle/tests/classify-docs-only.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -12,6 +12,7 @@ import { isDocsOnlyPath, parseGhFilesListing, } from '../../../scripts/classify-docs-only.mjs'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const scriptPath = join(dirname(fileURLToPath(import.meta.url)), '../../../scripts/classify-docs-only.mjs'); @@ -182,6 +183,6 @@ it('writes docs_only to GITHUB_OUTPUT and always exits 0', async () => { expect(failedListing.stdout).toContain('listing-error'); expect(await readFile(outputPath, 'utf8')).toBe('docs_only=false\n'); } finally { - await rm(fixtureRoot, { force: true, recursive: true }); + await removeTree(fixtureRoot); } }); diff --git a/packages/agent-bundle/tests/claude-hooks-schema.test.ts b/packages/agent-bundle/tests/claude-hooks-schema.test.ts index 2384a0ed1..ad0b5828d 100644 --- a/packages/agent-bundle/tests/claude-hooks-schema.test.ts +++ b/packages/agent-bundle/tests/claude-hooks-schema.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, readdir, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -12,6 +12,7 @@ import schemaProvenance from '../src/adapters/schemas/claude/PROVENANCE.json' wi import { createAdapterValidator } from '../src/adapters/types.ts'; import { normalizeProject } from '../src/config/normalize.ts'; import type { LoadedConfig } from '../src/config/load.ts'; +import { removeTree } from './support/remove-tree.ts'; const fixtureRoot = new URL('./fixtures/claude-hooks-schema/', import.meta.url); /** @@ -257,6 +258,6 @@ it('plans a Claude native hooks document that uses every documented handler type const rejected = registry.get('claude').plan(await normalizeProject(loaded, { skills: [] }, registry)); expect(rejected.diagnostics.map((diagnostic) => diagnostic.code)).toEqual(['claude.native-hooks.schema']); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/claude-plugin-validate-acceptance.test.ts b/packages/agent-bundle/tests/claude-plugin-validate-acceptance.test.ts index 12f087005..28c11502c 100644 --- a/packages/agent-bundle/tests/claude-plugin-validate-acceptance.test.ts +++ b/packages/agent-bundle/tests/claude-plugin-validate-acceptance.test.ts @@ -1,5 +1,5 @@ import { spawnSync } from 'node:child_process'; -import { mkdtemp, rm } from 'node:fs/promises'; +import { mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -10,6 +10,7 @@ import { emitPlanEntries } from '../src/build/emit.ts'; import { pathTokens, type NormalizedHook, type NormalizedPlugin } from '../src/core/types.ts'; import { validateClaudePluginFiles } from '../src/host-contracts/claude-plugin-validation.ts'; import { claudePluginRowErrors } from '../src/install/install.ts'; +import { removeTree } from './support/remove-tree.ts'; const configPath = '/workspace/agent-bundle.config.ts'; @@ -90,7 +91,7 @@ beforeAll(async () => { }); afterAll(async () => { - await rm(root, { force: true, recursive: true }); + await removeTree(root); }); /** Runs the real Claude Code CLI against an isolated config dir so the user's plugin state is never read or written. */ diff --git a/packages/agent-bundle/tests/claude-plugin-validation.test.ts b/packages/agent-bundle/tests/claude-plugin-validation.test.ts index d158b0950..e7aa8b953 100644 --- a/packages/agent-bundle/tests/claude-plugin-validation.test.ts +++ b/packages/agent-bundle/tests/claude-plugin-validation.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; @@ -10,6 +10,7 @@ import { validateClaudePluginFiles, type ClaudePluginCommandRunner, } from '../src/host-contracts/claude-plugin-validation.ts'; +import { removeTree } from './support/remove-tree.ts'; const fixtureRoots: string[] = []; @@ -68,7 +69,7 @@ const runByTarget = ( }; afterEach(async () => { - await Promise.all(fixtureRoots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(fixtureRoots.splice(0).map((root) => removeTree(root))); }); const pluginWithNumberOption = async ( diff --git a/packages/agent-bundle/tests/cli-projection.test.ts b/packages/agent-bundle/tests/cli-projection.test.ts index a40516ca8..a9ab89f33 100644 --- a/packages/agent-bundle/tests/cli-projection.test.ts +++ b/packages/agent-bundle/tests/cli-projection.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, realpath, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -12,11 +12,12 @@ import { } from '../src/routes/cli-projection.ts'; import { compileRouteGraph } from '../src/routes/graph.ts'; import type { CompiledAgentRoute, RouteInputSchema } from '../src/routes/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const createRoot = async (): Promise => { diff --git a/packages/agent-bundle/tests/cli-routes-build.test.ts b/packages/agent-bundle/tests/cli-routes-build.test.ts index 76b914be5..721ad244a 100644 --- a/packages/agent-bundle/tests/cli-routes-build.test.ts +++ b/packages/agent-bundle/tests/cli-routes-build.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { mkdir, mkdtemp, readFile, realpath, rm, stat, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, realpath, stat, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { promisify } from 'node:util'; @@ -10,12 +10,13 @@ import { build, parseArtifactManifest, type ReadyInspectResult, validate } from import { runCli } from '../src/cli.ts'; import { DiagnosticError } from '../src/core/diagnostics.ts'; import { captureCliTerminal } from './support/cli-terminal.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const writeProjectFile = async (root: string, path: string, contents: string): Promise => { @@ -636,7 +637,7 @@ describe('the CLI surface projection in the generated routed-CLI executable', () }, 120_000); afterAll(async () => { - await rm(root, { force: true, recursive: true }); + await removeTree(root); }); it('bundles the projection module into the executable and keeps the tool as the only route behind it', async () => { diff --git a/packages/agent-bundle/tests/cli-routes.test.ts b/packages/agent-bundle/tests/cli-routes.test.ts index f27011c0a..e91806a6c 100644 --- a/packages/agent-bundle/tests/cli-routes.test.ts +++ b/packages/agent-bundle/tests/cli-routes.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, realpath, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -24,11 +24,12 @@ import type { AgentBundleConfig } from '../src/core/types.ts'; import { projectInputSchemaOptions } from '../src/routes/cli-argv.ts'; import { compileRouteGraph } from '../src/routes/graph.ts'; import type { CompiledCliCommand, CompiledCliSurface, RouteInputSchema } from '../src/routes/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const createRoot = async (): Promise => { diff --git a/packages/agent-bundle/tests/cli.test.ts b/packages/agent-bundle/tests/cli.test.ts index 3f9e4694f..b7d6bad0f 100644 --- a/packages/agent-bundle/tests/cli.test.ts +++ b/packages/agent-bundle/tests/cli.test.ts @@ -1,6 +1,6 @@ import { execFile as executeFile, spawn } from 'node:child_process'; import { createHash } from 'node:crypto'; -import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -13,6 +13,7 @@ import { runCli as runSourceCli, type CliDependencies } from '../src/cli.ts'; import { captureCliTerminal } from './support/cli-terminal.ts'; import { cachedNpmInstallArguments, packOutputFromJson } from './support/shared-pack.ts'; import { timeScale } from './support/time-scale.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); @@ -301,7 +302,7 @@ it('builds a selected target through the built executable from a path containing projections: [{ host: 'codex' }, { host: 'portable' }], }); } finally { - await rm(resolve(project.root, '..'), { force: true, recursive: true }); + await removeTree(resolve(project.root, '..')); } }, 30_000 * timeScale); @@ -322,7 +323,7 @@ it('rejects --target plugin as an unknown target (#555 acceptance 3)', async () target: 'plugin', })]); } finally { - await rm(resolve(project.root, '..'), { force: true, recursive: true }); + await removeTree(resolve(project.root, '..')); } }); @@ -352,7 +353,7 @@ const runCliRecordingModuleLoads = async ( const modules = (await readFile(recordPath, 'utf8')).split('\n').filter((line) => line.length > 0); return { code, modules, stderr, stdout }; } finally { - await rm(recordRoot, { force: true, recursive: true }); + await removeTree(recordRoot); } }; @@ -538,8 +539,8 @@ it('runs MCP and hook operations from a packed consumer with explicit and tempor expect(missingHook).toMatchObject({ code: 2, stdout: '' }); } finally { await Promise.all([ - rm(join(source, '..'), { force: true, recursive: true }), - rm(consumer.root, { force: true, recursive: true }), + removeTree(join(source, '..')), + removeTree(consumer.root), ]); } }, 60_000 * timeScale); @@ -600,7 +601,7 @@ it('keeps inspect JSON stable and validates only the supplied artifact', async ( ]); expect(humanValidation).toEqual({ code: 0, stderr: '', stdout: 'Validation succeeded\n' }); } finally { - await rm(resolve(project.root, '..'), { force: true, recursive: true }); + await removeTree(resolve(project.root, '..')); } }, 30_000 * timeScale); @@ -640,7 +641,7 @@ it('includes a built-manifest summary on inspect --json after a build, and omits expect(JSON.parse(artifact.stdout).manifest.application.id).toBe('plugin:cli-fixture'); expect(JSON.parse(artifact.stdout).application.identity.id).toBe('plugin:cli-fixture'); } finally { - await rm(resolve(project.root, '..'), { force: true, recursive: true }); + await removeTree(resolve(project.root, '..')); } }, 30_000 * timeScale); @@ -756,7 +757,7 @@ it('build compiles a declared MCP App view and reports its document and measured /^MCP App dashboard \(codex\+portable\): mcp-apps\/dashboard\.html \d+(?:\.\d)? [KM]iB \(\d+(?:\.\d)? [KM]iB gzip\)$/mu, ); } finally { - await rm(resolve(project.root, '..'), { force: true, recursive: true }); + await removeTree(resolve(project.root, '..')); } }, 60_000 * timeScale); @@ -816,7 +817,7 @@ it('prints a complete invalid inspection on JSON and human output', async () => expect(human.stdout).toContain('Recovery:'); expect(human.stdout).not.toContain('opaque cli inspect sentinel'); } finally { - await rm(resolve(project.root, '..'), { force: true, recursive: true }); + await removeTree(resolve(project.root, '..')); } }, 30_000 * timeScale); @@ -851,7 +852,7 @@ it('explains selected and omitted components per target on human inspect output' expect(withCursor).toMatchObject({ code: 0, stderr: '' }); expect(withCursor.stdout).toMatch(/^ {2}command deploy omits argumentHint: commands\.argumentHint unavailable — .*frontmatter-free.*$/mu); await Promise.all([ - rm(join(project.root, 'src', 'commands'), { force: true, recursive: true }), + removeTree(join(project.root, 'src', 'commands')), writeFile(join(project.root, 'agent-bundle.config.ts'), originalConfig), ]); // The canonical kind matrix names every kind a host cannot emit, even @@ -887,7 +888,7 @@ it('explains selected and omitted components per target on human inspect output' state: 'ready', }); } finally { - await rm(resolve(project.root, '..'), { force: true, recursive: true }); + await removeTree(resolve(project.root, '..')); } }, 30_000 * timeScale); @@ -921,7 +922,7 @@ it('reports an unselected inspect target on JSON and human output', async () => expect(human.stdout).toContain('portabl'); expect(human.stdout).toContain('Recovery:'); } finally { - await rm(resolve(project.root, '..'), { force: true, recursive: true }); + await removeTree(resolve(project.root, '..')); } }, 30_000 * timeScale); @@ -994,7 +995,7 @@ it('dumps the lowered Rspack configuration of every output with inspect --bundle expect(ambiguous.code).toBe(1); expect(JSON.parse(ambiguous.stderr)).toMatchObject([{ code: 'AB5000', severity: 'error' }]); } finally { - await rm(resolve(project.root, '..'), { force: true, recursive: true }); + await removeTree(resolve(project.root, '..')); } }, 30_000 * timeScale); @@ -1026,7 +1027,7 @@ it('reports source validation diagnostics on stderr before staging an artifact', expect(validation.stdout).toBe(''); expect(JSON.parse(validation.stderr)).toMatchObject([{ code: 'AB4000', severity: 'error' }]); } finally { - await rm(resolve(project.root, '..'), { force: true, recursive: true }); + await removeTree(resolve(project.root, '..')); } }, 30_000 * timeScale); @@ -1061,7 +1062,7 @@ it('reports a generated Flight worker collision before compiling scripts', async severity: 'error', }]); } finally { - await rm(resolve(project.root, '..'), { force: true, recursive: true }); + await removeTree(resolve(project.root, '..')); } }, 30_000 * timeScale); diff --git a/packages/agent-bundle/tests/codex-plugin-validation.test.ts b/packages/agent-bundle/tests/codex-plugin-validation.test.ts index 60a089e4d..e5c84042b 100644 --- a/packages/agent-bundle/tests/codex-plugin-validation.test.ts +++ b/packages/agent-bundle/tests/codex-plugin-validation.test.ts @@ -1,4 +1,4 @@ -import { access, copyFile, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { access, copyFile, mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -9,6 +9,7 @@ import { validateCodexPlugin, type CodexPluginCommandRunner, } from '../src/host-contracts/codex-plugin-validation.ts'; +import { removeTree } from './support/remove-tree.ts'; const generatedSchemaNames = Object.freeze( Object.keys(codexCapabilityTable.validation.pinnedGeneratedComparison.pinnedRepositorySha256).sort(), @@ -164,7 +165,7 @@ it('validates Codex bundle documents and matching generated schemas without shel expect(Object.isFrozen(report)).toBe(true); expect(Object.isFrozen(report.diagnostics)).toBe(true); } finally { - await rm(pluginDirectory, { force: true, recursive: true }); + await removeTree(pluginDirectory); } }); @@ -233,7 +234,7 @@ it('reports the missing schema generator verb honestly and still checks pinned d version: '0.147.0', }); } finally { - await rm(pluginDirectory, { force: true, recursive: true }); + await removeTree(pluginDirectory); } }); @@ -259,7 +260,7 @@ it('warns when live generated schemas drift from the pinned revision', async () version: '0.147.0', }); } finally { - await rm(pluginDirectory, { force: true, recursive: true }); + await removeTree(pluginDirectory); } }); @@ -288,7 +289,7 @@ it('reports app-server-only schema output as unassessable information even in st version: '0.147.0', }); } finally { - await rm(pluginDirectory, { force: true, recursive: true }); + await removeTree(pluginDirectory); } }); @@ -306,7 +307,7 @@ it('judges only the Codex documents under .codex-plugin/ in a root shared with C expect(report.diagnostics.filter((entry) => entry.code === 'AB6032')).toEqual([]); expect(report.status).toBe('passed'); } finally { - await rm(pluginDirectory, { force: true, recursive: true }); + await removeTree(pluginDirectory); } }); @@ -353,7 +354,7 @@ it('rejects malformed fixtures for every locally validated Codex schema', async status: 'failed', }); } finally { - await rm(pluginDirectory, { force: true, recursive: true }); + await removeTree(pluginDirectory); } } }); @@ -380,7 +381,7 @@ it('maps schema-generation timeout and output-limit terminations to stable failu }); } } finally { - await rm(pluginDirectory, { force: true, recursive: true }); + await removeTree(pluginDirectory); } }); diff --git a/packages/agent-bundle/tests/command-config.test.ts b/packages/agent-bundle/tests/command-config.test.ts index 228af8edf..6b629252f 100644 --- a/packages/agent-bundle/tests/command-config.test.ts +++ b/packages/agent-bundle/tests/command-config.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, mkdir, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -15,6 +15,7 @@ import { type LoadedConfig, } from '../src/config/index.ts'; import type { AgentBundleConfig } from '../src/core/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const loadedProject = ( root: string, @@ -44,7 +45,7 @@ const withProject = async ( ); await run(root); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; @@ -172,7 +173,7 @@ it('discovers flat non-ignored commands deterministically and omits the collecti join(root, 'src', 'commands', 'zeta.md'), ]); - await rm(join(root, 'src', 'commands'), { recursive: true }); + await removeTree(join(root, 'src', 'commands')); expect(await discoverProject(root, config)).not.toHaveProperty('commands'); }); }); diff --git a/packages/agent-bundle/tests/compile-evidence.test.ts b/packages/agent-bundle/tests/compile-evidence.test.ts index 3c1f5376f..ca40c916c 100644 --- a/packages/agent-bundle/tests/compile-evidence.test.ts +++ b/packages/agent-bundle/tests/compile-evidence.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -14,11 +14,12 @@ import { } from '../src/build/compile-evidence.ts'; import type { CompileResult } from '../src/build/compile-result.ts'; import { sha256Hex } from '../src/core/digest.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const createFixture = async (): Promise<{ readonly record: CompileEvidenceRecord; readonly root: string }> => { diff --git a/packages/agent-bundle/tests/compile-stages.test.ts b/packages/agent-bundle/tests/compile-stages.test.ts index 85cd0cca5..b30492ecb 100644 --- a/packages/agent-bundle/tests/compile-stages.test.ts +++ b/packages/agent-bundle/tests/compile-stages.test.ts @@ -1,6 +1,6 @@ import { rspack } from '@rslib/core'; import { describe, expect, it } from '@rstest/core'; -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -10,6 +10,7 @@ import { generatedMetaModulePath, metaModuleSpecifier } from '../src/build/meta. import { buildRslibSurfaces, compileResultOf, entryLibId, type RslibEntry } from '../src/build/rslib.ts'; import { planCompileStages } from '../src/build/compile-stages.ts'; import type { AgentBundleMeta } from '../src/meta.ts'; +import { removeTree } from './support/remove-tree.ts'; const meta: AgentBundleMeta = Object.freeze({ name: 'stages-fixture', @@ -244,7 +245,7 @@ describe('buildRslibSurfaces', () => { }, ]); } finally { - await rm(outputRoot, { force: true, recursive: true }); + await removeTree(outputRoot); } }); diff --git a/packages/agent-bundle/tests/compiler-evidence.test.ts b/packages/agent-bundle/tests/compiler-evidence.test.ts index c4f74bfa7..728dab17e 100644 --- a/packages/agent-bundle/tests/compiler-evidence.test.ts +++ b/packages/agent-bundle/tests/compiler-evidence.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, realpath, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -13,13 +13,14 @@ import { DiagnosticError, type Diagnostic } from '../src/core/diagnostics.ts'; import type { NormalizedPlugin } from '../src/core/types.ts'; import { emptyCompiledRouteGraph } from '../src/routes/graph.ts'; import { build } from './support/build.ts'; +import { removeTree } from './support/remove-tree.ts'; type RspackMutator = (config: Rspack.Configuration) => void; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const fixtureRoot = async (files: Readonly>): Promise => { diff --git a/packages/agent-bundle/tests/composite-rules.test.ts b/packages/agent-bundle/tests/composite-rules.test.ts index 2a71c1258..c30f0f246 100644 --- a/packages/agent-bundle/tests/composite-rules.test.ts +++ b/packages/agent-bundle/tests/composite-rules.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readdir, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -9,6 +9,7 @@ import { cursorArtifactPaths } from '../src/adapters/cursor.ts'; import { build, type BuildProjectResult, inspect, validate } from '../src/api.ts'; import { type ArtifactManifest, parseArtifactManifest } from '../src/build/manifest.ts'; import { type Diagnostic, DiagnosticError } from '../src/core/diagnostics.ts'; +import { removeTree } from './support/remove-tree.ts'; /** * Composite-root rules ported from the superseded #569 (#555): the ones that @@ -22,7 +23,7 @@ import { type Diagnostic, DiagnosticError } from '../src/core/diagnostics.ts'; const roots: string[] = []; afterAll(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const writeProjectFile = async (root: string, path: string, content: string): Promise => { diff --git a/packages/agent-bundle/tests/config.test.ts b/packages/agent-bundle/tests/config.test.ts index 747f2871e..4f47a73a5 100644 --- a/packages/agent-bundle/tests/config.test.ts +++ b/packages/agent-bundle/tests/config.test.ts @@ -1,5 +1,5 @@ import { expect, it } from '@rstest/core'; -import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdtemp, mkdir, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -12,6 +12,7 @@ import { createProjectFixture, removeProjectFixture, } from './helpers/project-fixture.ts'; +import { removeTree } from './support/remove-tree.ts'; it('loads an async TypeScript config and discovers its conventional skill files', async () => { const fixture = await createProjectFixture(); @@ -215,7 +216,7 @@ it('rejects external config paths before evaluating their modules', async () => root, })).rejects.toThrow(/outside project root/i); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); @@ -236,7 +237,7 @@ it('rejects config symlinks whose resolved targets escape the real project root root, })).rejects.toThrow(/outside project root/i); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); diff --git a/packages/agent-bundle/tests/cursor-plugin-validation.test.ts b/packages/agent-bundle/tests/cursor-plugin-validation.test.ts index 034009169..96aa3608f 100644 --- a/packages/agent-bundle/tests/cursor-plugin-validation.test.ts +++ b/packages/agent-bundle/tests/cursor-plugin-validation.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdtemp, mkdir, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -8,11 +8,12 @@ import { validateCursorPlugin, type CursorPluginCommandRunner, } from '../src/host-contracts/cursor-plugin-validation.ts'; +import { removeTree } from './support/remove-tree.ts'; const fixtureRoots: string[] = []; afterEach(async () => { - await Promise.all(fixtureRoots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(fixtureRoots.splice(0).map((root) => removeTree(root))); }); const createFixtureRoot = async (): Promise => { diff --git a/packages/agent-bundle/tests/dependency-audit-plugin.test.ts b/packages/agent-bundle/tests/dependency-audit-plugin.test.ts index 433fd15e4..a788bcd9e 100644 --- a/packages/agent-bundle/tests/dependency-audit-plugin.test.ts +++ b/packages/agent-bundle/tests/dependency-audit-plugin.test.ts @@ -1,7 +1,7 @@ import type { Rspack } from '@rsbuild/core'; import { createRslib } from '@rslib/core'; import { describe, expect, it } from '@rstest/core'; -import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -10,6 +10,7 @@ import type { CompilationEvidence } from '../src/build/compile-result.ts'; import { ArtifactDependencyAuditPlugin } from '../src/build/dependency-audit-plugin.ts'; import { composeEntryLibConfig, entryLibId, type RslibEntry } from '../src/build/rslib.ts'; import type { AgentBundleMeta } from '../src/meta.ts'; +import { removeTree } from './support/remove-tree.ts'; const testMeta: AgentBundleMeta = Object.freeze({ name: 'dependency-audit-probe-plugin', @@ -114,7 +115,7 @@ describe('ArtifactDependencyAuditPlugin', () => { expect(Object.isFrozen(record?.externals)).toBe(true); expect(Object.isFrozen(record?.modules)).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -124,7 +125,7 @@ describe('ArtifactDependencyAuditPlugin', () => { const [record] = await buildRecording(root, [entry], withExternals('left-pad')); expectLeftPadExternal(record, source, 'module'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -137,7 +138,7 @@ describe('ArtifactDependencyAuditPlugin', () => { }); expectLeftPadExternal(record, source, 'node-commonjs'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -150,7 +151,7 @@ describe('ArtifactDependencyAuditPlugin', () => { })); expectLeftPadExternal(record, source, 'module'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -166,7 +167,7 @@ describe('ArtifactDependencyAuditPlugin', () => { { externalType: 'module', issuers: [source], request: 'lp', userRequest: 'left-pad' }, ]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -178,7 +179,7 @@ describe('ArtifactDependencyAuditPlugin', () => { { externalType: 'module', issuers: [source], request: 'lp|"x', userRequest: 'left-pad' }, ]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -202,7 +203,7 @@ describe('ArtifactDependencyAuditPlugin', () => { { externalType: 'module', issuers: [source], request: 'lp', userRequest: 'left-pad' }, ]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -213,7 +214,7 @@ describe('ArtifactDependencyAuditPlugin', () => { expect(record?.externals).toEqual([{ externalType: 'module', issuers: [source], request: './sibling.js', userRequest: './sibling.js' }]); expect(record?.modules.map((module) => module.resource)).toEqual([source]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -232,7 +233,7 @@ describe('ArtifactDependencyAuditPlugin', () => { source, ]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -253,7 +254,7 @@ describe('ArtifactDependencyAuditPlugin', () => { await expect(readFile(join(root, 'dist', 'scripts', 'probe.mjs'), 'utf8')).resolves .toContain('import(process.argv[2])'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -267,7 +268,7 @@ describe('ArtifactDependencyAuditPlugin', () => { expect(record.externals.map((external) => external.request)).toEqual(['node:fs']); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); }); @@ -295,6 +296,6 @@ it('uses transformed dependencies to allow types and reject direct or aliased co expect(diagnostics).toEqual([expect.objectContaining({ code: 'AB4837', sourcePath: source })]); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 60_000); diff --git a/packages/agent-bundle/tests/dev-artifact-service.test.ts b/packages/agent-bundle/tests/dev-artifact-service.test.ts index 1bd85e6b4..de2767e4b 100644 --- a/packages/agent-bundle/tests/dev-artifact-service.test.ts +++ b/packages/agent-bundle/tests/dev-artifact-service.test.ts @@ -15,6 +15,7 @@ import { ProjectService } from '../src/dev/project-service.ts'; import { createProjectFixture, removeProjectFixture } from './helpers/project-fixture.ts'; import { seedEvalProject, writeEvalSuite } from './support/eval-project.ts'; import { writeFixtureManifest } from './support/manifest.ts'; +import { removeTree } from './support/remove-tree.ts'; const sha256 = (value: string | Uint8Array): string => createHash('sha256').update(value).digest('hex'); @@ -80,7 +81,7 @@ it('publishes one validated prepared project as an immutable epoch and removes i now: () => new Date('2026-08-14T12:00:00.000Z'), removeAttempt: async (path) => { removedAttempts.push(path); - await rm(path, { force: true, recursive: true }); + await removeTree(path); }, }); @@ -115,7 +116,7 @@ it('publishes one validated prepared project as an immutable epoch and removes i expect(removedAttempts).toEqual([attemptRoot]); await expect(readFile(attemptRoot, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -169,7 +170,7 @@ it('reports active-metadata durability uncertainty as a committed build warning' ])); await expect(store.readActiveEpoch()).resolves.toEqual(result.epoch); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -239,7 +240,7 @@ it('allows only an exact epoch store marker as an extra staged artifact file', a expect.objectContaining({ code: 'AB6004' }), ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -289,7 +290,7 @@ it.each(['added', 'changed', 'removed'] as const)( }); await expect(store.readActiveEpoch()).resolves.toBeUndefined(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, ); @@ -317,7 +318,7 @@ it('rejects publication when an executable source loses its execute bit after co }); await expect(store.readActiveEpoch()).resolves.toBeUndefined(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -342,7 +343,7 @@ it('uses the prepared output exclusions when checking source changes after compi expect(result.outcome).toBe('succeeded'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -375,7 +376,7 @@ it('compiles in development mode and carries MCP App compile advisories onto the expect(result.epoch.diagnostics).toEqual({ errors: 0, infos: 0, warnings: 1 }); await expect(store.readActiveEpoch()).resolves.toEqual(result.epoch); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -398,7 +399,7 @@ it('reports a failed MCP App compile as the compiler\'s own AB4770 diagnostics, expect(result).toEqual({ diagnostics: [compileError], outcome: 'failed' }); await expect(store.readActiveEpoch()).resolves.toBeUndefined(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -422,7 +423,7 @@ it('keeps AB7100 for compiler throws that carry no diagnostics', async () => { outcome: 'failed', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -447,8 +448,8 @@ it('uses a root-independent digest for equivalent normalized project models', as expect(left.epoch.modelDigest).toBe(right.epoch.modelDigest); } finally { await Promise.all([ - rm(leftRoot, { force: true, recursive: true }), - rm(rightRoot, { force: true, recursive: true }), + removeTree(leftRoot), + removeTree(rightRoot), ]); } }); @@ -492,8 +493,8 @@ it('changes the canonical model digest when a registered extension changes', asy expect(left.epoch.modelDigest).not.toBe(right.epoch.modelDigest); } finally { await Promise.all([ - rm(leftRoot, { force: true, recursive: true }), - rm(rightRoot, { force: true, recursive: true }), + removeTree(leftRoot), + removeTree(rightRoot), ]); } }); @@ -525,7 +526,7 @@ it('rejects a tampered staging transfer, retains the last good epoch, and cleans }, removeAttempt: async (path) => { removedAttempts.push(path); - await rm(path, { force: true, recursive: true }); + await removeTree(path); }, }); @@ -546,7 +547,7 @@ it('rejects a tampered staging transfer, retains the last good epoch, and cleans await expect(readFile(join(root, '.agent-bundle', 'epochs', 'epoch-tampered', 'plugin.json'), 'utf8')) .rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -568,7 +569,7 @@ it('settles staging and attempt cleanup failures into diagnostics without maskin move: async () => { throw new Error('transfer failed'); }, removeAttempt: async (path) => { removedAttempts.push(path); - await rm(path, { force: true, recursive: true }); + await removeTree(path); throw new Error('attempt cleanup rejected'); }, }); @@ -583,7 +584,7 @@ it('settles staging and attempt cleanup failures into diagnostics without maskin expect(removedAttempts).toEqual([attemptRoot]); await expect(readFile(attemptRoot, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -603,7 +604,7 @@ it('retains a published epoch and reports attempt cleanup failure as a warning', createEpochId: () => 'epoch-cleanup-warning', epochStore: store, removeAttempt: async (path) => { - await rm(path, { force: true, recursive: true }); + await removeTree(path); throw new Error('published attempt cleanup rejected'); }, }); @@ -620,6 +621,6 @@ it('retains a published epoch and reports attempt cleanup failure as a warning', ])); await expect(readFile(attemptRoot, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/dev-coordinator.test.ts b/packages/agent-bundle/tests/dev-coordinator.test.ts index 8535270bf..d1f1e08ca 100644 --- a/packages/agent-bundle/tests/dev-coordinator.test.ts +++ b/packages/agent-bundle/tests/dev-coordinator.test.ts @@ -1,4 +1,4 @@ -import { mkdir, rm, writeFile } from 'node:fs/promises'; +import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { expect, it } from '@rstest/core'; @@ -20,6 +20,7 @@ import { type PreparedProject, } from '../src/dev/index.ts'; import { createProjectFixture } from './helpers/project-fixture.ts'; +import { removeTree } from './support/remove-tree.ts'; const createProject = async (): Promise => (await createProjectFixture({ config: [ @@ -193,7 +194,7 @@ it('serializes a running build and coalesces all concurrent invalidations into o expect(events.filter((type) => type === 'invalidation')).toHaveLength(3); await coordinator.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -255,7 +256,7 @@ it('runs the package build inside the rebuild pass and surfaces its warnings on await coordinator.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -322,7 +323,7 @@ it('queues watcher add, change, and delete paths as one rebuild during a running expect(lintPaths).toEqual([[], ['src/running.ts'], ['src/added.ts', 'src/changed.ts']]); await coordinator.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -359,7 +360,7 @@ it('publishes artifact.available when the built epoch revision disagrees with th expect(events).toEqual(expect.arrayContaining(['artifact.available'])); await coordinator.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -406,7 +407,7 @@ it('retains the last good epoch as stale when a later rebuild fails', async () = expect(events).toEqual(expect.arrayContaining(['artifact.available', 'build.failed', 'artifact.status'])); await coordinator.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -448,7 +449,7 @@ it('surfaces the compiler\'s own MCP App diagnostics on build.failed instead of }); await session.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -502,7 +503,7 @@ it('waits for an in-flight build and closes watcher, diagnostics, and lock exact expect([watcherCloses, diagnosticCloses, lockCloses]).toEqual([1, 1, 1]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -559,7 +560,7 @@ it('reports every failed release structurally after closing the remaining resour { error: watcherFailure, resource: 'watcher' }, ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -622,7 +623,7 @@ it('does not build until its watcher is ready and forwards project watcher exclu expect(buildCalls).toBe(1); await coordinator.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -660,7 +661,7 @@ it('forwards prepared artifact and eval output roots to its watcher', async () = expect(watcherOptions?.outputPaths).toContain(evalRuns); await coordinator.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -714,7 +715,7 @@ it('adds recovered artifact and eval roots to the live watcher before generated expect(builds).toBe(2); await coordinator.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -739,7 +740,7 @@ it('rejects public rebuild requests before startup without preparing or publishi expect(result).toMatchObject({ diagnostics: [expect.objectContaining({ code: 'AB7200' })], outcome: 'failed' }); expect(builds).toBe(0); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -797,7 +798,7 @@ it('rejects public rebuilds until startup has completed watcher readiness', asyn } finally { releaseWatcherReady?.(); await starting?.catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -827,7 +828,7 @@ it('does not enable public rebuilds when startup fails before readiness', async expect(result).toMatchObject({ diagnostics: [expect.objectContaining({ code: 'AB7200' })], outcome: 'failed' }); expect(prepares).toBe(0); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -867,7 +868,7 @@ it('cancels blocked startup readiness and releases its watcher and lock before c await expect(starting).rejects.toThrow('DevCoordinator is closed.'); } finally { releaseWatcherReady?.(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -922,7 +923,7 @@ it('cancels blocked startup recovery without creating later watcher or build sta expect([watcherCreates, builds, events]).toEqual([0, 0, []]); } finally { releaseRecovery?.(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -977,7 +978,7 @@ it('cancels blocked active epoch recovery without creating later watcher or buil expect([watcherCreates, builds, events]).toEqual([0, 0, []]); } finally { releaseActiveRead?.(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1009,7 +1010,7 @@ it('loads the active epoch before a failed initial build and retains it as stale }); await session.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1055,7 +1056,7 @@ it('uses one initial development preparation before preparing later development expect(built).toEqual([initial, later]); await coordinator.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1088,7 +1089,7 @@ it('re-raises a startup failure after releasing the watcher and lock it acquired await coordinator.close(); expect([watcherCloses, lockCloses]).toEqual([1, 1]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1111,7 +1112,7 @@ it('fails a synchronous watcher construction error closed and releases the lock' await coordinator.close(); expect(lockCloses).toBe(1); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1153,7 +1154,7 @@ it('turns a rejected prepared-project hook into a failed prepare attempt', async expect(events).toEqual(expect.arrayContaining(['build.started', 'build.failed', 'artifact.status'])); await session.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1189,7 +1190,7 @@ it('turns prepare, lint, and artifact rejections into failed attempts and events expect(events).toEqual(expect.arrayContaining(['build.failed', 'artifact.status'])); await session.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } } }); diff --git a/packages/agent-bundle/tests/dev-host-install-manager.test.ts b/packages/agent-bundle/tests/dev-host-install-manager.test.ts index 64f6b75eb..b10ded2bb 100644 --- a/packages/agent-bundle/tests/dev-host-install-manager.test.ts +++ b/packages/agent-bundle/tests/dev-host-install-manager.test.ts @@ -1,4 +1,4 @@ -import { cp, lstat, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { cp, lstat, mkdir, mkdtemp, readFile, readdir, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -9,6 +9,7 @@ import { ProjectEventHub } from '../src/dev/events.ts'; import { DevHostInstallManager } from '../src/dev/host-install-manager.ts'; import type { ArtifactEpoch } from '../src/dev/types.ts'; import { writeInstallFixtureManifest } from './support/install-fixture.ts'; +import { removeTree } from './support/remove-tree.ts'; it('removes app-server-managed entries on the first Codex filesystem fallback', async () => { const root = await mkdtemp(join(tmpdir(), 'agent-bundle-host-fallback-')); @@ -91,7 +92,7 @@ it('removes app-server-managed entries on the first Codex filesystem fallback', } finally { await manager.close(); appServer.mockRestore(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -169,6 +170,6 @@ it('reconciles removed generation entries without deleting unmanaged installatio expect(await readFile(join(destination, 'host-receipt.json'), 'utf8')).toBe('keep file'); } finally { await manager.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/dev-host-install.test.ts b/packages/agent-bundle/tests/dev-host-install.test.ts index c5ad14e4f..bfecdedfd 100644 --- a/packages/agent-bundle/tests/dev-host-install.test.ts +++ b/packages/agent-bundle/tests/dev-host-install.test.ts @@ -1,7 +1,7 @@ import { spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { createServer } from 'node:http'; -import { cp, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'; +import { cp, lstat, mkdir, mkdtemp, readFile, readdir, realpath, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; @@ -27,6 +27,7 @@ import { type BuiltHostInstallFixture, } from './support/host-install.ts'; import { writeInstallFixtureManifest } from './support/install-fixture.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; let fixture: BuiltHostInstallFixture | undefined; @@ -51,7 +52,7 @@ const createRoot = async (): Promise => { }; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const builtFixture = (): BuiltHostInstallFixture => { @@ -226,7 +227,7 @@ it('installs a marked public-host dev variant from a stable source and removes i }; const uninstallBundle = async (options: UninstallBundleOptions): Promise => { uninstalls.push(options); - await rm(destination, { force: true, recursive: true }); + await removeTree(destination); return { bundleRoot: options.from, data: { detail: 'test', outcome: 'kept', paths: [], policy: 'keep' }, @@ -435,7 +436,7 @@ unixSocketIt('refreshes a persistent Codex component snapshot before attaching e const mcp = JSON.parse(await readFile(join(source, '.codex-plugin', 'mcp.json'), 'utf8')) as { readonly mcpServers: Readonly>; }; - await rm(destination, { force: true, recursive: true }); + await removeTree(destination); await mkdir(dirname(destination), { recursive: true }); await cp(source, destination, { recursive: true }); appServer.set(`${pluginName}@${marketplaceDocument.name}`, { diff --git a/packages/agent-bundle/tests/dev-lock.test.ts b/packages/agent-bundle/tests/dev-lock.test.ts index d44350ed2..bb0c81b09 100644 --- a/packages/agent-bundle/tests/dev-lock.test.ts +++ b/packages/agent-bundle/tests/dev-lock.test.ts @@ -5,6 +5,7 @@ import { join } from 'node:path'; import { expect, it } from '@rstest/core'; import { acquireDevLock, discoverDevServerUrl, type DevLockStorage } from '../src/dev/dev-lock.ts'; +import { removeTree } from './support/remove-tree.ts'; const lockPathFor = (root: string): string => join(root, '.agent-bundle', 'dev.lock'); const recoveryPathFor = (root: string): string => `${lockPathFor(root)}.recovery`; @@ -21,7 +22,7 @@ it('discovers only a URL published by a live development lock owner', async () = })).rejects.toMatchObject({ code: 'DEV_LOCK_INVALID' }); } finally { await lock.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -68,7 +69,7 @@ it('rejects a second writer with the live owning process URL', async () => { await first.close(); await expect(readFile(lockPathFor(root), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -103,7 +104,7 @@ it('does not overwrite a replacement lock while publishing the server URL', asyn expect(published.nonce).toBe(replacement?.owner.nonce); } finally { await Promise.allSettled([first.close(), replacement?.close()]); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -137,7 +138,7 @@ it('recovers a dead lock only after probing its recorded pid', async () => { expect(typeof published.nonce).toBe('string'); await recovered.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -194,7 +195,7 @@ it('recovers an abandoned recovery gate and serializes eight stale-lock contende await acquired[0]!.lock.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -243,7 +244,7 @@ for (const [label, tmpPrefix, corruptPayload] of CORRUPT_CURRENT_LOCK_CASES) { projectRoot: root, })).rejects.toMatchObject({ code: 'DEV_LOCK_INVALID' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); } @@ -267,7 +268,7 @@ it('rejects a versioned recovery gate instead of accepting an obsolete record sh projectRoot: root, })).rejects.toMatchObject({ code: 'DEV_LOCK_INVALID' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -285,7 +286,7 @@ it('does not let an old handle remove a lock acquired after its record disappear await expect(readFile(lockPathFor(root), 'utf8')).resolves.toBe(replacementRecord); await replacement.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -314,7 +315,7 @@ it('makes concurrent close callers wait for the same cleanup operation', async ( await expect(readFile(lockPathFor(root), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { recoveryGateHeld = false; - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -331,7 +332,7 @@ it('allows close to retry after cleanup fails', async () => { await expect(readFile(lockPathFor(root), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -347,8 +348,8 @@ it('rejects a symlinked agent-bundle directory without writing outside the proje }); await expect(readFile(join(outside, 'dev.lock'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); - await rm(outside, { force: true, recursive: true }); + await removeTree(root); + await removeTree(outside); } }); @@ -370,7 +371,7 @@ it('rejects duplicate keys in a recovery gate record', async () => { projectRoot: root, })).rejects.toMatchObject({ code: 'DEV_LOCK_INVALID' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -408,7 +409,7 @@ it('syncs candidate contents and the containing directory before acquisition res expect(syncBoundaries).toEqual(['candidate', 'directory']); await lock.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -452,7 +453,7 @@ it('unpublishes the lock and fails loudly when candidate cleanup fails after pub const lock = await acquireDevLock({ projectRoot: root }); await lock.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -477,6 +478,6 @@ it('removes an abandoned candidate hardlink while recovering its stale owner', a await expect(readFile(candidatePath, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); await recovered.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/dev-package-build-service.test.ts b/packages/agent-bundle/tests/dev-package-build-service.test.ts index 187a10e1b..df9713d11 100644 --- a/packages/agent-bundle/tests/dev-package-build-service.test.ts +++ b/packages/agent-bundle/tests/dev-package-build-service.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readdir, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -9,11 +9,12 @@ import type { AgentBundleToolsConfig, NormalizedPlugin } from '../src/core/types import { DevPackageBuildService } from '../src/dev/package-build-service.ts'; import type { PreparedProject } from '../src/dev/project-service.ts'; import type { Invalidation } from '../src/dev/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); type BuildOutputs = typeof buildPackageOutputs; diff --git a/packages/agent-bundle/tests/dev-services.test.ts b/packages/agent-bundle/tests/dev-services.test.ts index 6eaaeb619..63df5a21e 100644 --- a/packages/agent-bundle/tests/dev-services.test.ts +++ b/packages/agent-bundle/tests/dev-services.test.ts @@ -17,6 +17,7 @@ import { snapshotProjectSource, type RslintEngine, } from '../src/dev/index.ts'; +import { removeTree } from './support/remove-tree.ts'; it('rejects an absolute Windows path outside its project', () => { expect(containedPathComponents('C:\\project', 'C:\\outside', win32)).toBeUndefined(); @@ -142,7 +143,7 @@ it('prepares a frozen server-only runtime declaration only for development calle expect(Object.isFrozen(runtime.devRuntime?.apps[0]!._meta?.labels)).toBe(true); expect('provenance' in runtime.devRuntime!.apps[0]!).toBe(false); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -161,8 +162,8 @@ it('keeps supplemental runtime declaration and App metadata failures out of the expect(metadata.devRuntimeDiagnostic).toMatchObject({ code: 'AB8200' }); } finally { await Promise.all([ - rm(malformedDeclaration.root, { force: true, recursive: true }), - rm(nonfiniteMetadata.root, { force: true, recursive: true }), + removeTree(malformedDeclaration.root), + removeTree(nonfiniteMetadata.root), ]); } }); @@ -185,7 +186,7 @@ it('surfaces a non-finite registered config extension as the closed AB4500 proje }); expect(JSON.stringify(prepared.source)).not.toContain('NaN'); } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); @@ -211,7 +212,7 @@ it('keeps constructor-shaped extension failures from config proxies behind AB700 }]); expect(JSON.stringify(prepared.source)).not.toContain('forged-extension-secret'); } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); @@ -282,8 +283,8 @@ it('keeps lookalike proxy failures and control-character extension keys redacted expect(keyedPrepared.source.diagnostics[0]!.message.length).toBeLessThan(128); } finally { await Promise.all([ - rm(proxyProject.root, { force: true, recursive: true }), - rm(keyedProject, { force: true, recursive: true }), + removeTree(proxyProject.root), + removeTree(keyedProject), ]); } }); @@ -311,7 +312,7 @@ it('keeps hostile config-extension accessors redacted behind AB7001', async () = }]); expect(JSON.stringify(prepared.source)).not.toContain('hostile-extension-secret'); } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); @@ -343,8 +344,8 @@ it('sanitizes top-level MCP App metadata accessors before source validation with } } finally { await Promise.all([ - rm(returned.root, { force: true, recursive: true }), - rm(thrown.root, { force: true, recursive: true }), + removeTree(returned.root), + removeTree(thrown.root), ]); } }); @@ -361,7 +362,7 @@ it('does not let supplemental metadata sanitization suppress unrelated source di }); expect(prepared.model).toBeUndefined(); } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); @@ -383,7 +384,7 @@ it('stops the shared project pipeline on source errors with a frozen structured expect(Object.isFrozen(prepared.source)).toBe(true); expect(Object.isFrozen(prepared.source.diagnostics)).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -402,7 +403,7 @@ it('returns a frozen source diagnostic when configuration loading fails', async expect(Object.isFrozen(prepared)).toBe(true); expect(Object.isFrozen(prepared.source.diagnostics[0])).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -430,7 +431,7 @@ it('retains the resolved configuration path in the prepared project', async () = ])); expect(Object.isFrozen(prepared.projectContext?.sourceInputs)).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -598,10 +599,10 @@ it('creates an exact deeply frozen root-independent project context', async () = } } finally { await Promise.all([ - rm(leftRoot, { force: true, recursive: true }), - rm(rightRoot, { force: true, recursive: true }), + removeTree(leftRoot), + removeTree(rightRoot), rm(`${leftRoot}-external-source.ts`, { force: true }), - rm(`${leftRoot}-external-dir`, { force: true, recursive: true }), + removeTree(`${leftRoot}-external-dir`), ]); } }); @@ -631,7 +632,7 @@ it('refuses a deleted configuration path after canonical containment', async () sourceInputs, })).toThrow(/ENOENT/i); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -662,7 +663,7 @@ it('refuses a deleted recorded source input after canonical containment', async sourceInputs, })).toThrow(/ENOENT/i); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -687,8 +688,8 @@ it('prepares a symlinked project root from its canonical filesystem identity', a expect(JSON.stringify(prepared.projectContext)).not.toContain(root); } finally { await Promise.all([ - rm(linkedRoot, { force: true, recursive: true }), - rm(root, { force: true, recursive: true }), + removeTree(linkedRoot), + removeTree(root), ]); } }); @@ -724,7 +725,7 @@ it('excludes configured output trees from project identity and reports unsafe ou expect(invalid.model).toBeUndefined(); expect(invalid.projectContext).toBeUndefined(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -781,9 +782,9 @@ it('resolves, excludes, and falls back from configured artifact output paths', a ]); } finally { await Promise.all([ - rm(configuredRoot, { force: true, recursive: true }), - rm(defaultRoot, { force: true, recursive: true }), - rm(malformedRoot, { force: true, recursive: true }), + removeTree(configuredRoot), + removeTree(defaultRoot), + removeTree(malformedRoot), ]); } }); @@ -818,7 +819,7 @@ it('treats a configured eval run directory as generated output, not project sour expect(changed.projectContext).toEqual(initial.projectContext); expect(changed.projectContext?.sourceInputs.map((input) => input.path)).not.toContain('recorded-evals/run.json'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -854,7 +855,7 @@ it('reports external configuration symlinks without exposing the underlying path expect(prepared.model).toBeUndefined(); expect(prepared.projectContext).toBeUndefined(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); await rm(externalConfig, { force: true }); } }); @@ -895,8 +896,8 @@ it('reports snapshot failures as frozen preparation diagnostics', async () => { expect(Object.isFrozen(prepared)).toBe(true); expect(Object.isFrozen(prepared.diagnostics[0])).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); - await rm(externalOutput, { force: true, recursive: true }); + await removeTree(root); + await removeTree(externalOutput); } }); @@ -938,7 +939,7 @@ it('rejects a dangling payload-root symlink that escapes the project', async () sourceInputs: prepared.projectContext?.sourceInputs ?? [], })).toThrow(/outside project root/i); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -970,7 +971,7 @@ it('rejects a missing path under chained relative dangling symlinks that escape ], })).toThrow(/outside project root/i); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -999,7 +1000,7 @@ it('rejects a missing path under a dangling symlink that escapes the project', a ], })).toThrow(/outside project root/i); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1024,7 +1025,7 @@ it('accepts a missing payload directory that stays inside the project', async () }); expect(context.modelDigest).toEqual(expect.any(String)); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1067,7 +1068,7 @@ it('rejects a cyclic payload-root symlink instead of hashing it as contained', a ], })).toThrow(/ELOOP/i); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1116,8 +1117,8 @@ it('rejects an outside-crossing symlink cycle from both entry points', async () })).toThrow(/ELOOP/i); } finally { await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(outside, { force: true, recursive: true }), + removeTree(root), + removeTree(outside), ]); } }); @@ -1157,8 +1158,8 @@ it('rejects a dangling relative symlink under a parent that itself is a symlink' })).toThrow(/outside project root/i); } finally { await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(elsewhere, { force: true, recursive: true }), + removeTree(root), + removeTree(elsewhere), ]); } }); @@ -1197,8 +1198,8 @@ it('rejects symlink/../payload that escapes after the symlink hop', async () => })).toThrow(/outside project root/i); } finally { await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(outside, { force: true, recursive: true }), + removeTree(root), + removeTree(outside), ]); } }); @@ -1260,8 +1261,8 @@ it.each([ expect(nodeFs.realpathSync.native(payload)).toBe(nodeFs.realpathSync.native(join(outside, 'missing'))); } finally { await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(outside, { force: true, recursive: true }), + removeTree(root), + removeTree(outside), ]); } }); @@ -1299,8 +1300,8 @@ posixContainmentIt('rejects a POSIX symlink target that uses a backslash in one expect(nodeFs.realpathSync.native(payload)).toBe(nodeFs.realpathSync.native(outsideMissing)); } finally { await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(outside, { force: true, recursive: true }), + removeTree(root), + removeTree(outside), ]); } }); @@ -1354,10 +1355,10 @@ posixContainmentIt('rejects a contained POSIX filename that includes a backslash }); expect(slashContext.modelDigest).toEqual(expect.any(String)); } finally { - await rm(clean, { force: true, recursive: true }); + await removeTree(clean); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1392,8 +1393,8 @@ it.each([ expect(prepared.diagnostics).not.toContainEqual(expect.objectContaining({ code: 'AB7003' })); } finally { await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(outside, { force: true, recursive: true }), + removeTree(root), + removeTree(outside), ]); } }); @@ -1407,7 +1408,7 @@ it('routes API validation through the project service for configuration failures expect(result).toEqual({ diagnostics: prepared.diagnostics }); expect(Object.isFrozen(result)).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1450,9 +1451,9 @@ it('derives source revisions from authored bytes, including resources and invali expect(invalidChanged.source.revision).not.toBe(invalidInitial.source.revision); } finally { await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(equivalentRoot, { force: true, recursive: true }), - rm(invalidRoot, { force: true, recursive: true }), + removeTree(root), + removeTree(equivalentRoot), + removeTree(invalidRoot), ]); } }); @@ -1491,7 +1492,7 @@ it('changes a payload source revision when an executable bit is lost', async () expect(changed.projectContext?.revision).toBe(changed.source.revision); expect(changed.source.revision).not.toBe(initial.source.revision); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1525,7 +1526,7 @@ it('invalidates cached source hashes after a same-size rewrite with a restored m expect(changed.source.revision).not.toBe(initial.source.revision); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1569,7 +1570,7 @@ it('derives source revisions from the broad authored project graph while excludi expect(importedSourceChanged.source.revision).not.toBe(initial.source.revision); expect(excludedOnlyChanged.source.revision).toBe(importedSourceChanged.source.revision); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/dev-watcher.test.ts b/packages/agent-bundle/tests/dev-watcher.test.ts index cf597fdd6..64deec313 100644 --- a/packages/agent-bundle/tests/dev-watcher.test.ts +++ b/packages/agent-bundle/tests/dev-watcher.test.ts @@ -1,10 +1,11 @@ -import { chmod, mkdtemp, mkdir, rm, stat, unlink, writeFile } from 'node:fs/promises'; +import { chmod, mkdtemp, mkdir, stat, unlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { expect, it } from '@rstest/core'; import { ProjectWatcher, type Invalidation } from '../src/dev/index.ts'; +import { removeTree } from './support/remove-tree.ts'; type Listener = (path: string) => void; @@ -178,7 +179,7 @@ it('invalidates a reported file after chmod changes only its executable mode', a ]); } finally { await watcher.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -232,6 +233,6 @@ it('waits for the real watcher root before reporting create, change, and delete expect(received).toHaveLength(4); } finally { await watcher.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/dev-workbench-packaging.test.ts b/packages/agent-bundle/tests/dev-workbench-packaging.test.ts index f5e41f7e6..39e054d52 100644 --- a/packages/agent-bundle/tests/dev-workbench-packaging.test.ts +++ b/packages/agent-bundle/tests/dev-workbench-packaging.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { access, cp, mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { access, cp, mkdtemp, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { promisify } from 'node:util'; @@ -8,6 +8,7 @@ import { describe, expect, it } from '@rstest/core'; import { availablePort } from './support/available-port.ts'; import { cachedNpmInstallArguments, installedEnvironment, sharedPackedTarball } from './support/shared-pack.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); @@ -63,7 +64,7 @@ it('prunes stale copied workbench assets without removing the package library ou await expect(access(join(isolatedDist, 'cli.js'))).resolves.toBeUndefined(); expect(await readdir(workbench, { recursive: true })).not.toContain('index.js.map'); } finally { - await rm(isolatedRoot, { force: true, recursive: true }); + await removeTree(isolatedRoot); } }, 60_000); @@ -103,7 +104,7 @@ it('serves prebuilt workbench assets from an installed tarball without the repos status: 200, }); } finally { - await rm(consumer, { force: true, recursive: true }); + await removeTree(consumer); } }, 60_000); @@ -186,7 +187,7 @@ it('packages both react-server render children and renders a route invocation fr status: 200, }); } finally { - await rm(consumer, { force: true, recursive: true }); + await removeTree(consumer); } }, 180_000); @@ -233,7 +234,7 @@ it('runs the Agent API from an omit-dev installed tarball with its runtime MCP d status: expect.objectContaining({ status: expect.any(Object) }), }); } finally { - await rm(consumer, { force: true, recursive: true }); + await removeTree(consumer); } }, 60_000); }); diff --git a/packages/agent-bundle/tests/dev-workbench.test.ts b/packages/agent-bundle/tests/dev-workbench.test.ts index 62e67c025..d8a32ae0f 100644 --- a/packages/agent-bundle/tests/dev-workbench.test.ts +++ b/packages/agent-bundle/tests/dev-workbench.test.ts @@ -24,6 +24,7 @@ import { createProjectFixture, removeProjectFixture } from './helpers/project-fi import { agentBundleNodeModules } from './helpers/workspace-paths.ts'; import { timeScale } from './support/time-scale.ts'; import { replaceWatchedSource } from './support/watched-files.ts'; +import { removeTree } from './support/remove-tree.ts'; const readToEnd = async (reader: ReadableStreamDefaultReader): Promise => { const decoder = new TextDecoder(); @@ -291,7 +292,7 @@ it('contains prebuilt workbench asset reads to their declared root', async () => await expect(assets.read('../secret.txt')).resolves.toBeUndefined(); await expect(assets.read('static/../../secret.txt')).resolves.toBeUndefined(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -317,7 +318,7 @@ it('types extensionless notice and license files as text and keeps the binary fa await expect(assets.read('static/payload')).resolves.toMatchObject({ contentType: 'application/octet-stream' }); await expect(assets.read('static/licensed-fixture')).resolves.toMatchObject({ contentType: 'application/octet-stream' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -382,7 +383,7 @@ it('serves packaged notice files over HTTP as text without loosening asset path await expect(probe('/LICENSE')).resolves.toMatchObject({ status: 404 }); await server.close(); } finally { - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 30_000); @@ -416,7 +417,7 @@ it('starts a loopback server with prebuilt assets, does not open on --no-open, a await expect(server.close()).resolves.toBeUndefined(); await expect(fetch(server.url)).rejects.toThrow(); } finally { - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 30_000); @@ -440,7 +441,7 @@ it('fails closed when the optional agent API is enabled without its fixed bearer } } finally { await server?.close(); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 30_000); @@ -474,7 +475,7 @@ it('enables the optional agent API from dev.agentApi when no CLI override is sup expect((await fetch(`${server.url}/mcp`, { method: 'POST' })).status).toBe(401); } finally { await server?.close(); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 30_000); @@ -499,7 +500,7 @@ it('normalizes a relative project root once before constructing every dev servic }); } finally { await server?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 30_000); @@ -663,7 +664,7 @@ it('latches a runtime declaration added to an ordinary Workbench session as rest } finally { events?.close(); await server?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 30_000); @@ -719,7 +720,7 @@ it('keeps the ordinary foreground and artifact lane available when provider star }); } finally { await server?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 30_000); @@ -913,7 +914,7 @@ it('retains Runtime App routes through invalid config updates and reconciles onl } finally { delete runtimeGlobal[stateKey]; await server?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 60_000); @@ -1006,7 +1007,7 @@ it('fences a closing foreground before a held valid runtime reconcile can attach releaseReconcile(); delete runtimeGlobal[stateKey]; await server?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 30_000); @@ -1136,7 +1137,7 @@ it('does not reconcile a valid preparation released after foreground close begin releasePrepare(); delete runtimeGlobal[stateKey]; await server?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 30_000); @@ -1263,7 +1264,7 @@ it('attaches Runtime App routes once when a compiling provider later activates, } finally { delete runtimeGlobal[stateKey]; await server?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 30_000); @@ -1295,7 +1296,7 @@ it('does not attach a compiling Runtime App preview service after foreground clo } finally { delete runtimeGlobal[stateKey]; await server?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 30_000); @@ -1446,7 +1447,7 @@ it('prepares the optional runtime once with the development config context befor } finally { await server?.close().catch(() => undefined); await failedServer?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 30_000); @@ -1482,7 +1483,7 @@ it('builds and serves a target owned only by the workbench registry', async () = expect(registry.names()).toEqual(['workbench-synthetic']); } finally { await server?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 30_000); @@ -1535,7 +1536,7 @@ it('binds real epoch MCP sessions to the workbench lifecycle and drains trace re } finally { await reader?.cancel(); await server?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 60_000); @@ -1597,7 +1598,7 @@ it('hosts real MCP App previews only on the foreground origin and closes their l await expect(access(join(project.root, '.agent-bundle', 'epochs', artifact.activeEpoch.id))).resolves.toBeUndefined(); } finally { await server?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 60_000); @@ -1679,7 +1680,7 @@ it('simulates and replays real epoch-bound hooks through the packaged foreground await expect(fetch(`${server.url}/api/hooks?epochId=${epochId}`, { headers })).rejects.toThrow(); } finally { await server?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 60_000); @@ -1802,7 +1803,7 @@ it('records a durable playground trace and promotes it through the packaged fore await expect(fetch(`${server.url}/api/playground/sessions/${run.session.id}`, { headers })).rejects.toThrow(); } finally { await server?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 60_000); @@ -1868,7 +1869,7 @@ it('inspects and diffs published epochs through the packaged foreground server', expect(unauthorized.status).toBe(403); } finally { await server?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 60_000); diff --git a/packages/agent-bundle/tests/dist-freshness.test.ts b/packages/agent-bundle/tests/dist-freshness.test.ts index f02732b06..23906b7cb 100644 --- a/packages/agent-bundle/tests/dist-freshness.test.ts +++ b/packages/agent-bundle/tests/dist-freshness.test.ts @@ -1,5 +1,5 @@ import { existsSync } from 'node:fs'; -import { lstat, mkdir, mkdtemp, readFile, readdir, rm, utimes, writeFile } from 'node:fs/promises'; +import { lstat, mkdir, mkdtemp, readFile, readdir, utimes, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; @@ -19,6 +19,7 @@ import { type DistFreshness, } from '../../../scripts/dist-freshness.mjs'; import { digestTree } from './support/tree-snapshot.ts'; +import { removeTree } from './support/remove-tree.ts'; const workspaceRoot = process.cwd(); @@ -30,7 +31,7 @@ const editTime = new Date('2026-01-03T00:00:00Z'); const temporaryRoots: string[] = []; afterEach(async () => { - await Promise.all(temporaryRoots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(temporaryRoots.splice(0).map((root) => removeTree(root))); }); /** Writes empty files at `paths` (creating directories) under `root`. */ @@ -123,7 +124,7 @@ describe('distFreshness', () => { it('is missing when the dist is absent, empty, or holds only empty directories', async () => { const { descriptor, root } = await createPackageFixture(); - await rm(join(root, 'dist'), { recursive: true }); + await removeTree(join(root, 'dist')); expect(distFreshness(descriptor)).toMatchObject({ newestOutput: undefined, status: 'missing' }); await mkdir(join(root, 'dist')); expect(distFreshness(descriptor).status).toBe('missing'); diff --git a/packages/agent-bundle/tests/doctor.test.ts b/packages/agent-bundle/tests/doctor.test.ts index 3d796bb9e..d56ba33cf 100644 --- a/packages/agent-bundle/tests/doctor.test.ts +++ b/packages/agent-bundle/tests/doctor.test.ts @@ -25,6 +25,7 @@ import { type DoctorReport, } from '../src/install/doctor.ts'; import { writeInstallFixtureManifest } from './support/install-fixture.ts'; +import { removeTree } from './support/remove-tree.ts'; const writeJson = async (path: string, value: unknown): Promise => { await mkdir(dirname(path), { recursive: true }); @@ -61,7 +62,7 @@ const temporaryDoctor = async (): Promise<{ const endpointDirectory = join(root, 'endpoints'); await mkdir(home, { recursive: true }); return { - cleanup: () => rm(root, { force: true, recursive: true }), + cleanup: () => removeTree(root), endpointDirectory, home, root, @@ -204,7 +205,7 @@ it('reports Cursor directory evidence as available, unavailable, or failed', asy status: 'available', }); - await rm(join(fixture.home, '.cursor'), { recursive: true }); + await removeTree(join(fixture.home, '.cursor')); const unavailable = await runDoctor({ endpointDirectory: fixture.endpointDirectory, home: fixture.home, @@ -362,7 +363,7 @@ it('proves Agent Plugins stdio launch on Cursor: unexpanded spec forms warn, the expect(hostReport(unexpanded, 'cursor').inventory.findings).toEqual([ expect.objectContaining({ launch: { servers: ['launcher', 'probe'], state: 'unexpanded' }, name: 'spec-shape', state: 'installed' }), ]); - await rm(join(installRoot, 'spec-shape'), { recursive: true }); + await removeTree(join(installRoot, 'spec-shape')); // 2. The same pack installed by the emitted install.mjs: expanded, recorded, and verified — the Agent Plugins // contract is checked against the bundle's document, so the absolute paths and §9.1 keys in the copy are no error. @@ -420,7 +421,7 @@ it('proves Agent Plugins stdio launch on Cursor: unexpanded spec forms warn, the ]); // 3. Drift: the data directory disappears and a referenced script is removed — Cursor would spawn paths that do not exist. - await rm(pluginData, { recursive: true }); + await removeTree(pluginData); await rm(join(destination, 'mcp', 'report.mjs')); const drifted = await doctor(); expect(ab7325(drifted)).toEqual([expect.objectContaining({ @@ -439,13 +440,13 @@ it('proves Agent Plugins stdio launch on Cursor: unexpanded spec forms warn, the await mkdir(pluginData, { recursive: true }); await writeFile(join(destination, 'mcp', 'report.mjs'), 'process.stdin.resume();\n'); await cp(destination, join(installRoot, 'moved'), { recursive: true }); - await rm(destination, { recursive: true }); + await removeTree(destination); const moved = await doctor(); expect(ab7325(moved).map((entry) => entry.severity)).toEqual(['error']); expect(ab7325(moved)[0]?.message).toContain(`the receipt expanded PLUGIN_ROOT to ${JSON.stringify(destination)} but the package is installed at ${JSON.stringify(join(installRoot, 'moved'))}`); // The moved copy's on-disk mcp.json is still validated against the recorded bundle document, not its expanded bytes. expect(ab7320Errors(moved)).toEqual([]); - await rm(join(installRoot, 'moved'), { recursive: true }); + await removeTree(join(installRoot, 'moved')); // 5. An edit to the installed copy that keeps every path valid (a bare command renamed) is still drift: // the installed bytes must equal the expansion of the recorded document, and the byte lane then @@ -1462,7 +1463,7 @@ it('reports AB7306 when the bundle identity fails for a reason that is not a man const bundle = await createBundle(fixture.root, 'cursor'); // The manifest points at `.cursor-plugin/plugin.json`; making `.cursor-plugin` a regular // file turns the pointer check into an ENOTDIR read failure rather than a missing file. - await rm(join(bundle, '.cursor-plugin'), { recursive: true }); + await removeTree(join(bundle, '.cursor-plugin')); await writeFile(join(bundle, '.cursor-plugin'), 'not a directory\n'); const report = await runDoctor({ commandRunner: versionRunner, @@ -1517,7 +1518,7 @@ it('classifies Cursor bundle state as installed, missing, drifted, or conflicted { expected: 'installed', mutate: async (_destination: string): Promise => {} }, { expected: 'missing', - mutate: async (destination: string): Promise => rm(destination, { recursive: true }), + mutate: async (destination: string): Promise => removeTree(destination), }, { expected: 'drifted', @@ -1618,7 +1619,7 @@ it('compares the installed Cursor copy against the artifact: current, stale, for expect(staleDiagnostic?.recovery).toContain('replaced automatically'); // Legacy pre-receipt copy with different content: stale, recovery points at --replace. - await rm(destination, { force: true, recursive: true }); + await removeTree(destination); await cp(bundle, destination, { recursive: true }); await writeFile(join(destination, 'payload.txt'), 'older\n'); const legacy = hostReport(await doctor(), 'cursor'); @@ -1626,7 +1627,7 @@ it('compares the installed Cursor copy against the artifact: current, stale, for expect(legacy.diagnostics.find((entry) => entry.code === 'AB7308')?.recovery).toContain('--replace'); // Foreign directory under the plugin name: no receipt, no install surface. - await rm(destination, { force: true, recursive: true }); + await removeTree(destination); await mkdir(destination, { recursive: true }); await writeJson(join(destination, '.cursor-plugin/plugin.json'), { name: 'doctor-fixture', version: '1.2.3' }); await writeFile(join(destination, 'payload.txt'), 'someone else\n'); @@ -2472,7 +2473,7 @@ it('reports a receipt-owned Codex marketplace whose source directory is gone', a `[marketplaces.doctor-fixture-marketplace]\nsource_type = "local"\nsource = ${JSON.stringify(bundle)}\n`, 'utf8', ); - await rm(bundle, { force: true, recursive: true }); + await removeTree(bundle); const report = await runDoctor({ commandRunner: async (request) => { @@ -2618,7 +2619,7 @@ it('explains a Cursor directory holding only preserved runtime state instead of expect(bare.diagnostics.filter((entry) => entry.code === 'AB7307').every((entry) => entry.message.includes('preserved runtime state'))).toBe(true); // With the remnant receipt `uninstall --keep-data` writes, Doctor names the plugin and the receipt too. - await rm(destination, { force: true, recursive: true }); + await removeTree(destination); await installBundle({ from: bundle, home: fixture.home, host: 'cursor' }); await mkdir(join(destination, 'state')); await writeFile(join(destination, 'state', 'plugin.sqlite'), 'durable\n'); @@ -2653,7 +2654,7 @@ it('explains a Cursor directory holding only preserved runtime state instead of } // Without any state left, a remnant receipt over unowned entries is still not "state-only". - await rm(join(destination, 'state'), { force: true, recursive: true }); + await removeTree(join(destination, 'state')); const noState = hostReport(await doctor(), 'cursor'); for (const entry of noState.diagnostics.filter((item) => item.code === 'AB7307')) { expect(entry.message).toContain('retained the unowned entry "operator-notes.md"'); @@ -2690,14 +2691,14 @@ it('explains a Cursor directory holding only preserved runtime state instead of expect(entry.message).not.toContain('PLUGIN_DATA'); expect(entry.recovery).toContain('to consume the remnant'); } - await rm(pluginData, { recursive: true }); + await removeTree(pluginData); expect((await remnantMessages()).every((message) => !message.includes('PLUGIN_DATA') && !message.includes('state/'))).toBe(true); // An emptied state/ directory left behind is not preserved state either. await mkdir(join(destination, 'state')); expect((await remnantMessages()).every((message) => message.includes('whose preserved runtime state has since been removed'))).toBe(true); await writeFile(join(destination, 'state', 'plugin.sqlite'), 'durable\n'); expect((await remnantMessages()).every((message) => message.includes('holds only preserved runtime state (state/)'))).toBe(true); - await rm(join(destination, 'state'), { recursive: true }); + await removeTree(join(destination, 'state')); const elsewhere = join(fixture.root, 'other-home', '.cursor', 'agent-bundle', 'plugin-data', 'doctor-fixture'); await mkdir(elsewhere, { recursive: true }); await writeFile(join(elsewhere, 'cache.sqlite'), 'foreign\n'); @@ -3635,7 +3636,7 @@ it('tracks staged Cursor marketplaces from staged to imported', async () => { expect(hostReport(drifted, 'cursor').bundle?.lifecycle?.registered).toMatchObject({ status: 'observed', value: true }); // Without an imported copy the same drift is only a stale staging: not registered, remove and restage. - await rm(cached, { recursive: true }); + await removeTree(cached); const driftedUnimported = await doctor(); expect(hostReport(driftedUnimported, 'cursor').bundle?.state).toBe('drifted'); expect(hostReport(driftedUnimported, 'cursor').bundle?.lifecycle?.registered).toMatchObject({ status: 'observed', value: false }); @@ -3695,14 +3696,14 @@ it('tracks staged Cursor marketplaces from staged to imported', async () => { expect(hostReport(await doctor(), 'cursor').inventory.findings).toEqual([{ ...stagedFinding, state: 'registered' }]); // A staged repository whose plugin copy was deleted is corrupt for `--from` too, not "missing". - await rm(join(repo, 'plugins', 'doctor-fixture'), { recursive: true }); + await removeTree(join(repo, 'plugins', 'doctor-fixture')); const gonePlugin = await doctor(); expect(hostReport(gonePlugin, 'cursor').bundle).toMatchObject({ marketplace: 'doctor-fixture-marketplace', path: repo, state: 'corrupt' }); expect(gonePlugin.diagnostics.filter((entry) => entry.code === 'AB7307')).toEqual([]); expect(gonePlugin.diagnostics.filter((entry) => entry.code === 'AB7324').length).toBeGreaterThan(0); await cp(bundle, join(repo, 'plugins', 'doctor-fixture'), { recursive: true }); - await rm(join(repo, '.git'), { recursive: true }); + await removeTree(join(repo, '.git')); const corrupt = await doctor(); expect(hostReport(corrupt, 'cursor').inventory.findings).toEqual([expect.objectContaining({ entry: 'doctor-fixture', state: 'corrupt' })]); expect(corrupt.diagnostics.filter((entry) => entry.code === 'AB7324')[0]).toMatchObject({ severity: 'error' }); diff --git a/packages/agent-bundle/tests/durable-fs.test.ts b/packages/agent-bundle/tests/durable-fs.test.ts index 56aaeba23..72a728a58 100644 --- a/packages/agent-bundle/tests/durable-fs.test.ts +++ b/packages/agent-bundle/tests/durable-fs.test.ts @@ -12,6 +12,7 @@ import { } from '../src/core/durable-fs.ts'; import { acquireOwnerLockFile, isProcessAlive } from '../src/core/owner-lock.ts'; import { errnoFailure } from './support/errors.ts'; +import { removeTree } from './support/remove-tree.ts'; const publicationMessages = Object.freeze({ publicationCleanupFailed: 'publication and cleanup both failed', @@ -57,7 +58,7 @@ it('publishes files by hard link, adopts raced winners, and never leaves staging await expect(readFile(path, 'utf8')).resolves.toBe('{"first":true}\n'); await expect(readdir(root)).resolves.toEqual(['published.json']); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -80,7 +81,7 @@ it('propagates staging failures raw while removing the staging file', async () = })).rejects.toBe(writeFailure); await expect(readdir(root)).resolves.toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -134,7 +135,7 @@ it('rolls back a linked publication when the directory fsync fails and aggregate stagingPath, })).rejects.toMatchObject({ errors: [removeFailure], message: 'staging cleanup failed' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -154,7 +155,7 @@ it('tolerates documented Windows directory fsync gaps during link publication', })).resolves.toBe(true); await expect(readFile(path, 'utf8')).resolves.toBe('contents\n'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/effect-filesystem-phase2-dev.test.ts b/packages/agent-bundle/tests/effect-filesystem-phase2-dev.test.ts index 1fb58ad91..f506dd863 100644 --- a/packages/agent-bundle/tests/effect-filesystem-phase2-dev.test.ts +++ b/packages/agent-bundle/tests/effect-filesystem-phase2-dev.test.ts @@ -1,4 +1,4 @@ -import { access, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { access, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -13,6 +13,7 @@ import { ScriptPlaygroundService } from '../src/dev/playground/script-playground import { createWorkbenchAssetSource } from '../src/dev/workbench-assets.ts'; import { platformLayer, runWithPlatform } from '../src/effect/platform.ts'; import { mcpCatalogStub, stdioTransportStub } from './support/mcp-client-stub.ts'; +import { removeTree } from './support/remove-tree.ts'; /** * Phase-2 FileSystem adoption, dev-server slice: every dev service takes a @@ -32,7 +33,7 @@ const runtimes: DevPlatformRuntime[] = []; afterEach(async () => { await Promise.all(runtimes.splice(0).map((runtime) => runtime.close())); - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); /** diff --git a/packages/agent-bundle/tests/effect-filesystem-phase2.test.ts b/packages/agent-bundle/tests/effect-filesystem-phase2.test.ts index cb073154a..ea91fedb7 100644 --- a/packages/agent-bundle/tests/effect-filesystem-phase2.test.ts +++ b/packages/agent-bundle/tests/effect-filesystem-phase2.test.ts @@ -1,6 +1,6 @@ import type { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; -import { access, mkdir, mkdtemp, readFile, rm, stat, symlink, writeFile } from 'node:fs/promises'; +import { access, mkdir, mkdtemp, readFile, stat, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -14,6 +14,7 @@ import { materializeEvalFixture, planEvalFixture } from '../src/eval/fixtures.ts import { copyOpaqueCodexAuthStateProgram } from '../src/host-contracts/native-codex-contract.ts'; import { validatePortablePluginFiles } from '../src/host-contracts/portable-plugin-validation.ts'; import { forwardingSignals } from '../src/services/mcp-run-signals.ts'; +import { removeTree } from './support/remove-tree.ts'; /** * Phase-2 FileSystem adoption: the ordinary reads, copies, and temp @@ -32,7 +33,7 @@ const scratch = async (prefix: string): Promise => { }; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const errno = (code: string, message: string): NodeJS.ErrnoException => { diff --git a/packages/agent-bundle/tests/effect-platform.test.ts b/packages/agent-bundle/tests/effect-platform.test.ts index 9c6e5c73c..2d73fc311 100644 --- a/packages/agent-bundle/tests/effect-platform.test.ts +++ b/packages/agent-bundle/tests/effect-platform.test.ts @@ -1,4 +1,4 @@ -import { access, mkdtemp, rm } from 'node:fs/promises'; +import { access, mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -10,6 +10,7 @@ import { liftPromise } from '../src/effect/lift.ts'; import { platformLayer, runWithPlatform, unwrapPlatformError, withTempDirectory } from '../src/effect/platform.ts'; import * as devApi from '../src/dev/index.ts'; import * as rootApi from '../src/index.ts'; +import { removeTree } from './support/remove-tree.ts'; /** * `runWithPlatform` is the Promise edge for platform-dependent programs: @@ -75,7 +76,7 @@ describe('effect platform layer (agent-bundle)', () => { )); await expect(access(directory)).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); @@ -94,7 +95,7 @@ describe('effect platform layer (agent-bundle)', () => { expect(directory).toBeDefined(); await expect(access(directory!)).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); @@ -111,7 +112,7 @@ describe('effect platform layer (agent-bundle)', () => { )); expect(result).toBe('settled'); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); @@ -130,7 +131,7 @@ describe('effect platform layer (agent-bundle)', () => { expect(directory).toBeDefined(); await expect(access(directory!)).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); @@ -157,7 +158,7 @@ describe('effect platform layer (agent-bundle)', () => { expect(Option.isSome(outcome.exit) && Exit.isFailure(outcome.exit.value) && Cause.hasInterrupts(outcome.exit.value.cause)).toBe(true); await expect(access(outcome.directory)).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); diff --git a/packages/agent-bundle/tests/emitted-artifact-effect-surface.test.ts b/packages/agent-bundle/tests/emitted-artifact-effect-surface.test.ts index c3ab19406..9ab6737d6 100644 --- a/packages/agent-bundle/tests/emitted-artifact-effect-surface.test.ts +++ b/packages/agent-bundle/tests/emitted-artifact-effect-surface.test.ts @@ -1,10 +1,11 @@ -import { cp, mkdtemp, readdir, readFile, rm, symlink } from 'node:fs/promises'; +import { cp, mkdtemp, readdir, readFile, symlink } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, relative } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from '@rstest/core'; import { build } from '../src/api.ts'; +import { removeTree } from './support/remove-tree.ts'; /** * Emitted artifacts never bundle the yieldable framework error bases @@ -120,7 +121,7 @@ beforeAll(async () => { }, 240_000); afterAll(async () => { - if (projectRoot !== undefined) await rm(projectRoot, { force: true, recursive: true }); + if (projectRoot !== undefined) await removeTree(projectRoot); }); describe('emitted artifacts and the yieldable framework error bases', () => { diff --git a/packages/agent-bundle/tests/epoch-store.test.ts b/packages/agent-bundle/tests/epoch-store.test.ts index 10869a70e..8c20b53ed 100644 --- a/packages/agent-bundle/tests/epoch-store.test.ts +++ b/packages/agent-bundle/tests/epoch-store.test.ts @@ -6,6 +6,7 @@ import { expect, it } from '@rstest/core'; import { EpochStore, type EpochStaging } from '../src/dev/epoch-store.ts'; import type { ArtifactEpoch } from '../src/dev/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const epochFor = ( root: string, @@ -96,7 +97,7 @@ it('publishes a validated staging directory as the active immutable epoch', asyn readFile(join(root, '.agent-bundle', 'epochs', 'epoch-1', 'codex', 'plugin.json'), 'utf8'), ).resolves.toBe('{"name":"codex"}\n'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -130,7 +131,7 @@ it('persists one canonical versionless staging and epoch metadata shape', async readFile(activeMetadataPathFor(root), 'utf8').then((value) => JSON.parse(value)), ).resolves.toEqual({ epoch }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -153,7 +154,7 @@ it.each(['active', 'per-epoch'] as const)( : store.acquireEpochReference(epoch.id); await expect(readMetadata).rejects.toMatchObject({ code: 'EPOCH_METADATA_INVALID' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, ); @@ -178,7 +179,7 @@ it.each(['active', 'per-epoch'] as const)( : store.acquireEpochReference(epoch.id); await expect(readMetadata).rejects.toMatchObject({ code: 'EPOCH_METADATA_INVALID' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, ); @@ -205,7 +206,7 @@ it.each([ await expect(store.readActiveEpoch()).rejects.toMatchObject({ code: 'EPOCH_METADATA_INVALID' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, ); @@ -265,7 +266,7 @@ it.each(['marker removal', 'marker file sync', 'marker directory sync'] as const await expect(store.readActiveEpoch()).resolves.toEqual(replacement); await expect(readFile(join(root, '.agent-bundle', 'epochs', replacement.id, marker), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, ); @@ -304,7 +305,7 @@ it('opens Windows regular files with write-capable non-truncating flags and pres expect(entry.flags).toBe(entry.directory ? 'r' : 'r+'); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -342,7 +343,7 @@ it('refuses publication when a Windows regular-file fsync fails and keeps the pr expect(epochEntries).not.toContain(replacement.id); expect(epochEntries.filter((entry) => entry.startsWith('.stage-'))).toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -371,7 +372,7 @@ it('fails epoch publication when file fsync EPERM is not a Windows directory Flu await expect(publishEpoch(store, epochFor(root, 'epoch-posix-file-fsync'))).rejects.toBe(eperm); await expect(store.readActiveEpoch()).resolves.toBeUndefined(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -435,7 +436,7 @@ it('fsyncs staged artifacts and each durable publication rename in commit order' expect(activeMetadataFileSync).toBeGreaterThan(epochMetadataRenameSync); expect(activeMetadataRenameSync).toBeGreaterThan(activeMetadataFileSync); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -449,7 +450,7 @@ it('exposes the validated immutable epoch directory on an acquired reference', a expect(reference.root).toBe(join(root, '.agent-bundle', 'epochs', 'epoch-1')); await reference.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -467,7 +468,7 @@ it('acquires the active epoch and its immutable metadata in one transition', asy expect(Object.isFrozen(reference.epoch)).toBe(true); await reference.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -487,7 +488,7 @@ it('lists detached immutable epoch identities newest first', async () => { expect(Object.isFrozen(listed[0]!)).toBe(true); expect(listed[0]).not.toBe(newest); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -506,7 +507,7 @@ it('rejects an unsafe epoch id before it can create a staging directory', async code: 'ENOENT', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -544,7 +545,7 @@ it('retains active, referenced, and five newest unreferenced epochs until the fi ).toEqual(['epoch-3', 'epoch-4', 'epoch-5', 'epoch-6', 'epoch-7', 'epoch-8']); await expect(readFile(epochMetadataPathFor(root, 'epoch-1'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -572,7 +573,7 @@ it('retains an epoch leased through another store for the same project', async ( readFile(join(root, '.agent-bundle', 'epochs', 'epoch-1', 'claude', 'plugin.json'), 'utf8'), ).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -618,7 +619,7 @@ it('does not admit a cross-store reference while final-release cleanup removes i }); } finally { permitDeletion?.(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -648,7 +649,7 @@ it('keeps a retired epoch until the final of multiple references closes', async ).rejects.toMatchObject({ code: 'ENOENT' }); await expect(readFile(epochMetadataPathFor(root, 'epoch-1'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -677,7 +678,7 @@ it('does not duplicate concurrent close calls before the final reference closes' readFile(join(root, '.agent-bundle', 'epochs', 'epoch-1', 'claude', 'plugin.json'), 'utf8'), ).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -707,7 +708,7 @@ it('retains an epoch when reference acquisition is serialized before its final c readFile(join(root, '.agent-bundle', 'epochs', 'epoch-1', 'claude', 'plugin.json'), 'utf8'), ).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -743,7 +744,7 @@ it('shares final-release cleanup failure with concurrent close callers without r readFile(join(root, '.agent-bundle', 'epochs', 'epoch-1', 'claude', 'plugin.json'), 'utf8'), ).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -784,7 +785,7 @@ it('preserves epoch metadata when final-reference cleanup cannot remove its dire readFile(join(epochsRoot, 'epoch-1', 'claude', 'plugin.json'), 'utf8'), ).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -867,7 +868,7 @@ it('waits for every eligible cleanup deletion before admitting a later reference } finally { earlyGate.resolve(); lateGate.resolve(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -931,7 +932,7 @@ it('aggregates sorted cleanup failures and retries retained metadata after a met await expect(readFile(epochTwoMetadata, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); await expect(readFile(epochThreeCatalog, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -960,7 +961,7 @@ it('keeps the previous active epoch when validation rejects a staged replacement readFile(join(root, '.agent-bundle', 'epochs', 'epoch-2', 'claude', 'plugin.json'), 'utf8'), ).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -986,7 +987,7 @@ it('removes abandoned staging directories without touching the active epoch', as await staging.close(); await staging.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1019,7 +1020,7 @@ it('recovers every abandoned staging directory at once and tolerates a store tha await expect(readdir(join(root, '.agent-bundle', 'epochs'))).resolves.toEqual(['not-staging']); await Promise.all(stagings.map((staging) => staging.close())); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1033,7 +1034,7 @@ it('requires selected targets to exactly match the epoch target digests', async targets: ['claude'], })).rejects.toMatchObject({ code: 'EPOCH_TARGET_SET_INVALID' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1050,7 +1051,7 @@ it('rejects epoch metadata whose manifest path escapes the final epoch directory code: 'EPOCH_MANIFEST_INVALID', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1061,7 +1062,7 @@ it('rejects a replaced staging root even when it has the expected files', async try { const store = new EpochStore({ projectRoot: root }); const staging = await store.createStagingEpoch({ epoch, targets: ['claude'] }); - await rm(staging.root, { force: true, recursive: true }); + await removeTree(staging.root); await mkdir(join(staging.root, 'claude'), { recursive: true }); await Promise.all([ writeFile(join(staging.root, 'claude', 'plugin.json'), 'replacement\n'), @@ -1073,7 +1074,7 @@ it('rejects a replaced staging root even when it has the expected files', async }); await expect(store.readActiveEpoch()).resolves.toBeUndefined(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1101,7 +1102,7 @@ it('rejects a selected target symlink and a missing staged manifest', async () = code: 'EPOCH_MANIFEST_INVALID', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1119,7 +1120,7 @@ it('surfaces corrupt per-epoch metadata instead of silently excluding it from cl readFile(join(root, '.agent-bundle', 'epochs', 'epoch-1', 'claude', 'plugin.json'), 'utf8'), ).resolves.toBe('epoch-1\n'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1150,7 +1151,7 @@ it('does not remove an epoch when concurrent references are admitted before its ).resolves.toBe('epoch-1\n'); await Promise.all(references.map((reference) => reference.close())); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1170,7 +1171,7 @@ it('fails closed when active metadata points at a ghost epoch and leaves cleanup readFile(join(root, '.agent-bundle', 'epochs', 'epoch-1', 'claude', 'plugin.json'), 'utf8'), ).resolves.toBe('epoch-1\n'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1193,7 +1194,7 @@ it('fails closed when active metadata differs from its epoch metadata or the man await expect(store.cleanup()).rejects.toMatchObject({ code: 'EPOCH_METADATA_INVALID' }); await expect(readFile(epochMetadataPathFor(root, 'epoch-1'), 'utf8')).resolves.toContain('epoch-1'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1214,7 +1215,7 @@ it('fails closed when matching active metadata gives its manifest an outside pat await expect(store.readActiveEpoch()).rejects.toMatchObject({ code: 'EPOCH_METADATA_INVALID' }); await expect(store.cleanup()).rejects.toMatchObject({ code: 'EPOCH_METADATA_INVALID' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1233,7 +1234,7 @@ it('continues processing later state transitions after a failed cleanup', async await expect(store.cleanup()).resolves.toBeUndefined(); await expect(store.readActiveEpoch()).resolves.toEqual(epoch); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1269,7 +1270,7 @@ it('reports a cleanup failure as post-commit when the new epoch is already activ }); await expect(store.readActiveEpoch()).resolves.toEqual(committed); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1310,7 +1311,7 @@ it('keeps epoch metadata and contents retryable when native catalog sidecar dele await expect(readFile(epochMetadataPathFor(root, 'epoch-1'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); await expect(readFile(join(root, '.agent-bundle', 'epochs', 'epoch-1', 'claude', 'plugin.json'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1357,7 +1358,7 @@ it('keeps an epoch committed when active-metadata rename succeeds but its parent await expect(readFile(join(root, '.agent-bundle', 'epochs', candidate.id, 'claude', 'plugin.json'), 'utf8')) .resolves.toBe(`${candidate.id}\n`); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1410,7 +1411,7 @@ it('rolls back a publisher-owned catalog sidecar when activation fails after pub expect(syncedPaths.filter((path) => path === join(root, '.agent-bundle', 'epochs'))).toHaveLength(3); expect(syncedPaths.filter((path) => path === join(root, '.agent-bundle', 'epochs', '.metadata'))).toHaveLength(2); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1471,6 +1472,6 @@ it('does not let a losing concurrent publisher remove the winning epoch catalog' await expect(leftStore.readActiveEpoch()).resolves.toEqual(epoch); } finally { releaseMoves.resolve(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/eval-claude-harness.test.ts b/packages/agent-bundle/tests/eval-claude-harness.test.ts index 06a987692..c9cfdde07 100644 --- a/packages/agent-bundle/tests/eval-claude-harness.test.ts +++ b/packages/agent-bundle/tests/eval-claude-harness.test.ts @@ -1,4 +1,4 @@ -import { lstat, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { lstat, mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -29,6 +29,7 @@ import type { } from '../src/host-contracts/native-claude-contract.ts'; import { createProjectFixture } from './helpers/project-fixture.ts'; import { deepFreeze } from '../src/core/freeze.ts'; +import { removeTree } from './support/remove-tree.ts'; const nativeIt = process.env.AGENT_BUNDLE_NATIVE_CLAUDE_SMOKE === '1' ? it : it.skip; @@ -143,8 +144,8 @@ const withClaudeContext = async ( await writer.close(); } } finally { - await rm(root, { force: true, recursive: true }); - await rm(project.root, { force: true, recursive: true }); + await removeTree(root); + await removeTree(project.root); } }; @@ -708,7 +709,7 @@ it('cancels a running child process and leaves no live process behind', async () expect(outcome.failure).toBeUndefined(); expect(() => process.kill(Number(pid), 0)).toThrow(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/eval-cli.test.ts b/packages/agent-bundle/tests/eval-cli.test.ts index 9d6f871c9..fc59cb0be 100644 --- a/packages/agent-bundle/tests/eval-cli.test.ts +++ b/packages/agent-bundle/tests/eval-cli.test.ts @@ -1,4 +1,4 @@ -import { access, cp, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { access, cp, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -11,6 +11,7 @@ import type { EvalRunResult } from '../src/dev/eval/eval-service.ts'; import { createProjectFixture, removeProjectFixture } from './helpers/project-fixture.ts'; import { captureCliTerminal } from './support/cli-terminal.ts'; import { seedEvalProject } from './support/eval-project.ts'; +import { removeTree } from './support/remove-tree.ts'; const runCliWithOutput = async (args: readonly string[]): Promise<{ readonly code: number; @@ -126,7 +127,7 @@ it('persists an explicit artifact outside the project as an opaque portable iden expect(result.run.artifact.manifestPath).not.toContain(artifactRoot); expect(result.run.artifact.manifestPath).not.toContain('..'); } finally { - await rm(externalRoot, { force: true, recursive: true }); + await removeTree(externalRoot); await removeProjectFixture(project.root); } }, 120_000); diff --git a/packages/agent-bundle/tests/eval-codex-harness.test.ts b/packages/agent-bundle/tests/eval-codex-harness.test.ts index bf3c27d40..92850b270 100644 --- a/packages/agent-bundle/tests/eval-codex-harness.test.ts +++ b/packages/agent-bundle/tests/eval-codex-harness.test.ts @@ -21,6 +21,7 @@ import { type EvalCase, type EvalTrialRecord, } from '../src/eval/index.ts'; +import { removeTree } from './support/remove-tree.ts'; const streamFixtureRoot = new URL('../fixtures/eval/codex/', import.meta.url); @@ -167,7 +168,7 @@ const withWorld = async (task: (world: TrialWorld) => Promise): Promise assertion.kind === 'skill-activation')?.evidence).toBe('inferred'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 600_000); diff --git a/packages/agent-bundle/tests/eval-codex-plugins.test.ts b/packages/agent-bundle/tests/eval-codex-plugins.test.ts index bd03b5545..3156bcf0f 100644 --- a/packages/agent-bundle/tests/eval-codex-plugins.test.ts +++ b/packages/agent-bundle/tests/eval-codex-plugins.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -10,6 +10,7 @@ import { codexPluginObserved, readCodexCandidatePlugin, } from '../src/eval/codex-plugins.ts'; +import { removeTree } from './support/remove-tree.ts'; const fixtureRoot = new URL('../fixtures/eval/codex/', import.meta.url); @@ -27,7 +28,7 @@ const withCandidate = async ( await build(root); await task(root); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; diff --git a/packages/agent-bundle/tests/eval-config.test.ts b/packages/agent-bundle/tests/eval-config.test.ts index be0066b35..807d4474a 100644 --- a/packages/agent-bundle/tests/eval-config.test.ts +++ b/packages/agent-bundle/tests/eval-config.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -12,6 +12,7 @@ import { findEvalSuiteFiles, normalizeEvalConfig, } from '../src/eval/index.ts'; +import { removeTree } from './support/remove-tree.ts'; const assertionsModule = fileURLToPath(new URL('../src/eval/assertions.ts', import.meta.url)); const suiteModule = fileURLToPath(new URL('../src/eval/suite.ts', import.meta.url)); @@ -40,7 +41,7 @@ const withProject = async (run: (root: string) => Promise): Promise try { await run(root); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; diff --git a/packages/agent-bundle/tests/eval-fixtures.test.ts b/packages/agent-bundle/tests/eval-fixtures.test.ts index d7769c05e..df017526e 100644 --- a/packages/agent-bundle/tests/eval-fixtures.test.ts +++ b/packages/agent-bundle/tests/eval-fixtures.test.ts @@ -13,6 +13,7 @@ import { planEvalFixture, type EvalCase, } from '../src/eval/index.ts'; +import { removeTree } from './support/remove-tree.ts'; const run = promisify(execFile); @@ -30,7 +31,7 @@ const withProject = async (task: (root: string) => Promise): Promise try { await task(root); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; diff --git a/packages/agent-bundle/tests/eval-graders.test.ts b/packages/agent-bundle/tests/eval-graders.test.ts index 471492a79..305bfa472 100644 --- a/packages/agent-bundle/tests/eval-graders.test.ts +++ b/packages/agent-bundle/tests/eval-graders.test.ts @@ -1,10 +1,11 @@ -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { expect, it } from '@rstest/core'; import { evalScriptGraderSpec, runEvalGraders } from '../src/eval/graders.ts'; +import { removeTree } from './support/remove-tree.ts'; it('replaces a thrown grader fixture path with stable inconclusive evidence', async () => { const root = await mkdtemp(join(tmpdir(), 'agent-bundle-eval-grader-')); @@ -36,6 +37,6 @@ it('replaces a thrown grader fixture path with stable inconclusive evidence', as }); expect(JSON.stringify(result)).not.toContain(fixturePath); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/eval-harness.test.ts b/packages/agent-bundle/tests/eval-harness.test.ts index 20a202de2..083c5f0d5 100644 --- a/packages/agent-bundle/tests/eval-harness.test.ts +++ b/packages/agent-bundle/tests/eval-harness.test.ts @@ -1,4 +1,4 @@ -import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { chmod, mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -28,6 +28,7 @@ import { type PreparedEvalArtifact, } from '../src/eval/index.ts'; import { createProjectFixture } from './helpers/project-fixture.ts'; +import { removeTree } from './support/remove-tree.ts'; const hosts = deepFreeze({ portable: { model: 'deterministic' } }); @@ -84,7 +85,7 @@ const withWorkspace = async (task: (root: string) => Promise): Promise { expect(harnessFailure.pluginFailure).toBeUndefined(); expect(harnessFailure.assertions.every((assertion) => assertion.outcome === 'inconclusive')).toBe(true); } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); }, 240_000); @@ -453,7 +454,7 @@ it('keeps a negative case inconclusive when activation evidence is unavailable', expect(JSON.parse(await readFile(join(writer.directory, unavailable.rawArtifacts[0] ?? ''), 'utf8'))) .toMatchObject({ skillActivation: { level: 'unavailable' } }); } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); }, 240_000); diff --git a/packages/agent-bundle/tests/eval-native-mount.test.ts b/packages/agent-bundle/tests/eval-native-mount.test.ts index 7a2e9ddc2..34e0f5227 100644 --- a/packages/agent-bundle/tests/eval-native-mount.test.ts +++ b/packages/agent-bundle/tests/eval-native-mount.test.ts @@ -14,6 +14,7 @@ import type { } from '../src/host-contracts/native-claude-contract.ts'; import { createProjectFixture, removeProjectFixture } from './helpers/project-fixture.ts'; import { seedEvalProject, writeEvalSuite } from './support/eval-project.ts'; +import { removeTree } from './support/remove-tree.ts'; const claudeModel = 'claude-sonnet-4-5'; const codexModel = 'gpt-5-codex'; @@ -536,7 +537,7 @@ it('keeps native Claude plugin and fixture failures path-free after they are mou claudeRun: async (request) => { if (request.args[0] === '--version') return { exitCode: 0, stderr: '', stdout: '2.1.240 (Claude Code)\n' }; if (request.args[0] === 'auth') { - await rm(join(request.cwd, '.claude-plugin'), { force: true, recursive: true }); + await removeTree(join(request.cwd, '.claude-plugin')); return { exitCode: 0, stderr: '', diff --git a/packages/agent-bundle/tests/eval-run-store.test.ts b/packages/agent-bundle/tests/eval-run-store.test.ts index e233a5d1f..ec4b3aaa0 100644 --- a/packages/agent-bundle/tests/eval-run-store.test.ts +++ b/packages/agent-bundle/tests/eval-run-store.test.ts @@ -16,6 +16,7 @@ import { type CreateEvalRunOptions, type EvalTrialRecordInput, } from '../src/eval/index.ts'; +import { removeTree } from './support/remove-tree.ts'; const artifact = Object.freeze({ manifestPath: 'artifacts/target/agent-bundle.manifest.json', @@ -73,7 +74,7 @@ const withProject = async (task: (root: string) => Promise): Promise try { await task(root); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; @@ -708,7 +709,7 @@ it('rejects trial authority when the cases root changes after its directory snap }, })).rejects.toMatchObject({ code: 'EVAL_RUN_CORRUPT' }); } finally { - await rm(outside, { force: true, recursive: true }); + await removeTree(outside); await writer.close().catch(() => undefined); } }); @@ -725,7 +726,7 @@ it('refuses lexical, absolute, and Windows-absolute run storage escapes without await expect(createEvalRun(runOptions(root, { runsDir: 'C:\\escaped-runs' }))) .rejects.toMatchObject({ code: 'EVAL_RUN_RECORD_INVALID' }); } finally { - await rm(outside, { force: true, recursive: true }); + await removeTree(outside); } }); }); @@ -738,7 +739,7 @@ it('refuses a configured storage ancestor that is a symlink outside the project' await expect(createEvalRun(runOptions(root))).rejects.toMatchObject({ code: 'EVAL_RUN_RECORD_INVALID' }); await expect(readdir(outside)).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(outside, { force: true, recursive: true }); + await removeTree(outside); } }); }); @@ -748,7 +749,7 @@ it('refuses writes when a run directory is replaced with an outside symlink', as const writer = await createEvalRun(runOptions(root)); const outside = `${root}-outside`; try { - await rm(writer.directory, { force: true, recursive: true }); + await removeTree(writer.directory); await symlink(outside, writer.directory); await expect(writer.appendEvent({ kind: 'escaped', payload: {} })) @@ -756,7 +757,7 @@ it('refuses writes when a run directory is replaced with an outside symlink', as await expect(readdir(outside)).rejects.toMatchObject({ code: 'ENOENT' }); await expect(writer.close()).rejects.toBeInstanceOf(AggregateError); } finally { - await rm(outside, { force: true, recursive: true }); + await removeTree(outside); } }); }); diff --git a/packages/agent-bundle/tests/eval-service.test.ts b/packages/agent-bundle/tests/eval-service.test.ts index 2861bc64c..0c7905fdb 100644 --- a/packages/agent-bundle/tests/eval-service.test.ts +++ b/packages/agent-bundle/tests/eval-service.test.ts @@ -14,6 +14,7 @@ import { evalCaseFromDraft } from '../src/eval/index.ts'; import { EvalRunWriter } from '../src/eval/run-store.ts'; import { createProjectFixture, removeProjectFixture } from './helpers/project-fixture.ts'; import { seedEvalProject, writeEvalSuite } from './support/eval-project.ts'; +import { removeTree } from './support/remove-tree.ts'; const service = (root: string): EvalService => new EvalService({ projectRoot: root, targets: ['portable'] }); @@ -369,7 +370,7 @@ it('fails closed when an ancestor of a persisted raw artifact becomes a symlink' const outside = join(project.root, 'outside-artifacts'); await mkdir(outside); await writeFile(join(outside, 'evidence.json'), '{"substituted":true}\n'); - await rm(artifactDirectory, { force: true, recursive: true }); + await removeTree(artifactDirectory); await symlink(outside, artifactDirectory); let opened: Awaited> | undefined; @@ -482,7 +483,7 @@ it('fails closed without a filesystem path when a run root is swapped after pers const directory = join(project.root, '.agent-bundle', 'runs', created.run.id); const outside = join(project.root, 'swapped-run'); await mkdir(outside); - await rm(directory, { force: true, recursive: true }); + await removeTree(directory); await symlink(outside, directory); await expect(evals.openArtifact(created.run.id, ref)).rejects.toMatchObject({ diff --git a/packages/agent-bundle/tests/eval-workbench.test.ts b/packages/agent-bundle/tests/eval-workbench.test.ts index f59f60159..3938fea3c 100644 --- a/packages/agent-bundle/tests/eval-workbench.test.ts +++ b/packages/agent-bundle/tests/eval-workbench.test.ts @@ -1,4 +1,4 @@ -import { access, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { access, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -10,6 +10,7 @@ import { startDevServer } from '../src/dev/workbench-server.ts'; import type { EvalRunRecord } from '../src/eval/run-store.ts'; import { createProjectFixture, removeProjectFixture } from './helpers/project-fixture.ts'; import { seedEvalProject } from './support/eval-project.ts'; +import { removeTree } from './support/remove-tree.ts'; it('runs a real deterministic eval through the packaged foreground server', async () => { const project = await createProjectFixture(); @@ -121,6 +122,6 @@ it('runs a real deterministic eval through the packaged foreground server', asyn await expect(fetch(`${server.url}/api/evals/suites`, { headers })).rejects.toThrow(); } finally { await server?.close().catch(() => undefined); - await Promise.all([removeProjectFixture(project.root), rm(assetsRoot, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(project.root), removeTree(assetsRoot)]); } }, 180_000); diff --git a/packages/agent-bundle/tests/event-handler-artifact-graph.test.ts b/packages/agent-bundle/tests/event-handler-artifact-graph.test.ts index ffe566ac3..e5846b298 100644 --- a/packages/agent-bundle/tests/event-handler-artifact-graph.test.ts +++ b/packages/agent-bundle/tests/event-handler-artifact-graph.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, realpath, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, realpath, symlink, writeFile } from 'node:fs/promises'; import { isBuiltin } from 'node:module'; import { tmpdir } from 'node:os'; import { dirname, join, relative } from 'node:path'; @@ -12,6 +12,7 @@ import { parseArtifactManifest } from '../src/build/manifest.ts'; import { validateArtifact } from '../src/build/validate-artifact.ts'; import { compileRouteGraph } from '../src/routes/graph.ts'; import { runNodeScript } from './support/run-node-script.ts'; +import { removeTree } from './support/remove-tree.ts'; /** * #595's emitted-graph proof, pre-staged at the built-artifact level: the @@ -218,7 +219,7 @@ describe('handler artifact graph (#595)', () => { }, 240_000); afterAll(async () => { - if (root !== undefined) await rm(root, { force: true, recursive: true }); + if (root !== undefined) await removeTree(root); }); it('attaches the handler leaf to the event route node, and the leaf\'s source graph is cheap by construction', async () => { diff --git a/packages/agent-bundle/tests/examples-check-script.test.ts b/packages/agent-bundle/tests/examples-check-script.test.ts index 33906ca83..6a89226d2 100644 --- a/packages/agent-bundle/tests/examples-check-script.test.ts +++ b/packages/agent-bundle/tests/examples-check-script.test.ts @@ -1,10 +1,11 @@ import { execFile as executeFile } from 'node:child_process'; -import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { promisify } from 'node:util'; import { expect, it } from '@rstest/core'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); @@ -55,6 +56,6 @@ await writeFile(process.env.FAKE_PNPM_CAPTURE, JSON.stringify({ }); } } finally { - await rm(fixtureRoot, { force: true, recursive: true }); + await removeTree(fixtureRoot); } }); diff --git a/packages/agent-bundle/tests/examples-contract.test.ts b/packages/agent-bundle/tests/examples-contract.test.ts index 273a26da7..40892f3fd 100644 --- a/packages/agent-bundle/tests/examples-contract.test.ts +++ b/packages/agent-bundle/tests/examples-contract.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { cp, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; +import { cp, mkdtemp, readFile, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { promisify } from 'node:util'; @@ -10,6 +10,7 @@ import { expect, it } from '@rstest/core'; import { build, inspect, invokeMcp, listHooks, listMcp, runEvals, simulateHook, validate } from '../src/api.ts'; import { projectVersionLabel } from '../src/core/project-context.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const examplesRoot = join(process.cwd(), 'examples'); @@ -17,7 +18,7 @@ const examplesRoot = join(process.cwd(), 'examples'); it('builds the Skills Starter through public Agent Bundle APIs', async () => { const root = join(examplesRoot, 'skills-starter'); const output = join(root, '.agent-bundle', 'example-contract'); - await rm(output, { force: true, recursive: true }); + await removeTree(output); try { const inspection = await inspect({ root }); @@ -97,7 +98,7 @@ it('builds the Skills Starter through public Agent Bundle APIs', async () => { }], }); } finally { - await rm(output, { force: true, recursive: true }); + await removeTree(output); } }); @@ -106,7 +107,7 @@ it('publishes the MCP App example service readiness across targets and returns d const stateRoot = join(root, '.agent-bundle'); const output = join(stateRoot, 'example-contract'); const unrelatedCwd = await mkdtemp(join(tmpdir(), 'mcp-app-fixture-check-')); - await rm(stateRoot, { force: true, recursive: true }); + await removeTree(stateRoot); try { const built = await build({ output, root }); @@ -235,8 +236,8 @@ it('publishes the MCP App example service readiness across targets and returns d }); } finally { await Promise.all([ - rm(stateRoot, { force: true, recursive: true }), - rm(unrelatedCwd, { force: true, recursive: true }), + removeTree(stateRoot), + removeTree(unrelatedCwd), ]); } }, 30_000); @@ -245,7 +246,7 @@ it('simulates the Hooks example and executes release checks', async () => { const root = join(examplesRoot, 'hooks-and-scripts'); const output = join(root, '.agent-bundle', 'example-contract'); const unrelatedCwd = await mkdtemp(join(tmpdir(), 'hooks-and-scripts-contract-')); - await rm(output, { force: true, recursive: true }); + await removeTree(output); try { const built = await build({ output, root }); @@ -289,8 +290,8 @@ it('simulates the Hooks example and executes release checks', async () => { expect(blocker.code).toBe(2); } finally { await Promise.all([ - rm(output, { force: true, recursive: true }), - rm(unrelatedCwd, { force: true, recursive: true }), + removeTree(output), + removeTree(unrelatedCwd), ]); } }); @@ -329,7 +330,7 @@ it('serves the routed Audiobook Curator artifact through a real MCP client and i let client: Client | undefined; try { const compiled = await build({ output, root, targets: ['claude'] }); - await rm(join(root, 'src'), { force: true, recursive: true }); + await removeTree(join(root, 'src')); const server = compiled.model.mcpServers.find((candidate) => candidate.name === 'curator'); expect(server?.generatedRoutes).toHaveLength(18); const entry = join(output, server!.args![0]!); @@ -375,6 +376,6 @@ it('serves the routed Audiobook Curator artifact through a real MCP client and i }); } finally { await client?.close(); - await rm(fixtureRoot, { force: true, recursive: true }); + await removeTree(fixtureRoot); } }); diff --git a/packages/agent-bundle/tests/function-authoring-build.test.ts b/packages/agent-bundle/tests/function-authoring-build.test.ts index b79050ecc..97b1aef20 100644 --- a/packages/agent-bundle/tests/function-authoring-build.test.ts +++ b/packages/agent-bundle/tests/function-authoring-build.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, symlink, writeFile } from 'node:fs/promises'; import { dirname, join } from 'node:path'; import { tmpdir } from 'node:os'; @@ -6,6 +6,7 @@ import { expect, it } from '@rstest/core'; import { build } from '../src/api.ts'; import { runNodeScript } from './support/run-node-script.ts'; +import { removeTree } from './support/remove-tree.ts'; it('runs compiled function events, explicit JSX views, and CLI metadata and JSON input without source', async () => { const root = await mkdtemp(join(tmpdir(), 'ab-function-authoring-')); @@ -53,7 +54,7 @@ export default function Convert({input}) { return input; } const prompt = result.build.compiledHooks.find((hook) => hook.id === 'hook:event-route:prompt-submit')!; expect(await readFile(prompt.output, 'utf8')).not.toContain('Rendered decision'); expect(await readFile(tool.output, 'utf8')).not.toContain('react.transitional.element'); - await rm(join(root, 'src'), { recursive: true }); + await removeTree(join(root, 'src')); const invoke = (output: string, input: object) => runNodeScript({ args: [output], cwd: root, input: JSON.stringify(input) }); const base = { permission_mode: 'default', cwd: root, session_id: 'session', transcript_path: join(root, 'transcript.json') }; const denial = await invoke(tool.output, { ...base, hook_event_name: 'PreToolUse', tool_use_id: 'use-1', tool_name: 'Write', tool_input: {} }); @@ -84,7 +85,7 @@ export default function Convert({input}) { return input; } expect(session.code, session.stderr).toBe(0); expect(session.stdout).toContain('Check the release checklist.'); } finally { - await rm(root, { recursive: true, force: true }); + await removeTree(root); } }, 240_000); @@ -118,7 +119,7 @@ export default async function AfterView() { const built = await build({ root, output: join(root, 'artifact') }); expect(built.diagnostics.filter((diagnostic) => diagnostic.severity === 'error')).toEqual([]); const output = built.build.compiledHooks[0]!.output; - await rm(join(root, 'src'), { recursive: true }); + await removeTree(join(root, 'src')); const startedAt = performance.now(); const result = await runNodeScript({ args: [output], @@ -147,7 +148,7 @@ export default async function AfterView() { if (childPid !== undefined) { try { process.kill(childPid, 'SIGKILL'); } catch { /* The deadline already terminated it. */ } } - await rm(root, { recursive: true, force: true }); + await removeTree(root); } }, 180_000); @@ -169,7 +170,7 @@ export default defineState({ id: 'event-context/state', lifetime: 'workspace-dur } const built = await build({ root, output: join(root, 'artifact') }); const output = built.build.compiledHooks[0]!.output; - await rm(join(root, 'src'), { recursive: true }); + await removeTree(join(root, 'src')); const native = { hook_event_name: 'PreToolUse', permission_mode: 'default', cwd: root, session_id: 'session', transcript_path: join(root, 'transcript.json'), tool_use_id: 'use-1', tool_name: 'Write', tool_input: {} }; const probe = await runNodeScript({ args: ['--input-type=module', '-e', `const { prepareRouteInvocation } = await import(${JSON.stringify(output)}); const trace = []; const result = await prepareRouteInvocation(${JSON.stringify(native)}, new AbortController().signal, event => trace.push(event)); const again = await prepareRouteInvocation(${JSON.stringify(native)}, new AbortController().signal); console.log(JSON.stringify({ value: JSON.parse(result.gate.reason), next: JSON.parse(again.gate.reason).process.hits, providers: result.providerObservations, trace }));`], @@ -191,6 +192,6 @@ export default defineState({ id: 'event-context/state', lifetime: 'workspace-dur expect(observed.providers).toEqual([expect.objectContaining({ key: 'context', status: 'mounted', durationMs: expect.any(Number) })]); expect(observed.trace.map((event: { kind: string }) => event.kind)).toEqual(['handler.start', 'providers.start', 'providers.finish', 'handler.outcome']); } finally { - await rm(root, { recursive: true, force: true }); + await removeTree(root); } }, 180_000); diff --git a/packages/agent-bundle/tests/generated-module-evidence.test.ts b/packages/agent-bundle/tests/generated-module-evidence.test.ts index 64768f2bc..ce56fa6b1 100644 --- a/packages/agent-bundle/tests/generated-module-evidence.test.ts +++ b/packages/agent-bundle/tests/generated-module-evidence.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, realpath, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, realpath, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -27,11 +27,12 @@ import type { NormalizedHook, SourceProvenance } from '../src/core/types.ts'; import type { AgentBundleMeta } from '../src/meta.ts'; import type { CompiledAgentRoute, CompiledCliCommand } from '../src/routes/types.ts'; import { agentBundleNodeModules } from './helpers/workspace-paths.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const testMeta: AgentBundleMeta = Object.freeze({ diff --git a/packages/agent-bundle/tests/generated-route-server.test.ts b/packages/agent-bundle/tests/generated-route-server.test.ts index 77790be4e..19908f824 100644 --- a/packages/agent-bundle/tests/generated-route-server.test.ts +++ b/packages/agent-bundle/tests/generated-route-server.test.ts @@ -2,7 +2,7 @@ import { Client } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; import { spawn } from 'node:child_process'; import { existsSync } from 'node:fs'; -import { mkdir, mkdtemp, readFile, rm, stat, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, stat, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; @@ -16,11 +16,12 @@ import { requestEventRuntimeStatus, } from '../src/events/ipc.ts'; import { eventuallyPasses } from './support/eventually.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const writeProjectFile = async (root: string, path: string, contents: string): Promise => { diff --git a/packages/agent-bundle/tests/helpers/project-fixture.ts b/packages/agent-bundle/tests/helpers/project-fixture.ts index 9ab4330cc..7581f6268 100644 --- a/packages/agent-bundle/tests/helpers/project-fixture.ts +++ b/packages/agent-bundle/tests/helpers/project-fixture.ts @@ -1,7 +1,8 @@ -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { dirname, join, resolve } from 'node:path'; import { rstestWorkerRoot } from '../../../../rstest.worker-isolation.ts'; +import { removeTree } from '../support/remove-tree.ts'; export interface ProjectFixture { configPath: string; @@ -119,5 +120,5 @@ export const createProjectFixture = async ( }; export const removeProjectFixture = async (root: string): Promise => { - await rm(root, { force: true, maxRetries: 5, recursive: true, retryDelay: 50 }); + await removeTree(root); }; diff --git a/packages/agent-bundle/tests/hook-handler-contract.test.ts b/packages/agent-bundle/tests/hook-handler-contract.test.ts index 9966d8867..699a1b070 100644 --- a/packages/agent-bundle/tests/hook-handler-contract.test.ts +++ b/packages/agent-bundle/tests/hook-handler-contract.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -25,6 +25,7 @@ import { createDefaultRegistry } from '../src/adapters/registry.ts'; import { launchEnvLayerSpecifier } from '../src/build/launch-env-shell.ts'; import { canonicalHookEvents } from '../src/core/types.ts'; import type { CanonicalAgentEvent } from '../src/routes/public.ts'; +import { removeTree } from './support/remove-tree.ts'; type Host = 'claude' | 'codex' | 'cursor'; @@ -107,7 +108,7 @@ beforeAll(async () => { }); afterAll(async () => { - await rm(root, { force: true, recursive: true }); + await removeTree(root); }); /** Every result object shape over the four admitted keys: 4 outcomes × 2 × 2 × 2. */ diff --git a/packages/agent-bundle/tests/hook-playground-service.test.ts b/packages/agent-bundle/tests/hook-playground-service.test.ts index 59312058e..a03fe6a30 100644 --- a/packages/agent-bundle/tests/hook-playground-service.test.ts +++ b/packages/agent-bundle/tests/hook-playground-service.test.ts @@ -1,5 +1,5 @@ import { supportedCapabilities } from './support/adapter-capabilities.ts'; -import { access, cp, mkdtemp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { access, cp, mkdtemp, mkdir, readFile, readdir, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -25,6 +25,7 @@ import { } from '../src/dev/playground/hook-playground-service.ts'; import { HookService } from '../src/services/hook-service.ts'; import type { ArtifactEpoch } from '../src/dev/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const registry: NormalizationTargetRegistry = { configExtensions: () => [], @@ -372,7 +373,7 @@ it('uses the injected adapter hook contract for custom manifests, mappings, matc }], }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -468,7 +469,7 @@ it('runs fixture and inline canonical input through the epoch-bound wrapper and stdout: JSON.stringify(inline.nativeOutput), }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -520,7 +521,7 @@ it('returns target diagnostics from simulation and replay for an unknown string }], }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -556,7 +557,7 @@ it('projects every emitted Codex and Claude event deterministically and exposes } } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -617,7 +618,7 @@ it('isolates malicious relative writes from the referenced epoch and rejects coo await writeFile(manifestPath, `${JSON.stringify(manifest)}\n`); await expect(service.simulate(request)).rejects.toThrow(/stored digest/i); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -653,7 +654,7 @@ it('settles route cancellation and cleans the per-simulation clone before releas expect(runnableArtifact).not.toBe(join(root, '.agent-bundle', 'epochs', 'epoch-1')); await expect(access(runnableArtifact)).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -693,8 +694,8 @@ it('settles clone copies before cleanup and reference release when an injected c await expect(access(store.cloneRoot)).rejects.toMatchObject({ code: 'ENOENT' }); } finally { await new Promise((resolvePromise) => { setTimeout(resolvePromise, 75); }); - if (store.cloneRoot !== undefined) await rm(store.cloneRoot, { force: true, recursive: true }); - await rm(root, { force: true, recursive: true }); + if (store.cloneRoot !== undefined) await removeTree(store.cloneRoot); + await removeTree(root); } }, 30_000); @@ -734,7 +735,7 @@ it('distinguishes an unsupported canonical event from an unsupported target', as }], }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -768,6 +769,6 @@ it('returns a diagnostic for a target without a hook event mapping', async () => }], }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); diff --git a/packages/agent-bundle/tests/hook-receipt-pipe.test.ts b/packages/agent-bundle/tests/hook-receipt-pipe.test.ts index 0614b85f5..26387565e 100644 --- a/packages/agent-bundle/tests/hook-receipt-pipe.test.ts +++ b/packages/agent-bundle/tests/hook-receipt-pipe.test.ts @@ -1,5 +1,5 @@ import { spawn } from 'node:child_process'; -import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, symlink, writeFile } from 'node:fs/promises'; import { createServer, type Server } from 'node:http'; import type { AddressInfo } from 'node:net'; import { tmpdir } from 'node:os'; @@ -13,6 +13,7 @@ import { diagnostic, isRequestDiagnostic, responseDiagnostic } from '../src/dev/ import type { TraceEntry } from '../src/dev/trace/trace-entry.ts'; import { TraceHub } from '../src/dev/trace/trace-hub.ts'; import { DEV_INSTALL_MARKER_FILE } from '../src/events/trace-receipt.ts'; +import { removeTree } from './support/remove-tree.ts'; /** * #600 PR 2, lane T7: a host-invoked hook against the dev plugin reports a @@ -86,7 +87,7 @@ const nativePreToolUse = (root: string, toolUseId: string): Readonly { const root = await mkdtemp(join(tmpdir(), 'agent-bundle-hook-receipt-pipe-')); - cleanups.push(() => rm(root, { force: true, recursive: true })); + cleanups.push(() => removeTree(root)); await symlink(join(process.cwd(), 'examples', 'audiobook-curator', 'node_modules'), join(root, 'node_modules'), 'dir'); await Promise.all([ writeProjectFile(root, 'package.json', JSON.stringify({ diff --git a/packages/agent-bundle/tests/hook-receipts.test.ts b/packages/agent-bundle/tests/hook-receipts.test.ts index d88188f1c..20e34c076 100644 --- a/packages/agent-bundle/tests/hook-receipts.test.ts +++ b/packages/agent-bundle/tests/hook-receipts.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, stat, writeFile } from 'node:fs/promises'; import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'; import type { AddressInfo } from 'node:net'; import { tmpdir } from 'node:os'; @@ -37,6 +37,7 @@ import { } from '../src/events/trace-receipt.ts'; import { createEventTracer, eventTraceExecution } from '../src/events/trace.ts'; import { isLoopbackHttpOrigin } from '../src/core/loopback-origin.ts'; +import { removeTree } from './support/remove-tree.ts'; const cleanups: (() => Promise | void)[] = []; @@ -377,7 +378,7 @@ it('refuses receipts without the token, with an Origin header, over the size cap it('publishes an owner-only endpoint record under the project and removes it on close', async () => { const projectRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-hook-receipts-')); - cleanups.push(() => rm(projectRoot, { force: true, recursive: true })); + cleanups.push(() => removeTree(projectRoot)); const hub = new TraceHub({ projectRoot: '/work/project' }); const attachment = attachHookReceipts({ projectRoot, trace: hub }); expect(attachment.token).toMatch(/^[A-Za-z0-9_-]{43}$/u); @@ -406,7 +407,7 @@ it('publishes an owner-only endpoint record under the project and removes it on it('resolves the wrapper endpoint from the environment, else the dev install marker beside the wrapper', async () => { const root = await mkdtemp(join(tmpdir(), 'agent-bundle-receipt-resolve-')); - cleanups.push(() => rm(root, { force: true, recursive: true })); + cleanups.push(() => removeTree(root)); const anchor = pathToFileURL(join(root, 'bundle', 'hooks', 'before-tool.claude.mjs')).href; const fromEnv = await resolveEventTraceReceiptEndpoint({ anchor, diff --git a/packages/agent-bundle/tests/hooks.test.ts b/packages/agent-bundle/tests/hooks.test.ts index 4b2726afc..a09594046 100644 --- a/packages/agent-bundle/tests/hooks.test.ts +++ b/packages/agent-bundle/tests/hooks.test.ts @@ -36,6 +36,7 @@ import { normalizeProject } from '../src/config/normalize.ts'; import type { LoadedConfig } from '../src/config/load.ts'; import type { NormalizationTargetRegistry, NormalizedPlugin } from '../src/core/types.ts'; import { validateModel, validateSource } from '../src/config/validate.ts'; +import { removeTree } from './support/remove-tree.ts'; const probeMeta: AgentBundleMeta = Object.freeze({ name: 'hook-probe', @@ -384,7 +385,7 @@ it('does not share a persistent Rslib cache between generated executables', asyn }, }); } finally { - await rm(outputRoot, { force: true, recursive: true }); + await removeTree(outputRoot); } const [{ config }] = createOptions as [{ @@ -493,7 +494,7 @@ it('closes the Rslib build result and serves the generated wrapper entry virtual .filter((plugin) => plugin instanceof rspack.experiments.VirtualModulesPlugin); expect(virtualPlugins).toHaveLength(1); } finally { - await Promise.all([outputRoot, projectRoot].map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all([outputRoot, projectRoot].map((root) => removeTree(root))); } }); @@ -525,7 +526,7 @@ it('refuses to compile while anything occupies the reserved generated-module nam ); expect(createRslib).not.toHaveBeenCalled(); } finally { - await Promise.all([outputRoot, projectRoot].map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all([outputRoot, projectRoot].map((root) => removeTree(root))); } }); @@ -579,7 +580,7 @@ it('fails closed when the resolved environment lost its virtual modules or wrapp plugins: [new rspack.experiments.VirtualModulesPlugin({})], })).rejects.toThrow(/without its reserved module aliases/u); } finally { - await rm(outputRoot, { force: true, recursive: true }); + await removeTree(outputRoot); } }); @@ -616,7 +617,7 @@ it('closes the Rslib build result when provenance stats are unavailable', async expect(close).toHaveBeenCalledOnce(); } finally { - await rm(outputRoot, { force: true, recursive: true }); + await removeTree(outputRoot); } }); @@ -655,7 +656,7 @@ it('normalizes a shorthand session-start hook into a frozen stable record', asyn expect(Object.isFrozen(hooks)).toBe(true); expect(Object.isFrozen((hooks as readonly unknown[])[0]!)).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -682,7 +683,7 @@ it('filters inherited hook targets through adapter hook capabilities', async () expect(targetRegistry.get('codex').plan(model).hookEntries).toHaveLength(1); expect(targetRegistry.get('claude').plan(model).hookEntries).toHaveLength(1); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -760,7 +761,7 @@ it('loads and deterministically merges target-native hook documents after genera }); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -792,7 +793,7 @@ it('reports stable target-native hook file diagnostics before merge', async () = }, { skills: [] }, targetRegistry); expect(targetRegistry.get('codex').plan(invalidSchema).diagnostics.map((diagnostic) => diagnostic.code)).toEqual(['codex.native-hooks.schema']); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -822,7 +823,7 @@ it('lists and simulates only validated wrappers from a clean copied artifact', a ]); await build({ model, outputRoot, projectRoot: root, registry: createDefaultRegistry(), routeGraph: emptyCompiledRouteGraph }); await cp(outputRoot, artifact, { recursive: true }); - await rm(root, { force: true, recursive: true }); + await removeTree(root); await expect(importPublishedHook(join(artifact, 'hooks', 'session-start-session-start-7ab7e8a5.codex.mjs'))).resolves.toEqual({ code: 0, @@ -896,8 +897,8 @@ it('lists and simulates only validated wrappers from a clean copied artifact', a })).rejects.toThrow(/artifact files do not match/i); } finally { await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(consumer, { force: true, recursive: true }), + removeTree(root), + removeTree(consumer), ]); } }, 15_000); @@ -1017,8 +1018,8 @@ it('escalates timed-out and aborted wrapper process trees from TERM to KILL befo if (previousDescendantPidPath === undefined) delete process.env.AGENT_BUNDLE_HOOK_TREE_TEST_PID; else process.env.AGENT_BUNDLE_HOOK_TREE_TEST_PID = previousDescendantPidPath; await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(consumer, { force: true, recursive: true }), + removeTree(root), + removeTree(consumer), ]); } }, 10_000); @@ -1103,8 +1104,8 @@ it('waits for an admitted Windows taskkill cleanup after its wrapper leader clos if (previousStartedPath === undefined) delete process.env.AGENT_BUNDLE_HOOK_SIMULATION_STARTED_PATH; else process.env.AGENT_BUNDLE_HOOK_SIMULATION_STARTED_PATH = previousStartedPath; await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(consumer, { force: true, recursive: true }), + removeTree(root), + removeTree(consumer), ]); } }, 10_000); @@ -1167,7 +1168,7 @@ it('compiles each native hook through a virtual Rslib entry without sibling chun 'stop.ts', ]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1232,11 +1233,11 @@ it('applies the operator .env layer of the installed pack before a hook handler const disabled = await runNodeScript({ args: [wrapper], env: { AGENT_BUNDLE_ENV_FILE: 'none' }, input: JSON.stringify(event) }); expect(context(disabled)).toBe('unset=unset:unset'); } finally { - await rm(elsewhere, { force: true, recursive: true }); + await removeTree(elsewhere); } } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1318,7 +1319,7 @@ it('runs the embedded Codex and Claude native codecs through their published wra }); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 15_000); @@ -1387,7 +1388,7 @@ it('runs the Cursor workspace/open lifecycle starter through a generated wrapper stdout: '', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 15_000); @@ -1469,7 +1470,7 @@ it('round-trips Claude and Codex subagent fields through published wrappers', as }); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 15_000); @@ -1639,7 +1640,7 @@ it('round-trips the documented Cursor subagent envelopes through published Curso }); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1701,7 +1702,7 @@ it('rejects malformed event-specific native input before calling generated Codex }); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 15_000); @@ -1753,7 +1754,7 @@ it('rejects canonical reason combinations whose selected native hook cannot repr }); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 15_000); @@ -1813,7 +1814,7 @@ it('rejects malformed native hook input, exports, and handler results concisely' stdout: '', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 15_000); @@ -1918,7 +1919,7 @@ it('plans deterministic Codex and Claude hook configurations from the same model { relativePath: 'hooks/stop-stop-bb2d7935.codex.mjs' }, ]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2013,6 +2014,6 @@ it('normalizes a mixed hook fixture and reports malformed hook declarations', as 'AB4202', ]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/host-adapters.native.test.ts b/packages/agent-bundle/tests/host-adapters.native.test.ts index 9c9945c78..3439e2ce6 100644 --- a/packages/agent-bundle/tests/host-adapters.native.test.ts +++ b/packages/agent-bundle/tests/host-adapters.native.test.ts @@ -1,5 +1,5 @@ import { spawn } from 'node:child_process'; -import { access, chmod, mkdtemp, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises'; +import { access, chmod, mkdtemp, mkdir, readFile, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -15,6 +15,7 @@ import { type ClaudePluginValidationReport, } from '../src/host-contracts/claude-plugin-validation.ts'; import { parseCliVersion } from '../../../scripts/host-cli-pins.mjs'; +import { removeTree } from './support/remove-tree.ts'; const nativeIt = process.env.AGENT_BUNDLE_NATIVE_HOST_CONTRACTS === '1' ? it : it.skip; @@ -327,7 +328,7 @@ nativeIt('registers an emitted Codex plugin carrying authored package metadata', expect(listedDocument.installed[0]).not.toHaveProperty('license'); expect(listedDocument.installed[0]).not.toHaveProperty('repository'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -401,7 +402,7 @@ nativeIt('installs and lists an emitted Codex plugin carrying the complete inter expect(listed.code, listed.stderr).toBe(0); expect(`${listed.stdout}${listed.stderr}`).toContain('review-tools'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -520,7 +521,7 @@ nativeIt('pins the Codex plugin and marketplace CLI JSON contracts, cache layout expect(cleared).not.toContain(pluginId); expect(cleared).not.toContain('[marketplaces.review-tools-marketplace]'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -568,7 +569,7 @@ nativeIt('pins Claude plugin and marketplace lifecycle command help', async () = expect(validateHelp.output).toContain('--json Output the validation report as JSON (same exit codes)'); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -620,7 +621,7 @@ nativeIt('adds, lists, and removes a marketplace only in an isolated config dire expect(listedAfterRemoval.code, listedAfterRemoval.output).toBe(0); expect(listedAfterRemoval.output).not.toContain(marketplaceName); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -632,7 +633,7 @@ nativeIt('accepts the emitted Claude marketplace under strict native validation' const validation = await runClaudeValidation(root, join(root, '.claude-plugin', 'marketplace.json')); expect(validation.code, validation.output).toBe(0); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -668,7 +669,7 @@ nativeIt('records that strict validation accepts package metadata without runnin expect(validation.output).not.toContain('package-lock.json'); await expect(access(join(root, 'node_modules'))).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -712,8 +713,8 @@ nativeIt('records that plugin-mode validation warns about a symlinked skill entr expect(marketplaceRun.output).not.toMatch(/is a symlink and was not read/u); } finally { await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(externalSkill, { force: true, recursive: true }), + removeTree(root), + removeTree(externalSkill), ]); } }); @@ -760,7 +761,7 @@ nativeIt('accepts the enriched Claude marketplace under strict native validation expect(validation.code, validation.output).toBe(0); expect(validation.output).toContain('Validation passed'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -859,7 +860,7 @@ nativeIt('accepts every documented Claude marketplace plugin source form', async expect(validation.code, `${label}: ${validation.output}`).toBe(0); expect(validation.output).toContain('Validation passed'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } } }); @@ -915,7 +916,7 @@ nativeIt('records which source constraints strict Claude marketplace validation expect(validation.code, `${label}: ${validation.output}`).toBe(expectedCode); expect(validation.output).toContain(output); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } } }); @@ -935,7 +936,7 @@ nativeIt('records whether strict native validation enforces marketplace allowlis expect(validation.output).toContain('Validation passed'); expect(validation.output).not.toContain('allowCrossMarketplaceDependenciesOn'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -962,7 +963,7 @@ nativeIt('records that strict native validation rejects archive authentication o expect(validation.output).toContain('only apply to "archive" sources'); expect(validation.output).toContain('--strict treats warnings as errors'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -984,7 +985,7 @@ nativeIt('accepts an emitted Claude artifact whose plugin root carries settings. const plugin = await validateClaudePluginRoot(root); expect(plugin.report.status, plugin.output).toBe('passed'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1008,7 +1009,7 @@ nativeIt('records that strict native validation never inspects plugin settings.j expect(validation.report.status, validation.output).toBe('passed'); expect(validation.output).not.toContain('settings.json'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1070,7 +1071,7 @@ nativeIt('records strict native validation behavior for documented and security- } } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1103,7 +1104,7 @@ nativeIt('accepts emitted Claude experimental themes and monitors under strict n expect(validation.report.status, validation.output).toBe('passed'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1136,7 +1137,7 @@ nativeIt('records whether strict native validation inspects monitors/monitors.js severity: 'error', })]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1156,7 +1157,7 @@ nativeIt('records whether strict native validation inspects plugin theme content expect(validation.report.status, validation.output).toBe('passed'); expect(validation.output).not.toContain('invalid.json'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1174,7 +1175,7 @@ nativeIt('records that strict native validation rejects the deprecated top-level expect(validation.report.status, validation.output).not.toBe('passed'); expect(validation.output).toContain('monitors'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1210,7 +1211,7 @@ nativeIt('accepts an emitted Claude plugin with bin under strict native validati ); expect(validation.code, validation.output).toBe(0); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1266,7 +1267,7 @@ nativeIt('accepts emitted Claude workflows and output styles under strict native expect(validation.report.status, validation.output).toBe('passed'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1283,7 +1284,7 @@ nativeIt('records whether strict native validation inspects output-style frontma expect(validation.report.status, validation.output).toBe('passed'); expect(validation.output).not.toContain('missing-frontmatter.md'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1348,7 +1349,7 @@ nativeIt('accepts emitted Claude userConfig under strict native validation', asy expect(result.code, result.output).toBe(0); expect(result.output).toContain('Validation passed'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1362,7 +1363,7 @@ nativeIt('accepts emitted Claude channels bound to a plugin MCP server under str expect(validation.report.status, validation.output).toBe('passed'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1382,7 +1383,7 @@ nativeIt('records whether strict native validation catches a dangling Claude cha expect(validation.report.status, validation.output).toBe('passed'); expect(validation.output).not.toContain('missing'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1411,7 +1412,7 @@ nativeIt('accepts emitted Claude plugin dependencies under strict native validat })]); expect((await validateClaudePluginRoot(root, { strict: true })).report.status).toBe('failed'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1436,7 +1437,7 @@ nativeIt('accepts emitted Claude manifest metadata fields under strict native va const validation = await validateClaudePluginRoot(root); expect(validation.report.status, validation.output).toBe('passed'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1459,6 +1460,6 @@ nativeIt('accepts a custom flat command path without a default commands director const validation = await validateClaudePluginRoot(root); expect(validation.report.status, validation.output).toBe('passed'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/host-adapters.test.ts b/packages/agent-bundle/tests/host-adapters.test.ts index ab2158710..0f7e0e2f4 100644 --- a/packages/agent-bundle/tests/host-adapters.test.ts +++ b/packages/agent-bundle/tests/host-adapters.test.ts @@ -1,4 +1,4 @@ -import { chmod, mkdtemp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { chmod, mkdtemp, mkdir, readFile, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -15,6 +15,7 @@ import type { TargetArtifactEntry } from '../src/adapters/types.ts'; import { emitPlanEntries } from '../src/build/emit.ts'; import { build } from './support/build.ts'; import { pathTokens, pluginRootEnvAnchor, type NormalizedPlugin } from '../src/core/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const installFormats = addFormats as unknown as (target: Ajv2020) => void; @@ -1946,7 +1947,7 @@ it('preserves the executable mode when emitting a Claude bin copy entry', async expect((await stat(join(output, 'bin', 'review-tool'))).mode & 0o777).toBe(0o751); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -3119,6 +3120,6 @@ it('filters host components and builds portable, Codex, and Claude target roots' 'skills/review/SKILL.md', ])); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/host-cli-pins.test.ts b/packages/agent-bundle/tests/host-cli-pins.test.ts index e12181dc9..7e76b5613 100644 --- a/packages/agent-bundle/tests/host-cli-pins.test.ts +++ b/packages/agent-bundle/tests/host-cli-pins.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { mkdtemp, readFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { delimiter, join } from 'node:path'; @@ -19,6 +19,7 @@ import { } from '../../../scripts/host-cli-pins.mjs'; import claudeCapabilities from '../src/adapters/capabilities/claude-2.1.260.json' with { type: 'json' }; import codexCapabilities from '../src/adapters/capabilities/codex-0.147.0.json' with { type: 'json' }; +import { removeTree } from './support/remove-tree.ts'; const pins: HostCliPins = Object.freeze({ claude: Object.freeze({ @@ -216,6 +217,6 @@ it('prints the pins and a cache key to stdout and GITHUB_OUTPUT', async () => { expect(await readFile(outputPath, 'utf8')).toBe(`${expected.join('\n')}\n`); } finally { process.stdout.write = originalWrite; - await rm(fixtureRoot, { force: true, recursive: true }); + await removeTree(fixtureRoot); } }); diff --git a/packages/agent-bundle/tests/host-discovery-dev-server.test.ts b/packages/agent-bundle/tests/host-discovery-dev-server.test.ts index 38a373b83..cf5b1d4cd 100644 --- a/packages/agent-bundle/tests/host-discovery-dev-server.test.ts +++ b/packages/agent-bundle/tests/host-discovery-dev-server.test.ts @@ -1,4 +1,4 @@ -import { mkdir, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, symlink, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { expect, it } from '@rstest/core'; @@ -9,6 +9,7 @@ import { startDevServer } from '../src/dev/workbench-server.ts'; import type { DoctorCommandRunner, DoctorCommandResult } from '../src/install/doctor.ts'; import { createProjectFixture } from './helpers/project-fixture.ts'; import { agentBundleNodeModules } from './helpers/workspace-paths.ts'; +import { removeTree } from './support/remove-tree.ts'; const successfulCommand = (stdout: string): DoctorCommandResult => Object.freeze({ exitCode: 0, @@ -147,6 +148,6 @@ it.each([ }); } finally { await server?.close().catch(() => undefined); - await rm(project.root, { force: true, maxRetries: 5, recursive: true, retryDelay: 50 }); + await removeTree(project.root); } }); diff --git a/packages/agent-bundle/tests/host-discovery-service.test.ts b/packages/agent-bundle/tests/host-discovery-service.test.ts index 461db55d2..7d1e307b3 100644 --- a/packages/agent-bundle/tests/host-discovery-service.test.ts +++ b/packages/agent-bundle/tests/host-discovery-service.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, rm, unlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, unlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -13,6 +13,7 @@ import type { DoctorOptions, DoctorReport, } from '../src/install/doctor.ts'; +import { removeTree } from './support/remove-tree.ts'; const diagnostic = Object.freeze({ code: 'AB7300', @@ -211,7 +212,7 @@ it('enumerates sorted modern MCP servers from a valid bundle manifest', async () expect(Object.isFrozen(report.hosts[0]?.bundle?.mcpServers)).toBe(true); expect(Object.isFrozen(report.hosts[0]?.bundle?.mcpServers?.[0])).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -239,7 +240,7 @@ it('distinguishes empty MCP manifests from manifests that could not be enumerate await unlink(join(root, '.mcp.json')); expect((await service.discover()).hosts[0]?.bundle).not.toHaveProperty('mcpServers'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/host-install-proof.test.ts b/packages/agent-bundle/tests/host-install-proof.test.ts index 8e721f982..1d3d24030 100644 --- a/packages/agent-bundle/tests/host-install-proof.test.ts +++ b/packages/agent-bundle/tests/host-install-proof.test.ts @@ -1,5 +1,5 @@ import { spawnSync } from 'node:child_process'; -import { cp, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { cp, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -26,6 +26,7 @@ import { HOST_INSTALL_PROOF_LEVEL, proofLevelLabel, } from '../src/test/manifest.ts'; +import { removeTree } from './support/remove-tree.ts'; const proofLabel = proofLevelLabel(HOST_INSTALL_PROOF_LEVEL); const simulatedProofLabel = @@ -219,7 +220,7 @@ it('does not execute a tampered installed MCP command after static integrity che expect((error as AgentTestError).message).toContain('version-digests'); await expect(readFile(marker, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(markerRoot, { force: true, recursive: true }); + await removeTree(markerRoot); } }, 180_000); @@ -257,7 +258,7 @@ it('accepts an installed artifact whose manifest declares no resource components expect(report.checks.resources).toEqual({ status: 'passed' }); } finally { - await rm(cloneParent, { force: true, recursive: true }); + await removeTree(cloneParent); } }, 180_000); diff --git a/packages/agent-bundle/tests/host-mcp-proxy.test.ts b/packages/agent-bundle/tests/host-mcp-proxy.test.ts index 108d3e3c3..83062a44d 100644 --- a/packages/agent-bundle/tests/host-mcp-proxy.test.ts +++ b/packages/agent-bundle/tests/host-mcp-proxy.test.ts @@ -11,6 +11,7 @@ import { startDevServer } from '../src/dev/workbench-server.ts'; import { createProjectFixture, removeProjectFixture } from './helpers/project-fixture.ts'; import { agentBundleNodeModules } from './helpers/workspace-paths.ts'; import { replaceWatchedSource } from './support/watched-files.ts'; +import { removeTree } from './support/remove-tree.ts'; const cliEntry = join(import.meta.dirname, '..', 'bin', 'agent-bundle.js'); @@ -179,7 +180,7 @@ it('fails the connected host session closed with AB8024 when its active epoch is const artifact = server.status().artifact; if (artifact.state !== 'active') throw new Error('Expected an active epoch.'); const epochId = artifact.activeEpoch.id; - await rm(join(project.root, '.agent-bundle', 'epochs', epochId), { force: true, recursive: true }); + await removeTree(join(project.root, '.agent-bundle', 'epochs', epochId)); await rm(join(project.root, '.agent-bundle', 'epochs', '.metadata', `${epochId}.json`), { force: true }); await expect(client.listTools()).rejects.toMatchObject({ diff --git a/packages/agent-bundle/tests/inspect-artifact.test.ts b/packages/agent-bundle/tests/inspect-artifact.test.ts index afd695fc6..9616df4f5 100644 --- a/packages/agent-bundle/tests/inspect-artifact.test.ts +++ b/packages/agent-bundle/tests/inspect-artifact.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -12,6 +12,7 @@ import { import { runCli as runSourceCli } from '../src/cli.ts'; import { captureCliTerminal } from './support/cli-terminal.ts'; import { writeInstallFixtureManifest } from './support/install-fixture.ts'; +import { removeTree } from './support/remove-tree.ts'; const runSourceCliWithOutput = async ( args: string[], @@ -83,7 +84,7 @@ it('inspect --artifact --json projects the fixture manifest and Workbench applic expect(human.stdout).toContain('Projections: cursor'); expect(human.stdout).toContain('Payloads: native (cursor: ffmpeg); tools (cursor: sharp)'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -95,7 +96,7 @@ it('inspect --artifact on a directory with no manifest fails AB7001 and exits 1' expect(result.stdout).toBe(''); expect(JSON.parse(result.stderr)).toMatchObject([{ code: 'AB7001', severity: 'error' }]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/inspect-bundler.test.ts b/packages/agent-bundle/tests/inspect-bundler.test.ts index 8617f82ac..4aea78df7 100644 --- a/packages/agent-bundle/tests/inspect-bundler.test.ts +++ b/packages/agent-bundle/tests/inspect-bundler.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readdir, realpath, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readdir, realpath, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -13,11 +13,12 @@ import { stableJson } from '../src/core/digest.ts'; import type { NormalizedHook, NormalizedPlugin } from '../src/core/types.ts'; import type { CompiledEventHandler } from '../src/routes/types.ts'; import { workspaceNodeModules } from './helpers/workspace-paths.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); /** diff --git a/packages/agent-bundle/tests/inspect-state.test.ts b/packages/agent-bundle/tests/inspect-state.test.ts index 1a41aec4b..f9dc5f729 100644 --- a/packages/agent-bundle/tests/inspect-state.test.ts +++ b/packages/agent-bundle/tests/inspect-state.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -8,6 +8,7 @@ import { expect, it } from '@rstest/core'; import { runCli } from '../src/cli.ts'; import { agentStateDefaultBudgets } from '../src/core/state-inspection.ts'; import { captureCliTerminal } from './support/cli-terminal.ts'; +import { removeTree } from './support/remove-tree.ts'; it('keeps static inspection defaults aligned with the runtime package', () => { expect(agentStateDefaultBudgets).toEqual({ @@ -156,7 +157,7 @@ it('inspects volatile and workspace-durable state without inventing runtime path }); expect(JSON.parse(dynamic.stdout).selected.state.budgets).not.toHaveProperty('resolved'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -199,7 +200,7 @@ it('reports the declared notice retention policy and rejects a malformed one as expect(malformed.code).not.toBe(0); expect(`${malformed.stdout}${malformed.stderr}`).toContain('AB4833'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -215,7 +216,7 @@ it('reports an invalid built manifest on inspect without treating it as missing' const human = await inspectCli(root, []); expect(human.stdout).toContain('Built manifest: invalid'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -254,6 +255,6 @@ it('reports stateless inspection and rejects competing state focuses', async () expect(ambiguous.code).toBe(1); expect(JSON.parse(ambiguous.stderr)).toMatchObject([{ code: 'AB5000', severity: 'error' }]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/install-cli.test.ts b/packages/agent-bundle/tests/install-cli.test.ts index 9e8659a7a..9fe9fb19a 100644 --- a/packages/agent-bundle/tests/install-cli.test.ts +++ b/packages/agent-bundle/tests/install-cli.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, rm } from 'node:fs/promises'; +import { mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -7,6 +7,7 @@ import { describe, expect, it } from '@rstest/core'; import { registerLifecycleCommands, type LifecycleApi } from '../src/install/commands.ts'; import { runInstallCli } from '../src/install/index.ts'; +import { removeTree } from './support/remove-tree.ts'; const capture = () => { const stdout: string[] = []; @@ -142,7 +143,7 @@ describe('runInstallCli', () => { expect(diagnostic).toMatchObject({ severity: 'error' }); expect(diagnostic.code).toMatch(/^AB\d{4}$/u); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); }); diff --git a/packages/agent-bundle/tests/install-surface.test.ts b/packages/agent-bundle/tests/install-surface.test.ts index ee4533bd7..b1a0f4561 100644 --- a/packages/agent-bundle/tests/install-surface.test.ts +++ b/packages/agent-bundle/tests/install-surface.test.ts @@ -20,6 +20,7 @@ import { treeInventory, } from '../src/install/receipt.ts'; import { diffTreeSnapshots, snapshotTree } from './support/tree-snapshot.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); @@ -641,7 +642,7 @@ it('emitted install.mjs expands Agent Plugins placeholders for the Cursor copy o const outside = join(root, 'outside-home'); await mkdir(join(outside, 'plugin-data', 'install-fixture'), { recursive: true }); await writeFile(join(outside, 'plugin-data', 'install-fixture', 'cache.sqlite'), 'elsewhere\n'); - await rm(join(home, '.cursor', 'agent-bundle'), { force: true, recursive: true }); + await removeTree(join(home, '.cursor', 'agent-bundle')); await symlink(outside, join(home, '.cursor', 'agent-bundle')); const linkedParent = await run(installer, ['--uninstall', '--purge-data', '--confirm-purge'], home); expect(linkedParent.code).toBe(1); @@ -654,8 +655,8 @@ it('emitted install.mjs expands Agent Plugins placeholders for the Cursor copy o expect(linkedChild.code).toBe(1); expect(linkedChild.stderr).toContain('Refusing unsupported filesystem entry'); expect(await readFile(join(outside, 'plugin-data', 'install-fixture', 'cache.sqlite'), 'utf8')).toBe('elsewhere\n'); - await rm(join(home, '.cursor', 'agent-bundle'), { force: true, recursive: true }); - await rm(outside, { force: true, recursive: true }); + await removeTree(join(home, '.cursor', 'agent-bundle')); + await removeTree(outside); expect(await run(installer, ['--uninstall'], home)).toMatchObject({ code: 0, stderr: '' }); // Fresh install, nothing written to PLUGIN_DATA: the default uninstall prunes it (and the empty agent-bundle parents). await writeFile(join(bundle, 'mcp.json'), mcpText); @@ -669,7 +670,7 @@ it('emitted install.mjs expands Agent Plugins placeholders for the Cursor copy o expect(await stat(join(home, '.cursor', 'agent-bundle')).catch(() => undefined)).toBeUndefined(); // A skills-only Agent Plugins pack (no stdio server) is copied byte-identically and records no expansion. - await rm(destination, { force: true, recursive: true }); + await removeTree(destination); await writeFile(join(bundle, 'mcp.json'), `${JSON.stringify({ $schema: agentPluginsMcp.$schema, mcpServers: { remote: agentPluginsMcp.mcpServers.remote }, @@ -681,7 +682,7 @@ it('emitted install.mjs expands Agent Plugins placeholders for the Cursor copy o expect((await readInstallReceipt(destination))?.contentHash).toBe((await treeInventory(bundle)).hash); // A Cursor Plugin bundle beside a root plugin.json is never rewritten: the expansion is for Agent Plugins packs only. - await rm(destination, { force: true, recursive: true }); + await removeTree(destination); await writeFile(join(bundle, 'mcp.json'), mcpText); await mkdir(join(bundle, '.cursor-plugin'), { recursive: true }); await writeFile(join(bundle, '.cursor-plugin', 'plugin.json'), JSON.stringify({ name: 'install-fixture', version: '1.2.3' })); @@ -690,7 +691,7 @@ it('emitted install.mjs expands Agent Plugins placeholders for the Cursor copy o expect(cursorPlugin.stdout).not.toContain('Expanded Agent Plugins'); expect(await readFile(join(destination, 'mcp.json'), 'utf8')).toBe(mcpText); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 60_000); @@ -748,7 +749,7 @@ it('reads plugin identity and MCP launch paths from the artifact manifest before await expect(readFile(join(destination, 'mcp.json'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); await expect(readFile(join(destination, 'package.json'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -873,7 +874,7 @@ it('emitted install.mjs mirrors the core replace policy: no-op, owned-only repla const aliasDirectory = await run(installer, [], home); expect(aliasDirectory.code).toBe(1); expect(aliasDirectory.stderr).toContain('Refusing unsupported filesystem entry ".Agent-Bundle-Install.json/payload"'); - await rm(join(bundle, '.Agent-Bundle-Install.json'), { recursive: true }); + await removeTree(join(bundle, '.Agent-Bundle-Install.json')); } // Empty directories are not plugin content: never hashed, installed, or owned. @@ -882,7 +883,7 @@ it('emitted install.mjs mirrors the core replace policy: no-op, owned-only repla expect(first).toMatchObject({ code: 0, stderr: '' }); expect(first.stdout).toContain('Installed install-fixture@1.2.3'); expect((await readdir(destination)).sort()).toEqual([installReceiptFile, '.cursor-plugin', 'INSTALL.md', 'install.mjs', 'payload.txt', 'removed-later.txt']); - await rm(join(bundle, 'empty'), { recursive: true }); + await removeTree(join(bundle, 'empty')); const firstArtifact = await treeInventory(bundle); // The emitted receipt is byte-compatible with the core reader, lifecycle fields included (#101). expect(await readInstallReceipt(destination)).toMatchObject({ @@ -939,7 +940,7 @@ it('emitted install.mjs mirrors the core replace policy: no-op, owned-only repla // Byte-identical legacy copy (receipt-less copies hash as a full tree, so runtime state is cleared // first): plain rerun is a no-op, --replace adopts it by writing the receipt. await rm(join(destination, installReceiptFile)); - await rm(join(destination, 'state'), { force: true, recursive: true }); + await removeTree(join(destination, 'state')); const identicalLegacy = await run(installer, [], home); expect(identicalLegacy).toMatchObject({ code: 0, stderr: '' }); expect(identicalLegacy.stdout).toContain('Already installed install-fixture@1.2.3'); @@ -960,7 +961,7 @@ it('emitted install.mjs mirrors the core replace policy: no-op, owned-only repla expect(restructured).toMatchObject({ code: 0, stderr: '' }); expect(restructured.stdout).toContain('Replaced install-fixture@1.2.3'); expect(await readFile(join(destination, 'payload.txt', 'nested.md'), 'utf8')).toBe('# nested\n'); - await rm(join(bundle, 'payload.txt'), { recursive: true }); + await removeTree(join(bundle, 'payload.txt')); await writeFile(join(bundle, 'payload.txt'), 'rebuilt\n'); const flattened = await run(installer, [], home); expect(flattened).toMatchObject({ code: 0, stderr: '' }); @@ -977,13 +978,13 @@ it('emitted install.mjs mirrors the core replace policy: no-op, owned-only repla await writeFile(join(bundle, 'skills', 'new', 'SKILL.md'), '# new\n'); expect((await run(installer, [], home)).stdout).toContain('Replaced install-fixture@1.2.3'); expect((await readInstallReceipt(destination))?.directories).toEqual(['skills', 'skills/new']); - await rm(join(bundle, 'operator-dir'), { recursive: true }); - await rm(join(bundle, 'skills'), { recursive: true }); + await removeTree(join(bundle, 'operator-dir')); + await removeTree(join(bundle, 'skills')); expect((await run(installer, [], home)).stdout).toContain('Replaced install-fixture@1.2.3'); expect(await readdir(join(destination, 'operator-dir'))).toEqual([]); await expect(readdir(join(destination, 'skills'))).rejects.toMatchObject({ code: 'ENOENT' }); expect((await readInstallReceipt(destination))?.directories).toEqual([]); - await rm(join(destination, 'operator-dir'), { recursive: true }); + await removeTree(join(destination, 'operator-dir')); // A receipt whose inventory drifted is refreshed even when the owned bytes hash equal. await writeFile(join(bundle, 'transient.txt'), 'transient\n'); @@ -1020,7 +1021,7 @@ it('emitted install.mjs mirrors the core replace policy: no-op, owned-only repla expect(adoptedOverState.stdout).toContain('Replaced install-fixture@1.2.3'); expect(await readFile(join(destination, 'state', 'plugin.sqlite'), 'utf8')).toBe('durable\n'); expect((await readInstallReceipt(destination))?.files.some((file) => file.startsWith('state/'))).toBe(false); - await rm(join(destination, 'state'), { recursive: true }); + await removeTree(join(destination, 'state')); await writeFile(join(bundle, 'payload.txt'), 'rebuilt\n'); await run(installer, [], home); @@ -1049,7 +1050,7 @@ it('emitted install.mjs mirrors the core replace policy: no-op, owned-only repla expect(symlinked.stderr).toContain('Refusing unsupported filesystem entry "skills"'); expect(await readdir(elsewhere)).toEqual([]); await rm(join(destination, 'skills')); - await rm(join(bundle, 'skills'), { recursive: true }); + await removeTree(join(bundle, 'skills')); // Legacy pre-receipt copy with drift: refused with a hash comparison until --replace adopts it. await rm(join(destination, installReceiptFile)); @@ -1068,7 +1069,7 @@ it('emitted install.mjs mirrors the core replace policy: no-op, owned-only repla expect(await readInstallReceipt(destination)).toMatchObject({ plugin: 'install-fixture' }); // Foreign directory under the plugin name: refused even with --replace. - await rm(destination, { force: true, recursive: true }); + await removeTree(destination); await mkdir(join(destination, '.cursor-plugin'), { recursive: true }); await writeFile(join(destination, '.cursor-plugin', 'plugin.json'), JSON.stringify({ name: 'install-fixture', version: '1.2.3' })); await writeFile(join(destination, 'payload.txt'), 'someone else\n'); @@ -1078,7 +1079,7 @@ it('emitted install.mjs mirrors the core replace policy: no-op, owned-only repla expect(foreign.stderr).toContain('same version, different content'); expect(await readFile(join(destination, 'payload.txt'), 'utf8')).toBe('someone else\n'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 60_000); @@ -1127,7 +1128,7 @@ it('emitted install.mjs refuses a foreign destination that lacks artifact-manife expect(broken.stderr).not.toContain('lstat'); expect(await readFile(foreignReceipt, 'utf8')).toBe('{ "installer": "plugin-library" }\n'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1175,7 +1176,7 @@ it('emitted install.mjs reruns a marketplace stage with unlisted files as alread expect(await readFile(join(stagedPlugin, 'payload.txt'), 'utf8')).toBe('payload\n'); await expect(readFile(join(stagedPlugin, 'extra.txt'))).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1211,7 +1212,7 @@ it('emitted install.mjs --uninstall --force removes present files from a pre-rec await expect(readFile(join(destination, 'operator.txt'))).rejects.toMatchObject({ code: 'ENOENT' }); expect(await readFile(join(destination, 'state', 'plugin.sqlite'), 'utf8')).toBe('durable\n'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1281,7 +1282,7 @@ it('emitted install.mjs marks new explicit state roots and retains pre-existing expect(retained.stdout).toContain(`Retained ${sharedRoot} (pre-existing)`); expect(await readFile(sentinel, 'utf8')).toBe('keep\n'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 60_000); @@ -1384,7 +1385,7 @@ it('emitted install.mjs never derives legacy purge ownership from the current en await expect(readFile(join(originalStateRoot, 'state.sqlite'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); expect(await readFile(currentSentinel, 'utf8')).toBe('unrelated\n'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 60_000); @@ -1471,7 +1472,7 @@ it('emitted install.mjs --uninstall mirrors the core lifecycle: plan, receipt-ow // The remnant receipt owns nothing and remembers the host directories the install created. expect(await readInstallReceipt(destination)).toMatchObject({ files: [], hostDirectories: ['plugins', 'plugins/local'], registrations: [] }); await rm(join(destination, 'notes.md')); - await rm(join(destination, 'scratch'), { recursive: true }); + await removeTree(join(destination, 'scratch')); // Reinstalling around the preserved state is an install, not a foreign-directory refusal. const reinstalled = await run(installer, [], home); expect(reinstalled).toMatchObject({ code: 0, stderr: '' }); @@ -1490,7 +1491,7 @@ it('emitted install.mjs --uninstall mirrors the core lifecycle: plan, receipt-ow await writeFile(join(destination, 'state', 'plugin.sqlite'), 'durable\n'); await run(installer, ['--uninstall'], home); expect((await run(installer, ['--uninstall'], home)).stdout).toContain('Not installed install-fixture@1.2.3'); - await rm(join(destination, 'state'), { recursive: true }); + await removeTree(join(destination, 'state')); const emptyRemnant = await run(installer, ['--uninstall'], home); expect(emptyRemnant).toMatchObject({ code: 0, stderr: '' }); expect(emptyRemnant.stdout).toContain('Uninstalled install-fixture@1.2.3'); @@ -1534,7 +1535,7 @@ it('emitted install.mjs --uninstall mirrors the core lifecycle: plan, receipt-ow expect(forcedLegacy.stdout).toContain('Receipt: forced-legacy'); // The legacy inventory owns no host directories: plugins/local stays (it was not proven ours). expect(await readdir(join(cursorRoot, 'plugins', 'local'))).toEqual([]); - await rm(join(cursorRoot, 'plugins'), { recursive: true }); + await removeTree(join(cursorRoot, 'plugins')); // A format/1 receipt is consumed as migrated. await run(installer, [], home); @@ -1545,7 +1546,7 @@ it('emitted install.mjs --uninstall mirrors the core lifecycle: plan, receipt-ow expect(migrated).toMatchObject({ code: 0, stderr: '' }); expect(migrated.stdout).toContain('Receipt: migrated'); // The migrated receipt carried no host directories, so plugins/ stays behind; that is the honest downgrade. - await rm(join(cursorRoot, 'plugins'), { recursive: true }); + await removeTree(join(cursorRoot, 'plugins')); expect(diffTreeSnapshots(before, await snapshotTree(home))).toEqual({ added: [], changed: [], removed: [] }); // Foreign directory (another plugin's receipt, or no receipt and no install surface): refused even with --force. @@ -1555,14 +1556,14 @@ it('emitted install.mjs --uninstall mirrors the core lifecycle: plan, receipt-ow const otherPlugin = await run(installer, ['--uninstall', '--force'], home); expect(otherPlugin.code).toBe(1); expect(otherPlugin.stderr).toContain('names plugin "someone-else"'); - await rm(destination, { force: true, recursive: true }); + await removeTree(destination); await mkdir(destination); await writeFile(join(destination, 'payload.txt'), 'someone else\n'); const foreign = await run(installer, ['--uninstall', '--force'], home); expect(foreign.code).toBe(1); expect(foreign.stderr).toContain('Refusing to uninstall foreign directory'); expect(await readFile(join(destination, 'payload.txt'), 'utf8')).toBe('someone else\n'); - await rm(join(cursorRoot, 'plugins'), { recursive: true }); + await removeTree(join(cursorRoot, 'plugins')); // Marketplace mode: staging writes a store receipt; --uninstall --mode marketplace removes the repository and receipt. const staged = await run(installer, ['--mode', 'marketplace'], home); @@ -1617,6 +1618,6 @@ it('emitted install.mjs --uninstall mirrors the core lifecycle: plan, receipt-ow expect(diffTreeSnapshots(before, await snapshotTree(home))).toEqual({ added: [], changed: [], removed: [] }); expect((await run(installer, ['--uninstall', '--mode', 'marketplace'], home)).stdout).toContain('Not installed install-fixture@1.2.3 for cursor (marketplace mode)'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 120_000); diff --git a/packages/agent-bundle/tests/install.test.ts b/packages/agent-bundle/tests/install.test.ts index e48ef7e34..25cf28b0b 100644 --- a/packages/agent-bundle/tests/install.test.ts +++ b/packages/agent-bundle/tests/install.test.ts @@ -35,6 +35,7 @@ import { toPosixPath } from '../src/core/paths.ts'; import { runCli } from '../src/cli.ts'; import { captureCliTerminal } from './support/cli-terminal.ts'; import { writeInstallFixtureManifest } from './support/install-fixture.ts'; +import { removeTree } from './support/remove-tree.ts'; interface CommandCall { readonly args: readonly string[]; @@ -350,7 +351,7 @@ it.each([ // (plugin first, then the marketplace this run created), so nothing stays registered without a receipt. // Inject the write failure after host verbs: chmod on the store is a no-op // on Windows and as root, and occupying the path breaks the pre-write read. - await rm(join(hostRoot, 'agent-bundle'), { force: true, recursive: true }); + await removeTree(join(hostRoot, 'agent-bundle')); const writeReceipt = rs.spyOn(installReceipt, 'writeStoredInstallReceipt') .mockRejectedValueOnce(new Error('receipt write failed')); const unwritable: CommandCall[] = []; @@ -378,7 +379,7 @@ it.each([ writeReceipt.mockRestore(); } } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -475,7 +476,7 @@ it('replaces a stale same-version Claude install through uninstall + install and expect(malformed.calls).toHaveLength(1); } } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -542,7 +543,7 @@ it('fails a Claude install (AB7006) when plugin list --json reports load errors await expect(installBundle({ ...isolated(fixture), commandRunner: healthy.runner, from: fixture.from, host: 'claude', scope: 'user' })) .resolves.toMatchObject({ state: 'already-installed' }); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -688,7 +689,7 @@ it('honours --replace for Codex through add-only and fails closed without a usab expect(unusable.calls).toHaveLength(1); } } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -734,7 +735,7 @@ it('preserves Codex nested MCP overrides and concurrent config edits across enab expect(config).toContain('model = "changed-by-add"'); expect(config).not.toContain('model = "keep-me"'); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -777,7 +778,7 @@ it('keeps Codex plugin settings when add fails during enabled replace', async () expect(config).toContain(`${codexPluginNestedMcp}\nenabled = false`); expect(config).toContain('model = "changed-by-add"'); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -825,7 +826,7 @@ it('does not plugin-remove after a failed Codex replace receipt write', async () expect(config).not.toBe(prior); } finally { writeReceipt.mockRestore(); - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -888,7 +889,7 @@ it('fails Codex replace closed for disabled or unknown enablement before any mut expect(unreadable.calls.map((call) => call.args.join(' '))).toEqual(['plugin list --json']); expect((await stat(join(codexHome, 'config.toml'))).isDirectory()).toBe(true); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -917,7 +918,7 @@ it.each(['claude', 'codex', 'cursor'] as const)( }); expect(calls).toEqual([]); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }, ); @@ -935,7 +936,7 @@ it('reads application identity from the manifest instead of the host plugin docu version: '1.2.3', }); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -954,7 +955,7 @@ it('reports a non-canonical artifact manifest as AB7001', async () => { target: 'cursor', }]); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -975,7 +976,7 @@ it('reports a host absent from manifest projections as AB7001', async () => { })], }); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -997,7 +998,7 @@ it('selects the host projection by adapter identity, not by the selected name', })], }); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -1013,7 +1014,7 @@ it('reports a manifest marketplace pointer at a missing document as AB7001', asy })], }); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -1031,7 +1032,7 @@ it('distinguishes an unreadable manifest from an absent one: a directory in its target: 'cursor', }]); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -1060,7 +1061,7 @@ it('fails with a typed diagnostic when the public host CLI is missing', async () target: 'codex', }]); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -1078,7 +1079,7 @@ it('rejects scopes the selected host does not support', async () => { expect(error).toBeInstanceOf(DiagnosticError); expect((error as DiagnosticError).diagnostics).toMatchObject([{ code: 'AB7003', target: 'codex' }]); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -1127,8 +1128,8 @@ it('copies a Cursor bundle into a fake home and is idempotent', async () => { ]); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -1141,7 +1142,7 @@ it('matches manifest inventory to the walk inventory for a built root', async () const indexed = await manifestInventory(fixture.bundleRoot, identity.manifest); expect(indexed).toEqual(walked); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -1160,8 +1161,8 @@ it('refuses a copy whose landed bytes are not the verified inventory', async () .rejects.toThrow(/^--from root changed while it was being copied: copied content [0-9a-f]{12} differs from verified content [0-9a-f]{12}\.$/u); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(staging, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(staging), ]); } }); @@ -1181,8 +1182,8 @@ it('installs only manifest-indexed files and records the installed-copy hash', a expect(installed.contentHash).toBe(receipt?.contentHash); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -1204,8 +1205,8 @@ it('reports manifest-indexed byte drift as AB7001 with the path', async () => { }]); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -1227,8 +1228,8 @@ posixPermissionIt('reports manifest-indexed mode drift as AB7001', async () => { }]); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -1313,7 +1314,7 @@ posixPermissionIt('accepts npm normalization while preserving executable-bit tam await chmod(join(installedRoot, 'executable.mjs'), 0o644); expect((await installedBundleInventory(installedRoot, 'cursor')).hash).not.toBe(current.hash); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1331,8 +1332,8 @@ it('copies and hashes an operator .env beside the artifact', async () => { expect((await readInstallReceipt(destination))?.files).toContain('.env'); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -1396,15 +1397,15 @@ it('replaces a stale same-version receipt-managed Cursor install in place, touch await refreshCursorBundle(fixture); await installBundle({ from: fixture.from, home, host: 'cursor', scope: 'user' }); expect((await readInstallReceipt(destination))?.directories).toEqual(['.cursor-plugin', 'skills', 'skills/new']); - await rm(join(fixture.bundleRoot, 'operator-dir'), { recursive: true }); - await rm(join(fixture.bundleRoot, 'skills'), { recursive: true }); + await removeTree(join(fixture.bundleRoot, 'operator-dir')); + await removeTree(join(fixture.bundleRoot, 'skills')); await refreshCursorBundle(fixture); expect(await installBundle({ from: fixture.from, home, host: 'cursor', scope: 'user' })).toMatchObject({ state: 'replaced' }); expect((await stat(join(destination, 'operator-dir'))).isDirectory()).toBe(true); expect(await readdir(join(destination, 'operator-dir'))).toEqual([]); await expect(access(join(destination, 'skills'))).rejects.toMatchObject({ code: 'ENOENT' }); expect((await readInstallReceipt(destination))?.directories).toEqual(['.cursor-plugin']); - await rm(join(destination, 'operator-dir'), { recursive: true }); + await removeTree(join(destination, 'operator-dir')); await mkdir(join(fixture.bundleRoot, 'skills', 'new'), { recursive: true }); await writeFile(join(fixture.bundleRoot, 'skills', 'new', 'SKILL.md'), '# new\n'); await refreshCursorBundle(fixture); @@ -1425,8 +1426,8 @@ it('replaces a stale same-version receipt-managed Cursor install in place, touch expect(await readInstallReceipt(destination)).toMatchObject({ contentHash: artifact.hash }); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -1485,8 +1486,8 @@ it('requires --replace for a legacy pre-receipt Cursor copy and then adopts it', expect(await readFile(join(destination, 'dropped-by-rebuild.txt'), 'utf8')).toBe('old artifact file\n'); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -1509,8 +1510,8 @@ it('fails closed when Cursor is not detected in the selected home', async () => }]); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -1542,8 +1543,8 @@ it('reports a Cursor home it cannot inspect as AB7004 for the cursor host, like } finally { await chmod(home, 0o755); await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(parent, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(parent), ]); } }); @@ -1579,8 +1580,8 @@ it('removes the staging parent after a failed replacement and re-raises the refu await expect(readFile(join(destination, 'skills', 'new', 'SKILL.md'), 'utf8')).resolves.toBe('# operator-owned\n'); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -1627,7 +1628,7 @@ it('refreshes a receipt whose inventory drifted even when the owned bytes hash e 'agent-bundle.manifest.json', 'payload.txt/nested.md', ]); - await rm(join(fixture.bundleRoot, 'payload.txt'), { recursive: true }); + await removeTree(join(fixture.bundleRoot, 'payload.txt')); await writeFile(join(fixture.bundleRoot, 'payload.txt'), 'flat again\n'); await refreshCursorBundle(fixture); const toFile = await installBundle({ from: fixture.from, home, host: 'cursor', scope: 'user' }); @@ -1654,7 +1655,7 @@ it('refreshes a receipt whose inventory drifted even when the owned bytes hash e expect(replacedBesideState).toMatchObject({ state: 'replaced' }); expect(await readFile(join(destination, 'state', 'plugin.sqlite'), 'utf8')).toBe('durable\n'); expect((await readInstallReceipt(destination))?.files.some((file) => file.startsWith('state/'))).toBe(false); - await rm(join(fixture.bundleRoot, 'state'), { recursive: true }); + await removeTree(join(fixture.bundleRoot, 'state')); // Flipping only the executable bit is a content change: the installed copy must receive it. // Windows stores no Unix execute bits; chmod 0755 is a no-op there. @@ -1692,7 +1693,7 @@ it('refreshes a receipt whose inventory drifted even when the owned bytes hash e expect((emptyCollision as DiagnosticError).diagnostics[0]?.message).toContain('Refusing to overwrite unowned files'); expect((emptyCollision as DiagnosticError).diagnostics[0]?.message).toContain('empty-dir'); await rm(join(fixture.bundleRoot, 'empty-dir')); - await rm(join(destination, 'empty-dir'), { recursive: true }); + await removeTree(join(destination, 'empty-dir')); // An owned directory that also holds an unowned empty subdirectory is a collision, not a restructure. await rm(join(fixture.bundleRoot, 'payload.txt')); @@ -1701,14 +1702,14 @@ it('refreshes a receipt whose inventory drifted even when the owned bytes hash e await refreshCursorBundle(fixture); await installBundle({ from: fixture.from, home, host: 'cursor', scope: 'user' }); await mkdir(join(destination, 'payload.txt', 'scratch')); - await rm(join(fixture.bundleRoot, 'payload.txt'), { recursive: true }); + await removeTree(join(fixture.bundleRoot, 'payload.txt')); await writeFile(join(fixture.bundleRoot, 'payload.txt'), 'flat\n'); await refreshCursorBundle(fixture); const emptyNested = await installBundle({ from: fixture.from, home, host: 'cursor', scope: 'user' }) .catch((failure: unknown) => failure); expect((emptyNested as DiagnosticError).diagnostics[0]?.message).toContain('Refusing to overwrite unowned files'); expect(await readFile(join(destination, 'payload.txt', 'nested.md'), 'utf8')).toBe('# nested\n'); - await rm(join(destination, 'payload.txt', 'scratch'), { recursive: true }); + await removeTree(join(destination, 'payload.txt', 'scratch')); await installBundle({ from: fixture.from, home, host: 'cursor', scope: 'user' }); // A directory that also holds an unowned file is a collision, not a restructure. @@ -1718,7 +1719,7 @@ it('refreshes a receipt whose inventory drifted even when the owned bytes hash e await refreshCursorBundle(fixture); await installBundle({ from: fixture.from, home, host: 'cursor', scope: 'user' }); await writeFile(join(destination, 'payload.txt', 'operator.md'), 'mine\n'); - await rm(join(fixture.bundleRoot, 'payload.txt'), { recursive: true }); + await removeTree(join(fixture.bundleRoot, 'payload.txt')); await writeFile(join(fixture.bundleRoot, 'payload.txt'), 'flat\n'); await refreshCursorBundle(fixture); const collision = await installBundle({ from: fixture.from, home, host: 'cursor', scope: 'user' }) @@ -1727,8 +1728,8 @@ it('refreshes a receipt whose inventory drifted even when the owned bytes hash e expect(await readFile(join(destination, 'payload.txt', 'operator.md'), 'utf8')).toBe('mine\n'); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -1757,7 +1758,7 @@ it('refuses to hash or write through a symlinked directory inside a receipt-mana expect(await readFile(join(destination, 'payload.txt'), 'utf8')).toBe('payload\n'); await rm(join(destination, 'skills')); - await rm(join(fixture.bundleRoot, 'skills'), { recursive: true }); + await removeTree(join(fixture.bundleRoot, 'skills')); await refreshCursorBundle(fixture); // A symlinked receipt is never deletion authority. @@ -1780,13 +1781,13 @@ it('refuses to hash or write through a symlinked directory inside a receipt-mana `Refusing unsupported filesystem entry "${installReceiptFile}"`, ); await rm(join(elsewhere, 'receipt.json')); - await rm(destination, { force: true, recursive: true }); + await removeTree(destination); expect(await installBundle({ from: fixture.from, home, host: 'cursor', scope: 'user' })) .toMatchObject({ state: 'installed' }); // An owned path whose ancestor became a symlink (development installs re-point top-level directories). await cp(join(destination, '.cursor-plugin'), join(destination, '.real-manifest'), { recursive: true }); - await rm(join(destination, '.cursor-plugin'), { recursive: true }); + await removeTree(join(destination, '.cursor-plugin')); await symlink(join(destination, '.real-manifest'), join(destination, '.cursor-plugin')); const owned = await installBundle({ from: fixture.from, home, host: 'cursor', scope: 'user' }) .catch((failure: unknown) => failure); @@ -1794,9 +1795,9 @@ it('refuses to hash or write through a symlinked directory inside a receipt-mana expect((owned as DiagnosticError).diagnostics[0]?.message).toContain('Refusing unsupported filesystem entry ".cursor-plugin"'); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), - rm(elsewhere, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), + removeTree(elsewhere), ]); } }); @@ -1893,7 +1894,7 @@ it('ignores receipts whose file list could escape the plugin root', async () => await writeJson(join(root, installReceiptFile), current); expect(await readInstallReceipt(root)).toEqual(current); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1916,19 +1917,19 @@ posixPermissionIt('tree inventory refuses paths that could not round-trip throug await mkdir(join(fixture.bundleRoot, 'skills', 'odd.'), { recursive: true }); await writeFile(join(fixture.bundleRoot, 'skills', 'odd.', 'SKILL.md'), '# odd\n'); await expect(treeInventory(fixture.bundleRoot)).rejects.toThrow('Refusing unsupported filesystem entry "skills/odd./SKILL.md"'); - await rm(join(fixture.bundleRoot, 'skills'), { recursive: true }); + await removeTree(join(fixture.bundleRoot, 'skills')); // So is a directory spelled like the receipt: on a case-insensitive filesystem it is the receipt's path. await mkdir(join(fixture.bundleRoot, '.Agent-Bundle-Install.json')); await writeFile(join(fixture.bundleRoot, '.Agent-Bundle-Install.json', 'payload'), 'odd\n'); await expect(treeInventory(fixture.bundleRoot)).rejects.toThrow( 'Refusing unsupported filesystem entry ".Agent-Bundle-Install.json/payload"', ); - await rm(join(fixture.bundleRoot, '.Agent-Bundle-Install.json'), { recursive: true }); + await removeTree(join(fixture.bundleRoot, '.Agent-Bundle-Install.json')); expect(await installBundle({ from: fixture.from, home, host: 'cursor', scope: 'user' })).toMatchObject({ state: 'installed' }); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -1971,8 +1972,8 @@ it('never lets a receipt claim runtime state: a receipt owning state/ reads as l expect((await readInstallReceipt(destination))?.files.some((file) => file.startsWith('state/'))).toBe(false); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -2009,8 +2010,8 @@ posixPermissionIt('refuses a receipt that is not a regular file before reading i expect(await readFile(join(destination, 'payload.txt'), 'utf8')).toBe('payload\n'); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -2038,7 +2039,7 @@ it('refuses foreign Cursor directories even with --replace and gates version col expect(await readFile(join(destination, 'payload.txt'), 'utf8')).toBe('someone else\n'); // A different plugin's receipt-managed install at this path is foreign as well, even byte-identical. - await rm(destination, { force: true, recursive: true }); + await removeTree(destination); await installBundle({ from: fixture.from, home, host: 'cursor', scope: 'user' }); const receipt = JSON.parse(await readFile(join(destination, installReceiptFile), 'utf8')) as Record; await writeJson(join(destination, installReceiptFile), { ...receipt, plugin: 'other-plugin' }); @@ -2053,7 +2054,7 @@ it('refuses foreign Cursor directories even with --replace and gates version col expect((otherError as DiagnosticError).diagnostics[0]?.message).toContain('installed other-plugin@1.2.3'); // A receipt-managed install of this plugin at another version needs --replace. - await rm(destination, { force: true, recursive: true }); + await removeTree(destination); await installBundle({ from: fixture.from, home, host: 'cursor', scope: 'user' }); await writeJson(join(destination, '.cursor-plugin/plugin.json'), { name: 'install-fixture', version: '9.0.0' }); const versionError = await installBundle({ from: fixture.from, home, host: 'cursor', scope: 'user' }) @@ -2069,8 +2070,8 @@ it('refuses foreign Cursor directories even with --replace and gates version col }); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -2099,8 +2100,8 @@ it('refuses symlinks in a Cursor source bundle', async () => { }]); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -2127,8 +2128,8 @@ it('refuses a symlinked Cursor install destination even when its content matches ); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -2154,8 +2155,8 @@ it('rejects a Cursor plugin name that could escape the local install root', asyn await expect(access(join(home, '.cursor', 'plugins', 'escape'))).rejects.toMatchObject({ code: 'ENOENT' }); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -2254,8 +2255,8 @@ it('stages a committed local marketplace repository for Cursor in marketplace mo await expect(access(join(home, '.cursor', 'plugins', 'local', 'install-fixture'))).rejects.toMatchObject({ code: 'ENOENT' }); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -2339,8 +2340,8 @@ it('re-runs marketplace mode idempotently with real git and refuses collisions', expect((versionError as DiagnosticError).diagnostics[0]?.message).toContain('version collision'); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -2365,8 +2366,8 @@ it('fails closed without git in marketplace mode and leaves no staged repository await expect(access(marketplaceRepo(home))).rejects.toMatchObject({ code: 'ENOENT' }); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -2377,7 +2378,7 @@ it('refuses marketplace mode when the manifest points at a missing Cursor plugin await mkdir(join(home, '.cursor')); const { calls, runner } = recordingRunner(); try { - await rm(join(fixture.bundleRoot, '.cursor-plugin'), { recursive: true }); + await removeTree(join(fixture.bundleRoot, '.cursor-plugin')); await writeJson(join(fixture.bundleRoot, 'plugin.json'), { name: 'install-fixture', version: '1.2.3' }); const error = await installBundle({ commandRunner: runner, @@ -2408,8 +2409,8 @@ it('refuses marketplace mode when the manifest points at a missing Cursor plugin expect(calls).toEqual([]); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -2438,8 +2439,8 @@ it('refuses marketplace mode for a bundle that contains nested Git metadata', as await expect(access(join(home, '.cursor', 'agent-bundle'))).rejects.toMatchObject({ code: 'ENOENT' }); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -2460,8 +2461,8 @@ it('fails closed when the committed tree does not hold the staged bytes', async expect(await readdir(join(home, '.cursor', 'agent-bundle', 'marketplaces'))).toEqual([]); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(home, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(home), ]); } }); @@ -2480,7 +2481,7 @@ it('rejects an install mode for hosts other than Cursor', async () => { expect((error as DiagnosticError).diagnostics).toMatchObject([{ code: 'AB7003', target: 'claude' }]); expect(calls).toEqual([]); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); diff --git a/packages/agent-bundle/tests/integration-matrix.test.ts b/packages/agent-bundle/tests/integration-matrix.test.ts index 122e2592b..c576f9cfb 100644 --- a/packages/agent-bundle/tests/integration-matrix.test.ts +++ b/packages/agent-bundle/tests/integration-matrix.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { chmod, cp, mkdir, mkdtemp, readFile, rm, stat, symlink } from 'node:fs/promises'; +import { chmod, cp, mkdir, mkdtemp, readFile, stat, symlink } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -12,6 +12,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; import { codexArtifactPaths } from '../src/adapters/codex.ts'; import { build, inspect, invokeMcp, listHooks, listMcp, simulateHook, validate } from '../src/api.ts'; import { agentBundleNodeModules } from './helpers/workspace-paths.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const fixturesRoot = join(process.cwd(), 'fixtures', 'integration'); @@ -183,7 +184,7 @@ it('builds the checked-in fixture matrix from a path with spaces', async () => { target: hooks[0]!.host, })).resolves.toEqual({ additionalContext: 'hook:fixture', outcome: 'continue' }); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }, 60_000); @@ -208,7 +209,7 @@ it('builds the checked-in portable skills-only fixture', async () => { await readFile(join(root, 'src', 'skills', 'portable-skill', 'assets', 'binary.bin')), ); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); @@ -229,6 +230,6 @@ it('reports checked-in unsupported-capability and canonical-collision diagnostic expect(unsupported.diagnostics.map((diagnostic) => diagnostic.code)).toContain('AB4204'); expect(collision.diagnostics.map((diagnostic) => diagnostic.code)).toContain('AB4408'); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); diff --git a/packages/agent-bundle/tests/launch-env.test.ts b/packages/agent-bundle/tests/launch-env.test.ts index f2a05236b..a82ebe7a7 100644 --- a/packages/agent-bundle/tests/launch-env.test.ts +++ b/packages/agent-bundle/tests/launch-env.test.ts @@ -1,4 +1,4 @@ -import { chmod, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { chmod, mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { delimiter, join, resolve } from 'node:path'; @@ -11,11 +11,12 @@ import { operatorEnvPluginRoot, parseOperatorEnv, } from '../src/launch-env.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const createRoot = async (): Promise => { diff --git a/packages/agent-bundle/tests/layout-build.test.ts b/packages/agent-bundle/tests/layout-build.test.ts index c8d859b6f..71808d07f 100644 --- a/packages/agent-bundle/tests/layout-build.test.ts +++ b/packages/agent-bundle/tests/layout-build.test.ts @@ -1,7 +1,7 @@ import { Client } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; import { execFile as executeFile } from 'node:child_process'; -import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { promisify } from 'node:util'; @@ -9,12 +9,13 @@ import { promisify } from 'node:util'; import { afterEach, expect, it } from '@rstest/core'; import { build } from '../src/api.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const writeProjectFile = async (root: string, path: string, contents: string): Promise => { diff --git a/packages/agent-bundle/tests/lifecycle-replay-dev-server.test.ts b/packages/agent-bundle/tests/lifecycle-replay-dev-server.test.ts index 645aed253..55fcb3d3a 100644 --- a/packages/agent-bundle/tests/lifecycle-replay-dev-server.test.ts +++ b/packages/agent-bundle/tests/lifecycle-replay-dev-server.test.ts @@ -1,4 +1,4 @@ -import { mkdir, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, symlink, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { expect, it } from '@rstest/core'; @@ -8,6 +8,7 @@ import { startDevServer } from '../src/dev/workbench-server.ts'; import type { LifecycleListResponse, LifecycleReplay } from '../src/contracts/lifecycles.ts'; import { createProjectFixture } from './helpers/project-fixture.ts'; import { agentBundleNodeModules } from './helpers/workspace-paths.ts'; +import { removeTree } from './support/remove-tree.ts'; it('renders a lifecycle replay through a real default-pool dev server', { timeout: 30_000 }, async () => { const project = await createProjectFixture({ @@ -118,6 +119,6 @@ it('renders a lifecycle replay through a real default-pool dev server', { timeou }); } finally { await server?.close().catch(() => undefined); - await rm(project.root, { force: true, maxRetries: 5, recursive: true, retryDelay: 50 }); + await removeTree(project.root); } }); diff --git a/packages/agent-bundle/tests/manifest-combined-proof.test.ts b/packages/agent-bundle/tests/manifest-combined-proof.test.ts index 616958bbf..f63ee4dc6 100644 --- a/packages/agent-bundle/tests/manifest-combined-proof.test.ts +++ b/packages/agent-bundle/tests/manifest-combined-proof.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { access, cp, mkdir, mkdtemp, readFile, rm, stat, symlink, writeFile } from 'node:fs/promises'; +import { access, cp, mkdir, mkdtemp, readFile, stat, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { isAbsolute, join, relative, resolve } from 'node:path'; import { promisify } from 'node:util'; @@ -16,6 +16,7 @@ import { runDoctor } from '../src/install/doctor.ts'; import { webPluginDataDirectory } from '../src/web-host/launch.ts'; import { createProjectFixture, removeProjectFixture } from './helpers/project-fixture.ts'; import { awaitStdoutLine, runBin } from './support/bin-process.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const fixtureName = 'manifest-combined-proof'; @@ -214,8 +215,8 @@ describe('the authoritative manifest combined proof', () => { afterAll(async () => { await Promise.all([ projectRoot === '' ? Promise.resolve() : removeProjectFixture(projectRoot), - relocatedPackageRoot === '' ? Promise.resolve() : rm(relocatedPackageRoot, { force: true, recursive: true }), - isolatedHome === '' ? Promise.resolve() : rm(isolatedHome, { force: true, recursive: true }), + relocatedPackageRoot === '' ? Promise.resolve() : removeTree(relocatedPackageRoot), + isolatedHome === '' ? Promise.resolve() : removeTree(isolatedHome), ]); }); diff --git a/packages/agent-bundle/tests/manifest-reindex.test.ts b/packages/agent-bundle/tests/manifest-reindex.test.ts index cb95925e7..b92840789 100644 --- a/packages/agent-bundle/tests/manifest-reindex.test.ts +++ b/packages/agent-bundle/tests/manifest-reindex.test.ts @@ -10,6 +10,7 @@ import { artifactManifestName, parseArtifactManifest } from '../src/build/manife import { reindexArtifactManifest } from '../src/build/manifest-reindex.ts'; import { sha256Hex } from '../src/core/digest.ts'; import { writeInstallFixtureManifest } from './support/install-fixture.ts'; +import { removeTree } from './support/remove-tree.ts'; it('reindexes changed, added, and removed artifact files canonically', async () => { const root = await mkdtemp(join(tmpdir(), 'agent-bundle-manifest-reindex-')); @@ -72,7 +73,7 @@ it('reindexes changed, added, and removed artifact files canonically', async () }); expect(await readFile(manifestPath, 'utf8')).toBe(originalBytes); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -120,6 +121,6 @@ it('refuses to reindex compiled files and the compile evidence record: only a re } expect(await readFile(join(root, artifactManifestName), 'utf8')).toBe(before); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/manifest-relocatable.test.ts b/packages/agent-bundle/tests/manifest-relocatable.test.ts index 429b94a13..186933879 100644 --- a/packages/agent-bundle/tests/manifest-relocatable.test.ts +++ b/packages/agent-bundle/tests/manifest-relocatable.test.ts @@ -1,4 +1,4 @@ -import { access, mkdir, mkdtemp, readFile, realpath, rename, rm, symlink, writeFile } from 'node:fs/promises'; +import { access, mkdir, mkdtemp, readFile, realpath, rename, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; @@ -18,6 +18,7 @@ import { import { validateArtifact } from '../src/build/validate-artifact.ts'; import { stableJson } from '../src/core/digest.ts'; import { readBundleIdentity, type BundleIdentityHost } from '../src/install/identity.ts'; +import { removeTree } from './support/remove-tree.ts'; /** * Relocatable-path proof for `agent-bundle.manifest.json` (#592 step 3 / #604 @@ -186,7 +187,7 @@ beforeAll(async () => { }, 180_000); afterAll(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); it('emits a relocatable manifest that survives moving the composite root', async () => { diff --git a/packages/agent-bundle/tests/mcp-apps-compile.test.ts b/packages/agent-bundle/tests/mcp-apps-compile.test.ts index 01b9df55f..cd6a79e91 100644 --- a/packages/agent-bundle/tests/mcp-apps-compile.test.ts +++ b/packages/agent-bundle/tests/mcp-apps-compile.test.ts @@ -14,11 +14,12 @@ import { MAX_APP_HTML_BYTES } from '../src/core/mcp-app-limits.ts'; import type { AgentBundleToolsConfig, NormalizedMcpApp } from '../src/core/types.ts'; import type { AgentBundleMeta } from '../src/meta.ts'; import { agentBundlePackageRoot, workbenchNodeModules } from './helpers/workspace-paths.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const meta: AgentBundleMeta = Object.freeze({ diff --git a/packages/agent-bundle/tests/mcp-probe-dev-server.test.ts b/packages/agent-bundle/tests/mcp-probe-dev-server.test.ts index 45b047e29..3f04a8221 100644 --- a/packages/agent-bundle/tests/mcp-probe-dev-server.test.ts +++ b/packages/agent-bundle/tests/mcp-probe-dev-server.test.ts @@ -1,4 +1,4 @@ -import { access, cp, mkdir, rm, symlink, writeFile } from 'node:fs/promises'; +import { access, cp, mkdir, symlink, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { expect, it } from '@rstest/core'; @@ -9,6 +9,7 @@ import { createWorkbenchAssetSource } from '../src/dev/workbench-assets.ts'; import { startDevServer } from '../src/dev/workbench-server.ts'; import { createProjectFixture } from './helpers/project-fixture.ts'; import { agentBundleNodeModules } from './helpers/workspace-paths.ts'; +import { removeTree } from './support/remove-tree.ts'; it('runs an authenticated initialize and tools/list probe against a real built stdio server', { timeout: 30_000 }, async () => { const project = await createProjectFixture({ @@ -101,7 +102,7 @@ it('runs an authenticated initialize and tools/list probe against a real built s ['FORCE_COLOR', 'LANG', 'LC_ALL', 'NO_COLOR', 'TZ'].includes(key))).toBe(true); } finally { await server?.close().catch(() => undefined); - await rm(project.root, { force: true, maxRetries: 5, recursive: true, retryDelay: 50 }); + await removeTree(project.root); } }); @@ -196,6 +197,6 @@ it('joins detached probe plugin-data cleanup into Workbench shutdown', { timeout await expect(access(join(pluginData, 'proof.txt'))).rejects.toMatchObject({ code: 'ENOENT' }); } finally { await server?.close().catch(() => undefined); - await rm(project.root, { force: true, maxRetries: 5, recursive: true, retryDelay: 50 }); + await removeTree(project.root); } }); diff --git a/packages/agent-bundle/tests/mcp-probe-service.test.ts b/packages/agent-bundle/tests/mcp-probe-service.test.ts index cc78e0313..decc49386 100644 --- a/packages/agent-bundle/tests/mcp-probe-service.test.ts +++ b/packages/agent-bundle/tests/mcp-probe-service.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -24,6 +24,7 @@ import { type McpProbeTimers, type McpProbeTransport, } from '../src/dev/playground/mcp-probe-service.ts'; +import { removeTree } from './support/remove-tree.ts'; interface ManualTimer { readonly callback: () => void; @@ -281,7 +282,7 @@ it('maps a bounded, frozen successful probe snapshot and redacted launch', async expect(Object.isFrozen(report.snapshot)).toBe(true); expect(Object.isFrozen(report.snapshot?.tools)).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -316,7 +317,7 @@ it('redacts absolute paths after key-value and list separators', async () => { '[REDACTED]', ]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -476,7 +477,7 @@ it('keeps URLs while redacting real absolute and bundle paths (#316 review)', as expect(serialized).not.toContain(secret); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -496,7 +497,7 @@ it('truncates server instructions to the named text budget', async () => { expect(instructions).toHaveLength(mcpProbeInstructionTextLimit); expect(instructions?.endsWith('…')).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -521,7 +522,7 @@ it('reports connect rejection as an honest unreachable probe result', async () = }); expect(report).not.toHaveProperty('snapshot'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -543,7 +544,7 @@ it('times out within the total budget and destroys the transport', async () => { expect(report.failure?.kind).toBe('connect'); expect(transportCloses).toBeGreaterThan(0); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -592,7 +593,7 @@ it('returns a timed-out report without awaiting stalled teardown', async () => { await service.settle(); expect(timers.pending()).toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -633,7 +634,7 @@ it('returns a timed-out report without awaiting stalled teardown when the budget await service.settle(); expect(timers.pending()).toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -680,8 +681,8 @@ it('chains plugin-data removal to the close a timeout already started, not a dup await expect(readFile(join(pluginData!, 'proof.txt'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { releaseClose(); - if (pluginData !== undefined) await rm(pluginData, { force: true, recursive: true }); - await rm(root, { force: true, recursive: true }); + if (pluginData !== undefined) await removeTree(pluginData); + await removeTree(root); } }); @@ -716,7 +717,7 @@ it('coalesces only identical in-flight probes and clears them after settlement', await service.probe({ host: 'claude', serverName: 'timeline' }); expect(clients).toBe(2); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -739,7 +740,7 @@ it('throws typed not-found errors for unavailable trusted probe targets', async serverName: 'timeline', })).rejects.toBeInstanceOf(McpProbeTargetNotFoundError); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -792,8 +793,8 @@ it('removes plugin data only after a slow transport teardown settles (#316 revie await expect(readFile(join(pluginData!, 'proof.txt'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); expect(events).toEqual(['report-returned', 'transport-closed', 'plugin-data-removed']); } finally { - if (pluginData !== undefined) await rm(pluginData, { force: true, recursive: true }); - await rm(root, { force: true, recursive: true }); + if (pluginData !== undefined) await removeTree(pluginData); + await removeTree(root); } }); @@ -853,8 +854,8 @@ it('settle() fences in-flight probes, not only already-registered teardowns (#39 await expect(readFile(join(pluginData!, 'proof.txt'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); expect(events).toEqual(['report-returned', 'transport-closed', 'settled']); } finally { - if (pluginData !== undefined) await rm(pluginData, { force: true, recursive: true }); - await rm(root, { force: true, recursive: true }); + if (pluginData !== undefined) await removeTree(pluginData); + await removeTree(root); } }); @@ -886,8 +887,8 @@ it('still closes the transport and removes plugin data when a close() throws syn expect(transportClosed).toBe(true); await expect(readFile(join(pluginData!, 'proof.txt'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - if (pluginData !== undefined) await rm(pluginData, { force: true, recursive: true }); - await rm(root, { force: true, recursive: true }); + if (pluginData !== undefined) await removeTree(pluginData); + await removeTree(root); } }); @@ -917,8 +918,8 @@ it('reports a timeout even when the timeout teardown throws synchronously', asyn await service.settle(); await expect(readFile(join(pluginData!, 'proof.txt'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - if (pluginData !== undefined) await rm(pluginData, { force: true, recursive: true }); - await rm(root, { force: true, recursive: true }); + if (pluginData !== undefined) await removeTree(pluginData); + await removeTree(root); } }); @@ -954,7 +955,7 @@ it('retries plugin-data removal once a capped teardown finally settles (#397 rev events.push('removal-rejected'); throw Object.assign(new Error('EPERM: operation not permitted'), { code: 'EPERM' }); } - await rm(target, { force: true, recursive: true }); + await removeTree(target); }, }); @@ -993,8 +994,8 @@ it('retries plugin-data removal once a capped teardown finally settles (#397 rev expect(events).toEqual(['removal-rejected', 'settled', 'transport-closed', 'plugin-data-removed']); await expect(readFile(join(pluginData, 'proof.txt'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(parent, { force: true, recursive: true }); - await rm(root, { force: true, recursive: true }); + await removeTree(parent); + await removeTree(root); } }); @@ -1017,7 +1018,7 @@ it('retries removal once, fenced, when the teardown settled but the directory wa if (removals === 1) { throw Object.assign(new Error('EPERM: operation not permitted'), { code: 'EPERM' }); } - await rm(target, { force: true, recursive: true }); + await removeTree(target); }, }); @@ -1028,8 +1029,8 @@ it('retries removal once, fenced, when the teardown settled but the directory wa expect(removals).toBe(2); await expect(readFile(join(pluginData!, 'proof.txt'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - if (pluginData !== undefined) await rm(pluginData, { force: true, recursive: true }); - await rm(root, { force: true, recursive: true }); + if (pluginData !== undefined) await removeTree(pluginData); + await removeTree(root); } }); @@ -1049,7 +1050,7 @@ it('removes the fresh plugin data directory after every probe', async () => { await expect(readFile(join(pluginData!, 'proof.txt'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - if (pluginData !== undefined) await rm(pluginData, { force: true, recursive: true }); - await rm(root, { force: true, recursive: true }); + if (pluginData !== undefined) await removeTree(pluginData); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/mcp-session-service.test.ts b/packages/agent-bundle/tests/mcp-session-service.test.ts index 5a3c9856a..8d5768400 100644 --- a/packages/agent-bundle/tests/mcp-session-service.test.ts +++ b/packages/agent-bundle/tests/mcp-session-service.test.ts @@ -34,6 +34,7 @@ import { agentBundleNodeModules } from './helpers/workspace-paths.ts'; import { eventually } from './support/eventually.ts'; import { mcpCatalogStub, stdioTransportStub } from './support/mcp-client-stub.ts'; import { loadedProject } from './support/loaded-project.ts'; +import { removeTree } from './support/remove-tree.ts'; const registry: NormalizationTargetRegistry = { configExtensions: () => [], @@ -310,7 +311,7 @@ it('keeps one generated server and plugin-data directory bound to the selected e } else { process.env[inheritedKey] = previousInherited; } - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -422,7 +423,7 @@ it('lowers every session trace entry onto the unified trace with request/respons await isolated.close(); await throwing.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -451,7 +452,7 @@ it('rejects an MCP server not declared for the selected projection', async () => })).rejects.toThrow('Expected exactly one portable MCP server matching "fixture".'); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -552,7 +553,7 @@ it('uses the admitted session timeout for initialization, catalog, operations, a await session.close(); } finally { await service?.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -583,8 +584,8 @@ it('uses the configured project root as the default workspace from a decoy cwd', process.chdir(originalCwd); await service?.close(); await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(decoy, { force: true, recursive: true }), + removeTree(root), + removeTree(decoy), ]); } }, 30_000); @@ -615,7 +616,7 @@ it('pins the selected epoch until the persistent session closes', async () => { }); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -647,7 +648,7 @@ it('fails tool calls closed with a typed stale-epoch error when the pinned epoch // Another process's build retention cannot observe this process's epoch // leases: it removes the pinned epoch directory and metadata underneath // the live session while `active-epoch.json` already names epoch-2. - await rm(join(root, '.agent-bundle', 'epochs', 'epoch-1'), { force: true, recursive: true }); + await removeTree(join(root, '.agent-bundle', 'epochs', 'epoch-1')); await rm(join(root, '.agent-bundle', 'epochs', '.metadata', 'epoch-1.json'), { force: true }); await expect(session.callTool({ arguments: {}, name: 'inspect' })).rejects.toMatchObject({ @@ -663,7 +664,7 @@ it('fails tool calls closed with a typed stale-epoch error when the pinned epoch await session.close(); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -685,8 +686,8 @@ it('executes only the acquired epoch reference root when service and store roots await service.close(); } finally { await Promise.all([ - rm(serviceRoot, { force: true, recursive: true }), - rm(storeRoot, { force: true, recursive: true }), + removeTree(serviceRoot), + removeTree(storeRoot), ]); } }, 30_000); @@ -754,7 +755,7 @@ it('closes an in-flight open instead of returning an untracked epoch-pinning ses code: 'ENOENT', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -824,7 +825,7 @@ it('retains a rejected cleanup from an opening drained during service close', as code: 'ENOENT', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -886,7 +887,7 @@ it('orders opening cleanup failures before active session cleanup failures durin })); await expect(service.close()).rejects.toBe(failure); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -958,7 +959,7 @@ it('waits for every session cleanup and retains every close failure', async () = 'MCP session service is closed.', ); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1011,7 +1012,7 @@ it('closes a replacement client when restart races with session shutdown', async expect(clients).toHaveLength(2); expect(clients[1]!.closes()).toBe(1); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1044,7 +1045,7 @@ it('rejects an already-aborted tool call without invoking the MCP SDK', async () await session.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1103,8 +1104,8 @@ it('rejects a tool call aborted while its epoch availability probe is pending', expect(calls).toBe(0); await session.close(); } finally { - await rm(root, { force: true, recursive: true }); - await rm(pluginData, { force: true, recursive: true }); + await removeTree(root); + await removeTree(pluginData); } }, 30_000); @@ -1171,7 +1172,7 @@ it('bounds frame and event retention with an explicit replay overflow cursor', a await session.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1213,7 +1214,7 @@ it('delivers replay and reentrant live trace entries in one monotonic order', as expect(second).toEqual(Array.from({ length: 512 }, (_, index) => index + 91)); await session.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1247,7 +1248,7 @@ it('fails and closes the session as soon as stderr exceeds its output bound', as expect(clientCloses).toBe(1); expect(Buffer.byteLength(session.stderr())).toBeLessThanOrEqual(1_000_000); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1341,7 +1342,7 @@ it('fails admission, lifecycle, and service misuse closed with coded McpSessionE 'MCP session service is closed.', ); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1372,7 +1373,7 @@ it('opens a generated streamable HTTP server through its modern transport', asyn await Promise.all([httpSession.close(), service.close()]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1559,7 +1560,7 @@ it('retains frozen transport snapshots without caller or subscriber mutation', a await session.close(); expect(closed).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1735,7 +1736,7 @@ it('exposes one opaque, epoch-bound session handle with a bounded ordered wire t } else { process.env[secretKey] = previousSecret; } - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1849,7 +1850,7 @@ it('leases immutable canonical MCP App data without closing the control-owned se await expect(session.listTools()).resolves.toEqual([visibleTool, hiddenTool, defaultTool]); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1890,7 +1891,7 @@ it('closes an unleased session immediately on closeSessionWhenUnleased and a lea expect(service.closeSessionWhenUnleased(leased.id)).toBe(false); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1968,7 +1969,7 @@ it('synchronously invalidates App leases when the control session closes during await serviceLease.release(); expect(clientCloses).toBe(2); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -2060,7 +2061,7 @@ it('revokes App authority before a direct session close drains its client and in await lease.release(); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -2123,8 +2124,8 @@ it('shares one close promise when a synchronous close observer re-enters shutdow expect(epochCloses).toBe(1); await expect(access(pluginData)).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); - await rm(pluginData, { force: true, recursive: true }); + await removeTree(root); + await removeTree(pluginData); } }, 30_000); diff --git a/packages/agent-bundle/tests/mcp.test.ts b/packages/agent-bundle/tests/mcp.test.ts index de12de430..c937b75e1 100644 --- a/packages/agent-bundle/tests/mcp.test.ts +++ b/packages/agent-bundle/tests/mcp.test.ts @@ -33,6 +33,7 @@ import { import { agentBundleNodeModules, agentBundlePackageRoot, workbenchNodeModules } from './helpers/workspace-paths.ts'; import { loadedProject } from './support/loaded-project.ts'; import { runNodeScript } from './support/run-node-script.ts'; +import { removeTree } from './support/remove-tree.ts'; const registry: NormalizationTargetRegistry = { configExtensions: () => [], @@ -156,7 +157,7 @@ it('normalizes local, prebuilt, and HTTP MCP server declarations', async () => { expect(Object.isFrozen(model.mcpServers)).toBe(true); expect(Object.isFrozen(model.mcpServers[0])).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -221,8 +222,8 @@ it('normalizes deeply frozen local MCP App declarations independently of the pro expect(portableIdentity(firstApp)).toEqual(portableIdentity(secondApp)); } finally { await Promise.all([ - rm(firstRoot, { force: true, recursive: true }), - rm(secondRoot, { force: true, recursive: true }), + removeTree(firstRoot), + removeTree(secondRoot), ]); } }); @@ -253,7 +254,7 @@ it('keeps local MCP server identities and output aliases independent of the proj source: join(right, 'src', 'server.ts'), }); } finally { - await Promise.all([rm(left, { force: true, recursive: true }), rm(right, { force: true, recursive: true })]); + await Promise.all([removeTree(left), removeTree(right)]); } }); @@ -314,7 +315,7 @@ it('reports source and model diagnostics before an MCP server can be compiled', { code: 'AB4321' }, ]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -349,7 +350,7 @@ it('rejects a hostile normalized legacy SSE transport before adapters can plan i sourcePath: join(root, 'agent-bundle.config.ts'), }]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -449,7 +450,7 @@ it('rejects unsafe, duplicate, and nonlocal MCP App declarations before browser { code: 'AB4336', target: 'unknown' }, ]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -498,7 +499,7 @@ it('rejects non-JSON MCP App metadata before normalization', async () => { ]); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -659,7 +660,7 @@ it('bundles each local MCP entry once and maps every target manifest to that art })).rejects.toThrow(); expect(await readFile(join(outputRoot, 'mcp', outputName), 'utf8')).toBe(previousBundle); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -718,7 +719,7 @@ it('inlines agent-bundle/launch-env into a self-connecting entry so it can apply expect(await probe({ PROBE_HOST: 'from-host' })).toEqual({ applied: ['PROBE_FILE'], file: 'from-file', host: 'from-host' }); expect(await probe({ AGENT_BUNDLE_ENV_FILE: 'none', PROBE_HOST: 'from-host' })).toEqual({ applied: [], file: null, host: 'from-host' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -824,7 +825,7 @@ it('lets the operator .env beat a manifest env default the host passed through, // `AGENT_BUNDLE_ENV_FILE=none` disables the layer: the manifest default stands. expect(await launch({ AGENT_BUNDLE_ENV_FILE: 'none' })).toEqual(delivered); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 60_000); @@ -898,7 +899,7 @@ it('redirects stdout written at module scope by the server module to stderr befo // The frames themselves never went through the wrapper. expect(stderr).not.toContain('wrapped:{'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 60_000); @@ -1017,7 +1018,7 @@ it('builds one deterministic self-contained MCP App view and injects it through ], }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1096,7 +1097,7 @@ it('injects one release identity into both the Node bundle and the browser MCP A expect(html).not.toContain('agent-bundle/meta'); expect(await validateArtifact({ artifactRoot: outputRoot })).toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1163,7 +1164,7 @@ it('compiles one shared MCP App once and serves it from every identically declar } expect(await validateArtifact({ artifactRoot: outputRoot })).toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1211,7 +1212,7 @@ it('rejects conflicting same-name MCP App declarations at compilation planning', 'Duplicate compiled MCP App destination "mcp-apps/widget.html"; servers may share an app name only with an identical declaration.', ); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -1355,7 +1356,7 @@ it('uses the selected streamable HTTP manifest with propagated cancellation and await expect(service.list({ artifact, server: 'http', target: 'claude' })).rejects.toThrow(); expect(closes).toBe(1); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1405,7 +1406,7 @@ it('rejects a selected projection without its manifest-declared MCP document', a target: 'codex', })).rejects.toThrow('The codex projection has no MCP document.'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1503,8 +1504,8 @@ it('creates session state only after setup succeeds and always inherits the stdi } else { process.env[inheritedKey] = previousInherited; } - await rm(sessionTmp, { force: true, recursive: true }); - await rm(root, { force: true, recursive: true }); + await removeTree(sessionTmp); + await removeTree(root); } }, 30_000); @@ -1582,8 +1583,8 @@ it('serves compiler-bundled MCP App resources from a copied artifact without pro await build({ model, outputRoot, projectRoot: root, registry: createDefaultRegistry(), routeGraph: emptyCompiledRouteGraph }); const expectedHtml = await readFile(join(outputRoot, 'mcp-apps', 'dashboard.html'), 'utf8'); await cp(outputRoot, artifact, { recursive: true }); - await rm(join(root, 'src'), { force: true, recursive: true }); - await rm(join(root, 'views'), { force: true, recursive: true }); + await removeTree(join(root, 'src')); + await removeTree(join(root, 'views')); expect(await validateArtifact({ artifactRoot: artifact })).toEqual([]); const client = new Client({ name: 'app-resource-consumer', version: '1.0.0' }); @@ -1628,8 +1629,8 @@ it('serves compiler-bundled MCP App resources from a copied artifact without pro } } finally { await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(consumer, { force: true, recursive: true }), + removeTree(root), + removeTree(consumer), ]); } }, 30_000); @@ -1712,7 +1713,7 @@ it('lists tools from a validated copied artifact without reading project source' const artifact = join(consumer, 'installed-plugin'); await build({ model, outputRoot, projectRoot: root, registry: createDefaultRegistry(), routeGraph: emptyCompiledRouteGraph }); await cp(outputRoot, artifact, { recursive: true }); - await rm(join(root, 'src'), { force: true, recursive: true }); + await removeTree(join(root, 'src')); const api = await import('../src/api.ts') as { readonly McpService?: new () => { @@ -1831,8 +1832,8 @@ it('lists tools from a validated copied artifact without reading project source' await expect(pending).rejects.toBeDefined(); } finally { await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(consumer, { force: true, recursive: true }), + removeTree(root), + removeTree(consumer), ]); } }, 30_000); diff --git a/packages/agent-bundle/tests/native-claude-contract.test.ts b/packages/agent-bundle/tests/native-claude-contract.test.ts index f3e3e8cc9..60ee33c61 100644 --- a/packages/agent-bundle/tests/native-claude-contract.test.ts +++ b/packages/agent-bundle/tests/native-claude-contract.test.ts @@ -1,9 +1,10 @@ -import { mkdir, mkdtemp, readFile, rm, utimes, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, utimes, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { describe, expect, it } from '@rstest/core'; +import { removeTree } from './support/remove-tree.ts'; const loadNativeClaudeContract = async () => import('./support/native-claude-smoke.ts').catch(() => undefined); const nativeIt = process.env.AGENT_BUNDLE_NATIVE_CLAUDE_SMOKE === '1' ? it : it.skip; @@ -23,7 +24,7 @@ const withIsolatedHome = async (run: (homeDirectory: string) => Promise): try { return await run(homeDirectory); } finally { - await rm(homeDirectory, { force: true, recursive: true }); + await removeTree(homeDirectory); } }; @@ -305,7 +306,7 @@ it('proves the normal Claude config, settings, and plugins stay unchanged withou 'claude-native.normal-home.changed', ]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -360,7 +361,7 @@ describe('the default sibling Claude state file', () => { if (initialState !== undefined) await writeFile(join(defaultHome, '.claude.json'), initialState); await operation(defaultHome); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; @@ -762,6 +763,6 @@ nativeIt('runs the checked-in candidate with the existing signed-in Claude subsc expect(report.status).toBe('passed'); } finally { - await rm(fixture, { force: true, recursive: true }); + await removeTree(fixture); } }, 120_000); diff --git a/packages/agent-bundle/tests/native-codex-contract.test.ts b/packages/agent-bundle/tests/native-codex-contract.test.ts index 141398f72..41304993d 100644 --- a/packages/agent-bundle/tests/native-codex-contract.test.ts +++ b/packages/agent-bundle/tests/native-codex-contract.test.ts @@ -1,8 +1,9 @@ -import { chmod, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { chmod, mkdir, mkdtemp, readFile, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { expect, it } from '@rstest/core'; +import { removeTree } from './support/remove-tree.ts'; const fixtureRoot = new URL('../../../fixtures/contracts/hosts/codex/', import.meta.url); const nativeIt = process.env.AGENT_BUNDLE_NATIVE_CODEX_SMOKE === '1' ? it : it.skip; @@ -139,7 +140,7 @@ it('infers automatic activation only from the candidate Skill sentinel', async ( }); expect(JSON.stringify(result)).not.toContain('agent-bundle-codex-skill-sentinel'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -187,7 +188,7 @@ it('copies auth bytes opaquely with the source mode preserved', async () => { expect(await readFile(destination, 'utf8')).toBe('{"opaque":"state"}\n'); expect((await stat(destination)).mode & 0o777).toBe((await stat(source)).mode & 0o777); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -244,7 +245,7 @@ it('uses and removes an isolated temporary home while retaining only redacted ev await expect(stat(temporaryHomes[0]!)).rejects.toMatchObject({ code: 'ENOENT' }); expect(JSON.stringify(result)).not.toContain('unretained local stderr'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -289,7 +290,7 @@ it('retains a failed exec JSONL error only as a redacted envelope', async () => }); expect(JSON.stringify(result)).not.toContain('unretained local stderr'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -333,7 +334,7 @@ it('times out a slow Codex step and bounds oversized process output', async () = expect(JSON.stringify(result)).not.toContain('x'.repeat(128)); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -414,7 +415,7 @@ it('contains snapshot, temporary-home, candidate-copy, and cleanup failures in h }); expect(JSON.stringify(result)).not.toContain('do not retain this cleanup detail'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/native-playground-service.test.ts b/packages/agent-bundle/tests/native-playground-service.test.ts index c5968362a..967ebb991 100644 --- a/packages/agent-bundle/tests/native-playground-service.test.ts +++ b/packages/agent-bundle/tests/native-playground-service.test.ts @@ -17,6 +17,7 @@ import type { DiscoveredEvalSuite } from '../src/eval/discovery.ts'; import type { EvalFixturePlan } from '../src/eval/fixtures.ts'; import { defineEvalSuite, normalizeEvalCase } from '../src/eval/suite.ts'; import { deepFreeze } from '../src/core/freeze.ts'; +import { removeTree } from './support/remove-tree.ts'; const epoch = (id: string, root: string, target?: 'claude' | 'codex') => Object.freeze({ @@ -75,7 +76,7 @@ const testCatalogDirectory = (): string => { afterEach(async () => { const directories = [...catalogDirectories]; catalogDirectories.clear(); - await Promise.all(directories.map((directory) => rm(directory, { force: true, recursive: true }))); + await Promise.all(directories.map((directory) => removeTree(directory))); }); const nativeCatalogDurabilityPlatformKey = Symbol.for('agent-bundle.native-playground-service.catalog-durability-platform'); @@ -290,7 +291,7 @@ it('retains an exact epoch catalog across service restart after fixture source c })); await restarted.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -333,7 +334,7 @@ it('rejects corrupt, oversized, and duplicate persisted catalog snapshots withou await reader.close(); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -365,7 +366,7 @@ it('rejects a persisted catalog replaced by a symbolic link before parsing it', await expect(reader.catalog(reference)).rejects.toThrow('catalog snapshot'); await reader.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -459,7 +460,7 @@ it('rejects catalog directories that escape epoch metadata through a symlinked d await expect(service.catalog(reference)).rejects.toThrow('catalog directory is invalid'); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } } }); @@ -496,7 +497,7 @@ it('requires every persisted fixture sha256 to be exactly 64 lowercase hexadecim .rejects.toThrow('Native Playground discovered an invalid fixture plan.'); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } } }); @@ -555,7 +556,7 @@ it('rejects a catalog whose cumulative nested values exceed the whole-sidecar bu await expect(service.catalog(reference)).rejects.toThrow('catalog snapshot is invalid'); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -601,7 +602,7 @@ it('tolerates only Windows directory fsync capability failures during catalog pu }; try { for (const code of ['EACCES', 'EINVAL', 'EPERM'] as const) { - await rm(catalogDirectory, { force: true, recursive: true }); + await removeTree(catalogDirectory); const service = serviceFor(code); await expect(service.catalog(epoch(`epoch-${code.toLowerCase()}`, join(root, code)))).resolves.toMatchObject({ epochId: `epoch-${code.toLowerCase()}` }); await service.close(); @@ -609,7 +610,7 @@ it('tolerates only Windows directory fsync capability failures during catalog pu } finally { if (previousPlatform === undefined) delete runtime[nativeCatalogDurabilityPlatformKey]; else runtime[nativeCatalogDurabilityPlatformKey] = previousPlatform; - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -664,7 +665,7 @@ it('fails catalog publication when Windows regular-file fsync EPERM is not a dir await service.close(); if (previousPlatform === undefined) delete runtime[nativeCatalogDurabilityPlatformKey]; else runtime[nativeCatalogDurabilityPlatformKey] = previousPlatform; - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -697,7 +698,7 @@ it('fails catalog publication when file fsync EPERM is not a Windows FlushFileBu await expect(service.catalog(epoch('epoch-posix-file-fsync', join(root, 'artifact')))).rejects.toBe(eperm); } finally { await service.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -759,7 +760,7 @@ it('preserves a catalog replacement raced into rollback and fsyncs the parent af expect(directorySyncs).toBe(publicationSyncs + 1); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -894,7 +895,7 @@ it('keeps close pending until admitted run cleanup has settled', async () => { await expect(closing).resolves.toBeUndefined(); } finally { releaseCleanup(); - await rm(projectRoot, { force: true, recursive: true }); + await removeTree(projectRoot); } }); @@ -938,7 +939,7 @@ it('retains an admitted workspace cleanup failure for service close', async () = expect(closeFailure).toBeInstanceOf(AggregateError); expect((closeFailure as AggregateError).errors).toEqual([cleanupFailure]); } finally { - await rm(projectRoot, { force: true, recursive: true }); + await removeTree(projectRoot); } }); @@ -995,7 +996,7 @@ it('refuses fixture bytes changed after cataloging without recomputing the serve })); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1061,7 +1062,7 @@ it('turns missing, incompatible, and unauthenticated native preflight into path- await service.close(); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1163,7 +1164,7 @@ it('projects only awaited normalized Claude completion evidence and removes its expect((await readdir(join(root, '.agent-bundle'))).filter((entry) => entry.startsWith('native-playground-'))).toEqual([]); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1229,7 +1230,7 @@ it('bounds normalized native evidence before it reaches durable Playground event expect(Buffer.byteLength(JSON.stringify(responseEvent), 'utf8')).toBeLessThan(1024 * 1024); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1298,7 +1299,7 @@ it('redacts hostile normalized Codex MCP labels without changing observed eviden expect(JSON.stringify(result)).not.toContain('sk-proj-1234567890abcdef'); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1384,7 +1385,7 @@ it('awaits a cancelled Codex child, preserves its harness failure, and removes a expect((await readdir(join(root, '.agent-bundle'))).filter((entry) => entry.startsWith('native-playground-'))).toEqual([]); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1461,7 +1462,7 @@ it('eagerly captures every epoch catalog before a later build can replace author })); await restarted.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1523,7 +1524,7 @@ it('fsyncs durable catalog publication, validates a no-replace winner, and retai }; try { for (const failure of ['write', 'file', 'link', 'directory'] as const) { - await rm(catalogDirectory, { force: true, recursive: true }); + await removeTree(catalogDirectory); const service = serviceFor(failure); if (failure === 'directory') { await expect(service.catalog(reference)).rejects.toMatchObject({ @@ -1537,12 +1538,12 @@ it('fsyncs durable catalog publication, validates a no-replace winner, and retai .rejects.toMatchObject({ code: 'ENOENT' }); await service.close(); } - await rm(catalogDirectory, { force: true, recursive: true }); + await removeTree(catalogDirectory); const cleanupFailure = new Error('stage cleanup failed'); const cleanupService = serviceFor('file', cleanupFailure); await expect(cleanupService.catalog(reference)).rejects.toMatchObject({ errors: [failures.get('file'), cleanupFailure] }); await cleanupService.close(); - await rm(catalogDirectory, { force: true, recursive: true }); + await removeTree(catalogDirectory); // Two independent services race on the same epoch: the loser validates the // link winner rather than replacing it. @@ -1552,7 +1553,7 @@ it('fsyncs durable catalog publication, validates a no-replace winner, and retai expect((await readdir(catalogDirectory)).filter((name) => name.includes('.stage-'))).toEqual([]); await Promise.all([left.close(), right.close()]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1613,7 +1614,7 @@ it('waits for a linked winner to release its staging link, then adopts it instea await Promise.all([winner.close(), loser.close()]); } finally { releaseWinnerCleanup(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1678,7 +1679,7 @@ it('never adopts a staged sidecar that its publisher rolls back, and republishes await Promise.all([winner.close(), loser.close()]); } finally { releaseWinnerCleanup(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1748,7 +1749,7 @@ it('withdraws a sidecar whose directory fsync fails before releasing its staging await Promise.all([winner.close(), loser.close()]); } finally { releaseDirectorySync(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1806,7 +1807,7 @@ it('keeps the staging link when a failed publication cannot roll its sidecar bac await expect(reader.catalog(reference)).rejects.toThrow('catalog snapshot is invalid'); await reader.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1964,7 +1965,7 @@ it('recovers a staging link abandoned by an exited publisher after the settle de // The exited publisher may never have flushed the directory after link(); recovery does. expect(directorySyncs).toEqual([catalogDirectory]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1991,7 +1992,7 @@ it('still rejects a persisted catalog aliased by a hard link that is not an epoc await rm(join(catalogDirectory, alias)); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2049,7 +2050,7 @@ it('sweeps staging files orphaned by exited publishers of other epochs on the ne await expect(readFile(join(catalogDirectory, 'epoch-next.json'), 'utf8')).resolves.toContain('"epochId":"epoch-next"'); } finally { await service.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2113,7 +2114,7 @@ it('keeps a live winner\'s staging link, live-publisher and foreign entries, and await reader.close(); } finally { await service.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2144,7 +2145,7 @@ it('bounds the orphan sweep per publish and finishes on later publications', asy expect(await stagingEntries()).toEqual([]); } finally { await service.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2181,7 +2182,7 @@ it('drains a gated catalog discovery before close and never publishes it after c await expect(service.catalog(reference)).rejects.toThrow('closed'); await expect(readFile(join(root, '.agent-bundle', 'epochs', '.metadata', 'native-playground', 'epoch-catalog-close.json'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2343,7 +2344,7 @@ it('rolls back an owned native catalog sidecar when staging cleanup alone fails' }); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2388,7 +2389,7 @@ it('preserves a replacement sidecar when staging cleanup fails after the owned l await expect(readFile(join(catalogDirectory, `${reference.epoch.id}.json`), 'utf8')).resolves.toBe(replacement); await service.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2461,7 +2462,7 @@ it('does not deadlock when a direct native Codex abort listener awaits a reentra await running; expect((await readdir(join(root, '.agent-bundle'))).filter((entry) => entry.startsWith('native-playground-'))).toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -2538,6 +2539,6 @@ it('does not deadlock when caller cancellation reaches a native Codex close list await closing; expect((await readdir(join(root, '.agent-bundle'))).filter((entry) => entry.startsWith('native-playground-'))).toEqual([]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/normalization.test.ts b/packages/agent-bundle/tests/normalization.test.ts index ddd20f9d3..36e9e2d42 100644 --- a/packages/agent-bundle/tests/normalization.test.ts +++ b/packages/agent-bundle/tests/normalization.test.ts @@ -1,5 +1,5 @@ import { expect, it } from '@rstest/core'; -import { chmod, mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises'; +import { chmod, mkdtemp, mkdir, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -16,6 +16,7 @@ import type { Diagnostic } from '../src/core/diagnostics.ts'; import type { DiscoveredProject } from '../src/config/discover.ts'; import type { LoadedConfig } from '../src/config/load.ts'; import { emptyRouteConfig, type CompiledAgentRoute, type CompiledRouteGraph } from '../src/routes/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const registry: NormalizationTargetRegistry = { configExtensions: () => [], @@ -295,7 +296,7 @@ it('enumerates claude.bin relative to the config file into immutable executable expect(Object.isFrozen(model.hostBins?.[0]?.files)).toBe(true); expect(Object.isFrozen(model.hostBins?.[0]?.files[0])).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -359,7 +360,7 @@ it('enumerates Claude workflows and output styles relative to the config file in expect(Object.isFrozen(model.hostOutputStyles)).toBe(true); expect(Object.isFrozen(model.hostOutputStyles?.[0]?.files[0])).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -383,7 +384,7 @@ it.each([ expect(model.hostBins?.[0]).toMatchObject({ files: [], issue, source: binRoot, target: 'claude' }); expect(plan.diagnostics).toContainEqual(expect.objectContaining({ code, severity: 'error' })); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -416,7 +417,7 @@ it.each([ expect(payload).toMatchObject({ files: [], issue, source: sourceRoot, target: 'claude' }); expect(plan.diagnostics).toContainEqual(expect.objectContaining({ code, severity: 'error' })); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -448,8 +449,8 @@ it('rejects a Claude payload directory symlink that resolves outside the project })); } finally { await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(outside, { force: true, recursive: true }), + removeTree(root), + removeTree(outside), ]); } }); diff --git a/packages/agent-bundle/tests/package-build.test.ts b/packages/agent-bundle/tests/package-build.test.ts index 6da897c99..31d797fa8 100644 --- a/packages/agent-bundle/tests/package-build.test.ts +++ b/packages/agent-bundle/tests/package-build.test.ts @@ -1,6 +1,6 @@ import { execFile as executeFile } from 'node:child_process'; import { EventEmitter } from 'node:events'; -import { copyFile, mkdir, mkdtemp, readdir, readFile, realpath, rm, stat, symlink, writeFile } from 'node:fs/promises'; +import { copyFile, mkdir, mkdtemp, readdir, readFile, realpath, stat, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -14,6 +14,7 @@ import { runCli } from '../src/cli.ts'; import { DiagnosticError, type Diagnostic } from '../src/core/diagnostics.ts'; import { captureCliTerminal } from './support/cli-terminal.ts'; import { mcpServerStateDirectory } from '../src/core/mcp-state-directory.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const workspaceNodeModules = join(process.cwd(), 'node_modules'); @@ -27,7 +28,7 @@ const installTypescriptToolchain = async (root: string): Promise => { }; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const fixtureRoot = async (files: Readonly>): Promise => { diff --git a/packages/agent-bundle/tests/package-conventions.test.ts b/packages/agent-bundle/tests/package-conventions.test.ts index 4fedf1c8a..9119f2a0c 100644 --- a/packages/agent-bundle/tests/package-conventions.test.ts +++ b/packages/agent-bundle/tests/package-conventions.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, realpath, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -14,6 +14,7 @@ import { import { normalizePackageBuild } from '../src/config/normalize.ts'; import type { AgentBundleConfig } from '../src/core/types.ts'; import type { LoadedConfig } from '../src/config/load.ts'; +import { removeTree } from './support/remove-tree.ts'; const registry: NormalizationTargetRegistry = { configExtensions: () => [], @@ -25,7 +26,7 @@ const registry: NormalizationTargetRegistry = { const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const projectRoot = async (files: Readonly>): Promise => { diff --git a/packages/agent-bundle/tests/package-identity.test.ts b/packages/agent-bundle/tests/package-identity.test.ts index 7ca18451d..ac1ddf529 100644 --- a/packages/agent-bundle/tests/package-identity.test.ts +++ b/packages/agent-bundle/tests/package-identity.test.ts @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto'; -import { mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdtemp, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -17,6 +17,7 @@ import { } from '../src/core/project-context.ts'; import type { AgentBundleConfig } from '../src/core/types.ts'; import { ProjectService } from '../src/dev/project-service.ts'; +import { removeTree } from './support/remove-tree.ts'; const registry: NormalizationTargetRegistry = { configExtensions: () => [], @@ -58,7 +59,7 @@ const withProject = async ( if (packageJson !== undefined) await writeFile(join(root, 'package.json'), packageJson); await run(root); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; @@ -213,7 +214,7 @@ it('ignores a package.json symlinked outside the project root', async () => { { code: 'AB4011', severity: 'warning' }, ]); }); - await rm(outside, { force: true, recursive: true }); + await removeTree(outside); }); it('infers the plugin version from package.json when the config omits it', async () => { diff --git a/packages/agent-bundle/tests/packed-consumer-typescript.test.ts b/packages/agent-bundle/tests/packed-consumer-typescript.test.ts index 610bb4ceb..c094272cf 100644 --- a/packages/agent-bundle/tests/packed-consumer-typescript.test.ts +++ b/packages/agent-bundle/tests/packed-consumer-typescript.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { access, mkdir, mkdtemp, readFile, readlink, rm, writeFile } from 'node:fs/promises'; +import { access, mkdir, mkdtemp, readFile, readlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { promisify } from 'node:util'; @@ -8,6 +8,7 @@ import { expect, it } from '@rstest/core'; import { isolatedCommandEnvironment } from '../../../rstest.worker-isolation.ts'; import { cachedNpmInstallArguments, sharedPackedTarball } from './support/shared-pack.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); @@ -85,6 +86,6 @@ it('never shadows the consumer\'s tsc bin from a packed npm install', async () = expect(JSON.stringify(JSON.parse(inspected))).toContain('"tool:demo/status"'); expect(inspected).toContain('Read status.'); } finally { - await rm(consumerRoot, { force: true, recursive: true }); + await removeTree(consumerRoot); } }, 120_000); diff --git a/packages/agent-bundle/tests/packed-consumer.test.ts b/packages/agent-bundle/tests/packed-consumer.test.ts index 32e6556bb..fc4e2814c 100644 --- a/packages/agent-bundle/tests/packed-consumer.test.ts +++ b/packages/agent-bundle/tests/packed-consumer.test.ts @@ -22,6 +22,7 @@ import { init, parse } from 'es-module-lexer/minimal'; import { sha256Hex } from '../src/core/digest.ts'; import { cachedNpmInstallArguments, installedEnvironment, linkWorkspaceTypes, packOutputFromJson } from './support/shared-pack.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); @@ -180,7 +181,7 @@ it('uses only an installed tarball after source deletion', async () => { const installedPackage = await realpath(join(projectRoot, 'node_modules', 'agent-bundle')); expect(installedPackage.startsWith(workspaceRoot)).toBe(false); expect(installedEnvironment().NODE_PATH).toBeUndefined(); - await rm(packedPackageRoot, { force: true, recursive: true }); + await removeTree(packedPackageRoot); const scriptPackage = await realpath(join(scriptProjectRoot, 'node_modules', 'agent-bundle')); expect(scriptPackage.startsWith(workspaceRoot)).toBe(false); @@ -276,11 +277,11 @@ it('uses only an installed tarball after source deletion', async () => { await Promise.all([ rm(join(projectRoot, 'agent-bundle.config.ts')), - rm(join(projectRoot, 'native'), { force: true, recursive: true }), + removeTree(join(projectRoot, 'native')), rm(join(projectRoot, 'package.json')), - rm(join(projectRoot, 'skills'), { force: true, recursive: true }), - rm(join(projectRoot, 'src'), { force: true, recursive: true }), - rm(join(projectRoot, 'views'), { force: true, recursive: true }), + removeTree(join(projectRoot, 'skills')), + removeTree(join(projectRoot, 'src')), + removeTree(join(projectRoot, 'views')), ]); await expect(access(join(projectRoot, 'agent-bundle.config.ts'))).rejects.toThrow(); @@ -510,6 +511,6 @@ it('uses only an installed tarball after source deletion', async () => { }], }); } finally { - await rm(consumerRoot, { force: true, recursive: true }); + await removeTree(consumerRoot); } }, 240_000); diff --git a/packages/agent-bundle/tests/packed-deleted-source.test.ts b/packages/agent-bundle/tests/packed-deleted-source.test.ts index 75a47af03..3708405bb 100644 --- a/packages/agent-bundle/tests/packed-deleted-source.test.ts +++ b/packages/agent-bundle/tests/packed-deleted-source.test.ts @@ -1,4 +1,4 @@ -import { access, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { access, mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -11,6 +11,7 @@ import { removeProjectSource, type DeletedSourceReceipt, } from '../src/test/index.ts'; +import { removeTree } from './support/remove-tree.ts'; describe('deleted-source artifact evidence', () => { it('removes conventional project source and returns a frozen relative receipt', async () => { @@ -33,7 +34,7 @@ describe('deleted-source artifact evidence', () => { await expect(access(join(projectRoot, 'agent-bundle.config.ts'))).rejects.toThrow(); await expect(access(join(projectRoot, 'src'))).rejects.toThrow(); } finally { - await rm(projectRoot, { force: true, recursive: true }); + await removeTree(projectRoot); } }); @@ -45,7 +46,7 @@ describe('deleted-source artifact evidence', () => { expect(error).toBeInstanceOf(AgentTestError); expect((error as AgentTestError).code).toBe('deleted-source-unverified'); } finally { - await rm(projectRoot, { force: true, recursive: true }); + await removeTree(projectRoot); } }); @@ -66,7 +67,7 @@ describe('deleted-source artifact evidence', () => { expect((error as AgentTestError).code).toBe('deleted-source-unverified'); expect((error as AgentTestError).message).toContain(proofLevelLabel('packed-deleted-source')); } finally { - await rm(projectRoot, { force: true, recursive: true }); + await removeTree(projectRoot); } }); @@ -94,7 +95,7 @@ describe('deleted-source artifact evidence', () => { expect((error as AgentTestError).message).toContain(projectA); expect((error as AgentTestError).message).toContain(entry); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -122,7 +123,7 @@ describe('deleted-source artifact evidence', () => { expect((error as AgentTestError).message).toContain(projectA); expect((error as AgentTestError).message).toContain(projectB); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); }); diff --git a/packages/agent-bundle/tests/packed-host-install-proof.test.ts b/packages/agent-bundle/tests/packed-host-install-proof.test.ts index bbcf06102..2a45e1746 100644 --- a/packages/agent-bundle/tests/packed-host-install-proof.test.ts +++ b/packages/agent-bundle/tests/packed-host-install-proof.test.ts @@ -1,6 +1,6 @@ import { execFile as executeFile, spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; -import { access, copyFile, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'; +import { access, copyFile, mkdir, mkdtemp, readFile, readdir, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { promisify } from 'node:util'; @@ -26,6 +26,7 @@ import { HOST_INSTALL_PROOF_LEVEL, proofLevelLabel, } from '../src/test/manifest.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const proofLabel = proofLevelLabel(HOST_INSTALL_PROOF_LEVEL); @@ -190,7 +191,7 @@ beforeAll(async () => { access(join(installedArtifactRoot, '.cursor-plugin', 'plugin.json')), ]); - await rm(projectRoot, { force: true, recursive: true }); + await removeTree(projectRoot); await expect(stat(projectRoot), proofLabel).rejects.toMatchObject({ code: 'ENOENT' }); await expect(access(join(projectRoot, 'dist', 'bin', `${pluginName}.mjs`)), proofLabel) .rejects.toMatchObject({ code: 'ENOENT' }); @@ -209,7 +210,7 @@ beforeAll(async () => { afterAll(async () => { await Promise.all([ - cleanupRoot === undefined ? Promise.resolve() : rm(cleanupRoot, { force: true, recursive: true }), + cleanupRoot === undefined ? Promise.resolve() : removeTree(cleanupRoot), sourceFixture === undefined ? Promise.resolve() : disposeHostInstallFixture(sourceFixture), ]); }); diff --git a/packages/agent-bundle/tests/packed-install-bin.test.ts b/packages/agent-bundle/tests/packed-install-bin.test.ts index 58d792ec7..0ca2fe4b1 100644 --- a/packages/agent-bundle/tests/packed-install-bin.test.ts +++ b/packages/agent-bundle/tests/packed-install-bin.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { access, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'; +import { access, mkdir, mkdtemp, readFile, realpath, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { promisify } from 'node:util'; @@ -19,6 +19,7 @@ import { sharedPackedTarball, } from './support/shared-pack.ts'; import { timeScale } from './support/time-scale.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const packageName = 'install-bin-fixture'; @@ -167,11 +168,11 @@ beforeAll(async () => { // `packed-deleted-source`: the source project, its build, and its node_modules // (the only `agent-bundle` on disk) are gone before the bin runs. await removeProjectSource({ projectRoot: project }); - await rm(project, { force: true, recursive: true }); + await removeTree(project); }, 300_000); afterAll(async () => { - if (consumer.length > 0) await rm(consumer, { force: true, recursive: true }); + if (consumer.length > 0) await removeTree(consumer); }); it('ships a self-contained installer bin that binds its own npm root', async () => { diff --git a/packages/agent-bundle/tests/packed-native-smoke.test.ts b/packages/agent-bundle/tests/packed-native-smoke.test.ts index c47591397..32e6d0cce 100644 --- a/packages/agent-bundle/tests/packed-native-smoke.test.ts +++ b/packages/agent-bundle/tests/packed-native-smoke.test.ts @@ -1,9 +1,10 @@ -import { mkdir, mkdtemp, rm, utimes, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, utimes, writeFile } from 'node:fs/promises'; import { spawnSync } from 'node:child_process'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { expect, it } from '@rstest/core'; +import { removeTree } from './support/remove-tree.ts'; const loadPackedNativeSmoke = async () => import('./support/packed-native-smoke.ts').catch(() => undefined); @@ -115,7 +116,7 @@ it('detects normal Claude config, settings, or plugin changes without retaining await utimes(pluginPath, fixedTime, fixedTime); })).resolves.toBe(false); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -162,8 +163,8 @@ it('opaquely detects default ~/.claude.json mutation without extending custom co expect(JSON.stringify(customChanged)).not.toContain(customHome); } finally { await Promise.all([ - rm(userHome, { force: true, recursive: true }), - rm(customHome, { force: true, recursive: true }), + removeTree(userHome), + removeTree(customHome), ]); } }); @@ -198,7 +199,7 @@ it('guards Claude settings and plugins across a real turn while tolerating the . await writeFile(join(userHome, '.claude', 'plugins', 'installed.json'), '{"plugins":["changed"]}\n'); }, { homeDirectory: userHome })).resolves.toBe(false); } finally { - await rm(userHome, { force: true, recursive: true }); + await removeTree(userHome); } }); diff --git a/packages/agent-bundle/tests/packed-readonly-state-root.test.ts b/packages/agent-bundle/tests/packed-readonly-state-root.test.ts index e0c68c449..2c9a830fe 100644 --- a/packages/agent-bundle/tests/packed-readonly-state-root.test.ts +++ b/packages/agent-bundle/tests/packed-readonly-state-root.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { chmod, cp, mkdir, mkdtemp, readdir, readFile, rm, stat } from 'node:fs/promises'; +import { chmod, cp, mkdir, mkdtemp, readdir, readFile, stat } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, relative, resolve } from 'node:path'; import { promisify } from 'node:util'; @@ -14,6 +14,7 @@ import { openPackedMcpServer, removeProjectSource } from '../src/test/packed.ts' import { resolveWebLaunch } from '../src/web-host/launch.ts'; import { readWebManifest } from '../src/web-host/manifest.ts'; import { cachedNpmInstallArguments, installedEnvironment, sharedPackedTarball } from './support/shared-pack.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const fixtureRoot = resolve(import.meta.dirname, '../fixtures/durable-web-surface'); @@ -230,6 +231,6 @@ it('serves a state-writing tool from a read-only installed artifact without writ expect(await exists(stateRoot)).toBe(false); } finally { if (readOnly) await chmodTree(installedRoot, { directory: 0o755, file: 0o644 }); - await rm(consumer, { force: true, recursive: true }); + await removeTree(consumer); } }, 300_000); diff --git a/packages/agent-bundle/tests/packed-small-plugin.test.ts b/packages/agent-bundle/tests/packed-small-plugin.test.ts index 71e7da0cd..32e96a52c 100644 --- a/packages/agent-bundle/tests/packed-small-plugin.test.ts +++ b/packages/agent-bundle/tests/packed-small-plugin.test.ts @@ -12,6 +12,7 @@ import { installedEnvironment, sharedPackedTarball, } from './support/shared-pack.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const examples = resolve(process.cwd(), 'examples'); @@ -233,6 +234,6 @@ it('keeps packed static and plain-hook plugins free of undeclared runtimes', asy expect(registrations).toEqual(['skills/review']); expect(await readFile(processTrace, 'utf8')).toBe(''); } finally { - await rm(consumer, { force: true, recursive: true }); + await removeTree(consumer); } }, 180_000); diff --git a/packages/agent-bundle/tests/packed-stdio-projection.test.ts b/packages/agent-bundle/tests/packed-stdio-projection.test.ts index 68d92b9b7..f2392f951 100644 --- a/packages/agent-bundle/tests/packed-stdio-projection.test.ts +++ b/packages/agent-bundle/tests/packed-stdio-projection.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { cp, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { cp, mkdtemp, readdir, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { promisify } from 'node:util'; @@ -20,6 +20,7 @@ import { } from '../src/test/packed.ts'; import { routeHarnessPackedContractFixtures } from './support/contract-matrix-fixtures.ts'; import { cachedNpmInstallArguments, installedEnvironment, sharedPackedTarball } from './support/shared-pack.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const fixtureRoot = resolve(import.meta.dirname, '../fixtures/route-harness'); @@ -462,6 +463,6 @@ it.each([ await secondSession.close(); } } finally { - await rm(consumer, { force: true, recursive: true }); + await removeTree(consumer); } }, 300_000); diff --git a/packages/agent-bundle/tests/packed-web-command.test.ts b/packages/agent-bundle/tests/packed-web-command.test.ts index 64c8facf6..156a1ea78 100644 --- a/packages/agent-bundle/tests/packed-web-command.test.ts +++ b/packages/agent-bundle/tests/packed-web-command.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile, type ChildProcess } from 'node:child_process'; -import { cp, mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { cp, mkdir, mkdtemp, readdir, readFile, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { promisify } from 'node:util'; @@ -20,6 +20,7 @@ import { sharedPackedTarball, } from './support/shared-pack.ts'; import { timeScale } from './support/time-scale.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const fixtureRoot = resolve(import.meta.dirname, '../fixtures/web-surface'); @@ -145,7 +146,7 @@ beforeAll(async () => { 'approve', pluginName, ], { cwd: installedConsumer, env: installedEnvironment() }); - await rm(join(installedConsumer, 'node_modules'), { force: true, recursive: true }); + await removeTree(join(installedConsumer, 'node_modules')); } await execFile('npm', [ 'install', @@ -180,7 +181,7 @@ beforeAll(async () => { afterAll(async () => { for (const child of spawned) child.kill('SIGKILL'); killAll(observedProcessIds); - if (consumer.length > 0) await rm(consumer, { force: true, recursive: true }); + if (consumer.length > 0) await removeTree(consumer); }); it('builds the exposed App into the composite root: a manifest web section, one launch record on the server row, and one self-contained bin carrying the host', { timeout: 60_000 }, async () => { diff --git a/packages/agent-bundle/tests/path-token-resolver.test.ts b/packages/agent-bundle/tests/path-token-resolver.test.ts index f4dbfa8ac..c5ec332eb 100644 --- a/packages/agent-bundle/tests/path-token-resolver.test.ts +++ b/packages/agent-bundle/tests/path-token-resolver.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -15,6 +15,7 @@ import { pathTokens } from '../src/core/types.ts'; import { createMcpPathTokenResolver, resolveMcpPathTokens } from '../src/services/mcp-path-tokens.ts'; import type { TargetMcpRuntimeContract } from '../src/services/mcp-runtime.ts'; import { McpService } from '../src/services/mcp-service.ts'; +import { removeTree } from './support/remove-tree.ts'; interface ResolutionFixture { readonly cases: readonly { @@ -309,6 +310,6 @@ it('resolves Claude path tokens outside command when launching a generated artif }, }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); diff --git a/packages/agent-bundle/tests/playground-orchestration-service.test.ts b/packages/agent-bundle/tests/playground-orchestration-service.test.ts index 2e907c629..d00c14d76 100644 --- a/packages/agent-bundle/tests/playground-orchestration-service.test.ts +++ b/packages/agent-bundle/tests/playground-orchestration-service.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, rm } from 'node:fs/promises'; +import { mkdir, mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -32,6 +32,7 @@ import { PlaygroundStore } from '../src/dev/playground/playground-store.ts'; import type { ProjectStatus } from '../src/dev/types.ts'; import { eventuallyPasses } from './support/eventually.ts'; import { deepFreeze } from '../src/core/freeze.ts'; +import { removeTree } from './support/remove-tree.ts'; const activeEpoch = Object.freeze({ @@ -611,7 +612,7 @@ it('exports and promotes the real durable response event reference from a native }); } finally { await service.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/playground-service.test.ts b/packages/agent-bundle/tests/playground-service.test.ts index 86971c354..a7ce33c3f 100644 --- a/packages/agent-bundle/tests/playground-service.test.ts +++ b/packages/agent-bundle/tests/playground-service.test.ts @@ -15,6 +15,7 @@ import { type PlaygroundServiceOptions, type PlaygroundTraceEvent, } from '../src/dev/playground/playground-store.ts'; +import { removeTree } from './support/remove-tree.ts'; interface SessionIndex { readonly kind: 'agent-bundle-playground-session-index'; @@ -194,7 +195,7 @@ const createFixture = async (input: Readonly<{ return Object.freeze({ close: async () => { await service.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); }, projectRoot, service, @@ -651,7 +652,7 @@ it('rejects arbitrary, symlinked, wrong-project, and unknown-session storage pat await expect(owner.openSession({ ...sessionInput(), sessionId: '../escape' })).rejects.toThrow('path-safe'); await Promise.allSettled([outside.close(), symlinked.close(), owner.close(), otherProject.close()]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -909,7 +910,7 @@ it('rolls back a failed finalization metadata commit so promotion cannot observe expect(fixture.service.session('transactional')).toMatchObject({ state: 'open' }); expect(fixture.service.session('transactional')).not.toHaveProperty('outcome'); await expect(fixture.service.promoteToDraftEval('transactional', [])).rejects.toThrow('durable'); - await rm(metadataPath, { recursive: true }); + await removeTree(metadataPath); await writeFile(metadataPath, original, 'utf8'); await expect(fixture.service.finalize('transactional', { status: 'passed' })).resolves.toMatchObject({ state: 'finalized' }); await expect(fixture.service.promoteToDraftEval('transactional', [])).resolves.toMatchObject({ outcome: { status: 'passed' } }); diff --git a/packages/agent-bundle/tests/plugin-logo.test.ts b/packages/agent-bundle/tests/plugin-logo.test.ts index dbac38888..7d7bb6832 100644 --- a/packages/agent-bundle/tests/plugin-logo.test.ts +++ b/packages/agent-bundle/tests/plugin-logo.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -9,13 +9,14 @@ import { cursorAdapter, cursorPluginValidator } from '../src/adapters/cursor.ts' import { normalizeProject, validateSource } from '../src/config/index.ts'; import type { LoadedConfig } from '../src/config/load.ts'; import type { AgentBundleConfig, NormalizedPlugin } from '../src/core/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const registry = createDefaultRegistry(); const logoSvg = '\n'; const tempRoots: string[] = []; afterAll(async () => { - await Promise.all(tempRoots.map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(tempRoots.map((root) => removeTree(root))); }); const loadedProject = async ( diff --git a/packages/agent-bundle/tests/portable-plugin-validation.test.ts b/packages/agent-bundle/tests/portable-plugin-validation.test.ts index ce5d43392..71b5e174e 100644 --- a/packages/agent-bundle/tests/portable-plugin-validation.test.ts +++ b/packages/agent-bundle/tests/portable-plugin-validation.test.ts @@ -7,6 +7,7 @@ import { validatePortablePlugin, validatePortablePluginFiles, } from '../src/host-contracts/portable-plugin-validation.ts'; +import { removeTree } from './support/remove-tree.ts'; const pluginSchema = 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json'; const mcpSchema = 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json'; @@ -14,7 +15,7 @@ const mcpSchema = 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const writeJson = async (path: string, value: unknown): Promise => { @@ -229,7 +230,7 @@ it('rejects every forbidden control character in HTTP header values while permit it('reports fixed component locations of the wrong filesystem kind and skill directories without SKILL.md', async () => { const root = await conformantBundle(); - await rm(join(root, 'skills'), { recursive: true }); + await removeTree(join(root, 'skills')); await writeText(join(root, 'skills'), 'not a directory'); await rm(join(root, 'mcp.json')); await mkdir(join(root, 'mcp.json')); @@ -241,7 +242,7 @@ it('reports fixed component locations of the wrong filesystem kind and skill dir expect(codes(wrongKinds)).toEqual(['AB6036', 'AB6036']); await rm(join(root, 'skills')); - await rm(join(root, 'mcp.json'), { recursive: true }); + await removeTree(join(root, 'mcp.json')); await mkdir(join(root, 'skills', 'empty'), { recursive: true }); await mkdir(join(root, 'skills', 'nested', 'SKILL.md'), { recursive: true }); await writeText(join(root, 'skills', 'README.md'), 'stray file, ignored by clients\n'); diff --git a/packages/agent-bundle/tests/prebuilt-payload.test.ts b/packages/agent-bundle/tests/prebuilt-payload.test.ts index 64787d9a5..2ffc39be2 100644 --- a/packages/agent-bundle/tests/prebuilt-payload.test.ts +++ b/packages/agent-bundle/tests/prebuilt-payload.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { mkdtemp, readFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -11,6 +11,7 @@ import { definePrebuilt as definePrebuiltFromIndex } from '../src/index.ts'; import { parseArtifactManifest } from '../src/build/manifest.ts'; import { resolveWebLaunch, webPluginDataDirectory } from '../src/web-host/launch.ts'; import { createProjectFixture, removeProjectFixture } from './helpers/project-fixture.ts'; +import { removeTree } from './support/remove-tree.ts'; const configSource = (options: { readonly payload?: string; readonly hooks?: string; readonly mcp?: string }): string => [ 'export default {', @@ -351,7 +352,7 @@ it('carries prebuilt args and env through the launch record and the web launcher expect(pluginData.startsWith(home)).toBe(true); expect(pluginData.startsWith(artifact)).toBe(false); } finally { - await Promise.all([removeProjectFixture(root), rm(home, { force: true, recursive: true })]); + await Promise.all([removeProjectFixture(root), removeTree(home)]); } }); diff --git a/packages/agent-bundle/tests/prepack.test.ts b/packages/agent-bundle/tests/prepack.test.ts index 67b0bd45a..f785ae10c 100644 --- a/packages/agent-bundle/tests/prepack.test.ts +++ b/packages/agent-bundle/tests/prepack.test.ts @@ -19,6 +19,7 @@ import { type PackOutput, } from '../src/build/pack-inventory.ts'; import type { PackageBuildResult } from '../src/build/package-build.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const workspaceNodeModules = join(process.cwd(), 'node_modules'); @@ -77,7 +78,7 @@ beforeAll(async () => { }); afterAll(async () => { - await rm(cleanupRoot, { force: true, recursive: true }); + await removeTree(cleanupRoot); }); const diagnostics = ( @@ -492,7 +493,7 @@ it('reports git, GitHub-shorthand, remote-tarball, and path dependency specifier for (const name of ['alias', 'tilde', 'versioned', 'embedded', 'vendored', 'tarred']) { expect(reported[0]?.message).not.toContain(JSON.stringify(name)); } - await rm(join(projectRoot, 'dist', 'vendor'), { force: true, recursive: true }); + await removeTree(join(projectRoot, 'dist', 'vendor')); expect(reported[0]?.recovery).toContain('registry'); const underPnpm = withCode(await diagnostics(pack, true), 'AB7015'); @@ -571,7 +572,7 @@ it('accepts a dependency a consumer install script names or runs, through delega }, ); } finally { - await rm(wrapper, { force: true, recursive: true }); + await removeTree(wrapper); } }, )); @@ -594,7 +595,7 @@ it('reads a dependency whose installed manifest is not JSON as an unknown execut expect(unused?.message).not.toContain('"broken-dep"'); expect(withCode(reported, 'AB7015')).toHaveLength(0); } finally { - await rm(broken, { force: true, recursive: true }); + await removeTree(broken); } }, )); @@ -618,7 +619,7 @@ it('reads an installed manifest as npm does, so the last of duplicate name keys expect(skipped?.severity).toBe('error'); expect(withCode(reported, 'AB7014')).toHaveLength(0); } finally { - await rm(dup, { force: true, recursive: true }); + await removeTree(dup); } }, )); @@ -636,7 +637,7 @@ it('surfaces a warning when the only finding is an unresolvable optional depende expect((await diagnostics(pack)).map((diagnostic) => [diagnostic.code, diagnostic.severity])) .toEqual([['AB7015', 'warning']]); } finally { - await rm(extras, { force: true, recursive: true }); + await removeTree(extras); } }, )); @@ -794,7 +795,7 @@ it('installs a real generated tarball and runs its manifest-driven Cursor instal await execFile('npm', ['install', '--ignore-scripts', '--no-audit', '--no-fund', join(tarballs, packed.filename)], { cwd: consumer, }); - await rm(projectRoot, { force: true, recursive: true }); + await removeTree(projectRoot); const installer = join(consumer, 'node_modules', 'installer-fixture', 'install.mjs'); const installed = await execFile(process.execPath, [installer], { diff --git a/packages/agent-bundle/tests/project-context-walk-bound.test.ts b/packages/agent-bundle/tests/project-context-walk-bound.test.ts index c21acccd9..3bbd93a79 100644 --- a/packages/agent-bundle/tests/project-context-walk-bound.test.ts +++ b/packages/agent-bundle/tests/project-context-walk-bound.test.ts @@ -1,11 +1,12 @@ import { realpathSync } from 'node:fs'; -import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, mkdir, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { expect, it, rs } from '@rstest/core'; import { createProjectContext, ProjectService } from '../src/dev/index.ts'; +import { removeTree } from './support/remove-tree.ts'; const createProject = async (): Promise => { const root = await mkdtemp(join(tmpdir(), 'agent-bundle-walk-bound-')); @@ -72,6 +73,6 @@ it('bounds filesystem probes for deep missing payload paths', async () => { expect(depth14).toBeLessThan(depth6 * 4); } finally { realpathNative.mockRestore(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/projection/contract-matrix.test.ts b/packages/agent-bundle/tests/projection/contract-matrix.test.ts index 5c0c3ea93..359bc0c4e 100644 --- a/packages/agent-bundle/tests/projection/contract-matrix.test.ts +++ b/packages/agent-bundle/tests/projection/contract-matrix.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, rm } from 'node:fs/promises'; +import { mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; @@ -28,6 +28,7 @@ import { routeHarnessContractFixtures, routeHarnessLifecycleWithoutLiveProgress, } from '../support/contract-matrix-fixtures.ts'; +import { removeTree } from '../support/remove-tree.ts'; const proofLabel = proofLevelLabel(MCP_IN_MEMORY_PROOF_LEVEL); const fixtureRoot = resolve(import.meta.dirname, '../../fixtures/route-harness'); @@ -58,7 +59,7 @@ const withStatefulMatrix = async ( return await body(options); } finally { await restarted?.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; @@ -165,7 +166,7 @@ const withPackedShapedSession = async ( return await body(packedSession, manifest); } finally { await session.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; @@ -309,7 +310,7 @@ describe('the generated-plugin contract matrix', () => { } finally { await session.close(); await runtime.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -369,7 +370,7 @@ describe('the generated-plugin contract matrix', () => { } finally { await session.close(); await runtime.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -417,7 +418,7 @@ describe('the generated-plugin contract matrix', () => { }); } finally { await session.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); @@ -504,7 +505,7 @@ describe('the generated-plugin contract matrix', () => { } finally { await session.close(); await runtime.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 30_000); diff --git a/packages/agent-bundle/tests/projection/mcp-in-memory.test.ts b/packages/agent-bundle/tests/projection/mcp-in-memory.test.ts index ebd8e4351..b15c711ef 100644 --- a/packages/agent-bundle/tests/projection/mcp-in-memory.test.ts +++ b/packages/agent-bundle/tests/projection/mcp-in-memory.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, rm } from 'node:fs/promises'; +import { mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -19,6 +19,7 @@ import { openInMemoryMcpServer, readMcpResource, } from '../../src/test/mcp.ts'; +import { removeTree } from '../support/remove-tree.ts'; /** * The `mcp-in-memory` proof level: the real generated MCP server, registered @@ -417,7 +418,7 @@ describe('the in-memory MCP projection level', () => { }); } finally { await session.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -480,7 +481,7 @@ describe('the in-memory MCP projection level', () => { } } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } expect((await listMcpSurface()).resources).not.toContain('agent-bundle://notices/inbox'); @@ -552,7 +553,7 @@ describe('the in-memory MCP projection level', () => { await driver.close(); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -808,7 +809,7 @@ describe('the in-memory MCP projection level', () => { await expect(volatile.client.subscribeResource({ uri: inboxUri })).rejects.toThrow(/Method not found/u); } finally { await volatile.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/provider-typegen.test.ts b/packages/agent-bundle/tests/provider-typegen.test.ts index 697e64d4c..4266baebd 100644 --- a/packages/agent-bundle/tests/provider-typegen.test.ts +++ b/packages/agent-bundle/tests/provider-typegen.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -6,11 +6,12 @@ import { afterEach, expect, it } from '@rstest/core'; import ts from 'typescript-5'; import { inspect } from '../src/api.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const writeProjectFile = async (root: string, path: string, contents: string): Promise => { diff --git a/packages/agent-bundle/tests/public-api-packed.test.ts b/packages/agent-bundle/tests/public-api-packed.test.ts index 90a62ba09..945294fc5 100644 --- a/packages/agent-bundle/tests/public-api-packed.test.ts +++ b/packages/agent-bundle/tests/public-api-packed.test.ts @@ -1,6 +1,6 @@ import { execFile as executeFile } from 'node:child_process'; import type { Dirent } from 'node:fs'; -import { mkdtemp, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'; +import { mkdtemp, mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, relative } from 'node:path'; import { promisify } from 'node:util'; @@ -21,6 +21,7 @@ import { isErrno } from '../src/core/errors.ts'; import { emptyCompiledRouteGraph } from '../src/routes/graph.ts'; import { agentSkillsSchemaRevision } from '../src/schemas/agent-skills/contract.ts'; import { cachedNpmInstallArguments, linkWorkspaceTypes, sharedPackedTarball } from './support/shared-pack.ts'; +import { removeTree } from './support/remove-tree.ts'; interface PackageManifest { bin: { @@ -225,7 +226,7 @@ beforeAll(async () => { { cwd: warmRoot, env: isolatedCommandEnvironment() }, ); } finally { - await rm(warmRoot, { force: true, recursive: true }); + await removeTree(warmRoot); } }, 180_000); @@ -257,7 +258,7 @@ it('writes the package version as the producer of a packed CLI manifest', async version: manifest.version, }); } finally { - await rm(consumerRoot, { force: true, recursive: true }); + await removeTree(consumerRoot); } }, 30_000); @@ -294,7 +295,7 @@ it('installs one Rspack engine into a packed consumer', async () => { expect(bindings, report).toHaveLength(1); expect(bindings[0]!.version, report).toBe(installed('@rspack/core')[0]!.version); } finally { - await rm(consumerRoot, { force: true, recursive: true }); + await removeTree(consumerRoot); } }, 30_000); @@ -376,7 +377,7 @@ it('imports the externalized config entry from a packed npm consumer', async () const aliasedRuntime = await readFile(join(installedDist, 'mcp-server-runtime.d.ts'), 'utf8'); expect(aliasedRuntime).toContain('GeneratedNoticeDeliveryBinding'); } finally { - await rm(consumerRoot, { force: true, recursive: true }); + await removeTree(consumerRoot); } }, 30_000); @@ -414,7 +415,7 @@ it('runs the packed App client through a dynamic-origin parent', async () => { expect(JSON.parse(stdout)).toEqual({ active: 0, status: 'healthy' }); } finally { - await rm(consumerRoot, { force: true, recursive: true }); + await removeTree(consumerRoot); } }, 30_000); @@ -496,6 +497,6 @@ it('invokes a prebuilt MCP server from a clean packed consumer', async () => { server: { name: 'packed-fixture', version: '1.0.0' }, }); } finally { - await rm(consumerRoot, { force: true, recursive: true }); + await removeTree(consumerRoot); } }, 30_000); diff --git a/packages/agent-bundle/tests/public-api.test.ts b/packages/agent-bundle/tests/public-api.test.ts index c99f77693..659778893 100644 --- a/packages/agent-bundle/tests/public-api.test.ts +++ b/packages/agent-bundle/tests/public-api.test.ts @@ -1,6 +1,6 @@ import { supportedCapabilities } from './support/adapter-capabilities.ts'; import { execFile as executeFile } from 'node:child_process'; -import { access, mkdtemp, mkdir, readFile, readdir, realpath, rm, symlink, writeFile } from 'node:fs/promises'; +import { access, mkdtemp, mkdir, readFile, readdir, realpath, symlink, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { tmpdir } from 'node:os'; import { dirname, join, relative } from 'node:path'; @@ -40,6 +40,7 @@ import type { } from '../src/api.ts'; import { runCli } from '../src/cli.ts'; import { agentBundleNodeModules, workspaceNodeModules } from './helpers/workspace-paths.ts'; +import { removeTree } from './support/remove-tree.ts'; interface PackageManifest { bin: { @@ -302,7 +303,7 @@ it('writes the package version as the producer of a built CLI manifest', async ( version: manifest.version, }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -426,7 +427,7 @@ it('keeps bundled config extension types in emitted root declarations', async () 'config.mts', ], { cwd: consumerRoot })).resolves.toMatchObject({ stderr: '', stdout: '' }); } finally { - await rm(consumerRoot, { force: true, recursive: true }); + await removeTree(consumerRoot); } }, 30_000); diff --git a/packages/agent-bundle/tests/publint-gate.test.ts b/packages/agent-bundle/tests/publint-gate.test.ts index a371f5b2c..9e5ddae3e 100644 --- a/packages/agent-bundle/tests/publint-gate.test.ts +++ b/packages/agent-bundle/tests/publint-gate.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, realpath, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -9,6 +9,7 @@ import { pluginPublint } from 'rsbuild-plugin-publint'; import agentBundleConfig from '../rslib.config.ts'; import createAgentBundleConfig from '../../create-agent-bundle/rslib.config.ts'; import rscRuntimeConfig from '../../rsc-runtime/rslib.config.ts'; +import { removeTree } from './support/remove-tree.ts'; /** * publint is not a separate CI step: every publishable package's `rslib @@ -36,7 +37,7 @@ describe('publint build gate', () => { const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const probePackage = async (manifest: Readonly>): Promise => { diff --git a/packages/agent-bundle/tests/rendered-skills.test.ts b/packages/agent-bundle/tests/rendered-skills.test.ts index c87e6ef76..5301179b7 100644 --- a/packages/agent-bundle/tests/rendered-skills.test.ts +++ b/packages/agent-bundle/tests/rendered-skills.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, realpath, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -16,6 +16,7 @@ import { renderElementToMarkdown } from '../src/config/render-markdown.ts'; import { standardPluginArtifactPlan } from '../src/adapters/types.ts'; import type { AgentBundleConfig } from '../src/core/types.ts'; import type { LoadedConfig } from '../src/config/load.ts'; +import { removeTree } from './support/remove-tree.ts'; const fixtureRoot = join(import.meta.dirname, 'fixtures', 'rendered-skill'); @@ -29,7 +30,7 @@ const registry: NormalizationTargetRegistry = { const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const projectRoot = async (files: Readonly>): Promise => { diff --git a/packages/agent-bundle/tests/route-caller-input-types.test.ts b/packages/agent-bundle/tests/route-caller-input-types.test.ts index 131276485..99a5de9a8 100644 --- a/packages/agent-bundle/tests/route-caller-input-types.test.ts +++ b/packages/agent-bundle/tests/route-caller-input-types.test.ts @@ -1,6 +1,6 @@ import { Client } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; -import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -8,11 +8,12 @@ import { afterEach, expect, it } from '@rstest/core'; import ts from 'typescript-5'; import { build, validate } from '../src/api.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const writeProjectFile = async (root: string, path: string, contents: string): Promise => { diff --git a/packages/agent-bundle/tests/route-contract-imports.test.ts b/packages/agent-bundle/tests/route-contract-imports.test.ts index 1f27c6b4c..cd4f87353 100644 --- a/packages/agent-bundle/tests/route-contract-imports.test.ts +++ b/packages/agent-bundle/tests/route-contract-imports.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { promisify } from 'node:util'; @@ -15,12 +15,13 @@ import type { CompiledRouteGraph, RouteInputSchema, } from '../src/routes/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const writeProjectFile = async (root: string, path: string, contents: string): Promise => { diff --git a/packages/agent-bundle/tests/route-graph.test.ts b/packages/agent-bundle/tests/route-graph.test.ts index 7c03be454..6674fb150 100644 --- a/packages/agent-bundle/tests/route-graph.test.ts +++ b/packages/agent-bundle/tests/route-graph.test.ts @@ -1,5 +1,5 @@ import { mkdirSync, unlinkSync } from 'node:fs'; -import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, realpath, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -15,11 +15,12 @@ import type { AgentBundleConfig } from '../src/core/types.ts'; import { compileRouteGraph, emptyCompiledRouteGraph, isEmptyRouteGraph } from '../src/routes/graph.ts'; import * as routesModule from '../src/routes/index.ts'; import { emptyRouteConfig, type CompiledAgentRoute, type CompiledRouteGraph } from '../src/routes/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const createRoot = async (): Promise => { diff --git a/packages/agent-bundle/tests/route-invocation-dev-server.test.ts b/packages/agent-bundle/tests/route-invocation-dev-server.test.ts index fc41aea6d..8cd203853 100644 --- a/packages/agent-bundle/tests/route-invocation-dev-server.test.ts +++ b/packages/agent-bundle/tests/route-invocation-dev-server.test.ts @@ -27,6 +27,7 @@ import { createProjectFixture } from './helpers/project-fixture.ts'; import { agentBundleNodeModules } from './helpers/workspace-paths.ts'; import { replaceWatchedSourceAndAwaitRebuild } from './support/watched-files.ts'; import { runNodeScript } from './support/run-node-script.ts'; +import { removeTree } from './support/remove-tree.ts'; const readEvent = async ( response: Response, @@ -1452,7 +1453,7 @@ it('invokes compiled tool and event routes through the foreground server', { tim }); } finally { await server?.close().catch(() => undefined); - await rm(project.root, { force: true, maxRetries: 5, recursive: true, retryDelay: 50 }); + await removeTree(project.root); } }); @@ -1554,7 +1555,7 @@ it('fails closed when a valid host is ineligible for the compiled event route', expect(existsSync(handlerMarker)).toBe(false); } finally { await server?.close().catch(() => undefined); - await rm(project.root, { force: true, maxRetries: 5, recursive: true, retryDelay: 50 }); + await removeTree(project.root); } }); @@ -1688,7 +1689,7 @@ it('enforces compiled handler, MCP schemas, and operator env across production s expect(denied.invocation.providers).toEqual([]); } finally { await server?.close().catch(() => undefined); - await rm(project.root, { force: true, maxRetries: 5, recursive: true, retryDelay: 50 }); + await removeTree(project.root); } }); @@ -1851,7 +1852,7 @@ it('publishes invocation routes only after a successful initial or recovered bui }); } finally { await server?.close().catch(() => undefined); - await rm(project.root, { force: true, maxRetries: 5, recursive: true, retryDelay: 50 }); + await removeTree(project.root); } }); @@ -1995,6 +1996,6 @@ it('bounds the render history a compiled child produces by count and bytes acros expect(renderEvents(live.seen).length).toBeGreaterThan(routeInvocationRenderHistoryLimits.maxEvents); } finally { await server?.close().catch(() => undefined); - await rm(project.root, { force: true, maxRetries: 5, recursive: true, retryDelay: 50 }); + await removeTree(project.root); } }); diff --git a/packages/agent-bundle/tests/route-invocation-service.test.ts b/packages/agent-bundle/tests/route-invocation-service.test.ts index 0c2a57eb5..26cecb13d 100644 --- a/packages/agent-bundle/tests/route-invocation-service.test.ts +++ b/packages/agent-bundle/tests/route-invocation-service.test.ts @@ -1,5 +1,5 @@ import { existsSync, readFileSync } from 'node:fs'; -import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -32,6 +32,7 @@ import { testManifestFromRouteGraph } from '../src/test/manifest.ts'; import { expectDocument } from '../src/test/matchers.ts'; import { isProcessGone } from './support/bin-process.ts'; import { deferred } from './support/eventually.ts'; +import { removeTree } from './support/remove-tree.ts'; const invocation = (id: string, completedAt: string): RouteInvocation => ({ completedAt, @@ -1044,7 +1045,7 @@ it('does not spawn a child for an invocation aborted while queued', async () => } finally { hold.resolve(); await service.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1335,7 +1336,7 @@ it('resolves a `.js` import of a `.tsx` sibling without rewriting the same strin .toContainText('panel rendered') .toContainText('./panel.js'); } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); @@ -1398,7 +1399,7 @@ it('bounds a real child\'s long, heavy render stream end to end', { timeout: 60_ expect(replay.at(-1)).toEqual({ invocation, type: 'final' }); } finally { await service.close(); - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); @@ -1431,7 +1432,7 @@ it('reaps the render child and its descendants after a successful reply', { time expect(alive(pids.child)).toBe(false); expect(alive(pids.descendant)).toBe(false); } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); @@ -1451,7 +1452,7 @@ it('reaps the render child and its descendants when the invocation times out', { expect(alive(pids.child)).toBe(false); expect(alive(pids.descendant)).toBe(false); } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); @@ -1470,7 +1471,7 @@ it('reaps the render child and its descendants when the invocation is cancelled' expect(alive(pids.descendant)).toBe(false); expect(await started.result).toBe(cancelled); } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); @@ -1491,7 +1492,7 @@ it('reaps the render child and its descendants when the service closes mid-rende status: 'failed', }); } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); @@ -1621,7 +1622,7 @@ it('forwards kernel events from tool and event routes rendered in the real child ]); } finally { await service.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/route-register-typegen.test.ts b/packages/agent-bundle/tests/route-register-typegen.test.ts index 841ce590f..4926aabd2 100644 --- a/packages/agent-bundle/tests/route-register-typegen.test.ts +++ b/packages/agent-bundle/tests/route-register-typegen.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -6,11 +6,12 @@ import { afterEach, expect, it } from '@rstest/core'; import ts from 'typescript-5'; import { inspect } from '../src/api.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const writeProjectFile = async (root: string, path: string, contents: string): Promise => { diff --git a/packages/agent-bundle/tests/route-task-support.test.ts b/packages/agent-bundle/tests/route-task-support.test.ts index cc461c9b8..2145c0f4a 100644 --- a/packages/agent-bundle/tests/route-task-support.test.ts +++ b/packages/agent-bundle/tests/route-task-support.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, realpath, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -7,6 +7,7 @@ import { afterEach, describe, expect, it } from '@rstest/core'; import type { AgentBundleConfig } from '../src/core/types.ts'; import { compileRouteGraph } from '../src/routes/graph.ts'; import { routeTaskSupport, toolTaskSupportValues } from '../src/routes/task-support.ts'; +import { removeTree } from './support/remove-tree.ts'; /** * `config.execution.taskSupport` (#369): the compiler validates the value once @@ -18,7 +19,7 @@ import { routeTaskSupport, toolTaskSupportValues } from '../src/routes/task-supp const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const createRoot = async (): Promise => { diff --git a/packages/agent-bundle/tests/route-typegen-write.test.ts b/packages/agent-bundle/tests/route-typegen-write.test.ts index 0b0c49a3c..d8b76152e 100644 --- a/packages/agent-bundle/tests/route-typegen-write.test.ts +++ b/packages/agent-bundle/tests/route-typegen-write.test.ts @@ -1,4 +1,4 @@ -import { access, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { access, mkdtemp, readdir, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -9,6 +9,7 @@ import { runWithPlatform } from '../src/effect/platform.ts'; import { emptyCompiledRouteGraph } from '../src/routes/graph.ts'; import { generateRouteTypes, routeTypesRelativePath, writeRouteTypes, writeRouteTypesProgram } from '../src/routes/typegen.ts'; import type { CompiledRouteGraph } from '../src/routes/types.ts'; +import { removeTree } from './support/remove-tree.ts'; /** * `writeRouteTypes` publishes `.agent-bundle/routes.d.ts` with a @@ -20,7 +21,7 @@ import type { CompiledRouteGraph } from '../src/routes/types.ts'; */ const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const scratchRoot = async (): Promise => { diff --git a/packages/agent-bundle/tests/route-unit/lifecycle-replay.test.ts b/packages/agent-bundle/tests/route-unit/lifecycle-replay.test.ts index b2010cef2..e5109efba 100644 --- a/packages/agent-bundle/tests/route-unit/lifecycle-replay.test.ts +++ b/packages/agent-bundle/tests/route-unit/lifecycle-replay.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -12,11 +12,12 @@ import { projectEventDocument } from '../../src/events/project.ts'; import { compileRouteGraph } from '../../src/routes/graph.ts'; import { renderRouteEvents } from '../../src/test/render.ts'; import type { AgentRouteModule } from '../../src/test/types.ts'; +import { removeTree } from '../support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const writeProjectFile = async (root: string, path: string, contents: string): Promise => { diff --git a/packages/agent-bundle/tests/route-unit/route-module-loader.test.ts b/packages/agent-bundle/tests/route-unit/route-module-loader.test.ts index 97da0a72a..1b9307b91 100644 --- a/packages/agent-bundle/tests/route-unit/route-module-loader.test.ts +++ b/packages/agent-bundle/tests/route-unit/route-module-loader.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -8,6 +8,7 @@ import { createRouteModuleLoader } from '../../src/dev/routes/route-module-loade import { expectDocument } from '../../src/test/matchers.ts'; import { renderRouteEvents } from '../../src/test/render.ts'; import type { AgentRouteModule } from '../../src/test/types.ts'; +import { removeTree } from '../support/remove-tree.ts'; const files: Readonly> = { 'count.ts': "export const count = 'from count.ts';\n", @@ -61,7 +62,7 @@ beforeAll(async () => { }); afterAll(async () => { - await rm(root, { force: true, recursive: true }); + await removeTree(root); }); it('resolves a `.js` import whose source is a `.tsx` component and renders the module', async () => { diff --git a/packages/agent-bundle/tests/route-unit/workbench-surface-rendered-skill.test.ts b/packages/agent-bundle/tests/route-unit/workbench-surface-rendered-skill.test.ts index f932219c6..86e78b6f2 100644 --- a/packages/agent-bundle/tests/route-unit/workbench-surface-rendered-skill.test.ts +++ b/packages/agent-bundle/tests/route-unit/workbench-surface-rendered-skill.test.ts @@ -1,4 +1,4 @@ -import { mkdir, rm, symlink } from 'node:fs/promises'; +import { mkdir, symlink } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; @@ -6,6 +6,7 @@ import { afterAll, expect, it } from '@rstest/core'; import { inspectWorkbenchSurface, workbenchLeafPath } from '../../src/test/index.ts'; import { createProjectFixture } from '../helpers/project-fixture.ts'; +import { removeTree } from '../support/remove-tree.ts'; /** * The route-unit pool runs under `--conditions=react-server`, so a rendered @@ -19,7 +20,7 @@ const reactPackageRoot = dirname(createRequire(import.meta.url).resolve('react/p const roots: string[] = []; afterAll(async () => { - await Promise.all(roots.map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.map((root) => removeTree(root))); }); it('inspects the Workbench surface of a project with a rendered skill under the react-server condition (#441)', async () => { diff --git a/packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts b/packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts index d48771e26..ebc748d23 100644 --- a/packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts +++ b/packages/agent-bundle/tests/rsc-runtime-optional-packaging.test.ts @@ -1,6 +1,6 @@ import { execFile as executeFile } from 'node:child_process'; import { existsSync } from 'node:fs'; -import { cp, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises'; +import { cp, mkdtemp, readdir, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { promisify } from 'node:util'; @@ -8,6 +8,7 @@ import { promisify } from 'node:util'; import { describe, expect, it } from '@rstest/core'; import { cachedNpmInstallArguments, installedEnvironment, sharedPackedTarball } from './support/shared-pack.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); @@ -107,7 +108,7 @@ describe.sequential('optional RSC runtime package boundary', () => { expect(await namedFiles(consumer, '.runtime-provider-loaded')).toEqual([]); expect(await namedFiles(project, '.runtime-provider-loaded')).toEqual([]); } finally { - await rm(consumer, { force: true, recursive: true }); + await removeTree(consumer); } }, 120_000); }); diff --git a/packages/agent-bundle/tests/rsc-runtime-topology-script.test.ts b/packages/agent-bundle/tests/rsc-runtime-topology-script.test.ts index 74cb96bcd..725193477 100644 --- a/packages/agent-bundle/tests/rsc-runtime-topology-script.test.ts +++ b/packages/agent-bundle/tests/rsc-runtime-topology-script.test.ts @@ -1,10 +1,11 @@ import { execFile as executeFile } from 'node:child_process'; -import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { promisify } from 'node:util'; import { describe, expect, it } from '@rstest/core'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); @@ -109,7 +110,7 @@ describe('rsc runtime topology script', () => { await run(root); await expect(run(root, true)).resolves.toBeDefined(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); }); diff --git a/packages/agent-bundle/tests/rstest-meta-alias.test.ts b/packages/agent-bundle/tests/rstest-meta-alias.test.ts index 0d81a8a4a..704333828 100644 --- a/packages/agent-bundle/tests/rstest-meta-alias.test.ts +++ b/packages/agent-bundle/tests/rstest-meta-alias.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { mkdtemp, readFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -19,6 +19,7 @@ import { agentBundleRstest } from '../src/rstest/index.ts'; import { metaModuleAliasKey, testMetaModuleSource } from '../src/rstest/meta-module.ts'; import { FALLBACK_PLUGIN_IDENTITY, isFallbackPluginIdentity, testManifestFromRouteGraph } from '../src/test/manifest.ts'; import { emptyCompiledRouteGraph } from '../src/routes/graph.ts'; +import { removeTree } from './support/remove-tree.ts'; const fixtureRoot = resolve(import.meta.dirname, '../fixtures/meta-consumer'); const metaModulePath = resolve(fixtureRoot, '.agent-bundle', 'test', 'meta.mjs'); @@ -94,7 +95,7 @@ describe('agentBundleRstest aliases agent-bundle/meta (#386)', () => { expect(error.recovery).toContain('Fix the compiler diagnostics'); expect(error.message).not.toContain('ReferenceError'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/rstest-worker-isolation.test.ts b/packages/agent-bundle/tests/rstest-worker-isolation.test.ts index 7d6a7c82a..35ee3a358 100644 --- a/packages/agent-bundle/tests/rstest-worker-isolation.test.ts +++ b/packages/agent-bundle/tests/rstest-worker-isolation.test.ts @@ -1,5 +1,5 @@ import { realpathSync } from 'node:fs'; -import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readdir, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { isAbsolute, join } from 'node:path'; @@ -17,6 +17,7 @@ import { rstestWorkerRootOwner, rstestWorkerRootPath, } from '../../../rstest.worker-isolation.ts'; +import { removeTree } from './support/remove-tree.ts'; it('keeps Doctor socket fixtures below the Linux AF_UNIX pathname cap', () => { const longLocalCiRoot = join( @@ -135,6 +136,6 @@ it('removes only the finished roots owned by one host temporary root', async () await expect(removeOwnedRstestWorkerRoots({ parent: join(parent, 'missing'), temporaryRoot: legTmp })) .resolves.toEqual({ removed: [], retained: [] }); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); diff --git a/packages/agent-bundle/tests/rstest-worker-root-teardown.test.ts b/packages/agent-bundle/tests/rstest-worker-root-teardown.test.ts index 3f5f63982..2f34994db 100644 --- a/packages/agent-bundle/tests/rstest-worker-root-teardown.test.ts +++ b/packages/agent-bundle/tests/rstest-worker-root-teardown.test.ts @@ -13,6 +13,7 @@ import { rstestWorkerRootPrefix, } from '../../../scripts/rstest-worker-roots.mjs'; import { rstestWorkerRoot, rstestWorkerRootOwner } from '../../../rstest.worker-isolation.ts'; +import { removeTree } from './support/remove-tree.ts'; /** * The pool teardown (rstest.global-setup.ts) removes the worker roots of one @@ -140,6 +141,6 @@ it('removes only the finished roots that carry one run id', async () => { await expect(removeRunRstestWorkerRoots({ parent: join(parent, 'missing'), runId })) .resolves.toEqual({ removed: [], retained: [] }); } finally { - await rm(parent, { force: true, recursive: true }); + await removeTree(parent); } }); diff --git a/packages/agent-bundle/tests/rule-config.test.ts b/packages/agent-bundle/tests/rule-config.test.ts index f0e735b0a..4e77efc4b 100644 --- a/packages/agent-bundle/tests/rule-config.test.ts +++ b/packages/agent-bundle/tests/rule-config.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, mkdir, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -15,6 +15,7 @@ import { type RuleDocument, } from '../src/config/index.ts'; import type { AgentBundleConfig } from '../src/core/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const loadedProject = ( root: string, @@ -44,7 +45,7 @@ const withProject = async ( ); await run(root); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; @@ -193,7 +194,7 @@ it('discovers flat non-ignored rules deterministically and omits the collection join(root, 'src', 'rules', 'zeta.mdc'), ]); - await rm(join(root, 'src', 'rules'), { recursive: true }); + await removeTree(join(root, 'src', 'rules')); expect(await discoverProject(root, config)).not.toHaveProperty('rules'); }); }); diff --git a/packages/agent-bundle/tests/runtime-generation-store.test.ts b/packages/agent-bundle/tests/runtime-generation-store.test.ts index a0bb74d3e..04c7801de 100644 --- a/packages/agent-bundle/tests/runtime-generation-store.test.ts +++ b/packages/agent-bundle/tests/runtime-generation-store.test.ts @@ -25,6 +25,7 @@ import { type RuntimeGenerationMetadataCodec, type RuntimeGenerationValidationInput, } from '../src/dev/index.ts'; +import { removeTree } from './support/remove-tree.ts'; interface TestMetadata { readonly label: string; @@ -293,7 +294,7 @@ it('prepares an opaque validated generation without publishing it before synchro expect(store.active()).toMatchObject({ id: 'g1' }); } finally { await store.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -369,14 +370,14 @@ it('rejects incomplete, altered, unsafe, and invalid candidate inputs', async () .rejects.toMatchObject({ code: 'RUNTIME_GENERATION_INVALID' }); } finally { await malformedStore.store.close().catch(() => undefined); - await rm(malformedStore.root, { force: true, recursive: true }); + await removeTree(malformedStore.root); } await expect(store.prepare(malformedCandidate, malformed.manifest, { guard: { check: () => false, wait: async () => undefined }, })).rejects.toMatchObject({ code: 'RUNTIME_GENERATION_SUPERSEDED' }); } finally { await store.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -394,7 +395,7 @@ it('rejects a provider metadata validator failure without publishing the candida expect(store.active()).toBeUndefined(); } finally { await store.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -446,7 +447,7 @@ it('revalidates reopened metadata before accepting a required entry redirected t expect(store.active()).toBeUndefined(); } finally { await store.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -498,7 +499,7 @@ it('revalidates reopened metadata before accepting internally inconsistent descr expect(store.active()).toBeUndefined(); } finally { await store.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -525,7 +526,7 @@ it('fences superseded candidates and keeps the last good active generation after expect(store.active()?.id).toBe('g3'); } finally { await store.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -551,7 +552,7 @@ it('serializes concurrent begins into distinct monotonic candidate sequences', a expect(store.active()?.id).toBe('g2'); } finally { await store.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -571,7 +572,7 @@ it('pins explicit leases to committed generations, defaults implicit leases to a await implicit.release(); } finally { await store.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -593,7 +594,7 @@ it('retains active plus five newest inactive generations and defers a leased pru } } finally { await store.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -606,11 +607,11 @@ it('does not admit an explicit lease after pruning has synchronously reserved it if (path.endsWith('/g1')) { removalStarted.resolve(); await allowRemoval.promise; - await rm(path, { force: true, recursive: true }); + await removeTree(path); removalFinished.resolve(); return; } - await rm(path, { force: true, recursive: true }); + await removeTree(path); }, retainInactive: 5, }); @@ -628,7 +629,7 @@ it('does not admit an explicit lease after pruning has synchronously reserved it } finally { allowRemoval.resolve(); await store.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -657,7 +658,7 @@ it('aborts prepared roots, removes abandoned session roots on reopen, and report await reopened.close(); } finally { await store.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } const failedRoot = await mkdtemp(join(tmpdir(), 'agent-bundle-runtime-generations-close-')); @@ -665,7 +666,7 @@ it('aborts prepared roots, removes abandoned session roots on reopen, and report metadataCodec, remove: async (path) => { if (path.endsWith('/prepared')) throw new Error('cleanup refused'); - await rm(path, { force: true, recursive: true }); + await removeTree(path); }, storageRoot: failedRoot, validateMetadata: (input) => input.metadata, @@ -683,7 +684,7 @@ it('aborts prepared roots, removes abandoned session roots on reopen, and report .rejects.toMatchObject({ code: 'RUNTIME_GENERATION_CLOSED' }); await expect(failingStore.lease()).rejects.toMatchObject({ code: 'RUNTIME_GENERATION_CLOSED' }); } finally { - await rm(failedRoot, { force: true, recursive: true }); + await removeTree(failedRoot); } }); @@ -705,7 +706,7 @@ it('drains an in-flight abort cleanup before the same close aggregates its failu await releaseAbortRemoval.promise; throw new Error('abort cleanup refused'); } - await rm(path, { force: true, recursive: true }); + await removeTree(path); }, storageRoot: root, validateMetadata: (input) => input.metadata, @@ -739,7 +740,7 @@ it('drains an in-flight abort cleanup before the same close aggregates its failu releaseAbortRemoval.resolve(); await abort?.catch(() => undefined); await close?.catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -767,7 +768,7 @@ it('runs synchronous guard checks directly after both asynchronous guard waits', await expectMissing(join(root, 'generations', 'guarded')); } finally { await store.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -792,7 +793,7 @@ it('keeps a candidate non-public when the pre-rename guard changes in its wait/c await expect(store.lease('pre-guard-race')).rejects.toMatchObject({ code: 'RUNTIME_GENERATION_NOT_FOUND' }); } finally { await store.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -820,7 +821,7 @@ it('keeps a candidate non-public when the post-rename guard changes in its wait/ await expectMissing(join(root, 'generations', 'post-guard-race')); } finally { await store.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -832,7 +833,7 @@ it('drains an admitted post-rename prepare before close removes its owned roots' const created = await createStore({ remove: async (path) => { if (closing) closeRemovals.push(path); - await rm(path, { force: true, recursive: true }); + await removeTree(path); }, }); const { root, store } = created; @@ -880,7 +881,7 @@ it('drains an admitted post-rename prepare before close removes its owned roots' releasePrepare.resolve(); await prepare?.catch(() => undefined); await close?.catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -895,7 +896,7 @@ it('reports a late post-rename prepare cleanup failure from the close that drain remove: async (path) => { if (closing) closeRemovals.push(path); if (path === join(root, 'generations', 'late-failure')) throw new Error('late cleanup refused'); - await rm(path, { force: true, recursive: true }); + await removeTree(path); }, storageRoot: root, validateMetadata: (input) => input.metadata, @@ -934,7 +935,7 @@ it('reports a late post-rename prepare cleanup failure from the close that drain releasePrepare.resolve(); await prepare?.catch(() => undefined); await close?.catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -950,7 +951,7 @@ it('shares one in-flight close failure across concurrent callers and remains ide await allowFailure.promise; throw new Error('deferred cleanup refused'); } - await rm(path, { force: true, recursive: true }); + await removeTree(path); }, storageRoot: root, validateMetadata: (input) => input.metadata, @@ -975,7 +976,7 @@ it('shares one in-flight close failure across concurrent callers and remains ide await expect(store.close()).resolves.toBeUndefined(); } finally { allowFailure.resolve(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/runtime-mcp-registry.test.ts b/packages/agent-bundle/tests/runtime-mcp-registry.test.ts index 7c10f3f7f..28d0668de 100644 --- a/packages/agent-bundle/tests/runtime-mcp-registry.test.ts +++ b/packages/agent-bundle/tests/runtime-mcp-registry.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, rm } from 'node:fs/promises'; +import { mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -21,6 +21,7 @@ import { type RuntimeMcpExecutionContext, type RuntimeMcpExecutionValue, } from '../src/dev/index.ts'; +import { removeTree } from './support/remove-tree.ts'; const deferred = (): Readonly<{ readonly promise: Promise; @@ -240,7 +241,7 @@ const createGenerationStore = async (retainInactive?: number): Promise = { close: async () => { await store.close().catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); }, commit: async (id) => { const candidate = await store.begin({ id, sourceRevision: `source-${id}` }); diff --git a/packages/agent-bundle/tests/runtime-provider.test.ts b/packages/agent-bundle/tests/runtime-provider.test.ts index f02986043..8bef653b8 100644 --- a/packages/agent-bundle/tests/runtime-provider.test.ts +++ b/packages/agent-bundle/tests/runtime-provider.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdtemp, mkdir, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -22,6 +22,7 @@ import { DevRuntimeProviderLoadError, resolveDevRuntimeProvider, } from '../src/dev/runtime-provider-loader.ts'; +import { removeTree } from './support/remove-tree.ts'; const createProviderFixture = async (): Promise<{ readonly provider: string; @@ -1334,7 +1335,7 @@ it('loads one contained named runtime provider export with a frozen descriptor', expect(Object.isFrozen(provider.descriptor)).toBe(true); expect(Object.isFrozen(provider.descriptor.environmentVariables)).toBe(true); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1358,8 +1359,8 @@ it('rejects lexical, symlink, and directory provider escapes before importing', expect(imports).toBe(0); } finally { await Promise.all([ - rm(root, { force: true, recursive: true }), - rm(outside, { force: true, recursive: true }), + removeTree(root), + removeTree(outside), ]); } }); @@ -1386,7 +1387,7 @@ it('rejects missing exports and malformed provider descriptors without leaking e expect(error).toMatchObject({ code: 'AB8200' }); expect((error as Error).message).not.toContain('must-not-leak'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1401,7 +1402,7 @@ it('normalizes provider property accessor failures to the stable load error', as }), }))).rejects.toMatchObject({ code: 'AB8200' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1431,7 +1432,7 @@ it('retains the factory provider as the start method receiver', async () => { await expect((provider.start as unknown as () => Promise)()).resolves.toBe(1); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1471,7 +1472,7 @@ it('captures a named factory export only from an own data property', async () => await expect(resolveDevRuntimeProvider(root, { provider: './src/dev/provider.ts' }, async () => inheritedModule)) .rejects.toMatchObject({ code: 'AB8200' }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1502,7 +1503,7 @@ it('rejects sparse, accessor-backed, and extended descriptor environment lists w } expect(getterCalls).toBe(0); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -1528,6 +1529,6 @@ it('snapshots a dense environment list without reading its indexed values or len expect(provider.descriptor.environmentVariables).toEqual(['RUNTIME_TOKEN']); expect(lengthReads).toBe(0); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/script-playground-service.test.ts b/packages/agent-bundle/tests/script-playground-service.test.ts index fb1c247a5..05c6e1592 100644 --- a/packages/agent-bundle/tests/script-playground-service.test.ts +++ b/packages/agent-bundle/tests/script-playground-service.test.ts @@ -1,6 +1,6 @@ import type { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; -import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -9,12 +9,13 @@ import { expect, it } from '@rstest/core'; import { ScriptPlaygroundService } from '../src/dev/playground/script-playground-service.ts'; import { taskkill, terminateProcessTree, waitForProcessTreeExit } from '../src/services/process-tree.ts'; import { timeScale } from './support/time-scale.ts'; +import { removeTree } from './support/remove-tree.ts'; const temporaryScript = async (source: string): Promise Promise; readonly path: string }>> => { const root = await mkdtemp(join(tmpdir(), 'agent-bundle-script-playground-test-')); const path = join(root, 'review.mjs'); await writeFile(path, source); - return Object.freeze({ close: () => rm(root, { force: true, recursive: true }), path }); + return Object.freeze({ close: () => removeTree(root), path }); }; const eventually = async (assertion: () => Promise | void): Promise => { @@ -78,7 +79,7 @@ it('uses a fresh server-owned workspace and deletes it only after the child exit try { const service = new ScriptPlaygroundService({ createWorkspace: async () => Object.freeze({ - close: async () => { closed = true; await rm(workspace, { force: true, recursive: true }); }, + close: async () => { closed = true; await removeTree(workspace); }, path: workspace, }), resolveScript: async () => Object.freeze({ @@ -97,7 +98,7 @@ it('uses a fresh server-owned workspace and deletes it only after the child exit expect(closed).toBe(true); await expect(readFile(workspace)).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await Promise.allSettled([emitted.close(), rm(workspace, { force: true, recursive: true })]); + await Promise.allSettled([emitted.close(), removeTree(workspace)]); } }); @@ -127,7 +128,7 @@ it('preserves a successful script result when workspace release fails', async () stdout: 'completed', }); } finally { - await Promise.allSettled([emitted.close(), rm(workspace, { force: true, recursive: true })]); + await Promise.allSettled([emitted.close(), removeTree(workspace)]); } }); @@ -181,7 +182,7 @@ it('preserves timeout and cancellation identity when workspace release fails', a name: 'AbortError', }); } finally { - await Promise.allSettled([emitted.close(), rm(workspace, { force: true, recursive: true })]); + await Promise.allSettled([emitted.close(), removeTree(workspace)]); } }, 10_000 * timeScale); @@ -415,7 +416,7 @@ it('cancels and drains the emitted script process group before its workspace is try { const service = new ScriptPlaygroundService({ createWorkspace: async () => Object.freeze({ - close: async () => { workspaceClosed = true; await rm(workspace, { force: true, recursive: true }); }, path: workspace, + close: async () => { workspaceClosed = true; await removeTree(workspace); }, path: workspace, }), resolveScript: async () => Object.freeze({ interpreter: Object.freeze({ args: Object.freeze([]), command: process.execPath }), name: 'review', path: emitted.path, @@ -437,7 +438,7 @@ it('cancels and drains the emitted script process group before its workspace is expect(workspaceClosed).toBe(true); expect(() => process.kill(descendant, 0)).toThrow(); } finally { - await Promise.allSettled([emitted.close(), rm(root, { force: true, recursive: true }), rm(workspace, { force: true, recursive: true })]); + await Promise.allSettled([emitted.close(), removeTree(root), removeTree(workspace)]); } }, 10_000 * timeScale); @@ -479,7 +480,7 @@ it('keeps SIGKILL process-group cleanup alive after the direct child closes', as try { process.kill(descendant, 'SIGKILL'); } catch { /* The cleanup contract already terminated it. */ } } - await Promise.allSettled([emitted.close(), rm(root, { force: true, recursive: true })]); + await Promise.allSettled([emitted.close(), removeTree(root)]); } }, 10_000 * timeScale); @@ -551,7 +552,7 @@ const assertStubbornDescendantIsGoneAtSettlement = async ( try { process.kill(descendant, 'SIGKILL'); } catch { /* The cleanup contract already terminated it. */ } } - await Promise.allSettled([emitted.close(), rm(root, { force: true, recursive: true })]); + await Promise.allSettled([emitted.close(), removeTree(root)]); } }; diff --git a/packages/agent-bundle/tests/self-contained-bundler-config.test.ts b/packages/agent-bundle/tests/self-contained-bundler-config.test.ts index a9780ad5c..45bb3d3f9 100644 --- a/packages/agent-bundle/tests/self-contained-bundler-config.test.ts +++ b/packages/agent-bundle/tests/self-contained-bundler-config.test.ts @@ -2,7 +2,7 @@ import { createRsbuild } from '@rsbuild/core'; import { createRslib } from '@rslib/core'; import { expect, it } from '@rstest/core'; import { init, parse } from 'es-module-lexer/minimal'; -import { mkdir, mkdtemp, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, readdir, symlink, writeFile } from 'node:fs/promises'; import { isBuiltin } from 'node:module'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -20,6 +20,7 @@ import { } from '../src/build/rslib.ts'; import type { AgentBundleMeta } from '../src/meta.ts'; import { agentBundleNodeModules } from './helpers/workspace-paths.ts'; +import { removeTree } from './support/remove-tree.ts'; type RslibInspection = Awaited>['inspectConfig']>>; @@ -191,7 +192,7 @@ it('lowers a generated executable with only Node builtins external and inlines i await expect(readdir(join(root, 'dist'))).resolves.toEqual(['scripts']); await expect(readdir(join(root, 'dist', 'scripts'))).resolves.toEqual(['probe.mjs']); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }, 20_000); @@ -217,6 +218,6 @@ it('composes MCP App views as fully inlined web bundles with nothing externalize expect(externalDeclarations(bundler.externals)).toEqual([]); expect(bundler.output?.asyncChunks).toBe(false); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/serve-app.test.ts b/packages/agent-bundle/tests/serve-app.test.ts index e50316e38..a3977d582 100644 --- a/packages/agent-bundle/tests/serve-app.test.ts +++ b/packages/agent-bundle/tests/serve-app.test.ts @@ -1,4 +1,4 @@ -import { cp, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'; +import { cp, mkdtemp, readFile, symlink, writeFile } from 'node:fs/promises'; import { request as httpRequest } from 'node:http'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -16,6 +16,7 @@ import { import { MCP_APP_PROTOCOL_VERSION } from '../src/dev/mcp-apps/mcp-app-bridge.ts'; import { WEB_HOST_TOKEN_HEADER } from '../src/web-host/page.ts'; import { timeScale } from './support/time-scale.ts'; +import { removeTree } from './support/remove-tree.ts'; /** * `agent-bundle serve-app` end to end over a real packed server: build the @@ -92,7 +93,7 @@ beforeAll(async () => { }, 180_000 * timeScale); afterAll(async () => { - await rm(fixtureRoot, { force: true, recursive: true }); + await removeTree(fixtureRoot); }); /** Sends one request with an explicit `Host` header, which `fetch` would overwrite. */ diff --git a/packages/agent-bundle/tests/shared-metadata.test.ts b/packages/agent-bundle/tests/shared-metadata.test.ts index 849d53114..dcf13cd02 100644 --- a/packages/agent-bundle/tests/shared-metadata.test.ts +++ b/packages/agent-bundle/tests/shared-metadata.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, realpath, rename, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdtemp, realpath, rename, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -11,6 +11,7 @@ import { portableAdapter } from '../src/adapters/portable.ts'; import { normalizeProject, validateSource, type NormalizationTargetRegistry } from '../src/config/index.ts'; import type { LoadedConfig } from '../src/config/load.ts'; import type { AgentBundleConfig, AgentBundleSharedMetadata, NormalizedPlugin } from '../src/core/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const hosts = ['portable', 'claude', 'codex', 'cursor'] as const; @@ -33,7 +34,7 @@ const withProject = async ( } await run(root); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; diff --git a/packages/agent-bundle/tests/skill-document-service.test.ts b/packages/agent-bundle/tests/skill-document-service.test.ts index 01c4280b4..99419b3d2 100644 --- a/packages/agent-bundle/tests/skill-document-service.test.ts +++ b/packages/agent-bundle/tests/skill-document-service.test.ts @@ -1,4 +1,4 @@ -import { mkdir, readFile, rm, symlink, unlink, writeFile } from 'node:fs/promises'; +import { mkdir, readFile, symlink, unlink, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { expect, it } from '@rstest/core'; @@ -9,6 +9,7 @@ import { ProjectEventHub, startForegroundServer } from '../src/dev/index.ts'; import { ProjectService } from '../src/dev/project-service.ts'; import { SkillDocumentService } from '../src/dev/skill-document-service.ts'; import { createProjectFixture } from './helpers/project-fixture.ts'; +import { removeTree } from './support/remove-tree.ts'; class TrackingEpochStore extends EpochStore { acquisitions = 0; @@ -90,7 +91,7 @@ it('serves parsed source documents and exact source resources by a model-owned S expect(binary.body).toEqual(new Uint8Array([0, 255, 17, 9])); expect([...await readFile(join(root, 'src', 'skills', 'review', 'assets', 'pixel.bin'))]).toEqual([...binary.body]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -112,7 +113,7 @@ it('marks active Skill resources for download while preserving their exact bytes }); expect(new TextDecoder().decode(resource.body)).toBe('\n'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -154,7 +155,7 @@ it('serves the generated parser result and byte-identical resources while pinnin expect(binary.body).toEqual(new Uint8Array([0, 255, 17, 9])); expect(binary.contentType).toBe('application/octet-stream'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -188,8 +189,8 @@ it('reads generated documents from the acquired epoch reference root, never a si await expect(readFile(join(alternateSkill, 'SKILL.md'), 'utf8')).resolves.toContain('# Alternate'); } finally { await Promise.all([ - rm(protectedRoot, { force: true, recursive: true }), - rm(alternateRoot, { force: true, recursive: true }), + removeTree(protectedRoot), + removeTree(alternateRoot), ]); } }); @@ -243,7 +244,7 @@ it('serves only typed source Skill routes and rejects encoded resource separator await server.close(); } } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -268,7 +269,7 @@ it('rejects traversal and symlink resource mutations after exact model membershi code: 'SKILL_RESOURCE_UNAVAILABLE', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -288,6 +289,6 @@ it('releases every acquired epoch reference after generated document and resourc expect(epochStore.acquisitions).toBe(2); expect(epochStore.releases).toBe(2); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/skill-ir.test.ts b/packages/agent-bundle/tests/skill-ir.test.ts index 0e23e5171..d0b151555 100644 --- a/packages/agent-bundle/tests/skill-ir.test.ts +++ b/packages/agent-bundle/tests/skill-ir.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, realpath, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -28,6 +28,7 @@ import { skillTokenSpellings, type SkillHost, } from '../src/skills/tokens.ts'; +import { removeTree } from './support/remove-tree.ts'; const portableMarkdown = [ '---', @@ -51,7 +52,7 @@ const registry: NormalizationTargetRegistry = { const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const projectRoot = async (files: Readonly>): Promise => { diff --git a/packages/agent-bundle/tests/support/fake-host-cli/fake-host.mjs b/packages/agent-bundle/tests/support/fake-host-cli/fake-host.mjs index bb9f2c024..032325663 100644 --- a/packages/agent-bundle/tests/support/fake-host-cli/fake-host.mjs +++ b/packages/agent-bundle/tests/support/fake-host-cli/fake-host.mjs @@ -88,7 +88,7 @@ const handleHostCommand = async (host, args) => { const marketplace = await readJson(join(source, '.claude-plugin', 'marketplace.json')); const destination = join(cacheRoot(host), marketplace.name, plugin.name, plugin.version); await mkdir(dirname(destination), { recursive: true }); - await rm(destination, { force: true, recursive: true }); + await rm(destination, { force: true, maxRetries: 5, recursive: true, retryDelay: 50 }); await cp(source, destination, { recursive: true, verbatimSymlinks: true }); return; } diff --git a/packages/agent-bundle/tests/support/host-install.ts b/packages/agent-bundle/tests/support/host-install.ts index 047926a46..c8c205d11 100644 --- a/packages/agent-bundle/tests/support/host-install.ts +++ b/packages/agent-bundle/tests/support/host-install.ts @@ -50,6 +50,7 @@ import { } from './packed-native-smoke.ts'; import { diffTreeSnapshots, snapshotTree, treesIdentical } from './tree-snapshot.ts'; import { replaceWatchedSource } from './watched-files.ts'; +import { removeTree } from './remove-tree.ts'; const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); @@ -655,7 +656,7 @@ const buildFixtureProject = async (options: { await Promise.all(options.expectedPaths.map((path) => access(join(artifactRoot, path)))); return Object.freeze({ artifactRoot, cli, root }); } catch (error) { - await rm(root, { force: true, recursive: true }); + await removeTree(root); throw error; } }; @@ -719,7 +720,7 @@ export const buildPortableHostInstallFixture = async (options: { }; export const disposeHostInstallFixture = async (fixture: BuiltFixtureProject): Promise => { - await rm(fixture.root, { force: true, recursive: true }); + await removeTree(fixture.root); }; /** Proves initial host-owned installation followed by the host-specific development re-sync. */ @@ -897,7 +898,7 @@ export const runDevHostInstallProof = async ( } finally { await restarted?.close(); await manager.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; @@ -1001,7 +1002,7 @@ export const runInstalledHostContractMatrixProof = async ( session, }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; @@ -1102,7 +1103,7 @@ export const runClaudeHostInstallProof = async ( status: 'passed', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; @@ -1261,7 +1262,7 @@ export const runCodexHostInstallProof = async ( status: 'passed', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; @@ -1473,7 +1474,7 @@ const assertUnifiedBundleCursorInstall = async ( staticFindings: Object.freeze({ AB6027: 0, AB7320: 0 }), }); } finally { - await rm(home, { force: true, recursive: true }); + await removeTree(home); } }; @@ -1578,7 +1579,7 @@ export const runCursorHostInstallProof = async ( unifiedBundle, }); } finally { - await rm(home, { force: true, recursive: true }); + await removeTree(home); } }; @@ -1883,7 +1884,7 @@ export const runPortableHostInstallProof = async ( status: 'passed', }); } finally { - await rm(home, { force: true, recursive: true }); + await removeTree(home); } }; @@ -2411,7 +2412,7 @@ const runLiveHostScenario = async ( await client?.close().catch(() => undefined); await server?.close().catch(() => undefined); await appServer?.close().catch(() => undefined); - await rm(scenarioRoot, { force: true, recursive: true }); + await removeTree(scenarioRoot); } }; @@ -2981,7 +2982,7 @@ export const runHostUninstallProof = async ( status: 'passed', }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; @@ -3047,7 +3048,7 @@ export const runPortableUninstallProof = async ( const missing = await runInstaller(['--uninstall']); assertProof(missing.exitCode === 1 && missing.stderr.includes('predates install receipts'), 'Portable uninstall without a receipt was not refused.'); await expectOk(['--uninstall', '--force'], `Uninstalled ${portablePlugin}@${version}`); - await rm(join(home, '.cursor', 'plugins'), { force: true, recursive: true }); + await removeTree(join(home, '.cursor', 'plugins')); await mkdir(destination, { recursive: true }); await writeFile(join(destination, 'payload.txt'), 'someone else\n'); const foreign = await runInstaller(['--uninstall', '--force']); @@ -3067,6 +3068,6 @@ export const runPortableUninstallProof = async ( status: 'passed', }); } finally { - await rm(home, { force: true, recursive: true }); + await removeTree(home); } }; diff --git a/packages/agent-bundle/tests/support/mcp-conformance.ts b/packages/agent-bundle/tests/support/mcp-conformance.ts index f7a1b4ce4..532d069f7 100644 --- a/packages/agent-bundle/tests/support/mcp-conformance.ts +++ b/packages/agent-bundle/tests/support/mcp-conformance.ts @@ -25,6 +25,7 @@ import { dirname, join, resolve } from 'node:path'; import { build } from '../../src/api.ts'; import { runBoundedChildProcess } from '../../src/host-contracts/process.ts'; +import { removeTree } from './remove-tree.ts'; const runnerVersion = '0.1.16'; const specVersion = '2025-11-25'; @@ -315,7 +316,7 @@ export const runMcpConformance = async (): Promise => { const outputRoot = resolve( process.env['AGENT_BUNDLE_MCP_CONFORMANCE_OUTPUT'] ?? defaultOutputRoot, ); - await rm(outputRoot, { force: true, recursive: true }); + await removeTree(outputRoot); await mkdir(dirname(outputRoot), { recursive: true }); const expectedFailures = await readExpectedFailures(); @@ -328,8 +329,8 @@ export const runMcpConformance = async (): Promise => { // journey, so omit unrelated state, event, and CLI surfaces and narrow the // copied fixture config to its generated MCP routes. await Promise.all([ - rm(join(project, 'src/cli'), { force: true, recursive: true }), - rm(join(project, 'src/events'), { force: true, recursive: true }), + removeTree(join(project, 'src/cli')), + removeTree(join(project, 'src/events')), rm(join(project, 'src/state.ts'), { force: true }), writeFile(join(project, 'agent-bundle.config.ts'), [ 'export default {', @@ -411,6 +412,6 @@ export const runMcpConformance = async (): Promise => { return report; } finally { await bridge?.close(); - await rm(fixture, { force: true, recursive: true }); + await removeTree(fixture); } }; diff --git a/packages/agent-bundle/tests/support/packed-native-smoke.ts b/packages/agent-bundle/tests/support/packed-native-smoke.ts index 9acaba634..42193ff2c 100644 --- a/packages/agent-bundle/tests/support/packed-native-smoke.ts +++ b/packages/agent-bundle/tests/support/packed-native-smoke.ts @@ -8,7 +8,6 @@ import { readFile, readdir, realpath, - rm, writeFile, } from 'node:fs/promises'; import { homedir, tmpdir } from 'node:os'; @@ -19,6 +18,7 @@ import { promisify } from 'node:util'; // so it stays on npm's default metadata staleness checks. import { npmInstallArguments, packOutputFromJson, sharedPackedTarball } from './shared-pack.ts'; import { deepFreeze } from '../../src/core/freeze.ts'; +import { removeTree } from './remove-tree.ts'; const execFile = promisify(executeFile); @@ -376,7 +376,7 @@ export const runPackedClaudePluginProof = async (options: { version: versionNumber, }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; @@ -476,6 +476,6 @@ export const runPackedNativeSmoke = async (options: { package: { externalBinary: true, productionOnly: true, tarballs: 1 }, }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; diff --git a/packages/agent-bundle/tests/support/remove-tree.test.ts b/packages/agent-bundle/tests/support/remove-tree.test.ts new file mode 100644 index 000000000..7f431b01a --- /dev/null +++ b/packages/agent-bundle/tests/support/remove-tree.test.ts @@ -0,0 +1,39 @@ +import { mkdtemp, rm as removeDirectory, stat, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { expect, it } from '@rstest/core'; + +import { removeTree, type TreeRemoval } from './remove-tree.ts'; + +const emptyError = Object.assign(new Error('ENOTEMPTY: directory not empty, rmdir'), { code: 'ENOTEMPTY' }); + +it('removeTree deletes a directory after one ENOTEMPTY', async () => { + const root = await mkdtemp(join(tmpdir(), 'remove-tree-')); + await writeFile(join(root, 'kept.txt'), 'x\n'); + let declined = false; + const fs: TreeRemoval = { + rm: async (path, options) => { + if (!declined) { + declined = true; + throw emptyError; + } + await removeDirectory(path, options); + }, + }; + await removeTree(root, fs); + await expect(stat(root)).rejects.toMatchObject({ code: 'ENOENT' }); +}); + +it('removeTree surfaces a persistent ENOTEMPTY', async () => { + const root = await mkdtemp(join(tmpdir(), 'remove-tree-busy-')); + await writeFile(join(root, 'kept.txt'), 'x\n'); + const fs: TreeRemoval = { + rm: async () => { + throw emptyError; + }, + }; + await expect(removeTree(root, fs)).rejects.toBe(emptyError); + expect((await stat(root)).isDirectory()).toBe(true); + await removeTree(root); +}); diff --git a/packages/agent-bundle/tests/support/remove-tree.ts b/packages/agent-bundle/tests/support/remove-tree.ts new file mode 100644 index 000000000..e164d4329 --- /dev/null +++ b/packages/agent-bundle/tests/support/remove-tree.ts @@ -0,0 +1,30 @@ +import { rm as removeDirectory } from 'node:fs/promises'; + +const nodeRetryCodes = new Set(['EBUSY', 'EMFILE', 'ENFILE', 'ENOTEMPTY', 'EPERM']); +const maxRetries = 5; +const retryDelay = 50; + +export type TreeRemoval = { + readonly rm: (path: string, options: { readonly force: true; readonly recursive: true }) => Promise; +}; + +const delay = (milliseconds: number): Promise => new Promise((resolve) => { + setTimeout(resolve, milliseconds); +}); + +const defaultRemoval: TreeRemoval = { + rm: (path, options) => removeDirectory(path, options), +}; + +export const removeTree = async (path: string, fs: TreeRemoval = defaultRemoval): Promise => { + for (let attempt = 0; attempt <= maxRetries; attempt += 1) { + try { + await fs.rm(path, { force: true, recursive: true }); + return; + } catch (error) { + const code = typeof error === 'object' && error !== null && 'code' in error ? error.code : undefined; + if (attempt === maxRetries || typeof code !== 'string' || !nodeRetryCodes.has(code)) throw error; + await delay(retryDelay * (attempt + 1)); + } + } +}; diff --git a/packages/agent-bundle/tests/target-hook-contract.test.ts b/packages/agent-bundle/tests/target-hook-contract.test.ts index f3f349b73..e8085ed9e 100644 --- a/packages/agent-bundle/tests/target-hook-contract.test.ts +++ b/packages/agent-bundle/tests/target-hook-contract.test.ts @@ -1,6 +1,6 @@ import { supportedCapabilities } from './support/adapter-capabilities.ts'; import { spawn } from 'node:child_process'; -import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -20,6 +20,7 @@ import type { AgentBundleConfig, NormalizedHook, NormalizedPlugin } from '../src import type { CompiledEventHandler } from '../src/routes/types.ts'; import { build } from './support/build.ts'; import { emptyCompiledRouteGraph } from '../src/routes/graph.ts'; +import { removeTree } from './support/remove-tree.ts'; const eventHandler: CompiledEventHandler = Object.freeze({ provenance: Object.freeze({ kind: 'conventional', relativePath: 'src/events/tool/before.handler.ts' }), @@ -278,7 +279,7 @@ it('builds adapter-owned native hook event, layout, and wrapper source', async ( }, }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/target-mcp-runtime.test.ts b/packages/agent-bundle/tests/target-mcp-runtime.test.ts index 58536c51e..68bbac40b 100644 --- a/packages/agent-bundle/tests/target-mcp-runtime.test.ts +++ b/packages/agent-bundle/tests/target-mcp-runtime.test.ts @@ -1,5 +1,5 @@ import { supportedCapabilities } from './support/adapter-capabilities.ts'; -import { access, cp, mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; +import { access, cp, mkdtemp, mkdir, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -22,6 +22,7 @@ import { createMcpPathTokenResolver, resolveMcpPathTokens } from '../src/service import { McpService } from '../src/services/mcp-service.ts'; import { build } from './support/build.ts'; import { emptyCompiledRouteGraph } from '../src/routes/graph.ts'; +import { removeTree } from './support/remove-tree.ts'; const metadata = Object.freeze({ adapterRevision: 'test', @@ -462,6 +463,6 @@ it('delegates one-shot and persistent MCP operations to an injected target runti }); await Promise.all([session.close(), persistent.close()]); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/terminal-capability.test.ts b/packages/agent-bundle/tests/terminal-capability.test.ts index 606c4523c..c99024a09 100644 --- a/packages/agent-bundle/tests/terminal-capability.test.ts +++ b/packages/agent-bundle/tests/terminal-capability.test.ts @@ -1,7 +1,7 @@ import { openSync, closeSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { mkdtemp, rm } from 'node:fs/promises'; +import { mkdtemp } from 'node:fs/promises'; import { describe, expect, it } from '@rstest/core'; @@ -12,6 +12,7 @@ import { terminalColor, type TerminalStreamProbe, } from '../src/terminal-capability.ts'; +import { removeTree } from './support/remove-tree.ts'; /** A descriptor number no process holds open: `fstat` on it fails with EBADF. */ const CLOSED_FD = 1_000_003; @@ -91,7 +92,7 @@ describe('process terminal detection (#511)', () => { expect(forced.stdout).toEqual({ color: 'truecolor', columns: 100, kind: 'pipe' }); } finally { closeSync(fd); - await rm(directory, { force: true, recursive: true }); + await removeTree(directory); } }); diff --git a/packages/agent-bundle/tests/test-browser-rstest.test.ts b/packages/agent-bundle/tests/test-browser-rstest.test.ts index 8637727df..b2a6361c0 100644 --- a/packages/agent-bundle/tests/test-browser-rstest.test.ts +++ b/packages/agent-bundle/tests/test-browser-rstest.test.ts @@ -1,4 +1,4 @@ -import { cp, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { cp, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; @@ -9,6 +9,7 @@ import { AGENT_BROWSER_TEST_REGISTRY_SYMBOL_KEY, AGENT_BROWSER_TEST_REGISTRY_VERSION, } from '../src/test/browser-registry.ts'; +import { removeTree } from './support/remove-tree.ts'; const fixtureRoot = resolve(import.meta.dirname, '../fixtures/route-harness'); @@ -87,7 +88,7 @@ describe('agentBundleBrowserRstest', () => { 'MCP App "panel" has no browser mount host selected by the project.', ); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/trace-dev-server.test.ts b/packages/agent-bundle/tests/trace-dev-server.test.ts index cd3aaf35f..9fe5be851 100644 --- a/packages/agent-bundle/tests/trace-dev-server.test.ts +++ b/packages/agent-bundle/tests/trace-dev-server.test.ts @@ -1,5 +1,5 @@ import { spawn } from 'node:child_process'; -import { cp, mkdir, readFile, readdir, rm, symlink, writeFile } from 'node:fs/promises'; +import { cp, mkdir, readFile, readdir, symlink, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { expect, it } from '@rstest/core'; @@ -13,6 +13,7 @@ import { startDevServer } from '../src/dev/workbench-server.ts'; import { createProjectFixture } from './helpers/project-fixture.ts'; import { agentBundleNodeModules } from './helpers/workspace-paths.ts'; import { replaceWatchedSourceAndAwaitRebuild } from './support/watched-files.ts'; +import { removeTree } from './support/remove-tree.ts'; const runHook = ( entry: string, @@ -257,6 +258,6 @@ it('serves replay and live trace entries and lowers build failures', { timeout: await expect(readFile(receiptRecordPath, 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); } finally { await server?.close().catch(() => undefined); - await rm(project.root, { force: true, maxRetries: 5, recursive: true, retryDelay: 50 }); + await removeTree(project.root); } }); diff --git a/packages/agent-bundle/tests/uninstall.test.ts b/packages/agent-bundle/tests/uninstall.test.ts index df11d3b96..5694fe83b 100644 --- a/packages/agent-bundle/tests/uninstall.test.ts +++ b/packages/agent-bundle/tests/uninstall.test.ts @@ -22,6 +22,7 @@ import { uninstallBundle, type UninstallResult } from '../src/install/uninstall. import { captureCliTerminal } from './support/cli-terminal.ts'; import { writeInstallFixtureManifest } from './support/install-fixture.ts'; import { diffTreeSnapshots, snapshotTree, treesIdentical } from './support/tree-snapshot.ts'; +import { removeTree } from './support/remove-tree.ts'; interface CommandCall { readonly args: readonly string[]; @@ -189,7 +190,7 @@ it('uninstalls a Cursor local install through its receipt and leaves the home by // Rerun: idempotent no-op. expect(await uninstallBundle(options)).toMatchObject({ state: 'not-installed' }); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -259,10 +260,10 @@ it('keeps Cursor runtime state and unowned entries by default and purges state o // Reinstall beside the retained state (an install, not a replacement), then purge it with confirmation. await rm(join(destination, 'operator-notes.md')); - await rm(join(destination, 'scratch'), { recursive: true }); + await removeTree(join(destination, 'scratch')); // skills/ survived the uninstall (its unowned child kept it alive), so a reinstall would find it pre-existing // and not claim it; clear it so the reinstall owns its directories again. - await rm(join(destination, 'skills'), { recursive: true }); + await removeTree(join(destination, 'skills')); expect(await installBundle(options)).toMatchObject({ state: 'installed' }); expect(await readFile(join(destination, 'state', 'plugin.sqlite'), 'utf8')).toBe('durable\n'); const purged = await uninstallBundle({ ...options, confirmPurge: true, purgeData: true }); @@ -278,7 +279,7 @@ it('keeps Cursor runtime state and unowned entries by default and purges state o await expect(readdir(derivedStateRoot)).rejects.toMatchObject({ code: 'ENOENT' }); expect(diffTreeSnapshots(before, await snapshotTree(fixture.home))).toEqual({ added: [], changed: [], removed: [] }); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -325,7 +326,7 @@ it('keeps created host directories receipt-owned across a --keep-data cycle in a await mkdir(join(destination, 'state')); await writeFile(join(destination, 'state', 'plugin.sqlite'), 'durable\n'); expect((await uninstallBundle(options)).remnantReceipt).toBe(join(destination, installReceiptFile)); - await rm(join(destination, 'state'), { force: true, recursive: true }); + await removeTree(join(destination, 'state')); const emptyPlan = await uninstallBundle({ ...options, plan: true }); expect(emptyPlan).toMatchObject({ data: { outcome: 'absent', policy: 'keep' }, @@ -394,7 +395,7 @@ it('keeps created host directories receipt-owned across a --keep-data cycle in a await mkdir(join(pluginData, 'cache'), { recursive: true }); await writeFile(join(pluginData, 'cache', 'index.json'), '{}\n'); expect(await uninstallBundle(options)).toMatchObject({ data: { outcome: 'kept' }, remnantReceipt: join(destination, installReceiptFile) }); - await rm(join(pluginData, 'cache'), { force: true, recursive: true }); + await removeTree(join(pluginData, 'cache')); const emptiedByHand = await uninstallBundle(options); expect(emptiedByHand).toMatchObject({ data: { detail: expect.stringContaining('is empty and is pruned'), outcome: 'absent' }, receipt: { status: 'consumed' }, state: 'uninstalled' }); expect(emptiedByHand.removed.directories).toEqual(expect.arrayContaining([pluginData, join(cursorRoot, 'agent-bundle', 'plugin-data'), join(cursorRoot, 'agent-bundle'), destination])); @@ -417,8 +418,8 @@ it('keeps created host directories receipt-owned across a --keep-data cycle in a const linkedChild = await failureOf(uninstallBundle(options)); expect(linkedChild.diagnostics[0]).toMatchObject({ code: 'AB7007', target: 'cursor' }); expect(await readFile(join(outside, 'plugin-data', 'uninstall-fixture', 'cache.sqlite'), 'utf8')).toBe('elsewhere\n'); - await rm(join(cursorRoot, 'agent-bundle'), { force: true, recursive: true }); - await rm(outside, { force: true, recursive: true }); + await removeTree(join(cursorRoot, 'agent-bundle')); + await removeTree(outside); expect(await uninstallBundle(options)).toMatchObject({ state: 'uninstalled' }); expect(diffTreeSnapshots(before, await snapshotTree(fixture.home))).toEqual({ added: [], changed: [], removed: [] }); @@ -439,7 +440,7 @@ it('keeps created host directories receipt-owned across a --keep-data cycle in a expect(await readFile(join(elsewhere, 'note.txt'), 'utf8')).toBe('theirs\n'); expect(diffTreeSnapshots(before, await snapshotTree(fixture.home))).toEqual({ added: [], changed: [], removed: [] }); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -482,8 +483,8 @@ it('purges AGENT_BUNDLE_STATE_ROOT from the host MCP document the installed mani await expect(readdir(declaredStateRoot)).rejects.toMatchObject({ code: 'ENOENT' }); } finally { await Promise.all([ - rm(fixture.cleanupRoot, { force: true, recursive: true }), - rm(cleanupRoot, { force: true, recursive: true }), + removeTree(fixture.cleanupRoot), + removeTree(cleanupRoot), ]); } }); @@ -533,7 +534,7 @@ it('never purges a pre-existing declared state root or its unrelated sentinel', await uninstallBundle({ ...options, confirmPurge: true, purgeData: true }); expect(await readFile(sentinel, 'utf8')).toBe('keep\n'); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -561,7 +562,7 @@ it('prunes a newly marked explicit root when no runtime state was written', asyn expect(removed.remnantReceipt).toBeUndefined(); await expect(readdir(declaredRoot)).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -593,7 +594,7 @@ it('records an inaccessible declared root as unproven without failing installati root: join(blockedParent, 'state'), }]); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -627,7 +628,7 @@ it('rolls back earlier state markers when a later root cannot be recorded', asyn }); await expect(readdir(firstRoot)).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -660,7 +661,7 @@ it('purges only the install-time AGENT_BUNDLE_STATE_ROOT when the uninstall envi await expect(readdir(recordedRoot)).rejects.toMatchObject({ code: 'ENOENT' }); expect(await readFile(sentinel, 'utf8')).toBe('keep\n'); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -707,7 +708,7 @@ it('records and purges each server state root using its execution cwd', async () await expect(readdir(firstRoot)).rejects.toMatchObject({ code: 'ENOENT' }); await expect(readdir(relativeRoot)).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -748,7 +749,7 @@ it('retains a marked root when its marker is replaced by another install identit }); expect(await readFile(sentinel, 'utf8')).toBe('keep\n'); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -799,8 +800,8 @@ it('lets only the owning installation purge a root shared by two installs', asyn await expect(readdir(sharedRoot)).rejects.toMatchObject({ code: 'ENOENT' }); } finally { await Promise.all([ - rm(owner.cleanupRoot, { force: true, recursive: true }), - rm(observer.cleanupRoot, { force: true, recursive: true }), + removeTree(owner.cleanupRoot), + removeTree(observer.cleanupRoot), ]); } }); @@ -846,7 +847,7 @@ it('retains a marked root when a symlinked ancestor is retargeted', async () => }); expect(await readFile(sentinel, 'utf8')).toBe('keep\n'); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -886,7 +887,7 @@ it('refuses Cursor local uninstalls without proof of ownership unless forced, an state: 'uninstalled', }); expect((await readdir(destination)).sort()).toEqual([installReceiptFile, 'state']); - await rm(destination, { force: true, recursive: true }); + await removeTree(destination); // A receipt naming another plugin, or a directory that is not ours at all: refused even with --force. await installBundle(options); @@ -895,7 +896,7 @@ it('refuses Cursor local uninstalls without proof of ownership unless forced, an const otherPlugin = await failureOf(uninstallBundle({ ...options, force: true })); expect(otherPlugin.diagnostics[0]).toMatchObject({ code: 'AB7007', target: 'cursor' }); expect(otherPlugin.diagnostics[0]?.message).toContain('names plugin "someone-else"'); - await rm(destination, { force: true, recursive: true }); + await removeTree(destination); await mkdir(join(destination, '.cursor-plugin'), { recursive: true }); await writeJson(join(destination, '.cursor-plugin', 'plugin.json'), { name: 'uninstall-fixture', version: '1.2.3' }); await writeFile(join(destination, 'payload.txt'), 'someone else\n'); @@ -905,7 +906,7 @@ it('refuses Cursor local uninstalls without proof of ownership unless forced, an expect(await readFile(join(destination, 'payload.txt'), 'utf8')).toBe('someone else\n'); // A symlinked destination is never traversed. - await rm(destination, { force: true, recursive: true }); + await removeTree(destination); const elsewhere = join(fixture.cleanupRoot, 'elsewhere'); await mkdir(elsewhere); await symlink(elsewhere, destination); @@ -913,7 +914,7 @@ it('refuses Cursor local uninstalls without proof of ownership unless forced, an expect(linked.diagnostics[0]).toMatchObject({ code: 'AB7007' }); expect(await readdir(elsewhere)).toEqual([]); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -946,7 +947,7 @@ it('consumes a migrated format/1 Cursor receipt without a crash', async () => { }); expect(diffTreeSnapshots(before, await snapshotTree(fixture.home))).toEqual({ added: [], changed: [], removed: [] }); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -1051,7 +1052,7 @@ it('never derives legacy receipt purge ownership from the current environment', await expect(readFile(join(originalStateRoot, 'state.sqlite'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' }); expect(await readFile(currentSentinel, 'utf8')).toBe('unrelated\n'); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -1135,7 +1136,7 @@ it('removes a staged Cursor marketplace repository only when its HEAD matches th expect(await uninstallBundle({ ...options, force: true })).toMatchObject({ receipt: { status: 'forced-missing' }, state: 'uninstalled' }); // An orphaned receipt (repository already gone) is consumed quietly. await installBundle(options); - await rm(repo, { force: true, recursive: true }); + await removeTree(repo); expect(await uninstallBundle(options)).toMatchObject({ registrations: [{ action: 'already-absent', kind: 'cursor-marketplace-staging' }], removed: { files: [receiptPath] }, @@ -1151,7 +1152,7 @@ it('removes a staged Cursor marketplace repository only when its HEAD matches th await mkdir(join(cachedCopy, '.cursor-plugin'), { recursive: true }); await writeFile(join(cachedCopy, '.cursor-plugin', 'plugin.json'), JSON.stringify({ name: 'uninstall-fixture', version: '1.2.3' })); await writeFile(join(cachedCopy, '.cache-complete'), ''); - await rm(repo, { force: true, recursive: true }); + await removeTree(repo); const stagingGone = await uninstallBundle(options); expect(stagingGone.registrations).toEqual([ expect.objectContaining({ action: 'already-absent', kind: 'cursor-marketplace-staging' }), @@ -1159,7 +1160,7 @@ it('removes a staged Cursor marketplace repository only when its HEAD matches th ]); expect(stagingGone.nextSteps?.[0]).toContain('Customize -> Plugins'); expect(await readFile(join(cachedCopy, '.cache-complete'), 'utf8')).toBe(''); - await rm(join(cursorRoot, 'plugins'), { force: true, recursive: true }); + await removeTree(join(cursorRoot, 'plugins')); expect(diffTreeSnapshots(before, await snapshotTree(fixture.home))).toEqual({ added: [], changed: [], removed: [] }); // The bundle was rebuilt to a newer version after Cursor imported the staging: the imported copy carries the @@ -1177,10 +1178,10 @@ it('removes a staged Cursor marketplace repository only when its HEAD matches th ]); expect(afterRebuild.nextSteps?.[0]).toContain('Customize -> Plugins'); await writeJson(join(fixture.bundleRoot, '.cursor-plugin/plugin.json'), { name: 'uninstall-fixture', version: '1.2.3' }); - await rm(join(cursorRoot, 'plugins'), { force: true, recursive: true }); + await removeTree(join(cursorRoot, 'plugins')); expect(diffTreeSnapshots(before, await snapshotTree(fixture.home))).toEqual({ added: [], changed: [], removed: [] }); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }, 60_000); @@ -1301,7 +1302,7 @@ it.each([ ]); expect(await readInstallReceiptFile(receiptPath)).toBeUndefined(); // The simulated cache copy is host-owned residue in this unit test; everything Agent Bundle wrote is gone. - await rm(join(hostRoot, 'plugins'), { force: true, recursive: true }); + await removeTree(join(hostRoot, 'plugins')); expect(diffTreeSnapshots(before, await snapshotTree(hostRoot))).toEqual({ added: [], changed: [], removed: [] }); // Host says installed but no receipt: refused (AB7009) until --force, which uninstalls through the host CLI. @@ -1425,7 +1426,7 @@ it.each([ expect(sharedPurge.diagnostics[0]?.message).toContain('(scope project)'); expect(scoped.calls.map((call) => call.args.join(' '))).not.toContain(uninstall); expect(await readFile(join(installPath, 'state', 'plugin.sqlite'), 'utf8')).toBe('durable\n'); - await rm(installPath, { force: true, recursive: true }); + await removeTree(installPath); scoped.calls.length = 0; const otherScope = await uninstallBundle({ ...options, commandRunner: scoped.runner }); expect(otherScope.registrations.find((registration) => registration.kind === 'claude-marketplace')).toMatchObject({ @@ -1521,7 +1522,7 @@ it.each([ expect(receiptSharedPurge.diagnostics[0]?.message).toContain(`receipt ${elsewhere} (scope project in /elsewhere/project)`); expect(calls.map((call) => call.args.join(' '))).not.toContain(uninstall); expect(await readFile(join(installPath, 'state', 'plugin.sqlite'), 'utf8')).toBe('durable\n'); - await rm(installPath, { force: true, recursive: true }); + await removeTree(installPath); calls.length = 0; // --plan announces the move without writing it. const planMove = await uninstallBundle({ ...options, plan: true }); @@ -1575,9 +1576,9 @@ it.each([ { kind: 'claude-marketplace', name: 'uninstall-fixture-marketplace', scope: 'project' }, ], }); - await rm(installPath, { force: true, recursive: true }); + await removeTree(installPath); await rm(otherPlugin); - await rm(join(hostRoot, 'agent-bundle'), { force: true, recursive: true }); + await removeTree(join(hostRoot, 'agent-bundle')); installed = false; marketplaceRegistered = false; @@ -1606,7 +1607,7 @@ it.each([ .toContain('uninstall-fixture@uninstall-fixture-marketplace (scope project in /elsewhere/by-hand, per plugins/installed_plugins.json)'); expect(calls.map((call) => call.args.join(' '))).not.toContain(uninstall); expect(await readFile(join(installPath, 'state', 'plugin.sqlite'), 'utf8')).toBe('durable\n'); - await rm(installPath, { force: true, recursive: true }); + await removeTree(installPath); calls.length = 0; const byRegistry = await uninstallBundle(options); expect(byRegistry.registrations.find((registration) => registration.kind === 'claude-marketplace')).toMatchObject({ @@ -1636,7 +1637,7 @@ it.each([ detail: expect.stringContaining('other-fixture@uninstall-fixture-marketplace (scope local in /elsewhere/other, per plugins/installed_plugins.json)'), }); expect(calls.map((call) => call.args.join(' '))).not.toContain(removeMarketplace); - await rm(installPath, { force: true, recursive: true }); + await removeTree(installPath); installed = false; marketplaceRegistered = false; @@ -1664,7 +1665,7 @@ it.each([ }); expect(calls.map((call) => call.args.join(' '))).toContain(uninstall); expect(calls.map((call) => call.args.join(' '))).not.toContain(removeMarketplace); - await rm(join(hostRoot, 'plugins'), { force: true, recursive: true }); + await removeTree(join(hostRoot, 'plugins')); installed = false; marketplaceRegistered = false; } @@ -1691,7 +1692,7 @@ it.each([ expect(error.diagnostics[0]).toMatchObject({ code: 'AB7004', target: host }); expect(unusable.calls).toHaveLength(1); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -1747,7 +1748,7 @@ it('purges Claude durable state only when confirmed and reports the host-retaine await expect(readdir(join(installPath, 'state'))).rejects.toMatchObject({ code: 'ENOENT' }); await expect(readdir(dataDirectory)).rejects.toMatchObject({ code: 'ENOENT' }); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -1825,7 +1826,7 @@ it('never derives legacy host-receipt purge ownership from the current environme expect(await readFile(currentSentinel, 'utf8')).toBe('unrelated\n'); expect(await readFile(join(originalStateRoot, 'state.sqlite'), 'utf8')).toBe('original\n'); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -1877,7 +1878,7 @@ it('reacquires Claude marketplace ownership after a keep-data remnant reinstall' .toMatchObject({ action: 'removed' }); expect(marketplaceRegistered).toBe(false); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -1927,7 +1928,7 @@ it('keeps external Codex state while reporting in-tree state only for purge', as const scoped = await failureOf(uninstallBundle({ ...options, scope: 'project' })); expect(scoped.diagnostics[0]).toMatchObject({ code: 'AB7003', target: 'codex' }); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); @@ -1946,7 +1947,7 @@ it('rejects an uninstall mode for hosts other than Cursor before touching anythi expect(error.diagnostics).toMatchObject([{ code: 'AB7003', target: 'claude' }]); expect(calls).toEqual([]); } finally { - await rm(fixture.cleanupRoot, { force: true, recursive: true }); + await removeTree(fixture.cleanupRoot); } }); diff --git a/packages/agent-bundle/tests/watched-files-support.test.ts b/packages/agent-bundle/tests/watched-files-support.test.ts index 42d2ee38c..6c2c7f17f 100644 --- a/packages/agent-bundle/tests/watched-files-support.test.ts +++ b/packages/agent-bundle/tests/watched-files-support.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, rm } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it } from '@rstest/core'; import type { CompletedBuildAttempt, ProjectStatus, RunningBuildAttempt } from '../src/dev/types.ts'; import { replaceWatchedSourceAndAwaitRebuild } from './support/watched-files.ts'; +import { removeTree } from './support/remove-tree.ts'; const running = (id: string): RunningBuildAttempt => Object.freeze({ diagnostics: Object.freeze([]), @@ -56,7 +57,7 @@ describe('replaceWatchedSourceAndAwaitRebuild', () => { }); afterEach(async () => { - await rm(root, { force: true, recursive: true }); + await removeTree(root); }); it('returns the first completed attempt that was unknown before the write, after the write landed', async () => { diff --git a/packages/agent-bundle/tests/web-command.test.ts b/packages/agent-bundle/tests/web-command.test.ts index 919478929..b87f39fb9 100644 --- a/packages/agent-bundle/tests/web-command.test.ts +++ b/packages/agent-bundle/tests/web-command.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, realpath, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -13,11 +13,12 @@ import type { McpAppJsonValue } from '../src/contracts/mcp-apps.ts'; import type { AppSelection, AppSelectionSource, OpenAppRequest } from '../src/web-host/select-app.ts'; import type { StdioAppSession, StdioLaunch } from '../src/web-host/session.ts'; import { deferred, eventually } from './support/eventually.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const statusApp: WebManifestApp = Object.freeze({ diff --git a/packages/agent-bundle/tests/web-config.test.ts b/packages/agent-bundle/tests/web-config.test.ts index 7116258c2..885359f1b 100644 --- a/packages/agent-bundle/tests/web-config.test.ts +++ b/packages/agent-bundle/tests/web-config.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -10,6 +10,7 @@ import type { AgentBundleConfig, NormalizationTargetRegistry } from '../src/core import type { DiscoveredProject } from '../src/config/discover.ts'; import type { LoadedConfig } from '../src/config/load.ts'; import type { CompiledAgentRoute, CompiledRouteGraph } from '../src/routes/types.ts'; +import { removeTree } from './support/remove-tree.ts'; const root = '/workspace/web-config'; const configPath = `${root}/agent-bundle.config.ts`; @@ -290,7 +291,7 @@ it('keeps a conventional src/cli.ts executable when only web is configured', asy recovery: 'Move that executable\'s commands under src/cli/** so the framework generates the bin, or remove web.apps.', }]); } finally { - await rm(fixtureRoot, { force: true, recursive: true }); + await removeTree(fixtureRoot); } }); diff --git a/packages/agent-bundle/tests/web-host-launch-selection.test.ts b/packages/agent-bundle/tests/web-host-launch-selection.test.ts index 55bd19f6e..ab0f6860f 100644 --- a/packages/agent-bundle/tests/web-host-launch-selection.test.ts +++ b/packages/agent-bundle/tests/web-host-launch-selection.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, realpath, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -10,6 +10,7 @@ import { WebLaunchSelectionError, type SelectWebLaunchOptions, } from '../src/dev/web-host-launch-selection.ts'; +import { removeTree } from './support/remove-tree.ts'; const registry = createDefaultRegistry(); const roots: string[] = []; @@ -21,7 +22,7 @@ const artifactRoot = async (): Promise => { }; afterEach(async () => { - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const writeManifest = async (root: string, relativePath: string, servers: Readonly>): Promise => { diff --git a/packages/agent-bundle/tests/web-host-routes-unit.test.ts b/packages/agent-bundle/tests/web-host-routes-unit.test.ts index b3e895d2d..7e55c5cc0 100644 --- a/packages/agent-bundle/tests/web-host-routes-unit.test.ts +++ b/packages/agent-bundle/tests/web-host-routes-unit.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, realpath, writeFile } from 'node:fs/promises'; import { createServer, type Server } from 'node:http'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -11,6 +11,7 @@ import type { McpAppRoutePreviewService } from '../src/dev/mcp-apps/mcp-app-rout import type { McpSession } from '../src/dev/mcp-session/mcp-session.ts'; import type { McpSessionService } from '../src/dev/mcp-session/mcp-session-service.ts'; import { WebHostRoutes, type WebHostEpochSource } from '../src/dev/web-host-routes.ts'; +import { removeTree } from './support/remove-tree.ts'; const registry = createDefaultRegistry(); const roots: string[] = []; @@ -24,7 +25,7 @@ const artifactRoot = async (): Promise => { afterEach(async () => { await Promise.all(servers.splice(0).map((server) => new Promise((done) => server.close(done)))); - await Promise.all(roots.splice(0).map((root) => rm(root, { force: true, recursive: true }))); + await Promise.all(roots.splice(0).map((root) => removeTree(root))); }); const resourceUri = 'ui://status/status.html'; diff --git a/packages/agent-bundle/tests/web-launch.test.ts b/packages/agent-bundle/tests/web-launch.test.ts index 88d131470..8a5897ddc 100644 --- a/packages/agent-bundle/tests/web-launch.test.ts +++ b/packages/agent-bundle/tests/web-launch.test.ts @@ -1,4 +1,4 @@ -import { chmod, mkdir, mkdtemp, readdir, realpath, rm, stat, symlink, writeFile } from 'node:fs/promises'; +import { chmod, mkdir, mkdtemp, readdir, realpath, stat, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; @@ -10,6 +10,7 @@ import { pathTokens, pluginRootEnvAnchor } from '../src/core/types.ts'; import { installedWebDataRoot } from '../src/install/state-root.ts'; import { resolveWebLaunch, WebLaunchError, webPluginDataDirectory } from '../src/web-host/launch.ts'; import type { ArtifactManifestLaunch, WebManifestApp } from '../src/web-host/manifest.ts'; +import { removeTree } from './support/remove-tree.ts'; const roots: string[] = []; @@ -31,7 +32,7 @@ const homeRoot = async (): Promise => { afterEach(async () => { await Promise.all(roots.splice(0).map(async (root) => { await chmod(root, 0o755).catch(() => undefined); - await rm(root, { force: true, recursive: true }); + await removeTree(root); })); }); diff --git a/packages/agent-bundle/tests/web-manifest.test.ts b/packages/agent-bundle/tests/web-manifest.test.ts index e90065c12..8d2465d30 100644 --- a/packages/agent-bundle/tests/web-manifest.test.ts +++ b/packages/agent-bundle/tests/web-manifest.test.ts @@ -1,5 +1,5 @@ import { expect, it } from '@rstest/core'; -import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -11,6 +11,7 @@ import { type ArtifactManifestLaunch, type WebManifest, } from '../src/web-host/manifest.ts'; +import { removeTree } from './support/remove-tree.ts'; const validWeb = (): WebManifest => ({ apps: [{ @@ -115,7 +116,7 @@ const withDocument = async ( try { await run(path, (value) => writeFile(path, JSON.stringify(value))); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; diff --git a/packages/agent-bundle/tests/workbench-asset-cache.test.ts b/packages/agent-bundle/tests/workbench-asset-cache.test.ts index 661ee0ac5..12286d810 100644 --- a/packages/agent-bundle/tests/workbench-asset-cache.test.ts +++ b/packages/agent-bundle/tests/workbench-asset-cache.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -13,6 +13,7 @@ import { workbenchDocumentCacheControl, workbenchHashedAssetCacheControl, } from '../src/dev/workbench-assets.ts'; +import { removeTree } from './support/remove-tree.ts'; const status = (): ProjectStatus => ({ artifact: { state: 'missing' }, @@ -83,6 +84,6 @@ it('serves hashed workbench assets as immutable and documents as no-store', asyn expect(notices.headers.get('cache-control')).toBe('no-store'); } finally { await server.close(); - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts b/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts index 1485fc07e..5150b1b8d 100644 --- a/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts +++ b/packages/agent-bundle/tests/workbench-surface-dev-server.test.ts @@ -1,4 +1,4 @@ -import { mkdir, rm, symlink, writeFile } from 'node:fs/promises'; +import { mkdir, symlink, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { expect, it } from '@rstest/core'; @@ -10,6 +10,7 @@ import { startDevServer } from '../src/dev/workbench-server.ts'; import { inspectWorkbenchSurface, workbenchLeafPath } from '../src/test/index.ts'; import { createProjectFixture } from './helpers/project-fixture.ts'; import { agentBundleNodeModules } from './helpers/workspace-paths.ts'; +import { removeTree } from './support/remove-tree.ts'; /** * The workbench-surface level claims to hand a consumer exactly what the dev @@ -146,6 +147,6 @@ it('matches the route manifest and lifecycle inventory a real dev server serves' expect(surface.advanced).toEqual(['artifact', 'protocol', 'hosts', 'logs']); } finally { await server?.close().catch(() => undefined); - await rm(project.root, { force: true, maxRetries: 5, recursive: true, retryDelay: 50 }); + await removeTree(project.root); } }); diff --git a/packages/agent-bundle/tests/workbench-surface.test.ts b/packages/agent-bundle/tests/workbench-surface.test.ts index 9b2b5c5fb..e276ebc89 100644 --- a/packages/agent-bundle/tests/workbench-surface.test.ts +++ b/packages/agent-bundle/tests/workbench-surface.test.ts @@ -1,4 +1,4 @@ -import { rm } from 'node:fs/promises'; + import { resolve } from 'node:path'; import { describe, expect, it } from '@rstest/core'; @@ -13,6 +13,7 @@ import { type WorkbenchSurface, } from '../src/test/index.ts'; import { createProjectFixture } from './helpers/project-fixture.ts'; +import { removeTree } from './support/remove-tree.ts'; const exampleRoot = (name: string): string => resolve(import.meta.dirname, '../../../examples', name); @@ -283,7 +284,7 @@ describe('capability counts', () => { expect(applicationGroup(surface, 'scripts')).toMatchObject({ leaves: expect.any(Array) }); expect(surface.application.groups.map((group) => group.kind)).not.toContain('events'); } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); @@ -340,10 +341,10 @@ describe('capability counts', () => { expect(hidden.counts).toMatchObject({ hooks: 0, targets: 1 }); expect(applicationGroup(hidden, 'events')).toMatchObject({ leaves: expect.any(Array) }); } finally { - await rm(prebuiltOnly.root, { force: true, recursive: true }); + await removeTree(prebuiltOnly.root); } } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); @@ -371,7 +372,7 @@ describe('capability counts', () => { expect(surface.counts).toMatchObject({ hooks: 0, scripts: 0, targets: 1 }); expect(surface.application.groups).toEqual([]); } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); }); @@ -435,7 +436,7 @@ describe('preparation parity with the Workbench server', () => { expect(byDefault.counts).toMatchObject({ evalSuites: 0, targets: 2 }); expect(byDefault.advanced).not.toContain('evals'); } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); }); @@ -473,7 +474,7 @@ describe('an unusable project', () => { expect((error as AgentTestError).message).toContain('source state invalid'); expect((error as AgentTestError).message).toContain('AB4100'); } finally { - await rm(project.root, { force: true, recursive: true }); + await removeTree(project.root); } }); }); diff --git a/packages/agent-bundle/tests/workspace-diff.test.ts b/packages/agent-bundle/tests/workspace-diff.test.ts index 576bed166..c56c44fa3 100644 --- a/packages/agent-bundle/tests/workspace-diff.test.ts +++ b/packages/agent-bundle/tests/workspace-diff.test.ts @@ -1,10 +1,11 @@ -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { expect, it } from '@rstest/core'; import { workspaceDiff } from '../src/eval/workspace-diff.ts'; +import { removeTree } from './support/remove-tree.ts'; it('reports bounded relative workspace changes by opaque identity and digest without file contents or absolute paths', async () => { const root = await mkdtemp(join(tmpdir(), 'agent-bundle-workspace-diff-')); @@ -28,7 +29,7 @@ it('reports bounded relative workspace changes by opaque identity and digest wit expect(JSON.stringify(diff)).not.toContain('/private/source-fixture'); expect(JSON.stringify(diff)).not.toContain('changed'); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -44,7 +45,7 @@ it('marks an oversized diff as truncated instead of expanding unbounded native w workspace: root, })).resolves.toMatchObject({ changes: expect.any(Array), truncated: true }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -68,7 +69,7 @@ it('bounds workspace traversal and file bytes before producing native evidence', await expect(workspaceDiff({ fileByteLimit: 8, plan, scanLimit: 2, totalByteLimit: 16, workspace: root })) .resolves.toMatchObject({ changes: expect.any(Array), truncated: true }); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); @@ -83,6 +84,6 @@ it('charges every visited empty directory against the native workspace traversal workspace: root, })).resolves.toEqual(Object.freeze({ changes: Object.freeze([]), truncated: true })); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); diff --git a/packages/agent-bundle/tests/worktree-proximity-journeys.test.ts b/packages/agent-bundle/tests/worktree-proximity-journeys.test.ts index c4e36a450..036928864 100644 --- a/packages/agent-bundle/tests/worktree-proximity-journeys.test.ts +++ b/packages/agent-bundle/tests/worktree-proximity-journeys.test.ts @@ -1,7 +1,7 @@ import { Client } from '@modelcontextprotocol/client'; import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; import { execFile as executeFile, spawn } from 'node:child_process'; -import { cp, mkdtemp, mkdir, rm, stat, symlink, writeFile } from 'node:fs/promises'; +import { cp, mkdtemp, mkdir, stat, symlink, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { promisify } from 'node:util'; @@ -10,6 +10,7 @@ import { afterAll, beforeAll, expect, it } from '@rstest/core'; import { build } from '../src/api.ts'; import { eventRuntimeEndpoint } from '../src/events/ipc.ts'; +import { removeTree } from './support/remove-tree.ts'; const execFile = promisify(executeFile); const exampleRoot = resolve(import.meta.dirname, '../../../examples/worktree-proximity'); @@ -293,7 +294,7 @@ beforeAll(async () => { afterAll(async () => { await liveSession?.stop(); if (fixture !== undefined) { - await rm(fixture.tempRoot, { force: true, recursive: true }); + await removeTree(fixture.tempRoot); } }); diff --git a/packages/create-agent-bundle/tests/scaffold-fixture.test.ts b/packages/create-agent-bundle/tests/scaffold-fixture.test.ts index 136bfd547..9291d8345 100644 --- a/packages/create-agent-bundle/tests/scaffold-fixture.test.ts +++ b/packages/create-agent-bundle/tests/scaffold-fixture.test.ts @@ -1,10 +1,11 @@ -import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from '@rstest/core'; import { expectPassedPool } from './support/scaffold-fixture.ts'; +import { removeTree } from '../../agent-bundle/tests/support/remove-tree.ts'; /** * `expectPassedPool` is the release matrix's verdict on a scaffolded pool, so @@ -62,7 +63,7 @@ describe('expectPassedPool', () => { }); afterAll(async () => { - await rm(projectRoot, { force: true, recursive: true }); + await removeTree(projectRoot); }); it('accepts a passing report whose script exited 0 and names the expected tests', async () => { diff --git a/packages/create-agent-bundle/tests/scaffold-packed-matrix.e2e.test.ts b/packages/create-agent-bundle/tests/scaffold-packed-matrix.e2e.test.ts index 8fd6a97b5..aae48d453 100644 --- a/packages/create-agent-bundle/tests/scaffold-packed-matrix.e2e.test.ts +++ b/packages/create-agent-bundle/tests/scaffold-packed-matrix.e2e.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { mkdtemp, readFile, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; @@ -16,6 +16,7 @@ import { npmRun, scaffoldProject, } from './support/scaffold-fixture.ts'; +import { removeTree } from '../../agent-bundle/tests/support/remove-tree.ts'; const execFile = promisify(executeFile); @@ -168,6 +169,6 @@ it.concurrent('scaffolds the cli-tool template with a routed bin, lib, and artif expect(packedPaths).toContain('.claude-plugin/plugin.json'); expect(packedPaths).toContain('bin/greeter.mjs'); } finally { - await rm(packDestination, { force: true, recursive: true }); + await removeTree(packDestination); } }, 600_000); diff --git a/packages/create-agent-bundle/tests/support/scaffold-fixture.ts b/packages/create-agent-bundle/tests/support/scaffold-fixture.ts index 4f514b393..6c4b1a6d5 100644 --- a/packages/create-agent-bundle/tests/support/scaffold-fixture.ts +++ b/packages/create-agent-bundle/tests/support/scaffold-fixture.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { copyFile, mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { basename, dirname, join } from 'node:path'; import { promisify } from 'node:util'; @@ -13,6 +13,7 @@ import { packOutputFromJson, sharedPackedTarball, } from '../../../agent-bundle/tests/support/shared-pack.ts'; +import { removeTree } from '../../../agent-bundle/tests/support/remove-tree.ts'; const execFile = promisify(executeFile); @@ -100,7 +101,7 @@ const fixture = (): Promise => { export const cleanupScaffoldFixture = async (): Promise => { if (fixturePromise === undefined) return; const { root } = await fixturePromise; - await rm(root, { force: true, recursive: true }); + await removeTree(root); }; export const scaffoldProject = async ( diff --git a/packages/rsc-runtime/tests/notices-ledger.test.ts b/packages/rsc-runtime/tests/notices-ledger.test.ts index 2a0510e68..8cf9334f7 100644 --- a/packages/rsc-runtime/tests/notices-ledger.test.ts +++ b/packages/rsc-runtime/tests/notices-ledger.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, rm } from 'node:fs/promises'; +import { mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -33,6 +33,7 @@ import { } from '../src/index.js'; import { createMemoryStateDriver, defineState } from '../src/state/index.js'; import { createSqliteStateDriver } from '../src/state/sqlite.js'; +import { removeTree } from '../../agent-bundle/tests/support/remove-tree.ts'; const document = (text: string) => ({ root: { kind: 'text' as const, text }, @@ -1441,7 +1442,7 @@ describe('notice ledger schema version', () => { expect(recorded.notices[0]).toMatchObject({ availability: { count: 1 }, state: 'acknowledged' }); await driver.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); }); diff --git a/packages/rsc-runtime/tests/notices-retention.test.ts b/packages/rsc-runtime/tests/notices-retention.test.ts index 119283c7e..1cdbc7470 100644 --- a/packages/rsc-runtime/tests/notices-retention.test.ts +++ b/packages/rsc-runtime/tests/notices-retention.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, rm } from 'node:fs/promises'; +import { mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -23,6 +23,7 @@ import { } from '../src/index.js'; import { createMemoryStateDriver, type AgentStateDriver, type AgentStateStore } from '../src/state/index.js'; import { createSqliteStateDriver } from '../src/state/sqlite.js'; +import { removeTree } from '../../agent-bundle/tests/support/remove-tree.ts'; const document = (text: string) => ({ root: { kind: 'text' as const, text }, @@ -311,7 +312,7 @@ describe('durable retention', () => { expect(deliveries.map((delivery) => delivery.notice.id)).toEqual([live.notice.id]); await second.driver.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); }); diff --git a/packages/rsc-runtime/tests/notices-sqlite-cross-process.test.ts b/packages/rsc-runtime/tests/notices-sqlite-cross-process.test.ts index c50d2ccaa..1e040ec81 100644 --- a/packages/rsc-runtime/tests/notices-sqlite-cross-process.test.ts +++ b/packages/rsc-runtime/tests/notices-sqlite-cross-process.test.ts @@ -1,6 +1,6 @@ import { spawn } from 'node:child_process'; import { once } from 'node:events'; -import { mkdtemp, rm } from 'node:fs/promises'; +import { mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -12,6 +12,7 @@ import { createAgentNoticeLedger, } from '../src/notices/index.js'; import { createSqliteStateDriver } from '../src/state/sqlite.js'; +import { removeTree } from '../../agent-bundle/tests/support/remove-tree.ts'; const packageRoot = fileURLToPath(new URL('..', import.meta.url)); const fixture = join(packageRoot, 'tests', 'fixtures', 'notices-sqlite-process.mjs'); @@ -96,7 +97,7 @@ describe.sequential('notice ledger cross-process proof', () => { await expect(store.read({ revision: 1 })).rejects.toMatchObject({ code: 'revision-unavailable' }); await driver.close(); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }); }); diff --git a/packages/rsc-runtime/tests/packed-entry-identity.test.ts b/packages/rsc-runtime/tests/packed-entry-identity.test.ts index c9b527d8d..f18b5eb70 100644 --- a/packages/rsc-runtime/tests/packed-entry-identity.test.ts +++ b/packages/rsc-runtime/tests/packed-entry-identity.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, readFile, readdir, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -26,6 +26,7 @@ import { runtimeEntryFiles, unreachedFiles, } from './support/dist-graph.ts'; +import { removeTree } from '../../agent-bundle/tests/support/remove-tree.ts'; const execFile = promisify(executeFile); const packageRoot = fileURLToPath(new URL('..', import.meta.url)); @@ -136,7 +137,7 @@ describe.sequential('packed @agent-bundle/runtime entry identity', () => { ); expect(fileURLToPath(resolved.stdout)).toBe(join(installed, 'package.json')); } finally { - await rm(consumer, { force: true, recursive: true }); + await removeTree(consumer); } }, 180_000); }); diff --git a/packages/rsc-runtime/tests/packed-zod-peer.test.ts b/packages/rsc-runtime/tests/packed-zod-peer.test.ts index 50a520bcb..814644c0a 100644 --- a/packages/rsc-runtime/tests/packed-zod-peer.test.ts +++ b/packages/rsc-runtime/tests/packed-zod-peer.test.ts @@ -1,6 +1,6 @@ import { execFile as executeFile } from 'node:child_process'; import type { Dirent } from 'node:fs'; -import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, readdir, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { basename, join, relative } from 'node:path'; import { promisify } from 'node:util'; @@ -14,6 +14,7 @@ import { packOutputFromJson, sharedPackedTarball, } from '../../agent-bundle/tests/support/shared-pack.ts'; +import { removeTree } from '../../agent-bundle/tests/support/remove-tree.ts'; const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); @@ -190,7 +191,7 @@ describe.sequential('packed @agent-bundle/runtime zod peer', () => { throw new Error(`Packed defineState typecheck failed.\n${typecheck.output}`); } } finally { - await rm(consumer, { force: true, recursive: true }); + await removeTree(consumer); } }, 180_000); @@ -217,7 +218,7 @@ describe.sequential('packed @agent-bundle/runtime zod peer', () => { expect(typecheck.output).toContain("Type '6' is not assignable to type '5'"); expect(typecheck.output).toContain('_zod.version.minor'); } finally { - await rm(workspace, { force: true, recursive: true }); + await removeTree(workspace); } }, 180_000); }); diff --git a/packages/rsc-runtime/tests/state-packaging.test.ts b/packages/rsc-runtime/tests/state-packaging.test.ts index 0f37d788b..3b50cfacf 100644 --- a/packages/rsc-runtime/tests/state-packaging.test.ts +++ b/packages/rsc-runtime/tests/state-packaging.test.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { mkdtemp, readFile, rm } from 'node:fs/promises'; +import { mkdtemp, readFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; @@ -21,6 +21,7 @@ import { unreachedFiles, type RuntimeEntrySubpath, } from './support/dist-graph.ts'; +import { removeTree } from '../../agent-bundle/tests/support/remove-tree.ts'; const execFile = promisify(executeFile); @@ -179,7 +180,7 @@ describe.sequential('state kernel packaging boundaries', () => { expect(report.sqliteRevisionError).toEqual({ code: 'invalid-input', instanceOfStateError: true, name: 'AgentStateError' }); expect(report.mountLedgerError).toEqual({ code: 'lifetime-mismatch', instanceOfStateError: true, name: 'AgentStateError' }); } finally { - await rm(stateRoot, { force: true, recursive: true }); + await removeTree(stateRoot); } }); }); diff --git a/packages/rsc-runtime/tests/state-sqlite-cross-process.test.ts b/packages/rsc-runtime/tests/state-sqlite-cross-process.test.ts index 510972bd9..78807a80c 100644 --- a/packages/rsc-runtime/tests/state-sqlite-cross-process.test.ts +++ b/packages/rsc-runtime/tests/state-sqlite-cross-process.test.ts @@ -1,6 +1,6 @@ import { spawn, type ChildProcess } from 'node:child_process'; import { once } from 'node:events'; -import { mkdtemp, rm } from 'node:fs/promises'; +import { mkdtemp } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { DatabaseSync } from 'node:sqlite'; @@ -11,6 +11,7 @@ import { z } from 'zod'; import { defineState, type AgentStateDefinition } from '../src/state/index.js'; import { createSqliteStateDriver } from '../src/state/sqlite.js'; +import { removeTree } from '../../agent-bundle/tests/support/remove-tree.ts'; /** * The two cross-process acceptance proofs for the workspace-durable driver @@ -64,7 +65,7 @@ const withStateFile = async (run: (file: string) => Promise): Promise Promise Promise): Promise => try { await run(root); } finally { - await rm(root, { force: true, recursive: true }); + await removeTree(root); } }; diff --git a/packages/workbench/tests/discovery-atoms-disposal.test.ts b/packages/workbench/tests/discovery-atoms-disposal.test.ts index 0a83bcd74..67027c563 100644 --- a/packages/workbench/tests/discovery-atoms-disposal.test.ts +++ b/packages/workbench/tests/discovery-atoms-disposal.test.ts @@ -1,5 +1,5 @@ import { createServer } from 'node:http'; -import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { join, normalize, relative } from 'node:path'; import { createRsbuild } from '@rsbuild/core'; @@ -8,6 +8,7 @@ import { describe, expect, it } from '@rstest/core'; import { createWorkbenchFixtureConfig } from './support/workbench-fixture-config.ts'; import { browserLaunchOptions } from './support/workbench-e2e.ts'; +import { removeTree } from './support/remove-tree.ts'; declare global { interface Window { @@ -252,7 +253,7 @@ describe('Discovery atoms', () => { } finally { await browser.close(); await new Promise((resolve, reject) => server.close((error) => error === undefined ? resolve() : reject(error))); - await rm(temp, { force: true, recursive: true }); + await removeTree(temp); } }, 60_000); }); diff --git a/packages/workbench/tests/discovery.e2e.test.ts b/packages/workbench/tests/discovery.e2e.test.ts index 149dd9947..df1a289a5 100644 --- a/packages/workbench/tests/discovery.e2e.test.ts +++ b/packages/workbench/tests/discovery.e2e.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -14,6 +14,7 @@ import { startRuntimePlaygroundFixture } from './helpers/runtime-playground-fixt import { replaceWatchedSource } from './support/watched-files.ts'; import { browserLaunchOptions, browserTrace, workbenchUrl } from './support/workbench-e2e.ts'; import { expectHeading } from './support/workbench-acceptance.ts'; +import { removeTree } from './support/remove-tree.ts'; const browserTimeout = 30_000 * timeScale; @@ -199,7 +200,7 @@ ${outputAnchor}`)); expect(pageErrors).toEqual([]); } finally { await fixture?.close(); - await rm(doctorRoot, { force: true, recursive: true }); + await removeTree(doctorRoot); } }, ); diff --git a/packages/workbench/tests/evals-compare-client-scope-browser.test.ts b/packages/workbench/tests/evals-compare-client-scope-browser.test.ts index dfbb17be3..1f0dcdf18 100644 --- a/packages/workbench/tests/evals-compare-client-scope-browser.test.ts +++ b/packages/workbench/tests/evals-compare-client-scope-browser.test.ts @@ -1,5 +1,5 @@ import { createServer, type Server } from 'node:http'; -import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, readdir, readFile, writeFile } from 'node:fs/promises'; import { once } from 'node:events'; import { tmpdir } from 'node:os'; import { join, relative } from 'node:path'; @@ -11,6 +11,7 @@ import { closeServer } from './support/http.ts'; import { createWorkbenchFixtureConfig } from './support/workbench-fixture-config.ts'; import { browserLaunchOptions, browserTrace } from './support/workbench-e2e.ts'; import { timeScale } from '../../agent-bundle/tests/support/time-scale.ts'; +import { removeTree } from './support/remove-tree.ts'; const workspaceRoot = process.cwd(); const evalsCompare = join(workspaceRoot, 'packages', 'workbench', 'src', 'evals', 'evals-compare.tsx'); @@ -88,7 +89,7 @@ const mountedComparisonsFixture = async (): Promise<{ readonly close: () => Prom return { close: async () => { await closeServer(server); - await rm(root, { force: true, recursive: true }); + await removeTree(root); }, url: `${origin}/page.html`, }; diff --git a/packages/workbench/tests/evals-real.e2e.test.ts b/packages/workbench/tests/evals-real.e2e.test.ts index 3765ff0a5..d6c1737bb 100644 --- a/packages/workbench/tests/evals-real.e2e.test.ts +++ b/packages/workbench/tests/evals-real.e2e.test.ts @@ -1,5 +1,5 @@ import { createServer, type Server } from 'node:http'; -import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, readdir, readFile, writeFile } from 'node:fs/promises'; import { once } from 'node:events'; import { tmpdir } from 'node:os'; import { join, relative } from 'node:path'; @@ -16,6 +16,7 @@ import { timeScale } from '../../agent-bundle/tests/support/time-scale.ts'; import { createWorkbenchFixtureConfig } from './support/workbench-fixture-config.ts'; import { buildWorkbench, e2e, workbenchAssets, workspaceRoot, workbenchUrl } from './support/workbench-e2e.ts'; import { expectHeading } from './support/workbench-acceptance.ts'; +import { removeTree } from './support/remove-tree.ts'; const evalsPage = join(workspaceRoot, 'packages', 'workbench', 'src', 'evals', 'evals-page.tsx'); const browserTimeout = 12_000 * timeScale; @@ -112,7 +113,7 @@ const mountedEvalClientScopeFixture = async (): Promise<{ readonly close: () => return { close: async () => { await closeServer(server); - await rm(root, { force: true, recursive: true }); + await removeTree(root); }, url: `${origin}/page.html`, }; diff --git a/packages/workbench/tests/helpers/runtime-playground-fixture.ts b/packages/workbench/tests/helpers/runtime-playground-fixture.ts index d00bf9023..401322787 100644 --- a/packages/workbench/tests/helpers/runtime-playground-fixture.ts +++ b/packages/workbench/tests/helpers/runtime-playground-fixture.ts @@ -1,5 +1,5 @@ import { execFile as executeFile } from 'node:child_process'; -import { cp, mkdtemp, rm, symlink } from 'node:fs/promises'; +import { cp, mkdtemp, symlink } from 'node:fs/promises'; import { join } from 'node:path'; import { promisify } from 'node:util'; @@ -10,6 +10,7 @@ import type { HostDiscoveryServiceOptions } from '../../../agent-bundle/src/dev/ import type { DevRuntimeClientSurfaceProxyBinding } from '../../../agent-bundle/src/dev/runtime-provider.ts'; import { startDevServer } from '../../../agent-bundle/src/dev/workbench-server.ts'; import { ensureRuntimeExamplePayload, runtimeExamplePayloads } from './runtime-example-payload.ts'; +import { removeTree } from '../support/remove-tree.ts'; const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); @@ -103,12 +104,12 @@ export const startRuntimePlaygroundFixture = async ( }, }); } catch (error) { - await rm(fixtureWorkspace, { force: true, recursive: true }); + await removeTree(fixtureWorkspace); throw error; } if (eventHub === undefined) { await server.close(); - await rm(fixtureWorkspace, { force: true, recursive: true }); + await removeTree(fixtureWorkspace); throw new Error('Runtime playground fixture did not receive the foreground event hub.'); } const foregroundEventHub = eventHub; @@ -142,7 +143,7 @@ export const startRuntimePlaygroundFixture = async ( clientSurfaceFailure = failed?.reason; await server.close(); } finally { - await rm(fixtureWorkspace, { force: true, recursive: true }); + await removeTree(fixtureWorkspace); resolveClosed(); } if (clientSurfaceFailure !== undefined) throw clientSurfaceFailure; diff --git a/packages/workbench/tests/mcp-app-frame.test.ts b/packages/workbench/tests/mcp-app-frame.test.ts index 15e1d53e9..7d5b9c006 100644 --- a/packages/workbench/tests/mcp-app-frame.test.ts +++ b/packages/workbench/tests/mcp-app-frame.test.ts @@ -1,5 +1,5 @@ import { createServer } from 'node:http'; -import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, readdir, readFile, writeFile } from 'node:fs/promises'; import { once } from 'node:events'; import { join, relative } from 'node:path'; import { tmpdir } from 'node:os'; @@ -27,6 +27,7 @@ import { } from '../../agent-bundle/src/web-host/browser/frame-relay.ts'; import type { McpAppJsonValue, McpAppRelayFrame, McpAppRouteClose, McpAppRouteMessages } from '../src/mcp/mcp-app-client.ts'; import { deferred, eventually } from './support/async.ts'; +import { removeTree } from './support/remove-tree.ts'; const frame: McpAppRelayFrame = Object.freeze({ allow: '', @@ -167,7 +168,7 @@ const mountedSecureRendererFixture = async () => { await new Promise((resolve, reject) => { bootstrap.close((error) => error === undefined ? resolve() : reject(error)); }); - await rm(root, { force: true, recursive: true }); + await removeTree(root); }, url: `http://127.0.0.1:${address.port}/renderer.html`, }; diff --git a/packages/workbench/tests/mcp-app-preview-browser.test.ts b/packages/workbench/tests/mcp-app-preview-browser.test.ts index fd5168ee8..a006c5727 100644 --- a/packages/workbench/tests/mcp-app-preview-browser.test.ts +++ b/packages/workbench/tests/mcp-app-preview-browser.test.ts @@ -1,5 +1,5 @@ import { createServer } from 'node:http'; -import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, readdir, readFile, writeFile } from 'node:fs/promises'; import { once } from 'node:events'; import { join, relative } from 'node:path'; import { tmpdir } from 'node:os'; @@ -13,6 +13,7 @@ import { timeScale } from '../../agent-bundle/tests/support/time-scale.ts'; import { requestRecorder } from './support/http.ts'; import { createWorkbenchFixtureConfig } from './support/workbench-fixture-config.ts'; import { browserLaunchOptions } from './support/workbench-e2e.ts'; +import { removeTree } from './support/remove-tree.ts'; const workspaceRoot = join(import.meta.dirname, '..', '..', '..'); const previewComponent = join(workspaceRoot, 'packages', 'workbench', 'src', 'mcp', 'mcp-app-preview.tsx'); @@ -124,7 +125,7 @@ const mountedPreviewFixture = async () => { reject(error); }); }); - await rm(root, { force: true, recursive: true }); + await removeTree(root); }, url: `http://127.0.0.1:${address.port}/preview.html`, }; diff --git a/packages/workbench/tests/mcp-json-input.test.ts b/packages/workbench/tests/mcp-json-input.test.ts index 709f8300f..8482981d7 100644 --- a/packages/workbench/tests/mcp-json-input.test.ts +++ b/packages/workbench/tests/mcp-json-input.test.ts @@ -1,7 +1,7 @@ import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { createServer, type Server } from 'node:http'; -import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, readdir, readFile, writeFile } from 'node:fs/promises'; import { once } from 'node:events'; import { join, relative } from 'node:path'; import { tmpdir } from 'node:os'; @@ -26,6 +26,7 @@ import { submitJsonValue, submitJsonRecord, } from '../src/mcp/mcp-json-input.tsx'; +import { removeTree } from './support/remove-tree.ts'; const workspaceRoot = join(import.meta.dirname, '..', '..', '..'); const inputComponent = join(workspaceRoot, 'packages', 'workbench', 'src', 'mcp', 'mcp-json-input.tsx'); @@ -69,7 +70,7 @@ const mountedInputFixture = async (source: readonly string[]) => { return { close: async () => { await new Promise((resolve, reject) => server.close((error) => error === undefined ? resolve() : reject(error))); - await rm(root, { force: true, recursive: true }); + await removeTree(root); }, url: `${origin}/input.html`, }; diff --git a/packages/workbench/tests/mcp-page-app-browser.test.ts b/packages/workbench/tests/mcp-page-app-browser.test.ts index 27f22fb7b..bd60a2eba 100644 --- a/packages/workbench/tests/mcp-page-app-browser.test.ts +++ b/packages/workbench/tests/mcp-page-app-browser.test.ts @@ -1,5 +1,5 @@ import { createServer, type Server } from 'node:http'; -import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, readdir, readFile, writeFile } from 'node:fs/promises'; import { once } from 'node:events'; import { join, relative } from 'node:path'; import { tmpdir } from 'node:os'; @@ -11,6 +11,7 @@ import { chromium, type Page } from 'playwright'; import { closeServer } from './support/http.ts'; import { createWorkbenchFixtureConfig } from './support/workbench-fixture-config.ts'; import { browserLaunchOptions } from './support/workbench-e2e.ts'; +import { removeTree } from './support/remove-tree.ts'; const workspaceRoot = join(import.meta.dirname, '..', '..', '..'); const pageComponent = join(workspaceRoot, 'packages', 'workbench', 'src', 'mcp', 'mcp-page.tsx'); @@ -171,7 +172,7 @@ const mountedPageFixture = async (mode: 'artifact' | 'runtime' | 'runtime-direct close: async () => { await closeServer(outer); await closeServer(sandbox); - await rm(root, { force: true, recursive: true }); + await removeTree(root); }, /** * Resolves with the next sandbox document request the server accepts. diff --git a/packages/workbench/tests/packed-release.e2e.test.ts b/packages/workbench/tests/packed-release.e2e.test.ts index 051bd610f..b51853f2c 100644 --- a/packages/workbench/tests/packed-release.e2e.test.ts +++ b/packages/workbench/tests/packed-release.e2e.test.ts @@ -1,5 +1,5 @@ import { spawn, type ChildProcess } from 'node:child_process'; -import { access, chmod, cp, mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'; +import { access, chmod, cp, mkdir, mkdtemp, readFile, readdir, realpath, writeFile } from 'node:fs/promises'; import { createServer } from 'node:net'; import { tmpdir } from 'node:os'; import { delimiter, dirname, isAbsolute, join, relative } from 'node:path'; @@ -33,6 +33,7 @@ import { replaceWatchedSource } from './support/watched-files.ts'; import { browserLaunchOptions, browserTrace, waitForWorkbenchIdle, workbenchUrl } from './support/workbench-e2e.ts'; import { deepFreeze } from '../src/freeze.ts'; import { expectHeading } from './support/workbench-acceptance.ts'; +import { removeTree } from './support/remove-tree.ts'; const fixtureRoot = join(workspaceRoot, 'fixtures', 'integration', 'packed-release'); const browserTimeout = 12_000 * timeScale; @@ -984,7 +985,7 @@ e2e('runs every Agent API tool from the installed tarball', { timeout: 360_000 * try { await closeChild(child); } catch (error) { cleanupFailures.push(error); } } - try { await rm(consumer, { force: true, recursive: true }); } + try { await removeTree(consumer); } catch (error) { cleanupFailures.push(error); } try { await access(consumer); cleanupFailures.push(new Error(`Packed consumer temporary directory still exists: ${consumer}`)); } catch (error) { diff --git a/packages/workbench/tests/route-editor-atoms-disposal.test.ts b/packages/workbench/tests/route-editor-atoms-disposal.test.ts index e50ea0586..9075d6093 100644 --- a/packages/workbench/tests/route-editor-atoms-disposal.test.ts +++ b/packages/workbench/tests/route-editor-atoms-disposal.test.ts @@ -1,5 +1,5 @@ import { createServer } from 'node:http'; -import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { mkdtemp, readFile, writeFile } from 'node:fs/promises'; import { join, normalize, relative } from 'node:path'; import { createRsbuild } from '@rsbuild/core'; @@ -8,6 +8,7 @@ import { describe, expect, it } from '@rstest/core'; import { createWorkbenchFixtureConfig } from './support/workbench-fixture-config.ts'; import { browserLaunchOptions } from './support/workbench-e2e.ts'; +import { removeTree } from './support/remove-tree.ts'; declare global { interface Window { @@ -184,7 +185,7 @@ describe('Route editor atoms', () => { } finally { await browser.close(); await new Promise((resolve, reject) => server.close((error) => error === undefined ? resolve() : reject(error))); - await rm(temp, { force: true, recursive: true }); + await removeTree(temp); } }, 60_000); }); diff --git a/packages/workbench/tests/sessions.e2e.test.ts b/packages/workbench/tests/sessions.e2e.test.ts index 8e2cbf09f..86eeace30 100644 --- a/packages/workbench/tests/sessions.e2e.test.ts +++ b/packages/workbench/tests/sessions.e2e.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { delimiter, join } from 'node:path'; @@ -20,6 +20,7 @@ import { withWorkbenchServer, workspaceRoot, } from './support/workbench-e2e.ts'; +import { removeTree } from './support/remove-tree.ts'; const browserTimeout = 15_000 * timeScale; const hostTimeout = 60_000 * timeScale; @@ -92,7 +93,7 @@ e2e('accepts Claude and Codex host sessions at 1440×900', { timeout: 300_000 * else process.env[key] = value; } }, - () => rm(homes, { force: true, recursive: true }), + () => removeTree(homes), ], }, async (server, project) => { await openWorkbench(page, server.url, '/sessions'); diff --git a/packages/workbench/tests/support/example-acceptance.ts b/packages/workbench/tests/support/example-acceptance.ts index 7748f0098..ccfeff0b9 100644 --- a/packages/workbench/tests/support/example-acceptance.ts +++ b/packages/workbench/tests/support/example-acceptance.ts @@ -1,11 +1,12 @@ import assert from 'node:assert/strict'; -import { cp, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'; +import { cp, mkdir, mkdtemp, symlink, writeFile } from 'node:fs/promises'; import { basename, join } from 'node:path'; import type { Page, Request } from 'playwright-core'; import { waitForWorkbenchIdle, workspaceRoot } from './workbench-e2e.ts'; import { timeScale } from '../../../agent-bundle/tests/support/time-scale.ts'; +import { removeTree } from './remove-tree.ts'; export type ExampleName = | 'audiobook-curator' @@ -59,7 +60,7 @@ export const copyExample = async (name: ExampleName): Promise<{ readonly release // Teardown intermittently hits ENOTEMPTY on rmdir of `/.agent-bundle` // (a late write landing after server.close()); retry like // playground/mcp-probe-service.ts does. - return { release: () => rm(root, { force: true, maxRetries: 3, recursive: true, retryDelay: 50 }), root }; + return { release: () => removeTree(root), root }; }; export const waitForSettledWorkbench = (page: Page): Promise => waitForWorkbenchIdle(page, browserTimeout); diff --git a/packages/workbench/tests/support/remove-tree.ts b/packages/workbench/tests/support/remove-tree.ts new file mode 100644 index 000000000..ea2e8a01c --- /dev/null +++ b/packages/workbench/tests/support/remove-tree.ts @@ -0,0 +1 @@ +export { removeTree } from '../../../agent-bundle/tests/support/remove-tree.ts'; diff --git a/packages/workbench/tests/web-command.e2e.test.ts b/packages/workbench/tests/web-command.e2e.test.ts index 44b21a126..c0b5c2fac 100644 --- a/packages/workbench/tests/web-command.e2e.test.ts +++ b/packages/workbench/tests/web-command.e2e.test.ts @@ -1,5 +1,5 @@ import type { ChildProcess } from 'node:child_process'; -import { readdir, readFile, rm, stat } from 'node:fs/promises'; +import { readdir, readFile, stat } from 'node:fs/promises'; import { join } from 'node:path'; import { expect } from '@rstest/playwright'; @@ -14,6 +14,7 @@ import { timeScale } from '../../agent-bundle/tests/support/time-scale.ts'; import { copyExample } from './support/example-acceptance.ts'; import { descendantProcessIds } from './support/packed-release-harness.ts'; import { e2e } from './support/workbench-e2e.ts'; +import { removeTree } from './support/remove-tree.ts'; const pluginName = 'mcp-app-example'; const app = 'status/status'; @@ -121,7 +122,7 @@ e2e('serves examples/mcp-app through ` web` from its composite root and const built = await build({ output: artifactRoot, root: example.root }); expect(built.diagnostics.filter((entry) => entry.severity === 'error')).toEqual([]); // The artifact is the whole product: the bin serves the App with no source beside it. - await rm(join(example.root, 'src'), { force: true, recursive: true }); + await removeTree(join(example.root, 'src')); const bin = join(artifactRoot, 'bin', `${pluginName}.mjs`); await expect(stat(bin)).resolves.toMatchObject({}); const manifest = JSON.parse(await readFile(join(artifactRoot, 'agent-bundle.manifest.json'), 'utf8')) as { diff --git a/scripts/check-test-remove-tree.d.mts b/scripts/check-test-remove-tree.d.mts new file mode 100644 index 000000000..d7056e395 --- /dev/null +++ b/scripts/check-test-remove-tree.d.mts @@ -0,0 +1,19 @@ +export interface RecursiveRmCall { + readonly call: string; + readonly hasRetries: boolean; + /** 1-based line of the call's first character. */ + readonly line: number; +} + +export interface RemovalBindings { + /** Local names bound to `rm` from node:fs or node:fs/promises, including aliases. */ + readonly bareNames: ReadonlySet; + /** Namespace and default import names whose `.rm` is node's. */ + readonly namespaceNames: ReadonlySet; +} + +export declare const removalBindings: (text: string, fileName?: string) => RemovalBindings; + +export declare const recursiveRmCalls: (text: string, fileName?: string) => RecursiveRmCall[]; + +export declare const bareRecursiveRmFailures: (file: string, text: string) => string[]; diff --git a/scripts/check-test-remove-tree.mjs b/scripts/check-test-remove-tree.mjs new file mode 100644 index 000000000..a63b1a9e3 --- /dev/null +++ b/scripts/check-test-remove-tree.mjs @@ -0,0 +1,207 @@ +/** + * Test teardown that deletes a tree calls `removeTree`. A bare `rm` with + * `recursive: true` and no `maxRetries` races a late writer and flakes with ENOTEMPTY. + * + * Catches bare `rm(`, aliased `import { rm as remove }` calls, and `ns.rm(` when + * `ns` is a namespace/default import from node:fs, fs, or their /promises forms. + * + * Call, option, and import-binding detection is parser-backed (typescript-5): + * only real node:fs(/promises) ImportDeclaration bindings count, only Node-bound + * call expressions are considered, and `recursive` / `maxRetries` are read from + * the second argument's object-literal properties (including quoted keys). Nested + * objects in the path argument, member calls, comments, strings, regexes, and + * template substitutions are handled by the AST rather than text masking. + */ +import { createRequire } from 'node:module'; +import { readdir, readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const require = createRequire(join(dirname(fileURLToPath(import.meta.url)), '../packages/agent-bundle/package.json')); +/** @type {typeof import('typescript-5')} */ +const ts = require('typescript-5'); + +const roots = [ + 'packages/agent-bundle/tests', + 'packages/workbench/tests', + 'packages/rsc-runtime/tests', + 'packages/rsc-markdown-stream/tests', + 'packages/create-agent-bundle/tests', +]; + +const nodeFsSpecifier = /^(?:node:)?fs(?:\/promises)?$/u; + +const isRemoveTreeHelper = (file) => /(?:^|\/)remove-tree\.ts$/u.test(file.replaceAll('\\', '/')); + +const walk = async (directory, files) => { + let entries; + try { + entries = await readdir(directory, { withFileTypes: true }); + } catch (error) { + if (error?.code === 'ENOENT') return; + throw error; + } + for (const entry of entries) { + const path = join(directory, entry.name); + if (entry.isDirectory()) await walk(path, files); + else if (/\.(?:ts|mts|mjs|js|tsx)$/u.test(entry.name)) files.push(path); + } +}; + +/** + * Named/aliased rm bindings and namespace/default bindings that expose .rm. + * Import bindings are collected from the TypeScript AST so comments and local + * identifiers cannot forge Node fs.rm bindings. + */ +export const removalBindings = (text, fileName = 'bindings.ts') => { + const bareNames = new Set(); + const namespaceNames = new Set(); + const sourceFile = ts.createSourceFile( + fileName, + text, + ts.ScriptTarget.Latest, + true, + scriptKindFor(fileName), + ); + + for (const statement of sourceFile.statements) { + if (!ts.isImportDeclaration(statement) || statement.importClause === undefined) continue; + if (statement.moduleSpecifier === undefined || !ts.isStringLiteral(statement.moduleSpecifier)) { + continue; + } + if (!nodeFsSpecifier.test(statement.moduleSpecifier.text)) continue; + + const { importClause } = statement; + if (importClause.isTypeOnly) continue; + + if (importClause.name !== undefined) { + namespaceNames.add(importClause.name.text); + } + + const bindings = importClause.namedBindings; + if (bindings === undefined) continue; + + if (ts.isNamespaceImport(bindings)) { + namespaceNames.add(bindings.name.text); + continue; + } + + if (!ts.isNamedImports(bindings)) continue; + for (const element of bindings.elements) { + if (element.isTypeOnly) continue; + if (element.propertyName !== undefined) { + if (element.propertyName.text !== 'rm') continue; + bareNames.add(element.name.text); + continue; + } + if (element.name.text !== 'rm') continue; + bareNames.add('rm'); + } + } + + return { bareNames, namespaceNames }; +}; + +const propertyName = (name) => { + if (ts.isIdentifier(name)) return name.text; + if (ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name)) return name.text; + return undefined; +}; + +/** Options flags from a call's second-argument object literal only. */ +const optionsFlags = (optionsArg) => { + if (optionsArg === undefined || !ts.isObjectLiteralExpression(optionsArg)) { + return { recursive: false, hasRetries: false }; + } + let recursive = false; + let hasRetries = false; + for (const property of optionsArg.properties) { + if (!ts.isPropertyAssignment(property)) continue; + const key = propertyName(property.name); + if (key === 'recursive' && property.initializer.kind === ts.SyntaxKind.TrueKeyword) { + recursive = true; + } + if (key === 'maxRetries') hasRetries = true; + } + return { recursive, hasRetries }; +}; + +const isNodeBoundRmCall = (expression, bareNames, namespaceNames) => { + if (ts.isIdentifier(expression)) return bareNames.has(expression.text); + if ( + ts.isPropertyAccessExpression(expression) + && !expression.questionDotToken + && expression.name.text === 'rm' + && ts.isIdentifier(expression.expression) + ) { + return namespaceNames.has(expression.expression.text); + } + return false; +}; + +const scriptKindFor = (fileName) => { + if (fileName.endsWith('.tsx')) return ts.ScriptKind.TSX; + if (fileName.endsWith('.jsx')) return ts.ScriptKind.JSX; + if (fileName.endsWith('.mjs') || fileName.endsWith('.js')) return ts.ScriptKind.JS; + return ts.ScriptKind.TS; +}; + +export const recursiveRmCalls = (text, fileName = 'check.ts') => { + const { bareNames, namespaceNames } = removalBindings(text); + const sourceFile = ts.createSourceFile( + fileName, + text, + ts.ScriptTarget.Latest, + true, + scriptKindFor(fileName), + ); + const calls = []; + + const visit = (node) => { + if (ts.isCallExpression(node) && isNodeBoundRmCall(node.expression, bareNames, namespaceNames)) { + const flags = optionsFlags(node.arguments[1]); + if (flags.recursive) { + const start = node.getStart(sourceFile); + calls.push({ + call: text.slice(start, node.getEnd()), + hasRetries: flags.hasRetries, + line: sourceFile.getLineAndCharacterOfPosition(start).line + 1, + }); + } + } + ts.forEachChild(node, visit); + }; + + visit(sourceFile); + return calls; +}; + +export const bareRecursiveRmFailures = (file, text) => { + if (isRemoveTreeHelper(file)) return []; + const failures = []; + for (const call of recursiveRmCalls(text, file)) { + if (call.hasRetries) continue; + failures.push(`${file}:${call.line} bare recursive rm. Use removeTree.`); + } + return failures; +}; + +const run = async () => { + const failures = []; + const files = []; + for (const root of roots) await walk(root, files); + for (const file of files) { + const text = await readFile(file, 'utf8'); + failures.push(...bareRecursiveRmFailures(file, text)); + } + + if (failures.length > 0) { + console.error(failures.join('\n')); + process.exitCode = 1; + } +}; + +const invokedDirectly = process.argv[1] !== undefined + && import.meta.url === pathToFileURL(process.argv[1]).href; + +if (invokedDirectly) await run();