From 2bf48879b789e2530df2bdaadabbc4b44997e090 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 17 Sep 2026 03:03:16 +0000 Subject: [PATCH 01/12] test(support): add removeTree, a retrying recursive delete for test teardown A late writer into a tree being removed makes rm reject with ENOTEMPTY. One helper retries the transient codes so each call site does not carry its own maxRetries. --- .../tests/support/remove-tree.test.ts | 38 +++++++++++++++++++ .../agent-bundle/tests/support/remove-tree.ts | 30 +++++++++++++++ .../workbench/tests/support/remove-tree.ts | 1 + 3 files changed, 69 insertions(+) create mode 100644 packages/agent-bundle/tests/support/remove-tree.test.ts create mode 100644 packages/agent-bundle/tests/support/remove-tree.ts create mode 100644 packages/workbench/tests/support/remove-tree.ts 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..92eb3225d --- /dev/null +++ b/packages/agent-bundle/tests/support/remove-tree.test.ts @@ -0,0 +1,38 @@ +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); +}); 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/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'; From a328e4e4ec0f180892960b1114177f4dc477eff0 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 17 Sep 2026 03:03:16 +0000 Subject: [PATCH 02/12] test: replace bare recursive rm in tests with removeTree and gate it in lint scripts/check-test-remove-tree.mjs fails pnpm lint on rm(..., { recursive: true }) without maxRetries under packages/*/tests. 1156 call sites across 204 files moved to removeTree. --- package.json | 2 +- .../tests/adapter-contract.test.ts | 9 +- .../agent-bundle/tests/amp-adapter.test.ts | 13 +- .../agent-bundle/tests/amp-install.test.ts | 17 +- packages/agent-bundle/tests/api.test.ts | 97 ++++----- .../tests/artifact-cli-bin.test.ts | 7 +- .../tests/artifact-inspection-service.test.ts | 39 ++-- .../tests/artifact-validator.test.ts | 185 +++++++++--------- .../tests/browser-stdio-bridge-spike.test.ts | 5 +- .../agent-bundle/tests/build-compose.test.ts | 5 +- .../tests/build-reproducibility.test.ts | 5 +- .../tests/build-source-publication.test.ts | 9 +- packages/agent-bundle/tests/build.test.ts | 29 +-- .../tests/check-declaration-imports.test.ts | 7 +- .../tests/classify-docs-only.test.ts | 5 +- .../tests/claude-hooks-schema.test.ts | 5 +- .../claude-plugin-validate-acceptance.test.ts | 5 +- .../tests/claude-plugin-validation.test.ts | 5 +- .../agent-bundle/tests/cli-projection.test.ts | 5 +- .../tests/cli-routes-build.test.ts | 7 +- .../agent-bundle/tests/cli-routes.test.ts | 5 +- packages/agent-bundle/tests/cli.test.ts | 33 ++-- .../tests/codex-plugin-validation.test.ts | 17 +- .../agent-bundle/tests/command-config.test.ts | 7 +- .../tests/compile-evidence.test.ts | 5 +- .../agent-bundle/tests/compile-stages.test.ts | 5 +- .../tests/compiler-evidence.test.ts | 5 +- .../tests/composite-rules.test.ts | 5 +- packages/agent-bundle/tests/config.test.ts | 7 +- .../tests/cursor-plugin-validation.test.ts | 5 +- .../tests/dependency-audit-plugin.test.ts | 27 +-- .../tests/dev-artifact-service.test.ts | 41 ++-- .../tests/dev-coordinator.test.ts | 49 ++--- .../tests/dev-host-install-manager.test.ts | 7 +- .../tests/dev-host-install.test.ts | 9 +- packages/agent-bundle/tests/dev-lock.test.ts | 33 ++-- .../tests/dev-package-build-service.test.ts | 5 +- .../agent-bundle/tests/dev-services.test.ts | 111 +++++------ .../agent-bundle/tests/dev-watcher.test.ts | 7 +- .../tests/dev-workbench-packaging.test.ts | 11 +- .../agent-bundle/tests/dev-workbench.test.ts | 43 ++-- .../agent-bundle/tests/dist-freshness.test.ts | 7 +- packages/agent-bundle/tests/doctor.test.ts | 37 ++-- .../agent-bundle/tests/durable-fs.test.ts | 9 +- .../effect-filesystem-phase2-dev.test.ts | 5 +- .../tests/effect-filesystem-phase2.test.ts | 5 +- .../tests/effect-platform.test.ts | 13 +- .../emitted-artifact-effect-surface.test.ts | 5 +- .../agent-bundle/tests/epoch-store.test.ts | 87 ++++---- .../tests/eval-claude-harness.test.ts | 9 +- packages/agent-bundle/tests/eval-cli.test.ts | 5 +- .../tests/eval-codex-harness.test.ts | 3 +- .../tests/eval-codex-home.test.ts | 7 +- .../tests/eval-codex-plugins.test.ts | 5 +- .../agent-bundle/tests/eval-config.test.ts | 5 +- .../agent-bundle/tests/eval-fixtures.test.ts | 3 +- .../agent-bundle/tests/eval-graders.test.ts | 5 +- .../agent-bundle/tests/eval-harness.test.ts | 15 +- .../tests/eval-native-mount.test.ts | 3 +- .../agent-bundle/tests/eval-run-store.test.ts | 13 +- .../agent-bundle/tests/eval-service.test.ts | 5 +- .../agent-bundle/tests/eval-workbench.test.ts | 5 +- .../event-handler-artifact-graph.test.ts | 5 +- .../tests/examples-check-script.test.ts | 5 +- .../tests/examples-contract.test.ts | 23 +-- .../tests/function-authoring-build.test.ts | 15 +- .../tests/generated-module-evidence.test.ts | 5 +- .../tests/generated-route-server.test.ts | 5 +- .../tests/helpers/project-fixture.ts | 5 +- .../tests/hook-handler-contract.test.ts | 5 +- .../tests/hook-playground-service.test.ts | 23 +-- .../tests/hook-receipt-pipe.test.ts | 5 +- .../agent-bundle/tests/hook-receipts.test.ts | 7 +- packages/agent-bundle/tests/hooks.test.ts | 57 +++--- .../tests/host-adapters.native.test.ts | 63 +++--- .../agent-bundle/tests/host-adapters.test.ts | 7 +- .../agent-bundle/tests/host-cli-pins.test.ts | 5 +- .../tests/host-discovery-dev-server.test.ts | 5 +- .../tests/host-discovery-service.test.ts | 7 +- .../tests/host-install-proof.test.ts | 7 +- .../agent-bundle/tests/host-mcp-proxy.test.ts | 3 +- .../tests/inspect-artifact.test.ts | 7 +- .../tests/inspect-bundler.test.ts | 5 +- .../agent-bundle/tests/inspect-state.test.ts | 11 +- .../agent-bundle/tests/install-cli.test.ts | 5 +- .../tests/install-surface.test.ts | 55 +++--- packages/agent-bundle/tests/install.test.ts | 177 ++++++++--------- .../tests/integration-matrix.test.ts | 9 +- .../agent-bundle/tests/launch-env.test.ts | 5 +- .../agent-bundle/tests/layout-build.test.ts | 5 +- .../tests/lifecycle-replay-dev-server.test.ts | 5 +- .../tests/manifest-combined-proof.test.ts | 7 +- .../tests/manifest-reindex.test.ts | 5 +- .../tests/manifest-relocatable.test.ts | 5 +- .../tests/mcp-apps-compile.test.ts | 3 +- .../tests/mcp-probe-dev-server.test.ts | 7 +- .../tests/mcp-probe-service.test.ts | 59 +++--- .../tests/mcp-session-service.test.ts | 65 +++--- packages/agent-bundle/tests/mcp.test.ts | 55 +++--- .../tests/native-claude-contract.test.ts | 11 +- .../tests/native-codex-contract.test.ts | 15 +- .../tests/native-playground-service.test.ts | 79 ++++---- .../agent-bundle/tests/normalization.test.ts | 15 +- .../agent-bundle/tests/package-build.test.ts | 5 +- .../tests/package-conventions.test.ts | 5 +- .../tests/package-identity.test.ts | 7 +- .../tests/packed-consumer-typescript.test.ts | 5 +- .../tests/packed-consumer.test.ts | 13 +- .../tests/packed-deleted-source.test.ts | 13 +- .../tests/packed-host-install-proof.test.ts | 7 +- .../tests/packed-install-bin.test.ts | 7 +- .../tests/packed-native-smoke.test.ts | 11 +- .../tests/packed-readonly-state-root.test.ts | 5 +- .../tests/packed-small-plugin.test.ts | 3 +- .../tests/packed-stdio-projection.test.ts | 5 +- .../tests/packed-web-command.test.ts | 7 +- .../tests/path-token-resolver.test.ts | 5 +- .../playground-orchestration-service.test.ts | 5 +- .../tests/playground-service.test.ts | 7 +- .../agent-bundle/tests/plugin-logo.test.ts | 5 +- .../tests/portable-plugin-validation.test.ts | 7 +- .../tests/prebuilt-payload.test.ts | 5 +- packages/agent-bundle/tests/prepack.test.ts | 15 +- .../tests/project-context-walk-bound.test.ts | 5 +- .../tests/projection/contract-matrix.test.ts | 15 +- .../tests/projection/mcp-in-memory.test.ts | 11 +- .../tests/provider-typegen.test.ts | 5 +- .../tests/public-api-packed.test.ts | 15 +- .../agent-bundle/tests/public-api.test.ts | 7 +- .../agent-bundle/tests/publint-gate.test.ts | 5 +- .../tests/rendered-skills.test.ts | 5 +- .../tests/route-caller-input-types.test.ts | 5 +- .../tests/route-contract-imports.test.ts | 5 +- .../agent-bundle/tests/route-graph.test.ts | 5 +- .../tests/route-invocation-dev-server.test.ts | 11 +- .../tests/route-invocation-service.test.ts | 19 +- .../tests/route-register-typegen.test.ts | 5 +- .../tests/route-task-support.test.ts | 5 +- .../tests/route-typegen-write.test.ts | 5 +- .../tests/route-unit/lifecycle-replay.test.ts | 5 +- .../route-unit/route-module-loader.test.ts | 5 +- .../workbench-surface-rendered-skill.test.ts | 5 +- .../rsc-runtime-optional-packaging.test.ts | 5 +- .../tests/rsc-runtime-topology-script.test.ts | 5 +- .../tests/rstest-meta-alias.test.ts | 5 +- .../tests/rstest-worker-isolation.test.ts | 5 +- .../tests/rstest-worker-root-teardown.test.ts | 3 +- .../agent-bundle/tests/rule-config.test.ts | 7 +- .../tests/runtime-generation-store.test.ts | 55 +++--- .../tests/runtime-mcp-registry.test.ts | 5 +- .../tests/runtime-provider.test.ts | 21 +- .../tests/script-playground-service.test.ts | 21 +- .../self-contained-bundler-config.test.ts | 7 +- packages/agent-bundle/tests/serve-app.test.ts | 5 +- .../tests/shared-metadata.test.ts | 5 +- .../tests/skill-document-service.test.ts | 19 +- packages/agent-bundle/tests/skill-ir.test.ts | 5 +- .../tests/support/fake-host-cli/fake-host.mjs | 2 +- .../tests/support/host-install.ts | 27 +-- .../tests/support/mcp-conformance.ts | 9 +- .../tests/support/packed-native-smoke.ts | 6 +- .../tests/target-hook-contract.test.ts | 5 +- .../tests/target-mcp-runtime.test.ts | 5 +- .../tests/terminal-capability.test.ts | 5 +- .../tests/test-browser-rstest.test.ts | 5 +- .../tests/trace-dev-server.test.ts | 5 +- packages/agent-bundle/tests/uninstall.test.ts | 93 ++++----- .../tests/watched-files-support.test.ts | 5 +- .../agent-bundle/tests/web-command.test.ts | 5 +- .../agent-bundle/tests/web-config.test.ts | 5 +- .../tests/web-host-launch-selection.test.ts | 5 +- .../tests/web-host-routes-unit.test.ts | 5 +- .../agent-bundle/tests/web-launch.test.ts | 5 +- .../agent-bundle/tests/web-manifest.test.ts | 5 +- .../tests/workbench-asset-cache.test.ts | 5 +- .../workbench-surface-dev-server.test.ts | 5 +- .../tests/workbench-surface.test.ts | 15 +- .../agent-bundle/tests/workspace-diff.test.ts | 11 +- .../tests/worktree-proximity-journeys.test.ts | 5 +- .../tests/scaffold-fixture.test.ts | 5 +- .../tests/scaffold-packed-matrix.e2e.test.ts | 5 +- .../tests/support/scaffold-fixture.ts | 5 +- .../rsc-runtime/tests/notices-ledger.test.ts | 5 +- .../tests/notices-retention.test.ts | 5 +- .../notices-sqlite-cross-process.test.ts | 5 +- .../tests/packed-entry-identity.test.ts | 5 +- .../rsc-runtime/tests/packed-zod-peer.test.ts | 7 +- .../rsc-runtime/tests/state-packaging.test.ts | 5 +- .../tests/state-sqlite-cross-process.test.ts | 5 +- .../rsc-runtime/tests/state-sqlite.test.ts | 7 +- .../tests/discovery-atoms-disposal.test.ts | 5 +- .../workbench/tests/discovery.e2e.test.ts | 5 +- ...evals-compare-client-scope-browser.test.ts | 5 +- .../workbench/tests/evals-real.e2e.test.ts | 5 +- .../helpers/runtime-playground-fixture.ts | 9 +- .../workbench/tests/mcp-app-frame.test.ts | 5 +- .../tests/mcp-app-preview-browser.test.ts | 5 +- .../workbench/tests/mcp-json-input.test.ts | 5 +- .../tests/mcp-page-app-browser.test.ts | 5 +- .../tests/packed-release.e2e.test.ts | 5 +- .../tests/route-editor-atoms-disposal.test.ts | 5 +- packages/workbench/tests/sessions.e2e.test.ts | 5 +- .../tests/support/example-acceptance.ts | 5 +- .../workbench/tests/web-command.e2e.test.ts | 5 +- scripts/check-test-remove-tree.mjs | 98 ++++++++++ 205 files changed, 1621 insertions(+), 1322 deletions(-) create mode 100644 scripts/check-test-remove-tree.mjs 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/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..18fdee411 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); @@ -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..870d47bb1 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); } }); @@ -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/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/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.mjs b/scripts/check-test-remove-tree.mjs new file mode 100644 index 000000000..17231edac --- /dev/null +++ b/scripts/check-test-remove-tree.mjs @@ -0,0 +1,98 @@ +/** + * 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. + */ +import { readdir, readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +const roots = [ + 'packages/agent-bundle/tests', + 'packages/workbench/tests', + 'packages/rsc-runtime/tests', + 'packages/rsc-markdown-stream/tests', + 'packages/create-agent-bundle/tests', +]; + +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); + } +}; + +const recursiveRmCalls = (text) => { + const calls = []; + const pattern = /\brm\s*\(/gu; + let match = pattern.exec(text); + while (match !== null) { + const before = text[match.index - 1]; + if (before === '.' || before === '$') { + match = pattern.exec(text); + continue; + } + let commented = false; + for (let index = match.index - 1; index >= 0; index -= 1) { + const char = text[index]; + if (char === '\n') break; + if (char === '/' && text[index - 1] === '/') { + commented = true; + break; + } + } + if (commented) { + match = pattern.exec(text); + continue; + } + const open = match.index + match[0].length - 1; + let depth = 1; + let inString = null; + let index = open + 1; + while (index < text.length && depth > 0) { + const char = text[index]; + if (inString !== null) { + if (char === '\\') { + index += 2; + continue; + } + if (char === inString) inString = null; + } else if (char === "'" || char === '"' || char === '`') inString = char; + else if (char === '(') depth += 1; + else if (char === ')') depth -= 1; + index += 1; + } + const call = text.slice(match.index, index); + if (/recursive\s*:\s*true/u.test(call)) { + calls.push({ + call, + hasRetries: /maxRetries\s*:/u.test(call), + line: text.slice(0, match.index).split('\n').length, + }); + } + match = pattern.exec(text); + } + return calls; +}; + +const failures = []; +const files = []; +for (const root of roots) await walk(root, files); +for (const file of files) { + const text = await readFile(file, 'utf8'); + for (const call of recursiveRmCalls(text)) { + if (call.hasRetries) continue; + failures.push(`${file}:${call.line} bare recursive rm. Use removeTree.`); + } +} + +if (failures.length > 0) { + console.error(failures.join('\n')); + process.exit(1); +} From fd6c94e78b9e4021d72a779e242fd75284db0370 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Sep 2026 20:13:12 -0700 Subject: [PATCH 03/12] ci: retrigger checks after unreproducible removeTree lint failure From d62dd5593f2c474b3cd1313e99fa88809b17f20a Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Sep 2026 20:26:34 -0700 Subject: [PATCH 04/12] fix(test): convert leftover recursive teardowns and catch aliased rm After merging main, three install-surface teardowns still used bare recursive rm. Convert them to removeTree, and teach the lint gate to recognize namespace and aliased Node fs removal bindings so those forms cannot escape the gate. --- .../tests/check-test-remove-tree.test.ts | 75 ++++++++ .../tests/install-surface.test.ts | 6 +- scripts/check-test-remove-tree.mjs | 182 +++++++++++++----- 3 files changed, 207 insertions(+), 56 deletions(-) create mode 100644 packages/agent-bundle/tests/check-test-remove-tree.test.ts 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..a7ea64184 --- /dev/null +++ b/packages/agent-bundle/tests/check-test-remove-tree.test.ts @@ -0,0 +1,75 @@ +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(['rm', 'remove']), + namespaceNames: new Set(), + }); + expect(removalBindings(`import * as fs from 'node:fs/promises';`)).toEqual({ + bareNames: new Set(['rm']), + namespaceNames: new Set(['fs']), + }); + expect(removalBindings(`import fs from 'node:fs';`)).toEqual({ + bareNames: new Set(['rm']), + 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.', + ]); +}); diff --git a/packages/agent-bundle/tests/install-surface.test.ts b/packages/agent-bundle/tests/install-surface.test.ts index 18fdee411..b1a0f4561 100644 --- a/packages/agent-bundle/tests/install-surface.test.ts +++ b/packages/agent-bundle/tests/install-surface.test.ts @@ -1128,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); } }); @@ -1176,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); } }); @@ -1212,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); } }); diff --git a/scripts/check-test-remove-tree.mjs b/scripts/check-test-remove-tree.mjs index 17231edac..5a0d65541 100644 --- a/scripts/check-test-remove-tree.mjs +++ b/scripts/check-test-remove-tree.mjs @@ -1,9 +1,13 @@ /** * 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. */ import { readdir, readFile } from 'node:fs/promises'; import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; const roots = [ 'packages/agent-bundle/tests', @@ -13,6 +17,10 @@ const roots = [ '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 { @@ -28,71 +36,139 @@ const walk = async (directory, files) => { } }; -const recursiveRmCalls = (text) => { - const calls = []; - const pattern = /\brm\s*\(/gu; - let match = pattern.exec(text); +const lineCommentAt = (text, index) => { + for (let cursor = index - 1; cursor >= 0; cursor -= 1) { + const char = text[cursor]; + if (char === '\n') return false; + if (char === '/' && text[cursor - 1] === '/') return true; + } + return false; +}; + +const sliceCall = (text, openParenIndex) => { + let depth = 1; + let inString = null; + let index = openParenIndex + 1; + while (index < text.length && depth > 0) { + const char = text[index]; + if (inString !== null) { + if (char === '\\') { + index += 2; + continue; + } + if (char === inString) inString = null; + } else if (char === "'" || char === '"' || char === String.fromCharCode(96)) inString = char; + else if (char === '(') depth += 1; + else if (char === ')') depth -= 1; + index += 1; + } + return { call: text.slice(0, index), end: index }; +}; + +/** Named/aliased rm bindings and namespace/default bindings that expose .rm. */ +export const removalBindings = (text) => { + const bareNames = new Set(['rm']); + const namespaceNames = new Set(); + + const named = /import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*(['"])([^'"]+)\2/gu; + let match = named.exec(text); while (match !== null) { - const before = text[match.index - 1]; - if (before === '.' || before === '$') { - match = pattern.exec(text); - continue; - } - let commented = false; - for (let index = match.index - 1; index >= 0; index -= 1) { - const char = text[index]; - if (char === '\n') break; - if (char === '/' && text[index - 1] === '/') { - commented = true; - break; + if (nodeFsSpecifier.test(match[3])) { + for (const part of match[1].split(',')) { + const specifier = part.trim(); + if (specifier.length === 0 || specifier.startsWith('type ')) continue; + const alias = /^\s*(?:type\s+)?rm(?:\s+as\s+([A-Za-z_$][\w$]*))?\s*$/u.exec(specifier); + if (alias === null) continue; + bareNames.add(alias[1] ?? 'rm'); } } - if (commented) { + match = named.exec(text); + } + + const star = /import\s*\*\s*as\s+([A-Za-z_$][\w$]*)\s*from\s*(['"])([^'"]+)\2/gu; + match = star.exec(text); + while (match !== null) { + if (nodeFsSpecifier.test(match[3])) namespaceNames.add(match[1]); + match = star.exec(text); + } + + const defaults = /import\s+([A-Za-z_$][\w$]*)\s*(?:,\s*\{[^}]*\})?\s*from\s*(['"])([^'"]+)\2/gu; + match = defaults.exec(text); + while (match !== null) { + if (nodeFsSpecifier.test(match[3])) namespaceNames.add(match[1]); + match = defaults.exec(text); + } + + return { bareNames, namespaceNames }; +}; + +const pushRecursiveCall = (calls, text, startIndex, openParenIndex) => { + if (lineCommentAt(text, startIndex)) return; + const { call } = sliceCall(text.slice(startIndex), openParenIndex - startIndex); + if (!/recursive\s*:\s*true/u.test(call)) return; + calls.push({ + call, + hasRetries: /maxRetries\s*:/u.test(call), + line: text.slice(0, startIndex).split('\n').length, + }); +}; + +export const recursiveRmCalls = (text) => { + const { bareNames, namespaceNames } = removalBindings(text); + const calls = []; + + for (const name of bareNames) { + const pattern = new RegExp(`\\b${name}\\s*\\(`, 'gu'); + let match = pattern.exec(text); + while (match !== null) { + const before = text[match.index - 1]; + if (before === '.' || before === '$') { + match = pattern.exec(text); + continue; + } + pushRecursiveCall(calls, text, match.index, match.index + match[0].length - 1); match = pattern.exec(text); - continue; } - const open = match.index + match[0].length - 1; - let depth = 1; - let inString = null; - let index = open + 1; - while (index < text.length && depth > 0) { - const char = text[index]; - if (inString !== null) { - if (char === '\\') { - index += 2; - continue; - } - if (char === inString) inString = null; - } else if (char === "'" || char === '"' || char === '`') inString = char; - else if (char === '(') depth += 1; - else if (char === ')') depth -= 1; - index += 1; - } - const call = text.slice(match.index, index); - if (/recursive\s*:\s*true/u.test(call)) { - calls.push({ - call, - hasRetries: /maxRetries\s*:/u.test(call), - line: text.slice(0, match.index).split('\n').length, - }); + } + + for (const namespace of namespaceNames) { + const pattern = new RegExp(`\\b${namespace}\\s*\\.\\s*rm\\s*\\(`, 'gu'); + let match = pattern.exec(text); + while (match !== null) { + pushRecursiveCall(calls, text, match.index, match.index + match[0].length - 1); + match = pattern.exec(text); } - match = pattern.exec(text); } + return calls; }; -const failures = []; -const files = []; -for (const root of roots) await walk(root, files); -for (const file of files) { - const text = await readFile(file, 'utf8'); +export const bareRecursiveRmFailures = (file, text) => { + if (isRemoveTreeHelper(file)) return []; + const failures = []; for (const call of recursiveRmCalls(text)) { 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 (failures.length > 0) { - console.error(failures.join('\n')); - process.exit(1); -} +if (invokedDirectly) await run(); From 4a37528052d4bf0ee5103ad366bda50d9ad64d10 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Sep 2026 20:39:22 -0700 Subject: [PATCH 05/12] fix(test): make remove-tree lint syntax-aware for comments and $ aliases Mask comments/strings before scanning, escape imported names literally, and read recursive/maxRetries from real options properties so GPT P2 cases pass. --- .../tests/check-test-remove-tree.test.ts | 40 ++++++ scripts/check-test-remove-tree.mjs | 136 +++++++++++++----- 2 files changed, 144 insertions(+), 32 deletions(-) diff --git a/packages/agent-bundle/tests/check-test-remove-tree.test.ts b/packages/agent-bundle/tests/check-test-remove-tree.test.ts index a7ea64184..95e000bd9 100644 --- a/packages/agent-bundle/tests/check-test-remove-tree.test.ts +++ b/packages/agent-bundle/tests/check-test-remove-tree.test.ts @@ -73,3 +73,43 @@ it('exempts the canonical removeTree helper and formats lint failures', () => { '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.', + ]); +}); diff --git a/scripts/check-test-remove-tree.mjs b/scripts/check-test-remove-tree.mjs index 5a0d65541..073f8e262 100644 --- a/scripts/check-test-remove-tree.mjs +++ b/scripts/check-test-remove-tree.mjs @@ -4,6 +4,10 @@ * * 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 and option detection is syntax-aware: comments and string/template contents + * are masked before matching, imported names are matched as literals (including `$`), + * and `recursive` / `maxRetries` are read from actual options-object properties. */ import { readdir, readFile } from 'node:fs/promises'; import { join } from 'node:path'; @@ -21,6 +25,10 @@ const nodeFsSpecifier = /^(?:node:)?fs(?:\/promises)?$/u; const isRemoveTreeHelper = (file) => /(?:^|\/)remove-tree\.ts$/u.test(file.replaceAll('\\', '/')); +const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); + +const isIdentPart = (char) => char !== undefined && /[\w$]/u.test(char); + const walk = async (directory, files) => { let entries; try { @@ -36,29 +44,78 @@ const walk = async (directory, files) => { } }; -const lineCommentAt = (text, index) => { - for (let cursor = index - 1; cursor >= 0; cursor -= 1) { - const char = text[cursor]; - if (char === '\n') return false; - if (char === '/' && text[cursor - 1] === '/') return true; +/** + * Replace comments and string/template contents with spaces so call/property + * scans see only code tokens. Length and newlines are preserved for line numbers. + */ +export const maskCommentsAndStrings = (text) => { + let result = ''; + let index = 0; + while (index < text.length) { + const char = text[index]; + if (char === '/' && text[index + 1] === '/') { + while (index < text.length && text[index] !== '\n') { + result += ' '; + index += 1; + } + continue; + } + if (char === '/' && text[index + 1] === '*') { + result += ' '; + index += 2; + while (index < text.length) { + if (text[index] === '\n') { + result += '\n'; + index += 1; + continue; + } + if (text[index] === '*' && text[index + 1] === '/') { + result += ' '; + index += 2; + break; + } + result += ' '; + index += 1; + } + continue; + } + if (char === "'" || char === '"' || char === '`') { + const quote = char; + result += ' '; + index += 1; + while (index < text.length) { + if (text[index] === '\\') { + result += ' '; + index += 2; + continue; + } + if (text[index] === '\n') { + result += '\n'; + index += 1; + continue; + } + if (text[index] === quote) { + result += ' '; + index += 1; + break; + } + result += ' '; + index += 1; + } + continue; + } + result += char; + index += 1; } - return false; + return result; }; const sliceCall = (text, openParenIndex) => { let depth = 1; - let inString = null; let index = openParenIndex + 1; while (index < text.length && depth > 0) { const char = text[index]; - if (inString !== null) { - if (char === '\\') { - index += 2; - continue; - } - if (char === inString) inString = null; - } else if (char === "'" || char === '"' || char === String.fromCharCode(96)) inString = char; - else if (char === '(') depth += 1; + if (char === '(') depth += 1; else if (char === ')') depth -= 1; index += 1; } @@ -102,41 +159,56 @@ export const removalBindings = (text) => { return { bareNames, namespaceNames }; }; -const pushRecursiveCall = (calls, text, startIndex, openParenIndex) => { - if (lineCommentAt(text, startIndex)) return; - const { call } = sliceCall(text.slice(startIndex), openParenIndex - startIndex); - if (!/recursive\s*:\s*true/u.test(call)) return; +/** + * Options flags from the call's object-literal properties (code already masked). + * Matches property keys, not comment/string text that previously looked like them. + */ +const optionsFlags = (call) => ({ + recursive: /(?:^|[,{\s])recursive\s*:\s*true\b/u.test(call), + hasRetries: /(?:^|[,{\s])maxRetries\s*:/u.test(call), +}); + +const pushRecursiveCall = (calls, code, startIndex, openParenIndex) => { + const { call } = sliceCall(code.slice(startIndex), openParenIndex - startIndex); + const flags = optionsFlags(call); + if (!flags.recursive) return; calls.push({ call, - hasRetries: /maxRetries\s*:/u.test(call), - line: text.slice(0, startIndex).split('\n').length, + hasRetries: flags.hasRetries, + line: code.slice(0, startIndex).split('\n').length, }); }; export const recursiveRmCalls = (text) => { const { bareNames, namespaceNames } = removalBindings(text); + const code = maskCommentsAndStrings(text); const calls = []; for (const name of bareNames) { - const pattern = new RegExp(`\\b${name}\\s*\\(`, 'gu'); - let match = pattern.exec(text); + const pattern = new RegExp(`${escapeRegExp(name)}\\s*\\(`, 'gu'); + let match = pattern.exec(code); while (match !== null) { - const before = text[match.index - 1]; - if (before === '.' || before === '$') { - match = pattern.exec(text); + const before = code[match.index - 1]; + if (isIdentPart(before) || before === '.') { + match = pattern.exec(code); continue; } - pushRecursiveCall(calls, text, match.index, match.index + match[0].length - 1); - match = pattern.exec(text); + pushRecursiveCall(calls, code, match.index, match.index + match[0].length - 1); + match = pattern.exec(code); } } for (const namespace of namespaceNames) { - const pattern = new RegExp(`\\b${namespace}\\s*\\.\\s*rm\\s*\\(`, 'gu'); - let match = pattern.exec(text); + const pattern = new RegExp(`${escapeRegExp(namespace)}\\s*\\.\\s*rm\\s*\\(`, 'gu'); + let match = pattern.exec(code); while (match !== null) { - pushRecursiveCall(calls, text, match.index, match.index + match[0].length - 1); - match = pattern.exec(text); + const before = code[match.index - 1]; + if (isIdentPart(before) || before === '.') { + match = pattern.exec(code); + continue; + } + pushRecursiveCall(calls, code, match.index, match.index + match[0].length - 1); + match = pattern.exec(code); } } From 4e3af17783509d440cbfd4f50a06c138146b5d19 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 17 Sep 2026 03:39:25 +0000 Subject: [PATCH 06/12] test(support): remove the tmp dir the persistent-ENOTEMPTY test leaves behind --- packages/agent-bundle/tests/support/remove-tree.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/agent-bundle/tests/support/remove-tree.test.ts b/packages/agent-bundle/tests/support/remove-tree.test.ts index 92eb3225d..7f431b01a 100644 --- a/packages/agent-bundle/tests/support/remove-tree.test.ts +++ b/packages/agent-bundle/tests/support/remove-tree.test.ts @@ -35,4 +35,5 @@ it('removeTree surfaces a persistent ENOTEMPTY', async () => { }; await expect(removeTree(root, fs)).rejects.toBe(emptyError); expect((await stat(root)).isDirectory()).toBe(true); + await removeTree(root); }); From 9f8184d1758fc0cdb380359228b756fe1c3703db Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 17 Sep 2026 03:41:08 +0000 Subject: [PATCH 07/12] test: declare scripts/check-test-remove-tree.mjs for the gate test's typecheck --- scripts/check-test-remove-tree.d.mts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 scripts/check-test-remove-tree.d.mts diff --git a/scripts/check-test-remove-tree.d.mts b/scripts/check-test-remove-tree.d.mts new file mode 100644 index 000000000..e11e095dd --- /dev/null +++ b/scripts/check-test-remove-tree.d.mts @@ -0,0 +1,21 @@ +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 maskCommentsAndStrings: (text: string) => string; + +export declare const removalBindings: (text: string) => RemovalBindings; + +export declare const recursiveRmCalls: (text: string) => RecursiveRmCall[]; + +export declare const bareRecursiveRmFailures: (file: string, text: string) => string[]; From 31151ad24b07024ab82e2380f616733114e16aa6 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Sep 2026 20:56:40 -0700 Subject: [PATCH 08/12] fix(test): parse remove-tree lint calls with typescript-5 Replace comment/string masking with an AST walk so options come only from the second argument (including quoted keys), and regex/template edge cases no longer hide or invent recursive rm hits. --- .../tests/check-test-remove-tree.test.ts | 39 ++++ scripts/check-test-remove-tree.d.mts | 4 +- scripts/check-test-remove-tree.mjs | 209 +++++++----------- 3 files changed, 116 insertions(+), 136 deletions(-) diff --git a/packages/agent-bundle/tests/check-test-remove-tree.test.ts b/packages/agent-bundle/tests/check-test-remove-tree.test.ts index 95e000bd9..61ff4f43f 100644 --- a/packages/agent-bundle/tests/check-test-remove-tree.test.ts +++ b/packages/agent-bundle/tests/check-test-remove-tree.test.ts @@ -113,3 +113,42 @@ it('matches $-suffixed removal aliases literally', () => { '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([]); +}); diff --git a/scripts/check-test-remove-tree.d.mts b/scripts/check-test-remove-tree.d.mts index e11e095dd..79fa61f32 100644 --- a/scripts/check-test-remove-tree.d.mts +++ b/scripts/check-test-remove-tree.d.mts @@ -12,10 +12,8 @@ export interface RemovalBindings { readonly namespaceNames: ReadonlySet; } -export declare const maskCommentsAndStrings: (text: string) => string; - export declare const removalBindings: (text: string) => RemovalBindings; -export declare const recursiveRmCalls: (text: string) => RecursiveRmCall[]; +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 index 073f8e262..13634cd24 100644 --- a/scripts/check-test-remove-tree.mjs +++ b/scripts/check-test-remove-tree.mjs @@ -5,13 +5,20 @@ * 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 and option detection is syntax-aware: comments and string/template contents - * are masked before matching, imported names are matched as literals (including `$`), - * and `recursive` / `maxRetries` are read from actual options-object properties. + * Call and option detection is parser-backed (typescript-5): 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 { join } from 'node:path'; -import { pathToFileURL } from 'node:url'; +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', @@ -25,10 +32,6 @@ const nodeFsSpecifier = /^(?:node:)?fs(?:\/promises)?$/u; const isRemoveTreeHelper = (file) => /(?:^|\/)remove-tree\.ts$/u.test(file.replaceAll('\\', '/')); -const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'); - -const isIdentPart = (char) => char !== undefined && /[\w$]/u.test(char); - const walk = async (directory, files) => { let entries; try { @@ -44,84 +47,6 @@ const walk = async (directory, files) => { } }; -/** - * Replace comments and string/template contents with spaces so call/property - * scans see only code tokens. Length and newlines are preserved for line numbers. - */ -export const maskCommentsAndStrings = (text) => { - let result = ''; - let index = 0; - while (index < text.length) { - const char = text[index]; - if (char === '/' && text[index + 1] === '/') { - while (index < text.length && text[index] !== '\n') { - result += ' '; - index += 1; - } - continue; - } - if (char === '/' && text[index + 1] === '*') { - result += ' '; - index += 2; - while (index < text.length) { - if (text[index] === '\n') { - result += '\n'; - index += 1; - continue; - } - if (text[index] === '*' && text[index + 1] === '/') { - result += ' '; - index += 2; - break; - } - result += ' '; - index += 1; - } - continue; - } - if (char === "'" || char === '"' || char === '`') { - const quote = char; - result += ' '; - index += 1; - while (index < text.length) { - if (text[index] === '\\') { - result += ' '; - index += 2; - continue; - } - if (text[index] === '\n') { - result += '\n'; - index += 1; - continue; - } - if (text[index] === quote) { - result += ' '; - index += 1; - break; - } - result += ' '; - index += 1; - } - continue; - } - result += char; - index += 1; - } - return result; -}; - -const sliceCall = (text, openParenIndex) => { - let depth = 1; - let index = openParenIndex + 1; - while (index < text.length && depth > 0) { - const char = text[index]; - if (char === '(') depth += 1; - else if (char === ')') depth -= 1; - index += 1; - } - return { call: text.slice(0, index), end: index }; -}; - /** Named/aliased rm bindings and namespace/default bindings that expose .rm. */ export const removalBindings = (text) => { const bareNames = new Set(['rm']); @@ -159,66 +84,84 @@ export const removalBindings = (text) => { return { bareNames, namespaceNames }; }; -/** - * Options flags from the call's object-literal properties (code already masked). - * Matches property keys, not comment/string text that previously looked like them. - */ -const optionsFlags = (call) => ({ - recursive: /(?:^|[,{\s])recursive\s*:\s*true\b/u.test(call), - hasRetries: /(?:^|[,{\s])maxRetries\s*:/u.test(call), -}); - -const pushRecursiveCall = (calls, code, startIndex, openParenIndex) => { - const { call } = sliceCall(code.slice(startIndex), openParenIndex - startIndex); - const flags = optionsFlags(call); - if (!flags.recursive) return; - calls.push({ - call, - hasRetries: flags.hasRetries, - line: code.slice(0, startIndex).split('\n').length, - }); +const propertyName = (name) => { + if (ts.isIdentifier(name)) return name.text; + if (ts.isStringLiteral(name) || ts.isNoSubstitutionTemplateLiteral(name)) return name.text; + return undefined; }; -export const recursiveRmCalls = (text) => { - const { bareNames, namespaceNames } = removalBindings(text); - const code = maskCommentsAndStrings(text); - const calls = []; - - for (const name of bareNames) { - const pattern = new RegExp(`${escapeRegExp(name)}\\s*\\(`, 'gu'); - let match = pattern.exec(code); - while (match !== null) { - const before = code[match.index - 1]; - if (isIdentPart(before) || before === '.') { - match = pattern.exec(code); - continue; - } - pushRecursiveCall(calls, code, match.index, match.index + match[0].length - 1); - match = pattern.exec(code); +/** 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 }; +}; - for (const namespace of namespaceNames) { - const pattern = new RegExp(`${escapeRegExp(namespace)}\\s*\\.\\s*rm\\s*\\(`, 'gu'); - let match = pattern.exec(code); - while (match !== null) { - const before = code[match.index - 1]; - if (isIdentPart(before) || before === '.') { - match = pattern.exec(code); - continue; +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, + }); } - pushRecursiveCall(calls, code, match.index, match.index + match[0].length - 1); - match = pattern.exec(code); } - } + 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)) { + for (const call of recursiveRmCalls(text, file)) { if (call.hasRetries) continue; failures.push(`${file}:${call.line} bare recursive rm. Use removeTree.`); } From e2be2d3d20c5f391b9d3a2b8123e875cada4ba3c Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Sep 2026 21:08:52 -0700 Subject: [PATCH 09/12] fix(test): parse Node fs removal bindings via TypeScript AST Stop forging bare `rm` bindings from text regex so local identifiers and commented-out imports cannot bypass or falsely trip the removeTree lint gate. --- .../tests/check-test-remove-tree.test.ts | 40 +++++++++- scripts/check-test-remove-tree.mjs | 78 ++++++++++++------- 2 files changed, 85 insertions(+), 33 deletions(-) diff --git a/packages/agent-bundle/tests/check-test-remove-tree.test.ts b/packages/agent-bundle/tests/check-test-remove-tree.test.ts index 61ff4f43f..524e51a1d 100644 --- a/packages/agent-bundle/tests/check-test-remove-tree.test.ts +++ b/packages/agent-bundle/tests/check-test-remove-tree.test.ts @@ -16,15 +16,15 @@ it('tracks named, aliased, and namespace Node fs removal bindings', () => { namespaceNames: new Set(), }); expect(removalBindings(`import { rm as remove } from 'node:fs/promises';`)).toEqual({ - bareNames: new Set(['rm', 'remove']), + bareNames: new Set(['remove']), namespaceNames: new Set(), }); expect(removalBindings(`import * as fs from 'node:fs/promises';`)).toEqual({ - bareNames: new Set(['rm']), + bareNames: new Set(), namespaceNames: new Set(['fs']), }); expect(removalBindings(`import fs from 'node:fs';`)).toEqual({ - bareNames: new Set(['rm']), + bareNames: new Set(), namespaceNames: new Set(['fs']), }); }); @@ -152,3 +152,37 @@ it('keeps regex literals, template substitutions, and spaced member calls correc `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([]); + + // Commented-out import must not create a binding. + 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 }), + ]); +}); diff --git a/scripts/check-test-remove-tree.mjs b/scripts/check-test-remove-tree.mjs index 13634cd24..a63b1a9e3 100644 --- a/scripts/check-test-remove-tree.mjs +++ b/scripts/check-test-remove-tree.mjs @@ -5,9 +5,10 @@ * 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 and option detection is parser-backed (typescript-5): only Node-bound call - * expressions are considered, and `recursive` / `maxRetries` are read from the - * second argument's object-literal properties (including quoted keys). Nested + * 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. */ @@ -47,38 +48,55 @@ const walk = async (directory, files) => { } }; -/** Named/aliased rm bindings and namespace/default bindings that expose .rm. */ -export const removalBindings = (text) => { - const bareNames = new Set(['rm']); +/** + * 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), + ); - const named = /import\s*(?:type\s*)?\{([^}]*)\}\s*from\s*(['"])([^'"]+)\2/gu; - let match = named.exec(text); - while (match !== null) { - if (nodeFsSpecifier.test(match[3])) { - for (const part of match[1].split(',')) { - const specifier = part.trim(); - if (specifier.length === 0 || specifier.startsWith('type ')) continue; - const alias = /^\s*(?:type\s+)?rm(?:\s+as\s+([A-Za-z_$][\w$]*))?\s*$/u.exec(specifier); - if (alias === null) continue; - bareNames.add(alias[1] ?? 'rm'); - } + for (const statement of sourceFile.statements) { + if (!ts.isImportDeclaration(statement) || statement.importClause === undefined) continue; + if (statement.moduleSpecifier === undefined || !ts.isStringLiteral(statement.moduleSpecifier)) { + continue; } - match = named.exec(text); - } + if (!nodeFsSpecifier.test(statement.moduleSpecifier.text)) continue; - const star = /import\s*\*\s*as\s+([A-Za-z_$][\w$]*)\s*from\s*(['"])([^'"]+)\2/gu; - match = star.exec(text); - while (match !== null) { - if (nodeFsSpecifier.test(match[3])) namespaceNames.add(match[1]); - match = star.exec(text); - } + const { importClause } = statement; + if (importClause.isTypeOnly) continue; + + if (importClause.name !== undefined) { + namespaceNames.add(importClause.name.text); + } + + const bindings = importClause.namedBindings; + if (bindings === undefined) continue; - const defaults = /import\s+([A-Za-z_$][\w$]*)\s*(?:,\s*\{[^}]*\})?\s*from\s*(['"])([^'"]+)\2/gu; - match = defaults.exec(text); - while (match !== null) { - if (nodeFsSpecifier.test(match[3])) namespaceNames.add(match[1]); - match = defaults.exec(text); + 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 }; From 3d6ba6cea2101c480d976c868c0493c52e796d8e Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Sep 2026 21:16:27 -0700 Subject: [PATCH 10/12] test(remove-tree): cover AST binding regressions for GPT P2 Lock commented bare imports, non-fs rm imports, and aliased/namespace maxRetries pass paths so removalBindings stays ImportDeclaration-backed. --- .../tests/check-test-remove-tree.test.ts | 56 ++++++++++++++++++- scripts/check-test-remove-tree.d.mts | 2 +- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/agent-bundle/tests/check-test-remove-tree.test.ts b/packages/agent-bundle/tests/check-test-remove-tree.test.ts index 524e51a1d..2b6458b8e 100644 --- a/packages/agent-bundle/tests/check-test-remove-tree.test.ts +++ b/packages/agent-bundle/tests/check-test-remove-tree.test.ts @@ -160,7 +160,21 @@ it('only counts real AST Node fs import bindings', () => { `await rm(root, { ${recursiveTrue} });`, ]))).toEqual([]); - // Commented-out import must not create a binding. + // 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} });`, @@ -186,3 +200,43 @@ it('only counts real AST Node fs import bindings', () => { 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/scripts/check-test-remove-tree.d.mts b/scripts/check-test-remove-tree.d.mts index 79fa61f32..d7056e395 100644 --- a/scripts/check-test-remove-tree.d.mts +++ b/scripts/check-test-remove-tree.d.mts @@ -12,7 +12,7 @@ export interface RemovalBindings { readonly namespaceNames: ReadonlySet; } -export declare const removalBindings: (text: string) => RemovalBindings; +export declare const removalBindings: (text: string, fileName?: string) => RemovalBindings; export declare const recursiveRmCalls: (text: string, fileName?: string) => RecursiveRmCall[]; From cf340ceb16ae3c99fe3182785ef14f7fa0fcf997 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Sep 2026 21:58:19 -0700 Subject: [PATCH 11/12] fix(test): route #824 install teardown through removeTree After rebasing onto main, the Codex add-only install tests still used bare recursive rm in finally blocks; the remove-tree lint gate correctly failed. --- packages/agent-bundle/tests/install.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/agent-bundle/tests/install.test.ts b/packages/agent-bundle/tests/install.test.ts index 870d47bb1..25cf28b0b 100644 --- a/packages/agent-bundle/tests/install.test.ts +++ b/packages/agent-bundle/tests/install.test.ts @@ -735,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); } }); @@ -778,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); } }); @@ -826,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); } }); @@ -889,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); } }); From a8c65bf5d72e07dd1b873d79d7ff1a43cb363ea3 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Wed, 16 Sep 2026 21:59:21 -0700 Subject: [PATCH 12/12] ci: retrigger checks after cancelled CI attempts on removeTree PR