@vibe-agent-toolkit/utils 0.2.0-rc.7 → 0.2.0-rc.8

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.
Files changed (175) hide show
  1. package/README.md +22 -3
  2. package/dist/asset-reference.js +3 -11
  3. package/dist/asset-reference.js.map +1 -1
  4. package/dist/crawl-timing.d.ts +3 -3
  5. package/dist/crawl-timing.js +3 -3
  6. package/dist/dirent-kind.d.ts +83 -0
  7. package/dist/dirent-kind.d.ts.map +1 -0
  8. package/dist/dirent-kind.js +129 -0
  9. package/dist/dirent-kind.js.map +1 -0
  10. package/dist/errors/errno.d.ts +51 -0
  11. package/dist/errors/errno.d.ts.map +1 -0
  12. package/dist/errors/errno.js +112 -0
  13. package/dist/errors/errno.js.map +1 -0
  14. package/dist/errors/vat-error.d.ts +60 -0
  15. package/dist/errors/vat-error.d.ts.map +1 -0
  16. package/dist/errors/vat-error.js +86 -0
  17. package/dist/errors/vat-error.js.map +1 -0
  18. package/dist/file-crawler.js +9 -9
  19. package/dist/file-crawler.js.map +1 -1
  20. package/dist/fs/file-hash.d.ts.map +1 -1
  21. package/dist/fs/file-hash.js +0 -1
  22. package/dist/fs/file-hash.js.map +1 -1
  23. package/dist/fs-utils.d.ts +16 -18
  24. package/dist/fs-utils.d.ts.map +1 -1
  25. package/dist/fs-utils.js +56 -80
  26. package/dist/fs-utils.js.map +1 -1
  27. package/dist/fs.d.ts +2 -1
  28. package/dist/fs.d.ts.map +1 -1
  29. package/dist/fs.js +2 -1
  30. package/dist/fs.js.map +1 -1
  31. package/dist/git-run.d.ts +1 -1
  32. package/dist/git-run.js +1 -1
  33. package/dist/git-tracker.d.ts +1 -1
  34. package/dist/git-tracker.d.ts.map +1 -1
  35. package/dist/git-tracker.js +1 -2
  36. package/dist/git-tracker.js.map +1 -1
  37. package/dist/git-utils.d.ts.map +1 -1
  38. package/dist/git-utils.js +0 -1
  39. package/dist/git-utils.js.map +1 -1
  40. package/dist/gitignore-checker.d.ts.map +1 -1
  41. package/dist/gitignore-checker.js +9 -3
  42. package/dist/gitignore-checker.js.map +1 -1
  43. package/dist/index.d.ts +7 -3
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +19 -4
  46. package/dist/index.js.map +1 -1
  47. package/dist/listing-refusal.d.ts +2 -1
  48. package/dist/listing-refusal.d.ts.map +1 -1
  49. package/dist/listing-refusal.js +3 -3
  50. package/dist/listing-refusal.js.map +1 -1
  51. package/dist/path-containment.d.ts +64 -0
  52. package/dist/path-containment.d.ts.map +1 -0
  53. package/dist/path-containment.js +111 -0
  54. package/dist/path-containment.js.map +1 -0
  55. package/dist/path-core.d.ts +77 -3
  56. package/dist/path-core.d.ts.map +1 -1
  57. package/dist/path-core.js +96 -6
  58. package/dist/path-core.js.map +1 -1
  59. package/dist/path-utils.d.ts.map +1 -1
  60. package/dist/path-utils.js +42 -47
  61. package/dist/path-utils.js.map +1 -1
  62. package/dist/path.d.ts +1 -1
  63. package/dist/path.d.ts.map +1 -1
  64. package/dist/path.js +1 -1
  65. package/dist/path.js.map +1 -1
  66. package/dist/project-utils.d.ts.map +1 -1
  67. package/dist/project-utils.js +9 -6
  68. package/dist/project-utils.js.map +1 -1
  69. package/dist/safe-exec.d.ts +2 -1
  70. package/dist/safe-exec.d.ts.map +1 -1
  71. package/dist/safe-exec.js +25 -7
  72. package/dist/safe-exec.js.map +1 -1
  73. package/dist/skill-targets.d.ts +2 -2
  74. package/dist/skill-targets.js +2 -2
  75. package/dist/skill-test/auth-resolver.d.ts +10 -2
  76. package/dist/skill-test/auth-resolver.d.ts.map +1 -1
  77. package/dist/skill-test/auth-resolver.js +26 -11
  78. package/dist/skill-test/auth-resolver.js.map +1 -1
  79. package/dist/skill-test/spawn-claude.d.ts +2 -1
  80. package/dist/skill-test/spawn-claude.d.ts.map +1 -1
  81. package/dist/skill-test/spawn-claude.js +20 -3
  82. package/dist/skill-test/spawn-claude.js.map +1 -1
  83. package/dist/skill-test/transcript.d.ts.map +1 -1
  84. package/dist/skill-test/transcript.js +18 -17
  85. package/dist/skill-test/transcript.js.map +1 -1
  86. package/dist/stdio-blocking.js +13 -14
  87. package/dist/stdio-blocking.js.map +1 -1
  88. package/dist/test-helpers.d.ts +47 -204
  89. package/dist/test-helpers.d.ts.map +1 -1
  90. package/dist/test-helpers.js +105 -310
  91. package/dist/test-helpers.js.map +1 -1
  92. package/dist/testing/executables.d.ts +32 -0
  93. package/dist/testing/executables.d.ts.map +1 -0
  94. package/dist/testing/executables.js +70 -0
  95. package/dist/testing/executables.js.map +1 -0
  96. package/dist/testing/hostile-tree.d.ts +79 -0
  97. package/dist/testing/hostile-tree.d.ts.map +1 -0
  98. package/dist/testing/hostile-tree.js +164 -0
  99. package/dist/testing/hostile-tree.js.map +1 -0
  100. package/dist/testing/platform-gates.d.ts +31 -0
  101. package/dist/testing/platform-gates.d.ts.map +1 -0
  102. package/dist/testing/platform-gates.js +31 -0
  103. package/dist/testing/platform-gates.js.map +1 -0
  104. package/dist/testing/temp-dir.d.ts +255 -0
  105. package/dist/testing/temp-dir.d.ts.map +1 -0
  106. package/dist/testing/temp-dir.js +363 -0
  107. package/dist/testing/temp-dir.js.map +1 -0
  108. package/dist/testing.d.ts +5 -0
  109. package/dist/testing.d.ts.map +1 -1
  110. package/dist/testing.js +10 -1
  111. package/dist/testing.js.map +1 -1
  112. package/dist/text-content.d.ts.map +1 -1
  113. package/dist/text-content.js +7 -1
  114. package/dist/text-content.js.map +1 -1
  115. package/dist/text-file.d.ts.map +1 -1
  116. package/dist/text-file.js +0 -2
  117. package/dist/text-file.js.map +1 -1
  118. package/dist/timing-dump.d.ts +1 -1
  119. package/dist/timing-dump.d.ts.map +1 -1
  120. package/dist/timing-dump.js +1 -2
  121. package/dist/timing-dump.js.map +1 -1
  122. package/dist/windows-shell.d.ts +0 -1
  123. package/dist/windows-shell.d.ts.map +1 -1
  124. package/dist/windows-shell.js +0 -1
  125. package/dist/windows-shell.js.map +1 -1
  126. package/dist/zod-introspection.js +2 -2
  127. package/dist/zod-introspection.js.map +1 -1
  128. package/eslint/README.md +125 -67
  129. package/eslint/index.cjs +111 -162
  130. package/eslint/index.d.cts +14 -17
  131. package/eslint/rules/callee-name.cjs +23 -0
  132. package/eslint/rules/commands-import-boundary.cjs +148 -0
  133. package/eslint/rules/dead-import.cjs +1 -1
  134. package/eslint/rules/dirent-type-needs-symlink-check.cjs +282 -0
  135. package/eslint/rules/eslint-rule-factory.cjs +5 -2
  136. package/eslint/rules/exempt-path-matcher.cjs +1 -1
  137. package/eslint/rules/explicit-zod-strictness.cjs +120 -0
  138. package/eslint/rules/no-bare-dynamic-import-path.cjs +5 -1
  139. package/eslint/rules/no-bare-executable-spawn.cjs +70 -0
  140. package/eslint/rules/no-bare-symlink-in-tests.cjs +16 -3
  141. package/eslint/rules/no-blind-catch.cjs +171 -0
  142. package/eslint/rules/no-child-process-execSync.cjs +8 -0
  143. package/eslint/rules/no-decaying-referent.cjs +216 -0
  144. package/eslint/rules/no-dotdot-containment.cjs +160 -0
  145. package/eslint/rules/no-file-url-string-concat.cjs +4 -1
  146. package/eslint/rules/no-fragile-entrypoint-guard.cjs +14 -1
  147. package/eslint/rules/no-fs-mkdirSync.cjs +8 -0
  148. package/eslint/rules/no-fs-promises-cp.cjs +7 -0
  149. package/eslint/rules/no-fs-realpathSync.cjs +8 -0
  150. package/eslint/rules/no-hardcoded-path-split.cjs +7 -2
  151. package/eslint/rules/no-io-in-unit-tier.cjs +128 -0
  152. package/eslint/rules/no-literal-process-exit.cjs +129 -0
  153. package/eslint/rules/no-manual-path-normalize.cjs +7 -2
  154. package/eslint/rules/no-os-tmpdir.cjs +8 -0
  155. package/eslint/rules/no-path-operations-in-comparisons.cjs +7 -2
  156. package/eslint/rules/no-path-sep-in-strings.cjs +7 -2
  157. package/eslint/rules/no-path-startswith.cjs +7 -2
  158. package/eslint/rules/no-process-exit-in-phase.cjs +12 -2
  159. package/eslint/rules/no-raw-node-path.cjs +563 -0
  160. package/eslint/rules/no-raw-text-decode.cjs +10 -1
  161. package/eslint/rules/no-registry-count-pin.cjs +255 -0
  162. package/eslint/rules/no-self-package-import.cjs +13 -2
  163. package/eslint/rules/no-test-scoped-functions.cjs +10 -3
  164. package/eslint/rules/no-unix-shell-commands.cjs +6 -2
  165. package/eslint/rules/no-unsafe-root-join.cjs +23 -2
  166. package/eslint/rules/no-url-pathname-for-fs.cjs +5 -1
  167. package/eslint/rules/no-version-literal.cjs +151 -0
  168. package/eslint/rules/prefer-startswith-over-regex.cjs +9 -0
  169. package/eslint/rules/require-justified-skip.cjs +8 -2
  170. package/eslint/rules/safe-import.cjs +1 -1
  171. package/package.json +4 -9
  172. package/eslint/rules/no-path-join.cjs +0 -14
  173. package/eslint/rules/no-path-relative.cjs +0 -14
  174. package/eslint/rules/no-path-resolve.cjs +0 -14
  175. package/eslint/rules/path-function-rule-factory.cjs +0 -496
