@orkestrel/test 0.0.13 → 0.0.14

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.
@@ -1,7 +1,7 @@
1
- import { Server } from 'node:net';
2
- import { Socket } from 'node:net';
3
- import { Stats } from 'node:fs';
4
- import { WaitOptions } from '@orkestrel/test';
1
+ import type { Server } from 'node:net';
2
+ import type { Socket } from 'node:net';
3
+ import type { Stats } from 'node:fs';
4
+ import type { WaitOptions } from '@orkestrel/test';
5
5
 
6
6
  /** Holds a name-keyed cookie store a test drives one origin with, filled from real responses. */
7
7
  export declare interface CookieJarInterface {
@@ -24,7 +24,8 @@ export declare interface CookieJarInterface {
24
24
  * @returns Those fields unmodified, in the order the response carried them.
25
25
  * @remarks Selection is by name alone. A field spelling `Max-Age=0` deletes its cookie and every
26
26
  * other field stores or replaces one, so `Domain`, `Path`, `Expires`, and `Secure` are read past
27
- * rather than honoured. Nothing outlives the jar.
27
+ * rather than honoured. A field carrying no `name=value` pair is read past too and still
28
+ * returned. Nothing outlives the jar.
28
29
  */
29
30
  capture(response: Response): readonly string[];
30
31
  }
@@ -51,6 +52,17 @@ export declare function createCookieJar(): CookieJarInterface;
51
52
  * `EPERM`, while a directory or missing source is passed to a junction attempt. A missing source is
52
53
  * accepted to create a dangling junction. Where the host creates a junction, its stored value is the
53
54
  * resolved absolute path.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { readFileSync } from 'node:fs'
59
+ * import { createLink } from '@orkestrel/test/server'
60
+ *
61
+ * // `/scratch/source` is a directory holding `file.txt`.
62
+ * createLink('/scratch/linked', '/scratch/source')
63
+ *
64
+ * readFileSync('/scratch/linked/file.txt', 'utf8') // 'linked'
65
+ * ```
54
66
  */
55
67
  export declare function createLink(path: string, source: string): void;
56
68
 
@@ -72,6 +84,67 @@ export declare function createLoopback(server: Server): Promise<LoopbackInterfac
72
84
  * `/` or `\`; or when allocation or seeding fails.
73
85
  * @remarks Default parent: the host temporary directory. Default prefix: `orkestrel-test-`. Seed
74
86
  * keys use root-relative paths.
87
+ *
88
+ * @example Own a temporary directory
89
+ * ```ts
90
+ * import { createScratch } from '@orkestrel/test/server'
91
+ *
92
+ * const scratch = createScratch({ prefix: 'guide-', files: { 'src/index.ts': 'export {}\n' } })
93
+ *
94
+ * scratch.read('src/index.ts') // 'export {}\n'
95
+ * scratch.has('src') // true
96
+ * scratch.read('src') // throws Error: Scratch path is a directory: src
97
+ * scratch.read('missing.ts') // undefined
98
+ * scratch.write('../escape.ts', '') // throws Error: Path outside scratch directory: ../escape.ts
99
+ *
100
+ * // `write` answers the contained path it wrote, the way `ensure` and `link` answer theirs, so the
101
+ * // path goes straight to the code under test without joining it again.
102
+ * scratch.write('src/notes.ts', 'export {}\n') // `${scratch.path}/src/notes.ts`
103
+ *
104
+ * // `ensure` is how you get an empty directory, because every `write` creates a file.
105
+ * scratch.ensure('empty')
106
+ * scratch.names() // ['empty', 'src']
107
+ * scratch.names('empty') // []
108
+ *
109
+ * // `parent` puts the allocation somewhere other than the host temporary directory.
110
+ * const child = createScratch({ parent: scratch.path, prefix: 'child-' })
111
+ * scratch.names().length // 3 — 'empty', 'src', and the child allocation
112
+ * child.destroy()
113
+ * scratch.names().length // 2 — the child removed itself and nothing else
114
+ *
115
+ * // `link` creates the symbolic link the threat model names, and `read` follows it. A directory
116
+ * // source runs on a host that creates no symbolic link too; see "Hosts that create no symbolic
117
+ * // link" for what such a host does with a file source.
118
+ * const outside = createScratch({ prefix: 'outside-', files: { 'read.ts': 'export {}\n' } })
119
+ * scratch.link('gate', outside.path) // `${scratch.path}/gate` — the link's own path, not its destination
120
+ * scratch.read('gate/read.ts') // 'export {}\n' — read through the link, at its destination
121
+ *
122
+ * // A link pointing out of the allocation is resolved through, so a contained path acts outside it.
123
+ * scratch.ensure('gate/made') // `${scratch.path}/gate/made` — the lexical path, not the destination
124
+ * outside.names() // ['made', 'read.ts'] — the directory was made under `outside.path`
125
+ * scratch.names('gate') // ['made', 'read.ts'] — the same entries, listed through the link
126
+ *
127
+ * // `link` acts at the final segment rather than through it, so `gate` is occupied.
128
+ * scratch.link('gate', outside.path) // throws Error: EEXIST: file already exists
129
+ *
130
+ * // `has` reads the final segment without following it, and `read` follows it.
131
+ * scratch.link('dangling', 'missing.ts')
132
+ * scratch.has('dangling') // true — the link is there
133
+ * scratch.read('dangling') // undefined — what it points at is not
134
+ *
135
+ * // `remove` takes one contained entry and acts at the final segment, so a link goes and whatever it
136
+ * // pointed at stays. A missing target is a no-op.
137
+ * scratch.remove('dangling')
138
+ * scratch.has('dangling') // false
139
+ * scratch.remove('missing.ts') // no throw — there was nothing there
140
+ * scratch.remove('src') // the directory and everything under it
141
+ * scratch.names() // ['empty', 'gate']
142
+ *
143
+ * scratch.destroy()
144
+ * scratch.destroy() // no-op — destroy is idempotent
145
+ * outside.has('made') // true — destroy unlinks `gate` and leaves what it pointed at
146
+ * outside.destroy()
147
+ * ```
75
148
  */
76
149
  export declare function createScratch(options?: ScratchOptions): ScratchInterface;
77
150
 
@@ -84,7 +157,7 @@ export declare function createScratch(options?: ScratchOptions): ScratchInterfac
84
157
  * @throws The abort reason, or an `Error` when a bound is invalid or the budget elapses. The
85
158
  * exhaustion error carries the last host refusal as its `cause`.
86
159
  * @remarks Default budget: `10000` milliseconds. Default interval: `25` milliseconds. A host holds a
87
- * directory for a short interval after the process that held it exits, and a just-stopped child's
160
+ * directory for a short interval after the process that held it exits, and a recently stopped child's
88
161
  * working directory is the case this exists for, so removal is attempted until the host lets go
89
162
  * rather than exactly once. {@link ScratchInterface.destroy} stays synchronous and is unchanged; this
90
163
  * is the bounded retry around it. A directory nothing releases still fails, with the host's own
@@ -120,6 +193,14 @@ export declare interface InventoryOptions {
120
193
  * @param key - The root-relative key to test.
121
194
  * @param exclusions - The normalized root-relative exclusion keys.
122
195
  * @returns True if an exclusion names the key or one of its ancestors; false otherwise.
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * import { isExcluded } from '@orkestrel/test/server'
200
+ *
201
+ * isExcluded('src/index.ts', ['src']) // true
202
+ * isExcluded('src-other/index.ts', ['src']) // false
203
+ * ```
123
204
  */
124
205
  export declare function isExcluded(key: string, exclusions: readonly string[]): boolean;
125
206
 
@@ -139,6 +220,14 @@ export declare function isExcluded(key: string, exclusions: readonly string[]):
139
220
  *
140
221
  * A Linux zombie — a process that has exited and whose parent has not reaped it — still accepts
141
222
  * signal `0`, so its `/proc` status is read and a `Z` state reads as false.
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * import { isRunning } from '@orkestrel/test/server'
227
+ *
228
+ * isRunning(process.pid) // true
229
+ * isRunning(2 ** 31) // false
230
+ * ```
142
231
  */
143
232
  export declare function isRunning(pid: number): boolean;
144
233
 
@@ -169,6 +258,17 @@ export declare interface LoopbackInterface {
169
258
  * @remarks All three fields are compared because none of them alone identifies an allocation. A
170
259
  * device is shared by every directory on one filesystem, an index node is reused once its directory
171
260
  * is removed, and a creation time repeats within the host's timestamp resolution.
261
+ *
262
+ * @example
263
+ * ```ts
264
+ * import { statSync } from 'node:fs'
265
+ * import { matchesIdentity, readIdentity } from '@orkestrel/test/server'
266
+ *
267
+ * const allocation = readIdentity(statSync('/scratch'))
268
+ *
269
+ * matchesIdentity(readIdentity(statSync('/scratch')), allocation) // true
270
+ * matchesIdentity({ birth: 3, device: 1, inode: 9 }, { birth: 3, device: 1, inode: 2 }) // false
271
+ * ```
172
272
  */
173
273
  export declare function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean;
174
274
 
@@ -181,6 +281,16 @@ export declare function matchesIdentity(current: ScratchIdentity, allocation: Sc
181
281
  * `code`, and one carrying a `code` that is not a string all answer `undefined`. A null-prototype
182
282
  * object is read the same way, because the key is tested with `in` rather than through
183
283
  * `hasOwnProperty`.
284
+ *
285
+ * @example
286
+ * ```ts
287
+ * import { readFileSync } from 'node:fs'
288
+ * import { captureError } from '@orkestrel/test'
289
+ * import { readErrorCode } from '@orkestrel/test/server'
290
+ *
291
+ * readErrorCode(captureError(() => readFileSync('/scratch/absent.txt', 'utf8'))) // 'ENOENT'
292
+ * readErrorCode(new Error('refused')) // undefined
293
+ * ```
184
294
  */
185
295
  export declare function readErrorCode(error: unknown): string | undefined;
186
296
 
@@ -189,6 +299,16 @@ export declare function readErrorCode(error: unknown): string | undefined;
189
299
  *
190
300
  * @param status - The status read from the directory's path.
191
301
  * @returns The device, index node, and creation time that together name the allocation.
302
+ *
303
+ * @example
304
+ * ```ts
305
+ * import { statSync } from 'node:fs'
306
+ * import { readIdentity } from '@orkestrel/test/server'
307
+ *
308
+ * const status = statSync('/scratch')
309
+ *
310
+ * readIdentity(status) // { birth: status.birthtimeMs, device: status.dev, inode: status.ino }
311
+ * ```
192
312
  */
193
313
  export declare function readIdentity(status: Stats): ScratchIdentity;
194
314
 
@@ -228,13 +348,23 @@ export declare const REMOVE_TREE_RETRYABLE_CODES: readonly string[];
228
348
  * @param path - The absolute directory to remove.
229
349
  * @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,
230
350
  * or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.
231
- * @remarks On Windows, a directory that a just-exited process still holds as its current
351
+ * @remarks On Windows, a directory that a recently exited process still holds as its current
232
352
  * working directory throws `EPERM` for a short interval after that process exits. Node's own
233
353
  * `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed
234
354
  * against a real held directory, they neither delay nor retry before rethrowing, so the retry
235
355
  * is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait
236
356
  * at roughly one second. A hold that outlasts that second is {@link destroyScratch}'s case, which
237
357
  * retries every refusal inside a caller's budget rather than the codes named here.
358
+ *
359
+ * @example
360
+ * ```ts
361
+ * import { existsSync } from 'node:fs'
362
+ * import { removeTree } from '@orkestrel/test/server'
363
+ *
364
+ * removeTree('/scratch/tree')
365
+ *
366
+ * existsSync('/scratch/tree') // false
367
+ * ```
238
368
  */
239
369
  export declare function removeTree(path: string): void;
240
370
 
@@ -282,6 +412,16 @@ export declare function requestUpgrade(port: number, options?: UpgradeOptions):
282
412
  * @remarks This is {@link resolveContained} with the refusal every contained scratch operation makes
283
413
  * of an escape, so the check and its one message are stated once. Read `resolveContained` where an
284
414
  * escape is an answer rather than a refusal.
415
+ *
416
+ * @example
417
+ * ```ts
418
+ * import { requireContained } from '@orkestrel/test/server'
419
+ *
420
+ * requireContained('/scratch', 'nested/file.txt') // '/scratch/nested/file.txt'
421
+ *
422
+ * // Throws Error: Path outside scratch directory: ../escape.ts
423
+ * requireContained('/scratch', '../escape.ts')
424
+ * ```
285
425
  */
286
426
  export declare function requireContained(root: string, target: string): string;
287
427
 
@@ -387,6 +527,7 @@ export declare interface ScratchInterface {
387
527
  * Removes the allocated directory and everything in it when its identity still matches.
388
528
  *
389
529
  * @throws When the host refuses to inspect or remove the matching allocation.
530
+ * @remarks Idempotent. An identity that no longer matches removes nothing.
390
531
  */
391
532
  destroy(): void;
392
533
  }
@@ -422,6 +563,13 @@ export declare interface ScratchOptions {
422
563
  * @remarks Byte `0x80` is an invalid UTF-8 lead byte. POSIX stores the name verbatim and Windows
423
564
  * rejects it with `ENOENT`, so the answer is true on POSIX and false on Windows. The path is passed
424
565
  * as a `Buffer` because the byte survives no string round trip.
566
+ *
567
+ * @example
568
+ * ```ts
569
+ * import { supportsBytes } from '@orkestrel/test/server'
570
+ *
571
+ * supportsBytes() // true on a POSIX host, false on Windows
572
+ * ```
425
573
  */
426
574
  export declare function supportsBytes(): boolean;
427
575
 
@@ -437,6 +585,13 @@ export declare function supportsBytes(): boolean;
437
585
  * routes the second write onto the first entry, so reading the first back returns the second's
438
586
  * contents and the answer is false. The answer is true on a typical POSIX host and false on a
439
587
  * case-folding Windows or macOS volume.
588
+ *
589
+ * @example
590
+ * ```ts
591
+ * import { supportsCase } from '@orkestrel/test/server'
592
+ *
593
+ * supportsCase() // true on a case-sensitive volume, false on a case-folding one
594
+ * ```
440
595
  */
441
596
  export declare function supportsCase(): boolean;
442
597
 
@@ -452,6 +607,13 @@ export declare function supportsCase(): boolean;
452
607
  * The answer is false on a filesystem carrying neither reparse points nor symbolic links. Every call
453
608
  * probes and cleans up after itself, so a host whose answer changes is read again rather than
454
609
  * remembered.
610
+ *
611
+ * @example
612
+ * ```ts
613
+ * import { supportsDirectoryLinks } from '@orkestrel/test/server'
614
+ *
615
+ * supportsDirectoryLinks() // true where the host creates a symbolic link or a junction
616
+ * ```
455
617
  */
456
618
  export declare function supportsDirectoryLinks(): boolean;
457
619
 
@@ -483,6 +645,13 @@ export declare function supportsFileLinks(): boolean;
483
645
  * it: a POSIX host running as uid `0` stores every bit faithfully and bypasses the access check the
484
646
  * bits describe, so a caller that needs a permission to be enforced probes the refusal it needs
485
647
  * rather than reading this.
648
+ *
649
+ * @example
650
+ * ```ts
651
+ * import { supportsMode } from '@orkestrel/test/server'
652
+ *
653
+ * supportsMode() // true on a POSIX host, false on Windows
654
+ * ```
486
655
  */
487
656
  export declare function supportsMode(): boolean;
488
657
 
@@ -531,6 +700,20 @@ export declare type UpgradeResult = {
531
700
  * every other error ends the wait. The interval is validated for consistency with the wait family but
532
701
  * is not used, because this helper parks on the socket's events. Both listeners are removed on every
533
702
  * settlement, so a caller may wait on one socket repeatedly.
703
+ *
704
+ * @example
705
+ * ```ts
706
+ * import { connect, createServer } from 'node:net'
707
+ * import { createLoopback, waitForSocketClose } from '@orkestrel/test/server'
708
+ *
709
+ * const loopback = await createLoopback(createServer((socket) => socket.end()))
710
+ * const client = connect(loopback.port, '127.0.0.1')
711
+ *
712
+ * await waitForSocketClose(client, { budget: 1000 }) // undefined
713
+ * client.destroyed // true
714
+ *
715
+ * await loopback.destroy()
716
+ * ```
534
717
  */
535
718
  export declare function waitForSocketClose(socket: Socket, options?: WaitOptions): Promise<void>;
536
719
 
@@ -1,7 +1,7 @@
1
- import { Server } from 'node:net';
2
- import { Socket } from 'node:net';
3
- import { Stats } from 'node:fs';
4
- import { WaitOptions } from '@orkestrel/test';
1
+ import type { Server } from 'node:net';
2
+ import type { Socket } from 'node:net';
3
+ import type { Stats } from 'node:fs';
4
+ import type { WaitOptions } from '@orkestrel/test';
5
5
 
6
6
  /** Holds a name-keyed cookie store a test drives one origin with, filled from real responses. */
7
7
  export declare interface CookieJarInterface {
@@ -24,7 +24,8 @@ export declare interface CookieJarInterface {
24
24
  * @returns Those fields unmodified, in the order the response carried them.
25
25
  * @remarks Selection is by name alone. A field spelling `Max-Age=0` deletes its cookie and every
26
26
  * other field stores or replaces one, so `Domain`, `Path`, `Expires`, and `Secure` are read past
27
- * rather than honoured. Nothing outlives the jar.
27
+ * rather than honoured. A field carrying no `name=value` pair is read past too and still
28
+ * returned. Nothing outlives the jar.
28
29
  */
29
30
  capture(response: Response): readonly string[];
30
31
  }
@@ -51,6 +52,17 @@ export declare function createCookieJar(): CookieJarInterface;
51
52
  * `EPERM`, while a directory or missing source is passed to a junction attempt. A missing source is
52
53
  * accepted to create a dangling junction. Where the host creates a junction, its stored value is the
53
54
  * resolved absolute path.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * import { readFileSync } from 'node:fs'
59
+ * import { createLink } from '@orkestrel/test/server'
60
+ *
61
+ * // `/scratch/source` is a directory holding `file.txt`.
62
+ * createLink('/scratch/linked', '/scratch/source')
63
+ *
64
+ * readFileSync('/scratch/linked/file.txt', 'utf8') // 'linked'
65
+ * ```
54
66
  */
55
67
  export declare function createLink(path: string, source: string): void;
56
68
 
@@ -72,6 +84,67 @@ export declare function createLoopback(server: Server): Promise<LoopbackInterfac
72
84
  * `/` or `\`; or when allocation or seeding fails.
73
85
  * @remarks Default parent: the host temporary directory. Default prefix: `orkestrel-test-`. Seed
74
86
  * keys use root-relative paths.
87
+ *
88
+ * @example Own a temporary directory
89
+ * ```ts
90
+ * import { createScratch } from '@orkestrel/test/server'
91
+ *
92
+ * const scratch = createScratch({ prefix: 'guide-', files: { 'src/index.ts': 'export {}\n' } })
93
+ *
94
+ * scratch.read('src/index.ts') // 'export {}\n'
95
+ * scratch.has('src') // true
96
+ * scratch.read('src') // throws Error: Scratch path is a directory: src
97
+ * scratch.read('missing.ts') // undefined
98
+ * scratch.write('../escape.ts', '') // throws Error: Path outside scratch directory: ../escape.ts
99
+ *
100
+ * // `write` answers the contained path it wrote, the way `ensure` and `link` answer theirs, so the
101
+ * // path goes straight to the code under test without joining it again.
102
+ * scratch.write('src/notes.ts', 'export {}\n') // `${scratch.path}/src/notes.ts`
103
+ *
104
+ * // `ensure` is how you get an empty directory, because every `write` creates a file.
105
+ * scratch.ensure('empty')
106
+ * scratch.names() // ['empty', 'src']
107
+ * scratch.names('empty') // []
108
+ *
109
+ * // `parent` puts the allocation somewhere other than the host temporary directory.
110
+ * const child = createScratch({ parent: scratch.path, prefix: 'child-' })
111
+ * scratch.names().length // 3 — 'empty', 'src', and the child allocation
112
+ * child.destroy()
113
+ * scratch.names().length // 2 — the child removed itself and nothing else
114
+ *
115
+ * // `link` creates the symbolic link the threat model names, and `read` follows it. A directory
116
+ * // source runs on a host that creates no symbolic link too; see "Hosts that create no symbolic
117
+ * // link" for what such a host does with a file source.
118
+ * const outside = createScratch({ prefix: 'outside-', files: { 'read.ts': 'export {}\n' } })
119
+ * scratch.link('gate', outside.path) // `${scratch.path}/gate` — the link's own path, not its destination
120
+ * scratch.read('gate/read.ts') // 'export {}\n' — read through the link, at its destination
121
+ *
122
+ * // A link pointing out of the allocation is resolved through, so a contained path acts outside it.
123
+ * scratch.ensure('gate/made') // `${scratch.path}/gate/made` — the lexical path, not the destination
124
+ * outside.names() // ['made', 'read.ts'] — the directory was made under `outside.path`
125
+ * scratch.names('gate') // ['made', 'read.ts'] — the same entries, listed through the link
126
+ *
127
+ * // `link` acts at the final segment rather than through it, so `gate` is occupied.
128
+ * scratch.link('gate', outside.path) // throws Error: EEXIST: file already exists
129
+ *
130
+ * // `has` reads the final segment without following it, and `read` follows it.
131
+ * scratch.link('dangling', 'missing.ts')
132
+ * scratch.has('dangling') // true — the link is there
133
+ * scratch.read('dangling') // undefined — what it points at is not
134
+ *
135
+ * // `remove` takes one contained entry and acts at the final segment, so a link goes and whatever it
136
+ * // pointed at stays. A missing target is a no-op.
137
+ * scratch.remove('dangling')
138
+ * scratch.has('dangling') // false
139
+ * scratch.remove('missing.ts') // no throw — there was nothing there
140
+ * scratch.remove('src') // the directory and everything under it
141
+ * scratch.names() // ['empty', 'gate']
142
+ *
143
+ * scratch.destroy()
144
+ * scratch.destroy() // no-op — destroy is idempotent
145
+ * outside.has('made') // true — destroy unlinks `gate` and leaves what it pointed at
146
+ * outside.destroy()
147
+ * ```
75
148
  */
76
149
  export declare function createScratch(options?: ScratchOptions): ScratchInterface;
77
150
 
@@ -84,7 +157,7 @@ export declare function createScratch(options?: ScratchOptions): ScratchInterfac
84
157
  * @throws The abort reason, or an `Error` when a bound is invalid or the budget elapses. The
85
158
  * exhaustion error carries the last host refusal as its `cause`.
86
159
  * @remarks Default budget: `10000` milliseconds. Default interval: `25` milliseconds. A host holds a
87
- * directory for a short interval after the process that held it exits, and a just-stopped child's
160
+ * directory for a short interval after the process that held it exits, and a recently stopped child's
88
161
  * working directory is the case this exists for, so removal is attempted until the host lets go
89
162
  * rather than exactly once. {@link ScratchInterface.destroy} stays synchronous and is unchanged; this
90
163
  * is the bounded retry around it. A directory nothing releases still fails, with the host's own
@@ -120,6 +193,14 @@ export declare interface InventoryOptions {
120
193
  * @param key - The root-relative key to test.
121
194
  * @param exclusions - The normalized root-relative exclusion keys.
122
195
  * @returns True if an exclusion names the key or one of its ancestors; false otherwise.
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * import { isExcluded } from '@orkestrel/test/server'
200
+ *
201
+ * isExcluded('src/index.ts', ['src']) // true
202
+ * isExcluded('src-other/index.ts', ['src']) // false
203
+ * ```
123
204
  */
124
205
  export declare function isExcluded(key: string, exclusions: readonly string[]): boolean;
125
206
 
@@ -139,6 +220,14 @@ export declare function isExcluded(key: string, exclusions: readonly string[]):
139
220
  *
140
221
  * A Linux zombie — a process that has exited and whose parent has not reaped it — still accepts
141
222
  * signal `0`, so its `/proc` status is read and a `Z` state reads as false.
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * import { isRunning } from '@orkestrel/test/server'
227
+ *
228
+ * isRunning(process.pid) // true
229
+ * isRunning(2 ** 31) // false
230
+ * ```
142
231
  */
143
232
  export declare function isRunning(pid: number): boolean;
144
233
 
@@ -169,6 +258,17 @@ export declare interface LoopbackInterface {
169
258
  * @remarks All three fields are compared because none of them alone identifies an allocation. A
170
259
  * device is shared by every directory on one filesystem, an index node is reused once its directory
171
260
  * is removed, and a creation time repeats within the host's timestamp resolution.
261
+ *
262
+ * @example
263
+ * ```ts
264
+ * import { statSync } from 'node:fs'
265
+ * import { matchesIdentity, readIdentity } from '@orkestrel/test/server'
266
+ *
267
+ * const allocation = readIdentity(statSync('/scratch'))
268
+ *
269
+ * matchesIdentity(readIdentity(statSync('/scratch')), allocation) // true
270
+ * matchesIdentity({ birth: 3, device: 1, inode: 9 }, { birth: 3, device: 1, inode: 2 }) // false
271
+ * ```
172
272
  */
173
273
  export declare function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean;
174
274
 
@@ -181,6 +281,16 @@ export declare function matchesIdentity(current: ScratchIdentity, allocation: Sc
181
281
  * `code`, and one carrying a `code` that is not a string all answer `undefined`. A null-prototype
182
282
  * object is read the same way, because the key is tested with `in` rather than through
183
283
  * `hasOwnProperty`.
284
+ *
285
+ * @example
286
+ * ```ts
287
+ * import { readFileSync } from 'node:fs'
288
+ * import { captureError } from '@orkestrel/test'
289
+ * import { readErrorCode } from '@orkestrel/test/server'
290
+ *
291
+ * readErrorCode(captureError(() => readFileSync('/scratch/absent.txt', 'utf8'))) // 'ENOENT'
292
+ * readErrorCode(new Error('refused')) // undefined
293
+ * ```
184
294
  */
185
295
  export declare function readErrorCode(error: unknown): string | undefined;
186
296
 
@@ -189,6 +299,16 @@ export declare function readErrorCode(error: unknown): string | undefined;
189
299
  *
190
300
  * @param status - The status read from the directory's path.
191
301
  * @returns The device, index node, and creation time that together name the allocation.
302
+ *
303
+ * @example
304
+ * ```ts
305
+ * import { statSync } from 'node:fs'
306
+ * import { readIdentity } from '@orkestrel/test/server'
307
+ *
308
+ * const status = statSync('/scratch')
309
+ *
310
+ * readIdentity(status) // { birth: status.birthtimeMs, device: status.dev, inode: status.ino }
311
+ * ```
192
312
  */
193
313
  export declare function readIdentity(status: Stats): ScratchIdentity;
194
314
 
@@ -228,13 +348,23 @@ export declare const REMOVE_TREE_RETRYABLE_CODES: readonly string[];
228
348
  * @param path - The absolute directory to remove.
229
349
  * @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,
230
350
  * or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.
231
- * @remarks On Windows, a directory that a just-exited process still holds as its current
351
+ * @remarks On Windows, a directory that a recently exited process still holds as its current
232
352
  * working directory throws `EPERM` for a short interval after that process exits. Node's own
233
353
  * `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed
234
354
  * against a real held directory, they neither delay nor retry before rethrowing, so the retry
235
355
  * is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait
236
356
  * at roughly one second. A hold that outlasts that second is {@link destroyScratch}'s case, which
237
357
  * retries every refusal inside a caller's budget rather than the codes named here.
358
+ *
359
+ * @example
360
+ * ```ts
361
+ * import { existsSync } from 'node:fs'
362
+ * import { removeTree } from '@orkestrel/test/server'
363
+ *
364
+ * removeTree('/scratch/tree')
365
+ *
366
+ * existsSync('/scratch/tree') // false
367
+ * ```
238
368
  */
239
369
  export declare function removeTree(path: string): void;
240
370
 
@@ -282,6 +412,16 @@ export declare function requestUpgrade(port: number, options?: UpgradeOptions):
282
412
  * @remarks This is {@link resolveContained} with the refusal every contained scratch operation makes
283
413
  * of an escape, so the check and its one message are stated once. Read `resolveContained` where an
284
414
  * escape is an answer rather than a refusal.
415
+ *
416
+ * @example
417
+ * ```ts
418
+ * import { requireContained } from '@orkestrel/test/server'
419
+ *
420
+ * requireContained('/scratch', 'nested/file.txt') // '/scratch/nested/file.txt'
421
+ *
422
+ * // Throws Error: Path outside scratch directory: ../escape.ts
423
+ * requireContained('/scratch', '../escape.ts')
424
+ * ```
285
425
  */
286
426
  export declare function requireContained(root: string, target: string): string;
287
427
 
@@ -387,6 +527,7 @@ export declare interface ScratchInterface {
387
527
  * Removes the allocated directory and everything in it when its identity still matches.
388
528
  *
389
529
  * @throws When the host refuses to inspect or remove the matching allocation.
530
+ * @remarks Idempotent. An identity that no longer matches removes nothing.
390
531
  */
391
532
  destroy(): void;
392
533
  }
@@ -422,6 +563,13 @@ export declare interface ScratchOptions {
422
563
  * @remarks Byte `0x80` is an invalid UTF-8 lead byte. POSIX stores the name verbatim and Windows
423
564
  * rejects it with `ENOENT`, so the answer is true on POSIX and false on Windows. The path is passed
424
565
  * as a `Buffer` because the byte survives no string round trip.
566
+ *
567
+ * @example
568
+ * ```ts
569
+ * import { supportsBytes } from '@orkestrel/test/server'
570
+ *
571
+ * supportsBytes() // true on a POSIX host, false on Windows
572
+ * ```
425
573
  */
426
574
  export declare function supportsBytes(): boolean;
427
575
 
@@ -437,6 +585,13 @@ export declare function supportsBytes(): boolean;
437
585
  * routes the second write onto the first entry, so reading the first back returns the second's
438
586
  * contents and the answer is false. The answer is true on a typical POSIX host and false on a
439
587
  * case-folding Windows or macOS volume.
588
+ *
589
+ * @example
590
+ * ```ts
591
+ * import { supportsCase } from '@orkestrel/test/server'
592
+ *
593
+ * supportsCase() // true on a case-sensitive volume, false on a case-folding one
594
+ * ```
440
595
  */
441
596
  export declare function supportsCase(): boolean;
442
597
 
@@ -452,6 +607,13 @@ export declare function supportsCase(): boolean;
452
607
  * The answer is false on a filesystem carrying neither reparse points nor symbolic links. Every call
453
608
  * probes and cleans up after itself, so a host whose answer changes is read again rather than
454
609
  * remembered.
610
+ *
611
+ * @example
612
+ * ```ts
613
+ * import { supportsDirectoryLinks } from '@orkestrel/test/server'
614
+ *
615
+ * supportsDirectoryLinks() // true where the host creates a symbolic link or a junction
616
+ * ```
455
617
  */
456
618
  export declare function supportsDirectoryLinks(): boolean;
457
619
 
@@ -483,6 +645,13 @@ export declare function supportsFileLinks(): boolean;
483
645
  * it: a POSIX host running as uid `0` stores every bit faithfully and bypasses the access check the
484
646
  * bits describe, so a caller that needs a permission to be enforced probes the refusal it needs
485
647
  * rather than reading this.
648
+ *
649
+ * @example
650
+ * ```ts
651
+ * import { supportsMode } from '@orkestrel/test/server'
652
+ *
653
+ * supportsMode() // true on a POSIX host, false on Windows
654
+ * ```
486
655
  */
487
656
  export declare function supportsMode(): boolean;
488
657
 
@@ -531,6 +700,20 @@ export declare type UpgradeResult = {
531
700
  * every other error ends the wait. The interval is validated for consistency with the wait family but
532
701
  * is not used, because this helper parks on the socket's events. Both listeners are removed on every
533
702
  * settlement, so a caller may wait on one socket repeatedly.
703
+ *
704
+ * @example
705
+ * ```ts
706
+ * import { connect, createServer } from 'node:net'
707
+ * import { createLoopback, waitForSocketClose } from '@orkestrel/test/server'
708
+ *
709
+ * const loopback = await createLoopback(createServer((socket) => socket.end()))
710
+ * const client = connect(loopback.port, '127.0.0.1')
711
+ *
712
+ * await waitForSocketClose(client, { budget: 1000 }) // undefined
713
+ * client.destroyed // true
714
+ *
715
+ * await loopback.destroy()
716
+ * ```
534
717
  */
535
718
  export declare function waitForSocketClose(socket: Socket, options?: WaitOptions): Promise<void>;
536
719