@vibe-agent-toolkit/utils 0.2.0-rc.1 → 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 +69 -23
- 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 +133 -29
- 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 +422 -191
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +534 -204
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +5 -2
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +9 -7
- 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 +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 +84 -1
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +208 -82
- 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 +32 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +95 -50
- 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/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 +113 -18
- package/dist/path-core.d.ts.map +1 -1
- package/dist/path-core.js +139 -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.map +1 -1
- package/dist/project-utils.js +15 -9
- 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 +135 -100
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +224 -146
- 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 +223 -49
- 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 +1 -1
- package/eslint/rules/dirent-type-needs-symlink-check.cjs +282 -0
- package/eslint/rules/eslint-rule-factory.cjs +5 -2
- 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 +136 -107
- 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 +9 -0
- 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 -496
package/dist/test-helpers.js
CHANGED
|
@@ -1,182 +1,260 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import nodeFs, { rmSync, symlinkSync } from 'node:fs';
|
|
3
3
|
import fs from 'node:fs/promises';
|
|
4
|
-
import {
|
|
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;
|
|
5
8
|
/**
|
|
6
|
-
*
|
|
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`).
|
|
12
|
+
*/
|
|
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);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether this PROCESS can create symlinks — probed once and memoized.
|
|
7
19
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
20
|
+
* On Windows, `symlink()` needs either Developer Mode or
|
|
21
|
+
* `SeCreateSymbolicLinkPrivilege`. That privilege lives on the process's
|
|
22
|
+
* security token, not on any one directory: it cannot change between calls
|
|
23
|
+
* within a single run, so probing it once and reusing the result is a
|
|
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.)
|
|
10
28
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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.
|
|
15
35
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* const dbPath = getTestOutputDir('rag-lancedb', 'system', 'databases', 'test-db');
|
|
20
|
-
* // Result: packages/rag-lancedb/.test-output/system/20260105-143022-abc123/databases/test-db
|
|
36
|
+
* Fixtures that depend on symlinks must ask rather than assume — and, having
|
|
37
|
+
* asked, must SAY they skipped. A symlink case that silently no-ops reads as
|
|
38
|
+
* a passing test for a property nobody exercised.
|
|
21
39
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* ```
|
|
40
|
+
* @returns A {@link SymlinkCapability} token when this process can create
|
|
41
|
+
* symlinks, else `null`. Route the `null` case through vitest's `skip()`
|
|
42
|
+
* rather than a plain `return`, so the skip is visible in the report.
|
|
26
43
|
*/
|
|
27
|
-
export function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
export function symlinkCapability() {
|
|
45
|
+
if (cachedCapability === undefined) {
|
|
46
|
+
const probe = safePath.join(normalizedTmpdir(), `.vat-symlink-probe-${randomBytes(4).toString('hex')}`);
|
|
47
|
+
try {
|
|
48
|
+
symlinkSync('.', probe);
|
|
49
|
+
cachedCapability = {};
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (!isSymlinkUnsupported(error))
|
|
53
|
+
throw error;
|
|
54
|
+
cachedCapability = null;
|
|
55
|
+
}
|
|
56
|
+
if (cachedCapability !== null) {
|
|
57
|
+
// Best-effort: the capability answer comes from creation succeeding, not
|
|
58
|
+
// from cleanup — a probe left behind by a failed rmSync (e.g. a transient
|
|
59
|
+
// lock on the freshly-created reparse point) must not flip a real "yes"
|
|
60
|
+
// into a memoized, process-wide "no". Only the filesystem refusing the
|
|
61
|
+
// delete is that case; a bug is not, and stays loud.
|
|
62
|
+
try {
|
|
63
|
+
rmSync(probe, { force: true });
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (!isFilesystemAccessError(error))
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return cachedCapability;
|
|
38
72
|
}
|
|
39
73
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
74
|
+
* Create a symlink — the one sanctioned call site for `fs.symlinkSync` in
|
|
75
|
+
* test code. Requires a {@link SymlinkCapability}, which only
|
|
76
|
+
* {@link symlinkCapability} can mint, so a test cannot reach the real
|
|
77
|
+
* syscall without first proving (or explicitly bypassing via `skip()`) that
|
|
78
|
+
* this host supports it.
|
|
42
79
|
*
|
|
43
|
-
* @param
|
|
44
|
-
* @
|
|
45
|
-
*
|
|
46
|
-
* @
|
|
47
|
-
* ```typescript
|
|
48
|
-
* const baseDir = getTestOutputBase('rag-lancedb');
|
|
49
|
-
* // Result: packages/rag-lancedb/.test-output
|
|
50
|
-
* ```
|
|
80
|
+
* @param _cap - Proof from {@link symlinkCapability} that this host can create symlinks
|
|
81
|
+
* @param target - The existing path the new link should point at
|
|
82
|
+
* @param path - Where to create the link
|
|
83
|
+
* @param type - Windows-only link-type hint (`'file'` \| `'dir'` \| `'junction'`); ignored on POSIX
|
|
51
84
|
*/
|
|
52
|
-
export function
|
|
53
|
-
|
|
54
|
-
return safePath.join(projectRoot, 'packages', packageName, '.test-output');
|
|
85
|
+
export function createSymlink(_cap, target, path, type) {
|
|
86
|
+
symlinkSync(target, path, type);
|
|
55
87
|
}
|
|
56
88
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* with subdirectories for each test. This is 3-5x faster on Windows
|
|
60
|
-
* than creating a new mkdtemp for each test.
|
|
61
|
-
*
|
|
62
|
-
* @param prefix - Prefix for the suite temp directory name
|
|
63
|
-
* @returns Suite helper with beforeAll, afterAll, beforeEach, afterEach, and getTempDir
|
|
64
|
-
*
|
|
65
|
-
* @example
|
|
66
|
-
* ```typescript
|
|
67
|
-
* const suite = setupAsyncTempDirSuite('my-test');
|
|
68
|
-
*
|
|
69
|
-
* describe('my tests', () => {
|
|
70
|
-
* beforeAll(suite.beforeAll);
|
|
71
|
-
* afterAll(suite.afterAll);
|
|
72
|
-
* beforeEach(suite.beforeEach);
|
|
89
|
+
* The async counterpart of {@link createSymlink}, for fixtures already using
|
|
90
|
+
* `node:fs/promises`. Same capability requirement, same reasoning.
|
|
73
91
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
* });
|
|
79
|
-
* ```
|
|
92
|
+
* @param _cap - Proof from {@link symlinkCapability} that this host can create symlinks
|
|
93
|
+
* @param target - The existing path the new link should point at
|
|
94
|
+
* @param path - Where to create the link
|
|
95
|
+
* @param type - Windows-only link-type hint (`'file'` \| `'dir'` \| `'junction'`); ignored on POSIX
|
|
80
96
|
*/
|
|
81
|
-
export function
|
|
82
|
-
|
|
83
|
-
let tempDir = '';
|
|
84
|
-
let testCounter = 0;
|
|
85
|
-
return {
|
|
86
|
-
beforeAll: async () => {
|
|
87
|
-
suiteDir = await fs.mkdtemp(safePath.join(normalizedTmpdir(), `${prefix}-suite-`));
|
|
88
|
-
},
|
|
89
|
-
afterAll: async () => {
|
|
90
|
-
if (suiteDir) {
|
|
91
|
-
await fs.rm(suiteDir, { recursive: true, force: true });
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
beforeEach: async () => {
|
|
95
|
-
testCounter++;
|
|
96
|
-
tempDir = safePath.join(suiteDir, `test-${testCounter}`);
|
|
97
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename -- tempDir is from mkdtemp
|
|
98
|
-
await fs.mkdir(tempDir, { recursive: true });
|
|
99
|
-
},
|
|
100
|
-
afterEach: async () => {
|
|
101
|
-
// Per-test cleanup handled by suite cleanup
|
|
102
|
-
},
|
|
103
|
-
getTempDir: () => tempDir,
|
|
104
|
-
};
|
|
97
|
+
export async function createSymlinkAsync(_cap, target, path, type) {
|
|
98
|
+
await fs.symlink(target, path, type);
|
|
105
99
|
}
|
|
106
100
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* with subdirectories for each test. This is 3-5x faster on Windows
|
|
110
|
-
* than creating a new mkdtemp for each test.
|
|
101
|
+
* The variables git exports into a hook, which a fixture must clear before it
|
|
102
|
+
* can fabricate its own.
|
|
111
103
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
104
|
+
* These are the ones git sets *for* you. Deliberately **not** the operator's own
|
|
105
|
+
* `GIT_CONFIG_COUNT`/`KEY_n`/`VALUE_n`/`GLOBAL`/`SYSTEM` channel — a test may be
|
|
106
|
+
* using that on purpose to point a clone at a local path, and clearing it sends
|
|
107
|
+
* the clone to the network instead.
|
|
108
|
+
*/
|
|
109
|
+
export const INHERITED_GIT_ENV = [
|
|
110
|
+
'GIT_ALTERNATE_OBJECT_DIRECTORIES',
|
|
111
|
+
'GIT_CEILING_DIRECTORIES',
|
|
112
|
+
'GIT_COMMON_DIR',
|
|
113
|
+
'GIT_CONFIG_PARAMETERS',
|
|
114
|
+
'GIT_DIR',
|
|
115
|
+
'GIT_DISCOVERY_ACROSS_FILESYSTEM',
|
|
116
|
+
'GIT_GRAFT_FILE',
|
|
117
|
+
'GIT_INDEX_FILE',
|
|
118
|
+
'GIT_INDEX_VERSION',
|
|
119
|
+
'GIT_NAMESPACE',
|
|
120
|
+
'GIT_NOTES_REF',
|
|
121
|
+
'GIT_OBJECT_DIRECTORY',
|
|
122
|
+
'GIT_PREFIX',
|
|
123
|
+
'GIT_SHALLOW_FILE',
|
|
124
|
+
'GIT_WORK_TREE',
|
|
125
|
+
];
|
|
126
|
+
/**
|
|
127
|
+
* Remove every inherited git redirection from `process.env`, and hand back the
|
|
128
|
+
* undo.
|
|
114
129
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
130
|
+
* A test that fabricates a hook environment has to start from a known-clean one,
|
|
131
|
+
* or it inherits whatever the *outer* runner exported and can no longer tell its
|
|
132
|
+
* own fixture apart from the ambient state — it then passes or fails for reasons
|
|
133
|
+
* it never set up. Restoring afterwards matters just as much: these are
|
|
134
|
+
* process-global, so a test that leaks `GIT_DIR` silently redirects every later
|
|
135
|
+
* test sharing the worker.
|
|
136
|
+
*
|
|
137
|
+
* ⚠️ **The key list is restated here on purpose, not by oversight.** Deriving it
|
|
138
|
+
* from `@vibe-validate/git`'s `stripGitEnv()` would be tidier, and it is exactly
|
|
139
|
+
* what this function did for one revision — but this module is the `./testing`
|
|
140
|
+
* subpath, which `subpath-purity.test.ts` pins as reaching **no third-party
|
|
141
|
+
* package at all** so it stays importable with zero dependencies installed. One
|
|
142
|
+
* import cost that property. The drift risk the derivation was avoiding is
|
|
143
|
+
* handled instead by {@link "../test/test-helpers-git-env.test".default}, which
|
|
144
|
+
* asserts this list equals what the shipped scrub removes.
|
|
118
145
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* afterAll(suite.afterAll);
|
|
122
|
-
* beforeEach(suite.beforeEach);
|
|
146
|
+
* @returns A function restoring every variable to its prior value, putting back
|
|
147
|
+
* "was not set" as unset rather than as an empty string
|
|
123
148
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* });
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* let restoreGitEnv: () => void;
|
|
152
|
+
* beforeEach(() => { restoreGitEnv = detachGitEnv(); });
|
|
153
|
+
* afterEach(() => { restoreGitEnv(); });
|
|
129
154
|
* ```
|
|
130
155
|
*/
|
|
131
|
-
export function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
},
|
|
149
|
-
afterEach: () => {
|
|
150
|
-
// Per-test cleanup handled by suite cleanup
|
|
151
|
-
},
|
|
152
|
-
getTempDir: () => tempDir,
|
|
156
|
+
export function detachGitEnv() {
|
|
157
|
+
const saved = new Map();
|
|
158
|
+
const forget = (name) => {
|
|
159
|
+
saved.set(name, process.env[name]);
|
|
160
|
+
delete process.env[name];
|
|
161
|
+
};
|
|
162
|
+
for (const name of INHERITED_GIT_ENV) {
|
|
163
|
+
forget(name);
|
|
164
|
+
}
|
|
165
|
+
return () => {
|
|
166
|
+
for (const [name, value] of saved) {
|
|
167
|
+
// Deleted first so an absent variable is restored as absent: assigning
|
|
168
|
+
// `undefined` would leave the literal string 'undefined' behind.
|
|
169
|
+
delete process.env[name];
|
|
170
|
+
if (value !== undefined)
|
|
171
|
+
process.env[name] = value;
|
|
172
|
+
}
|
|
153
173
|
};
|
|
154
174
|
}
|
|
155
175
|
/**
|
|
156
|
-
*
|
|
176
|
+
* The errno-shaped error a refused `fs` call throws: a message, the `code`,
|
|
177
|
+
* and the `syscall`, exactly as Node shapes one.
|
|
178
|
+
*/
|
|
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.
|
|
157
184
|
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
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.
|
|
163
199
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
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.
|
|
166
206
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
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));
|
|
230
|
+
return original(target, ...rest);
|
|
231
|
+
});
|
|
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}.
|
|
169
237
|
*/
|
|
170
|
-
export function
|
|
171
|
-
const
|
|
238
|
+
export async function withSyncFsRefused(method, targetPath, code, body) {
|
|
239
|
+
const restore = refuseSyncFs(method, targetPath, code);
|
|
172
240
|
try {
|
|
173
|
-
|
|
174
|
-
symlinkSync('.', probe);
|
|
241
|
+
return await body();
|
|
175
242
|
}
|
|
176
|
-
|
|
177
|
-
|
|
243
|
+
finally {
|
|
244
|
+
restore();
|
|
178
245
|
}
|
|
179
|
-
|
|
180
|
-
|
|
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);
|
|
181
259
|
}
|
|
182
260
|
//# 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,
|
|
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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One hostile fixture tree, built the same way for every sink.
|
|
3
|
+
*
|
|
4
|
+
* A correctness sweep found a delete, a copy and an uninstall that
|
|
5
|
+
* each walked out of their root, and found them by hand-building the same
|
|
6
|
+
* shapes — a symlink pointing out, a symlink pointing back at the root, a
|
|
7
|
+
* `..`-named entry, an unreadable directory, a traversal name — one throwaway
|
|
8
|
+
* fixture per probe. This module
|
|
9
|
+
* is that fixture, once, so a sink's test says `buildHostileTree(base)` and
|
|
10
|
+
* `it.each(HOSTILE_NAMES)` and is refusing the shapes the next sink's test is
|
|
11
|
+
* refusing too. A shape added here reaches every sink that uses it.
|
|
12
|
+
*
|
|
13
|
+
* ⛔ Framework-free, like everything under `testing/`: no `vitest` import, so
|
|
14
|
+
* the `./testing` subpath keeps the empty third-party set its purity pin
|
|
15
|
+
* asserts. Fields that the host cannot build are `null` — route those through
|
|
16
|
+
* the suite's own `skip()` so the skip is visible in the report.
|
|
17
|
+
*/
|
|
18
|
+
/** The names a sink must refuse when they arrive as "the entry to act on". */
|
|
19
|
+
export declare const HOSTILE_NAMES: readonly string[];
|
|
20
|
+
/** The tree {@link buildHostileTree} plants, every path absolute and forward-slashed. */
|
|
21
|
+
export interface HostileTree {
|
|
22
|
+
/** The trusted root — the directory a sink must stay inside. */
|
|
23
|
+
readonly root: string;
|
|
24
|
+
/** A regular directory under the root; `root/member`. */
|
|
25
|
+
readonly member: string;
|
|
26
|
+
/** A member whose NAME begins with two dots; `root/..cache`. Legitimate. */
|
|
27
|
+
readonly dotdotNamed: string;
|
|
28
|
+
/** A sibling of the root, outside it. */
|
|
29
|
+
readonly outside: string;
|
|
30
|
+
/** `outside/victim` — holds `secret.txt`; a traversal that succeeds deletes or copies this. */
|
|
31
|
+
readonly victim: string;
|
|
32
|
+
/** `root/link-out` → `outside/victim`, or `null` where the host cannot symlink. */
|
|
33
|
+
readonly linkOut: string | null;
|
|
34
|
+
/** `root/link-in` → `root/member`, or `null` where the host cannot symlink. */
|
|
35
|
+
readonly linkIn: string | null;
|
|
36
|
+
/** `root/dangling` → a path that does not exist, or `null` where the host cannot symlink. */
|
|
37
|
+
readonly dangling: string | null;
|
|
38
|
+
/** `root/loop` → `root` itself — a walk that follows it never ends — or `null` where the host cannot symlink. */
|
|
39
|
+
readonly linkLoop: string | null;
|
|
40
|
+
/** A link OUTSIDE the tree that points AT the root, or `null` where the host cannot symlink. */
|
|
41
|
+
readonly rootAlias: string | null;
|
|
42
|
+
/** `root/unreadable`, mode 000, or `null` where the host cannot deny reads. */
|
|
43
|
+
readonly unreadable: string | null;
|
|
44
|
+
/** A directory whose own name is 200 characters, under the root; `null` where the OS refused it. */
|
|
45
|
+
readonly longPath: string | null;
|
|
46
|
+
/** Restore modes and remove everything this call created. Idempotent. */
|
|
47
|
+
cleanup(): void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Plant the hostile tree under `base`, which must exist and be empty enough
|
|
51
|
+
* to take `root`, `outside` and `root-alias` as direct children.
|
|
52
|
+
*
|
|
53
|
+
* @param base - A scratch directory the caller owns (a per-test temp dir)
|
|
54
|
+
* @returns The tree, with `null` for every shape this host cannot build
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildHostileTree(base: string): HostileTree;
|
|
57
|
+
/** A hostile tree replanted per test, driven from the suite's own hooks. */
|
|
58
|
+
export interface HostileTreePerTest {
|
|
59
|
+
/** Mint a scratch root and plant a fresh tree in it. Drive from `beforeEach`. */
|
|
60
|
+
plant: () => void;
|
|
61
|
+
/** Tear the tree down and remove the scratch root. Drive from `afterEach`. */
|
|
62
|
+
clear: () => void;
|
|
63
|
+
/** The tree planted by the most recent {@link HostileTreePerTest.plant}. */
|
|
64
|
+
tree: () => HostileTree;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Hold a per-test hostile tree, so a suite's wiring is three lines:
|
|
68
|
+
* `const hostile = hostileTreePerTest('x'); beforeEach(hostile.plant); afterEach(hostile.clear);`.
|
|
69
|
+
*
|
|
70
|
+
* Same shape and same reasons as `replantableCorpus`: the tree comes back
|
|
71
|
+
* through a GETTER because it is reminted per test, `plant` clears any tree
|
|
72
|
+
* still standing so nested `describe`s do not leak one, and `tree()` before
|
|
73
|
+
* `plant()` throws by name rather than surfacing as an undefined read.
|
|
74
|
+
*
|
|
75
|
+
* @param prefix - `mkdtemp` prefix for the scratch root, so a leak names its suite
|
|
76
|
+
* @returns Plant/clear/tree, to be driven from the caller's own hooks
|
|
77
|
+
*/
|
|
78
|
+
export declare function hostileTreePerTest(prefix: string): HostileTreePerTest;
|
|
79
|
+
//# sourceMappingURL=hostile-tree.d.ts.map
|