@vibe-agent-toolkit/utils 0.1.42 → 0.2.0-rc.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -19
- package/dist/asset-reference.js +3 -11
- package/dist/asset-reference.js.map +1 -1
- package/dist/compare-code-units.d.ts +28 -0
- package/dist/compare-code-units.d.ts.map +1 -0
- package/dist/compare-code-units.js +33 -0
- package/dist/compare-code-units.js.map +1 -0
- package/dist/crawl-timing.d.ts +657 -0
- package/dist/crawl-timing.d.ts.map +1 -0
- package/dist/crawl-timing.js +765 -0
- package/dist/crawl-timing.js.map +1 -0
- package/dist/crawl.d.ts +2 -4
- package/dist/crawl.d.ts.map +1 -1
- package/dist/crawl.js +2 -4
- package/dist/crawl.js.map +1 -1
- package/dist/dirent-kind.d.ts +83 -0
- package/dist/dirent-kind.d.ts.map +1 -0
- package/dist/dirent-kind.js +129 -0
- package/dist/dirent-kind.js.map +1 -0
- package/dist/entrypoint.d.ts +54 -0
- package/dist/entrypoint.d.ts.map +1 -0
- package/dist/entrypoint.js +71 -0
- package/dist/entrypoint.js.map +1 -0
- package/dist/env-flag.d.ts +51 -0
- package/dist/env-flag.d.ts.map +1 -0
- package/dist/env-flag.js +62 -0
- package/dist/env-flag.js.map +1 -0
- package/dist/errors/errno.d.ts +51 -0
- package/dist/errors/errno.d.ts.map +1 -0
- package/dist/errors/errno.js +112 -0
- package/dist/errors/errno.js.map +1 -0
- package/dist/errors/vat-error.d.ts +60 -0
- package/dist/errors/vat-error.d.ts.map +1 -0
- package/dist/errors/vat-error.js +86 -0
- package/dist/errors/vat-error.js.map +1 -0
- package/dist/file-crawler.d.ts +60 -0
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +214 -24
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs/file-hash.d.ts.map +1 -1
- package/dist/fs/file-hash.js +0 -1
- package/dist/fs/file-hash.js.map +1 -1
- package/dist/fs-utils.d.ts +628 -38
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +770 -71
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +5 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +9 -1
- package/dist/fs.js.map +1 -1
- package/dist/git-root-cache.d.ts +44 -0
- package/dist/git-root-cache.d.ts.map +1 -0
- package/dist/git-root-cache.js +68 -0
- package/dist/git-root-cache.js.map +1 -0
- package/dist/git-run.d.ts +167 -0
- package/dist/git-run.d.ts.map +1 -0
- package/dist/git-run.js +154 -0
- package/dist/git-run.js.map +1 -0
- package/dist/git-snapshot.d.ts +199 -0
- package/dist/git-snapshot.d.ts.map +1 -0
- package/dist/git-snapshot.js +250 -0
- package/dist/git-snapshot.js.map +1 -0
- package/dist/git-tracker.d.ts +166 -3
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +250 -22
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts +95 -1
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +227 -81
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +13 -4
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +12 -4
- package/dist/git.js.map +1 -1
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +15 -6
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/glob/glob-pattern.js +5 -5
- package/dist/glob/glob-pattern.js.map +1 -1
- package/dist/index.d.ts +33 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +113 -33
- package/dist/index.js.map +1 -1
- package/dist/listing-refusal.d.ts +112 -0
- package/dist/listing-refusal.d.ts.map +1 -0
- package/dist/listing-refusal.js +105 -0
- package/dist/listing-refusal.js.map +1 -0
- package/dist/numeric-args.d.ts +24 -0
- package/dist/numeric-args.d.ts.map +1 -0
- package/dist/numeric-args.js +37 -0
- package/dist/numeric-args.js.map +1 -0
- package/dist/path-containment.d.ts +64 -0
- package/dist/path-containment.d.ts.map +1 -0
- package/dist/path-containment.js +111 -0
- package/dist/path-containment.js.map +1 -0
- package/dist/path-core.d.ts +143 -18
- package/dist/path-core.d.ts.map +1 -1
- package/dist/path-core.js +171 -23
- package/dist/path-core.js.map +1 -1
- package/dist/path-utils.d.ts.map +1 -1
- package/dist/path-utils.js +42 -47
- package/dist/path-utils.js.map +1 -1
- package/dist/path.d.ts +1 -1
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +1 -1
- package/dist/path.js.map +1 -1
- package/dist/process.d.ts +7 -0
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +7 -0
- package/dist/process.js.map +1 -1
- package/dist/project-utils.d.ts +7 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +24 -10
- package/dist/project-utils.js.map +1 -1
- package/dist/project.d.ts +7 -5
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +7 -5
- package/dist/project.js.map +1 -1
- package/dist/safe-exec.d.ts +40 -11
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +111 -25
- package/dist/safe-exec.js.map +1 -1
- package/dist/skill-targets.d.ts +2 -2
- package/dist/skill-targets.js +2 -2
- package/dist/skill-test/auth-resolver.d.ts +10 -2
- package/dist/skill-test/auth-resolver.d.ts.map +1 -1
- package/dist/skill-test/auth-resolver.js +26 -11
- package/dist/skill-test/auth-resolver.js.map +1 -1
- package/dist/skill-test/index.d.ts +1 -1
- package/dist/skill-test/index.d.ts.map +1 -1
- package/dist/skill-test/index.js +1 -1
- package/dist/skill-test/index.js.map +1 -1
- package/dist/skill-test/spawn-claude.d.ts +30 -1
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +71 -5
- package/dist/skill-test/spawn-claude.js.map +1 -1
- package/dist/skill-test/transcript.d.ts +41 -2
- package/dist/skill-test/transcript.d.ts.map +1 -1
- package/dist/skill-test/transcript.js +55 -10
- package/dist/skill-test/transcript.js.map +1 -1
- package/dist/spawn-hardened.js +0 -1
- package/dist/spawn-hardened.js.map +1 -1
- package/dist/stdio-blocking.js +13 -14
- package/dist/stdio-blocking.js.map +1 -1
- package/dist/test-helpers.d.ts +130 -79
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +229 -124
- package/dist/test-helpers.js.map +1 -1
- package/dist/testing/executables.d.ts +32 -0
- package/dist/testing/executables.d.ts.map +1 -0
- package/dist/testing/executables.js +70 -0
- package/dist/testing/executables.js.map +1 -0
- package/dist/testing/hostile-tree.d.ts +79 -0
- package/dist/testing/hostile-tree.d.ts.map +1 -0
- package/dist/testing/hostile-tree.js +164 -0
- package/dist/testing/hostile-tree.js.map +1 -0
- package/dist/testing/platform-gates.d.ts +31 -0
- package/dist/testing/platform-gates.d.ts.map +1 -0
- package/dist/testing/platform-gates.js +31 -0
- package/dist/testing/platform-gates.js.map +1 -0
- package/dist/testing/temp-dir.d.ts +255 -0
- package/dist/testing/temp-dir.d.ts.map +1 -0
- package/dist/testing/temp-dir.js +363 -0
- package/dist/testing/temp-dir.js.map +1 -0
- package/dist/testing.d.ts +94 -3
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +119 -3
- package/dist/testing.js.map +1 -1
- package/dist/text-content.d.ts +197 -0
- package/dist/text-content.d.ts.map +1 -0
- package/dist/text-content.js +354 -0
- package/dist/text-content.js.map +1 -0
- package/dist/text-file.d.ts +45 -0
- package/dist/text-file.d.ts.map +1 -0
- package/dist/text-file.js +51 -0
- package/dist/text-file.js.map +1 -0
- package/dist/text.d.ts +16 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +16 -0
- package/dist/text.js.map +1 -0
- package/dist/timing-dump.d.ts +136 -0
- package/dist/timing-dump.d.ts.map +1 -0
- package/dist/timing-dump.js +229 -0
- package/dist/timing-dump.js.map +1 -0
- package/dist/windows-shell.d.ts +0 -1
- package/dist/windows-shell.d.ts.map +1 -1
- package/dist/windows-shell.js +0 -1
- package/dist/windows-shell.js.map +1 -1
- package/dist/zod-introspection.js +2 -2
- package/dist/zod-introspection.js.map +1 -1
- package/eslint/README.md +224 -50
- package/eslint/index.cjs +111 -97
- package/eslint/index.d.cts +16 -6
- package/eslint/rules/callee-name.cjs +23 -0
- package/eslint/rules/commands-import-boundary.cjs +148 -0
- package/eslint/rules/dead-import.cjs +62 -12
- package/eslint/rules/dirent-type-needs-symlink-check.cjs +282 -0
- package/eslint/rules/eslint-rule-factory.cjs +21 -3
- package/eslint/rules/exempt-path-matcher.cjs +2 -1
- package/eslint/rules/explicit-zod-strictness.cjs +120 -0
- package/eslint/rules/no-bare-dynamic-import-path.cjs +5 -1
- package/eslint/rules/no-bare-executable-spawn.cjs +70 -0
- package/eslint/rules/no-bare-symlink-in-tests.cjs +213 -0
- package/eslint/rules/no-blind-catch.cjs +171 -0
- package/eslint/rules/no-child-process-execSync.cjs +8 -0
- package/eslint/rules/no-decaying-referent.cjs +217 -0
- package/eslint/rules/no-dotdot-containment.cjs +160 -0
- package/eslint/rules/no-file-url-string-concat.cjs +4 -1
- package/eslint/rules/no-fragile-entrypoint-guard.cjs +228 -0
- package/eslint/rules/no-fs-mkdirSync.cjs +8 -0
- package/eslint/rules/no-fs-promises-cp.cjs +7 -0
- package/eslint/rules/no-fs-realpathSync.cjs +8 -0
- package/eslint/rules/no-hardcoded-path-split.cjs +29 -18
- package/eslint/rules/no-io-in-unit-tier.cjs +129 -0
- package/eslint/rules/no-literal-process-exit.cjs +129 -0
- package/eslint/rules/no-manual-path-normalize.cjs +146 -96
- package/eslint/rules/no-os-tmpdir.cjs +8 -0
- package/eslint/rules/no-path-operations-in-comparisons.cjs +7 -2
- package/eslint/rules/no-path-sep-in-strings.cjs +7 -2
- package/eslint/rules/no-path-startswith.cjs +7 -2
- package/eslint/rules/no-process-exit-in-phase.cjs +127 -0
- package/eslint/rules/no-raw-node-path.cjs +563 -0
- package/eslint/rules/no-raw-text-decode.cjs +305 -0
- package/eslint/rules/no-registry-count-pin.cjs +255 -0
- package/eslint/rules/no-self-package-import.cjs +168 -0
- package/eslint/rules/no-test-scoped-functions.cjs +10 -3
- package/eslint/rules/no-unix-shell-commands.cjs +6 -2
- package/eslint/rules/no-unsafe-root-join.cjs +23 -2
- package/eslint/rules/no-url-pathname-for-fs.cjs +5 -1
- package/eslint/rules/no-version-literal.cjs +151 -0
- package/eslint/rules/prefer-startswith-over-regex.cjs +33 -1
- package/eslint/rules/require-justified-skip.cjs +8 -2
- package/eslint/rules/safe-import.cjs +1 -1
- package/package.json +16 -16
- package/dist/link-auth/build-headers.d.ts +0 -34
- package/dist/link-auth/build-headers.d.ts.map +0 -1
- package/dist/link-auth/build-headers.js +0 -58
- package/dist/link-auth/build-headers.js.map +0 -1
- package/dist/link-auth/expand-macro.d.ts +0 -38
- package/dist/link-auth/expand-macro.d.ts.map +0 -1
- package/dist/link-auth/expand-macro.js +0 -133
- package/dist/link-auth/expand-macro.js.map +0 -1
- package/dist/link-auth/macros.yaml +0 -50
- package/dist/link-auth/resolve-token.d.ts +0 -83
- package/dist/link-auth/resolve-token.d.ts.map +0 -1
- package/dist/link-auth/resolve-token.js +0 -101
- package/dist/link-auth/resolve-token.js.map +0 -1
- package/dist/link-auth/resolve.d.ts +0 -102
- package/dist/link-auth/resolve.d.ts.map +0 -1
- package/dist/link-auth/resolve.js +0 -66
- package/dist/link-auth/resolve.js.map +0 -1
- package/dist/link-auth/rewrite.d.ts +0 -52
- package/dist/link-auth/rewrite.d.ts.map +0 -1
- package/dist/link-auth/rewrite.js +0 -102
- package/dist/link-auth/rewrite.js.map +0 -1
- package/dist/link-auth/select-provider.d.ts +0 -30
- package/dist/link-auth/select-provider.d.ts.map +0 -1
- package/dist/link-auth/select-provider.js +0 -55
- package/dist/link-auth/select-provider.js.map +0 -1
- package/dist/link-auth/template.d.ts +0 -40
- package/dist/link-auth/template.d.ts.map +0 -1
- package/dist/link-auth/template.js +0 -89
- package/dist/link-auth/template.js.map +0 -1
- package/dist/link-auth/transforms.d.ts +0 -46
- package/dist/link-auth/transforms.d.ts.map +0 -1
- package/dist/link-auth/transforms.js +0 -52
- package/dist/link-auth/transforms.js.map +0 -1
- package/dist/template-entry.d.ts +0 -10
- package/dist/template-entry.d.ts.map +0 -1
- package/dist/template-entry.js +0 -10
- package/dist/template-entry.js.map +0 -1
- package/dist/template.d.ts +0 -7
- package/dist/template.d.ts.map +0 -1
- package/dist/template.js +0 -18
- package/dist/template.js.map +0 -1
- package/eslint/rules/no-path-join.cjs +0 -14
- package/eslint/rules/no-path-relative.cjs +0 -14
- package/eslint/rules/no-path-resolve.cjs +0 -14
- package/eslint/rules/path-function-rule-factory.cjs +0 -417
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE temp-directory helper family.
|
|
3
|
+
*
|
|
4
|
+
* Every shape a suite needs to mint scratch space lives here: a per-call
|
|
5
|
+
* `createTempDir`, a tracker that removes what it minted, and the per-suite
|
|
6
|
+
* sync/async suite helpers with their bounded teardown. The audit counted
|
|
7
|
+
* eleven definitions of "give me a temp dir that cleans itself up" across
|
|
8
|
+
* eight modules, six of them named differently and none of them visible to
|
|
9
|
+
* the duplication gate; this module is where they were collapsed to.
|
|
10
|
+
*
|
|
11
|
+
* ⛔ Framework-free, like everything under `testing/`: nothing here imports
|
|
12
|
+
* `vitest`, so the `./testing` subpath keeps the empty third-party set its
|
|
13
|
+
* purity pin asserts. Each suite owns its own hooks and calls these from
|
|
14
|
+
* inside them.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Mint a fresh temp directory under the host's (short-name-resolved) tmpdir.
|
|
18
|
+
*
|
|
19
|
+
* The one-liner that used to be spelled in three packages as
|
|
20
|
+
* `createTestTempDir`. Pair with {@link removeTempDir}, or let
|
|
21
|
+
* {@link tempDirTracker} pair them for you.
|
|
22
|
+
*
|
|
23
|
+
* @param prefix - `mkdtemp` prefix, so a leaked directory names its own suite
|
|
24
|
+
* @returns Absolute, forward-slashed path of the new directory
|
|
25
|
+
*/
|
|
26
|
+
export declare function createTempDir(prefix: string): string;
|
|
27
|
+
/** The async counterpart of {@link createTempDir}, for suites already on `fs/promises`. */
|
|
28
|
+
export declare function createTempDirAsync(prefix: string): Promise<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Remove a directory {@link createTempDir} minted — and ONLY such a directory.
|
|
31
|
+
*
|
|
32
|
+
* Refuses, by name, to remove anything that is not strictly under the host
|
|
33
|
+
* tmpdir as the filesystem sees it. A teardown is the one place test code
|
|
34
|
+
* runs `rm -rf` on a variable, and a variable that was never assigned (a
|
|
35
|
+
* `beforeAll` that threw), or was assigned a fixture INSIDE the repo, must
|
|
36
|
+
* not become `rm -rf ''` or `rm -rf packages/`. The `dev-tools` copy of this
|
|
37
|
+
* helper carried that guard as a lexical `startsWith`; the guard here asks
|
|
38
|
+
* the filesystem, so a symlinked tmpdir (macOS) still passes.
|
|
39
|
+
*
|
|
40
|
+
* `force: true` tolerates a directory already gone; the retries are Node's
|
|
41
|
+
* own remedy for the transient `EBUSY` / `EPERM` a just-closed handle
|
|
42
|
+
* produces on Windows. Anything left after that is a real teardown failure
|
|
43
|
+
* and stays loud — a teardown that swallows it hides a leaking fixture.
|
|
44
|
+
*
|
|
45
|
+
* @param dir - The directory to remove
|
|
46
|
+
* @throws When `dir` is not strictly under the host tmpdir
|
|
47
|
+
*/
|
|
48
|
+
export declare function removeTempDir(dir: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* A factory that remembers every directory it minted, so one `afterEach`
|
|
51
|
+
* removes them all.
|
|
52
|
+
*
|
|
53
|
+
* @param prefix - `mkdtemp` prefix for every directory this tracker mints
|
|
54
|
+
* @returns `create` to mint one more, `cleanupAll` to remove every one so far
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const scratch = tempDirTracker('my-suite-');
|
|
59
|
+
* afterEach(() => scratch.cleanupAll());
|
|
60
|
+
* it('…', () => { const dir = scratch.create(); … });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function tempDirTracker(prefix: string): {
|
|
64
|
+
create: () => string;
|
|
65
|
+
cleanupAll: () => void;
|
|
66
|
+
};
|
|
67
|
+
/** Knobs for {@link removeScratchDir}; all three exist so the behaviour is testable. */
|
|
68
|
+
export interface RemoveScratchDirOptions {
|
|
69
|
+
/** Deadline before the removal is abandoned. Default {@link SCRATCH_REMOVAL_BUDGET_MS}. */
|
|
70
|
+
readonly budgetMs?: number;
|
|
71
|
+
/** Where the give-up notice goes. Default `console.warn`. */
|
|
72
|
+
readonly onWarn?: (message: string) => void;
|
|
73
|
+
/**
|
|
74
|
+
* The removal itself. Defaults to `fs.rm` with recursive/force/retries.
|
|
75
|
+
*
|
|
76
|
+
* Injectable because the *contract* — a removal that fails must warn rather
|
|
77
|
+
* than throw — cannot otherwise be tested on every platform. Driving a real
|
|
78
|
+
* `fs.rm` failure needs a path the OS refuses, and those diverge: a path
|
|
79
|
+
* whose parent component is a regular file yields `ENOTDIR` on POSIX, and
|
|
80
|
+
* resolves silently on Windows. A test written against the POSIX shape
|
|
81
|
+
* passes locally and fails in CI, which is exactly what it did once.
|
|
82
|
+
*/
|
|
83
|
+
readonly remove?: (dir: string) => Promise<void>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Delete a scratch directory as *best effort* — never failing the suite that
|
|
87
|
+
* created it, and never taking longer than its own budget to say so.
|
|
88
|
+
*
|
|
89
|
+
* ## Why this is not just `await rm(dir, { recursive: true, force: true })`
|
|
90
|
+
*
|
|
91
|
+
* A teardown hook that can redden a suite whose every assertion passed is a
|
|
92
|
+
* defect in the harness, not a flake. `packages/lab/test/instrument.test.ts`
|
|
93
|
+
* timed out here on two consecutive Windows runs with all 655 assertions
|
|
94
|
+
* green — only the cleanup lost.
|
|
95
|
+
*
|
|
96
|
+
* The measurement is what rules out the obvious fixes: that scratch dir holds
|
|
97
|
+
* 490 files / 378 KiB across 14 fixture git repos, and deletes in **59 ms**
|
|
98
|
+
* idle. Against vitest's 10,000 ms unit-hook budget that is 170x of headroom,
|
|
99
|
+
* and Windows blew through it anyway. No quantity of real work explains that,
|
|
100
|
+
* so the cause is scheduling — contention from a fully parallel `validate`,
|
|
101
|
+
* plus per-unlink antivirus on Windows — which is unbounded by nature. Hence:
|
|
102
|
+
*
|
|
103
|
+
* - **Raising `hookTimeout` cannot be argued.** You would be picking a number
|
|
104
|
+
* to beat an unbounded quantity, when 10s of 170x headroom already lost. It
|
|
105
|
+
* also punches a hole in the deliberate policy in `vitest.shared.ts` ("no
|
|
106
|
+
* hookTimeout override here on purpose") for every unit hook, to fix one.
|
|
107
|
+
* - **`try`/`catch` around the `rm` cannot work.** A vitest hook timeout is a
|
|
108
|
+
* race decided on the *timer* side; the hook's own catch never sees it. It
|
|
109
|
+
* addresses a failure mode we did not observe and leaves the one we did.
|
|
110
|
+
* - **`maxRetries` alone makes it worse.** Retries target transient
|
|
111
|
+
* `EPERM`/`EBUSY`, which fail *fast*; our failure was *slow*, and retry
|
|
112
|
+
* backoff only adds to it. Kept below as a cheap inner win, not as the fix.
|
|
113
|
+
*
|
|
114
|
+
* So the deadline is taken away from vitest: the removal races a timer of our
|
|
115
|
+
* own, well inside the hook budget, and expiry is a warning rather than a
|
|
116
|
+
* failure. The hook therefore always resolves in time, which makes it
|
|
117
|
+
* *structurally* incapable of reddening a green suite on any machine at any
|
|
118
|
+
* load — rather than merely unlikely to.
|
|
119
|
+
*
|
|
120
|
+
* The cost, stated plainly: under pathological contention the directory
|
|
121
|
+
* survives in the OS temp dir, which the OS reclaims, and the warning names
|
|
122
|
+
* the path. It can never surface as an unhandled rejection, because the only
|
|
123
|
+
* rejection handler is installed before the race.
|
|
124
|
+
*
|
|
125
|
+
* ⚠️ **Abandoning the removal does not stop it, and does not free the worker.**
|
|
126
|
+
* A pending libuv `fs` request is an active handle, so the `rm` runs to
|
|
127
|
+
* completion regardless — measured at 2,407 ms on an 8,000-file tree after the
|
|
128
|
+
* race was decided at 5 ms — and the process cannot exit until it does.
|
|
129
|
+
* `timer.unref()` below unrefs the *timer*, not the removal. So what this buys
|
|
130
|
+
* is bounded: the **hook** always resolves in time, which is what stops a green
|
|
131
|
+
* suite going red. It does **not** shed the work, and under the contention it
|
|
132
|
+
* targets the abandoned removal competes for disk with whatever runs next in
|
|
133
|
+
* the same worker. That is the trade, and it is why the budget wants to be as
|
|
134
|
+
* large as the tier's hook allows rather than as small as possible.
|
|
135
|
+
*
|
|
136
|
+
* @param dir - Directory to remove. An empty string is a no-op, so a suite
|
|
137
|
+
* whose `beforeAll` never ran can call this unconditionally.
|
|
138
|
+
* @param options - Deadline and warning sink
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* afterAll(async () => {
|
|
143
|
+
* await removeScratchDir(scratch);
|
|
144
|
+
* });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export declare function removeScratchDir(dir: string, options?: RemoveScratchDirOptions): Promise<void>;
|
|
148
|
+
/**
|
|
149
|
+
* Get isolated test output directory for current test run
|
|
150
|
+
*
|
|
151
|
+
* Creates a unique directory under `packages/{packageName}/.test-output/{testType}/{runId}`
|
|
152
|
+
* where runId is `{timestamp}-{randomId}` to ensure isolation across parallel test runs.
|
|
153
|
+
*
|
|
154
|
+
* @param packageName - Name of package (e.g., 'rag-lancedb')
|
|
155
|
+
* @param testType - Type of test ('unit', 'integration', 'system')
|
|
156
|
+
* @param subdirs - Optional subdirectories to create within the test output directory
|
|
157
|
+
* @returns Absolute path to the created directory
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* ```typescript
|
|
161
|
+
* // Create isolated database directory for system tests
|
|
162
|
+
* const dbPath = getTestOutputDir('rag-lancedb', 'system', 'databases', 'test-db');
|
|
163
|
+
* // Result: packages/rag-lancedb/.test-output/system/20260105-143022-abc123/databases/test-db
|
|
164
|
+
*
|
|
165
|
+
* // Create temporary file directory for integration tests
|
|
166
|
+
* const tempDir = getTestOutputDir('agent-skills', 'integration', 'temp-files');
|
|
167
|
+
* // Result: packages/agent-skills/.test-output/integration/20260105-143022-def456/temp-files
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
export declare function getTestOutputDir(packageName: string, testType: 'unit' | 'integration' | 'system', ...subdirs: string[]): string;
|
|
171
|
+
/**
|
|
172
|
+
* Get the base test output directory for a package
|
|
173
|
+
* Useful for cleanup operations that need to remove all test output
|
|
174
|
+
*
|
|
175
|
+
* @param packageName - Name of package (e.g., 'rag-lancedb')
|
|
176
|
+
* @returns Absolute path to packages/{packageName}/.test-output
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* const baseDir = getTestOutputBase('rag-lancedb');
|
|
181
|
+
* // Result: packages/rag-lancedb/.test-output
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
export declare function getTestOutputBase(packageName: string): string;
|
|
185
|
+
/**
|
|
186
|
+
* Per-suite temp directory pattern (async version)
|
|
187
|
+
* Creates a single temp directory for the entire test suite,
|
|
188
|
+
* with subdirectories for each test. This is 3-5x faster on Windows
|
|
189
|
+
* than creating a new mkdtemp for each test.
|
|
190
|
+
*
|
|
191
|
+
* @param prefix - Prefix for the suite temp directory name
|
|
192
|
+
* @param teardown - Forwarded to {@link removeScratchDir}. Raise `budgetMs` for a
|
|
193
|
+
* suite whose fixture tree is heavy or whose tier allows a longer hook than the
|
|
194
|
+
* unit tier this default is sized against — see {@link SCRATCH_REMOVAL_BUDGET_MS}.
|
|
195
|
+
* @returns Suite helper with beforeAll, afterAll, beforeEach, afterEach, and getTempDir
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* const suite = setupAsyncTempDirSuite('my-test');
|
|
200
|
+
*
|
|
201
|
+
* describe('my tests', () => {
|
|
202
|
+
* beforeAll(suite.beforeAll);
|
|
203
|
+
* afterAll(suite.afterAll);
|
|
204
|
+
* beforeEach(suite.beforeEach);
|
|
205
|
+
*
|
|
206
|
+
* it('test 1', async () => {
|
|
207
|
+
* const tempDir = suite.getTempDir();
|
|
208
|
+
* // Use tempDir...
|
|
209
|
+
* });
|
|
210
|
+
* });
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export declare function setupAsyncTempDirSuite(prefix: string, teardown?: RemoveScratchDirOptions): {
|
|
214
|
+
beforeAll: () => Promise<void>;
|
|
215
|
+
afterAll: () => Promise<void>;
|
|
216
|
+
beforeEach: () => Promise<void>;
|
|
217
|
+
afterEach: () => Promise<void>;
|
|
218
|
+
getTempDir: () => string;
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* Per-suite temp directory pattern (sync version)
|
|
222
|
+
* Creates a single temp directory for the entire test suite,
|
|
223
|
+
* with subdirectories for each test. This is 3-5x faster on Windows
|
|
224
|
+
* than creating a new mkdtemp for each test.
|
|
225
|
+
*
|
|
226
|
+
* @param prefix - Prefix for the suite temp directory name
|
|
227
|
+
* @param teardown - Forwarded to {@link removeScratchDir}. Raise `budgetMs` for a
|
|
228
|
+
* suite whose fixture tree is heavy or whose tier allows a longer hook than the
|
|
229
|
+
* unit tier this default is sized against — see {@link SCRATCH_REMOVAL_BUDGET_MS}.
|
|
230
|
+
* @returns Suite helper with beforeAll, afterAll, beforeEach, afterEach, and getTempDir
|
|
231
|
+
*
|
|
232
|
+
* @example
|
|
233
|
+
* ```typescript
|
|
234
|
+
* const suite = setupSyncTempDirSuite('my-test');
|
|
235
|
+
*
|
|
236
|
+
* describe('my tests', () => {
|
|
237
|
+
* beforeAll(suite.beforeAll);
|
|
238
|
+
* afterAll(suite.afterAll);
|
|
239
|
+
* beforeEach(suite.beforeEach);
|
|
240
|
+
*
|
|
241
|
+
* it('test 1', () => {
|
|
242
|
+
* const tempDir = suite.getTempDir();
|
|
243
|
+
* // Use tempDir...
|
|
244
|
+
* });
|
|
245
|
+
* });
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
export declare function setupSyncTempDirSuite(prefix: string, teardown?: RemoveScratchDirOptions): {
|
|
249
|
+
beforeAll: () => void;
|
|
250
|
+
afterAll: () => Promise<void>;
|
|
251
|
+
beforeEach: () => void;
|
|
252
|
+
afterEach: () => void;
|
|
253
|
+
getTempDir: () => string;
|
|
254
|
+
};
|
|
255
|
+
//# sourceMappingURL=temp-dir.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temp-dir.d.ts","sourceRoot":"","sources":["../../src/testing/temp-dir.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AASH;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,2FAA2F;AAC3F,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAExE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAK/C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,MAAM,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,IAAI,CAAA;CAAE,CAa/F;AAuBD,wFAAwF;AACxF,MAAM,WAAW,uBAAuB;IACtC,2FAA2F;IAC3F,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C;;;;;;;;;OASG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAClD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,IAAI,CAAC,CA4Cf;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GAAG,aAAa,GAAG,QAAQ,EAC3C,GAAG,OAAO,EAAE,MAAM,EAAE,GACnB,MAAM,CAuBR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,uBAA4B,GAAG;IAC9F,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,SAAS,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,UAAU,EAAE,MAAM,MAAM,CAAC;CAC1B,CAsBA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,uBAA4B,GAAG;IAC7F,SAAS,EAAE,MAAM,IAAI,CAAC;IAItB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,UAAU,EAAE,MAAM,MAAM,CAAC;CAC1B,CAsBA"}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE temp-directory helper family.
|
|
3
|
+
*
|
|
4
|
+
* Every shape a suite needs to mint scratch space lives here: a per-call
|
|
5
|
+
* `createTempDir`, a tracker that removes what it minted, and the per-suite
|
|
6
|
+
* sync/async suite helpers with their bounded teardown. The audit counted
|
|
7
|
+
* eleven definitions of "give me a temp dir that cleans itself up" across
|
|
8
|
+
* eight modules, six of them named differently and none of them visible to
|
|
9
|
+
* the duplication gate; this module is where they were collapsed to.
|
|
10
|
+
*
|
|
11
|
+
* ⛔ Framework-free, like everything under `testing/`: nothing here imports
|
|
12
|
+
* `vitest`, so the `./testing` subpath keeps the empty third-party set its
|
|
13
|
+
* purity pin asserts. Each suite owns its own hooks and calls these from
|
|
14
|
+
* inside them.
|
|
15
|
+
*/
|
|
16
|
+
import { randomBytes } from 'node:crypto';
|
|
17
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
18
|
+
import fs, { mkdtemp } from 'node:fs/promises';
|
|
19
|
+
import { isUnderRoot } from '../path-containment.js';
|
|
20
|
+
import { mkdirSyncReal, normalizedTmpdir, safePath } from '../path-utils.js';
|
|
21
|
+
/**
|
|
22
|
+
* Mint a fresh temp directory under the host's (short-name-resolved) tmpdir.
|
|
23
|
+
*
|
|
24
|
+
* The one-liner that used to be spelled in three packages as
|
|
25
|
+
* `createTestTempDir`. Pair with {@link removeTempDir}, or let
|
|
26
|
+
* {@link tempDirTracker} pair them for you.
|
|
27
|
+
*
|
|
28
|
+
* @param prefix - `mkdtemp` prefix, so a leaked directory names its own suite
|
|
29
|
+
* @returns Absolute, forward-slashed path of the new directory
|
|
30
|
+
*/
|
|
31
|
+
export function createTempDir(prefix) {
|
|
32
|
+
return safePath.resolve(mkdtempSync(safePath.join(normalizedTmpdir(), prefix)));
|
|
33
|
+
}
|
|
34
|
+
/** The async counterpart of {@link createTempDir}, for suites already on `fs/promises`. */
|
|
35
|
+
export async function createTempDirAsync(prefix) {
|
|
36
|
+
return safePath.resolve(await mkdtemp(safePath.join(normalizedTmpdir(), prefix)));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Remove a directory {@link createTempDir} minted — and ONLY such a directory.
|
|
40
|
+
*
|
|
41
|
+
* Refuses, by name, to remove anything that is not strictly under the host
|
|
42
|
+
* tmpdir as the filesystem sees it. A teardown is the one place test code
|
|
43
|
+
* runs `rm -rf` on a variable, and a variable that was never assigned (a
|
|
44
|
+
* `beforeAll` that threw), or was assigned a fixture INSIDE the repo, must
|
|
45
|
+
* not become `rm -rf ''` or `rm -rf packages/`. The `dev-tools` copy of this
|
|
46
|
+
* helper carried that guard as a lexical `startsWith`; the guard here asks
|
|
47
|
+
* the filesystem, so a symlinked tmpdir (macOS) still passes.
|
|
48
|
+
*
|
|
49
|
+
* `force: true` tolerates a directory already gone; the retries are Node's
|
|
50
|
+
* own remedy for the transient `EBUSY` / `EPERM` a just-closed handle
|
|
51
|
+
* produces on Windows. Anything left after that is a real teardown failure
|
|
52
|
+
* and stays loud — a teardown that swallows it hides a leaking fixture.
|
|
53
|
+
*
|
|
54
|
+
* @param dir - The directory to remove
|
|
55
|
+
* @throws When `dir` is not strictly under the host tmpdir
|
|
56
|
+
*/
|
|
57
|
+
export function removeTempDir(dir) {
|
|
58
|
+
if (isUnderRoot(normalizedTmpdir(), dir) === 'outside') {
|
|
59
|
+
throw new Error(`removeTempDir: refusing to remove ${dir} — not inside the host tmpdir ${normalizedTmpdir()}`);
|
|
60
|
+
}
|
|
61
|
+
rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A factory that remembers every directory it minted, so one `afterEach`
|
|
65
|
+
* removes them all.
|
|
66
|
+
*
|
|
67
|
+
* @param prefix - `mkdtemp` prefix for every directory this tracker mints
|
|
68
|
+
* @returns `create` to mint one more, `cleanupAll` to remove every one so far
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const scratch = tempDirTracker('my-suite-');
|
|
73
|
+
* afterEach(() => scratch.cleanupAll());
|
|
74
|
+
* it('…', () => { const dir = scratch.create(); … });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function tempDirTracker(prefix) {
|
|
78
|
+
const minted = [];
|
|
79
|
+
return {
|
|
80
|
+
create: () => {
|
|
81
|
+
const dir = createTempDir(prefix);
|
|
82
|
+
minted.push(dir);
|
|
83
|
+
return dir;
|
|
84
|
+
},
|
|
85
|
+
cleanupAll: () => {
|
|
86
|
+
for (const dir of minted)
|
|
87
|
+
removeTempDir(dir);
|
|
88
|
+
minted.length = 0;
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* How long a scratch-dir teardown may run before it gives up and warns.
|
|
94
|
+
*
|
|
95
|
+
* The value only has to be comfortably *under* the hook timeout it runs in —
|
|
96
|
+
* that is the whole design. Sizing a teardown budget to beat contention is
|
|
97
|
+
* unprovable (see {@link removeScratchDir}); sizing it below a known constant is
|
|
98
|
+
* arithmetic.
|
|
99
|
+
*
|
|
100
|
+
* ⚠️ **The known constant is the UNIT tier's**, which is the only tier that
|
|
101
|
+
* takes vitest's 10s default (`vitest.shared.ts` declines to override it there
|
|
102
|
+
* on purpose) and the tier where the flake was actually observed. The other two
|
|
103
|
+
* tiers set their own, far larger: integration gets `platformTestTimeout`
|
|
104
|
+
* (60s on Unix, 900s on Windows) and system gets 300s. A suite in those tiers
|
|
105
|
+
* inherits this 4s default and therefore gives up 15x–225x earlier than its hook
|
|
106
|
+
* would have allowed — for a heavy fixture tree that is a leaked directory and a
|
|
107
|
+
* warning bought for nothing, since an abandoned removal does not stop (see
|
|
108
|
+
* {@link removeScratchDir}). Such a suite should pass its own `budgetMs`, which
|
|
109
|
+
* both suite helpers forward.
|
|
110
|
+
*/
|
|
111
|
+
const SCRATCH_REMOVAL_BUDGET_MS = 4000;
|
|
112
|
+
/**
|
|
113
|
+
* Delete a scratch directory as *best effort* — never failing the suite that
|
|
114
|
+
* created it, and never taking longer than its own budget to say so.
|
|
115
|
+
*
|
|
116
|
+
* ## Why this is not just `await rm(dir, { recursive: true, force: true })`
|
|
117
|
+
*
|
|
118
|
+
* A teardown hook that can redden a suite whose every assertion passed is a
|
|
119
|
+
* defect in the harness, not a flake. `packages/lab/test/instrument.test.ts`
|
|
120
|
+
* timed out here on two consecutive Windows runs with all 655 assertions
|
|
121
|
+
* green — only the cleanup lost.
|
|
122
|
+
*
|
|
123
|
+
* The measurement is what rules out the obvious fixes: that scratch dir holds
|
|
124
|
+
* 490 files / 378 KiB across 14 fixture git repos, and deletes in **59 ms**
|
|
125
|
+
* idle. Against vitest's 10,000 ms unit-hook budget that is 170x of headroom,
|
|
126
|
+
* and Windows blew through it anyway. No quantity of real work explains that,
|
|
127
|
+
* so the cause is scheduling — contention from a fully parallel `validate`,
|
|
128
|
+
* plus per-unlink antivirus on Windows — which is unbounded by nature. Hence:
|
|
129
|
+
*
|
|
130
|
+
* - **Raising `hookTimeout` cannot be argued.** You would be picking a number
|
|
131
|
+
* to beat an unbounded quantity, when 10s of 170x headroom already lost. It
|
|
132
|
+
* also punches a hole in the deliberate policy in `vitest.shared.ts` ("no
|
|
133
|
+
* hookTimeout override here on purpose") for every unit hook, to fix one.
|
|
134
|
+
* - **`try`/`catch` around the `rm` cannot work.** A vitest hook timeout is a
|
|
135
|
+
* race decided on the *timer* side; the hook's own catch never sees it. It
|
|
136
|
+
* addresses a failure mode we did not observe and leaves the one we did.
|
|
137
|
+
* - **`maxRetries` alone makes it worse.** Retries target transient
|
|
138
|
+
* `EPERM`/`EBUSY`, which fail *fast*; our failure was *slow*, and retry
|
|
139
|
+
* backoff only adds to it. Kept below as a cheap inner win, not as the fix.
|
|
140
|
+
*
|
|
141
|
+
* So the deadline is taken away from vitest: the removal races a timer of our
|
|
142
|
+
* own, well inside the hook budget, and expiry is a warning rather than a
|
|
143
|
+
* failure. The hook therefore always resolves in time, which makes it
|
|
144
|
+
* *structurally* incapable of reddening a green suite on any machine at any
|
|
145
|
+
* load — rather than merely unlikely to.
|
|
146
|
+
*
|
|
147
|
+
* The cost, stated plainly: under pathological contention the directory
|
|
148
|
+
* survives in the OS temp dir, which the OS reclaims, and the warning names
|
|
149
|
+
* the path. It can never surface as an unhandled rejection, because the only
|
|
150
|
+
* rejection handler is installed before the race.
|
|
151
|
+
*
|
|
152
|
+
* ⚠️ **Abandoning the removal does not stop it, and does not free the worker.**
|
|
153
|
+
* A pending libuv `fs` request is an active handle, so the `rm` runs to
|
|
154
|
+
* completion regardless — measured at 2,407 ms on an 8,000-file tree after the
|
|
155
|
+
* race was decided at 5 ms — and the process cannot exit until it does.
|
|
156
|
+
* `timer.unref()` below unrefs the *timer*, not the removal. So what this buys
|
|
157
|
+
* is bounded: the **hook** always resolves in time, which is what stops a green
|
|
158
|
+
* suite going red. It does **not** shed the work, and under the contention it
|
|
159
|
+
* targets the abandoned removal competes for disk with whatever runs next in
|
|
160
|
+
* the same worker. That is the trade, and it is why the budget wants to be as
|
|
161
|
+
* large as the tier's hook allows rather than as small as possible.
|
|
162
|
+
*
|
|
163
|
+
* @param dir - Directory to remove. An empty string is a no-op, so a suite
|
|
164
|
+
* whose `beforeAll` never ran can call this unconditionally.
|
|
165
|
+
* @param options - Deadline and warning sink
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```typescript
|
|
169
|
+
* afterAll(async () => {
|
|
170
|
+
* await removeScratchDir(scratch);
|
|
171
|
+
* });
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
export async function removeScratchDir(dir, options = {}) {
|
|
175
|
+
if (dir === '')
|
|
176
|
+
return;
|
|
177
|
+
const budgetMs = options.budgetMs ?? SCRATCH_REMOVAL_BUDGET_MS;
|
|
178
|
+
const onWarn = options.onWarn ??
|
|
179
|
+
((message) => {
|
|
180
|
+
console.warn(message);
|
|
181
|
+
});
|
|
182
|
+
// Latches on the first outcome so a removal that finishes (or fails) after
|
|
183
|
+
// the budget expired cannot log a second time into an already-finished suite.
|
|
184
|
+
let settled = false;
|
|
185
|
+
const giveUp = (reason) => {
|
|
186
|
+
if (settled)
|
|
187
|
+
return;
|
|
188
|
+
settled = true;
|
|
189
|
+
onWarn(`scratch dir left behind at ${dir}: ${reason}`);
|
|
190
|
+
};
|
|
191
|
+
const remove = options.remove ??
|
|
192
|
+
((target) => fs.rm(target, { recursive: true, force: true, maxRetries: 2, retryDelay: 50 }));
|
|
193
|
+
const removal = remove(dir)
|
|
194
|
+
.then(() => {
|
|
195
|
+
settled = true;
|
|
196
|
+
})
|
|
197
|
+
.catch((error) => {
|
|
198
|
+
giveUp(error instanceof Error ? error.message : String(error));
|
|
199
|
+
});
|
|
200
|
+
let timer;
|
|
201
|
+
const deadline = new Promise((resolve) => {
|
|
202
|
+
timer = setTimeout(() => {
|
|
203
|
+
giveUp(`removal did not finish within ${budgetMs}ms`);
|
|
204
|
+
resolve();
|
|
205
|
+
}, budgetMs);
|
|
206
|
+
// Never hold the process open for a teardown nobody is waiting on.
|
|
207
|
+
timer.unref();
|
|
208
|
+
});
|
|
209
|
+
await Promise.race([removal, deadline]);
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get isolated test output directory for current test run
|
|
214
|
+
*
|
|
215
|
+
* Creates a unique directory under `packages/{packageName}/.test-output/{testType}/{runId}`
|
|
216
|
+
* where runId is `{timestamp}-{randomId}` to ensure isolation across parallel test runs.
|
|
217
|
+
*
|
|
218
|
+
* @param packageName - Name of package (e.g., 'rag-lancedb')
|
|
219
|
+
* @param testType - Type of test ('unit', 'integration', 'system')
|
|
220
|
+
* @param subdirs - Optional subdirectories to create within the test output directory
|
|
221
|
+
* @returns Absolute path to the created directory
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```typescript
|
|
225
|
+
* // Create isolated database directory for system tests
|
|
226
|
+
* const dbPath = getTestOutputDir('rag-lancedb', 'system', 'databases', 'test-db');
|
|
227
|
+
* // Result: packages/rag-lancedb/.test-output/system/20260105-143022-abc123/databases/test-db
|
|
228
|
+
*
|
|
229
|
+
* // Create temporary file directory for integration tests
|
|
230
|
+
* const tempDir = getTestOutputDir('agent-skills', 'integration', 'temp-files');
|
|
231
|
+
* // Result: packages/agent-skills/.test-output/integration/20260105-143022-def456/temp-files
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
export function getTestOutputDir(packageName, testType, ...subdirs) {
|
|
235
|
+
// Generate unique run ID: timestamp + random hex
|
|
236
|
+
const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-').slice(0, 19);
|
|
237
|
+
const randomId = randomBytes(4).toString('hex');
|
|
238
|
+
const runId = `${timestamp}-${randomId}`;
|
|
239
|
+
// Find project root (assuming we're always in packages/*/test/*)
|
|
240
|
+
const projectRoot = safePath.resolve(process.cwd());
|
|
241
|
+
// Build path: packages/{packageName}/.test-output/{testType}/{runId}/{...subdirs}
|
|
242
|
+
const testOutputDir = safePath.join(projectRoot, 'packages', packageName, '.test-output', testType, runId, ...subdirs);
|
|
243
|
+
// Create directory structure and return normalized path
|
|
244
|
+
return mkdirSyncReal(testOutputDir, { recursive: true });
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Get the base test output directory for a package
|
|
248
|
+
* Useful for cleanup operations that need to remove all test output
|
|
249
|
+
*
|
|
250
|
+
* @param packageName - Name of package (e.g., 'rag-lancedb')
|
|
251
|
+
* @returns Absolute path to packages/{packageName}/.test-output
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```typescript
|
|
255
|
+
* const baseDir = getTestOutputBase('rag-lancedb');
|
|
256
|
+
* // Result: packages/rag-lancedb/.test-output
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
export function getTestOutputBase(packageName) {
|
|
260
|
+
const projectRoot = safePath.resolve(process.cwd());
|
|
261
|
+
return safePath.join(projectRoot, 'packages', packageName, '.test-output');
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Per-suite temp directory pattern (async version)
|
|
265
|
+
* Creates a single temp directory for the entire test suite,
|
|
266
|
+
* with subdirectories for each test. This is 3-5x faster on Windows
|
|
267
|
+
* than creating a new mkdtemp for each test.
|
|
268
|
+
*
|
|
269
|
+
* @param prefix - Prefix for the suite temp directory name
|
|
270
|
+
* @param teardown - Forwarded to {@link removeScratchDir}. Raise `budgetMs` for a
|
|
271
|
+
* suite whose fixture tree is heavy or whose tier allows a longer hook than the
|
|
272
|
+
* unit tier this default is sized against — see {@link SCRATCH_REMOVAL_BUDGET_MS}.
|
|
273
|
+
* @returns Suite helper with beforeAll, afterAll, beforeEach, afterEach, and getTempDir
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* const suite = setupAsyncTempDirSuite('my-test');
|
|
278
|
+
*
|
|
279
|
+
* describe('my tests', () => {
|
|
280
|
+
* beforeAll(suite.beforeAll);
|
|
281
|
+
* afterAll(suite.afterAll);
|
|
282
|
+
* beforeEach(suite.beforeEach);
|
|
283
|
+
*
|
|
284
|
+
* it('test 1', async () => {
|
|
285
|
+
* const tempDir = suite.getTempDir();
|
|
286
|
+
* // Use tempDir...
|
|
287
|
+
* });
|
|
288
|
+
* });
|
|
289
|
+
* ```
|
|
290
|
+
*/
|
|
291
|
+
export function setupAsyncTempDirSuite(prefix, teardown = {}) {
|
|
292
|
+
let suiteDir = '';
|
|
293
|
+
let tempDir = '';
|
|
294
|
+
let testCounter = 0;
|
|
295
|
+
return {
|
|
296
|
+
beforeAll: async () => {
|
|
297
|
+
suiteDir = await fs.mkdtemp(safePath.join(normalizedTmpdir(), `${prefix}-suite-`));
|
|
298
|
+
},
|
|
299
|
+
afterAll: async () => {
|
|
300
|
+
await removeScratchDir(suiteDir, teardown);
|
|
301
|
+
},
|
|
302
|
+
beforeEach: async () => {
|
|
303
|
+
testCounter++;
|
|
304
|
+
tempDir = safePath.join(suiteDir, `test-${testCounter}`);
|
|
305
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
306
|
+
},
|
|
307
|
+
afterEach: async () => {
|
|
308
|
+
// Per-test cleanup handled by suite cleanup
|
|
309
|
+
},
|
|
310
|
+
getTempDir: () => tempDir,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Per-suite temp directory pattern (sync version)
|
|
315
|
+
* Creates a single temp directory for the entire test suite,
|
|
316
|
+
* with subdirectories for each test. This is 3-5x faster on Windows
|
|
317
|
+
* than creating a new mkdtemp for each test.
|
|
318
|
+
*
|
|
319
|
+
* @param prefix - Prefix for the suite temp directory name
|
|
320
|
+
* @param teardown - Forwarded to {@link removeScratchDir}. Raise `budgetMs` for a
|
|
321
|
+
* suite whose fixture tree is heavy or whose tier allows a longer hook than the
|
|
322
|
+
* unit tier this default is sized against — see {@link SCRATCH_REMOVAL_BUDGET_MS}.
|
|
323
|
+
* @returns Suite helper with beforeAll, afterAll, beforeEach, afterEach, and getTempDir
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* ```typescript
|
|
327
|
+
* const suite = setupSyncTempDirSuite('my-test');
|
|
328
|
+
*
|
|
329
|
+
* describe('my tests', () => {
|
|
330
|
+
* beforeAll(suite.beforeAll);
|
|
331
|
+
* afterAll(suite.afterAll);
|
|
332
|
+
* beforeEach(suite.beforeEach);
|
|
333
|
+
*
|
|
334
|
+
* it('test 1', () => {
|
|
335
|
+
* const tempDir = suite.getTempDir();
|
|
336
|
+
* // Use tempDir...
|
|
337
|
+
* });
|
|
338
|
+
* });
|
|
339
|
+
* ```
|
|
340
|
+
*/
|
|
341
|
+
export function setupSyncTempDirSuite(prefix, teardown = {}) {
|
|
342
|
+
let suiteDir = '';
|
|
343
|
+
let tempDir = '';
|
|
344
|
+
let testCounter = 0;
|
|
345
|
+
return {
|
|
346
|
+
beforeAll: () => {
|
|
347
|
+
suiteDir = mkdtempSync(safePath.join(normalizedTmpdir(), `${prefix}-suite-`));
|
|
348
|
+
},
|
|
349
|
+
afterAll: async () => {
|
|
350
|
+
await removeScratchDir(suiteDir, teardown);
|
|
351
|
+
},
|
|
352
|
+
beforeEach: () => {
|
|
353
|
+
testCounter++;
|
|
354
|
+
tempDir = safePath.join(suiteDir, `test-${testCounter}`);
|
|
355
|
+
mkdirSyncReal(tempDir);
|
|
356
|
+
},
|
|
357
|
+
afterEach: () => {
|
|
358
|
+
// Per-test cleanup handled by suite cleanup
|
|
359
|
+
},
|
|
360
|
+
getTempDir: () => tempDir,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
//# sourceMappingURL=temp-dir.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temp-dir.js","sourceRoot":"","sources":["../../src/testing/temp-dir.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAE/C,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE7E;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,OAAO,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;AAClF,CAAC;AAED,2FAA2F;AAC3F,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,MAAc;IACrD,OAAO,QAAQ,CAAC,OAAO,CAAC,MAAM,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;AACpF,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,IAAI,WAAW,CAAC,gBAAgB,EAAE,EAAE,GAAG,CAAC,KAAK,SAAS,EAAE,CAAC;QACvD,MAAM,IAAI,KAAK,CAAC,qCAAqC,GAAG,iCAAiC,gBAAgB,EAAE,EAAE,CAAC,CAAC;IACjH,CAAC;IACD,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC;AAChF,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,OAAO;QACL,MAAM,EAAE,GAAG,EAAE;YACX,MAAM,GAAG,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;YAClC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACjB,OAAO,GAAG,CAAC;QACb,CAAC;QACD,UAAU,EAAE,GAAG,EAAE;YACf,KAAK,MAAM,GAAG,IAAI,MAAM;gBAAE,aAAa,CAAC,GAAG,CAAC,CAAC;YAC7C,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;QACpB,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,yBAAyB,GAAG,IAAI,CAAC;AAqBvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAW,EACX,UAAmC,EAAE;IAErC,IAAI,GAAG,KAAK,EAAE;QAAE,OAAO;IAEvB,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,yBAAyB,CAAC;IAC/D,MAAM,MAAM,GACV,OAAO,CAAC,MAAM;QACd,CAAC,CAAC,OAAe,EAAQ,EAAE;YACzB,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC,CAAC,CAAC;IAEL,2EAA2E;IAC3E,8EAA8E;IAC9E,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,MAAM,GAAG,CAAC,MAAc,EAAQ,EAAE;QACtC,IAAI,OAAO;YAAE,OAAO;QACpB,OAAO,GAAG,IAAI,CAAC;QACf,MAAM,CAAC,8BAA8B,GAAG,KAAK,MAAM,EAAE,CAAC,CAAC;IACzD,CAAC,CAAC;IAEF,MAAM,MAAM,GACV,OAAO,CAAC,MAAM;QACd,CAAC,CAAC,MAAc,EAAiB,EAAE,CACjC,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IAEpF,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC;SACxB,IAAI,CAAC,GAAG,EAAE;QACT,OAAO,GAAG,IAAI,CAAC;IACjB,CAAC,CAAC;SACD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QACxB,MAAM,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACjE,CAAC,CAAC,CAAC;IAEL,IAAI,KAAgD,CAAC;IACrD,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAC7C,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YACtB,MAAM,CAAC,iCAAiC,QAAQ,IAAI,CAAC,CAAC;YACtD,OAAO,EAAE,CAAC;QACZ,CAAC,EAAE,QAAQ,CAAC,CAAC;QACb,mEAAmE;QACnE,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;IACxC,YAAY,CAAC,KAAK,CAAC,CAAC;AACtB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,gBAAgB,CAC9B,WAAmB,EACnB,QAA2C,EAC3C,GAAG,OAAiB;IAEpB,iDAAiD;IACjD,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACjF,MAAM,QAAQ,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG,GAAG,SAAS,IAAI,QAAQ,EAAE,CAAC;IAEzC,iEAAiE;IACjE,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAEpD,kFAAkF;IAClF,MAAM,aAAa,GAAG,QAAQ,CAAC,IAAI,CACjC,WAAW,EACX,UAAU,EACV,WAAW,EACX,cAAc,EACd,QAAQ,EACR,KAAK,EACL,GAAG,OAAO,CACX,CAAC;IAEF,wDAAwD;IAExD,OAAO,aAAa,CAAC,aAAa,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,iBAAiB,CAAC,WAAmB;IACnD,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACpD,OAAO,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,cAAc,CAAC,CAAC;AAC7E,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAc,EAAE,WAAoC,EAAE;IAO3F,IAAI,QAAQ,GAAG,EAAE,CAAC;IAClB,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,WAAW,GAAG,CAAC,CAAC;IAEpB,OAAO;QACL,SAAS,EAAE,KAAK,IAAI,EAAE;YACpB,QAAQ,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,GAAG,MAAM,SAAS,CAAC,CAAC,CAAC;QACrF,CAAC;QACD,QAAQ,EAAE,KAAK,IAAI,EAAE;YACnB,MAAM,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC7C,CAAC;QACD,UAAU,EAAE,KAAK,IAAI,EAAE;YACrB,WAAW,EAAE,CAAC;YACd,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,WAAW,EAAE,CAAC,CAAC;YACzD,MAAM,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/C,CAAC;QACD,SAAS,EAAE,KAAK,IAAI,EAAE;YACpB,4CAA4C;QAC9C,CAAC;QACD,UAAU,EAAE,GAAG,EAAE,CAAC,OAAO;KAC1B,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAc,EAAE,WAAoC,EAAE;IAU1F,IAAI,QAAQ,GAAG,EAAE,CAAC;IAClB,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,WAAW,GAAG,CAAC,CAAC;IAEpB,OAAO;QACL,SAAS,EAAE,GAAG,EAAE;YACd,QAAQ,GAAG,WAAW,CAAC,QAAQ,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,GAAG,MAAM,SAAS,CAAC,CAAC,CAAC;QAChF,CAAC;QACD,QAAQ,EAAE,KAAK,IAAI,EAAE;YACnB,MAAM,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC7C,CAAC;QACD,UAAU,EAAE,GAAG,EAAE;YACf,WAAW,EAAE,CAAC;YACd,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,WAAW,EAAE,CAAC,CAAC;YACzD,aAAa,CAAC,OAAO,CAAC,CAAC;QACzB,CAAC;QACD,SAAS,EAAE,GAAG,EAAE;YACd,4CAA4C;QAC9C,CAAC;QACD,UAAU,EAAE,GAAG,EAAE,CAAC,OAAO;KAC1B,CAAC;AACJ,CAAC"}
|