@@ -1,280 +1,19 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import nodeFs, { mkdtempSync, rmSync, symlinkSync } from 'node:fs';
2
+ import nodeFs, { rmSync, symlinkSync } from 'node:fs';
3
3
  import fs from 'node:fs/promises';
4
- import { mkdirSyncReal, normalizedTmpdir, safePath, toForwardSlash } 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
- }
125
- /**
126
- * Get isolated test output directory for current test run
127
- *
128
- * Creates a unique directory under `packages/{packageName}/.test-output/{testType}/{runId}`
129
- * where runId is `{timestamp}-{randomId}` to ensure isolation across parallel test runs.
130
- *
131
- * @param packageName - Name of package (e.g., 'rag-lancedb')
132
- * @param testType - Type of test ('unit', 'integration', 'system')
133
- * @param subdirs - Optional subdirectories to create within the test output directory
134
- * @returns Absolute path to the created directory
135
- *
136
- * @example
137
- * ```typescript
138
- * // Create isolated database directory for system tests
139
- * const dbPath = getTestOutputDir('rag-lancedb', 'system', 'databases', 'test-db');
140
- * // Result: packages/rag-lancedb/.test-output/system/20260105-143022-abc123/databases/test-db
141
- *
142
- * // Create temporary file directory for integration tests
143
- * const tempDir = getTestOutputDir('agent-skills', 'integration', 'temp-files');
144
- * // Result: packages/agent-skills/.test-output/integration/20260105-143022-def456/temp-files
145
- * ```
146
- */
147
- export function getTestOutputDir(packageName, testType, ...subdirs) {
148
- // Generate unique run ID: timestamp + random hex
149
- const timestamp = new Date().toISOString().replaceAll(/[:.]/g, '-').slice(0, 19);
150
- const randomId = randomBytes(4).toString('hex');
151
- const runId = `${timestamp}-${randomId}`;
152
- // Find project root (assuming we're always in packages/*/test/*)
153
- const projectRoot = safePath.resolve(process.cwd());
154
- // Build path: packages/{packageName}/.test-output/{testType}/{runId}/{...subdirs}
155
- const testOutputDir = safePath.join(projectRoot, 'packages', packageName, '.test-output', testType, runId, ...subdirs);
156
- // Create directory structure and return normalized path
157
- return mkdirSyncReal(testOutputDir, { recursive: true });
158
- }
159
- /**
160
- * Get the base test output directory for a package
161
- * Useful for cleanup operations that need to remove all test output
162
- *
163
- * @param packageName - Name of package (e.g., 'rag-lancedb')
164
- * @returns Absolute path to packages/{packageName}/.test-output
165
- *
166
- * @example
167
- * ```typescript
168
- * const baseDir = getTestOutputBase('rag-lancedb');
169
- * // Result: packages/rag-lancedb/.test-output
170
- * ```
171
- */
172
- export function getTestOutputBase(packageName) {
173
- const projectRoot = safePath.resolve(process.cwd());
174
- return safePath.join(projectRoot, 'packages', packageName, '.test-output');
175
- }
176
- /**
177
- * Per-suite temp directory pattern (async version)
178
- * Creates a single temp directory for the entire test suite,
179
- * with subdirectories for each test. This is 3-5x faster on Windows
180
- * than creating a new mkdtemp for each test.
181
- *
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}.
186
- * @returns Suite helper with beforeAll, afterAll, beforeEach, afterEach, and getTempDir
187
- *
188
- * @example
189
- * ```typescript
190
- * const suite = setupAsyncTempDirSuite('my-test');
191
- *
192
- * describe('my tests', () => {
193
- * beforeAll(suite.beforeAll);
194
- * afterAll(suite.afterAll);
195
- * beforeEach(suite.beforeEach);
196
- *
197
- * it('test 1', async () => {
198
- * const tempDir = suite.getTempDir();
199
- * // Use tempDir...
200
- * });
201
- * });
202
- * ```
203
- */
204
- export function setupAsyncTempDirSuite(prefix, teardown = {}) {
205
- let suiteDir = '';
206
- let tempDir = '';
207
- let testCounter = 0;
208
- return {
209
- beforeAll: async () => {
210
- suiteDir = await fs.mkdtemp(safePath.join(normalizedTmpdir(), `${prefix}-suite-`));
211
- },
212
- afterAll: async () => {
213
- await removeScratchDir(suiteDir, teardown);
214
- },
215
- beforeEach: async () => {
216
- testCounter++;
217
- tempDir = safePath.join(suiteDir, `test-${testCounter}`);
218
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- tempDir is from mkdtemp
219
- await fs.mkdir(tempDir, { recursive: true });
220
- },
221
- afterEach: async () => {
222
- // Per-test cleanup handled by suite cleanup
223
- },
224
- getTempDir: () => tempDir,
225
- };
226
- }
4
+ import { syncBuiltinESMExports } from 'node:module';
5
+ import { isFilesystemAccessError } from './errors/errno.js';
6
+ import { normalizedTmpdir, safePath, toForwardSlash } from './path-utils.js';
7
+ let cachedCapability;
227
8
  /**
228
- * Per-suite temp directory pattern (sync version)
229
- * Creates a single temp directory for the entire test suite,
230
- * with subdirectories for each test. This is 3-5x faster on Windows
231
- * than creating a new mkdtemp for each test.
232
- *
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}.
237
- * @returns Suite helper with beforeAll, afterAll, beforeEach, afterEach, and getTempDir
238
- *
239
- * @example
240
- * ```typescript
241
- * const suite = setupSyncTempDirSuite('my-test');
242
- *
243
- * describe('my tests', () => {
244
- * beforeAll(suite.beforeAll);
245
- * afterAll(suite.afterAll);
246
- * beforeEach(suite.beforeEach);
247
- *
248
- * it('test 1', () => {
249
- * const tempDir = suite.getTempDir();
250
- * // Use tempDir...
251
- * });
252
- * });
253
- * ```
9
+ * The errnos that mean "this host cannot create symlinks": Windows without
10
+ * Developer Mode or `SeCreateSymbolicLinkPrivilege` (`EPERM`), and a
11
+ * filesystem that has no symlinks to offer (`ENOTSUP` / `EOPNOTSUPP`).
254
12
  */
