@vibe-agent-toolkit/utils 0.2.0-rc.2 → 0.2.0-rc.4
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 +26 -15
- 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/file-crawler.d.ts +22 -0
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +31 -7
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs-utils.d.ts +17 -0
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +69 -0
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +3 -1
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +5 -1
- package/dist/fs.js.map +1 -1
- 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 +181 -0
- package/dist/git-snapshot.d.ts.map +1 -0
- package/dist/git-snapshot.js +232 -0
- package/dist/git-snapshot.js.map +1 -0
- package/dist/git-tracker.d.ts +94 -1
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +135 -3
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts +38 -0
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +103 -80
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +12 -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 +6 -3
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/index.d.ts +24 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -30
- package/dist/index.js.map +1 -1
- package/dist/path-core.d.ts +1 -1
- package/dist/path-core.js +1 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +6 -3
- 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 +38 -10
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +88 -19
- package/dist/safe-exec.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 +28 -0
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +51 -2
- 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 +47 -3
- package/dist/skill-test/transcript.js.map +1 -1
- package/dist/test-helpers.d.ts +185 -13
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +274 -27
- package/dist/test-helpers.js.map +1 -1
- package/dist/testing.d.ts +73 -3
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +93 -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 +348 -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 +53 -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 +230 -0
- package/dist/timing-dump.js.map +1 -0
- package/eslint/README.md +105 -3
- package/eslint/index.cjs +50 -0
- package/eslint/index.d.cts +14 -4
- package/eslint/rules/no-bare-symlink-in-tests.cjs +200 -0
- package/eslint/rules/no-process-exit-in-phase.cjs +117 -0
- package/eslint/rules/no-raw-text-decode.cjs +296 -0
- package/eslint/rules/no-self-package-import.cjs +157 -0
- package/package.json +13 -8
- 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/dist/test-helpers.d.ts
CHANGED
|
@@ -1,3 +1,84 @@
|
|
|
1
|
+
/** Knobs for {@link removeScratchDir}; all three exist so the behaviour is testable. */
|
|
2
|
+
export interface RemoveScratchDirOptions {
|
|
3
|
+
/** Deadline before the removal is abandoned. Default {@link SCRATCH_REMOVAL_BUDGET_MS}. */
|
|
4
|
+
readonly budgetMs?: number;
|
|
5
|
+
/** Where the give-up notice goes. Default `console.warn`. */
|
|
6
|
+
readonly onWarn?: (message: string) => void;
|
|
7
|
+
/**
|
|
8
|
+
* The removal itself. Defaults to `fs.rm` with recursive/force/retries.
|
|
9
|
+
*
|
|
10
|
+
* Injectable because the *contract* — a removal that fails must warn rather
|
|
11
|
+
* than throw — cannot otherwise be tested on every platform. Driving a real
|
|
12
|
+
* `fs.rm` failure needs a path the OS refuses, and those diverge: a path
|
|
13
|
+
* whose parent component is a regular file yields `ENOTDIR` on POSIX, and
|
|
14
|
+
* resolves silently on Windows. A test written against the POSIX shape
|
|
15
|
+
* passes locally and fails in CI, which is exactly what it did once.
|
|
16
|
+
*/
|
|
17
|
+
readonly remove?: (dir: string) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Delete a scratch directory as *best effort* — never failing the suite that
|
|
21
|
+
* created it, and never taking longer than its own budget to say so.
|
|
22
|
+
*
|
|
23
|
+
* ## Why this is not just `await rm(dir, { recursive: true, force: true })`
|
|
24
|
+
*
|
|
25
|
+
* A teardown hook that can redden a suite whose every assertion passed is a
|
|
26
|
+
* defect in the harness, not a flake. `packages/lab/test/instrument.test.ts`
|
|
27
|
+
* timed out here on two consecutive Windows runs with all 655 assertions
|
|
28
|
+
* green — only the cleanup lost.
|
|
29
|
+
*
|
|
30
|
+
* The measurement is what rules out the obvious fixes: that scratch dir holds
|
|
31
|
+
* 490 files / 378 KiB across 14 fixture git repos, and deletes in **59 ms**
|
|
32
|
+
* idle. Against vitest's 10,000 ms unit-hook budget that is 170x of headroom,
|
|
33
|
+
* and Windows blew through it anyway. No quantity of real work explains that,
|
|
34
|
+
* so the cause is scheduling — contention from a fully parallel `validate`,
|
|
35
|
+
* plus per-unlink antivirus on Windows — which is unbounded by nature. Hence:
|
|
36
|
+
*
|
|
37
|
+
* - **Raising `hookTimeout` cannot be argued.** You would be picking a number
|
|
38
|
+
* to beat an unbounded quantity, when 10s of 170x headroom already lost. It
|
|
39
|
+
* also punches a hole in the deliberate policy in `vitest.shared.ts` ("no
|
|
40
|
+
* hookTimeout override here on purpose") for every unit hook, to fix one.
|
|
41
|
+
* - **`try`/`catch` around the `rm` cannot work.** A vitest hook timeout is a
|
|
42
|
+
* race decided on the *timer* side; the hook's own catch never sees it. It
|
|
43
|
+
* addresses a failure mode we did not observe and leaves the one we did.
|
|
44
|
+
* - **`maxRetries` alone makes it worse.** Retries target transient
|
|
45
|
+
* `EPERM`/`EBUSY`, which fail *fast*; our failure was *slow*, and retry
|
|
46
|
+
* backoff only adds to it. Kept below as a cheap inner win, not as the fix.
|
|
47
|
+
*
|
|
48
|
+
* So the deadline is taken away from vitest: the removal races a timer of our
|
|
49
|
+
* own, well inside the hook budget, and expiry is a warning rather than a
|
|
50
|
+
* failure. The hook therefore always resolves in time, which makes it
|
|
51
|
+
* *structurally* incapable of reddening a green suite on any machine at any
|
|
52
|
+
* load — rather than merely unlikely to.
|
|
53
|
+
*
|
|
54
|
+
* The cost, stated plainly: under pathological contention the directory
|
|
55
|
+
* survives in the OS temp dir, which the OS reclaims, and the warning names
|
|
56
|
+
* the path. It can never surface as an unhandled rejection, because the only
|
|
57
|
+
* rejection handler is installed before the race.
|
|
58
|
+
*
|
|
59
|
+
* ⚠️ **Abandoning the removal does not stop it, and does not free the worker.**
|
|
60
|
+
* A pending libuv `fs` request is an active handle, so the `rm` runs to
|
|
61
|
+
* completion regardless — measured at 2,407 ms on an 8,000-file tree after the
|
|
62
|
+
* race was decided at 5 ms — and the process cannot exit until it does.
|
|
63
|
+
* `timer.unref()` below unrefs the *timer*, not the removal. So what this buys
|
|
64
|
+
* is bounded: the **hook** always resolves in time, which is what stops a green
|
|
65
|
+
* suite going red. It does **not** shed the work, and under the contention it
|
|
66
|
+
* targets the abandoned removal competes for disk with whatever runs next in
|
|
67
|
+
* the same worker. That is the trade, and it is why the budget wants to be as
|
|
68
|
+
* large as the tier's hook allows rather than as small as possible.
|
|
69
|
+
*
|
|
70
|
+
* @param dir - Directory to remove. An empty string is a no-op, so a suite
|
|
71
|
+
* whose `beforeAll` never ran can call this unconditionally.
|
|
72
|
+
* @param options - Deadline and warning sink
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```typescript
|
|
76
|
+
* afterAll(async () => {
|
|
77
|
+
* await removeScratchDir(scratch);
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export declare function removeScratchDir(dir: string, options?: RemoveScratchDirOptions): Promise<void>;
|
|
1
82
|
/**
|
|
2
83
|
* Get isolated test output directory for current test run
|
|
3
84
|
*
|
|
@@ -42,6 +123,9 @@ export declare function getTestOutputBase(packageName: string): string;
|
|
|
42
123
|
* than creating a new mkdtemp for each test.
|
|
43
124
|
*
|
|
44
125
|
* @param prefix - Prefix for the suite temp directory name
|
|
126
|
+
* @param teardown - Forwarded to {@link removeScratchDir}. Raise `budgetMs` for a
|
|
127
|
+
* suite whose fixture tree is heavy or whose tier allows a longer hook than the
|
|
128
|
+
* unit tier this default is sized against — see {@link SCRATCH_REMOVAL_BUDGET_MS}.
|
|
45
129
|
* @returns Suite helper with beforeAll, afterAll, beforeEach, afterEach, and getTempDir
|
|
46
130
|
*
|
|
47
131
|
* @example
|
|
@@ -60,7 +144,7 @@ export declare function getTestOutputBase(packageName: string): string;
|
|
|
60
144
|
* });
|
|
61
145
|
* ```
|
|
62
146
|
*/
|
|
63
|
-
export declare function setupAsyncTempDirSuite(prefix: string): {
|
|
147
|
+
export declare function setupAsyncTempDirSuite(prefix: string, teardown?: RemoveScratchDirOptions): {
|
|
64
148
|
beforeAll: () => Promise<void>;
|
|
65
149
|
afterAll: () => Promise<void>;
|
|
66
150
|
beforeEach: () => Promise<void>;
|
|
@@ -74,6 +158,9 @@ export declare function setupAsyncTempDirSuite(prefix: string): {
|
|
|
74
158
|
* than creating a new mkdtemp for each test.
|
|
75
159
|
*
|
|
76
160
|
* @param prefix - Prefix for the suite temp directory name
|
|
161
|
+
* @param teardown - Forwarded to {@link removeScratchDir}. Raise `budgetMs` for a
|
|
162
|
+
* suite whose fixture tree is heavy or whose tier allows a longer hook than the
|
|
163
|
+
* unit tier this default is sized against — see {@link SCRATCH_REMOVAL_BUDGET_MS}.
|
|
77
164
|
* @returns Suite helper with beforeAll, afterAll, beforeEach, afterEach, and getTempDir
|
|
78
165
|
*
|
|
79
166
|
* @example
|
|
@@ -92,27 +179,112 @@ export declare function setupAsyncTempDirSuite(prefix: string): {
|
|
|
92
179
|
* });
|
|
93
180
|
* ```
|
|
94
181
|
*/
|
|
95
|
-
export declare function setupSyncTempDirSuite(prefix: string): {
|
|
182
|
+
export declare function setupSyncTempDirSuite(prefix: string, teardown?: RemoveScratchDirOptions): {
|
|
96
183
|
beforeAll: () => void;
|
|
97
|
-
afterAll: () => void
|
|
184
|
+
afterAll: () => Promise<void>;
|
|
98
185
|
beforeEach: () => void;
|
|
99
186
|
afterEach: () => void;
|
|
100
187
|
getTempDir: () => string;
|
|
101
188
|
};
|
|
189
|
+
declare const symlinkCapabilityBrand: unique symbol;
|
|
190
|
+
/**
|
|
191
|
+
* Proof that this process can create filesystem symlinks.
|
|
192
|
+
*
|
|
193
|
+
* The only way to obtain one is {@link symlinkCapability}, and it exists at
|
|
194
|
+
* all only when a real probe already succeeded — so a function that requires
|
|
195
|
+
* this as a parameter cannot be reached by code that skipped the check. That
|
|
196
|
+
* is the point of branding it rather than passing a `boolean`: forgetting the
|
|
197
|
+
* check becomes a type error instead of a runtime `EPERM` on a machine you
|
|
198
|
+
* don't control.
|
|
199
|
+
*/
|
|
200
|
+
export type SymlinkCapability = {
|
|
201
|
+
readonly [symlinkCapabilityBrand]: true;
|
|
202
|
+
};
|
|
102
203
|
/**
|
|
103
|
-
*
|
|
204
|
+
* Whether this PROCESS can create symlinks — probed once and memoized.
|
|
104
205
|
*
|
|
105
206
|
* On Windows, `symlink()` needs either Developer Mode or
|
|
106
|
-
* `SeCreateSymbolicLinkPrivilege
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
207
|
+
* `SeCreateSymbolicLinkPrivilege`. That privilege lives on the process's
|
|
208
|
+
* security token, not on any one directory: it cannot change between calls
|
|
209
|
+
* within a single run, so probing it once and reusing the result is a
|
|
210
|
+
* memoization, not a shortcut that risks a stale answer. (An exotic
|
|
211
|
+
* filesystem that itself refuses symlinks — some network shares, some FAT
|
|
212
|
+
* variants — is a real exception this does not model; every fixture in this
|
|
213
|
+
* repo creates its roots under {@link normalizedTmpdir}, so it never arises
|
|
214
|
+
* here.)
|
|
215
|
+
*
|
|
216
|
+
* Fixtures that depend on symlinks must ask rather than assume — and, having
|
|
217
|
+
* asked, must SAY they skipped. A symlink case that silently no-ops reads as
|
|
218
|
+
* a passing test for a property nobody exercised.
|
|
219
|
+
*
|
|
220
|
+
* @returns A {@link SymlinkCapability} token when this process can create
|
|
221
|
+
* symlinks, else `null`. Route the `null` case through vitest's `skip()`
|
|
222
|
+
* rather than a plain `return`, so the skip is visible in the report.
|
|
223
|
+
*/
|
|
224
|
+
export declare function symlinkCapability(): SymlinkCapability | null;
|
|
225
|
+
/**
|
|
226
|
+
* Create a symlink — the one sanctioned call site for `fs.symlinkSync` in
|
|
227
|
+
* test code. Requires a {@link SymlinkCapability}, which only
|
|
228
|
+
* {@link symlinkCapability} can mint, so a test cannot reach the real
|
|
229
|
+
* syscall without first proving (or explicitly bypassing via `skip()`) that
|
|
230
|
+
* this host supports it.
|
|
110
231
|
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
232
|
+
* @param _cap - Proof from {@link symlinkCapability} that this host can create symlinks
|
|
233
|
+
* @param target - The existing path the new link should point at
|
|
234
|
+
* @param path - Where to create the link
|
|
235
|
+
* @param type - Windows-only link-type hint (`'file'` \| `'dir'` \| `'junction'`); ignored on POSIX
|
|
236
|
+
*/
|
|
237
|
+
export declare function createSymlink(_cap: SymlinkCapability, target: string, path: string, type?: 'dir' | 'file' | 'junction'): void;
|
|
238
|
+
/**
|
|
239
|
+
* The async counterpart of {@link createSymlink}, for fixtures already using
|
|
240
|
+
* `node:fs/promises`. Same capability requirement, same reasoning.
|
|
241
|
+
*
|
|
242
|
+
* @param _cap - Proof from {@link symlinkCapability} that this host can create symlinks
|
|
243
|
+
* @param target - The existing path the new link should point at
|
|
244
|
+
* @param path - Where to create the link
|
|
245
|
+
* @param type - Windows-only link-type hint (`'file'` \| `'dir'` \| `'junction'`); ignored on POSIX
|
|
246
|
+
*/
|
|
247
|
+
export declare function createSymlinkAsync(_cap: SymlinkCapability, target: string, path: string, type?: 'dir' | 'file' | 'junction'): Promise<void>;
|
|
248
|
+
/**
|
|
249
|
+
* The variables git exports into a hook, which a fixture must clear before it
|
|
250
|
+
* can fabricate its own.
|
|
113
251
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
252
|
+
* These are the ones git sets *for* you. Deliberately **not** the operator's own
|
|
253
|
+
* `GIT_CONFIG_COUNT`/`KEY_n`/`VALUE_n`/`GLOBAL`/`SYSTEM` channel — a test may be
|
|
254
|
+
* using that on purpose to point a clone at a local path, and clearing it sends
|
|
255
|
+
* the clone to the network instead.
|
|
256
|
+
*/
|
|
257
|
+
export declare const INHERITED_GIT_ENV: readonly ["GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_CEILING_DIRECTORIES", "GIT_COMMON_DIR", "GIT_CONFIG_PARAMETERS", "GIT_DIR", "GIT_DISCOVERY_ACROSS_FILESYSTEM", "GIT_GRAFT_FILE", "GIT_INDEX_FILE", "GIT_INDEX_VERSION", "GIT_NAMESPACE", "GIT_NOTES_REF", "GIT_OBJECT_DIRECTORY", "GIT_PREFIX", "GIT_SHALLOW_FILE", "GIT_WORK_TREE"];
|
|
258
|
+
/**
|
|
259
|
+
* Remove every inherited git redirection from `process.env`, and hand back the
|
|
260
|
+
* undo.
|
|
261
|
+
*
|
|
262
|
+
* A test that fabricates a hook environment has to start from a known-clean one,
|
|
263
|
+
* or it inherits whatever the *outer* runner exported and can no longer tell its
|
|
264
|
+
* own fixture apart from the ambient state — it then passes or fails for reasons
|
|
265
|
+
* it never set up. Restoring afterwards matters just as much: these are
|
|
266
|
+
* process-global, so a test that leaks `GIT_DIR` silently redirects every later
|
|
267
|
+
* test sharing the worker.
|
|
268
|
+
*
|
|
269
|
+
* ⚠️ **The key list is restated here on purpose, not by oversight.** Deriving it
|
|
270
|
+
* from `@vibe-validate/git`'s `stripGitEnv()` would be tidier, and it is exactly
|
|
271
|
+
* what this function did for one revision — but this module is the `./testing`
|
|
272
|
+
* subpath, which `subpath-purity.test.ts` pins as reaching **no third-party
|
|
273
|
+
* package at all** so it stays importable with zero dependencies installed. One
|
|
274
|
+
* import cost that property. The drift risk the derivation was avoiding is
|
|
275
|
+
* handled instead by {@link "../test/test-helpers-git-env.test".default}, which
|
|
276
|
+
* asserts this list equals what the shipped scrub removes.
|
|
277
|
+
*
|
|
278
|
+
* @returns A function restoring every variable to its prior value, putting back
|
|
279
|
+
* "was not set" as unset rather than as an empty string
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* ```typescript
|
|
283
|
+
* let restoreGitEnv: () => void;
|
|
284
|
+
* beforeEach(() => { restoreGitEnv = detachGitEnv(); });
|
|
285
|
+
* afterEach(() => { restoreGitEnv(); });
|
|
286
|
+
* ```
|
|
116
287
|
*/
|
|
117
|
-
export declare function
|
|
288
|
+
export declare function detachGitEnv(): () => void;
|
|
289
|
+
export {};
|
|
118
290
|
//# sourceMappingURL=test-helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../src/test-helpers.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"test-helpers.d.ts","sourceRoot":"","sources":["../src/test-helpers.ts"],"names":[],"mappings":"AA4BA,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,CAuBA;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;AAED,OAAO,CAAC,MAAM,sBAAsB,EAAE,OAAO,MAAM,CAAC;AAEpD;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAAE,QAAQ,CAAC,CAAC,sBAAsB,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAI5E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,IAAI,iBAAiB,GAAG,IAAI,CAuB5D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,iBAAiB,EACvB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,UAAU,GACjC,IAAI,CAGN;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,iBAAiB,EACvB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,UAAU,GACjC,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,yUAgBpB,CAAC;AAEX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,YAAY,IAAI,MAAM,IAAI,CAoBzC"}
|
package/dist/test-helpers.js
CHANGED
|
@@ -2,6 +2,126 @@ import { randomBytes } from 'node:crypto';
|
|
|
2
2
|
import { mkdtempSync, rmSync, symlinkSync } from 'node:fs';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
4
|
import { mkdirSyncReal, normalizedTmpdir, safePath } from './path-utils.js';
|
|
5
|
+
/**
|
|
6
|
+
* How long a scratch-dir teardown may run before it gives up and warns.
|
|
7
|
+
*
|
|
8
|
+
* The value only has to be comfortably *under* the hook timeout it runs in —
|
|
9
|
+
* that is the whole design. Sizing a teardown budget to beat contention is
|
|
10
|
+
* unprovable (see {@link removeScratchDir}); sizing it below a known constant is
|
|
11
|
+
* arithmetic.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ **The known constant is the UNIT tier's**, which is the only tier that
|
|
14
|
+
* takes vitest's 10s default (`vitest.shared.ts` declines to override it there
|
|
15
|
+
* on purpose) and the tier where the flake was actually observed. The other two
|
|
16
|
+
* tiers set their own, far larger: integration gets `platformTestTimeout`
|
|
17
|
+
* (60s on Unix, 900s on Windows) and system gets 300s. A suite in those tiers
|
|
18
|
+
* inherits this 4s default and therefore gives up 15x–225x earlier than its hook
|
|
19
|
+
* would have allowed — for a heavy fixture tree that is a leaked directory and a
|
|
20
|
+
* warning bought for nothing, since an abandoned removal does not stop (see
|
|
21
|
+
* {@link removeScratchDir}). Such a suite should pass its own `budgetMs`, which
|
|
22
|
+
* both suite helpers forward.
|
|
23
|
+
*/
|
|
24
|
+
const SCRATCH_REMOVAL_BUDGET_MS = 4000;
|
|
25
|
+
/**
|
|
26
|
+
* Delete a scratch directory as *best effort* — never failing the suite that
|
|
27
|
+
* created it, and never taking longer than its own budget to say so.
|
|
28
|
+
*
|
|
29
|
+
* ## Why this is not just `await rm(dir, { recursive: true, force: true })`
|
|
30
|
+
*
|
|
31
|
+
* A teardown hook that can redden a suite whose every assertion passed is a
|
|
32
|
+
* defect in the harness, not a flake. `packages/lab/test/instrument.test.ts`
|
|
33
|
+
* timed out here on two consecutive Windows runs with all 655 assertions
|
|
34
|
+
* green — only the cleanup lost.
|
|
35
|
+
*
|
|
36
|
+
* The measurement is what rules out the obvious fixes: that scratch dir holds
|
|
37
|
+
* 490 files / 378 KiB across 14 fixture git repos, and deletes in **59 ms**
|
|
38
|
+
* idle. Against vitest's 10,000 ms unit-hook budget that is 170x of headroom,
|
|
39
|
+
* and Windows blew through it anyway. No quantity of real work explains that,
|
|
40
|
+
* so the cause is scheduling — contention from a fully parallel `validate`,
|
|
41
|
+
* plus per-unlink antivirus on Windows — which is unbounded by nature. Hence:
|
|
42
|
+
*
|
|
43
|
+
* - **Raising `hookTimeout` cannot be argued.** You would be picking a number
|
|
44
|
+
* to beat an unbounded quantity, when 10s of 170x headroom already lost. It
|
|
45
|
+
* also punches a hole in the deliberate policy in `vitest.shared.ts` ("no
|
|
46
|
+
* hookTimeout override here on purpose") for every unit hook, to fix one.
|
|
47
|
+
* - **`try`/`catch` around the `rm` cannot work.** A vitest hook timeout is a
|
|
48
|
+
* race decided on the *timer* side; the hook's own catch never sees it. It
|
|
49
|
+
* addresses a failure mode we did not observe and leaves the one we did.
|
|
50
|
+
* - **`maxRetries` alone makes it worse.** Retries target transient
|
|
51
|
+
* `EPERM`/`EBUSY`, which fail *fast*; our failure was *slow*, and retry
|
|
52
|
+
* backoff only adds to it. Kept below as a cheap inner win, not as the fix.
|
|
53
|
+
*
|
|
54
|
+
* So the deadline is taken away from vitest: the removal races a timer of our
|
|
55
|
+
* own, well inside the hook budget, and expiry is a warning rather than a
|
|
56
|
+
* failure. The hook therefore always resolves in time, which makes it
|
|
57
|
+
* *structurally* incapable of reddening a green suite on any machine at any
|
|
58
|
+
* load — rather than merely unlikely to.
|
|
59
|
+
*
|
|
60
|
+
* The cost, stated plainly: under pathological contention the directory
|
|
61
|
+
* survives in the OS temp dir, which the OS reclaims, and the warning names
|
|
62
|
+
* the path. It can never surface as an unhandled rejection, because the only
|
|
63
|
+
* rejection handler is installed before the race.
|
|
64
|
+
*
|
|
65
|
+
* ⚠️ **Abandoning the removal does not stop it, and does not free the worker.**
|
|
66
|
+
* A pending libuv `fs` request is an active handle, so the `rm` runs to
|
|
67
|
+
* completion regardless — measured at 2,407 ms on an 8,000-file tree after the
|
|
68
|
+
* race was decided at 5 ms — and the process cannot exit until it does.
|
|
69
|
+
* `timer.unref()` below unrefs the *timer*, not the removal. So what this buys
|
|
70
|
+
* is bounded: the **hook** always resolves in time, which is what stops a green
|
|
71
|
+
* suite going red. It does **not** shed the work, and under the contention it
|
|
72
|
+
* targets the abandoned removal competes for disk with whatever runs next in
|
|
73
|
+
* the same worker. That is the trade, and it is why the budget wants to be as
|
|
74
|
+
* large as the tier's hook allows rather than as small as possible.
|
|
75
|
+
*
|
|
76
|
+
* @param dir - Directory to remove. An empty string is a no-op, so a suite
|
|
77
|
+
* whose `beforeAll` never ran can call this unconditionally.
|
|
78
|
+
* @param options - Deadline and warning sink
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* afterAll(async () => {
|
|
83
|
+
* await removeScratchDir(scratch);
|
|
84
|
+
* });
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export async function removeScratchDir(dir, options = {}) {
|
|
88
|
+
if (dir === '')
|
|
89
|
+
return;
|
|
90
|
+
const budgetMs = options.budgetMs ?? SCRATCH_REMOVAL_BUDGET_MS;
|
|
91
|
+
const onWarn = options.onWarn ??
|
|
92
|
+
((message) => {
|
|
93
|
+
console.warn(message);
|
|
94
|
+
});
|
|
95
|
+
// Latches on the first outcome so a removal that finishes (or fails) after
|
|
96
|
+
// the budget expired cannot log a second time into an already-finished suite.
|
|
97
|
+
let settled = false;
|
|
98
|
+
const giveUp = (reason) => {
|
|
99
|
+
if (settled)
|
|
100
|
+
return;
|
|
101
|
+
settled = true;
|
|
102
|
+
onWarn(`scratch dir left behind at ${dir}: ${reason}`);
|
|
103
|
+
};
|
|
104
|
+
const remove = options.remove ??
|
|
105
|
+
((target) => fs.rm(target, { recursive: true, force: true, maxRetries: 2, retryDelay: 50 }));
|
|
106
|
+
const removal = remove(dir)
|
|
107
|
+
.then(() => {
|
|
108
|
+
settled = true;
|
|
109
|
+
})
|
|
110
|
+
.catch((error) => {
|
|
111
|
+
giveUp(error instanceof Error ? error.message : String(error));
|
|
112
|
+
});
|
|
113
|
+
let timer;
|
|
114
|
+
const deadline = new Promise((resolve) => {
|
|
115
|
+
timer = setTimeout(() => {
|
|
116
|
+
giveUp(`removal did not finish within ${budgetMs}ms`);
|
|
117
|
+
resolve();
|
|
118
|
+
}, budgetMs);
|
|
119
|
+
// Never hold the process open for a teardown nobody is waiting on.
|
|
120
|
+
timer.unref();
|
|
121
|
+
});
|
|
122
|
+
await Promise.race([removal, deadline]);
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
}
|
|
5
125
|
/**
|
|
6
126
|
* Get isolated test output directory for current test run
|
|
7
127
|
*
|
|
@@ -60,6 +180,9 @@ export function getTestOutputBase(packageName) {
|
|
|
60
180
|
* than creating a new mkdtemp for each test.
|
|
61
181
|
*
|
|
62
182
|
* @param prefix - Prefix for the suite temp directory name
|
|
183
|
+
* @param teardown - Forwarded to {@link removeScratchDir}. Raise `budgetMs` for a
|
|
184
|
+
* suite whose fixture tree is heavy or whose tier allows a longer hook than the
|
|
185
|
+
* unit tier this default is sized against — see {@link SCRATCH_REMOVAL_BUDGET_MS}.
|
|
63
186
|
* @returns Suite helper with beforeAll, afterAll, beforeEach, afterEach, and getTempDir
|
|
64
187
|
*
|
|
65
188
|
* @example
|
|
@@ -78,7 +201,7 @@ export function getTestOutputBase(packageName) {
|
|
|
78
201
|
* });
|
|
79
202
|
* ```
|
|
80
203
|
*/
|
|
81
|
-
export function setupAsyncTempDirSuite(prefix) {
|
|
204
|
+
export function setupAsyncTempDirSuite(prefix, teardown = {}) {
|
|
82
205
|
let suiteDir = '';
|
|
83
206
|
let tempDir = '';
|
|
84
207
|
let testCounter = 0;
|
|
@@ -87,9 +210,7 @@ export function setupAsyncTempDirSuite(prefix) {
|
|
|
87
210
|
suiteDir = await fs.mkdtemp(safePath.join(normalizedTmpdir(), `${prefix}-suite-`));
|
|
88
211
|
},
|
|
89
212
|
afterAll: async () => {
|
|
90
|
-
|
|
91
|
-
await fs.rm(suiteDir, { recursive: true, force: true });
|
|
92
|
-
}
|
|
213
|
+
await removeScratchDir(suiteDir, teardown);
|
|
93
214
|
},
|
|
94
215
|
beforeEach: async () => {
|
|
95
216
|
testCounter++;
|
|
@@ -110,6 +231,9 @@ export function setupAsyncTempDirSuite(prefix) {
|
|
|
110
231
|
* than creating a new mkdtemp for each test.
|
|
111
232
|
*
|
|
112
233
|
* @param prefix - Prefix for the suite temp directory name
|
|
234
|
+
* @param teardown - Forwarded to {@link removeScratchDir}. Raise `budgetMs` for a
|
|
235
|
+
* suite whose fixture tree is heavy or whose tier allows a longer hook than the
|
|
236
|
+
* unit tier this default is sized against — see {@link SCRATCH_REMOVAL_BUDGET_MS}.
|
|
113
237
|
* @returns Suite helper with beforeAll, afterAll, beforeEach, afterEach, and getTempDir
|
|
114
238
|
*
|
|
115
239
|
* @example
|
|
@@ -128,7 +252,7 @@ export function setupAsyncTempDirSuite(prefix) {
|
|
|
128
252
|
* });
|
|
129
253
|
* ```
|
|
130
254
|
*/
|
|
131
|
-
export function setupSyncTempDirSuite(prefix) {
|
|
255
|
+
export function setupSyncTempDirSuite(prefix, teardown = {}) {
|
|
132
256
|
let suiteDir = '';
|
|
133
257
|
let tempDir = '';
|
|
134
258
|
let testCounter = 0;
|
|
@@ -136,10 +260,8 @@ export function setupSyncTempDirSuite(prefix) {
|
|
|
136
260
|
beforeAll: () => {
|
|
137
261
|
suiteDir = mkdtempSync(safePath.join(normalizedTmpdir(), `${prefix}-suite-`));
|
|
138
262
|
},
|
|
139
|
-
afterAll: () => {
|
|
140
|
-
|
|
141
|
-
rmSync(suiteDir, { recursive: true, force: true });
|
|
142
|
-
}
|
|
263
|
+
afterAll: async () => {
|
|
264
|
+
await removeScratchDir(suiteDir, teardown);
|
|
143
265
|
},
|
|
144
266
|
beforeEach: () => {
|
|
145
267
|
testCounter++;
|
|
@@ -152,31 +274,156 @@ export function setupSyncTempDirSuite(prefix) {
|
|
|
152
274
|
getTempDir: () => tempDir,
|
|
153
275
|
};
|
|
154
276
|
}
|
|
277
|
+
let cachedCapability;
|
|
155
278
|
/**
|
|
156
|
-
*
|
|
279
|
+
* Whether this PROCESS can create symlinks — probed once and memoized.
|
|
157
280
|
*
|
|
158
281
|
* On Windows, `symlink()` needs either Developer Mode or
|
|
159
|
-
* `SeCreateSymbolicLinkPrivilege
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
282
|
+
* `SeCreateSymbolicLinkPrivilege`. That privilege lives on the process's
|
|
283
|
+
* security token, not on any one directory: it cannot change between calls
|
|
284
|
+
* within a single run, so probing it once and reusing the result is a
|
|
285
|
+
* memoization, not a shortcut that risks a stale answer. (An exotic
|
|
286
|
+
* filesystem that itself refuses symlinks — some network shares, some FAT
|
|
287
|
+
* variants — is a real exception this does not model; every fixture in this
|
|
288
|
+
* repo creates its roots under {@link normalizedTmpdir}, so it never arises
|
|
289
|
+
* here.)
|
|
163
290
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
291
|
+
* Fixtures that depend on symlinks must ask rather than assume — and, having
|
|
292
|
+
* asked, must SAY they skipped. A symlink case that silently no-ops reads as
|
|
293
|
+
* a passing test for a property nobody exercised.
|
|
166
294
|
*
|
|
167
|
-
* @
|
|
168
|
-
*
|
|
295
|
+
* @returns A {@link SymlinkCapability} token when this process can create
|
|
296
|
+
* symlinks, else `null`. Route the `null` case through vitest's `skip()`
|
|
297
|
+
* rather than a plain `return`, so the skip is visible in the report.
|
|
169
298
|
*/
|
|
170
|
-
export function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
299
|
+
export function symlinkCapability() {
|
|
300
|
+
if (cachedCapability === undefined) {
|
|
301
|
+
const probe = safePath.join(normalizedTmpdir(), `.vat-symlink-probe-${randomBytes(4).toString('hex')}`);
|
|
302
|
+
try {
|
|
303
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- fixed tmp dir plus a random basename generated here
|
|
304
|
+
symlinkSync('.', probe);
|
|
305
|
+
cachedCapability = {};
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
cachedCapability = null;
|
|
309
|
+
}
|
|
310
|
+
if (cachedCapability !== null) {
|
|
311
|
+
// Best-effort: the capability answer comes from creation succeeding, not
|
|
312
|
+
// from cleanup — a probe left behind by a failed rmSync (e.g. a transient
|
|
313
|
+
// lock on the freshly-created reparse point) must not flip a real "yes"
|
|
314
|
+
// into a memoized, process-wide "no".
|
|
315
|
+
try {
|
|
316
|
+
rmSync(probe, { force: true });
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
// Leftover probe file; harmless, and not this function's concern.
|
|
320
|
+
}
|
|
321
|
+
}
|
|
175
322
|
}
|
|
176
|
-
|
|
177
|
-
|
|
323
|
+
return cachedCapability;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Create a symlink — the one sanctioned call site for `fs.symlinkSync` in
|
|
327
|
+
* test code. Requires a {@link SymlinkCapability}, which only
|
|
328
|
+
* {@link symlinkCapability} can mint, so a test cannot reach the real
|
|
329
|
+
* syscall without first proving (or explicitly bypassing via `skip()`) that
|
|
330
|
+
* this host supports it.
|
|
331
|
+
*
|
|
332
|
+
* @param _cap - Proof from {@link symlinkCapability} that this host can create symlinks
|
|
333
|
+
* @param target - The existing path the new link should point at
|
|
334
|
+
* @param path - Where to create the link
|
|
335
|
+
* @param type - Windows-only link-type hint (`'file'` \| `'dir'` \| `'junction'`); ignored on POSIX
|
|
336
|
+
*/
|
|
337
|
+
export function createSymlink(_cap, target, path, type) {
|
|
338
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- caller-supplied path; the capability parameter is what proves this call site is sanctioned
|
|
339
|
+
symlinkSync(target, path, type);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The async counterpart of {@link createSymlink}, for fixtures already using
|
|
343
|
+
* `node:fs/promises`. Same capability requirement, same reasoning.
|
|
344
|
+
*
|
|
345
|
+
* @param _cap - Proof from {@link symlinkCapability} that this host can create symlinks
|
|
346
|
+
* @param target - The existing path the new link should point at
|
|
347
|
+
* @param path - Where to create the link
|
|
348
|
+
* @param type - Windows-only link-type hint (`'file'` \| `'dir'` \| `'junction'`); ignored on POSIX
|
|
349
|
+
*/
|
|
350
|
+
export async function createSymlinkAsync(_cap, target, path, type) {
|
|
351
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- caller-supplied path; the capability parameter is what proves this call site is sanctioned
|
|
352
|
+
await fs.symlink(target, path, type);
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The variables git exports into a hook, which a fixture must clear before it
|
|
356
|
+
* can fabricate its own.
|
|
357
|
+
*
|
|
358
|
+
* These are the ones git sets *for* you. Deliberately **not** the operator's own
|
|
359
|
+
* `GIT_CONFIG_COUNT`/`KEY_n`/`VALUE_n`/`GLOBAL`/`SYSTEM` channel — a test may be
|
|
360
|
+
* using that on purpose to point a clone at a local path, and clearing it sends
|
|
361
|
+
* the clone to the network instead.
|
|
362
|
+
*/
|
|
363
|
+
export const INHERITED_GIT_ENV = [
|
|
364
|
+
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
|
|
365
|
+
'GIT_CEILING_DIRECTORIES',
|
|
366
|
+
'GIT_COMMON_DIR',
|
|
367
|
+
'GIT_CONFIG_PARAMETERS',
|
|
368
|
+
'GIT_DIR',
|
|
369
|
+
'GIT_DISCOVERY_ACROSS_FILESYSTEM',
|
|
370
|
+
'GIT_GRAFT_FILE',
|
|
371
|
+
'GIT_INDEX_FILE',
|
|
372
|
+
'GIT_INDEX_VERSION',
|
|
373
|
+
'GIT_NAMESPACE',
|
|
374
|
+
'GIT_NOTES_REF',
|
|
375
|
+
'GIT_OBJECT_DIRECTORY',
|
|
376
|
+
'GIT_PREFIX',
|
|
377
|
+
'GIT_SHALLOW_FILE',
|
|
378
|
+
'GIT_WORK_TREE',
|
|
379
|
+
];
|
|
380
|
+
/**
|
|
381
|
+
* Remove every inherited git redirection from `process.env`, and hand back the
|
|
382
|
+
* undo.
|
|
383
|
+
*
|
|
384
|
+
* A test that fabricates a hook environment has to start from a known-clean one,
|
|
385
|
+
* or it inherits whatever the *outer* runner exported and can no longer tell its
|
|
386
|
+
* own fixture apart from the ambient state — it then passes or fails for reasons
|
|
387
|
+
* it never set up. Restoring afterwards matters just as much: these are
|
|
388
|
+
* process-global, so a test that leaks `GIT_DIR` silently redirects every later
|
|
389
|
+
* test sharing the worker.
|
|
390
|
+
*
|
|
391
|
+
* ⚠️ **The key list is restated here on purpose, not by oversight.** Deriving it
|
|
392
|
+
* from `@vibe-validate/git`'s `stripGitEnv()` would be tidier, and it is exactly
|
|
393
|
+
* what this function did for one revision — but this module is the `./testing`
|
|
394
|
+
* subpath, which `subpath-purity.test.ts` pins as reaching **no third-party
|
|
395
|
+
* package at all** so it stays importable with zero dependencies installed. One
|
|
396
|
+
* import cost that property. The drift risk the derivation was avoiding is
|
|
397
|
+
* handled instead by {@link "../test/test-helpers-git-env.test".default}, which
|
|
398
|
+
* asserts this list equals what the shipped scrub removes.
|
|
399
|
+
*
|
|
400
|
+
* @returns A function restoring every variable to its prior value, putting back
|
|
401
|
+
* "was not set" as unset rather than as an empty string
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* ```typescript
|
|
405
|
+
* let restoreGitEnv: () => void;
|
|
406
|
+
* beforeEach(() => { restoreGitEnv = detachGitEnv(); });
|
|
407
|
+
* afterEach(() => { restoreGitEnv(); });
|
|
408
|
+
* ```
|
|
409
|
+
*/
|
|
410
|
+
export function detachGitEnv() {
|
|
411
|
+
const saved = new Map();
|
|
412
|
+
const forget = (name) => {
|
|
413
|
+
saved.set(name, process.env[name]);
|
|
414
|
+
delete process.env[name];
|
|
415
|
+
};
|
|
416
|
+
for (const name of INHERITED_GIT_ENV) {
|
|
417
|
+
forget(name);
|
|
178
418
|
}
|
|
179
|
-
|
|
180
|
-
|
|
419
|
+
return () => {
|
|
420
|
+
for (const [name, value] of saved) {
|
|
421
|
+
// Deleted first so an absent variable is restored as absent: assigning
|
|
422
|
+
// `undefined` would leave the literal string 'undefined' behind.
|
|
423
|
+
delete process.env[name];
|
|
424
|
+
if (value !== undefined)
|
|
425
|
+
process.env[name] = value;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
181
428
|
}
|
|
182
429
|
//# sourceMappingURL=test-helpers.js.map
|
package/dist/test-helpers.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-helpers.js","sourceRoot":"","sources":["../src/test-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3D,OAAO,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"test-helpers.js","sourceRoot":"","sources":["../src/test-helpers.ts"],"names":[],"mappings":"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"}
|