@orkestrel/test 0.0.7 → 0.0.9

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,4 +1,56 @@
1
1
  import { Server } from 'node:net';
2
+ import { Socket } from 'node:net';
3
+ import { WaitOptions } from '@orkestrel/test';
4
+
5
+ /** A name-keyed cookie store a test drives one origin with, filled from real responses. */
6
+ export declare interface CookieJarInterface {
7
+ /**
8
+ * The `Cookie` request header naming every stored cookie, or `undefined` while the jar holds none.
9
+ */
10
+ readonly header: string | undefined;
11
+ /**
12
+ * Reads one stored cookie value.
13
+ *
14
+ * @param name - The cookie name.
15
+ * @returns The stored value, or `undefined` when the jar holds no cookie of that name.
16
+ */
17
+ read(name: string): string | undefined;
18
+ /**
19
+ * Applies every `Set-Cookie` field a response carries.
20
+ *
21
+ * @param response - The response whose `Set-Cookie` fields are applied.
22
+ * @returns Those fields unmodified, in the order the response carried them.
23
+ * @remarks Selection is by name alone. A field spelling `Max-Age=0` deletes its cookie and every
24
+ * other field stores or replaces one, so `Domain`, `Path`, `Expires`, and `Secure` are read past
25
+ * rather than honoured. Nothing outlives the jar.
26
+ */
27
+ capture(response: Response): readonly string[];
28
+ }
29
+
30
+ /**
31
+ * Creates a cookie jar that records a real response's cookies and replays them as one header.
32
+ *
33
+ * @returns The rendered request header, and the members that read and capture cookies.
34
+ * @remarks Selection is by name alone: no `Domain` or `Path` matching, no `Expires` or `Secure`
35
+ * handling, and no persistence beyond the jar. That is what a test driving one origin over one path
36
+ * needs, and a fixture needing a browser's cookie store needs a browser rather than this.
37
+ */
38
+ export declare function createCookieJar(): CookieJarInterface;
39
+
40
+ /**
41
+ * Creates a symbolic link with a directory-junction fallback for hosts that refuse symbolic links.
42
+ *
43
+ * @param path - The path where the link is created.
44
+ * @param source - The destination path the link points at.
45
+ * @throws The original link error when its code is not `EPERM`, or when the source names an
46
+ * existing non-directory; otherwise, any error from inspecting the source or creating the junction.
47
+ * @remarks Only `EPERM` from the first symbolic-link attempt triggers the fallback. The fallback
48
+ * resolves the source against the link's directory. An existing non-directory rethrows the original
49
+ * `EPERM`, while a directory or missing source is passed to a junction attempt. A missing source is
50
+ * accepted to create a dangling junction. Where the host creates a junction, its stored value is the
51
+ * resolved absolute path.
52
+ */
53
+ export declare function createLink(path: string, source: string): void;
2
54
 
3
55
  /**
4
56
  * Starts a server on an ephemeral IPv4 loopback port.
@@ -21,6 +73,33 @@ export declare function createLoopback(server: Server): Promise<LoopbackInterfac
21
73
  */
22
74
  export declare function createScratch(options?: ScratchOptions): ScratchInterface;
23
75
 
76
+ /**
77
+ * Destroys a scratch directory, retrying until the host releases it.
78
+ *
79
+ * @param scratch - The scratch directory to destroy.
80
+ * @param options - The time bounds and abort signal.
81
+ * @returns A promise that resolves once `destroy()` returns without throwing.
82
+ * @throws The abort reason, or an `Error` when a bound is invalid or the budget elapses. The
83
+ * exhaustion error carries the last host refusal as its `cause`.
84
+ * @remarks Default budget: `10000` milliseconds. Default interval: `25` milliseconds. A host holds a
85
+ * directory for a short interval after the process that held it exits, and a just-stopped child's
86
+ * working directory is the case this exists for, so removal is attempted until the host lets go
87
+ * rather than exactly once. {@link ScratchInterface.destroy} stays synchronous and is unchanged; this
88
+ * is the bounded retry around it. A directory nothing releases still fails, with the host's own
89
+ * refusal as the `cause`.
90
+ *
91
+ * Every refusal is retried, deliberately, and that is wider than {@link removeTree}'s policy: that
92
+ * one retries the codes {@link REMOVE_TREE_RETRYABLE_CODES} names and rethrows the rest at once.
93
+ * The hold this waits out is not classifiable across hosts — Windows reports a working-directory
94
+ * hold as `EPERM`, POSIX hosts and network filesystems report their own — so a code list here would
95
+ * be a list of the hosts it had been run on. The residual is the cost of that: a fault no wait can
96
+ * clear, such as a path removed from under the allocation or a permission the process never had,
97
+ * spends the whole budget before it surfaces, and it surfaces wrapped in the exhaustion error with
98
+ * the host's refusal as `cause` rather than by identity. Pass a shorter `budget` or a `signal`
99
+ * wherever a caller must bound that cost.
100
+ */
101
+ export declare function destroyScratch(scratch: ScratchInterface, options?: WaitOptions): Promise<void>;
102
+
24
103
  /** Options for reading a source inventory. */
