@orkestrel/test 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Orkestrel
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # @orkestrel/test
2
+
3
+ The test helpers every `@orkestrel` package had already written for itself, published once. A call
4
+ recorder that is a real callback rather than a spy. A real host delay. A throw-to-value converter and
5
+ a presence narrower, so `!` and `as` stay banned in tests. Two async collectors and a JSON copier. A
6
+ scratch directory the test owns and destroys, and a symlink-refusing source-file walker. Add it as a
7
+ devDependency; nothing here runs in production code. Part of the `@orkestrel` line.
8
+
9
+ It has **zero runtime dependencies**, and no exported signature names an `@orkestrel/*` type. Both
10
+ rules exist for one reason: a test helper hands its types straight into the consumer's assertions,
11
+ and a second copy of a package inside its own repository makes the compiler read one type as two.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm install -D @orkestrel/test
17
+ ```
18
+
19
+ ## Requirements
20
+
21
+ - Node.js >= 22.12.0
22
+ - ESM and CommonJS
23
+
24
+ ## Usage
25
+
26
+ `@orkestrel/test` is the host-independent core. `@orkestrel/test/server` is the Node face. Core
27
+ touches neither `node:*` nor the DOM, so a browser test project imports it unchanged.
28
+
29
+ ```ts
30
+ import { captureError, createRecorder, requireValue, waitForDelay } from '@orkestrel/test'
31
+ import { createScratch } from '@orkestrel/test/server'
32
+
33
+ // A temporary directory the test owns, seeded with the input under test.
34
+ const scratch = createScratch({ prefix: 'loader-', files: { 'input.txt': 'hello' } })
35
+
36
+ // A real callback, not a spy: hand `handler` to the code under test.
37
+ const recorder = createRecorder<[path: string]>()
38
+ loader.on('read', recorder.handler)
39
+
40
+ loader.watch(scratch.path)
41
+ await waitForDelay(10) // let a real host timer elapse
42
+
43
+ recorder.count // how many reads arrived
44
+ recorder.calls // the arguments of each, oldest first
45
+ recorder.clear() // truncates in place, so a `calls` reference captured earlier empties too
46
+
47
+ // A throw becomes a value; absence becomes a throw. The test still does the asserting.
48
+ captureError(() => loader.read('missing.txt')) // the thrown value, or undefined
49
+ requireValue(scratch.read('input.txt')) // 'hello' — narrows `string | undefined` without `!`
50
+
51
+ scratch.destroy() // idempotent, and it removes only the directory it allocated
52
+ ```
53
+
54
+ The rest of core is `collect` (drains an async iterable), `collectStream` (drains a
55
+ `ReadableStream`), `roundTripJSON` (copies a `JSONValue`, and throws rather than turning a non-finite
56
+ number into `null`), and `resolveRoot` (the directory above the calling module, from `import.meta`).
57
+
58
+ The server face adds `readInventory`, which reads a checkout into a map of root-relative path to
59
+ file text that a parity suite can assert against, plus `resolveContained`, the lexical check it
60
+ refuses escapes with.
61
+
62
+ ```ts
63
+ import { resolveRoot } from '@orkestrel/test'
64
+ import { readInventory } from '@orkestrel/test/server'
65
+
66
+ // A suite in tests/ is one directory below the workspace root, which is what `resolveRoot` returns.
67
+ const root = resolveRoot(import.meta)
68
+
69
+ // Nothing is walked by default, so the directories are a required argument rather than an option.
70
+ const sources = readInventory(root, ['src/core', 'src/server'], { extensions: ['.ts'] })
71
+
72
+ Object.keys(sources)
73
+ // ['src/core/factories.ts', 'src/core/helpers.ts', 'src/core/index.ts', 'src/core/types.ts',
74
+ // 'src/server/factories.ts', 'src/server/helpers.ts', 'src/server/index.ts', 'src/server/types.ts']
75
+
76
+ // The keys are paths; the values are the file contents.
77
+ sources['src/core/index.ts']
78
+ // "export * from './types.js'\nexport * from './helpers.js'\nexport * from './factories.js'\n"
79
+
80
+ // An `exclude` entry is a whole key. A file key drops that file.
81
+ Object.keys(
82
+ readInventory(root, ['src/core'], { extensions: ['.ts'], exclude: ['src/core/index.ts'] }),
83
+ )
84
+ // ['src/core/factories.ts', 'src/core/helpers.ts', 'src/core/types.ts']
85
+
86
+ // A directory key prunes its whole subtree.
87
+ Object.keys(readInventory(root, ['src'], { extensions: ['.ts'], exclude: ['src/server'] }))
88
+ // ['src/core/factories.ts', 'src/core/helpers.ts', 'src/core/index.ts', 'src/core/types.ts']
89
+ ```
90
+
91
+ Keys are root-relative and `/`-separated whatever the host separator is, though this package's own
92
+ suite runs on POSIX, where that conversion is an identity, so it proves the key shape and not the
93
+ conversion. Keys are inserted in sorted order, and a plain object reads that order back for every
94
+ key that is not integer-like.
95
+
96
+ Two boundaries are worth stating up front, because the two filesystem helpers promise different
97
+ things. `createScratch` allocates its own directory at POSIX mode `0700` and refuses a path that
98
+ lexically escapes it. The suite asserts those bits on POSIX and proves nothing about a host that
99
+ emulates them. The mode keeps another uid out, and neither a sibling test worker nor the code under
100
+ test is another uid. It does not walk segments for symbolic links. A link inside its own allocation
101
+ was created by the test process or by the code the test drives — handing that code `scratch.path`
102
+ is the ordinary use of this helper — and `write` and `read` follow such a link, so either can reach
103
+ outside the allocation through it. `readInventory` walks a directory you supply, usually a checkout
104
+ the test did not create, so it throws on a symlinked root or requested directory and skips a
105
+ symlink met while walking. Neither is a sandbox against hostile filesystem content:
106
+ those refusals stop accidental escape, not an adversary who can create hard links where the test
107
+ process already writes.
108
+
109
+ ## Guide
110
+
111
+ For the full surface — every export, the behavioral contract each one holds to, and the measured
112
+ rule deciding what ships and what stays in the package that owns it — see
113
+ [`guides/test.md`](guides/test.md).
114
+
115
+ ## Package
116
+
117
+ Published as two typed entry points per the `exports` field in `package.json`: `@orkestrel/test` for
118
+ the host-independent core, `@orkestrel/test/server` for the Node helpers. Both ship ESM and
119
+ CommonJS.
120
+
121
+ ## License
122
+
123
+ MIT © [Orkestrel](https://github.com/orkestrel) — see [LICENSE](./LICENSE).
@@ -0,0 +1,137 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/core/helpers.ts
3
+ /**
4
+ * Waits for a host timer to elapse.
5
+ *
6
+ * @param ms - The delay in milliseconds.
7
+ * @returns A promise that resolves after the timer fires.
8
+ */
9
+ function waitForDelay(ms = 0) {
10
+ return new Promise((resolve) => setTimeout(resolve, ms));
11
+ }
12
+ /**
13
+ * Captures the value thrown by a thunk.
14
+ *
15
+ * @param thunk - The work whose thrown value to capture.
16
+ * @returns The thrown value, or `undefined` when the thunk completes.
17
+ */
18
+ function captureError(thunk) {
19
+ try {
20
+ thunk();
21
+ } catch (error) {
22
+ return error;
23
+ }
24
+ }
25
+ /**
26
+ * Requires a value to be present.
27
+ *
28
+ * @typeParam T - The required value type.
29
+ * @param value - The value to check.
30
+ * @param message - The error message used when the value is absent.
31
+ * @returns The present value.
32
+ */
33
+ function requireValue(value, message = "Value is required") {
34
+ if (value === null || value === void 0) throw new Error(message);
35
+ return value;
36
+ }
37
+ /**
38
+ * Collects every value from an async iterable.
39
+ *
40
+ * @typeParam T - The yielded value type.
41
+ * @param source - The async iterable to drain.
42
+ * @returns The yielded values in iteration order.
43
+ */
44
+ async function collect(source) {
45
+ const values = [];
46
+ for await (const value of source) values.push(value);
47
+ return values;
48
+ }
49
+ /**
50
+ * Collects every value from a readable stream.
51
+ *
52
+ * @typeParam T - The streamed value type.
53
+ * @param stream - The readable stream to drain.
54
+ * @returns The streamed values in read order.
55
+ */
56
+ async function collectStream(stream) {
57
+ const reader = stream.getReader();
58
+ const values = [];
59
+ try {
60
+ while (true) {
61
+ const result = await reader.read();
62
+ if (result.done) return values;
63
+ values.push(result.value);
64
+ }
65
+ } finally {
66
+ reader.releaseLock();
67
+ }
68
+ }
69
+ /**
70
+ * Copies a JSON value through serialization and parsing.
71
+ *
72
+ * @typeParam T - The JSON value type.
73
+ * @param value - The value to copy.
74
+ * @returns The parsed JSON copy.
75
+ * @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is
76
+ * normalized to zero by JSON serialization.
77
+ */
78
+ function roundTripJSON(value) {
79
+ const serialized = JSON.stringify(value, (_key, current) => {
80
+ if (typeof current === "number" && !Number.isFinite(current)) throw new Error("JSON values must contain finite numbers");
81
+ return current;
82
+ });
83
+ const parsed = JSON.parse(serialized);
84
+ const pending = [parsed];
85
+ while (pending.length > 0) {
86
+ const current = pending.pop();
87
+ if (current === void 0) continue;
88
+ if (typeof current === "number" && !Number.isFinite(current)) throw new Error("JSON values must contain finite numbers");
89
+ if (Array.isArray(current)) for (const child of current) pending.push(child);
90
+ else if (typeof current === "object" && current !== null) for (const child of Object.values(current)) pending.push(child);
91
+ }
92
+ return parsed;
93
+ }
94
+ /**
95
+ * Resolves the parent directory of a calling module, which is the workspace root when called from
96
+ * the conventional `tests/setup.ts` location.
97
+ *
98
+ * @param meta - The calling module metadata.
99
+ * @returns The root URL one directory above the calling file.
100
+ */
101
+ function resolveRoot(meta) {
102
+ return new URL("../", meta.url);
103
+ }
104
+ //#endregion
105
+ //#region src/core/factories.ts
106
+ /**
107
+ * Creates a recorder for callback arguments.
108
+ *
109
+ * @typeParam TArgs - The argument tuple to record.
110
+ * @returns A recorder whose handler appends calls in order.
111
+ */
112
+ function createRecorder() {
113
+ const calls = [];
114
+ return {
115
+ calls,
116
+ get count() {
117
+ return calls.length;
118
+ },
119
+ handler(...args) {
120
+ calls.push(args);
121
+ },
122
+ clear() {
123
+ calls.length = 0;
124
+ }
125
+ };
126
+ }
127
+ //#endregion
128
+ exports.captureError = captureError;
129
+ exports.collect = collect;
130
+ exports.collectStream = collectStream;
131
+ exports.createRecorder = createRecorder;
132
+ exports.requireValue = requireValue;
133
+ exports.resolveRoot = resolveRoot;
134
+ exports.roundTripJSON = roundTripJSON;
135
+ exports.waitForDelay = waitForDelay;
136
+
137
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../../src/core/helpers.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { JSONValue } from './types.js'\n\n/**\n * Waits for a host timer to elapse.\n *\n * @param ms - The delay in milliseconds.\n * @returns A promise that resolves after the timer fires.\n */\nexport function waitForDelay(ms = 0): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Captures the value thrown by a thunk.\n *\n * @param thunk - The work whose thrown value to capture.\n * @returns The thrown value, or `undefined` when the thunk completes.\n */\nexport function captureError(thunk: () => unknown): unknown {\n\ttry {\n\t\tthunk()\n\t} catch (error) {\n\t\treturn error\n\t}\n\treturn undefined\n}\n\n/**\n * Requires a value to be present.\n *\n * @typeParam T - The required value type.\n * @param value - The value to check.\n * @param message - The error message used when the value is absent.\n * @returns The present value.\n */\nexport function requireValue<T>(value: T | null | undefined, message = 'Value is required'): T {\n\tif (value === null || value === undefined) throw new Error(message)\n\treturn value\n}\n\n/**\n * Collects every value from an async iterable.\n *\n * @typeParam T - The yielded value type.\n * @param source - The async iterable to drain.\n * @returns The yielded values in iteration order.\n */\nexport async function collect<T>(source: AsyncIterable<T>): Promise<readonly T[]> {\n\tconst values: T[] = []\n\tfor await (const value of source) values.push(value)\n\treturn values\n}\n\n/**\n * Collects every value from a readable stream.\n *\n * @typeParam T - The streamed value type.\n * @param stream - The readable stream to drain.\n * @returns The streamed values in read order.\n */\nexport async function collectStream<T>(stream: ReadableStream<T>): Promise<readonly T[]> {\n\tconst reader = stream.getReader()\n\tconst values: T[] = []\n\ttry {\n\t\twhile (true) {\n\t\t\tconst result = await reader.read()\n\t\t\tif (result.done) return values\n\t\t\tvalues.push(result.value)\n\t\t}\n\t} finally {\n\t\treader.releaseLock()\n\t}\n}\n\n/**\n * Copies a JSON value through serialization and parsing.\n *\n * @typeParam T - The JSON value type.\n * @param value - The value to copy.\n * @returns The parsed JSON copy.\n * @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is\n * normalized to zero by JSON serialization.\n */\nexport function roundTripJSON<T extends JSONValue>(value: T): T {\n\tconst serialized = JSON.stringify(value, (_key, current) => {\n\t\tif (typeof current === 'number' && !Number.isFinite(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\treturn current\n\t})\n\tconst parsed: T = JSON.parse(serialized)\n\tconst pending: JSONValue[] = [parsed]\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop()\n\t\tif (current === undefined) continue\n\t\tif (typeof current === 'number' && !Number.isFinite(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\tif (Array.isArray(current)) {\n\t\t\tfor (const child of current) pending.push(child)\n\t\t} else if (typeof current === 'object' && current !== null) {\n\t\t\tfor (const child of Object.values(current)) pending.push(child)\n\t\t}\n\t}\n\treturn parsed\n}\n\n/**\n * Resolves the parent directory of a calling module, which is the workspace root when called from\n * the conventional `tests/setup.ts` location.\n *\n * @param meta - The calling module metadata.\n * @returns The root URL one directory above the calling file.\n */\nexport function resolveRoot(meta: ImportMeta): URL {\n\treturn new URL('../', meta.url)\n}\n","import type { RecorderInterface } from './types.js'\n\n/**\n * Creates a recorder for callback arguments.\n *\n * @typeParam TArgs - The argument tuple to record.\n * @returns A recorder whose handler appends calls in order.\n */\nexport function createRecorder<TArgs extends readonly unknown[]>(): RecorderInterface<TArgs> {\n\tconst calls: TArgs[] = []\n\treturn {\n\t\tcalls,\n\t\tget count() {\n\t\t\treturn calls.length\n\t\t},\n\t\thandler(...args) {\n\t\t\tcalls.push(args)\n\t\t},\n\t\tclear() {\n\t\t\tcalls.length = 0\n\t\t},\n\t}\n}\n"],"mappings":";;;;;;;;AAQA,SAAgB,aAAa,KAAK,GAAkB;CACnD,OAAO,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;AACxD;;;;;;;AAQA,SAAgB,aAAa,OAA+B;CAC3D,IAAI;EACH,MAAM;CACP,SAAS,OAAO;EACf,OAAO;CACR;AAED;;;;;;;;;AAUA,SAAgB,aAAgB,OAA6B,UAAU,qBAAwB;CAC9F,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,MAAM,IAAI,MAAM,OAAO;CAClE,OAAO;AACR;;;;;;;;AASA,eAAsB,QAAW,QAAiD;CACjF,MAAM,SAAc,CAAC;CACrB,WAAW,MAAM,SAAS,QAAQ,OAAO,KAAK,KAAK;CACnD,OAAO;AACR;;;;;;;;AASA,eAAsB,cAAiB,QAAkD;CACxF,MAAM,SAAS,OAAO,UAAU;CAChC,MAAM,SAAc,CAAC;CACrB,IAAI;EACH,OAAO,MAAM;GACZ,MAAM,SAAS,MAAM,OAAO,KAAK;GACjC,IAAI,OAAO,MAAM,OAAO;GACxB,OAAO,KAAK,OAAO,KAAK;EACzB;CACD,UAAU;EACT,OAAO,YAAY;CACpB;AACD;;;;;;;;;;AAWA,SAAgB,cAAmC,OAAa;CAC/D,MAAM,aAAa,KAAK,UAAU,QAAQ,MAAM,YAAY;EAC3D,IAAI,OAAO,YAAY,YAAY,CAAC,OAAO,SAAS,OAAO,GAC1D,MAAM,IAAI,MAAM,yCAAyC;EAE1D,OAAO;CACR,CAAC;CACD,MAAM,SAAY,KAAK,MAAM,UAAU;CACvC,MAAM,UAAuB,CAAC,MAAM;CACpC,OAAO,QAAQ,SAAS,GAAG;EAC1B,MAAM,UAAU,QAAQ,IAAI;EAC5B,IAAI,YAAY,KAAA,GAAW;EAC3B,IAAI,OAAO,YAAY,YAAY,CAAC,OAAO,SAAS,OAAO,GAC1D,MAAM,IAAI,MAAM,yCAAyC;EAE1D,IAAI,MAAM,QAAQ,OAAO,GACxB,KAAK,MAAM,SAAS,SAAS,QAAQ,KAAK,KAAK;OACzC,IAAI,OAAO,YAAY,YAAY,YAAY,MACrD,KAAK,MAAM,SAAS,OAAO,OAAO,OAAO,GAAG,QAAQ,KAAK,KAAK;CAEhE;CACA,OAAO;AACR;;;;;;;;AASA,SAAgB,YAAY,MAAuB;CAClD,OAAO,IAAI,IAAI,OAAO,KAAK,GAAG;AAC/B;;;;;;;;;AC5GA,SAAgB,iBAA6E;CAC5F,MAAM,QAAiB,CAAC;CACxB,OAAO;EACN;EACA,IAAI,QAAQ;GACX,OAAO,MAAM;EACd;EACA,QAAQ,GAAG,MAAM;GAChB,MAAM,KAAK,IAAI;EAChB;EACA,QAAQ;GACP,MAAM,SAAS;EAChB;CACD;AACD"}
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Captures the value thrown by a thunk.
3
+ *
4
+ * @param thunk - The work whose thrown value to capture.
5
+ * @returns The thrown value, or `undefined` when the thunk completes.
6
+ */
7
+ export declare function captureError(thunk: () => unknown): unknown;
8
+
9
+ /**
10
+ * Collects every value from an async iterable.
11
+ *
12
+ * @typeParam T - The yielded value type.
13
+ * @param source - The async iterable to drain.
14
+ * @returns The yielded values in iteration order.
15
+ */
16
+ export declare function collect<T>(source: AsyncIterable<T>): Promise<readonly T[]>;
17
+
18
+ /**
19
+ * Collects every value from a readable stream.
20
+ *
21
+ * @typeParam T - The streamed value type.
22
+ * @param stream - The readable stream to drain.
23
+ * @returns The streamed values in read order.
24
+ */
25
+ export declare function collectStream<T>(stream: ReadableStream<T>): Promise<readonly T[]>;
26
+
27
+ /**
28
+ * Creates a recorder for callback arguments.
29
+ *
30
+ * @typeParam TArgs - The argument tuple to record.
31
+ * @returns A recorder whose handler appends calls in order.
32
+ */
33
+ export declare function createRecorder<TArgs extends readonly unknown[]>(): RecorderInterface<TArgs>;
34
+
35
+ /** Any value JSON can represent, so a round trip through JSON preserves the type. */
36
+ export declare type JSONValue = string | number | boolean | null | readonly JSONValue[] | {
37
+ readonly [key: string]: JSONValue;
38
+ };
39
+
40
+ /**
41
+ * Records every call made to its handler.
42
+ *
43
+ * @typeParam TArgs - The argument tuple the recorded handler accepts.
44
+ */
45
+ export declare interface RecorderInterface<TArgs extends readonly unknown[]> {
46
+ /** Every recorded call, oldest first, each entry the arguments of one call. */
47
+ readonly calls: readonly TArgs[];
48
+ /** How many calls have been recorded. */
49
+ readonly count: number;
50
+ /** The callback to hand to the code under test. */
51
+ readonly handler: (...args: TArgs) => void;
52
+ /** Discards the recorded calls and keeps the recorder usable. */
53
+ clear(): void;
54
+ }
55
+
56
+ /**
57
+ * Requires a value to be present.
58
+ *
59
+ * @typeParam T - The required value type.
60
+ * @param value - The value to check.
61
+ * @param message - The error message used when the value is absent.
62
+ * @returns The present value.
63
+ */
64
+ export declare function requireValue<T>(value: T | null | undefined, message?: string): T;
65
+
66
+ /**
67
+ * Resolves the parent directory of a calling module, which is the workspace root when called from
68
+ * the conventional `tests/setup.ts` location.
69
+ *
70
+ * @param meta - The calling module metadata.
71
+ * @returns The root URL one directory above the calling file.
72
+ */
73
+ export declare function resolveRoot(meta: ImportMeta): URL;
74
+
75
+ /**
76
+ * Copies a JSON value through serialization and parsing.
77
+ *
78
+ * @typeParam T - The JSON value type.
79
+ * @param value - The value to copy.
80
+ * @returns The parsed JSON copy.
81
+ * @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is
82
+ * normalized to zero by JSON serialization.
83
+ */
84
+ export declare function roundTripJSON<T extends JSONValue>(value: T): T;
85
+
86
+ /**
87
+ * Waits for a host timer to elapse.
88
+ *
89
+ * @param ms - The delay in milliseconds.
90
+ * @returns A promise that resolves after the timer fires.
91
+ */
92
+ export declare function waitForDelay(ms?: number): Promise<void>;
93
+
94
+ export { }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Captures the value thrown by a thunk.
3
+ *
4
+ * @param thunk - The work whose thrown value to capture.
5
+ * @returns The thrown value, or `undefined` when the thunk completes.
6
+ */
7
+ export declare function captureError(thunk: () => unknown): unknown;
8
+
9
+ /**
10
+ * Collects every value from an async iterable.
11
+ *
12
+ * @typeParam T - The yielded value type.
13
+ * @param source - The async iterable to drain.
14
+ * @returns The yielded values in iteration order.
15
+ */
16
+ export declare function collect<T>(source: AsyncIterable<T>): Promise<readonly T[]>;
17
+
18
+ /**
19
+ * Collects every value from a readable stream.
20
+ *
21
+ * @typeParam T - The streamed value type.
22
+ * @param stream - The readable stream to drain.
23
+ * @returns The streamed values in read order.
24
+ */
25
+ export declare function collectStream<T>(stream: ReadableStream<T>): Promise<readonly T[]>;
26
+
27
+ /**
28
+ * Creates a recorder for callback arguments.
29
+ *
30
+ * @typeParam TArgs - The argument tuple to record.
31
+ * @returns A recorder whose handler appends calls in order.
32
+ */
33
+ export declare function createRecorder<TArgs extends readonly unknown[]>(): RecorderInterface<TArgs>;
34
+
35
+ /** Any value JSON can represent, so a round trip through JSON preserves the type. */
36
+ export declare type JSONValue = string | number | boolean | null | readonly JSONValue[] | {
37
+ readonly [key: string]: JSONValue;
38
+ };
39
+
40
+ /**
41
+ * Records every call made to its handler.
42
+ *
43
+ * @typeParam TArgs - The argument tuple the recorded handler accepts.
44
+ */
45
+ export declare interface RecorderInterface<TArgs extends readonly unknown[]> {
46
+ /** Every recorded call, oldest first, each entry the arguments of one call. */
47
+ readonly calls: readonly TArgs[];
48
+ /** How many calls have been recorded. */
49
+ readonly count: number;
50
+ /** The callback to hand to the code under test. */
51
+ readonly handler: (...args: TArgs) => void;
52
+ /** Discards the recorded calls and keeps the recorder usable. */
53
+ clear(): void;
54
+ }
55
+
56
+ /**
57
+ * Requires a value to be present.
58
+ *
59
+ * @typeParam T - The required value type.
60
+ * @param value - The value to check.
61
+ * @param message - The error message used when the value is absent.
62
+ * @returns The present value.
63
+ */
64
+ export declare function requireValue<T>(value: T | null | undefined, message?: string): T;
65
+
66
+ /**
67
+ * Resolves the parent directory of a calling module, which is the workspace root when called from
68
+ * the conventional `tests/setup.ts` location.
69
+ *
70
+ * @param meta - The calling module metadata.
71
+ * @returns The root URL one directory above the calling file.
72
+ */
73
+ export declare function resolveRoot(meta: ImportMeta): URL;
74
+
75
+ /**
76
+ * Copies a JSON value through serialization and parsing.
77
+ *
78
+ * @typeParam T - The JSON value type.
79
+ * @param value - The value to copy.
80
+ * @returns The parsed JSON copy.
81
+ * @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is
82
+ * normalized to zero by JSON serialization.
83
+ */
84
+ export declare function roundTripJSON<T extends JSONValue>(value: T): T;
85
+
86
+ /**
87
+ * Waits for a host timer to elapse.
88
+ *
89
+ * @param ms - The delay in milliseconds.
90
+ * @returns A promise that resolves after the timer fires.
91
+ */
92
+ export declare function waitForDelay(ms?: number): Promise<void>;
93
+
94
+ export { }
@@ -0,0 +1,129 @@
1
+ //#region src/core/helpers.ts
2
+ /**
3
+ * Waits for a host timer to elapse.
4
+ *
5
+ * @param ms - The delay in milliseconds.
6
+ * @returns A promise that resolves after the timer fires.
7
+ */
8
+ function waitForDelay(ms = 0) {
9
+ return new Promise((resolve) => setTimeout(resolve, ms));
10
+ }
11
+ /**
12
+ * Captures the value thrown by a thunk.
13
+ *
14
+ * @param thunk - The work whose thrown value to capture.
15
+ * @returns The thrown value, or `undefined` when the thunk completes.
16
+ */
17
+ function captureError(thunk) {
18
+ try {
19
+ thunk();
20
+ } catch (error) {
21
+ return error;
22
+ }
23
+ }
24
+ /**
25
+ * Requires a value to be present.
26
+ *
27
+ * @typeParam T - The required value type.
28
+ * @param value - The value to check.
29
+ * @param message - The error message used when the value is absent.
30
+ * @returns The present value.
31
+ */
32
+ function requireValue(value, message = "Value is required") {
33
+ if (value === null || value === void 0) throw new Error(message);
34
+ return value;
35
+ }
36
+ /**
37
+ * Collects every value from an async iterable.
38
+ *
39
+ * @typeParam T - The yielded value type.
40
+ * @param source - The async iterable to drain.
41
+ * @returns The yielded values in iteration order.
42
+ */
43
+ async function collect(source) {
44
+ const values = [];
45
+ for await (const value of source) values.push(value);
46
+ return values;
47
+ }
48
+ /**
49
+ * Collects every value from a readable stream.
50
+ *
51
+ * @typeParam T - The streamed value type.
52
+ * @param stream - The readable stream to drain.
53
+ * @returns The streamed values in read order.
54
+ */
55
+ async function collectStream(stream) {
56
+ const reader = stream.getReader();
57
+ const values = [];
58
+ try {
59
+ while (true) {
60
+ const result = await reader.read();
61
+ if (result.done) return values;
62
+ values.push(result.value);
63
+ }
64
+ } finally {
65
+ reader.releaseLock();
66
+ }
67
+ }
68
+ /**
69
+ * Copies a JSON value through serialization and parsing.
70
+ *
71
+ * @typeParam T - The JSON value type.
72
+ * @param value - The value to copy.
73
+ * @returns The parsed JSON copy.
74
+ * @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is
75
+ * normalized to zero by JSON serialization.
76
+ */
77
+ function roundTripJSON(value) {
78
+ const serialized = JSON.stringify(value, (_key, current) => {
79
+ if (typeof current === "number" && !Number.isFinite(current)) throw new Error("JSON values must contain finite numbers");
80
+ return current;
81
+ });
82
+ const parsed = JSON.parse(serialized);
83
+ const pending = [parsed];
84
+ while (pending.length > 0) {
85
+ const current = pending.pop();
86
+ if (current === void 0) continue;
87
+ if (typeof current === "number" && !Number.isFinite(current)) throw new Error("JSON values must contain finite numbers");
88
+ if (Array.isArray(current)) for (const child of current) pending.push(child);
89
+ else if (typeof current === "object" && current !== null) for (const child of Object.values(current)) pending.push(child);
90
+ }
91
+ return parsed;
92
+ }
93
+ /**
94
+ * Resolves the parent directory of a calling module, which is the workspace root when called from
95
+ * the conventional `tests/setup.ts` location.
96
+ *
97
+ * @param meta - The calling module metadata.
98
+ * @returns The root URL one directory above the calling file.
99
+ */
100
+ function resolveRoot(meta) {
101
+ return new URL("../", meta.url);
102
+ }
103
+ //#endregion
104
+ //#region src/core/factories.ts
105
+ /**
106
+ * Creates a recorder for callback arguments.
107
+ *
108
+ * @typeParam TArgs - The argument tuple to record.
109
+ * @returns A recorder whose handler appends calls in order.
110
+ */
111
+ function createRecorder() {
112
+ const calls = [];
113
+ return {
114
+ calls,
115
+ get count() {
116
+ return calls.length;
117
+ },
118
+ handler(...args) {
119
+ calls.push(args);
120
+ },
121
+ clear() {
122
+ calls.length = 0;
123
+ }
124
+ };
125
+ }
126
+ //#endregion
127
+ export { captureError, collect, collectStream, createRecorder, requireValue, resolveRoot, roundTripJSON, waitForDelay };
128
+
129
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/core/helpers.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { JSONValue } from './types.js'\n\n/**\n * Waits for a host timer to elapse.\n *\n * @param ms - The delay in milliseconds.\n * @returns A promise that resolves after the timer fires.\n */\nexport function waitForDelay(ms = 0): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Captures the value thrown by a thunk.\n *\n * @param thunk - The work whose thrown value to capture.\n * @returns The thrown value, or `undefined` when the thunk completes.\n */\nexport function captureError(thunk: () => unknown): unknown {\n\ttry {\n\t\tthunk()\n\t} catch (error) {\n\t\treturn error\n\t}\n\treturn undefined\n}\n\n/**\n * Requires a value to be present.\n *\n * @typeParam T - The required value type.\n * @param value - The value to check.\n * @param message - The error message used when the value is absent.\n * @returns The present value.\n */\nexport function requireValue<T>(value: T | null | undefined, message = 'Value is required'): T {\n\tif (value === null || value === undefined) throw new Error(message)\n\treturn value\n}\n\n/**\n * Collects every value from an async iterable.\n *\n * @typeParam T - The yielded value type.\n * @param source - The async iterable to drain.\n * @returns The yielded values in iteration order.\n */\nexport async function collect<T>(source: AsyncIterable<T>): Promise<readonly T[]> {\n\tconst values: T[] = []\n\tfor await (const value of source) values.push(value)\n\treturn values\n}\n\n/**\n * Collects every value from a readable stream.\n *\n * @typeParam T - The streamed value type.\n * @param stream - The readable stream to drain.\n * @returns The streamed values in read order.\n */\nexport async function collectStream<T>(stream: ReadableStream<T>): Promise<readonly T[]> {\n\tconst reader = stream.getReader()\n\tconst values: T[] = []\n\ttry {\n\t\twhile (true) {\n\t\t\tconst result = await reader.read()\n\t\t\tif (result.done) return values\n\t\t\tvalues.push(result.value)\n\t\t}\n\t} finally {\n\t\treader.releaseLock()\n\t}\n}\n\n/**\n * Copies a JSON value through serialization and parsing.\n *\n * @typeParam T - The JSON value type.\n * @param value - The value to copy.\n * @returns The parsed JSON copy.\n * @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is\n * normalized to zero by JSON serialization.\n */\nexport function roundTripJSON<T extends JSONValue>(value: T): T {\n\tconst serialized = JSON.stringify(value, (_key, current) => {\n\t\tif (typeof current === 'number' && !Number.isFinite(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\treturn current\n\t})\n\tconst parsed: T = JSON.parse(serialized)\n\tconst pending: JSONValue[] = [parsed]\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop()\n\t\tif (current === undefined) continue\n\t\tif (typeof current === 'number' && !Number.isFinite(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\tif (Array.isArray(current)) {\n\t\t\tfor (const child of current) pending.push(child)\n\t\t} else if (typeof current === 'object' && current !== null) {\n\t\t\tfor (const child of Object.values(current)) pending.push(child)\n\t\t}\n\t}\n\treturn parsed\n}\n\n/**\n * Resolves the parent directory of a calling module, which is the workspace root when called from\n * the conventional `tests/setup.ts` location.\n *\n * @param meta - The calling module metadata.\n * @returns The root URL one directory above the calling file.\n */\nexport function resolveRoot(meta: ImportMeta): URL {\n\treturn new URL('../', meta.url)\n}\n","import type { RecorderInterface } from './types.js'\n\n/**\n * Creates a recorder for callback arguments.\n *\n * @typeParam TArgs - The argument tuple to record.\n * @returns A recorder whose handler appends calls in order.\n */\nexport function createRecorder<TArgs extends readonly unknown[]>(): RecorderInterface<TArgs> {\n\tconst calls: TArgs[] = []\n\treturn {\n\t\tcalls,\n\t\tget count() {\n\t\t\treturn calls.length\n\t\t},\n\t\thandler(...args) {\n\t\t\tcalls.push(args)\n\t\t},\n\t\tclear() {\n\t\t\tcalls.length = 0\n\t\t},\n\t}\n}\n"],"mappings":";;;;;;;AAQA,SAAgB,aAAa,KAAK,GAAkB;CACnD,OAAO,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;AACxD;;;;;;;AAQA,SAAgB,aAAa,OAA+B;CAC3D,IAAI;EACH,MAAM;CACP,SAAS,OAAO;EACf,OAAO;CACR;AAED;;;;;;;;;AAUA,SAAgB,aAAgB,OAA6B,UAAU,qBAAwB;CAC9F,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,MAAM,IAAI,MAAM,OAAO;CAClE,OAAO;AACR;;;;;;;;AASA,eAAsB,QAAW,QAAiD;CACjF,MAAM,SAAc,CAAC;CACrB,WAAW,MAAM,SAAS,QAAQ,OAAO,KAAK,KAAK;CACnD,OAAO;AACR;;;;;;;;AASA,eAAsB,cAAiB,QAAkD;CACxF,MAAM,SAAS,OAAO,UAAU;CAChC,MAAM,SAAc,CAAC;CACrB,IAAI;EACH,OAAO,MAAM;GACZ,MAAM,SAAS,MAAM,OAAO,KAAK;GACjC,IAAI,OAAO,MAAM,OAAO;GACxB,OAAO,KAAK,OAAO,KAAK;EACzB;CACD,UAAU;EACT,OAAO,YAAY;CACpB;AACD;;;;;;;;;;AAWA,SAAgB,cAAmC,OAAa;CAC/D,MAAM,aAAa,KAAK,UAAU,QAAQ,MAAM,YAAY;EAC3D,IAAI,OAAO,YAAY,YAAY,CAAC,OAAO,SAAS,OAAO,GAC1D,MAAM,IAAI,MAAM,yCAAyC;EAE1D,OAAO;CACR,CAAC;CACD,MAAM,SAAY,KAAK,MAAM,UAAU;CACvC,MAAM,UAAuB,CAAC,MAAM;CACpC,OAAO,QAAQ,SAAS,GAAG;EAC1B,MAAM,UAAU,QAAQ,IAAI;EAC5B,IAAI,YAAY,KAAA,GAAW;EAC3B,IAAI,OAAO,YAAY,YAAY,CAAC,OAAO,SAAS,OAAO,GAC1D,MAAM,IAAI,MAAM,yCAAyC;EAE1D,IAAI,MAAM,QAAQ,OAAO,GACxB,KAAK,MAAM,SAAS,SAAS,QAAQ,KAAK,KAAK;OACzC,IAAI,OAAO,YAAY,YAAY,YAAY,MACrD,KAAK,MAAM,SAAS,OAAO,OAAO,OAAO,GAAG,QAAQ,KAAK,KAAK;CAEhE;CACA,OAAO;AACR;;;;;;;;AASA,SAAgB,YAAY,MAAuB;CAClD,OAAO,IAAI,IAAI,OAAO,KAAK,GAAG;AAC/B;;;;;;;;;AC5GA,SAAgB,iBAA6E;CAC5F,MAAM,QAAiB,CAAC;CACxB,OAAO;EACN;EACA,IAAI,QAAQ;GACX,OAAO,MAAM;EACd;EACA,QAAQ,GAAG,MAAM;GAChB,MAAM,KAAK,IAAI;EAChB;EACA,QAAQ;GACP,MAAM,SAAS;EAChB;CACD;AACD"}
@@ -0,0 +1,152 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_fs = require("node:fs");
3
+ let node_path = require("node:path");
4
+ let node_url = require("node:url");
5
+ let node_os = require("node:os");
6
+ //#region src/server/helpers.ts
7
+ /**
8
+ * Resolves a target that stays below a root directory.
9
+ *
10
+ * @param root - The absolute root directory.
11
+ * @param target - The relative or absolute target to resolve.
12
+ * @returns The absolute target, or `undefined` when the target escapes the root.
13
+ */
14
+ function resolveContained(root, target) {
15
+ const candidate = (0, node_path.resolve)(root, target);
16
+ const contained = (0, node_path.relative)(root, candidate);
17
+ if (contained === ".." || contained.startsWith(`..${node_path.sep}`) || (0, node_path.isAbsolute)(contained)) return;
18
+ return candidate;
19
+ }
20
+ /**
21
+ * Reads files from selected directories below a root directory.
22
+ *
23
+ * @param root - The root directory as a path or file URL.
24
+ * @param directories - The directories to visit below the root.
25
+ * @param options - Optional file extension and exact-path exclusions.
26
+ * @returns File contents keyed by sorted root-relative paths.
27
+ * @remarks An absent extension filter includes every file. Exclusions match full root-relative keys.
28
+ */
29
+ function readInventory(root, directories, options) {
30
+ const supplied = (0, node_path.resolve)(typeof root === "string" ? root : (0, node_url.fileURLToPath)(root));
31
+ const rootStatus = (0, node_fs.lstatSync)(supplied);
32
+ if (rootStatus.isSymbolicLink()) throw new Error("Root is a symbolic link");
33
+ if (!rootStatus.isDirectory()) throw new Error("Root is not a directory");
34
+ const base = node_fs.realpathSync.native(supplied);
35
+ if (directories.length === 0) return Object.fromEntries([]);
36
+ const excluded = new Set(options?.exclude);
37
+ const pending = [];
38
+ const queued = /* @__PURE__ */ new Set();
39
+ const contents = /* @__PURE__ */ new Map();
40
+ for (const directory of directories) {
41
+ const candidate = resolveContained(base, directory);
42
+ if (candidate === void 0) throw new Error(`Directory outside root: ${directory}`);
43
+ const status = (0, node_fs.lstatSync)(candidate);
44
+ if (status.isSymbolicLink()) throw new Error(`Directory is a symbolic link: ${directory}`);
45
+ if (!status.isDirectory()) throw new Error(`Not a directory: ${directory}`);
46
+ const physical = node_fs.realpathSync.native(candidate);
47
+ const resolved = resolveContained(base, (0, node_path.relative)(base, physical));
48
+ if (resolved === void 0) throw new Error(`Directory outside root: ${directory}`);
49
+ const key = (0, node_path.relative)(base, resolved).split(node_path.sep).join("/");
50
+ if (excluded.has(key) || queued.has(physical)) continue;
51
+ queued.add(physical);
52
+ pending.push(physical);
53
+ }
54
+ while (pending.length > 0) {
55
+ const directory = pending.pop();
56
+ if (directory === void 0) continue;
57
+ for (const entry of (0, node_fs.readdirSync)(directory, { withFileTypes: true })) {
58
+ const path = (0, node_path.resolve)(directory, entry.name);
59
+ const status = (0, node_fs.lstatSync)(path);
60
+ if (status.isSymbolicLink()) continue;
61
+ const key = (0, node_path.relative)(base, path).split(node_path.sep).join("/");
62
+ if (excluded.has(key)) continue;
63
+ if (status.isDirectory()) {
64
+ const physical = node_fs.realpathSync.native(path);
65
+ if (resolveContained(base, (0, node_path.relative)(base, physical)) === void 0 || queued.has(physical)) continue;
66
+ queued.add(physical);
67
+ pending.push(physical);
68
+ continue;
69
+ }
70
+ if (!status.isFile() || options?.extensions !== void 0 && !options.extensions.some((extension) => entry.name.endsWith(extension))) continue;
71
+ contents.set(key, (0, node_fs.readFileSync)(path, "utf8"));
72
+ }
73
+ }
74
+ return Object.fromEntries(Array.from(contents).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
75
+ }
76
+ //#endregion
77
+ //#region src/server/factories.ts
78
+ /**
79
+ * Allocates an owned temporary directory with contained file operations.
80
+ *
81
+ * @param options - Optional directory prefix and initial files.
82
+ * @returns The scratch directory and its file operations.
83
+ * @remarks The prefix defaults to `orkestrel-test-`. Seed keys use root-relative paths.
84
+ */
85
+ function createScratch(options) {
86
+ const temporary = (0, node_path.resolve)((0, node_os.tmpdir)());
87
+ const prefix = (0, node_path.resolve)(temporary, options?.prefix ?? "orkestrel-test-");
88
+ if ((0, node_path.dirname)(prefix) !== temporary) throw new Error("Scratch prefix must stay within the temporary directory");
89
+ const path = (0, node_fs.mkdtempSync)(prefix);
90
+ const allocation = (0, node_fs.statSync)(path);
91
+ const outside = "Path outside scratch directory";
92
+ try {
93
+ for (const [target, text] of Object.entries(options?.files ?? {})) {
94
+ const candidate = resolveContained(path, target);
95
+ if (candidate === void 0) throw new Error(`${outside}: ${target}`);
96
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(candidate), { recursive: true });
97
+ (0, node_fs.writeFileSync)(candidate, text);
98
+ }
99
+ } catch (error) {
100
+ (0, node_fs.rmSync)(path, {
101
+ force: true,
102
+ recursive: true
103
+ });
104
+ throw error;
105
+ }
106
+ const scratch = {
107
+ path,
108
+ write(target, text) {
109
+ const candidate = resolveContained(path, target);
110
+ if (candidate === void 0) throw new Error(`${outside}: ${target}`);
111
+ const rootStatus = (0, node_fs.lstatSync)(path, { throwIfNoEntry: false });
112
+ if (rootStatus === void 0) throw new Error("Scratch directory does not exist");
113
+ if (rootStatus.isSymbolicLink()) throw new Error("Scratch directory is a symbolic link");
114
+ if (!rootStatus.isDirectory()) throw new Error("Scratch path is not a directory");
115
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(candidate), { recursive: true });
116
+ (0, node_fs.writeFileSync)(candidate, text);
117
+ },
118
+ read(target) {
119
+ const candidate = resolveContained(path, target);
120
+ if (candidate === void 0) throw new Error(`${outside}: ${target}`);
121
+ if (!scratch.exists(target)) return void 0;
122
+ const status = (0, node_fs.statSync)(candidate, { throwIfNoEntry: false });
123
+ if (status === void 0) return void 0;
124
+ if (status.isDirectory()) throw new Error(`Scratch path is a directory: ${target}`);
125
+ return (0, node_fs.readFileSync)(candidate, "utf8");
126
+ },
127
+ exists(target) {
128
+ const candidate = resolveContained(path, target);
129
+ if (candidate === void 0) throw new Error(`${outside}: ${target}`);
130
+ const rootStatus = (0, node_fs.lstatSync)(path, { throwIfNoEntry: false });
131
+ if (rootStatus === void 0) return false;
132
+ if (rootStatus.isSymbolicLink()) throw new Error("Scratch directory is a symbolic link");
133
+ if (!rootStatus.isDirectory()) throw new Error("Scratch path is not a directory");
134
+ return (0, node_fs.lstatSync)(candidate, { throwIfNoEntry: false }) !== void 0;
135
+ },
136
+ destroy() {
137
+ const status = (0, node_fs.lstatSync)(path, { throwIfNoEntry: false });
138
+ if (status === void 0 || status.dev !== allocation.dev || status.ino !== allocation.ino || status.birthtimeMs !== allocation.birthtimeMs) return;
139
+ (0, node_fs.rmSync)(path, {
140
+ force: true,
141
+ recursive: true
142
+ });
143
+ }
144
+ };
145
+ return scratch;
146
+ }
147
+ //#endregion
148
+ exports.createScratch = createScratch;
149
+ exports.readInventory = readInventory;
150
+ exports.resolveContained = resolveContained;
151
+
152
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["import type { InventoryOptions } from './types.js'\nimport { lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs'\nimport { isAbsolute, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Resolves a target that stays below a root directory.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target, or `undefined` when the target escapes the root.\n */\nexport function resolveContained(root: string, target: string): string | undefined {\n\tconst candidate = resolve(root, target)\n\tconst contained = relative(root, candidate)\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Reads files from selected directories below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param directories - The directories to visit below the root.\n * @param options - Optional file extension and exact-path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @remarks An absent extension filter includes every file. Exclusions match full root-relative keys.\n */\nexport function readInventory(\n\troot: URL | string,\n\tdirectories: readonly string[],\n\toptions?: InventoryOptions,\n): Readonly<Record<string, string>> {\n\tconst supplied = resolve(typeof root === 'string' ? root : fileURLToPath(root))\n\tconst rootStatus = lstatSync(supplied)\n\tif (rootStatus.isSymbolicLink()) throw new Error('Root is a symbolic link')\n\tif (!rootStatus.isDirectory()) throw new Error('Root is not a directory')\n\n\tconst base = realpathSync.native(supplied)\n\tif (directories.length === 0) return Object.fromEntries([])\n\n\tconst excluded = new Set(options?.exclude)\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const directory of directories) {\n\t\tconst candidate = resolveContained(base, directory)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Directory outside root: ${directory}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Directory is a symbolic link: ${directory}`)\n\t\tif (!status.isDirectory()) throw new Error(`Not a directory: ${directory}`)\n\n\t\tconst physical = realpathSync.native(candidate)\n\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\tif (resolved === undefined) {\n\t\t\tthrow new Error(`Directory outside root: ${directory}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (excluded.has(key) || queued.has(physical)) continue\n\t\tqueued.add(physical)\n\t\tpending.push(physical)\n\t}\n\n\twhile (pending.length > 0) {\n\t\tconst directory = pending.pop()\n\t\tif (directory === undefined) continue\n\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tconst path = resolve(directory, entry.name)\n\t\t\tconst status = lstatSync(path)\n\t\t\tif (status.isSymbolicLink()) continue\n\n\t\t\tconst key = relative(base, path).split(sep).join('/')\n\t\t\tif (excluded.has(key)) continue\n\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tconst physical = realpathSync.native(path)\n\t\t\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\t\t\tif (resolved === undefined || queued.has(physical)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tqueued.add(physical)\n\t\t\t\tpending.push(physical)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!status.isFile() ||\n\t\t\t\t(options?.extensions !== undefined &&\n\t\t\t\t\t!options.extensions.some((extension) => entry.name.endsWith(extension)))\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontents.set(key, readFileSync(path, 'utf8'))\n\t\t}\n\t}\n\n\treturn Object.fromEntries(\n\t\tArray.from(contents).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),\n\t)\n}\n","import type { ScratchInterface, ScratchOptions } from './types.js'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\trmSync,\n\tstatSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve } from 'node:path'\nimport { resolveContained } from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional directory prefix and initial files.\n * @returns The scratch directory and its file operations.\n * @remarks The prefix defaults to `orkestrel-test-`. Seed keys use root-relative paths.\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst temporary = resolve(tmpdir())\n\tconst prefix = resolve(temporary, options?.prefix ?? 'orkestrel-test-')\n\tif (dirname(prefix) !== temporary)\n\t\tthrow new Error('Scratch prefix must stay within the temporary directory')\n\n\tconst path = mkdtempSync(prefix)\n\tconst allocation = statSync(path)\n\tconst outside = 'Path outside scratch directory'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\trmSync(path, { force: true, recursive: true })\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) throw new Error('Scratch directory does not exist')\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.exists(target)) return undefined\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return undefined\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is a directory: ${target}`)\n\t\t\t}\n\t\t\treturn readFileSync(candidate, 'utf8')\n\t\t},\n\t\texists(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) return false\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\treturn lstatSync(candidate, { throwIfNoEntry: false }) !== undefined\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (\n\t\t\t\tstatus === undefined ||\n\t\t\t\tstatus.dev !== allocation.dev ||\n\t\t\t\tstatus.ino !== allocation.ino ||\n\t\t\t\tstatus.birthtimeMs !== allocation.birthtimeMs\n\t\t\t)\n\t\t\t\treturn\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t},\n\t}\n\treturn scratch\n}\n"],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Allocates an owned temporary directory with contained file operations.
3
+ *
4
+ * @param options - Optional directory prefix and initial files.
5
+ * @returns The scratch directory and its file operations.
6
+ * @remarks The prefix defaults to `orkestrel-test-`. Seed keys use root-relative paths.
7
+ */
8
+ export declare function createScratch(options?: ScratchOptions): ScratchInterface;
9
+
10
+ /** Options for reading a source inventory. */
11
+ export declare interface InventoryOptions {
12
+ /** The file extensions to include, each written with its leading dot. */
13
+ readonly extensions?: readonly string[];
14
+ /** The root-relative path keys to exclude. A directory key also excludes its descendants. */
15
+ readonly exclude?: readonly string[];
16
+ }
17
+
18
+ /**
19
+ * Reads files from selected directories below a root directory.
20
+ *
21
+ * @param root - The root directory as a path or file URL.
22
+ * @param directories - The directories to visit below the root.
23
+ * @param options - Optional file extension and exact-path exclusions.
24
+ * @returns File contents keyed by sorted root-relative paths.
25
+ * @remarks An absent extension filter includes every file. Exclusions match full root-relative keys.
26
+ */
27
+ export declare function readInventory(root: URL | string, directories: readonly string[], options?: InventoryOptions): Readonly<Record<string, string>>;
28
+
29
+ /**
30
+ * Resolves a target that stays below a root directory.
31
+ *
32
+ * @param root - The absolute root directory.
33
+ * @param target - The relative or absolute target to resolve.
34
+ * @returns The absolute target, or `undefined` when the target escapes the root.
35
+ */
36
+ export declare function resolveContained(root: string, target: string): string | undefined;
37
+
38
+ /** A temporary directory a test owns, writes into, reads back, and removes when it is done. */
39
+ export declare interface ScratchInterface {
40
+ /** The absolute path of the allocated directory. */
41
+ readonly path: string;
42
+ /**
43
+ * Writes a file, creating each parent directory that does not exist.
44
+ *
45
+ * @param target - A relative or absolute file path contained by the scratch directory.
46
+ * @param text - The file contents.
47
+ */
48
+ write(target: string, text: string): void;
49
+ /**
50
+ * Reads a file.
51
+ *
52
+ * @param target - A relative or absolute file path contained by the scratch directory.
53
+ * @returns The file contents, or `undefined` when no file can be read, including through a
54
+ * symbolic link whose target is missing.
55
+ * @throws When the path is a directory, escapes the scratch directory, or its root is a symbolic
56
+ * link or file. Reading follows links, so a link the host cannot resolve — a cycle, for one —
57
+ * surfaces the host's own error rather than `undefined`.
58
+ */
59
+ read(target: string): string | undefined;
60
+ /**
61
+ * Reports whether a path exists.
62
+ *
63
+ * @param target - A relative or absolute path contained by the scratch directory.
64
+ * @returns True when the entry exists, including a symbolic link whose target is missing.
65
+ * @throws When the path escapes the scratch directory or its root is a symbolic link or file.
66
+ */
67
+ exists(target: string): boolean;
68
+ /** Removes the directory and everything in it. */
69
+ destroy(): void;
70
+ }
71
+
72
+ /** Options for allocating a scratch directory. */
73
+ export declare interface ScratchOptions {
74
+ /** The leading text of the generated directory name. */
75
+ readonly prefix?: string;
76
+ /** Files to write on allocation, keyed by path below the scratch directory. */
77
+ readonly files?: Readonly<Record<string, string>>;
78
+ }
79
+
80
+ export { }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Allocates an owned temporary directory with contained file operations.
3
+ *
4
+ * @param options - Optional directory prefix and initial files.
5
+ * @returns The scratch directory and its file operations.
6
+ * @remarks The prefix defaults to `orkestrel-test-`. Seed keys use root-relative paths.
7
+ */
8
+ export declare function createScratch(options?: ScratchOptions): ScratchInterface;
9
+
10
+ /** Options for reading a source inventory. */
11
+ export declare interface InventoryOptions {
12
+ /** The file extensions to include, each written with its leading dot. */
13
+ readonly extensions?: readonly string[];
14
+ /** The root-relative path keys to exclude. A directory key also excludes its descendants. */
15
+ readonly exclude?: readonly string[];
16
+ }
17
+
18
+ /**
19
+ * Reads files from selected directories below a root directory.
20
+ *
21
+ * @param root - The root directory as a path or file URL.
22
+ * @param directories - The directories to visit below the root.
23
+ * @param options - Optional file extension and exact-path exclusions.
24
+ * @returns File contents keyed by sorted root-relative paths.
25
+ * @remarks An absent extension filter includes every file. Exclusions match full root-relative keys.
26
+ */
27
+ export declare function readInventory(root: URL | string, directories: readonly string[], options?: InventoryOptions): Readonly<Record<string, string>>;
28
+
29
+ /**
30
+ * Resolves a target that stays below a root directory.
31
+ *
32
+ * @param root - The absolute root directory.
33
+ * @param target - The relative or absolute target to resolve.
34
+ * @returns The absolute target, or `undefined` when the target escapes the root.
35
+ */
36
+ export declare function resolveContained(root: string, target: string): string | undefined;
37
+
38
+ /** A temporary directory a test owns, writes into, reads back, and removes when it is done. */
39
+ export declare interface ScratchInterface {
40
+ /** The absolute path of the allocated directory. */
41
+ readonly path: string;
42
+ /**
43
+ * Writes a file, creating each parent directory that does not exist.
44
+ *
45
+ * @param target - A relative or absolute file path contained by the scratch directory.
46
+ * @param text - The file contents.
47
+ */
48
+ write(target: string, text: string): void;
49
+ /**
50
+ * Reads a file.
51
+ *
52
+ * @param target - A relative or absolute file path contained by the scratch directory.
53
+ * @returns The file contents, or `undefined` when no file can be read, including through a
54
+ * symbolic link whose target is missing.
55
+ * @throws When the path is a directory, escapes the scratch directory, or its root is a symbolic
56
+ * link or file. Reading follows links, so a link the host cannot resolve — a cycle, for one —
57
+ * surfaces the host's own error rather than `undefined`.
58
+ */
59
+ read(target: string): string | undefined;
60
+ /**
61
+ * Reports whether a path exists.
62
+ *
63
+ * @param target - A relative or absolute path contained by the scratch directory.
64
+ * @returns True when the entry exists, including a symbolic link whose target is missing.
65
+ * @throws When the path escapes the scratch directory or its root is a symbolic link or file.
66
+ */
67
+ exists(target: string): boolean;
68
+ /** Removes the directory and everything in it. */
69
+ destroy(): void;
70
+ }
71
+
72
+ /** Options for allocating a scratch directory. */
73
+ export declare interface ScratchOptions {
74
+ /** The leading text of the generated directory name. */
75
+ readonly prefix?: string;
76
+ /** Files to write on allocation, keyed by path below the scratch directory. */
77
+ readonly files?: Readonly<Record<string, string>>;
78
+ }
79
+
80
+ export { }
@@ -0,0 +1,149 @@
1
+ import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { tmpdir } from "node:os";
5
+ //#region src/server/helpers.ts
6
+ /**
7
+ * Resolves a target that stays below a root directory.
8
+ *
9
+ * @param root - The absolute root directory.
10
+ * @param target - The relative or absolute target to resolve.
11
+ * @returns The absolute target, or `undefined` when the target escapes the root.
12
+ */
13
+ function resolveContained(root, target) {
14
+ const candidate = resolve(root, target);
15
+ const contained = relative(root, candidate);
16
+ if (contained === ".." || contained.startsWith(`..${sep}`) || isAbsolute(contained)) return;
17
+ return candidate;
18
+ }
19
+ /**
20
+ * Reads files from selected directories below a root directory.
21
+ *
22
+ * @param root - The root directory as a path or file URL.
23
+ * @param directories - The directories to visit below the root.
24
+ * @param options - Optional file extension and exact-path exclusions.
25
+ * @returns File contents keyed by sorted root-relative paths.
26
+ * @remarks An absent extension filter includes every file. Exclusions match full root-relative keys.
27
+ */
28
+ function readInventory(root, directories, options) {
29
+ const supplied = resolve(typeof root === "string" ? root : fileURLToPath(root));
30
+ const rootStatus = lstatSync(supplied);
31
+ if (rootStatus.isSymbolicLink()) throw new Error("Root is a symbolic link");
32
+ if (!rootStatus.isDirectory()) throw new Error("Root is not a directory");
33
+ const base = realpathSync.native(supplied);
34
+ if (directories.length === 0) return Object.fromEntries([]);
35
+ const excluded = new Set(options?.exclude);
36
+ const pending = [];
37
+ const queued = /* @__PURE__ */ new Set();
38
+ const contents = /* @__PURE__ */ new Map();
39
+ for (const directory of directories) {
40
+ const candidate = resolveContained(base, directory);
41
+ if (candidate === void 0) throw new Error(`Directory outside root: ${directory}`);
42
+ const status = lstatSync(candidate);
43
+ if (status.isSymbolicLink()) throw new Error(`Directory is a symbolic link: ${directory}`);
44
+ if (!status.isDirectory()) throw new Error(`Not a directory: ${directory}`);
45
+ const physical = realpathSync.native(candidate);
46
+ const resolved = resolveContained(base, relative(base, physical));
47
+ if (resolved === void 0) throw new Error(`Directory outside root: ${directory}`);
48
+ const key = relative(base, resolved).split(sep).join("/");
49
+ if (excluded.has(key) || queued.has(physical)) continue;
50
+ queued.add(physical);
51
+ pending.push(physical);
52
+ }
53
+ while (pending.length > 0) {
54
+ const directory = pending.pop();
55
+ if (directory === void 0) continue;
56
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
57
+ const path = resolve(directory, entry.name);
58
+ const status = lstatSync(path);
59
+ if (status.isSymbolicLink()) continue;
60
+ const key = relative(base, path).split(sep).join("/");
61
+ if (excluded.has(key)) continue;
62
+ if (status.isDirectory()) {
63
+ const physical = realpathSync.native(path);
64
+ if (resolveContained(base, relative(base, physical)) === void 0 || queued.has(physical)) continue;
65
+ queued.add(physical);
66
+ pending.push(physical);
67
+ continue;
68
+ }
69
+ if (!status.isFile() || options?.extensions !== void 0 && !options.extensions.some((extension) => entry.name.endsWith(extension))) continue;
70
+ contents.set(key, readFileSync(path, "utf8"));
71
+ }
72
+ }
73
+ return Object.fromEntries(Array.from(contents).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
74
+ }
75
+ //#endregion
76
+ //#region src/server/factories.ts
77
+ /**
78
+ * Allocates an owned temporary directory with contained file operations.
79
+ *
80
+ * @param options - Optional directory prefix and initial files.
81
+ * @returns The scratch directory and its file operations.
82
+ * @remarks The prefix defaults to `orkestrel-test-`. Seed keys use root-relative paths.
83
+ */
84
+ function createScratch(options) {
85
+ const temporary = resolve(tmpdir());
86
+ const prefix = resolve(temporary, options?.prefix ?? "orkestrel-test-");
87
+ if (dirname(prefix) !== temporary) throw new Error("Scratch prefix must stay within the temporary directory");
88
+ const path = mkdtempSync(prefix);
89
+ const allocation = statSync(path);
90
+ const outside = "Path outside scratch directory";
91
+ try {
92
+ for (const [target, text] of Object.entries(options?.files ?? {})) {
93
+ const candidate = resolveContained(path, target);
94
+ if (candidate === void 0) throw new Error(`${outside}: ${target}`);
95
+ mkdirSync(dirname(candidate), { recursive: true });
96
+ writeFileSync(candidate, text);
97
+ }
98
+ } catch (error) {
99
+ rmSync(path, {
100
+ force: true,
101
+ recursive: true
102
+ });
103
+ throw error;
104
+ }
105
+ const scratch = {
106
+ path,
107
+ write(target, text) {
108
+ const candidate = resolveContained(path, target);
109
+ if (candidate === void 0) throw new Error(`${outside}: ${target}`);
110
+ const rootStatus = lstatSync(path, { throwIfNoEntry: false });
111
+ if (rootStatus === void 0) throw new Error("Scratch directory does not exist");
112
+ if (rootStatus.isSymbolicLink()) throw new Error("Scratch directory is a symbolic link");
113
+ if (!rootStatus.isDirectory()) throw new Error("Scratch path is not a directory");
114
+ mkdirSync(dirname(candidate), { recursive: true });
115
+ writeFileSync(candidate, text);
116
+ },
117
+ read(target) {
118
+ const candidate = resolveContained(path, target);
119
+ if (candidate === void 0) throw new Error(`${outside}: ${target}`);
120
+ if (!scratch.exists(target)) return void 0;
121
+ const status = statSync(candidate, { throwIfNoEntry: false });
122
+ if (status === void 0) return void 0;
123
+ if (status.isDirectory()) throw new Error(`Scratch path is a directory: ${target}`);
124
+ return readFileSync(candidate, "utf8");
125
+ },
126
+ exists(target) {
127
+ const candidate = resolveContained(path, target);
128
+ if (candidate === void 0) throw new Error(`${outside}: ${target}`);
129
+ const rootStatus = lstatSync(path, { throwIfNoEntry: false });
130
+ if (rootStatus === void 0) return false;
131
+ if (rootStatus.isSymbolicLink()) throw new Error("Scratch directory is a symbolic link");
132
+ if (!rootStatus.isDirectory()) throw new Error("Scratch path is not a directory");
133
+ return lstatSync(candidate, { throwIfNoEntry: false }) !== void 0;
134
+ },
135
+ destroy() {
136
+ const status = lstatSync(path, { throwIfNoEntry: false });
137
+ if (status === void 0 || status.dev !== allocation.dev || status.ino !== allocation.ino || status.birthtimeMs !== allocation.birthtimeMs) return;
138
+ rmSync(path, {
139
+ force: true,
140
+ recursive: true
141
+ });
142
+ }
143
+ };
144
+ return scratch;
145
+ }
146
+ //#endregion
147
+ export { createScratch, readInventory, resolveContained };
148
+
149
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["import type { InventoryOptions } from './types.js'\nimport { lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs'\nimport { isAbsolute, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Resolves a target that stays below a root directory.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target, or `undefined` when the target escapes the root.\n */\nexport function resolveContained(root: string, target: string): string | undefined {\n\tconst candidate = resolve(root, target)\n\tconst contained = relative(root, candidate)\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Reads files from selected directories below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param directories - The directories to visit below the root.\n * @param options - Optional file extension and exact-path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @remarks An absent extension filter includes every file. Exclusions match full root-relative keys.\n */\nexport function readInventory(\n\troot: URL | string,\n\tdirectories: readonly string[],\n\toptions?: InventoryOptions,\n): Readonly<Record<string, string>> {\n\tconst supplied = resolve(typeof root === 'string' ? root : fileURLToPath(root))\n\tconst rootStatus = lstatSync(supplied)\n\tif (rootStatus.isSymbolicLink()) throw new Error('Root is a symbolic link')\n\tif (!rootStatus.isDirectory()) throw new Error('Root is not a directory')\n\n\tconst base = realpathSync.native(supplied)\n\tif (directories.length === 0) return Object.fromEntries([])\n\n\tconst excluded = new Set(options?.exclude)\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const directory of directories) {\n\t\tconst candidate = resolveContained(base, directory)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Directory outside root: ${directory}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Directory is a symbolic link: ${directory}`)\n\t\tif (!status.isDirectory()) throw new Error(`Not a directory: ${directory}`)\n\n\t\tconst physical = realpathSync.native(candidate)\n\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\tif (resolved === undefined) {\n\t\t\tthrow new Error(`Directory outside root: ${directory}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (excluded.has(key) || queued.has(physical)) continue\n\t\tqueued.add(physical)\n\t\tpending.push(physical)\n\t}\n\n\twhile (pending.length > 0) {\n\t\tconst directory = pending.pop()\n\t\tif (directory === undefined) continue\n\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tconst path = resolve(directory, entry.name)\n\t\t\tconst status = lstatSync(path)\n\t\t\tif (status.isSymbolicLink()) continue\n\n\t\t\tconst key = relative(base, path).split(sep).join('/')\n\t\t\tif (excluded.has(key)) continue\n\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tconst physical = realpathSync.native(path)\n\t\t\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\t\t\tif (resolved === undefined || queued.has(physical)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tqueued.add(physical)\n\t\t\t\tpending.push(physical)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!status.isFile() ||\n\t\t\t\t(options?.extensions !== undefined &&\n\t\t\t\t\t!options.extensions.some((extension) => entry.name.endsWith(extension)))\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontents.set(key, readFileSync(path, 'utf8'))\n\t\t}\n\t}\n\n\treturn Object.fromEntries(\n\t\tArray.from(contents).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),\n\t)\n}\n","import type { ScratchInterface, ScratchOptions } from './types.js'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\trmSync,\n\tstatSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve } from 'node:path'\nimport { resolveContained } from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional directory prefix and initial files.\n * @returns The scratch directory and its file operations.\n * @remarks The prefix defaults to `orkestrel-test-`. Seed keys use root-relative paths.\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst temporary = resolve(tmpdir())\n\tconst prefix = resolve(temporary, options?.prefix ?? 'orkestrel-test-')\n\tif (dirname(prefix) !== temporary)\n\t\tthrow new Error('Scratch prefix must stay within the temporary directory')\n\n\tconst path = mkdtempSync(prefix)\n\tconst allocation = statSync(path)\n\tconst outside = 'Path outside scratch directory'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\trmSync(path, { force: true, recursive: true })\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) throw new Error('Scratch directory does not exist')\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.exists(target)) return undefined\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return undefined\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is a directory: ${target}`)\n\t\t\t}\n\t\t\treturn readFileSync(candidate, 'utf8')\n\t\t},\n\t\texists(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) return false\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\treturn lstatSync(candidate, { throwIfNoEntry: false }) !== undefined\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (\n\t\t\t\tstatus === undefined ||\n\t\t\t\tstatus.dev !== allocation.dev ||\n\t\t\t\tstatus.ino !== allocation.ino ||\n\t\t\t\tstatus.birthtimeMs !== allocation.birthtimeMs\n\t\t\t)\n\t\t\t\treturn\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t},\n\t}\n\treturn scratch\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@orkestrel/test",
3
+ "version": "0.0.1",
4
+ "description": "The test helpers the Orkestrel fleet repeats — a call recorder, a real delay, JSON and async collectors, and an owned scratch directory with a source-file walker. Zero runtime dependencies. Part of the @orkestrel line.",
5
+ "keywords": [],
6
+ "homepage": "https://github.com/orkestrel/test#readme",
7
+ "bugs": "https://github.com/orkestrel/test/issues",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/orkestrel/test.git"
12
+ },
13
+ "files": [
14
+ "dist/src",
15
+ "README.md"
16
+ ],
17
+ "type": "module",
18
+ "sideEffects": false,
19
+ "main": "./dist/src/core/index.cjs",
20
+ "module": "./dist/src/core/index.js",
21
+ "exports": {
22
+ ".": {
23
+ "import": {
24
+ "types": "./dist/src/core/index.d.ts",
25
+ "default": "./dist/src/core/index.js"
26
+ },
27
+ "require": {
28
+ "types": "./dist/src/core/index.d.cts",
29
+ "default": "./dist/src/core/index.cjs"
30
+ }
31
+ },
32
+ "./server": {
33
+ "import": {
34
+ "types": "./dist/src/server/index.d.ts",
35
+ "default": "./dist/src/server/index.js"
36
+ },
37
+ "require": {
38
+ "types": "./dist/src/server/index.d.cts",
39
+ "default": "./dist/src/server/index.cjs"
40
+ }
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "scripts": {
48
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
49
+ "copy": "node -e \"const fs=require('node:fs'),p=require('node:path'),a=process.argv[1],b=process.argv[2];fs.mkdirSync(p.dirname(b),{recursive:true});fs.cpSync(a,b,{force:true});console.log('Copied: '+a+' to '+b)\"",
50
+ "format": "oxfmt --config .oxfmtrc.json --write .",
51
+ "format:check": "oxfmt --config .oxfmtrc.json --check .",
52
+ "lint": "oxlint --config .oxlintrc.json --fix .",
53
+ "lint:check": "oxlint --config .oxlintrc.json --deny-warnings .",
54
+ "check": "tsc --noEmit --project tsconfig.json && npm run check:src",
55
+ "check:src": "npm run check:src:core && npm run check:src:server",
56
+ "check:src:core": "tsc --noEmit -p configs/src/tsconfig.core.json",
57
+ "check:src:server": "tsc --noEmit -p configs/src/tsconfig.server.json",
58
+ "test": "npm run test:src && npm run test:policy && npm run test:config && npm run test:guides",
59
+ "test:src": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core --project src:server",
60
+ "test:src:core": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:core",
61
+ "test:src:server": "vitest run --config vite.config.ts --no-cache --reporter=dot --project src:server",
62
+ "test:policy": "vitest run --config vite.config.ts --no-cache --reporter=dot --project policy",
63
+ "test:config": "vitest run --config vite.config.ts --no-cache --reporter=dot --project config",
64
+ "test:guides": "vitest run --config vite.config.ts --no-cache --reporter=dot --project guides",
65
+ "test:probe": "vitest run --config vite.config.ts --no-cache --reporter=verbose --project probe",
66
+ "build": "npm run clean && npm run build:src",
67
+ "build:src": "npm run build:src:core && npm run build:src:server",
68
+ "build:src:core": "vite build --config configs/src/vite.core.config.ts && npm run copy dist/src/core/index.d.ts dist/src/core/index.d.cts",
69
+ "build:src:server": "vite build --config configs/src/vite.server.config.ts && npm run copy dist/src/server/index.d.ts dist/src/server/index.d.cts",
70
+ "prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test"
71
+ },
72
+ "dependencies": {},
73
+ "devDependencies": {
74
+ "@microsoft/api-extractor": "^7.58.12",
75
+ "@orkestrel/guide": "^0.0.10",
76
+ "@orkestrel/scaffold": "^0.0.30",
77
+ "@types/node": "^26.2.0",
78
+ "oxfmt": "^0.62.0",
79
+ "oxlint": "^1.77.0",
80
+ "typescript": "^6.0.3",
81
+ "vite": "~8.2.0",
82
+ "vite-plugin-dts": "^5.0.3",
83
+ "vitest": "^4.1.10"
84
+ },
85
+ "engines": {
86
+ "node": ">=22.12.0"
87
+ }
88
+ }