255
- export function setupSyncTempDirSuite(prefix, teardown = {}) {
256
- let suiteDir = '';
257
- let tempDir = '';
258
- let testCounter = 0;
259
- return {
260
- beforeAll: () => {
261
- suiteDir = mkdtempSync(safePath.join(normalizedTmpdir(), `${prefix}-suite-`));
262
- },
263
- afterAll: async () => {
264
- await removeScratchDir(suiteDir, teardown);
265
- },
266
- beforeEach: () => {
267
- testCounter++;
268
- tempDir = safePath.join(suiteDir, `test-${testCounter}`);
269
- mkdirSyncReal(tempDir);
270
- },
271
- afterEach: () => {
272
- // Per-test cleanup handled by suite cleanup
273
- },
274
- getTempDir: () => tempDir,
275
- };
13
+ const SYMLINK_UNSUPPORTED_ERRNOS = new Set(['EPERM', 'ENOTSUP', 'EOPNOTSUPP']);
14
+ function isSymlinkUnsupported(error) {
15
+ return error instanceof Error && 'code' in error && typeof error.code === 'string' && SYMLINK_UNSUPPORTED_ERRNOS.has(error.code);
276
16
  }
277
- let cachedCapability;
278
17
  /**
279
18
  * Whether this PROCESS can create symlinks — probed once and memoized.
280
19
  *
@@ -282,11 +21,17 @@ let cachedCapability;
282
21
  * `SeCreateSymbolicLinkPrivilege`. That privilege lives on the process's
283
22
  * security token, not on any one directory: it cannot change between calls
284
23
  * 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.)
24
+ * memoization, not a shortcut that risks a stale answer. (A filesystem that
25
+ * itself has no symlinks — some network shares, some FAT variants — answers
26
+ * `ENOTSUP` and is read as the same "no"; every fixture in this repo creates
27
+ * its roots under {@link normalizedTmpdir}, so it never arises here.)
28
+ *
29
+ * Because the answer is memoized for the whole process, what reads as "no"
30
+ * matters more than usual: a `null` here silently `skip()`s every symlink test
31
+ * for the rest of the run. So ONLY {@link SYMLINK_UNSUPPORTED_ERRNOS} is a
32
+ * no. A tmpdir that is unwritable or missing, or a bug, is not an answer about
33
+ * symlinks at all and stays loud rather than becoming a process-wide skip for
34
+ * a reason nothing reported.
290
35
  *
291
36
  * Fixtures that depend on symlinks must ask rather than assume — and, having
292
37
  * asked, must SAY they skipped. A symlink case that silently no-ops reads as
@@ -300,23 +45,26 @@ export function symlinkCapability() {
300
45
  if (cachedCapability === undefined) {
301
46
  const probe = safePath.join(normalizedTmpdir(), `.vat-symlink-probe-${randomBytes(4).toString('hex')}`);
302
47
  try {
303
- // eslint-disable-next-line security/detect-non-literal-fs-filename -- fixed tmp dir plus a random basename generated here
304
48
  symlinkSync('.', probe);
305
49
  cachedCapability = {};
306
50
  }
307
- catch {
51
+ catch (error) {
52
+ if (!isSymlinkUnsupported(error))
53
+ throw error;
308
54
  cachedCapability = null;
309
55
  }
310
56
  if (cachedCapability !== null) {
311
57
  // Best-effort: the capability answer comes from creation succeeding, not
312
58
  // from cleanup — a probe left behind by a failed rmSync (e.g. a transient
313
59
  // lock on the freshly-created reparse point) must not flip a real "yes"
314
- // into a memoized, process-wide "no".
60
+ // into a memoized, process-wide "no". Only the filesystem refusing the
61
+ // delete is that case; a bug is not, and stays loud.
315
62
  try {
316
63
  rmSync(probe, { force: true });
317
64
  }
318
- catch {
319
- // Leftover probe file; harmless, and not this function's concern.
65
+ catch (error) {
66
+ if (!isFilesystemAccessError(error))
67
+ throw error;
320
68
  }
321
69
  }
322
70
  }
@@ -335,7 +83,6 @@ export function symlinkCapability() {
335
83
  * @param type - Windows-only link-type hint (`'file'` \| `'dir'` \| `'junction'`); ignored on POSIX
336
84
  */
337
85
  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
86
  symlinkSync(target, path, type);
340
87
  }