25
104
  export declare interface InventoryOptions {
26
105
  /** The file extensions to include, each written with its leading dot. */
@@ -41,6 +120,25 @@ export declare interface InventoryOptions {
41
120
  */
42
121
  export declare function isExcluded(key: string, exclusions: readonly string[]): boolean;
43
122
 
123
+ /**
124
+ * Reports whether a process id names a live process.
125
+ *
126
+ * @param pid - The process id to read.
127
+ * @returns True if a process holds that id at the moment of the call; false otherwise, including a
128
+ * pid the host refuses.
129
+ * @throws Nothing. Every host refusal reads as false.
130
+ * @remarks This is an instantaneous observation rather than a claim of ownership. A host reuses a
131
+ * process id after the process holding it exits, so a true answer says some process holds that id now
132
+ * and never says it is the process the caller started. Two host answers are worth knowing. A POSIX
133
+ * host refuses signal `0` to a process another user owns with `EPERM`, and that refusal reads as
134
+ * false here. A pid of `0` names the caller's own process group on POSIX and the system idle process
135
+ * on Windows, so it reads as true on both without naming a process anyone started.
136
+ *
137
+ * A Linux zombie — a process that has exited and whose parent has not reaped it — still accepts
138
+ * signal `0`, so its `/proc` status is read and a `Z` state reads as false.
139
+ */
140
+ export declare function isRunning(pid: number): boolean;
141
+
44
142
  /** A server a test owns, listening on an ephemeral loopback port until the test releases it. */
45
143
  export declare interface LoopbackInterface {
46
144
  /**
@@ -112,10 +210,44 @@ export declare const REMOVE_TREE_RETRYABLE_CODES: readonly string[];
112
210
  * `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed
113
211
  * against a real held directory, they neither delay nor retry before rethrowing, so the retry
114
212
  * is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait
115
- * at roughly one second.
213
+ * at roughly one second. A hold that outlasts that second is {@link destroyScratch}'s case, which
214
+ * retries every refusal inside a caller's budget rather than the codes named here.
116
215
  */
117
216
  export declare function removeTree(path: string): void;
118
217
 
218
+ /**
219
+ * Drives a real client upgrade request against a loopback port and reports what the server did.
220
+ *
221
+ * @param port - The port the server listens on at `127.0.0.1`.
222
+ * @param options - Optional request path, offered subprotocols, time bounds, and abort signal.
223
+ * @returns A promise resolving to the server's answer: a claimed upgrade with the protocol it
224
+ * selected, or a refusal with the status it answered.
225
+ * @throws The client's own transport error, such as the `ECONNREFUSED` a closed port answers, the
226
+ * abort reason, or an `Error` when a bound is invalid or the server does not answer within the
227
+ * budget.
228
+ * @remarks Default budget: `1000` milliseconds. The request carries `Connection: Upgrade` and
229
+ * `Upgrade: websocket`, which is what makes a server's `upgrade` handler the one that answers it.
230
+ * The `upgrade`, `response`, and `error` events are mutually exclusive in practice and the promise
231
+ * settles on whichever arrives first, so a second event changes nothing. The client socket is
232
+ * destroyed before every settlement, on the claimed path because an upgraded socket is detached from
233
+ * the request and outlives it otherwise. The request is made with no agent, so no pooled connection
234
+ * survives the call to keep a suite's event loop alive.
235
+ *
236
+ * A server that accepts the connection and answers nothing raises no transport error, so the budget
237
+ * is what ends that call: the rejection names the port and path it was waiting on. The interval is
238
+ * validated for consistency with the wait family but is not used, because this helper parks on the
239
+ * request's events.
240
+ *
241
+ * A `101` is the claimed path's status on the wire and is deliberately not reported: `status` is the
242
+ * refused arm's member, and a claimed upgrade produced no plain answer.
243
+ * @example
244
+ * ```ts
245
+ * const answer = await requestUpgrade(loopback.port, { path: '/socket', protocols: ['chat'] })
246
+ * // { claimed: true, protocol: 'chat' }
247
+ * ```
248
+ */
249
+ export declare function requestUpgrade(port: number, options?: UpgradeOptions): Promise<UpgradeResult>;
250
+
119
251
  /**
120
252
  * Resolves a target that stays below a root directory.
121
253
  *
@@ -192,10 +324,13 @@ export declare interface ScratchInterface {
192
324
  * @param target - The relative or absolute contained path where the link is created. Unlike
193
325
  * `node:fs`'s `symlinkSync(target, path)` vocabulary, this interface consistently calls the
194
326
  * contained path the target.
195
- * @param source - The link text naming the pointed-at path. It may name a path outside the scratch
196
- * directory and is not containment-checked.
327
+ * @param source - The destination path the link points at. The stored value is a path naming that
328
+ * destination, but its exact text is not promised. The path may name a destination outside the
329
+ * scratch directory and is not containment-checked.
197
330
  * @throws When the target escapes the scratch directory, the scratch root is missing, a symbolic
198
- * link, or a file, or the host refuses to create the link.
331
+ * link, or a file, or the host refuses to create the link, including a host that creates no
332
+ * symbolic link when the source names an existing non-directory.
333
+ * @remarks {@link createLink} owns the host-specific link mechanism.
199
334
  */
200
335
  link(target: string, source: string): void;
201
336
  /**
@@ -237,4 +372,126 @@ export declare interface ScratchOptions {
237
372
  readonly files?: Readonly<Record<string, string>>;
238
373
  }
239
374
 
375
+ /**
376
+ * Checks whether this host accepts a filename carrying a raw byte no UTF-8 decoder resolves.
377
+ *
378
+ * @returns True if a name ending in byte `0x80` is written and read back; false otherwise, including
379
+ * every host refusal.
380
+ * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
381
+ * remove it afterwards, both propagate.
382
+ * @remarks Byte `0x80` is an invalid UTF-8 lead byte. POSIX stores the name verbatim and Windows
383
+ * rejects it with `ENOENT`, so the answer is true on POSIX and false on Windows. The path is passed
384
+ * as a `Buffer` because the byte survives no string round trip.
385
+ */
386
+ export declare function supportsBytes(): boolean;
387
+
388
+ /**
389
+ * Checks whether this host treats two names differing only by case as distinct files.
390
+ *
391
+ * @returns True if `A` and `a` hold the contents each was written with; false otherwise, including
392
+ * every host refusal.
393
+ * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
394
+ * remove it afterwards, both propagate.
395
+ * @remarks The names `A` and `a` differ by case and by nothing else, which is what makes the reading
396
+ * an answer about case folding rather than an answer about two unrelated files. A case-folding volume
397
+ * routes the second write onto the first entry, so reading the first back returns the second's
398
+ * contents and the answer is false. The answer is true on a typical POSIX host and false on a
399
+ * case-folding Windows or macOS volume.
400
+ */
401
+ export declare function supportsCase(): boolean;
402
+
403
+ /**
404
+ * Checks whether this host links a directory, by creating one link and reading through it.
405
+ *
406
+ * @returns True if the created link reports as a symbolic link, resolves to a directory, and reaches
407
+ * the destination's contents; false otherwise, including every host refusal.
408
+ * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
409
+ * remove it afterwards, both propagate.
410
+ * @remarks `symlinkSync(source, target, 'junction')` creates a directory junction on Windows, which
411
+ * needs no privilege, and Node ignores the type argument off Windows, so one call covers both hosts.
412
+ * The answer is false on a filesystem carrying neither reparse points nor symbolic links. Every call
413
+ * probes and cleans up after itself, so a host whose answer changes is read again rather than
414
+ * remembered.
415
+ */
416
+ export declare function supportsDirectoryLinks(): boolean;
417
+
418
+ /**
419
+ * Checks whether this host links a file, by creating one link and reading the file through it.
420
+ *
421
+ * @returns True if the file's contents are readable through the link; false otherwise, including
422
+ * every host refusal.
423
+ * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
424
+ * remove it afterwards, both propagate.
425
+ * @remarks `symlinkSync(source, target, 'file')` needs the symbolic-link privilege, which Windows
426
+ * grants under Developer Mode or administrator rights and refuses with `EPERM` otherwise, so the
427
+ * answer is true on POSIX and on a privileged Windows host. Where it is false, no mechanism reaches a
428
+ * file through a link and a proof that reads one back cannot run. This is a separate question from
429
+ * {@link supportsDirectoryLinks}, which an unprivileged Windows host answers true through a junction
430
+ * while answering this one false.
431
+ */
432
+ export declare function supportsFileLinks(): boolean;
433
+
434
+ /**
435
+ * Checks whether POSIX permission bits round-trip through this host's `chmod` and `stat`.
436
+ *
437
+ * @returns True if a directory created with mode `0o700` reports that mode back; false otherwise,
438
+ * including every host refusal.
439
+ * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to
440
+ * remove it afterwards, both propagate.
441
+ * @remarks POSIX reports `mode & 0o777 === 0o700` and Windows reports `0o666` regardless, so the
442
+ * answer is true on POSIX and false on Windows. Storing a bit is a narrower question than enforcing
443
+ * it: a POSIX host running as uid `0` stores every bit faithfully and bypasses the access check the
444
+ * bits describe, so a caller that needs a permission to be enforced probes the refusal it needs
445
+ * rather than reading this.
446
+ */
447
+ export declare function supportsMode(): boolean;
448
+
449
+ /**
450
+ * Options for driving a client upgrade request.
451
+ *
452
+ * @remarks The time bounds and abort signal bound the wait for the server's answer, so a server
453
+ * that accepts the connection and never answers ends the call rather than parking it.
454
+ */
455
+ export declare interface UpgradeOptions extends WaitOptions {
456
+ /** The request path, written with its leading slash. Defaults to `/`. */
457
+ readonly path?: string;
458
+ /**
459
+ * The subprotocol tokens the request offers. They are sent as one comma-separated
460
+ * `Sec-WebSocket-Protocol` field, and an empty or omitted list sends no field at all.
461
+ */
462
+ readonly protocols?: readonly string[];
463
+ }
464
+
465
+ /**
466
+ * What one server did with a client upgrade request.
467
+ *
468
+ * @remarks `claimed` is the discriminant. The claimed arm carries `protocol`, the subprotocol the
469
+ * server selected, which is `undefined` when it selected none; a claimed upgrade produced no plain
470
+ * answer, so it carries no status and the `101` on the wire is deliberately not reported as one.
471
+ * The refused arm carries `status`, the plain answer's status, and no subprotocol.
472
+ */
473
+ export declare type UpgradeResult = {
474
+ readonly claimed: true;
475
+ readonly protocol: string | undefined;
476
+ } | {
477
+ readonly claimed: false;
478
+ readonly status: number;
479
+ };
480
+
481
+ /**
482
+ * Waits for a socket to close, accepting a peer reset as a forced close.
483
+ *
484
+ * @param socket - The socket to wait on. One that has already closed resolves without listening.
485
+ * @param options - The time bounds and abort signal.
486
+ * @returns A promise that resolves when the socket emits `close`.
487
+ * @throws The socket's own error when its code is not `ECONNRESET`, the abort reason, or an `Error`
488
+ * when a bound is invalid or the socket does not close within the budget.
489
+ * @remarks Default budget: `1000` milliseconds. A reset is the peer forcing the connection down, and
490
+ * the socket still emits `close` afterwards, so `ECONNRESET` is waited past rather than raised while
491
+ * every other error ends the wait. The interval is validated for consistency with the wait family but
492
+ * is not used, because this helper parks on the socket's events. Both listeners are removed on every
493
+ * settlement, so a caller may wait on one socket repeatedly.
494
+ */
495
+ export declare function waitForSocketClose(socket: Socket, options?: WaitOptions): Promise<void>;
496
+
240
497
  export { }