341
88
  /**
@@ -348,7 +95,6 @@ export function createSymlink(_cap, target, path, type) {
348
95
  * @param type - Windows-only link-type hint (`'file'` \| `'dir'` \| `'junction'`); ignored on POSIX
349
96
  */
350
97
  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
98
  await fs.symlink(target, path, type);
353
99
  }
354
100
  /**
@@ -427,39 +173,88 @@ export function detachGitEnv() {
427
173
  };
428
174
  }
429
175
  /**
430
- * Run `body` with `fs.readdirSync` of exactly `directory` throwing an error
431
- * carrying errno `code`. Every other directory lists for real, so what a walk
432
- * under test meets is ONE refused listing inside an otherwise ordinary tree —
433
- * a walk that gave up entirely would pass a test where everything was refused.
434
- *
435
- * A patch rather than a `chmod`, for the reason the promise-API twin in
436
- * `resources/test/helpers/refused-listing.ts` gives: `chmod` reaches one errno
437
- * (`EACCES`), only where POSIX modes bind, and not as root. The mapping under
438
- * test is "anything that is not an absence errno", and `EMFILE` / `ENFILE` /
439
- * `ELOOP` are just as reachable in ordinary operation. This one lives in the
440
- * shipped helpers because the sync crawler has consumers in three packages that
441
- * each need to refuse a listing, and the duplication gate refuses three copies.
442
- *
443
- * @param directory - Absolute path of the one directory to refuse
444
- * @param code - The errno to reject with
445
- * @param body - Runs while the refusal is in force; may be async
446
- * @returns Whatever `body` returned
176
+ * The errno-shaped error a refused `fs` call throws: a message, the `code`,
177
+ * and the `syscall`, exactly as Node shapes one.
447
178
  */
448
- export async function withReaddirSyncRefused(directory, code, body) {
449
- const original = nodeFs.readdirSync;
450
- const refused = toForwardSlash(directory);
451
- const patched = ((target, ...rest) => {
452
- if (toForwardSlash(String(target)) === refused) {
453
- throw Object.assign(new Error(`${code}: refused, scandir '${String(target)}'`), { code });
454
- }
179
+ export function errnoError(code, syscall, target) {
180
+ return Object.assign(new Error(`${code}: refused, ${syscall} '${target}'`), { code, syscall });
181
+ }
182
+ /**
183
+ * Assign `fn` over `module[method]` and republish the builtin's ESM bindings.
184
+ *
185
+ * Assigning on the CJS object alone reaches ONLY a `import fs from 'node:fs'`
186
+ * caller — a named or namespace import reads the builtin's ESM bindings, which
187
+ * Node snapshots at import time. `syncBuiltinESMExports()` after each
188
+ * assignment republishes the patch (and the restore) to every import style;
189
+ * measured under vitest: without it, a spy on a named-import caller attached
190
+ * and counted zero, which reads exactly like "this function performs no I/O".
191
+ */
192
+ function republish(module, method, fn) {
193
+ module[method] = fn;
194
+ syncBuiltinESMExports();
195
+ }
196
+ /**
197
+ * Make `fs[method]` throw `code` for exactly `targetPath` until the returned
198
+ * restore is called; every other path, and every other method, stays real.
199
+ *
200
+ * A patch rather than a `chmod`: `chmod` reaches one errno (`EACCES`), only
201
+ * where POSIX modes bind, and not as root — and the property under test is
202
+ * "any refusal that is not an absence", so `EACCES`, `ELOOP`, `EMFILE` must all
203
+ * be reachable. What a walk under test meets is ONE refused call inside an
204
+ * otherwise ordinary tree; a walk that gave up entirely would pass a test where
205
+ * everything was refused.
206
+ *
207
+ * Lives in the shipped helpers because consumers in five packages each need to
208
+ * refuse a call, and the duplication gate refuses five copies.
209
+ */
210
+ export function refuseSyncFs(method, targetPath, code) {
211
+ const original = nodeFs[method];
212
+ const refused = toForwardSlash(targetPath);
213
+ republish(nodeFs, method, (target, ...rest) => {
214
+ if (toForwardSlash(String(target)) === refused)
215
+ throw errnoError(code, method, String(target));
216
+ return original(target, ...rest);
217
+ });
218
+ return () => republish(nodeFs, method, original);
219
+ }
220
+ /**
221
+ * `fs/promises[method]` rejects with `code` for exactly `targetPath` until the
222
+ * returned restore is called; every other path, and every other method, is real.
223
+ */
224
+ export function refuseAsyncFs(method, targetPath, code) {
225
+ const original = fs[method].bind(fs);
226
+ const refused = toForwardSlash(targetPath);
227
+ republish(fs, method, async (target, ...rest) => {
228
+ if (toForwardSlash(String(target)) === refused)
229
+ throw errnoError(code, method, String(target));
455
230
  return original(target, ...rest);
456
231
  });
457
- nodeFs.readdirSync = patched;
232
+ return () => republish(fs, method, original);
233
+ }
234
+ /**
235
+ * Run `body` while `fs[method]` throws `code` for exactly `targetPath`; the
236
+ * patch is lifted however `body` exits. See {@link refuseSyncFs}.
237
+ */
238
+ export async function withSyncFsRefused(method, targetPath, code, body) {
239
+ const restore = refuseSyncFs(method, targetPath, code);
458
240
  try {
459
241
  return await body();
460
242
  }
461
243
  finally {
462
- nodeFs.readdirSync = original;
244
+ restore();
463
245
  }
464
246
  }
247
+ /**
248
+ * Run `body` with `fs.readdirSync` of exactly `directory` throwing an error
249
+ * carrying errno `code`. The listing case of {@link withSyncFsRefused}, named
250
+ * because refusing a LISTING is the question the crawler's consumers ask.
251
+ *
252
+ * @param directory - Absolute path of the one directory to refuse
253
+ * @param code - The errno to reject with
254
+ * @param body - Runs while the refusal is in force; may be async
255
+ * @returns Whatever `body` returned
256
+ */
257
+ export async function withReaddirSyncRefused(directory, code, body) {
258
+ return withSyncFsRefused('readdirSync', directory, code, body);
259
+ }
465
260
  //# sourceMappingURL=test-helpers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"test-helpers.js","sourceRoot":"","sources":["../src/test-helpers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"test-helpers.js","sourceRoot":"","sources":["../src/test-helpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Absolute paths for the executables test fixtures spawn.
3
+ *
4
+ * A test that spawns `'git'` or `'node'` by bare name asks the OS to search
5
+ * `PATH`, and a writable directory on `PATH` turns that into a place to plant
6
+ * a binary (the class SonarCloud's S4036 flags). Fixtures resolve the binary
7
+ * ONCE, here, to an absolute path and spawn that: `node` is the process that
8
+ * is running the test (`process.execPath`, the only node whose version the
9
+ * test can vouch for), and `git` is found by walking `PATH`.
10
+ *
11
+ * Why a second `PATH` walk beside `safe-exec.ts`'s `which`: `which` resolves
12
+ * in-process too, but it is a third-party package, and the `./testing` subpath
13
+ * is pinned dependency-free (`subpath-purity.test.ts`) — an adopter's test
14
+ * suite pulls nothing in. This walk answers the same question under that
15
+ * constraint: the first REGULAR FILE on `PATH` named `name` (with a `PATHEXT`
16
+ * extension on Windows) that the process may execute.
17
+ */
18
+ /** The `node` running this test, absolute. */
19
+ export declare const NODE_EXECUTABLE: string;
20
+ /** The names to try in one `PATH` directory: `PATHEXT` variants on Windows, the bare name elsewhere. */
21
+ export declare function executableCandidates(name: string, platform: NodeJS.Platform, pathext: string | undefined): string[];
22
+ /**
23
+ * The first executable named `name` on `PATH`, absolute.
24
+ *
25
+ * @throws {Error} when nothing on `PATH` is executable under that name — a
26
+ * fixture that needs `git` and has none should fail at the first spawn with
27
+ * the reason, not with the OS's `ENOENT` for a bare word.
28
+ */
29
+ export declare function resolveExecutable(name: string): string;
30
+ /** `git`, absolute, resolved on first use and cached for the process. */
31
+ export declare function gitExecutable(): string;
32
+ //# sourceMappingURL=executables.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executables.d.ts","sourceRoot":"","sources":["../../src/testing/executables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,8CAA8C;AAC9C,eAAO,MAAM,eAAe,EAAE,MAAyB,CAAC;AAExD,wGAAwG;AACxG,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAInH;AAcD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAUtD;AAID,yEAAyE;AACzE,wBAAgB,aAAa,IAAI,MAAM,CAGtC"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Absolute paths for the executables test fixtures spawn.
3
+ *
4
+ * A test that spawns `'git'` or `'node'` by bare name asks the OS to search
5
+ * `PATH`, and a writable directory on `PATH` turns that into a place to plant
6
+ * a binary (the class SonarCloud's S4036 flags). Fixtures resolve the binary
7
+ * ONCE, here, to an absolute path and spawn that: `node` is the process that
8
+ * is running the test (`process.execPath`, the only node whose version the
9
+ * test can vouch for), and `git` is found by walking `PATH`.
10
+ *
11
+ * Why a second `PATH` walk beside `safe-exec.ts`'s `which`: `which` resolves
12
+ * in-process too, but it is a third-party package, and the `./testing` subpath
13
+ * is pinned dependency-free (`subpath-purity.test.ts`) — an adopter's test
14
+ * suite pulls nothing in. This walk answers the same question under that
15
+ * constraint: the first REGULAR FILE on `PATH` named `name` (with a `PATHEXT`
16
+ * extension on Windows) that the process may execute.
17
+ */
18
+ import { accessSync, constants, statSync } from 'node:fs';
19
+ import { delimiter } from 'node:path';
20
+ import { isFilesystemAccessError } from '../errors/errno.js';
21
+ import { safePath } from '../path-core.js';
22
+ /** The `node` running this test, absolute. */
23
+ export const NODE_EXECUTABLE = process.execPath;
24
+ /** The names to try in one `PATH` directory: `PATHEXT` variants on Windows, the bare name elsewhere. */
25
+ export function executableCandidates(name, platform, pathext) {
26
+ if (platform !== 'win32')
27
+ return [name];
28
+ const extensions = (pathext ?? '.EXE;.CMD;.BAT;.COM').split(';').filter((ext) => ext !== '');
29
+ return [...extensions.map((ext) => `${name}${ext}`), name];
30
+ }
31
+ /** Is `full` a regular file the process may execute? A directory named `git` passes `X_OK` (search permission) and is not one. */
32
+ function isExecutableFile(full) {
33
+ try {
34
+ accessSync(full, constants.X_OK);
35
+ return statSync(full).isFile();
36
+ }
37
+ catch (error) {
38
+ // Absent or not executable here: the next directory on PATH may have it.
39
+ if (isFilesystemAccessError(error))
40
+ return false;
41
+ throw error;
42
+ }
43
+ }
44
+ /**
45
+ * The first executable named `name` on `PATH`, absolute.
46
+ *
47
+ * @throws {Error} when nothing on `PATH` is executable under that name — a
48
+ * fixture that needs `git` and has none should fail at the first spawn with
49
+ * the reason, not with the OS's `ENOENT` for a bare word.
50
+ */
51
+ export function resolveExecutable(name) {
52
+ const candidates = executableCandidates(name, process.platform, process.env['PATHEXT']);
53
+ for (const dir of (process.env['PATH'] ?? '').split(delimiter)) {
54
+ if (dir === '')
55
+ continue;
56
+ for (const candidate of candidates) {
57
+ const full = safePath.join(dir, candidate);
58
+ if (isExecutableFile(full))
59
+ return full;
60
+ }
61
+ }
62
+ throw new Error(`No executable named "${name}" on PATH (${process.env['PATH'] ?? '<unset>'})`);
63
+ }
64
+ let resolvedGit;
65
+ /** `git`, absolute, resolved on first use and cached for the process. */
66
+ export function gitExecutable() {
67
+ resolvedGit ??= resolveExecutable('git');
68
+ return resolvedGit;
69
+ }
70
+ //# sourceMappingURL=executables.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executables.js","sourceRoot":"","sources":["../../src/testing/executables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE3C,8CAA8C;AAC9C,MAAM,CAAC,MAAM,eAAe,GAAW,OAAO,CAAC,QAAQ,CAAC;AAExD,wGAAwG;AACxG,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAE,QAAyB,EAAE,OAA2B;IACvG,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,UAAU,GAAG,CAAC,OAAO,IAAI,qBAAqB,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,CAAC;IAC7F,OAAO,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,GAAG,GAAG,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;AAC7D,CAAC;AAED,kIAAkI;AAClI,SAAS,gBAAgB,CAAC,IAAY;IACpC,IAAI,CAAC;QACH,UAAU,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QACjC,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;IACjC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,yEAAyE;QACzE,IAAI,uBAAuB,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QACjD,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,MAAM,UAAU,GAAG,oBAAoB,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC;IACxF,KAAK,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;QAC/D,IAAI,GAAG,KAAK,EAAE;YAAE,SAAS;QACzB,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;YACnC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YAC3C,IAAI,gBAAgB,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC;QAC1C,CAAC;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,wBAAwB,IAAI,cAAc,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,SAAS,GAAG,CAAC,CAAC;AACjG,CAAC;AAED,IAAI,WAA+B,CAAC;AAEpC,yEAAyE;AACzE,MAAM,UAAU,aAAa;IAC3B,WAAW,KAAK,iBAAiB,CAAC,KAAK,CAAC,CAAC;IACzC,OAAO,WAAW,CAAC;AACrB,CAAC"}