@orkestrel/test 0.0.14 → 0.0.15

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 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/core/constants.ts","../../../src/core/validators.ts","../../../src/core/helpers.ts","../../../src/core/factories.ts"],"sourcesContent":["/**\n * Names the attributes a statechart harness publishes, keyed by the fact each one carries.\n *\n * @remarks\n * A harness renders its own table and a gate outside the page polls the rendered markup, so these\n * names are the whole contract between the two. `status`, `passed`, `failed`, and `total` belong on\n * the harness root, because a gate finds the harness by `status` and reads the tally from the same\n * element. `scenario` and `result` belong on each row, so a failing row is found by `result` and\n * named by `scenario`. `state` belongs on the element rendering the entity's current state.\n *\n * The values are the attribute names themselves, so a harness writes `setAttribute` against this map\n * and a gate writes `querySelector` against it, and neither spells a `data-statechart-*` string of\n * its own.\n *\n * @example\n * ```ts\n * harness.getAttribute(STATECHART_ATTRIBUTES.status) // 'passed'\n * ```\n */\nexport const STATECHART_ATTRIBUTES = Object.freeze({\n\tstatus: 'data-statechart-status',\n\tpassed: 'data-statechart-passed',\n\tfailed: 'data-statechart-failed',\n\ttotal: 'data-statechart-total',\n\tscenario: 'data-statechart-scenario',\n\tresult: 'data-statechart-result',\n\tstate: 'data-statechart-state',\n})\n\n/**\n * Lists every value a statechart harness reports through its `status` attribute.\n *\n * @remarks\n * `pending` is what a harness carries before a run has produced a result for every row, `idle` is a\n * harness standing ready with nothing running, and `running` is a run in flight. `passed` and\n * `failed` are the two terminal readings, so a gate waits for membership in that pair rather than\n * for a fixed duration.\n *\n * The tuple's order is the order a run passes through, and its element type is the literal union, so\n * `(typeof STATECHART_STATUSES)[number]` is the status type a harness and its gate share.\n *\n * @example\n * ```ts\n * const terminal = new Set<string>([STATECHART_STATUSES[3], STATECHART_STATUSES[4]])\n * ```\n */\nexport const STATECHART_STATUSES = Object.freeze([\n\t'pending',\n\t'idle',\n\t'running',\n\t'passed',\n\t'failed',\n] as const)\n","import type { RecorderMap } from './types.js'\n\n/**\n * Checks whether a value contains a recorder for every listed event.\n *\n * @typeParam TMap - The source's event names and delivered argument tuples.\n * @typeParam TName - The event names represented in the map.\n * @param value - The value to inspect.\n * @param events - The events the completed map must contain.\n * @returns True if every listed event has a structurally valid recorder; false otherwise.\n * @remarks Per-key tuple precision is the predicate's claim. The factory proves that claim by\n * wiring each recorder to exactly the event where it stores that recorder. A direct caller must\n * establish the same pairing before it relies on the narrowing. This guard takes the listed events\n * through a reference parameter rather than using the canonical single-value guard form.\n *\n * @example\n * ```ts\n * import { createRecorder, isRecorderMapComplete } from '@orkestrel/test'\n *\n * type ReadyEvents = { readonly ready: readonly [name: string, step: number] }\n *\n * const value: unknown = { ready: createRecorder<readonly [name: string, step: number]>() }\n *\n * isRecorderMapComplete<ReadyEvents, 'ready'>(value, ['ready']) // true\n * isRecorderMapComplete<ReadyEvents, 'ready'>({ ready: 1 }, ['ready']) // false\n * ```\n */\nexport function isRecorderMapComplete<\n\tTMap extends Record<string, readonly unknown[]>,\n\tTName extends keyof TMap,\n>(value: unknown, events: readonly TName[]): value is RecorderMap<TMap, TName> {\n\ttry {\n\t\tif (typeof value !== 'object' || value === null) return false\n\t\treturn events.every((event) => {\n\t\t\tif (!Object.hasOwn(value, event)) return false\n\t\t\tconst recorder = Reflect.get(value, event)\n\t\t\tif (typeof recorder !== 'object' || recorder === null) return false\n\t\t\treturn (\n\t\t\t\ttypeof Reflect.get(recorder, 'handler') === 'function' &&\n\t\t\t\tArray.isArray(Reflect.get(recorder, 'calls'))\n\t\t\t)\n\t\t})\n\t} catch {\n\t\treturn false\n\t}\n}\n","import type {\n\tEventSubscriber,\n\tHeadersSource,\n\tJSONSafe,\n\tResult,\n\tRetryOptions,\n\tSignalRegistration,\n\tStateScenario,\n\tWaitOptions,\n} from './types.js'\n\n/**\n * Checks the resolved bounds one bounded wait runs under.\n *\n * @param subject - The bound's owner, which opens each refusal message.\n * @param budget - The resolved elapsed-time limit in milliseconds.\n * @param interval - The resolved delay between readings in milliseconds.\n * @throws An `Error` reading `<subject> budget must be finite and non-negative` or\n * `<subject> interval must be finite and non-negative`.\n * @remarks Every member of the wait family resolves its own defaults first and passes the resolved\n * numbers here, so each keeps its own defaults while one contract states what a bound must be.\n *\n * @example\n * ```ts\n * import { checkBounds } from '@orkestrel/test'\n *\n * checkBounds('Wait', 1000, 10) // undefined\n *\n * // Throws Error: Retry budget must be finite and non-negative\n * checkBounds('Retry', -1, 10)\n * ```\n */\nexport function checkBounds(subject: string, budget: number, interval: number): void {\n\tif (!Number.isFinite(budget) || budget < 0) {\n\t\tthrow new Error(`${subject} budget must be finite and non-negative`)\n\t}\n\tif (!Number.isFinite(interval) || interval < 0) {\n\t\tthrow new Error(`${subject} interval must be finite and non-negative`)\n\t}\n}\n\n/**\n * Builds the error {@link retryUntil} raises when its elapsed-time budget runs out.\n *\n * @param description - The operation the retry was named for.\n * @param budget - The elapsed-time limit in milliseconds.\n * @param elapsed - The milliseconds the retry actually spent.\n * @param last - The rendered last unsatisfying value, or `undefined` when the retry produced none.\n * @param cause - The last producer error, which becomes the returned error's `cause`.\n * @returns The exhaustion error, unthrown.\n * @remarks Both of `retryUntil`'s elapsed checks raise this one message, so an edit to it lands in\n * one place. The rendered value is appended only when the retry produced one.\n *\n * @example\n * ```ts\n * import { buildRetryExhausted } from '@orkestrel/test'\n *\n * const exhausted = buildRetryExhausted('registry answers', 30, 31, '\"starting\"', undefined)\n *\n * exhausted.message\n * // 'Retry \"registry answers\" did not succeed within 30ms (waited 31ms) (last value: \"starting\")'\n * ```\n */\nexport function buildRetryExhausted(\n\tdescription: string,\n\tbudget: number,\n\telapsed: number,\n\tlast: string | undefined,\n\tcause: unknown,\n): Error {\n\treturn new Error(\n\t\t`Retry \"${description}\" did not succeed within ${budget}ms (waited ${elapsed}ms)${last === undefined ? '' : ` (last value: ${last})`}`,\n\t\t{ cause },\n\t)\n}\n\n/**\n * Drops the registration an instrumented signal installed for one listener.\n *\n * @param registrations - The live registration list, spliced in place.\n * @param installed - The installed listener naming the registration to drop.\n * @returns The dropped registration, or `undefined` when the list holds none for that listener.\n * @remarks The dropped registration's cleanup controller is aborted as it leaves, so the scope\n * subscription installed beside it leaves with it. Removing the installed listener from the signal\n * stays with the caller, because only the scope-abort path has one to remove.\n *\n * @example\n * ```ts\n * import type { SignalRegistration } from '@orkestrel/test'\n * import { dropRegistration } from '@orkestrel/test'\n *\n * const listener: EventListener = () => undefined\n * const installed: EventListenerObject = { handleEvent: () => undefined }\n * const cleanup = new AbortController()\n * const registrations: SignalRegistration[] = [[listener, installed, true, cleanup]]\n *\n * dropRegistration(registrations, installed)?.[1] === installed // true\n * cleanup.signal.aborted // true\n * dropRegistration(registrations, installed) // undefined\n * ```\n */\nexport function dropRegistration(\n\tregistrations: SignalRegistration[],\n\tinstalled: EventListener | EventListenerObject,\n): SignalRegistration | undefined {\n\tconst index = registrations.findIndex((registration) => registration[1] === installed)\n\tconst registration = registrations[index]\n\tif (registration === undefined) return undefined\n\tregistrations.splice(index, 1)\n\tregistration[3]?.abort()\n\treturn registration\n}\n\n/**\n * Waits for a host timer to elapse.\n *\n * @param ms - The delay in milliseconds. Default: `0`.\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 * Waits until an abort signal is aborted.\n *\n * @param signal - The signal to observe.\n * @returns A promise that resolves when the signal is aborted.\n * @remarks An already-aborted signal resolves immediately. Otherwise the wait parks on a one-shot\n * abort listener without a timer or polling.\n */\nexport function waitForAbort(signal: AbortSignal): Promise<void> {\n\tif (signal.aborted) return Promise.resolve()\n\treturn new Promise((resolve) => signal.addEventListener('abort', () => resolve(), { once: true }))\n}\n\n/**\n * Waits until a condition holds within an elapsed-time budget.\n *\n * @param description - The condition described in a timeout error.\n * @param condition - The synchronous or asynchronous condition to read.\n * @param options - The time bounds and abort signal.\n * @returns A promise that resolves when the condition first returns `true`.\n * @throws The condition's thrown value, the abort reason, or an `Error` when a bound is invalid or\n * the condition does not hold within the budget.\n * @remarks The first read is immediate. Default budget: `1000` milliseconds. Default interval: `10`\n * milliseconds.\n */\nexport async function waitForCondition(\n\tdescription: string,\n\tcondition: () => boolean | Promise<boolean>,\n\toptions?: WaitOptions,\n): Promise<void> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tcheckBounds('Wait', budget, interval)\n\n\tconst start = performance.now()\n\twhile (true) {\n\t\toptions?.signal?.throwIfAborted()\n\t\tconst held = await condition()\n\t\toptions?.signal?.throwIfAborted()\n\t\tif (held) return\n\n\t\tconst elapsed = performance.now() - start\n\t\tif (elapsed >= budget) {\n\t\t\tthrow new Error(\n\t\t\t\t`Condition \"${description}\" did not hold within ${budget}ms (waited ${elapsed}ms)`,\n\t\t\t)\n\t\t}\n\t\tawait waitForDelay(interval)\n\t}\n}\n\n/**\n * Repeats a producer until one produced value satisfies a predicate.\n *\n * @typeParam T - The produced value type.\n * @param description - The operation described in an exhaustion error.\n * @param produce - The synchronous or asynchronous operation to repeat.\n * @param satisfied - The predicate that accepts a produced value.\n * @param options - The time, attempt, and abort bounds.\n * @returns The first produced value the predicate accepts.\n * @throws The predicate's thrown value, the abort reason, or an `Error` when a bound is invalid or\n * the retry exhausts its budget or attempts.\n * @remarks A producer throw counts as an unsatisfied attempt. The last producer error becomes the\n * exhaustion error's `cause`. Default budget: `1000` milliseconds. Default interval: `10`\n * milliseconds.\n */\nexport async function retryUntil<T>(\n\tdescription: string,\n\tproduce: () => T | Promise<T>,\n\tsatisfied: (value: T) => boolean,\n\toptions?: RetryOptions,\n): Promise<T> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tconst attempts = options?.attempts\n\tcheckBounds('Retry', budget, interval)\n\tif (attempts !== undefined && (!Number.isInteger(attempts) || attempts < 1)) {\n\t\tthrow new Error('Retry attempts must be a positive integer')\n\t}\n\n\tconst start = performance.now()\n\tlet count = 0\n\tlet cause: unknown\n\tlet last: string | undefined\n\twhile (true) {\n\t\toptions?.signal?.throwIfAborted()\n\t\tif (count > 0) {\n\t\t\tconst elapsed = performance.now() - start\n\t\t\tif (elapsed >= budget) {\n\t\t\t\tthrow buildRetryExhausted(description, budget, elapsed, last, cause)\n\t\t\t}\n\t\t}\n\t\tlet produced: Result<T, unknown>\n\t\ttry {\n\t\t\tproduced = { success: true, value: await produce() }\n\t\t} catch (error) {\n\t\t\tproduced = { success: false, error }\n\t\t}\n\t\tcount += 1\n\t\toptions?.signal?.throwIfAborted()\n\n\t\tif (produced.success) {\n\t\t\tif (satisfied(produced.value)) return produced.value\n\t\t\tlet rendered: string\n\t\t\ttry {\n\t\t\t\tconst serialized = JSON.stringify(produced.value)\n\t\t\t\trendered = serialized === undefined ? String(produced.value) : serialized\n\t\t\t} catch {\n\t\t\t\ttry {\n\t\t\t\t\trendered = String(produced.value)\n\t\t\t\t} catch {\n\t\t\t\t\trendered = '[unrenderable]'\n\t\t\t\t}\n\t\t\t}\n\t\t\tlast = rendered.length > 200 ? `${rendered.slice(0, 197)}...` : rendered\n\t\t} else {\n\t\t\tcause = produced.error\n\t\t}\n\n\t\tconst elapsed = performance.now() - start\n\t\tif (elapsed >= budget) {\n\t\t\tthrow buildRetryExhausted(description, budget, elapsed, last, cause)\n\t\t}\n\t\tif (attempts !== undefined && count >= attempts) {\n\t\t\tthrow new Error(\n\t\t\t\t`Retry \"${description}\" did not succeed within ${attempts} attempts${last === undefined ? '' : ` (last value: ${last})`}`,\n\t\t\t\t{ cause },\n\t\t\t)\n\t\t}\n\t\tawait waitForDelay(Math.min(interval, budget - elapsed))\n\t}\n}\n\n/**\n * Invokes an unknown method through an explicit unchecked result contract.\n *\n * @typeParam T - The result type claimed by the caller.\n * @param target - The value used as the method's `this` argument.\n * @param method - The unknown method to invoke.\n * @param args - The arguments to pass.\n * @returns The method's result under the caller's claimed type.\n * @throws A `TypeError` when `method` is not callable.\n * @remarks The caller owns the claim that the returned value has type `T`. The contained `any`\n * bridges the unchecked runtime result to that caller-owned claim.\n */\nexport function invokeUnchecked<T>(target: unknown, method: unknown, args: readonly unknown[]): T {\n\tif (typeof method !== 'function') throw new TypeError('Method must be callable')\n\tconst result: T = Reflect.apply(method, target, args)\n\treturn result\n}\n\n/**\n * Reads a property from an unknown object or function.\n *\n * @typeParam T - The property type claimed by the caller.\n * @param target - The unknown value to read.\n * @param key - The property key to read.\n * @returns The property value under the caller's claimed type.\n * @throws A `TypeError` when `target` is neither an object nor a function.\n * @remarks The caller owns the claim that the returned value has type `T`. The contained `any`\n * bridges the unchecked runtime result to that caller-owned claim.\n */\nexport function readProperty<T>(target: unknown, key: PropertyKey): T {\n\tif ((typeof target !== 'object' || target === null) && typeof target !== 'function') {\n\t\tthrow new TypeError('Target must be an object or function')\n\t}\n\tconst result: T = Reflect.get(target, key)\n\treturn result\n}\n\n/**\n * Normalizes headers into a frozen plain record.\n *\n * @param init - The platform header initializer to normalize.\n * @returns A frozen record of normalized header names and values.\n * @remarks Normalization follows the host `Headers` implementation, including lowercased names and\n * combined values.\n */\nexport function flattenHeaders(init: HeadersSource): Readonly<Record<string, string>> {\n\treturn Object.freeze(Object.fromEntries(new Headers(init).entries()))\n}\n\n/**\n * Waits for the first delivery from an event subscription.\n *\n * @typeParam TArgs - The delivered argument tuple.\n * @param subscribe - The function that installs the event listener and may return its cleanup.\n * @param description - The event described in a timeout error.\n * @param options - The time bounds and abort signal.\n * @returns The first delivered argument tuple.\n * @throws The subscription's thrown value, the abort reason, or an `Error` when a bound is invalid\n * or the event is not delivered within the budget.\n * @remarks Default budget: `1000` milliseconds. The interval is validated for consistency with the\n * wait family but is not used because this helper parks on the event.\n */\nexport async function waitForEvent<TArgs extends readonly unknown[]>(\n\tsubscribe: EventSubscriber<TArgs>,\n\tdescription: string,\n\toptions?: WaitOptions,\n): Promise<TArgs> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tcheckBounds('Event', budget, interval)\n\n\tconst signal = options?.signal\n\tsignal?.throwIfAborted()\n\tconst delivery = Promise.withResolvers<TArgs>()\n\tconst controller = new AbortController()\n\tlet timeout: ReturnType<typeof setTimeout> | undefined\n\tconst pending: Array<Promise<TArgs>> = [\n\t\tdelivery.promise,\n\t\tnew Promise((_resolve, reject) => {\n\t\t\ttimeout = setTimeout(() => {\n\t\t\t\treject(new Error(`Event \"${description}\" was not delivered within ${budget}ms`))\n\t\t\t}, budget)\n\t\t}),\n\t]\n\tif (signal !== undefined) {\n\t\tpending.push(\n\t\t\tnew Promise((_resolve, reject) => {\n\t\t\t\tconst combined = AbortSignal.any([signal, controller.signal])\n\t\t\t\tcombined.addEventListener(\n\t\t\t\t\t'abort',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tif (signal.aborted) reject(signal.reason)\n\t\t\t\t\t},\n\t\t\t\t\t{ once: true },\n\t\t\t\t)\n\t\t\t}),\n\t\t)\n\t}\n\tconst result = Promise.race(pending)\n\tlet cleanup: (() => void) | void = undefined\n\ttry {\n\t\ttry {\n\t\t\tcleanup = subscribe((...args) => delivery.resolve(args))\n\t\t} catch (error) {\n\t\t\tdelivery.reject(error)\n\t\t}\n\t\treturn await result\n\t} finally {\n\t\tcontroller.abort()\n\t\tif (timeout !== undefined) clearTimeout(timeout)\n\t\tcleanup?.()\n\t}\n}\n\n/**\n * Decodes newline-delimited JSON values.\n *\n * @param text - The JSON Lines text to decode.\n * @returns The decoded values in physical-line order.\n * @throws An `Error` naming the malformed physical line, with the native `SyntaxError` as its\n * `cause`.\n * @remarks An empty line contributes no value, and a trailing carriage return is dropped before the\n * line is parsed, so text written with either line ending decodes the same.\n *\n * @example\n * ```ts\n * import { decodeJSONLines } from '@orkestrel/test'\n *\n * decodeJSONLines('{\"ready\":true}\\n7\\n') // [{ ready: true }, 7]\n *\n * // Throws Error: Invalid JSON on line 3\n * decodeJSONLines('{}\\n\\n{')\n * ```\n */\nexport function decodeJSONLines(text: string): readonly unknown[] {\n\tconst values: unknown[] = []\n\tfor (const [index, physical] of text.split('\\n').entries()) {\n\t\tconst line = physical.endsWith('\\r') ? physical.slice(0, -1) : physical\n\t\tif (line.length === 0) continue\n\t\ttry {\n\t\t\tconst value: unknown = JSON.parse(line)\n\t\t\tvalues.push(value)\n\t\t} catch (cause) {\n\t\t\tthrow new Error(`Invalid JSON on line ${index + 1}`, { cause })\n\t\t}\n\t}\n\treturn values\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 * Narrows a value away from `null` and `undefined`, throwing when it is absent.\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. Default: `'Value is required'`.\n * @returns The present value.\n * @throws An `Error` carrying `message` when the value is `null` or `undefined`.\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 copied value's type, which the copy keeps.\n * @param value - The value to copy, bounded by its own `JSONSafe` projection.\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. The bound intersects `JSONSafe<T>` rather than\n * constraining `T` to `JSONValue`, so an interface-typed value round-trips.\n */\nexport function roundTripJSON<T>(value: T & JSONSafe<T>): T {\n\tconst serialized = JSON.stringify(value, (_key, current) => {\n\t\tif (current === undefined || typeof current === 'function' || typeof current === 'symbol') {\n\t\t\tthrow new Error('JSON values must not contain undefined, functions, or symbols')\n\t\t}\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: unknown[] = [parsed]\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop()\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\n/**\n * Drives one statechart scenario through its arrange, act, and assert phases.\n *\n * @typeParam TState - The states the entity moves between.\n * @typeParam TEvent - The events the entity accepts.\n * @typeParam TContext - The fixture the phases drive.\n * @param scenario - The scenario to drive.\n * @param context - The fixture handed to each phase.\n * @returns A promise that resolves after the assert phase completes.\n * @throws An `Error` whose message opens with the transition's `name` and whose `cause` is the value\n * the failing phase threw.\n * @remarks Each phase is awaited before the next begins, so an asynchronous arrange settles before\n * the event is applied. The phases receive the transition's own parts: `arrange` receives `from`,\n * `act` receives `event`, and `assert` receives `to`.\n *\n * The row's name is prepended because a table's rows run under one test name, so a bare assertion\n * message says what failed and never which row. A thrown `Error` keeps its own message after the\n * name and arrives as the `cause`; anything else thrown is named by its type and arrives as the\n * `cause` unchanged.\n *\n * @example\n * ```ts\n * await executeScenario(scenarios[0], { disclosure: new Disclosure() })\n * ```\n */\nexport async function executeScenario<TState extends string, TEvent extends string, TContext>(\n\tscenario: StateScenario<TState, TEvent, TContext>,\n\tcontext: TContext,\n): Promise<void> {\n\tconst transition = scenario.transition\n\ttry {\n\t\tawait scenario.arrange(context, transition.from)\n\t\tawait scenario.act(context, transition.event)\n\t\tawait scenario.assert(context, transition.to)\n\t} catch (cause) {\n\t\tconst message =\n\t\t\tcause instanceof Error ? cause.message : `threw a non-error ${typeof cause} value`\n\t\tthrow new Error(`${transition.name}: ${message}`, { cause })\n\t}\n}\n\n/**\n * Drives a statechart table row by row, each row against a context of its own.\n *\n * @typeParam TState - The states the entity moves between.\n * @typeParam TEvent - The events the entity accepts.\n * @typeParam TContext - The fixture the phases drive.\n * @param scenarios - The table to drive, in the order it is written.\n * @param build - The fixture builder, called once per row and awaited when it returns a promise.\n * @returns A promise that resolves after the last row completes.\n * @throws An `Error` reading `<name>: build refused` when the row's builder throws or rejects, whose\n * `cause` is the value the builder refused with, or whatever {@link executeScenario} throws for the\n * first row whose phases fail. Either way the run stops at that row and the rows after it never\n * start.\n * @remarks The rows run one after another rather than together: a statechart's rows drive one\n * entity on one page, so a parallel run would have them arranging over each other. `build` receives\n * the row it is building for, which is what lets one table mix fixtures.\n *\n * A refusing builder is named for its row the way a failing phase is, because a table's rows build\n * under one test name too. The refusal itself arrives as the `cause`, by identity, so its own\n * message and stack survive the naming.\n *\n * @example\n * ```ts\n * await executeScenarios(SCENARIOS, () => ({ disclosure: new Disclosure() }))\n * ```\n */\nexport async function executeScenarios<TState extends string, TEvent extends string, TContext>(\n\tscenarios: ReadonlyArray<StateScenario<TState, TEvent, TContext>>,\n\tbuild: (scenario: StateScenario<TState, TEvent, TContext>) => TContext | Promise<TContext>,\n): Promise<void> {\n\tfor (const scenario of scenarios) {\n\t\tlet context: TContext\n\t\ttry {\n\t\t\tcontext = await build(scenario)\n\t\t} catch (cause) {\n\t\t\tthrow new Error(`${scenario.transition.name}: build refused`, { cause })\n\t\t}\n\t\tawait executeScenario(scenario, context)\n\t}\n}\n","import type {\n\tEventSourceInterface,\n\tRecorderInterface,\n\tRecorderMap,\n\tResourceFactoryInterface,\n\tSignalInterface,\n\tSignalRegistration,\n\tTeardownHandler,\n\tTeardownInterface,\n} from './types.js'\nimport { dropRegistration } from './helpers.js'\nimport { isRecorderMapComplete } from './validators.js'\n\n/**\n * Creates values that make common object readers throw or violate their assumptions.\n *\n * @returns A frozen array whose values are fresh on every call.\n * @remarks Every member makes a naive read throw or violates a naive structural assumption. A total\n * guard survives every member without throwing. Whether it accepts or refuses one is that guard's\n * own contract. Membership may grow in a release, so test the whole returned set in a loop and\n * include the index in each failure.\n *\n * - The self-referential record makes JSON record serialization throw.\n * - The revoked object proxy makes reflective object access throw.\n * - The property proxy makes a named property read throw.\n * - The key proxy makes key enumeration throw.\n * - The prototype proxy makes a prototype read throw.\n * - The null-prototype record breaks a direct `hasOwnProperty` call.\n * - The array-target proxy passes an array check and makes an index read throw.\n * - The self-referential array makes JSON collection serialization throw.\n * - The sparse array violates the assumption that every index is enumerable.\n * - The hidden-key record violates the assumption that every own key is enumerable.\n * - The named getter makes its property read throw.\n * @example\n * ```ts\n * import { expect } from 'vitest'\n * import { createHostileValues } from '@orkestrel/test'\n *\n * function isWireRecord(value: unknown): value is Readonly<Record<string, string>> {\n * \tif (typeof value !== 'object' || value === null) return false\n * \ttry {\n * \t\tif (Object.getPrototypeOf(value) !== Object.prototype) return false\n * \t\tReflect.get(value, 'value')\n * \t\tif (Reflect.ownKeys(value).length === 0) return false\n * \t\treturn Object.values(value).every((member) => typeof member === 'string')\n * \t} catch {\n * \t\treturn false\n * \t}\n * }\n *\n * for (const [index, value] of createHostileValues().entries()) {\n * \tlet accepted: boolean | undefined\n * \texpect(() => {\n * \t\taccepted = isWireRecord(value)\n * \t}, `hostile value ${index}`).not.toThrow()\n * \texpect(accepted, `hostile value ${index}`).toBe(false)\n * }\n * ```\n */\nexport function createHostileValues(): readonly unknown[] {\n\tconst cyclic: Record<string, unknown> = {}\n\tcyclic.self = cyclic\n\tconst revoked = Proxy.revocable({}, {})\n\trevoked.revoke()\n\tconst cyclicArray: unknown[] = []\n\tcyclicArray.push(cyclicArray)\n\tconst sparseArray = Array<unknown>(2)\n\tsparseArray[1] = 'present'\n\tconst hidden = {}\n\tObject.defineProperty(hidden, 'hidden', { value: true })\n\tReflect.set(hidden, 'self', hidden)\n\tconst getter = {}\n\tObject.defineProperty(getter, 'danger', {\n\t\tenumerable: true,\n\t\tget() {\n\t\t\tthrow new Error('Hostile named getter read')\n\t\t},\n\t})\n\n\treturn Object.freeze([\n\t\tcyclic,\n\t\trevoked.proxy,\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\tget() {\n\t\t\t\t\tthrow new Error('Hostile property read')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\townKeys() {\n\t\t\t\t\tthrow new Error('Hostile key enumeration')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\tgetPrototypeOf() {\n\t\t\t\t\tthrow new Error('Hostile prototype read')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tObject.create(null),\n\t\tnew Proxy([], {\n\t\t\tget() {\n\t\t\t\tthrow new Error('Hostile array index read')\n\t\t\t},\n\t\t}),\n\t\tcyclicArray,\n\t\tsparseArray,\n\t\thidden,\n\t\tgetter,\n\t])\n}\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<\n\tTArgs extends readonly unknown[] = readonly unknown[],\n>(): 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\n/**\n * Creates event recorders and subscribes them to the source.\n *\n * @typeParam TMap - The source's event names and delivered argument tuples.\n * @typeParam TName - The requested event names.\n * @param source - The source to subscribe to.\n * @param events - The events to record.\n * @returns A map from each requested event name to its recorder.\n * @throws Thrown when a listed event has no recorder, which a well-formed events array cannot\n * produce.\n * @remarks A duplicate event name installs a fresh recorder for every occurrence. The returned map\n * keeps the recorder installed for the last occurrence. `TName` derives from the array's element\n * type. An array declared with a wider union than its contents widens `TName` beyond the listed\n * events. The omitted key reads `undefined` at runtime under a non-optional type, and the guard\n * reports `true` because it checks the listed events. Pass a literal array or a tuple.\n */\nexport function createRecorders<\n\tTMap extends Record<string, readonly unknown[]>,\n\tTName extends keyof TMap,\n>(source: EventSourceInterface<TMap>, events: readonly TName[]): RecorderMap<TMap, TName> {\n\tconst building: { -readonly [K in TName]?: RecorderInterface<TMap[TName]> } = {}\n\tfor (const event of events) {\n\t\tconst recorder = createRecorder<TMap[TName]>()\n\t\tsource.on(event, recorder.handler)\n\t\tbuilding[event] = recorder\n\t}\n\tif (!isRecorderMapComplete<TMap, TName>(building, events)) {\n\t\tthrow new Error('Emitter recorder map is incomplete')\n\t}\n\treturn building\n}\n\n/**\n * Creates a real abort controller whose signal reports its live abort listeners.\n *\n * @returns The controller, its instrumented signal, and the current listener tally.\n * @remarks Instrumentation is installed on the created signal instance. A one-shot listener leaves\n * the tally when it fires, and removal accepts the original listener supplied by the caller. A\n * listener scoped by another signal leaves when that signal aborts. An already-aborted scope\n * installs and records nothing.\n */\nexport function createSignal(): SignalInterface {\n\tconst controller = new AbortController()\n\tconst signal = controller.signal\n\tconst add = signal.addEventListener.bind(signal)\n\tconst remove = signal.removeEventListener.bind(signal)\n\tconst registrations: SignalRegistration[] = []\n\n\tObject.defineProperty(signal, 'addEventListener', {\n\t\tconfigurable: true,\n\t\tvalue(\n\t\t\ttype: string,\n\t\t\tlistener: EventListener | EventListenerObject | null,\n\t\t\toptions?: boolean | AddEventListenerOptions,\n\t\t) {\n\t\t\tif (listener === null) return\n\t\t\tif (type !== 'abort') {\n\t\t\t\tadd(type, listener, options)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst capture = typeof options === 'boolean' ? options : (options?.capture ?? false)\n\t\t\tconst scope = typeof options === 'object' ? options?.signal : undefined\n\t\t\tif (scope?.aborted === true) return\n\t\t\tif (\n\t\t\t\tregistrations.some(\n\t\t\t\t\t(registration) => registration[0] === listener && registration[2] === capture,\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst once = typeof options === 'object' && options?.once === true\n\t\t\tconst cleanup = scope === undefined ? undefined : new AbortController()\n\t\t\tconst installed: EventListenerObject = {\n\t\t\t\thandleEvent(event) {\n\t\t\t\t\tif (once) dropRegistration(registrations, installed)\n\t\t\t\t\tif (typeof listener === 'function') listener.call(signal, event)\n\t\t\t\t\telse listener.handleEvent(event)\n\t\t\t\t},\n\t\t\t}\n\t\t\tadd(\n\t\t\t\ttype,\n\t\t\t\tinstalled,\n\t\t\t\ttypeof options === 'object'\n\t\t\t\t\t? options?.passive === undefined\n\t\t\t\t\t\t? { capture, once }\n\t\t\t\t\t\t: { capture, once, passive: options.passive }\n\t\t\t\t\t: options,\n\t\t\t)\n\t\t\tregistrations.push([listener, installed, capture, cleanup])\n\t\t\tif (scope !== undefined && cleanup !== undefined) {\n\t\t\t\tscope.addEventListener(\n\t\t\t\t\t'abort',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tconst registration = dropRegistration(registrations, installed)\n\t\t\t\t\t\tif (registration === undefined) return\n\t\t\t\t\t\tremove(type, registration[1], { capture })\n\t\t\t\t\t},\n\t\t\t\t\t{ once: true, signal: cleanup.signal },\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\t})\n\tObject.defineProperty(signal, 'removeEventListener', {\n\t\tconfigurable: true,\n\t\tvalue(\n\t\t\ttype: string,\n\t\t\tlistener: EventListener | EventListenerObject | null,\n\t\t\toptions?: boolean | EventListenerOptions,\n\t\t) {\n\t\t\tif (listener === null) return\n\t\t\tif (type !== 'abort') {\n\t\t\t\tremove(type, listener, options)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst capture = typeof options === 'boolean' ? options : (options?.capture ?? false)\n\t\t\tconst index = registrations.findIndex(\n\t\t\t\t(registration) => registration[0] === listener && registration[2] === capture,\n\t\t\t)\n\t\t\tconst registration = registrations[index]\n\t\t\tif (registration === undefined) {\n\t\t\t\tremove(type, listener, options)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tregistrations.splice(index, 1)\n\t\t\tregistration[3]?.abort()\n\t\t\tremove(type, registration[1], options)\n\t\t},\n\t})\n\n\treturn {\n\t\tcontroller,\n\t\tsignal,\n\t\tget count() {\n\t\t\treturn registrations.length\n\t\t},\n\t}\n}\n\n/**\n * Creates a monotonically numbered resource factory with creation and destruction records.\n *\n * @returns A resource factory whose recorders retain every affected id in order.\n */\nexport function createResourceFactory(): ResourceFactoryInterface {\n\tconst created = createRecorder<readonly [id: number]>()\n\tconst destroyed = createRecorder<readonly [id: number]>()\n\treturn {\n\t\tcreated,\n\t\tdestroyed,\n\t\tcreate() {\n\t\t\tconst id = created.calls.length + 1\n\t\t\tcreated.handler(id)\n\t\t\treturn id\n\t\t},\n\t\tdestroy(id) {\n\t\t\tdestroyed.handler(id)\n\t\t},\n\t}\n}\n\n/**\n * Creates a teardown list that runs registered handlers newest-first.\n *\n * @returns A teardown list that awaits every handler and collects failures.\n */\nexport function createTeardown(): TeardownInterface {\n\tlet handlers: TeardownHandler[] = []\n\treturn {\n\t\tget count() {\n\t\t\treturn handlers.length\n\t\t},\n\t\tadd(handler) {\n\t\t\thandlers.push(handler)\n\t\t},\n\t\tasync destroy() {\n\t\t\tconst snapshot = handlers\n\t\t\thandlers = []\n\t\t\tconst failures: unknown[] = []\n\t\t\tfor (const handler of snapshot.reverse()) {\n\t\t\t\ttry {\n\t\t\t\t\tawait handler()\n\t\t\t\t} catch (error) {\n\t\t\t\t\tfailures.push(error)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (failures.length === 1) throw failures[0]\n\t\t\tif (failures.length > 1) throw new AggregateError(failures)\n\t\t},\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/core/constants.ts","../../../src/core/validators.ts","../../../src/core/helpers.ts","../../../src/core/factories.ts"],"sourcesContent":["/**\n * Names the attributes a statechart harness publishes, keyed by the fact each one carries.\n *\n * @remarks\n * A harness renders its own table and a gate outside the page polls the rendered markup, so these\n * names are the whole contract between the two. `status`, `passed`, `failed`, and `total` belong on\n * the harness root, because a gate finds the harness by `status` and reads the tally from the same\n * element. `scenario` and `result` belong on each row, so a failing row is found by `result` and\n * named by `scenario`. `state` belongs on the element rendering the entity's current state.\n *\n * The values are the attribute names themselves, so a harness writes `setAttribute` against this map\n * and a gate writes `querySelector` against it, and neither spells a `data-statechart-*` string of\n * its own.\n *\n * @example\n * ```ts\n * harness.getAttribute(STATECHART_ATTRIBUTES.status) // 'passed'\n * ```\n */\nexport const STATECHART_ATTRIBUTES = Object.freeze({\n\tstatus: 'data-statechart-status',\n\tpassed: 'data-statechart-passed',\n\tfailed: 'data-statechart-failed',\n\ttotal: 'data-statechart-total',\n\tscenario: 'data-statechart-scenario',\n\tresult: 'data-statechart-result',\n\tstate: 'data-statechart-state',\n})\n\n/**\n * Lists every value a statechart harness reports through its `status` attribute.\n *\n * @remarks\n * `pending` is what a harness carries before a run has produced a result for every row, `idle` is a\n * harness standing ready with nothing running, and `running` is a run in flight. `passed` and\n * `failed` are the two terminal readings, so a gate waits for membership in that pair rather than\n * for a fixed duration.\n *\n * The tuple's order is the order a run passes through, and its element type is the literal union, so\n * `(typeof STATECHART_STATUSES)[number]` is the status type a harness and its gate share.\n *\n * @example\n * ```ts\n * const terminal = new Set<string>([STATECHART_STATUSES[3], STATECHART_STATUSES[4]])\n * ```\n */\nexport const STATECHART_STATUSES = Object.freeze([\n\t'pending',\n\t'idle',\n\t'running',\n\t'passed',\n\t'failed',\n] as const)\n","import type { RecorderMap } from './types.js'\nimport { holds, isArray, isFunction, isObject } from '@orkestrel/contract'\n\n/**\n * Checks whether a value contains a recorder for every listed event.\n *\n * @typeParam TMap - The source's event names and delivered argument tuples.\n * @typeParam TName - The event names represented in the map.\n * @param value - The value to inspect.\n * @param events - The events the completed map must contain.\n * @returns True if every listed event has a structurally valid recorder; false otherwise.\n * @remarks Per-key tuple precision is the predicate's claim. The factory proves that claim by\n * wiring each recorder to exactly the event where it stores that recorder. A direct caller must\n * establish the same pairing before it relies on the narrowing. This guard takes the listed events\n * through a reference parameter rather than using the canonical single-value guard form.\n *\n * @example\n * ```ts\n * import { createRecorder, isRecorderMapComplete } from '@orkestrel/test'\n *\n * type ReadyEvents = { readonly ready: readonly [name: string, step: number] }\n *\n * const value: unknown = { ready: createRecorder<readonly [name: string, step: number]>() }\n *\n * isRecorderMapComplete<ReadyEvents, 'ready'>(value, ['ready']) // true\n * isRecorderMapComplete<ReadyEvents, 'ready'>({ ready: 1 }, ['ready']) // false\n * ```\n */\nexport function isRecorderMapComplete<\n\tTMap extends Record<string, readonly unknown[]>,\n\tTName extends keyof TMap,\n>(value: unknown, events: readonly TName[]): value is RecorderMap<TMap, TName> {\n\treturn holds(() => {\n\t\tif (!isObject(value)) return false\n\t\treturn events.every((event) => {\n\t\t\tif (!Object.hasOwn(value, event)) return false\n\t\t\tconst recorder = Reflect.get(value, event)\n\t\t\tif (!isObject(recorder)) return false\n\t\t\treturn isFunction(Reflect.get(recorder, 'handler')) && isArray(Reflect.get(recorder, 'calls'))\n\t\t})\n\t})\n}\n","import type { Result } from '@orkestrel/contract'\nimport type {\n\tEventSubscriber,\n\tHeadersSource,\n\tJSONSafe,\n\tRetryOptions,\n\tSignalRegistration,\n\tStateScenario,\n\tWaitOptions,\n} from './types.js'\nimport {\n\tattempt,\n\tisArray,\n\tisDefined,\n\tisError,\n\tisFiniteNumber,\n\tisFunction,\n\tisInteger,\n\tisNumber,\n\tisObject,\n\tisSymbol,\n} from '@orkestrel/contract'\n\n/**\n * Checks the resolved bounds one bounded wait runs under.\n *\n * @param subject - The bound's owner, which opens each refusal message.\n * @param budget - The resolved elapsed-time limit in milliseconds.\n * @param interval - The resolved delay between readings in milliseconds.\n * @throws An `Error` reading `<subject> budget must be finite and non-negative` or\n * `<subject> interval must be finite and non-negative`.\n * @remarks Every member of the wait family resolves its own defaults first and passes the resolved\n * numbers here, so each keeps its own defaults while one contract states what a bound must be.\n *\n * @example\n * ```ts\n * import { checkBounds } from '@orkestrel/test'\n *\n * checkBounds('Wait', 1000, 10) // undefined\n *\n * // Throws Error: Retry budget must be finite and non-negative\n * checkBounds('Retry', -1, 10)\n * ```\n */\nexport function checkBounds(subject: string, budget: number, interval: number): void {\n\tif (!isFiniteNumber(budget) || budget < 0) {\n\t\tthrow new Error(`${subject} budget must be finite and non-negative`)\n\t}\n\tif (!isFiniteNumber(interval) || interval < 0) {\n\t\tthrow new Error(`${subject} interval must be finite and non-negative`)\n\t}\n}\n\n/**\n * Builds the error {@link retryUntil} raises when its elapsed-time budget runs out.\n *\n * @param description - The operation the retry was named for.\n * @param budget - The elapsed-time limit in milliseconds.\n * @param elapsed - The milliseconds the retry actually spent.\n * @param last - The rendered last unsatisfying value, or `undefined` when the retry produced none.\n * @param cause - The last producer error, which becomes the returned error's `cause`.\n * @returns The exhaustion error, unthrown.\n * @remarks Both of `retryUntil`'s elapsed checks raise this one message, so an edit to it lands in\n * one place. The rendered value is appended only when the retry produced one.\n *\n * @example\n * ```ts\n * import { buildRetryExhausted } from '@orkestrel/test'\n *\n * const exhausted = buildRetryExhausted('registry answers', 30, 31, '\"starting\"', undefined)\n *\n * exhausted.message\n * // 'Retry \"registry answers\" did not succeed within 30ms (waited 31ms) (last value: \"starting\")'\n * ```\n */\nexport function buildRetryExhausted(\n\tdescription: string,\n\tbudget: number,\n\telapsed: number,\n\tlast: string | undefined,\n\tcause: unknown,\n): Error {\n\treturn new Error(\n\t\t`Retry \"${description}\" did not succeed within ${budget}ms (waited ${elapsed}ms)${last === undefined ? '' : ` (last value: ${last})`}`,\n\t\t{ cause },\n\t)\n}\n\n/**\n * Drops the registration an instrumented signal installed for one listener.\n *\n * @param registrations - The live registration list, spliced in place.\n * @param installed - The installed listener naming the registration to drop.\n * @returns The dropped registration, or `undefined` when the list holds none for that listener.\n * @remarks The dropped registration's cleanup controller is aborted as it leaves, so the scope\n * subscription installed beside it leaves with it. Removing the installed listener from the signal\n * stays with the caller, because only the scope-abort path has one to remove.\n *\n * @example\n * ```ts\n * import type { SignalRegistration } from '@orkestrel/test'\n * import { dropRegistration } from '@orkestrel/test'\n *\n * const listener: EventListener = () => undefined\n * const installed: EventListenerObject = { handleEvent: () => undefined }\n * const cleanup = new AbortController()\n * const registrations: SignalRegistration[] = [[listener, installed, true, cleanup]]\n *\n * dropRegistration(registrations, installed)?.[1] === installed // true\n * cleanup.signal.aborted // true\n * dropRegistration(registrations, installed) // undefined\n * ```\n */\nexport function dropRegistration(\n\tregistrations: SignalRegistration[],\n\tinstalled: EventListener | EventListenerObject,\n): SignalRegistration | undefined {\n\tconst index = registrations.findIndex((registration) => registration[1] === installed)\n\tconst registration = registrations[index]\n\tif (registration === undefined) return undefined\n\tregistrations.splice(index, 1)\n\tregistration[3]?.abort()\n\treturn registration\n}\n\n/**\n * Waits for a host timer to elapse.\n *\n * @param ms - The delay in milliseconds. Default: `0`.\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 * Waits until an abort signal is aborted.\n *\n * @param signal - The signal to observe.\n * @returns A promise that resolves when the signal is aborted.\n * @remarks An already-aborted signal resolves immediately. Otherwise the wait parks on a one-shot\n * abort listener without a timer or polling.\n */\nexport function waitForAbort(signal: AbortSignal): Promise<void> {\n\tif (signal.aborted) return Promise.resolve()\n\treturn new Promise((resolve) => signal.addEventListener('abort', () => resolve(), { once: true }))\n}\n\n/**\n * Waits until a condition holds within an elapsed-time budget.\n *\n * @param description - The condition described in a timeout error.\n * @param condition - The synchronous or asynchronous condition to read.\n * @param options - The time bounds and abort signal.\n * @returns A promise that resolves when the condition first returns `true`.\n * @throws The condition's thrown value, the abort reason, or an `Error` when a bound is invalid or\n * the condition does not hold within the budget.\n * @remarks The first read is immediate. Default budget: `1000` milliseconds. Default interval: `10`\n * milliseconds.\n */\nexport async function waitForCondition(\n\tdescription: string,\n\tcondition: () => boolean | Promise<boolean>,\n\toptions?: WaitOptions,\n): Promise<void> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tcheckBounds('Wait', budget, interval)\n\n\tconst start = performance.now()\n\twhile (true) {\n\t\toptions?.signal?.throwIfAborted()\n\t\tconst held = await condition()\n\t\toptions?.signal?.throwIfAborted()\n\t\tif (held) return\n\n\t\tconst elapsed = performance.now() - start\n\t\tif (elapsed >= budget) {\n\t\t\tthrow new Error(\n\t\t\t\t`Condition \"${description}\" did not hold within ${budget}ms (waited ${elapsed}ms)`,\n\t\t\t)\n\t\t}\n\t\tawait waitForDelay(interval)\n\t}\n}\n\n/**\n * Repeats a producer until one produced value satisfies a predicate.\n *\n * @typeParam T - The produced value type.\n * @param description - The operation described in an exhaustion error.\n * @param produce - The synchronous or asynchronous operation to repeat.\n * @param satisfied - The predicate that accepts a produced value.\n * @param options - The time, attempt, and abort bounds.\n * @returns The first produced value the predicate accepts.\n * @throws The predicate's thrown value, the abort reason, or an `Error` when a bound is invalid or\n * the retry exhausts its budget or attempts.\n * @remarks A producer throw counts as an unsatisfied attempt. The last producer error becomes the\n * exhaustion error's `cause`. Default budget: `1000` milliseconds. Default interval: `10`\n * milliseconds.\n */\nexport async function retryUntil<T>(\n\tdescription: string,\n\tproduce: () => T | Promise<T>,\n\tsatisfied: (value: T) => boolean,\n\toptions?: RetryOptions,\n): Promise<T> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tconst attempts = options?.attempts\n\tcheckBounds('Retry', budget, interval)\n\tif (isDefined(attempts) && (!isInteger(attempts) || attempts < 1)) {\n\t\tthrow new Error('Retry attempts must be a positive integer')\n\t}\n\n\tconst start = performance.now()\n\tlet count = 0\n\tlet cause: unknown\n\tlet last: string | undefined\n\twhile (true) {\n\t\toptions?.signal?.throwIfAborted()\n\t\tif (count > 0) {\n\t\t\tconst elapsed = performance.now() - start\n\t\t\tif (elapsed >= budget) {\n\t\t\t\tthrow buildRetryExhausted(description, budget, elapsed, last, cause)\n\t\t\t}\n\t\t}\n\t\tlet produced: Result<T, unknown>\n\t\ttry {\n\t\t\tproduced = { success: true, value: await produce() }\n\t\t} catch (error) {\n\t\t\tproduced = { success: false, error }\n\t\t}\n\t\tcount += 1\n\t\toptions?.signal?.throwIfAborted()\n\n\t\tif (produced.success) {\n\t\t\tif (satisfied(produced.value)) return produced.value\n\t\t\tlet rendered: string\n\t\t\ttry {\n\t\t\t\tconst serialized = JSON.stringify(produced.value)\n\t\t\t\trendered = serialized === undefined ? String(produced.value) : serialized\n\t\t\t} catch {\n\t\t\t\ttry {\n\t\t\t\t\trendered = String(produced.value)\n\t\t\t\t} catch {\n\t\t\t\t\trendered = '[unrenderable]'\n\t\t\t\t}\n\t\t\t}\n\t\t\tlast = rendered.length > 200 ? `${rendered.slice(0, 197)}...` : rendered\n\t\t} else {\n\t\t\tcause = produced.error\n\t\t}\n\n\t\tconst elapsed = performance.now() - start\n\t\tif (elapsed >= budget) {\n\t\t\tthrow buildRetryExhausted(description, budget, elapsed, last, cause)\n\t\t}\n\t\tif (attempts !== undefined && count >= attempts) {\n\t\t\tthrow new Error(\n\t\t\t\t`Retry \"${description}\" did not succeed within ${attempts} attempts${last === undefined ? '' : ` (last value: ${last})`}`,\n\t\t\t\t{ cause },\n\t\t\t)\n\t\t}\n\t\tawait waitForDelay(Math.min(interval, budget - elapsed))\n\t}\n}\n\n/**\n * Invokes an unknown method through an explicit unchecked result contract.\n *\n * @typeParam T - The result type claimed by the caller.\n * @param target - The value used as the method's `this` argument.\n * @param method - The unknown method to invoke.\n * @param args - The arguments to pass.\n * @returns The method's result under the caller's claimed type.\n * @throws A `TypeError` when `method` is not callable.\n * @remarks The caller owns the claim that the returned value has type `T`. The contained `any`\n * bridges the unchecked runtime result to that caller-owned claim.\n */\nexport function invokeUnchecked<T>(target: unknown, method: unknown, args: readonly unknown[]): T {\n\tif (typeof method !== 'function') throw new TypeError('Method must be callable')\n\tconst result: T = Reflect.apply(method, target, args)\n\treturn result\n}\n\n/**\n * Reads a property from an unknown object or function.\n *\n * @typeParam T - The property type claimed by the caller.\n * @param target - The unknown value to read.\n * @param key - The property key to read.\n * @returns The property value under the caller's claimed type.\n * @throws A `TypeError` when `target` is neither an object nor a function.\n * @remarks The caller owns the claim that the returned value has type `T`. The contained `any`\n * bridges the unchecked runtime result to that caller-owned claim.\n */\nexport function readProperty<T>(target: unknown, key: PropertyKey): T {\n\tif ((typeof target !== 'object' || target === null) && typeof target !== 'function') {\n\t\tthrow new TypeError('Target must be an object or function')\n\t}\n\tconst result: T = Reflect.get(target, key)\n\treturn result\n}\n\n/**\n * Normalizes headers into a frozen plain record.\n *\n * @param init - The platform header initializer to normalize.\n * @returns A frozen record of normalized header names and values.\n * @remarks Normalization follows the host `Headers` implementation, including lowercased names and\n * combined values.\n */\nexport function flattenHeaders(init: HeadersSource): Readonly<Record<string, string>> {\n\treturn Object.freeze(Object.fromEntries(new Headers(init).entries()))\n}\n\n/**\n * Waits for the first delivery from an event subscription.\n *\n * @typeParam TArgs - The delivered argument tuple.\n * @param subscribe - The function that installs the event listener and may return its cleanup.\n * @param description - The event described in a timeout error.\n * @param options - The time bounds and abort signal.\n * @returns The first delivered argument tuple.\n * @throws The subscription's thrown value, the abort reason, or an `Error` when a bound is invalid\n * or the event is not delivered within the budget.\n * @remarks Default budget: `1000` milliseconds. The interval is validated for consistency with the\n * wait family but is not used because this helper parks on the event.\n */\nexport async function waitForEvent<TArgs extends readonly unknown[]>(\n\tsubscribe: EventSubscriber<TArgs>,\n\tdescription: string,\n\toptions?: WaitOptions,\n): Promise<TArgs> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tcheckBounds('Event', budget, interval)\n\n\tconst signal = options?.signal\n\tsignal?.throwIfAborted()\n\tconst delivery = Promise.withResolvers<TArgs>()\n\tconst controller = new AbortController()\n\tlet timeout: ReturnType<typeof setTimeout> | undefined\n\tconst pending: Array<Promise<TArgs>> = [\n\t\tdelivery.promise,\n\t\tnew Promise((_resolve, reject) => {\n\t\t\ttimeout = setTimeout(() => {\n\t\t\t\treject(new Error(`Event \"${description}\" was not delivered within ${budget}ms`))\n\t\t\t}, budget)\n\t\t}),\n\t]\n\tif (signal !== undefined) {\n\t\tpending.push(\n\t\t\tnew Promise((_resolve, reject) => {\n\t\t\t\tconst combined = AbortSignal.any([signal, controller.signal])\n\t\t\t\tcombined.addEventListener(\n\t\t\t\t\t'abort',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tif (signal.aborted) reject(signal.reason)\n\t\t\t\t\t},\n\t\t\t\t\t{ once: true },\n\t\t\t\t)\n\t\t\t}),\n\t\t)\n\t}\n\tconst result = Promise.race(pending)\n\tlet cleanup: (() => void) | void = undefined\n\ttry {\n\t\ttry {\n\t\t\tcleanup = subscribe((...args) => delivery.resolve(args))\n\t\t} catch (error) {\n\t\t\tdelivery.reject(error)\n\t\t}\n\t\treturn await result\n\t} finally {\n\t\tcontroller.abort()\n\t\tif (timeout !== undefined) clearTimeout(timeout)\n\t\tcleanup?.()\n\t}\n}\n\n/**\n * Decodes newline-delimited JSON values.\n *\n * @param text - The JSON Lines text to decode.\n * @returns The decoded values in physical-line order.\n * @throws An `Error` naming the malformed physical line, with the native `SyntaxError` as its\n * `cause`.\n * @remarks An empty line contributes no value, and a trailing carriage return is dropped before the\n * line is parsed, so text written with either line ending decodes the same.\n *\n * @example\n * ```ts\n * import { decodeJSONLines } from '@orkestrel/test'\n *\n * decodeJSONLines('{\"ready\":true}\\n7\\n') // [{ ready: true }, 7]\n *\n * // Throws Error: Invalid JSON on line 3\n * decodeJSONLines('{}\\n\\n{')\n * ```\n */\nexport function decodeJSONLines(text: string): readonly unknown[] {\n\tconst values: unknown[] = []\n\tfor (const [index, physical] of text.split('\\n').entries()) {\n\t\tconst line = physical.endsWith('\\r') ? physical.slice(0, -1) : physical\n\t\tif (line.length === 0) continue\n\t\ttry {\n\t\t\tconst value: unknown = JSON.parse(line)\n\t\t\tvalues.push(value)\n\t\t} catch (cause) {\n\t\t\tthrow new Error(`Invalid JSON on line ${index + 1}`, { cause })\n\t\t}\n\t}\n\treturn values\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\tconst outcome = attempt(thunk)\n\treturn outcome.success ? undefined : outcome.error\n}\n\n/**\n * Narrows a value away from `null` and `undefined`, throwing when it is absent.\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. Default: `'Value is required'`.\n * @returns The present value.\n * @throws An `Error` carrying `message` when the value is `null` or `undefined`.\n */\nexport function requireValue<T>(value: T | null | undefined, message = 'Value is required'): T {\n\tif (!isDefined(value)) 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 copied value's type, which the copy keeps.\n * @param value - The value to copy, bounded by its own `JSONSafe` projection.\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. The bound intersects `JSONSafe<T>` rather than\n * constraining `T` to `JSONValue`, so an interface-typed value round-trips.\n */\nexport function roundTripJSON<T>(value: T & JSONSafe<T>): T {\n\tconst serialized = JSON.stringify(value, (_key, current) => {\n\t\tif (current === undefined || isFunction(current) || isSymbol(current)) {\n\t\t\tthrow new Error('JSON values must not contain undefined, functions, or symbols')\n\t\t}\n\t\tif (isNumber(current) && !isFiniteNumber(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: unknown[] = [parsed]\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop()\n\t\tif (isNumber(current) && !isFiniteNumber(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\tif (isArray(current)) {\n\t\t\tfor (const child of current) pending.push(child)\n\t\t} else if (isObject(current)) {\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\n/**\n * Drives one statechart scenario through its arrange, act, and assert phases.\n *\n * @typeParam TState - The states the entity moves between.\n * @typeParam TEvent - The events the entity accepts.\n * @typeParam TContext - The fixture the phases drive.\n * @param scenario - The scenario to drive.\n * @param context - The fixture handed to each phase.\n * @returns A promise that resolves after the assert phase completes.\n * @throws An `Error` whose message opens with the transition's `name` and whose `cause` is the value\n * the failing phase threw.\n * @remarks Each phase is awaited before the next begins, so an asynchronous arrange settles before\n * the event is applied. The phases receive the transition's own parts: `arrange` receives `from`,\n * `act` receives `event`, and `assert` receives `to`.\n *\n * The row's name is prepended because a table's rows run under one test name, so a bare assertion\n * message says what failed and never which row. A thrown `Error` keeps its own message after the\n * name and arrives as the `cause`; anything else thrown is named by its type and arrives as the\n * `cause` unchanged.\n *\n * @example\n * ```ts\n * await executeScenario(scenarios[0], { disclosure: new Disclosure() })\n * ```\n */\nexport async function executeScenario<TState extends string, TEvent extends string, TContext>(\n\tscenario: StateScenario<TState, TEvent, TContext>,\n\tcontext: TContext,\n): Promise<void> {\n\tconst transition = scenario.transition\n\ttry {\n\t\tawait scenario.arrange(context, transition.from)\n\t\tawait scenario.act(context, transition.event)\n\t\tawait scenario.assert(context, transition.to)\n\t} catch (cause) {\n\t\tconst message = isError(cause) ? cause.message : `threw a non-error ${typeof cause} value`\n\t\tthrow new Error(`${transition.name}: ${message}`, { cause })\n\t}\n}\n\n/**\n * Drives a statechart table row by row, each row against a context of its own.\n *\n * @typeParam TState - The states the entity moves between.\n * @typeParam TEvent - The events the entity accepts.\n * @typeParam TContext - The fixture the phases drive.\n * @param scenarios - The table to drive, in the order it is written.\n * @param build - The fixture builder, called once per row and awaited when it returns a promise.\n * @returns A promise that resolves after the last row completes.\n * @throws An `Error` reading `<name>: build refused` when the row's builder throws or rejects, whose\n * `cause` is the value the builder refused with, or whatever {@link executeScenario} throws for the\n * first row whose phases fail. Either way the run stops at that row and the rows after it never\n * start.\n * @remarks The rows run one after another rather than together: a statechart's rows drive one\n * entity on one page, so a parallel run would have them arranging over each other. `build` receives\n * the row it is building for, which is what lets one table mix fixtures.\n *\n * A refusing builder is named for its row the way a failing phase is, because a table's rows build\n * under one test name too. The refusal itself arrives as the `cause`, by identity, so its own\n * message and stack survive the naming.\n *\n * @example\n * ```ts\n * await executeScenarios(SCENARIOS, () => ({ disclosure: new Disclosure() }))\n * ```\n */\nexport async function executeScenarios<TState extends string, TEvent extends string, TContext>(\n\tscenarios: ReadonlyArray<StateScenario<TState, TEvent, TContext>>,\n\tbuild: (scenario: StateScenario<TState, TEvent, TContext>) => TContext | Promise<TContext>,\n): Promise<void> {\n\tfor (const scenario of scenarios) {\n\t\tlet context: TContext\n\t\ttry {\n\t\t\tcontext = await build(scenario)\n\t\t} catch (cause) {\n\t\t\tthrow new Error(`${scenario.transition.name}: build refused`, { cause })\n\t\t}\n\t\tawait executeScenario(scenario, context)\n\t}\n}\n","import type {\n\tEventSourceInterface,\n\tRecorderInterface,\n\tRecorderMap,\n\tResourceFactoryInterface,\n\tSignalInterface,\n\tSignalRegistration,\n\tTeardownHandler,\n\tTeardownInterface,\n} from './types.js'\nimport { dropRegistration } from './helpers.js'\nimport { isRecorderMapComplete } from './validators.js'\n\n/**\n * Creates values that make common object readers throw or violate their assumptions.\n *\n * @returns A frozen array whose values are fresh on every call.\n * @remarks Every member makes a naive read throw or violates a naive structural assumption. A total\n * guard survives every member without throwing. Whether it accepts or refuses one is that guard's\n * own contract. Membership may grow in a release, so test the whole returned set in a loop and\n * include the index in each failure.\n *\n * - The self-referential record makes JSON record serialization throw.\n * - The revoked object proxy makes reflective object access throw.\n * - The property proxy makes a named property read throw.\n * - The key proxy makes key enumeration throw.\n * - The prototype proxy makes a prototype read throw.\n * - The null-prototype record breaks a direct `hasOwnProperty` call.\n * - The array-target proxy passes an array check and makes an index read throw.\n * - The self-referential array makes JSON collection serialization throw.\n * - The sparse array violates the assumption that every index is enumerable.\n * - The hidden-key record violates the assumption that every own key is enumerable.\n * - The named getter makes its property read throw.\n * @example\n * ```ts\n * import { expect } from 'vitest'\n * import { createHostileValues } from '@orkestrel/test'\n *\n * function isWireRecord(value: unknown): value is Readonly<Record<string, string>> {\n * \tif (typeof value !== 'object' || value === null) return false\n * \ttry {\n * \t\tif (Object.getPrototypeOf(value) !== Object.prototype) return false\n * \t\tReflect.get(value, 'value')\n * \t\tif (Reflect.ownKeys(value).length === 0) return false\n * \t\treturn Object.values(value).every((member) => typeof member === 'string')\n * \t} catch {\n * \t\treturn false\n * \t}\n * }\n *\n * for (const [index, value] of createHostileValues().entries()) {\n * \tlet accepted: boolean | undefined\n * \texpect(() => {\n * \t\taccepted = isWireRecord(value)\n * \t}, `hostile value ${index}`).not.toThrow()\n * \texpect(accepted, `hostile value ${index}`).toBe(false)\n * }\n * ```\n */\nexport function createHostileValues(): readonly unknown[] {\n\tconst cyclic: Record<string, unknown> = {}\n\tcyclic.self = cyclic\n\tconst revoked = Proxy.revocable({}, {})\n\trevoked.revoke()\n\tconst cyclicArray: unknown[] = []\n\tcyclicArray.push(cyclicArray)\n\tconst sparseArray = Array<unknown>(2)\n\tsparseArray[1] = 'present'\n\tconst hidden = {}\n\tObject.defineProperty(hidden, 'hidden', { value: true })\n\tReflect.set(hidden, 'self', hidden)\n\tconst getter = {}\n\tObject.defineProperty(getter, 'danger', {\n\t\tenumerable: true,\n\t\tget() {\n\t\t\tthrow new Error('Hostile named getter read')\n\t\t},\n\t})\n\n\treturn Object.freeze([\n\t\tcyclic,\n\t\trevoked.proxy,\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\tget() {\n\t\t\t\t\tthrow new Error('Hostile property read')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\townKeys() {\n\t\t\t\t\tthrow new Error('Hostile key enumeration')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\tgetPrototypeOf() {\n\t\t\t\t\tthrow new Error('Hostile prototype read')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tObject.create(null),\n\t\tnew Proxy([], {\n\t\t\tget() {\n\t\t\t\tthrow new Error('Hostile array index read')\n\t\t\t},\n\t\t}),\n\t\tcyclicArray,\n\t\tsparseArray,\n\t\thidden,\n\t\tgetter,\n\t])\n}\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<\n\tTArgs extends readonly unknown[] = readonly unknown[],\n>(): 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\n/**\n * Creates event recorders and subscribes them to the source.\n *\n * @typeParam TMap - The source's event names and delivered argument tuples.\n * @typeParam TName - The requested event names.\n * @param source - The source to subscribe to.\n * @param events - The events to record.\n * @returns A map from each requested event name to its recorder.\n * @throws Thrown when a listed event has no recorder, which a well-formed events array cannot\n * produce.\n * @remarks A duplicate event name installs a fresh recorder for every occurrence. The returned map\n * keeps the recorder installed for the last occurrence. `TName` derives from the array's element\n * type. An array declared with a wider union than its contents widens `TName` beyond the listed\n * events. The omitted key reads `undefined` at runtime under a non-optional type, and the guard\n * reports `true` because it checks the listed events. Pass a literal array or a tuple.\n */\nexport function createRecorders<\n\tTMap extends Record<string, readonly unknown[]>,\n\tTName extends keyof TMap,\n>(source: EventSourceInterface<TMap>, events: readonly TName[]): RecorderMap<TMap, TName> {\n\tconst building: { -readonly [K in TName]?: RecorderInterface<TMap[TName]> } = {}\n\tfor (const event of events) {\n\t\tconst recorder = createRecorder<TMap[TName]>()\n\t\tsource.on(event, recorder.handler)\n\t\tbuilding[event] = recorder\n\t}\n\tif (!isRecorderMapComplete<TMap, TName>(building, events)) {\n\t\tthrow new Error('Emitter recorder map is incomplete')\n\t}\n\treturn building\n}\n\n/**\n * Creates a real abort controller whose signal reports its live abort listeners.\n *\n * @returns The controller, its instrumented signal, and the current listener tally.\n * @remarks Instrumentation is installed on the created signal instance. A one-shot listener leaves\n * the tally when it fires, and removal accepts the original listener supplied by the caller. A\n * listener scoped by another signal leaves when that signal aborts. An already-aborted scope\n * installs and records nothing.\n */\nexport function createSignal(): SignalInterface {\n\tconst controller = new AbortController()\n\tconst signal = controller.signal\n\tconst add = signal.addEventListener.bind(signal)\n\tconst remove = signal.removeEventListener.bind(signal)\n\tconst registrations: SignalRegistration[] = []\n\n\tObject.defineProperty(signal, 'addEventListener', {\n\t\tconfigurable: true,\n\t\tvalue(\n\t\t\ttype: string,\n\t\t\tlistener: EventListener | EventListenerObject | null,\n\t\t\toptions?: boolean | AddEventListenerOptions,\n\t\t) {\n\t\t\tif (listener === null) return\n\t\t\tif (type !== 'abort') {\n\t\t\t\tadd(type, listener, options)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst capture = typeof options === 'boolean' ? options : (options?.capture ?? false)\n\t\t\tconst scope = typeof options === 'object' ? options?.signal : undefined\n\t\t\tif (scope?.aborted === true) return\n\t\t\tif (\n\t\t\t\tregistrations.some(\n\t\t\t\t\t(registration) => registration[0] === listener && registration[2] === capture,\n\t\t\t\t)\n\t\t\t) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst once = typeof options === 'object' && options?.once === true\n\t\t\tconst cleanup = scope === undefined ? undefined : new AbortController()\n\t\t\tconst installed: EventListenerObject = {\n\t\t\t\thandleEvent(event) {\n\t\t\t\t\tif (once) dropRegistration(registrations, installed)\n\t\t\t\t\tif (typeof listener === 'function') listener.call(signal, event)\n\t\t\t\t\telse listener.handleEvent(event)\n\t\t\t\t},\n\t\t\t}\n\t\t\tadd(\n\t\t\t\ttype,\n\t\t\t\tinstalled,\n\t\t\t\ttypeof options === 'object'\n\t\t\t\t\t? options?.passive === undefined\n\t\t\t\t\t\t? { capture, once }\n\t\t\t\t\t\t: { capture, once, passive: options.passive }\n\t\t\t\t\t: options,\n\t\t\t)\n\t\t\tregistrations.push([listener, installed, capture, cleanup])\n\t\t\tif (scope !== undefined && cleanup !== undefined) {\n\t\t\t\tscope.addEventListener(\n\t\t\t\t\t'abort',\n\t\t\t\t\t() => {\n\t\t\t\t\t\tconst registration = dropRegistration(registrations, installed)\n\t\t\t\t\t\tif (registration === undefined) return\n\t\t\t\t\t\tremove(type, registration[1], { capture })\n\t\t\t\t\t},\n\t\t\t\t\t{ once: true, signal: cleanup.signal },\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\t})\n\tObject.defineProperty(signal, 'removeEventListener', {\n\t\tconfigurable: true,\n\t\tvalue(\n\t\t\ttype: string,\n\t\t\tlistener: EventListener | EventListenerObject | null,\n\t\t\toptions?: boolean | EventListenerOptions,\n\t\t) {\n\t\t\tif (listener === null) return\n\t\t\tif (type !== 'abort') {\n\t\t\t\tremove(type, listener, options)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst capture = typeof options === 'boolean' ? options : (options?.capture ?? false)\n\t\t\tconst index = registrations.findIndex(\n\t\t\t\t(registration) => registration[0] === listener && registration[2] === capture,\n\t\t\t)\n\t\t\tconst registration = registrations[index]\n\t\t\tif (registration === undefined) {\n\t\t\t\tremove(type, listener, options)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tregistrations.splice(index, 1)\n\t\t\tregistration[3]?.abort()\n\t\t\tremove(type, registration[1], options)\n\t\t},\n\t})\n\n\treturn {\n\t\tcontroller,\n\t\tsignal,\n\t\tget count() {\n\t\t\treturn registrations.length\n\t\t},\n\t}\n}\n\n/**\n * Creates a monotonically numbered resource factory with creation and destruction records.\n *\n * @returns A resource factory whose recorders retain every affected id in order.\n */\nexport function createResourceFactory(): ResourceFactoryInterface {\n\tconst created = createRecorder<readonly [id: number]>()\n\tconst destroyed = createRecorder<readonly [id: number]>()\n\treturn {\n\t\tcreated,\n\t\tdestroyed,\n\t\tcreate() {\n\t\t\tconst id = created.calls.length + 1\n\t\t\tcreated.handler(id)\n\t\t\treturn id\n\t\t},\n\t\tdestroy(id) {\n\t\t\tdestroyed.handler(id)\n\t\t},\n\t}\n}\n\n/**\n * Creates a teardown list that runs registered handlers newest-first.\n *\n * @returns A teardown list that awaits every handler and collects failures.\n */\nexport function createTeardown(): TeardownInterface {\n\tlet handlers: TeardownHandler[] = []\n\treturn {\n\t\tget count() {\n\t\t\treturn handlers.length\n\t\t},\n\t\tadd(handler) {\n\t\t\thandlers.push(handler)\n\t\t},\n\t\tasync destroy() {\n\t\t\tconst snapshot = handlers\n\t\t\thandlers = []\n\t\t\tconst failures: unknown[] = []\n\t\t\tfor (const handler of snapshot.reverse()) {\n\t\t\t\ttry {\n\t\t\t\t\tawait handler()\n\t\t\t\t} catch (error) {\n\t\t\t\t\tfailures.push(error)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (failures.length === 1) throw failures[0]\n\t\t\tif (failures.length > 1) throw new AggregateError(failures)\n\t\t},\n\t}\n}\n"],"mappings":"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"}
@@ -5,6 +5,7 @@ let node_http = require("node:http");
5
5
  let node_os = require("node:os");
6
6
  let node_path = require("node:path");
7
7
  let node_url = require("node:url");
8
+ let _orkestrel_contract = require("@orkestrel/contract");
8
9
  let _src_core = require("../core/index.cjs");
9
10
  let node_events = require("node:events");
10
11
  //#region src/server/constants.ts
@@ -110,7 +111,7 @@ function readIdentity(status) {
110
111
  * ```
111
112
  */
112
113
  function readErrorCode(error) {
113
- return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0;
114
+ return (0, _orkestrel_contract.isObject)(error) && "code" in error && (0, _orkestrel_contract.isString)(error.code) ? error.code : void 0;
114
115
  }
115
116
  /**
116
117
  * Reports whether two directory identities name the same allocation.
@@ -240,7 +241,7 @@ function removeTree(path) {
240
241
  * key below it, and it applies to a named target and a walked entry alike.
241
242
  */
242
243
  function readInventory(root, targets, options) {
243
- const supplied = (0, node_path.resolve)(typeof root === "string" ? root : (0, node_url.fileURLToPath)(root));
244
+ const supplied = (0, node_path.resolve)((0, _orkestrel_contract.isString)(root) ? root : (0, node_url.fileURLToPath)(root));
244
245
  const rootStatus = (0, node_fs.lstatSync)(supplied);
245
246
  if (rootStatus.isSymbolicLink()) throw new Error("Root is a symbolic link");
246
247
  if (!rootStatus.isDirectory()) throw new Error("Root is not a directory");
@@ -878,7 +879,7 @@ async function createLoopback(server) {
878
879
  server.listen(0, "127.0.0.1");
879
880
  await (0, node_events.once)(server, "listening");
880
881
  const address = server.address();
881
- if (typeof address !== "object" || address === null || !("port" in address) || typeof address.port !== "number") throw new Error(`Loopback address must have a numeric port; found ${String(address)}`);
882
+ if (!(0, _orkestrel_contract.isObject)(address) || !("port" in address) || !(0, _orkestrel_contract.isNumber)(address.port)) throw new Error(`Loopback address must have a numeric port; found ${String(address)}`);
882
883
  const port = address.port;
883
884
  let destruction;
884
885
  return {
@@ -886,7 +887,7 @@ async function createLoopback(server) {
886
887
  port,
887
888
  destroy() {
888
889
  if (destruction === void 0) destruction = new Promise((resolveClose, rejectClose) => {
889
- if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") server.closeAllConnections();
890
+ if ("closeAllConnections" in server && (0, _orkestrel_contract.isFunction)(server.closeAllConnections)) server.closeAllConnections();
890
891
  server.close((error) => {
891
892
  if (error === void 0 || "code" in error && error.code === "ERR_SERVER_NOT_RUNNING") resolveClose();
892
893
  else rejectClose(error);
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["/**\n * Caps the attempts `removeTree` makes before rethrowing a retryable removal error.\n */\nexport const REMOVE_TREE_MAX_ATTEMPTS = 10\n\n/**\n * Names the synchronous delay, in milliseconds, `removeTree` waits between attempts.\n */\nexport const REMOVE_TREE_RETRY_DELAY_MS = 100\n\n/**\n * Names the error codes `removeTree` retries; every other code rethrows immediately.\n */\nexport const REMOVE_TREE_RETRYABLE_CODES: readonly string[] = Object.freeze([\n\t'EBUSY',\n\t'ENOTEMPTY',\n\t'EPERM',\n])\n","import type { Stats } from 'node:fs'\nimport type { Socket } from 'node:net'\nimport type { WaitOptions } from '@src/core'\nimport type {\n\tInventoryOptions,\n\tScratchIdentity,\n\tScratchInterface,\n\tUpgradeOptions,\n\tUpgradeResult,\n} from './types.js'\nimport { Buffer } from 'node:buffer'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treaddirSync,\n\treadFileSync,\n\trealpathSync,\n\trmSync,\n\tstatSync,\n\tsymlinkSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { request as requestHTTP } from 'node:http'\nimport { tmpdir } from 'node:os'\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { checkBounds, waitForDelay } from '@src/core'\nimport {\n\tREMOVE_TREE_MAX_ATTEMPTS,\n\tREMOVE_TREE_RETRY_DELAY_MS,\n\tREMOVE_TREE_RETRYABLE_CODES,\n} from './constants.js'\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\t// `relative` answers with an absolute path where the target carries a root of its own — a second\n\t// drive letter or a UNC share on Windows — and that spelling names no ancestor, so the `..` tests\n\t// miss it and containment turns on `isAbsolute`.\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Resolves a target that stays below a root directory, refusing an escape.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target below the root.\n * @throws An `Error` reading `Path outside scratch directory: <target>` when the target escapes the\n * root.\n * @remarks This is {@link resolveContained} with the refusal every contained scratch operation makes\n * of an escape, so the check and its one message are stated once. Read `resolveContained` where an\n * escape is an answer rather than a refusal.\n *\n * @example\n * ```ts\n * import { requireContained } from '@orkestrel/test/server'\n *\n * requireContained('/scratch', 'nested/file.txt') // '/scratch/nested/file.txt'\n *\n * // Throws Error: Path outside scratch directory: ../escape.ts\n * requireContained('/scratch', '../escape.ts')\n * ```\n */\nexport function requireContained(root: string, target: string): string {\n\tconst candidate = resolveContained(root, target)\n\tif (candidate === undefined) throw new Error(`Path outside scratch directory: ${target}`)\n\treturn candidate\n}\n\n/**\n * Reads the identity of one allocated directory off a host status.\n *\n * @param status - The status read from the directory's path.\n * @returns The device, index node, and creation time that together name the allocation.\n *\n * @example\n * ```ts\n * import { statSync } from 'node:fs'\n * import { readIdentity } from '@orkestrel/test/server'\n *\n * const status = statSync('/scratch')\n *\n * readIdentity(status) // { birth: status.birthtimeMs, device: status.dev, inode: status.ino }\n * ```\n */\nexport function readIdentity(status: Stats): ScratchIdentity {\n\treturn { birth: status.birthtimeMs, device: status.dev, inode: status.ino }\n}\n\n/**\n * Reads the `code` an unknown thrown value carries.\n *\n * @param error - The thrown value to read.\n * @returns The string `code` the value carries, or `undefined` when it carries none.\n * @remarks The read is contained on its own terms: a value that is not an object, one carrying no\n * `code`, and one carrying a `code` that is not a string all answer `undefined`. A null-prototype\n * object is read the same way, because the key is tested with `in` rather than through\n * `hasOwnProperty`.\n *\n * @example\n * ```ts\n * import { readFileSync } from 'node:fs'\n * import { captureError } from '@orkestrel/test'\n * import { readErrorCode } from '@orkestrel/test/server'\n *\n * readErrorCode(captureError(() => readFileSync('/scratch/absent.txt', 'utf8'))) // 'ENOENT'\n * readErrorCode(new Error('refused')) // undefined\n * ```\n */\nexport function readErrorCode(error: unknown): string | undefined {\n\treturn typeof error === 'object' &&\n\t\terror !== null &&\n\t\t'code' in error &&\n\t\ttypeof error.code === 'string'\n\t\t? error.code\n\t\t: undefined\n}\n\n/**\n * Reports whether two directory identities name the same allocation.\n *\n * @param current - The identity read from the path now.\n * @param allocation - The identity recorded when the directory was allocated.\n * @returns True if the device, the index node, and the creation time all match; false otherwise.\n * @remarks All three fields are compared because none of them alone identifies an allocation. A\n * device is shared by every directory on one filesystem, an index node is reused once its directory\n * is removed, and a creation time repeats within the host's timestamp resolution.\n *\n * @example\n * ```ts\n * import { statSync } from 'node:fs'\n * import { matchesIdentity, readIdentity } from '@orkestrel/test/server'\n *\n * const allocation = readIdentity(statSync('/scratch'))\n *\n * matchesIdentity(readIdentity(statSync('/scratch')), allocation) // true\n * matchesIdentity({ birth: 3, device: 1, inode: 9 }, { birth: 3, device: 1, inode: 2 }) // false\n * ```\n */\nexport function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean {\n\treturn (\n\t\tcurrent.device === allocation.device &&\n\t\tcurrent.inode === allocation.inode &&\n\t\tcurrent.birth === allocation.birth\n\t)\n}\n\n/**\n * Reports whether a root-relative key matches an exclusion.\n *\n * @param key - The root-relative key to test.\n * @param exclusions - The normalized root-relative exclusion keys.\n * @returns True if an exclusion names the key or one of its ancestors; false otherwise.\n *\n * @example\n * ```ts\n * import { isExcluded } from '@orkestrel/test/server'\n *\n * isExcluded('src/index.ts', ['src']) // true\n * isExcluded('src-other/index.ts', ['src']) // false\n * ```\n */\nexport function isExcluded(key: string, exclusions: readonly string[]): boolean {\n\treturn exclusions.some((rule) => rule === '' || key === rule || key.startsWith(`${rule}/`))\n}\n\n/**\n * Creates a symbolic link with a directory-junction fallback for hosts that refuse symbolic links.\n *\n * @param path - The path where the link is created.\n * @param source - The destination path the link points at.\n * @throws The original link error when its code is not `EPERM`, or when the source names an\n * existing non-directory; otherwise, any error from inspecting the source or creating the junction.\n * @remarks Only `EPERM` from the first symbolic-link attempt triggers the fallback. The fallback\n * resolves the source against the link's directory. An existing non-directory rethrows the original\n * `EPERM`, while a directory or missing source is passed to a junction attempt. A missing source is\n * accepted to create a dangling junction. Where the host creates a junction, its stored value is the\n * resolved absolute path.\n *\n * @example\n * ```ts\n * import { readFileSync } from 'node:fs'\n * import { createLink } from '@orkestrel/test/server'\n *\n * // `/scratch/source` is a directory holding `file.txt`.\n * createLink('/scratch/linked', '/scratch/source')\n *\n * readFileSync('/scratch/linked/file.txt', 'utf8') // 'linked'\n * ```\n */\nexport function createLink(path: string, source: string): void {\n\ttry {\n\t\tsymlinkSync(source, path)\n\t} catch (error) {\n\t\tif (readErrorCode(error) !== 'EPERM') throw error\n\n\t\tconst resolved = resolve(dirname(path), source)\n\t\tconst status = statSync(resolved, { throwIfNoEntry: false })\n\t\tif (status !== undefined && !status.isDirectory()) throw error\n\t\tsymlinkSync(resolved, path, 'junction')\n\t}\n}\n\n/**\n * Removes a directory tree, retrying past a transient Windows handle-release race.\n *\n * @param path - The absolute directory to remove.\n * @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,\n * or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.\n * @remarks On Windows, a directory that a recently exited process still holds as its current\n * working directory throws `EPERM` for a short interval after that process exits. Node's own\n * `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed\n * against a real held directory, they neither delay nor retry before rethrowing, so the retry\n * is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait\n * at roughly one second. A hold that outlasts that second is {@link destroyScratch}'s case, which\n * retries every refusal inside a caller's budget rather than the codes named here.\n *\n * @example\n * ```ts\n * import { existsSync } from 'node:fs'\n * import { removeTree } from '@orkestrel/test/server'\n *\n * removeTree('/scratch/tree')\n *\n * existsSync('/scratch/tree') // false\n * ```\n */\nexport function removeTree(path: string): void {\n\tfor (let attempt = 1; ; attempt++) {\n\t\ttry {\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\tconst code = readErrorCode(error)\n\t\t\tif (\n\t\t\t\tcode === undefined ||\n\t\t\t\t!REMOVE_TREE_RETRYABLE_CODES.includes(code) ||\n\t\t\t\tattempt >= REMOVE_TREE_MAX_ATTEMPTS\n\t\t\t) {\n\t\t\t\tthrow error\n\t\t\t}\n\t\t\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, REMOVE_TREE_RETRY_DELAY_MS)\n\t\t}\n\t}\n}\n\n/**\n * Reads files from selected targets below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param targets - The files to read directly and directories to visit below the root.\n * @param options - Optional file extension and path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves\n * outside the root.\n * @remarks A named file is included regardless of the extension filter. An absent extension filter\n * includes every walked file. An exclusion matches whole root-relative key segments and covers every\n * key below it, and it applies to a named target and a walked entry alike.\n */\nexport function readInventory(\n\troot: URL | string,\n\ttargets: 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 (targets.length === 0) return Object.fromEntries([])\n\n\tconst exclusions = (options?.exclude ?? []).map((rule) => {\n\t\tconst unprefixed = rule.startsWith('./') ? rule.slice(2) : rule\n\t\tconst collapsed = unprefixed.replace(/\\/+/g, '/')\n\t\tconst untrailed = collapsed.endsWith('/') ? collapsed.slice(0, -1) : collapsed\n\t\treturn untrailed === '.' ? '' : untrailed\n\t})\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const target of targets) {\n\t\tconst candidate = resolveContained(base, target)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`)\n\t\tif (!status.isDirectory() && !status.isFile()) {\n\t\t\tthrow new Error(`Target is not a file or directory: ${target}`)\n\t\t}\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(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (isExcluded(key, exclusions)) continue\n\t\tif (status.isFile()) {\n\t\t\tcontents.set(key, readFileSync(physical, 'utf8'))\n\t\t\tcontinue\n\t\t}\n\t\tif (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 (isExcluded(key, exclusions)) 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\t// Walk containment is unproven because POSIX CI skips links before `realpath`;\n\t\t\t\t// a host that resolves a walked directory outside `base` would drive this branch.\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\n/**\n * Reports whether a process id names a live process.\n *\n * @param pid - The process id to read.\n * @returns True if a process holds that id at the moment of the call; false otherwise, including a\n * pid the host refuses.\n * @throws Nothing. Every host refusal reads as false.\n * @remarks This is an instantaneous observation rather than a claim of ownership. A host reuses a\n * process id after the process holding it exits, so a true answer says some process holds that id now\n * and never says it is the process the caller started. Two host answers are worth knowing. A POSIX\n * host refuses signal `0` to a process another user owns with `EPERM`, and that refusal reads as\n * false here. A pid of `0` names the caller's own process group on POSIX and the system idle process\n * on Windows, so it reads as true on both without naming a process anyone started.\n *\n * A Linux zombie — a process that has exited and whose parent has not reaped it — still accepts\n * signal `0`, so its `/proc` status is read and a `Z` state reads as false.\n *\n * @example\n * ```ts\n * import { isRunning } from '@orkestrel/test/server'\n *\n * isRunning(process.pid) // true\n * isRunning(2 ** 31) // false\n * ```\n */\nexport function isRunning(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0)\n\t} catch {\n\t\treturn false\n\t}\n\tif (process.platform !== 'linux') return true\n\n\t// The zombie refinement is unproven on a host that carries no `/proc`; a Linux gate drives it.\n\ttry {\n\t\tconst status = readFileSync(`/proc/${String(pid)}/stat`, 'utf8')\n\t\tconst boundary = status.lastIndexOf(') ')\n\t\treturn boundary < 0 || status.slice(boundary + 2, boundary + 3) !== 'Z'\n\t} catch {\n\t\treturn false\n\t}\n}\n\n/**\n * Waits for a socket to close, accepting a peer reset as a forced close.\n *\n * @param socket - The socket to wait on. One that has already closed resolves without listening.\n * @param options - The time bounds and abort signal.\n * @returns A promise that resolves when the socket emits `close`.\n * @throws The socket's own error when its code is not `ECONNRESET`, the abort reason, or an `Error`\n * when a bound is invalid or the socket does not close within the budget.\n * @remarks Default budget: `1000` milliseconds. A reset is the peer forcing the connection down, and\n * the socket still emits `close` afterwards, so `ECONNRESET` is waited past rather than raised while\n * every other error ends the wait. The interval is validated for consistency with the wait family but\n * is not used, because this helper parks on the socket's events. Both listeners are removed on every\n * settlement, so a caller may wait on one socket repeatedly.\n *\n * @example\n * ```ts\n * import { connect, createServer } from 'node:net'\n * import { createLoopback, waitForSocketClose } from '@orkestrel/test/server'\n *\n * const loopback = await createLoopback(createServer((socket) => socket.end()))\n * const client = connect(loopback.port, '127.0.0.1')\n *\n * await waitForSocketClose(client, { budget: 1000 }) // undefined\n * client.destroyed // true\n *\n * await loopback.destroy()\n * ```\n */\nexport async function waitForSocketClose(socket: Socket, options?: WaitOptions): Promise<void> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tcheckBounds('Socket', budget, interval)\n\n\tconst signal = options?.signal\n\tsignal?.throwIfAborted()\n\tif (socket.closed) return\n\n\t// The resolvers are the listeners themselves, so the same references remove them afterwards.\n\tconst closed = Promise.withResolvers<boolean>()\n\tconst failed = Promise.withResolvers<NodeJS.ErrnoException>()\n\tconst expiry = Promise.withResolvers<never>()\n\tconst aborted = Promise.withResolvers<never>()\n\tconst subscription = new AbortController()\n\tsocket.on('close', closed.resolve)\n\tsocket.on('error', failed.resolve)\n\tsignal?.addEventListener('abort', () => aborted.reject(signal.reason), {\n\t\tonce: true,\n\t\tsignal: subscription.signal,\n\t})\n\tconst timer = setTimeout(() => {\n\t\texpiry.reject(new Error(`Socket did not close within ${budget}ms`))\n\t}, budget)\n\n\ttry {\n\t\tconst error = await Promise.race([\n\t\t\tclosed.promise.then(() => undefined),\n\t\t\tfailed.promise,\n\t\t\texpiry.promise,\n\t\t\taborted.promise,\n\t\t])\n\t\tif (error === undefined) return\n\t\tif (error.code !== 'ECONNRESET') throw error\n\t\tawait Promise.race([closed.promise, expiry.promise, aborted.promise])\n\t} finally {\n\t\tclearTimeout(timer)\n\t\tsubscription.abort()\n\t\tsocket.off('close', closed.resolve)\n\t\tsocket.off('error', failed.resolve)\n\t}\n}\n\n/**\n * Destroys a scratch directory, retrying until the host releases it.\n *\n * @param scratch - The scratch directory to destroy.\n * @param options - The time bounds and abort signal.\n * @returns A promise that resolves once `destroy()` returns without throwing.\n * @throws The abort reason, or an `Error` when a bound is invalid or the budget elapses. The\n * exhaustion error carries the last host refusal as its `cause`.\n * @remarks Default budget: `10000` milliseconds. Default interval: `25` milliseconds. A host holds a\n * directory for a short interval after the process that held it exits, and a recently stopped child's\n * working directory is the case this exists for, so removal is attempted until the host lets go\n * rather than exactly once. {@link ScratchInterface.destroy} stays synchronous and is unchanged; this\n * is the bounded retry around it. A directory nothing releases still fails, with the host's own\n * refusal as the `cause`.\n *\n * Every refusal is retried, deliberately, and that is wider than {@link removeTree}'s policy: that\n * one retries the codes {@link REMOVE_TREE_RETRYABLE_CODES} names and rethrows the rest at once.\n * The hold this waits out is not classifiable across hosts — Windows reports a working-directory\n * hold as `EPERM`, POSIX hosts and network filesystems report their own — so a code list here would\n * be a list of the hosts it had been run on. The residual is the cost of that: a fault no wait can\n * clear, such as a path removed from under the allocation or a permission the process never had,\n * spends the whole budget before it surfaces, and it surfaces wrapped in the exhaustion error with\n * the host's refusal as `cause` rather than by identity. Pass a shorter `budget` or a `signal`\n * wherever a caller must bound that cost.\n */\nexport async function destroyScratch(\n\tscratch: ScratchInterface,\n\toptions?: WaitOptions,\n): Promise<void> {\n\tconst budget = options?.budget ?? 10_000\n\tconst interval = options?.interval ?? 25\n\tcheckBounds('Scratch', budget, interval)\n\n\tconst start = performance.now()\n\tlet refusal: unknown\n\twhile (true) {\n\t\toptions?.signal?.throwIfAborted()\n\t\ttry {\n\t\t\tscratch.destroy()\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\trefusal = error\n\t\t}\n\n\t\tif (performance.now() - start >= budget) {\n\t\t\tthrow new Error(`Scratch directory was not destroyed within ${budget}ms`, {\n\t\t\t\tcause: refusal,\n\t\t\t})\n\t\t}\n\t\tawait waitForDelay(interval)\n\t}\n}\n\n/**\n * Drives a real client upgrade request against a loopback port and reports what the server did.\n *\n * @param port - The port the server listens on at `127.0.0.1`.\n * @param options - Optional request path, offered subprotocols, time bounds, and abort signal.\n * @returns A promise resolving to the server's answer: a claimed upgrade with the protocol it\n * selected, or a refusal with the status it answered.\n * @throws The client's own transport error, such as the `ECONNREFUSED` a closed port answers, the\n * abort reason, or an `Error` when a bound is invalid or the server does not answer within the\n * budget.\n * @remarks Default budget: `1000` milliseconds. The request carries `Connection: Upgrade` and\n * `Upgrade: websocket`, which is what makes a server's `upgrade` handler the one that answers it.\n * The `upgrade`, `response`, and `error` events are mutually exclusive in practice and the promise\n * settles on whichever arrives first, so a second event changes nothing. The client socket is\n * destroyed before every settlement, on the claimed path because an upgraded socket is detached from\n * the request and outlives it otherwise. The request is made with no agent, so no pooled connection\n * survives the call to keep a suite's event loop alive.\n *\n * A server that accepts the connection and answers nothing raises no transport error, so the budget\n * is what ends that call: the rejection names the port and path it was waiting on. The interval is\n * validated for consistency with the wait family but is not used, because this helper parks on the\n * request's events.\n *\n * A `101` is the claimed path's status on the wire and is deliberately not reported: `status` is the\n * refused arm's member, and a claimed upgrade produced no plain answer.\n * @example\n * ```ts\n * const answer = await requestUpgrade(loopback.port, { path: '/socket', protocols: ['chat'] })\n * // { claimed: true, protocol: 'chat' }\n * ```\n */\nexport async function requestUpgrade(\n\tport: number,\n\toptions?: UpgradeOptions,\n): Promise<UpgradeResult> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tcheckBounds('Upgrade', budget, interval)\n\n\tconst signal = options?.signal\n\tsignal?.throwIfAborted()\n\n\tconst path = options?.path ?? '/'\n\tconst target = `127.0.0.1:${port}${path}`\n\tconst headers: Record<string, string> = { connection: 'Upgrade', upgrade: 'websocket' }\n\tconst protocols = options?.protocols ?? []\n\tif (protocols.length > 0) headers['sec-websocket-protocol'] = protocols.join(', ')\n\n\tconst request = requestHTTP({ agent: false, headers, host: '127.0.0.1', path, port })\n\tconst settled = Promise.withResolvers<UpgradeResult>()\n\tconst expiry = Promise.withResolvers<never>()\n\tconst aborted = Promise.withResolvers<never>()\n\tconst subscription = new AbortController()\n\trequest.on('upgrade', (response, socket) => {\n\t\tconst protocol = response.headers['sec-websocket-protocol']\n\t\tsocket.destroy()\n\t\tsettled.resolve({ claimed: true, protocol })\n\t})\n\trequest.on('response', (response) => {\n\t\tconst status = response.statusCode\n\t\tresponse.destroy()\n\t\trequest.destroy()\n\t\t// A client response reaches this listener only after its status line is parsed, so the\n\t\t// refusal is unproven; the server-side `IncomingMessage` that shares this type carries no\n\t\t// status and would drive it.\n\t\tif (status === undefined) {\n\t\t\tsettled.reject(new Error(`Upgrade request to ${target} was answered without a status`))\n\t\t\treturn\n\t\t}\n\t\tsettled.resolve({ claimed: false, status })\n\t})\n\trequest.on('error', (error) => {\n\t\trequest.destroy()\n\t\tsettled.reject(error)\n\t})\n\tsignal?.addEventListener('abort', () => aborted.reject(signal.reason), {\n\t\tonce: true,\n\t\tsignal: subscription.signal,\n\t})\n\tconst timer = setTimeout(() => {\n\t\texpiry.reject(new Error(`Upgrade request to ${target} was not answered within ${budget}ms`))\n\t}, budget)\n\trequest.end()\n\n\ttry {\n\t\treturn await Promise.race([settled.promise, expiry.promise, aborted.promise])\n\t} finally {\n\t\tclearTimeout(timer)\n\t\tsubscription.abort()\n\t\trequest.destroy()\n\t}\n}\n\n/**\n * Checks whether this host links a directory, by creating one link and reading through it.\n *\n * @returns True if the created link reports as a symbolic link, resolves to a directory, and reaches\n * the destination's contents; false otherwise, including every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks `symlinkSync(source, target, 'junction')` creates a directory junction on Windows, which\n * needs no privilege, and Node ignores the type argument off Windows, so one call covers both hosts.\n * The answer is false on a filesystem carrying neither reparse points nor symbolic links. Every call\n * probes and cleans up after itself, so a host whose answer changes is read again rather than\n * remembered.\n *\n * @example\n * ```ts\n * import { supportsDirectoryLinks } from '@orkestrel/test/server'\n *\n * supportsDirectoryLinks() // true where the host creates a symbolic link or a junction\n * ```\n */\nexport function supportsDirectoryLinks(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-directory-links-'))\n\ttry {\n\t\tconst source = join(directory, 'source')\n\t\tconst link = join(directory, 'link')\n\t\tmkdirSync(source)\n\t\twriteFileSync(join(source, 'marker.txt'), 'marked')\n\t\tsymlinkSync(source, link, 'junction')\n\t\treturn (\n\t\t\tlstatSync(link).isSymbolicLink() &&\n\t\t\tstatSync(link).isDirectory() &&\n\t\t\treadFileSync(join(link, 'marker.txt'), 'utf8') === 'marked'\n\t\t)\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether this host links a file, by creating one link and reading the file through it.\n *\n * @returns True if the file's contents are readable through the link; false otherwise, including\n * every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks `symlinkSync(source, target, 'file')` needs the symbolic-link privilege, which Windows\n * grants under Developer Mode or administrator rights and refuses with `EPERM` otherwise, so the\n * answer is true on POSIX and on a privileged Windows host. Where it is false, no mechanism reaches a\n * file through a link and a proof that reads one back cannot run. This is a separate question from\n * {@link supportsDirectoryLinks}, which an unprivileged Windows host answers true through a junction\n * while answering this one false.\n */\nexport function supportsFileLinks(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-file-links-'))\n\ttry {\n\t\tconst source = join(directory, 'source.txt')\n\t\tconst link = join(directory, 'link.txt')\n\t\twriteFileSync(source, 'linked')\n\t\tsymlinkSync(source, link, 'file')\n\t\treturn readFileSync(link, 'utf8') === 'linked'\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether POSIX permission bits round-trip through this host's `chmod` and `stat`.\n *\n * @returns True if a directory created with mode `0o700` reports that mode back; false otherwise,\n * including every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks POSIX reports `mode & 0o777 === 0o700` and Windows reports `0o666` regardless, so the\n * answer is true on POSIX and false on Windows. Storing a bit is a narrower question than enforcing\n * it: a POSIX host running as uid `0` stores every bit faithfully and bypasses the access check the\n * bits describe, so a caller that needs a permission to be enforced probes the refusal it needs\n * rather than reading this.\n *\n * @example\n * ```ts\n * import { supportsMode } from '@orkestrel/test/server'\n *\n * supportsMode() // true on a POSIX host, false on Windows\n * ```\n */\nexport function supportsMode(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-mode-'))\n\ttry {\n\t\tconst path = join(directory, 'moded')\n\t\tmkdirSync(path, { mode: 0o700 })\n\t\treturn (statSync(path).mode & 0o777) === 0o700\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether this host treats two names differing only by case as distinct files.\n *\n * @returns True if `A` and `a` hold the contents each was written with; false otherwise, including\n * every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks The names `A` and `a` differ by case and by nothing else, which is what makes the reading\n * an answer about case folding rather than an answer about two unrelated files. A case-folding volume\n * routes the second write onto the first entry, so reading the first back returns the second's\n * contents and the answer is false. The answer is true on a typical POSIX host and false on a\n * case-folding Windows or macOS volume.\n *\n * @example\n * ```ts\n * import { supportsCase } from '@orkestrel/test/server'\n *\n * supportsCase() // true on a case-sensitive volume, false on a case-folding one\n * ```\n */\nexport function supportsCase(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-case-'))\n\ttry {\n\t\tconst upper = join(directory, 'A')\n\t\tconst lower = join(directory, 'a')\n\t\twriteFileSync(upper, 'upper')\n\t\twriteFileSync(lower, 'lower')\n\t\treturn readFileSync(upper, 'utf8') === 'upper' && readFileSync(lower, 'utf8') === 'lower'\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether this host accepts a filename carrying a raw byte no UTF-8 decoder resolves.\n *\n * @returns True if a name ending in byte `0x80` is written and read back; false otherwise, including\n * every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks Byte `0x80` is an invalid UTF-8 lead byte. POSIX stores the name verbatim and Windows\n * rejects it with `ENOENT`, so the answer is true on POSIX and false on Windows. The path is passed\n * as a `Buffer` because the byte survives no string round trip.\n *\n * @example\n * ```ts\n * import { supportsBytes } from '@orkestrel/test/server'\n *\n * supportsBytes() // true on a POSIX host, false on Windows\n * ```\n */\nexport function supportsBytes(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-bytes-'))\n\ttry {\n\t\tconst name = Buffer.concat([Buffer.from(`${directory}${sep}`), Buffer.from([0x80])])\n\t\twriteFileSync(name, 'raw')\n\t\treturn readFileSync(name, 'utf8') === 'raw'\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n","import type { Server } from 'node:net'\nimport type {\n\tCookieJarInterface,\n\tLoopbackInterface,\n\tScratchInterface,\n\tScratchOptions,\n} from './types.js'\nimport { once } from 'node:events'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\treaddirSync,\n\tstatSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve, sep } from 'node:path'\nimport {\n\tcreateLink,\n\tmatchesIdentity,\n\treadIdentity,\n\tremoveTree,\n\trequireContained,\n} from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional parent directory, name prefix, and initial files.\n * @returns The scratch directory and its file operations.\n * @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains\n * `/` or `\\`; or when allocation or seeding fails.\n * @remarks Default parent: the host temporary directory. Default prefix: `orkestrel-test-`. Seed\n * keys use root-relative paths.\n *\n * @example Own a temporary directory\n * ```ts\n * import { createScratch } from '@orkestrel/test/server'\n *\n * const scratch = createScratch({ prefix: 'guide-', files: { 'src/index.ts': 'export {}\\n' } })\n *\n * scratch.read('src/index.ts') // 'export {}\\n'\n * scratch.has('src') // true\n * scratch.read('src') // throws Error: Scratch path is a directory: src\n * scratch.read('missing.ts') // undefined\n * scratch.write('../escape.ts', '') // throws Error: Path outside scratch directory: ../escape.ts\n *\n * // `write` answers the contained path it wrote, the way `ensure` and `link` answer theirs, so the\n * // path goes straight to the code under test without joining it again.\n * scratch.write('src/notes.ts', 'export {}\\n') // `${scratch.path}/src/notes.ts`\n *\n * // `ensure` is how you get an empty directory, because every `write` creates a file.\n * scratch.ensure('empty')\n * scratch.names() // ['empty', 'src']\n * scratch.names('empty') // []\n *\n * // `parent` puts the allocation somewhere other than the host temporary directory.\n * const child = createScratch({ parent: scratch.path, prefix: 'child-' })\n * scratch.names().length // 3 — 'empty', 'src', and the child allocation\n * child.destroy()\n * scratch.names().length // 2 — the child removed itself and nothing else\n *\n * // `link` creates the symbolic link the threat model names, and `read` follows it. A directory\n * // source runs on a host that creates no symbolic link too; see \"Hosts that create no symbolic\n * // link\" for what such a host does with a file source.\n * const outside = createScratch({ prefix: 'outside-', files: { 'read.ts': 'export {}\\n' } })\n * scratch.link('gate', outside.path) // `${scratch.path}/gate` — the link's own path, not its destination\n * scratch.read('gate/read.ts') // 'export {}\\n' — read through the link, at its destination\n *\n * // A link pointing out of the allocation is resolved through, so a contained path acts outside it.\n * scratch.ensure('gate/made') // `${scratch.path}/gate/made` — the lexical path, not the destination\n * outside.names() // ['made', 'read.ts'] — the directory was made under `outside.path`\n * scratch.names('gate') // ['made', 'read.ts'] — the same entries, listed through the link\n *\n * // `link` acts at the final segment rather than through it, so `gate` is occupied.\n * scratch.link('gate', outside.path) // throws Error: EEXIST: file already exists\n *\n * // `has` reads the final segment without following it, and `read` follows it.\n * scratch.link('dangling', 'missing.ts')\n * scratch.has('dangling') // true — the link is there\n * scratch.read('dangling') // undefined — what it points at is not\n *\n * // `remove` takes one contained entry and acts at the final segment, so a link goes and whatever it\n * // pointed at stays. A missing target is a no-op.\n * scratch.remove('dangling')\n * scratch.has('dangling') // false\n * scratch.remove('missing.ts') // no throw — there was nothing there\n * scratch.remove('src') // the directory and everything under it\n * scratch.names() // ['empty', 'gate']\n *\n * scratch.destroy()\n * scratch.destroy() // no-op — destroy is idempotent\n * outside.has('made') // true — destroy unlinks `gate` and leaves what it pointed at\n * outside.destroy()\n * ```\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst parent = resolve(options?.parent ?? tmpdir())\n\tconst parentStatus = lstatSync(parent, { throwIfNoEntry: false })\n\tif (parentStatus === undefined) throw new Error('Scratch parent does not exist')\n\tif (parentStatus.isSymbolicLink()) throw new Error('Scratch parent is a symbolic link')\n\tif (!parentStatus.isDirectory()) throw new Error('Scratch parent is not a directory')\n\n\tconst prefix = options?.prefix ?? 'orkestrel-test-'\n\tif (prefix.includes('/') || prefix.includes('\\\\')) {\n\t\tthrow new Error('Scratch prefix must be a name fragment')\n\t}\n\n\tconst path = mkdtempSync(`${parent}${sep}${prefix}`)\n\tconst allocation = readIdentity(statSync(path))\n\tconst unremovable = 'Scratch directory is not a removable target'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\tremoveTree(path)\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t\treturn candidate\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (!scratch.has(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\thas(target) {\n\t\t\tconst candidate = requireContained(path, target)\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\tnames(target = '.') {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) throw new Error(`Scratch path does not exist: ${target}`)\n\t\t\tif (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`)\n\t\t\treturn readdirSync(candidate).sort()\n\t\t},\n\t\tensure(target) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && !status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is not a directory: ${target}`)\n\t\t\t}\n\t\t\tif (status === undefined) mkdirSync(candidate, { recursive: true })\n\t\t\treturn candidate\n\t\t},\n\t\tlink(target, source) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\tcreateLink(candidate, source)\n\t\t\treturn candidate\n\t\t},\n\t\tremove(target) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (candidate === path) throw new Error(`${unremovable}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = lstatSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && matchesIdentity(readIdentity(status), allocation)) {\n\t\t\t\tthrow new Error(`${unremovable}: ${target}`)\n\t\t\t}\n\t\t\tremoveTree(candidate)\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return\n\t\t\tif (!matchesIdentity(readIdentity(status), allocation)) return\n\t\t\tremoveTree(path)\n\t\t},\n\t}\n\treturn scratch\n}\n\n/**\n * Starts a server on an ephemeral IPv4 loopback port.\n *\n * @param server - The unstarted server to bind.\n * @returns The bound origin, assigned port, and asynchronous teardown.\n * @throws When the server cannot bind or reports an address without a numeric port.\n */\nexport async function createLoopback(server: Server): Promise<LoopbackInterface> {\n\tserver.listen(0, '127.0.0.1')\n\tawait once(server, 'listening')\n\n\tconst address = server.address()\n\tif (\n\t\ttypeof address !== 'object' ||\n\t\taddress === null ||\n\t\t!('port' in address) ||\n\t\ttypeof address.port !== 'number'\n\t) {\n\t\tthrow new Error(`Loopback address must have a numeric port; found ${String(address)}`)\n\t}\n\n\tconst port = address.port\n\tlet destruction: Promise<void> | undefined\n\treturn {\n\t\turl: `http://127.0.0.1:${port}`,\n\t\tport,\n\t\tdestroy() {\n\t\t\tif (destruction === undefined) {\n\t\t\t\tdestruction = new Promise<void>((resolveClose, rejectClose) => {\n\t\t\t\t\tif ('closeAllConnections' in server && typeof server.closeAllConnections === 'function') {\n\t\t\t\t\t\tserver.closeAllConnections()\n\t\t\t\t\t}\n\t\t\t\t\tserver.close((error) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\terror === undefined ||\n\t\t\t\t\t\t\t('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tresolveClose()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trejectClose(error)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn destruction\n\t\t},\n\t}\n}\n\n/**\n * Creates a cookie jar that records a real response's cookies and replays them as one header.\n *\n * @returns The rendered request header, and the members that read and capture cookies.\n * @remarks Selection is by name alone: no `Domain` or `Path` matching, no `Expires` or `Secure`\n * handling, and no persistence beyond the jar. That is what a test driving one origin over one path\n * needs, and a fixture needing a browser's cookie store needs a browser rather than this.\n */\nexport function createCookieJar(): CookieJarInterface {\n\tconst cookies = new Map<string, string>()\n\treturn {\n\t\tget header() {\n\t\t\tconst pairs = [...cookies].map(([name, value]) => `${name}=${value}`)\n\t\t\treturn pairs.length === 0 ? undefined : pairs.join('; ')\n\t\t},\n\t\tread(name) {\n\t\t\treturn cookies.get(name)\n\t\t},\n\t\tcapture(response) {\n\t\t\tconst fields = response.headers.getSetCookie()\n\t\t\tfor (const field of fields) {\n\t\t\t\tconst boundary = field.indexOf(';')\n\t\t\t\tconst pair = boundary < 0 ? field : field.slice(0, boundary)\n\t\t\t\tconst separator = pair.indexOf('=')\n\t\t\t\tif (separator < 1) continue\n\n\t\t\t\tconst name = pair.slice(0, separator)\n\t\t\t\t// An origin spells a deletion `Max-Age=0` in whatever case and spacing it likes, so the\n\t\t\t\t// attribute is matched rather than compared.\n\t\t\t\tif (/;\\s*max-age\\s*=\\s*0\\s*(?:;|$)/iu.test(field)) cookies.delete(name)\n\t\t\t\telse cookies.set(name, pair.slice(separator + 1))\n\t\t\t}\n\t\t\treturn fields\n\t\t},\n\t}\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["/**\n * Caps the attempts `removeTree` makes before rethrowing a retryable removal error.\n */\nexport const REMOVE_TREE_MAX_ATTEMPTS = 10\n\n/**\n * Names the synchronous delay, in milliseconds, `removeTree` waits between attempts.\n */\nexport const REMOVE_TREE_RETRY_DELAY_MS = 100\n\n/**\n * Names the error codes `removeTree` retries; every other code rethrows immediately.\n */\nexport const REMOVE_TREE_RETRYABLE_CODES: readonly string[] = Object.freeze([\n\t'EBUSY',\n\t'ENOTEMPTY',\n\t'EPERM',\n])\n","import type { Stats } from 'node:fs'\nimport type { Socket } from 'node:net'\nimport type { WaitOptions } from '@src/core'\nimport type {\n\tInventoryOptions,\n\tScratchIdentity,\n\tScratchInterface,\n\tUpgradeOptions,\n\tUpgradeResult,\n} from './types.js'\nimport { Buffer } from 'node:buffer'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treaddirSync,\n\treadFileSync,\n\trealpathSync,\n\trmSync,\n\tstatSync,\n\tsymlinkSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { request as requestHTTP } from 'node:http'\nimport { tmpdir } from 'node:os'\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { isObject, isString } from '@orkestrel/contract'\nimport { checkBounds, waitForDelay } from '@src/core'\nimport {\n\tREMOVE_TREE_MAX_ATTEMPTS,\n\tREMOVE_TREE_RETRY_DELAY_MS,\n\tREMOVE_TREE_RETRYABLE_CODES,\n} from './constants.js'\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\t// `relative` answers with an absolute path where the target carries a root of its own — a second\n\t// drive letter or a UNC share on Windows — and that spelling names no ancestor, so the `..` tests\n\t// miss it and containment turns on `isAbsolute`.\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Resolves a target that stays below a root directory, refusing an escape.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target below the root.\n * @throws An `Error` reading `Path outside scratch directory: <target>` when the target escapes the\n * root.\n * @remarks This is {@link resolveContained} with the refusal every contained scratch operation makes\n * of an escape, so the check and its one message are stated once. Read `resolveContained` where an\n * escape is an answer rather than a refusal.\n *\n * @example\n * ```ts\n * import { requireContained } from '@orkestrel/test/server'\n *\n * requireContained('/scratch', 'nested/file.txt') // '/scratch/nested/file.txt'\n *\n * // Throws Error: Path outside scratch directory: ../escape.ts\n * requireContained('/scratch', '../escape.ts')\n * ```\n */\nexport function requireContained(root: string, target: string): string {\n\tconst candidate = resolveContained(root, target)\n\tif (candidate === undefined) throw new Error(`Path outside scratch directory: ${target}`)\n\treturn candidate\n}\n\n/**\n * Reads the identity of one allocated directory off a host status.\n *\n * @param status - The status read from the directory's path.\n * @returns The device, index node, and creation time that together name the allocation.\n *\n * @example\n * ```ts\n * import { statSync } from 'node:fs'\n * import { readIdentity } from '@orkestrel/test/server'\n *\n * const status = statSync('/scratch')\n *\n * readIdentity(status) // { birth: status.birthtimeMs, device: status.dev, inode: status.ino }\n * ```\n */\nexport function readIdentity(status: Stats): ScratchIdentity {\n\treturn { birth: status.birthtimeMs, device: status.dev, inode: status.ino }\n}\n\n/**\n * Reads the `code` an unknown thrown value carries.\n *\n * @param error - The thrown value to read.\n * @returns The string `code` the value carries, or `undefined` when it carries none.\n * @remarks The read is contained on its own terms: a value that is not an object, one carrying no\n * `code`, and one carrying a `code` that is not a string all answer `undefined`. A null-prototype\n * object is read the same way, because the key is tested with `in` rather than through\n * `hasOwnProperty`.\n *\n * @example\n * ```ts\n * import { readFileSync } from 'node:fs'\n * import { captureError } from '@orkestrel/test'\n * import { readErrorCode } from '@orkestrel/test/server'\n *\n * readErrorCode(captureError(() => readFileSync('/scratch/absent.txt', 'utf8'))) // 'ENOENT'\n * readErrorCode(new Error('refused')) // undefined\n * ```\n */\nexport function readErrorCode(error: unknown): string | undefined {\n\treturn isObject(error) && 'code' in error && isString(error.code) ? error.code : undefined\n}\n\n/**\n * Reports whether two directory identities name the same allocation.\n *\n * @param current - The identity read from the path now.\n * @param allocation - The identity recorded when the directory was allocated.\n * @returns True if the device, the index node, and the creation time all match; false otherwise.\n * @remarks All three fields are compared because none of them alone identifies an allocation. A\n * device is shared by every directory on one filesystem, an index node is reused once its directory\n * is removed, and a creation time repeats within the host's timestamp resolution.\n *\n * @example\n * ```ts\n * import { statSync } from 'node:fs'\n * import { matchesIdentity, readIdentity } from '@orkestrel/test/server'\n *\n * const allocation = readIdentity(statSync('/scratch'))\n *\n * matchesIdentity(readIdentity(statSync('/scratch')), allocation) // true\n * matchesIdentity({ birth: 3, device: 1, inode: 9 }, { birth: 3, device: 1, inode: 2 }) // false\n * ```\n */\nexport function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean {\n\treturn (\n\t\tcurrent.device === allocation.device &&\n\t\tcurrent.inode === allocation.inode &&\n\t\tcurrent.birth === allocation.birth\n\t)\n}\n\n/**\n * Reports whether a root-relative key matches an exclusion.\n *\n * @param key - The root-relative key to test.\n * @param exclusions - The normalized root-relative exclusion keys.\n * @returns True if an exclusion names the key or one of its ancestors; false otherwise.\n *\n * @example\n * ```ts\n * import { isExcluded } from '@orkestrel/test/server'\n *\n * isExcluded('src/index.ts', ['src']) // true\n * isExcluded('src-other/index.ts', ['src']) // false\n * ```\n */\nexport function isExcluded(key: string, exclusions: readonly string[]): boolean {\n\treturn exclusions.some((rule) => rule === '' || key === rule || key.startsWith(`${rule}/`))\n}\n\n/**\n * Creates a symbolic link with a directory-junction fallback for hosts that refuse symbolic links.\n *\n * @param path - The path where the link is created.\n * @param source - The destination path the link points at.\n * @throws The original link error when its code is not `EPERM`, or when the source names an\n * existing non-directory; otherwise, any error from inspecting the source or creating the junction.\n * @remarks Only `EPERM` from the first symbolic-link attempt triggers the fallback. The fallback\n * resolves the source against the link's directory. An existing non-directory rethrows the original\n * `EPERM`, while a directory or missing source is passed to a junction attempt. A missing source is\n * accepted to create a dangling junction. Where the host creates a junction, its stored value is the\n * resolved absolute path.\n *\n * @example\n * ```ts\n * import { readFileSync } from 'node:fs'\n * import { createLink } from '@orkestrel/test/server'\n *\n * // `/scratch/source` is a directory holding `file.txt`.\n * createLink('/scratch/linked', '/scratch/source')\n *\n * readFileSync('/scratch/linked/file.txt', 'utf8') // 'linked'\n * ```\n */\nexport function createLink(path: string, source: string): void {\n\ttry {\n\t\tsymlinkSync(source, path)\n\t} catch (error) {\n\t\tif (readErrorCode(error) !== 'EPERM') throw error\n\n\t\tconst resolved = resolve(dirname(path), source)\n\t\tconst status = statSync(resolved, { throwIfNoEntry: false })\n\t\tif (status !== undefined && !status.isDirectory()) throw error\n\t\tsymlinkSync(resolved, path, 'junction')\n\t}\n}\n\n/**\n * Removes a directory tree, retrying past a transient Windows handle-release race.\n *\n * @param path - The absolute directory to remove.\n * @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,\n * or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.\n * @remarks On Windows, a directory that a recently exited process still holds as its current\n * working directory throws `EPERM` for a short interval after that process exits. Node's own\n * `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed\n * against a real held directory, they neither delay nor retry before rethrowing, so the retry\n * is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait\n * at roughly one second. A hold that outlasts that second is {@link destroyScratch}'s case, which\n * retries every refusal inside a caller's budget rather than the codes named here.\n *\n * @example\n * ```ts\n * import { existsSync } from 'node:fs'\n * import { removeTree } from '@orkestrel/test/server'\n *\n * removeTree('/scratch/tree')\n *\n * existsSync('/scratch/tree') // false\n * ```\n */\nexport function removeTree(path: string): void {\n\tfor (let attempt = 1; ; attempt++) {\n\t\ttry {\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\tconst code = readErrorCode(error)\n\t\t\tif (\n\t\t\t\tcode === undefined ||\n\t\t\t\t!REMOVE_TREE_RETRYABLE_CODES.includes(code) ||\n\t\t\t\tattempt >= REMOVE_TREE_MAX_ATTEMPTS\n\t\t\t) {\n\t\t\t\tthrow error\n\t\t\t}\n\t\t\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, REMOVE_TREE_RETRY_DELAY_MS)\n\t\t}\n\t}\n}\n\n/**\n * Reads files from selected targets below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param targets - The files to read directly and directories to visit below the root.\n * @param options - Optional file extension and path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves\n * outside the root.\n * @remarks A named file is included regardless of the extension filter. An absent extension filter\n * includes every walked file. An exclusion matches whole root-relative key segments and covers every\n * key below it, and it applies to a named target and a walked entry alike.\n */\nexport function readInventory(\n\troot: URL | string,\n\ttargets: readonly string[],\n\toptions?: InventoryOptions,\n): Readonly<Record<string, string>> {\n\tconst supplied = resolve(isString(root) ? 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 (targets.length === 0) return Object.fromEntries([])\n\n\tconst exclusions = (options?.exclude ?? []).map((rule) => {\n\t\tconst unprefixed = rule.startsWith('./') ? rule.slice(2) : rule\n\t\tconst collapsed = unprefixed.replace(/\\/+/g, '/')\n\t\tconst untrailed = collapsed.endsWith('/') ? collapsed.slice(0, -1) : collapsed\n\t\treturn untrailed === '.' ? '' : untrailed\n\t})\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const target of targets) {\n\t\tconst candidate = resolveContained(base, target)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`)\n\t\tif (!status.isDirectory() && !status.isFile()) {\n\t\t\tthrow new Error(`Target is not a file or directory: ${target}`)\n\t\t}\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(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (isExcluded(key, exclusions)) continue\n\t\tif (status.isFile()) {\n\t\t\tcontents.set(key, readFileSync(physical, 'utf8'))\n\t\t\tcontinue\n\t\t}\n\t\tif (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 (isExcluded(key, exclusions)) 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\t// Walk containment is unproven because POSIX CI skips links before `realpath`;\n\t\t\t\t// a host that resolves a walked directory outside `base` would drive this branch.\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\n/**\n * Reports whether a process id names a live process.\n *\n * @param pid - The process id to read.\n * @returns True if a process holds that id at the moment of the call; false otherwise, including a\n * pid the host refuses.\n * @throws Nothing. Every host refusal reads as false.\n * @remarks This is an instantaneous observation rather than a claim of ownership. A host reuses a\n * process id after the process holding it exits, so a true answer says some process holds that id now\n * and never says it is the process the caller started. Two host answers are worth knowing. A POSIX\n * host refuses signal `0` to a process another user owns with `EPERM`, and that refusal reads as\n * false here. A pid of `0` names the caller's own process group on POSIX and the system idle process\n * on Windows, so it reads as true on both without naming a process anyone started.\n *\n * A Linux zombie — a process that has exited and whose parent has not reaped it — still accepts\n * signal `0`, so its `/proc` status is read and a `Z` state reads as false.\n *\n * @example\n * ```ts\n * import { isRunning } from '@orkestrel/test/server'\n *\n * isRunning(process.pid) // true\n * isRunning(2 ** 31) // false\n * ```\n */\nexport function isRunning(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0)\n\t} catch {\n\t\treturn false\n\t}\n\tif (process.platform !== 'linux') return true\n\n\t// The zombie refinement is unproven on a host that carries no `/proc`; a Linux gate drives it.\n\ttry {\n\t\tconst status = readFileSync(`/proc/${String(pid)}/stat`, 'utf8')\n\t\tconst boundary = status.lastIndexOf(') ')\n\t\treturn boundary < 0 || status.slice(boundary + 2, boundary + 3) !== 'Z'\n\t} catch {\n\t\treturn false\n\t}\n}\n\n/**\n * Waits for a socket to close, accepting a peer reset as a forced close.\n *\n * @param socket - The socket to wait on. One that has already closed resolves without listening.\n * @param options - The time bounds and abort signal.\n * @returns A promise that resolves when the socket emits `close`.\n * @throws The socket's own error when its code is not `ECONNRESET`, the abort reason, or an `Error`\n * when a bound is invalid or the socket does not close within the budget.\n * @remarks Default budget: `1000` milliseconds. A reset is the peer forcing the connection down, and\n * the socket still emits `close` afterwards, so `ECONNRESET` is waited past rather than raised while\n * every other error ends the wait. The interval is validated for consistency with the wait family but\n * is not used, because this helper parks on the socket's events. Both listeners are removed on every\n * settlement, so a caller may wait on one socket repeatedly.\n *\n * @example\n * ```ts\n * import { connect, createServer } from 'node:net'\n * import { createLoopback, waitForSocketClose } from '@orkestrel/test/server'\n *\n * const loopback = await createLoopback(createServer((socket) => socket.end()))\n * const client = connect(loopback.port, '127.0.0.1')\n *\n * await waitForSocketClose(client, { budget: 1000 }) // undefined\n * client.destroyed // true\n *\n * await loopback.destroy()\n * ```\n */\nexport async function waitForSocketClose(socket: Socket, options?: WaitOptions): Promise<void> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tcheckBounds('Socket', budget, interval)\n\n\tconst signal = options?.signal\n\tsignal?.throwIfAborted()\n\tif (socket.closed) return\n\n\t// The resolvers are the listeners themselves, so the same references remove them afterwards.\n\tconst closed = Promise.withResolvers<boolean>()\n\tconst failed = Promise.withResolvers<NodeJS.ErrnoException>()\n\tconst expiry = Promise.withResolvers<never>()\n\tconst aborted = Promise.withResolvers<never>()\n\tconst subscription = new AbortController()\n\tsocket.on('close', closed.resolve)\n\tsocket.on('error', failed.resolve)\n\tsignal?.addEventListener('abort', () => aborted.reject(signal.reason), {\n\t\tonce: true,\n\t\tsignal: subscription.signal,\n\t})\n\tconst timer = setTimeout(() => {\n\t\texpiry.reject(new Error(`Socket did not close within ${budget}ms`))\n\t}, budget)\n\n\ttry {\n\t\tconst error = await Promise.race([\n\t\t\tclosed.promise.then(() => undefined),\n\t\t\tfailed.promise,\n\t\t\texpiry.promise,\n\t\t\taborted.promise,\n\t\t])\n\t\tif (error === undefined) return\n\t\tif (error.code !== 'ECONNRESET') throw error\n\t\tawait Promise.race([closed.promise, expiry.promise, aborted.promise])\n\t} finally {\n\t\tclearTimeout(timer)\n\t\tsubscription.abort()\n\t\tsocket.off('close', closed.resolve)\n\t\tsocket.off('error', failed.resolve)\n\t}\n}\n\n/**\n * Destroys a scratch directory, retrying until the host releases it.\n *\n * @param scratch - The scratch directory to destroy.\n * @param options - The time bounds and abort signal.\n * @returns A promise that resolves once `destroy()` returns without throwing.\n * @throws The abort reason, or an `Error` when a bound is invalid or the budget elapses. The\n * exhaustion error carries the last host refusal as its `cause`.\n * @remarks Default budget: `10000` milliseconds. Default interval: `25` milliseconds. A host holds a\n * directory for a short interval after the process that held it exits, and a recently stopped child's\n * working directory is the case this exists for, so removal is attempted until the host lets go\n * rather than exactly once. {@link ScratchInterface.destroy} stays synchronous and is unchanged; this\n * is the bounded retry around it. A directory nothing releases still fails, with the host's own\n * refusal as the `cause`.\n *\n * Every refusal is retried, deliberately, and that is wider than {@link removeTree}'s policy: that\n * one retries the codes {@link REMOVE_TREE_RETRYABLE_CODES} names and rethrows the rest at once.\n * The hold this waits out is not classifiable across hosts — Windows reports a working-directory\n * hold as `EPERM`, POSIX hosts and network filesystems report their own — so a code list here would\n * be a list of the hosts it had been run on. The residual is the cost of that: a fault no wait can\n * clear, such as a path removed from under the allocation or a permission the process never had,\n * spends the whole budget before it surfaces, and it surfaces wrapped in the exhaustion error with\n * the host's refusal as `cause` rather than by identity. Pass a shorter `budget` or a `signal`\n * wherever a caller must bound that cost.\n */\nexport async function destroyScratch(\n\tscratch: ScratchInterface,\n\toptions?: WaitOptions,\n): Promise<void> {\n\tconst budget = options?.budget ?? 10_000\n\tconst interval = options?.interval ?? 25\n\tcheckBounds('Scratch', budget, interval)\n\n\tconst start = performance.now()\n\tlet refusal: unknown\n\twhile (true) {\n\t\toptions?.signal?.throwIfAborted()\n\t\ttry {\n\t\t\tscratch.destroy()\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\trefusal = error\n\t\t}\n\n\t\tif (performance.now() - start >= budget) {\n\t\t\tthrow new Error(`Scratch directory was not destroyed within ${budget}ms`, {\n\t\t\t\tcause: refusal,\n\t\t\t})\n\t\t}\n\t\tawait waitForDelay(interval)\n\t}\n}\n\n/**\n * Drives a real client upgrade request against a loopback port and reports what the server did.\n *\n * @param port - The port the server listens on at `127.0.0.1`.\n * @param options - Optional request path, offered subprotocols, time bounds, and abort signal.\n * @returns A promise resolving to the server's answer: a claimed upgrade with the protocol it\n * selected, or a refusal with the status it answered.\n * @throws The client's own transport error, such as the `ECONNREFUSED` a closed port answers, the\n * abort reason, or an `Error` when a bound is invalid or the server does not answer within the\n * budget.\n * @remarks Default budget: `1000` milliseconds. The request carries `Connection: Upgrade` and\n * `Upgrade: websocket`, which is what makes a server's `upgrade` handler the one that answers it.\n * The `upgrade`, `response`, and `error` events are mutually exclusive in practice and the promise\n * settles on whichever arrives first, so a second event changes nothing. The client socket is\n * destroyed before every settlement, on the claimed path because an upgraded socket is detached from\n * the request and outlives it otherwise. The request is made with no agent, so no pooled connection\n * survives the call to keep a suite's event loop alive.\n *\n * A server that accepts the connection and answers nothing raises no transport error, so the budget\n * is what ends that call: the rejection names the port and path it was waiting on. The interval is\n * validated for consistency with the wait family but is not used, because this helper parks on the\n * request's events.\n *\n * A `101` is the claimed path's status on the wire and is deliberately not reported: `status` is the\n * refused arm's member, and a claimed upgrade produced no plain answer.\n * @example\n * ```ts\n * const answer = await requestUpgrade(loopback.port, { path: '/socket', protocols: ['chat'] })\n * // { claimed: true, protocol: 'chat' }\n * ```\n */\nexport async function requestUpgrade(\n\tport: number,\n\toptions?: UpgradeOptions,\n): Promise<UpgradeResult> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tcheckBounds('Upgrade', budget, interval)\n\n\tconst signal = options?.signal\n\tsignal?.throwIfAborted()\n\n\tconst path = options?.path ?? '/'\n\tconst target = `127.0.0.1:${port}${path}`\n\tconst headers: Record<string, string> = { connection: 'Upgrade', upgrade: 'websocket' }\n\tconst protocols = options?.protocols ?? []\n\tif (protocols.length > 0) headers['sec-websocket-protocol'] = protocols.join(', ')\n\n\tconst request = requestHTTP({ agent: false, headers, host: '127.0.0.1', path, port })\n\tconst settled = Promise.withResolvers<UpgradeResult>()\n\tconst expiry = Promise.withResolvers<never>()\n\tconst aborted = Promise.withResolvers<never>()\n\tconst subscription = new AbortController()\n\trequest.on('upgrade', (response, socket) => {\n\t\tconst protocol = response.headers['sec-websocket-protocol']\n\t\tsocket.destroy()\n\t\tsettled.resolve({ claimed: true, protocol })\n\t})\n\trequest.on('response', (response) => {\n\t\tconst status = response.statusCode\n\t\tresponse.destroy()\n\t\trequest.destroy()\n\t\t// A client response reaches this listener only after its status line is parsed, so the\n\t\t// refusal is unproven; the server-side `IncomingMessage` that shares this type carries no\n\t\t// status and would drive it.\n\t\tif (status === undefined) {\n\t\t\tsettled.reject(new Error(`Upgrade request to ${target} was answered without a status`))\n\t\t\treturn\n\t\t}\n\t\tsettled.resolve({ claimed: false, status })\n\t})\n\trequest.on('error', (error) => {\n\t\trequest.destroy()\n\t\tsettled.reject(error)\n\t})\n\tsignal?.addEventListener('abort', () => aborted.reject(signal.reason), {\n\t\tonce: true,\n\t\tsignal: subscription.signal,\n\t})\n\tconst timer = setTimeout(() => {\n\t\texpiry.reject(new Error(`Upgrade request to ${target} was not answered within ${budget}ms`))\n\t}, budget)\n\trequest.end()\n\n\ttry {\n\t\treturn await Promise.race([settled.promise, expiry.promise, aborted.promise])\n\t} finally {\n\t\tclearTimeout(timer)\n\t\tsubscription.abort()\n\t\trequest.destroy()\n\t}\n}\n\n/**\n * Checks whether this host links a directory, by creating one link and reading through it.\n *\n * @returns True if the created link reports as a symbolic link, resolves to a directory, and reaches\n * the destination's contents; false otherwise, including every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks `symlinkSync(source, target, 'junction')` creates a directory junction on Windows, which\n * needs no privilege, and Node ignores the type argument off Windows, so one call covers both hosts.\n * The answer is false on a filesystem carrying neither reparse points nor symbolic links. Every call\n * probes and cleans up after itself, so a host whose answer changes is read again rather than\n * remembered.\n *\n * @example\n * ```ts\n * import { supportsDirectoryLinks } from '@orkestrel/test/server'\n *\n * supportsDirectoryLinks() // true where the host creates a symbolic link or a junction\n * ```\n */\nexport function supportsDirectoryLinks(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-directory-links-'))\n\ttry {\n\t\tconst source = join(directory, 'source')\n\t\tconst link = join(directory, 'link')\n\t\tmkdirSync(source)\n\t\twriteFileSync(join(source, 'marker.txt'), 'marked')\n\t\tsymlinkSync(source, link, 'junction')\n\t\treturn (\n\t\t\tlstatSync(link).isSymbolicLink() &&\n\t\t\tstatSync(link).isDirectory() &&\n\t\t\treadFileSync(join(link, 'marker.txt'), 'utf8') === 'marked'\n\t\t)\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether this host links a file, by creating one link and reading the file through it.\n *\n * @returns True if the file's contents are readable through the link; false otherwise, including\n * every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks `symlinkSync(source, target, 'file')` needs the symbolic-link privilege, which Windows\n * grants under Developer Mode or administrator rights and refuses with `EPERM` otherwise, so the\n * answer is true on POSIX and on a privileged Windows host. Where it is false, no mechanism reaches a\n * file through a link and a proof that reads one back cannot run. This is a separate question from\n * {@link supportsDirectoryLinks}, which an unprivileged Windows host answers true through a junction\n * while answering this one false.\n */\nexport function supportsFileLinks(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-file-links-'))\n\ttry {\n\t\tconst source = join(directory, 'source.txt')\n\t\tconst link = join(directory, 'link.txt')\n\t\twriteFileSync(source, 'linked')\n\t\tsymlinkSync(source, link, 'file')\n\t\treturn readFileSync(link, 'utf8') === 'linked'\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether POSIX permission bits round-trip through this host's `chmod` and `stat`.\n *\n * @returns True if a directory created with mode `0o700` reports that mode back; false otherwise,\n * including every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks POSIX reports `mode & 0o777 === 0o700` and Windows reports `0o666` regardless, so the\n * answer is true on POSIX and false on Windows. Storing a bit is a narrower question than enforcing\n * it: a POSIX host running as uid `0` stores every bit faithfully and bypasses the access check the\n * bits describe, so a caller that needs a permission to be enforced probes the refusal it needs\n * rather than reading this.\n *\n * @example\n * ```ts\n * import { supportsMode } from '@orkestrel/test/server'\n *\n * supportsMode() // true on a POSIX host, false on Windows\n * ```\n */\nexport function supportsMode(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-mode-'))\n\ttry {\n\t\tconst path = join(directory, 'moded')\n\t\tmkdirSync(path, { mode: 0o700 })\n\t\treturn (statSync(path).mode & 0o777) === 0o700\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether this host treats two names differing only by case as distinct files.\n *\n * @returns True if `A` and `a` hold the contents each was written with; false otherwise, including\n * every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks The names `A` and `a` differ by case and by nothing else, which is what makes the reading\n * an answer about case folding rather than an answer about two unrelated files. A case-folding volume\n * routes the second write onto the first entry, so reading the first back returns the second's\n * contents and the answer is false. The answer is true on a typical POSIX host and false on a\n * case-folding Windows or macOS volume.\n *\n * @example\n * ```ts\n * import { supportsCase } from '@orkestrel/test/server'\n *\n * supportsCase() // true on a case-sensitive volume, false on a case-folding one\n * ```\n */\nexport function supportsCase(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-case-'))\n\ttry {\n\t\tconst upper = join(directory, 'A')\n\t\tconst lower = join(directory, 'a')\n\t\twriteFileSync(upper, 'upper')\n\t\twriteFileSync(lower, 'lower')\n\t\treturn readFileSync(upper, 'utf8') === 'upper' && readFileSync(lower, 'utf8') === 'lower'\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n\n/**\n * Checks whether this host accepts a filename carrying a raw byte no UTF-8 decoder resolves.\n *\n * @returns True if a name ending in byte `0x80` is written and read back; false otherwise, including\n * every host refusal.\n * @throws Nothing the attempt itself raises. Failing to allocate the probe directory, and failing to\n * remove it afterwards, both propagate.\n * @remarks Byte `0x80` is an invalid UTF-8 lead byte. POSIX stores the name verbatim and Windows\n * rejects it with `ENOENT`, so the answer is true on POSIX and false on Windows. The path is passed\n * as a `Buffer` because the byte survives no string round trip.\n *\n * @example\n * ```ts\n * import { supportsBytes } from '@orkestrel/test/server'\n *\n * supportsBytes() // true on a POSIX host, false on Windows\n * ```\n */\nexport function supportsBytes(): boolean {\n\tconst directory = mkdtempSync(join(tmpdir(), 'orkestrel-test-bytes-'))\n\ttry {\n\t\tconst name = Buffer.concat([Buffer.from(`${directory}${sep}`), Buffer.from([0x80])])\n\t\twriteFileSync(name, 'raw')\n\t\treturn readFileSync(name, 'utf8') === 'raw'\n\t} catch {\n\t\treturn false\n\t} finally {\n\t\tremoveTree(directory)\n\t}\n}\n","import type { Server } from 'node:net'\nimport type {\n\tCookieJarInterface,\n\tLoopbackInterface,\n\tScratchInterface,\n\tScratchOptions,\n} from './types.js'\nimport { isFunction, isNumber, isObject } from '@orkestrel/contract'\nimport { once } from 'node:events'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\treaddirSync,\n\tstatSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve, sep } from 'node:path'\nimport {\n\tcreateLink,\n\tmatchesIdentity,\n\treadIdentity,\n\tremoveTree,\n\trequireContained,\n} from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional parent directory, name prefix, and initial files.\n * @returns The scratch directory and its file operations.\n * @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains\n * `/` or `\\`; or when allocation or seeding fails.\n * @remarks Default parent: the host temporary directory. Default prefix: `orkestrel-test-`. Seed\n * keys use root-relative paths.\n *\n * @example Own a temporary directory\n * ```ts\n * import { createScratch } from '@orkestrel/test/server'\n *\n * const scratch = createScratch({ prefix: 'guide-', files: { 'src/index.ts': 'export {}\\n' } })\n *\n * scratch.read('src/index.ts') // 'export {}\\n'\n * scratch.has('src') // true\n * scratch.read('src') // throws Error: Scratch path is a directory: src\n * scratch.read('missing.ts') // undefined\n * scratch.write('../escape.ts', '') // throws Error: Path outside scratch directory: ../escape.ts\n *\n * // `write` answers the contained path it wrote, the way `ensure` and `link` answer theirs, so the\n * // path goes straight to the code under test without joining it again.\n * scratch.write('src/notes.ts', 'export {}\\n') // `${scratch.path}/src/notes.ts`\n *\n * // `ensure` is how you get an empty directory, because every `write` creates a file.\n * scratch.ensure('empty')\n * scratch.names() // ['empty', 'src']\n * scratch.names('empty') // []\n *\n * // `parent` puts the allocation somewhere other than the host temporary directory.\n * const child = createScratch({ parent: scratch.path, prefix: 'child-' })\n * scratch.names().length // 3 — 'empty', 'src', and the child allocation\n * child.destroy()\n * scratch.names().length // 2 — the child removed itself and nothing else\n *\n * // `link` creates the symbolic link the threat model names, and `read` follows it. A directory\n * // source runs on a host that creates no symbolic link too; see \"Hosts that create no symbolic\n * // link\" for what such a host does with a file source.\n * const outside = createScratch({ prefix: 'outside-', files: { 'read.ts': 'export {}\\n' } })\n * scratch.link('gate', outside.path) // `${scratch.path}/gate` — the link's own path, not its destination\n * scratch.read('gate/read.ts') // 'export {}\\n' — read through the link, at its destination\n *\n * // A link pointing out of the allocation is resolved through, so a contained path acts outside it.\n * scratch.ensure('gate/made') // `${scratch.path}/gate/made` — the lexical path, not the destination\n * outside.names() // ['made', 'read.ts'] — the directory was made under `outside.path`\n * scratch.names('gate') // ['made', 'read.ts'] — the same entries, listed through the link\n *\n * // `link` acts at the final segment rather than through it, so `gate` is occupied.\n * scratch.link('gate', outside.path) // throws Error: EEXIST: file already exists\n *\n * // `has` reads the final segment without following it, and `read` follows it.\n * scratch.link('dangling', 'missing.ts')\n * scratch.has('dangling') // true — the link is there\n * scratch.read('dangling') // undefined — what it points at is not\n *\n * // `remove` takes one contained entry and acts at the final segment, so a link goes and whatever it\n * // pointed at stays. A missing target is a no-op.\n * scratch.remove('dangling')\n * scratch.has('dangling') // false\n * scratch.remove('missing.ts') // no throw — there was nothing there\n * scratch.remove('src') // the directory and everything under it\n * scratch.names() // ['empty', 'gate']\n *\n * scratch.destroy()\n * scratch.destroy() // no-op — destroy is idempotent\n * outside.has('made') // true — destroy unlinks `gate` and leaves what it pointed at\n * outside.destroy()\n * ```\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst parent = resolve(options?.parent ?? tmpdir())\n\tconst parentStatus = lstatSync(parent, { throwIfNoEntry: false })\n\tif (parentStatus === undefined) throw new Error('Scratch parent does not exist')\n\tif (parentStatus.isSymbolicLink()) throw new Error('Scratch parent is a symbolic link')\n\tif (!parentStatus.isDirectory()) throw new Error('Scratch parent is not a directory')\n\n\tconst prefix = options?.prefix ?? 'orkestrel-test-'\n\tif (prefix.includes('/') || prefix.includes('\\\\')) {\n\t\tthrow new Error('Scratch prefix must be a name fragment')\n\t}\n\n\tconst path = mkdtempSync(`${parent}${sep}${prefix}`)\n\tconst allocation = readIdentity(statSync(path))\n\tconst unremovable = 'Scratch directory is not a removable target'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\tremoveTree(path)\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t\treturn candidate\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (!scratch.has(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\thas(target) {\n\t\t\tconst candidate = requireContained(path, target)\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\tnames(target = '.') {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) throw new Error(`Scratch path does not exist: ${target}`)\n\t\t\tif (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`)\n\t\t\treturn readdirSync(candidate).sort()\n\t\t},\n\t\tensure(target) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && !status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is not a directory: ${target}`)\n\t\t\t}\n\t\t\tif (status === undefined) mkdirSync(candidate, { recursive: true })\n\t\t\treturn candidate\n\t\t},\n\t\tlink(target, source) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\tcreateLink(candidate, source)\n\t\t\treturn candidate\n\t\t},\n\t\tremove(target) {\n\t\t\tconst candidate = requireContained(path, target)\n\t\t\tif (candidate === path) throw new Error(`${unremovable}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = lstatSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && matchesIdentity(readIdentity(status), allocation)) {\n\t\t\t\tthrow new Error(`${unremovable}: ${target}`)\n\t\t\t}\n\t\t\tremoveTree(candidate)\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return\n\t\t\tif (!matchesIdentity(readIdentity(status), allocation)) return\n\t\t\tremoveTree(path)\n\t\t},\n\t}\n\treturn scratch\n}\n\n/**\n * Starts a server on an ephemeral IPv4 loopback port.\n *\n * @param server - The unstarted server to bind.\n * @returns The bound origin, assigned port, and asynchronous teardown.\n * @throws When the server cannot bind or reports an address without a numeric port.\n */\nexport async function createLoopback(server: Server): Promise<LoopbackInterface> {\n\tserver.listen(0, '127.0.0.1')\n\tawait once(server, 'listening')\n\n\tconst address = server.address()\n\tif (!isObject(address) || !('port' in address) || !isNumber(address.port)) {\n\t\tthrow new Error(`Loopback address must have a numeric port; found ${String(address)}`)\n\t}\n\n\tconst port = address.port\n\tlet destruction: Promise<void> | undefined\n\treturn {\n\t\turl: `http://127.0.0.1:${port}`,\n\t\tport,\n\t\tdestroy() {\n\t\t\tif (destruction === undefined) {\n\t\t\t\tdestruction = new Promise<void>((resolveClose, rejectClose) => {\n\t\t\t\t\tif ('closeAllConnections' in server && isFunction(server.closeAllConnections)) {\n\t\t\t\t\t\tserver.closeAllConnections()\n\t\t\t\t\t}\n\t\t\t\t\tserver.close((error) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\terror === undefined ||\n\t\t\t\t\t\t\t('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tresolveClose()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trejectClose(error)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn destruction\n\t\t},\n\t}\n}\n\n/**\n * Creates a cookie jar that records a real response's cookies and replays them as one header.\n *\n * @returns The rendered request header, and the members that read and capture cookies.\n * @remarks Selection is by name alone: no `Domain` or `Path` matching, no `Expires` or `Secure`\n * handling, and no persistence beyond the jar. That is what a test driving one origin over one path\n * needs, and a fixture needing a browser's cookie store needs a browser rather than this.\n */\nexport function createCookieJar(): CookieJarInterface {\n\tconst cookies = new Map<string, string>()\n\treturn {\n\t\tget header() {\n\t\t\tconst pairs = [...cookies].map(([name, value]) => `${name}=${value}`)\n\t\t\treturn pairs.length === 0 ? undefined : pairs.join('; ')\n\t\t},\n\t\tread(name) {\n\t\t\treturn cookies.get(name)\n\t\t},\n\t\tcapture(response) {\n\t\t\tconst fields = response.headers.getSetCookie()\n\t\t\tfor (const field of fields) {\n\t\t\t\tconst boundary = field.indexOf(';')\n\t\t\t\tconst pair = boundary < 0 ? field : field.slice(0, boundary)\n\t\t\t\tconst separator = pair.indexOf('=')\n\t\t\t\tif (separator < 1) continue\n\n\t\t\t\tconst name = pair.slice(0, separator)\n\t\t\t\t// An origin spells a deletion `Max-Age=0` in whatever case and spacing it likes, so the\n\t\t\t\t// attribute is matched rather than compared.\n\t\t\t\tif (/;\\s*max-age\\s*=\\s*0\\s*(?:;|$)/iu.test(field)) cookies.delete(name)\n\t\t\t\telse cookies.set(name, pair.slice(separator + 1))\n\t\t\t}\n\t\t\treturn fields\n\t\t},\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;AAGA,IAAa,2BAA2B;;;;AAKxC,IAAa,6BAA6B;;;;AAK1C,IAAa,8BAAiD,OAAO,OAAO;CAC3E;CACA;CACA;AACD,CAAC;;;;;;;;;;ACyBD,SAAgB,iBAAiB,MAAc,QAAoC;CAClF,MAAM,aAAA,GAAY,UAAA,QAAA,CAAQ,MAAM,MAAM;CACtC,MAAM,aAAA,GAAY,UAAA,SAAA,CAAS,MAAM,SAAS;CAI1C,IAAI,cAAc,QAAQ,UAAU,WAAW,KAAK,UAAA,KAAK,MAAA,GAAK,UAAA,WAAA,CAAW,SAAS,GACjF;CAED,OAAO;AACR;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,iBAAiB,MAAc,QAAwB;CACtE,MAAM,YAAY,iBAAiB,MAAM,MAAM;CAC/C,IAAI,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,mCAAmC,QAAQ;CACxF,OAAO;AACR;;;;;;;;;;;;;;;;;AAkBA,SAAgB,aAAa,QAAgC;CAC5D,OAAO;EAAE,OAAO,OAAO;EAAa,QAAQ,OAAO;EAAK,OAAO,OAAO;CAAI;AAC3E;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,cAAc,OAAoC;CACjE,QAAA,GAAO,oBAAA,SAAA,CAAS,KAAK,KAAK,UAAU,UAAA,GAAS,oBAAA,SAAA,CAAS,MAAM,IAAI,IAAI,MAAM,OAAO,KAAA;AAClF;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,gBAAgB,SAA0B,YAAsC;CAC/F,OACC,QAAQ,WAAW,WAAW,UAC9B,QAAQ,UAAU,WAAW,SAC7B,QAAQ,UAAU,WAAW;AAE/B;;;;;;;;;;;;;;;;AAiBA,SAAgB,WAAW,KAAa,YAAwC;CAC/E,OAAO,WAAW,MAAM,SAAS,SAAS,MAAM,QAAQ,QAAQ,IAAI,WAAW,GAAG,KAAK,EAAE,CAAC;AAC3F;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,WAAW,MAAc,QAAsB;CAC9D,IAAI;EACH,CAAA,GAAA,QAAA,YAAA,CAAY,QAAQ,IAAI;CACzB,SAAS,OAAO;EACf,IAAI,cAAc,KAAK,MAAM,SAAS,MAAM;EAE5C,MAAM,YAAA,GAAW,UAAA,QAAA,EAAA,GAAQ,UAAA,QAAA,CAAQ,IAAI,GAAG,MAAM;EAC9C,MAAM,UAAA,GAAS,QAAA,SAAA,CAAS,UAAU,EAAE,gBAAgB,MAAM,CAAC;EAC3D,IAAI,WAAW,KAAA,KAAa,CAAC,OAAO,YAAY,GAAG,MAAM;EACzD,CAAA,GAAA,QAAA,YAAA,CAAY,UAAU,MAAM,UAAU;CACvC;AACD;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,WAAW,MAAoB;CAC9C,KAAK,IAAI,UAAU,IAAK,WACvB,IAAI;EACH,CAAA,GAAA,QAAA,OAAA,CAAO,MAAM;GAAE,OAAO;GAAM,WAAW;EAAK,CAAC;EAC7C;CACD,SAAS,OAAO;EACf,MAAM,OAAO,cAAc,KAAK;EAChC,IACC,SAAS,KAAA,KACT,CAAC,4BAA4B,SAAS,IAAI,KAC1C,WAAA,IAEA,MAAM;EAEP,QAAQ,KAAK,IAAI,WAAW,IAAI,kBAAkB,CAAC,CAAC,GAAG,GAAG,GAAA,GAA6B;CACxF;AAEF;;;;;;;;;;;;;;AAeA,SAAgB,cACf,MACA,SACA,SACmC;CACnC,MAAM,YAAA,GAAW,UAAA,QAAA,EAAA,GAAQ,oBAAA,SAAA,CAAS,IAAI,IAAI,QAAA,GAAO,SAAA,cAAA,CAAc,IAAI,CAAC;CACpE,MAAM,cAAA,GAAa,QAAA,UAAA,CAAU,QAAQ;CACrC,IAAI,WAAW,eAAe,GAAG,MAAM,IAAI,MAAM,yBAAyB;CAC1E,IAAI,CAAC,WAAW,YAAY,GAAG,MAAM,IAAI,MAAM,yBAAyB;CAExE,MAAM,OAAO,QAAA,aAAa,OAAO,QAAQ;CACzC,IAAI,QAAQ,WAAW,GAAG,OAAO,OAAO,YAAY,CAAC,CAAC;CAEtD,MAAM,cAAc,SAAS,WAAW,CAAC,EAAA,CAAG,KAAK,SAAS;EAEzD,MAAM,aADa,KAAK,WAAW,IAAI,IAAI,KAAK,MAAM,CAAC,IAAI,KAAA,CAC9B,QAAQ,QAAQ,GAAG;EAChD,MAAM,YAAY,UAAU,SAAS,GAAG,IAAI,UAAU,MAAM,GAAG,EAAE,IAAI;EACrE,OAAO,cAAc,MAAM,KAAK;CACjC,CAAC;CACD,MAAM,UAAoB,CAAC;CAC3B,MAAM,yBAAS,IAAI,IAAY;CAC/B,MAAM,2BAAW,IAAI,IAAoB;CAEzC,KAAK,MAAM,UAAU,SAAS;EAC7B,MAAM,YAAY,iBAAiB,MAAM,MAAM;EAC/C,IAAI,cAAc,KAAA,GACjB,MAAM,IAAI,MAAM,wBAAwB,QAAQ;EAGjD,MAAM,UAAA,GAAS,QAAA,UAAA,CAAU,SAAS;EAClC,IAAI,OAAO,eAAe,GAAG,MAAM,IAAI,MAAM,8BAA8B,QAAQ;EACnF,IAAI,CAAC,OAAO,YAAY,KAAK,CAAC,OAAO,OAAO,GAC3C,MAAM,IAAI,MAAM,sCAAsC,QAAQ;EAG/D,MAAM,WAAW,QAAA,aAAa,OAAO,SAAS;EAC9C,MAAM,WAAW,iBAAiB,OAAA,GAAM,UAAA,SAAA,CAAS,MAAM,QAAQ,CAAC;EAChE,IAAI,aAAa,KAAA,GAChB,MAAM,IAAI,MAAM,wBAAwB,QAAQ;EAGjD,MAAM,OAAA,GAAM,UAAA,SAAA,CAAS,MAAM,QAAQ,CAAC,CAAC,MAAM,UAAA,GAAG,CAAC,CAAC,KAAK,GAAG;EACxD,IAAI,WAAW,KAAK,UAAU,GAAG;EACjC,IAAI,OAAO,OAAO,GAAG;GACpB,SAAS,IAAI,MAAA,GAAK,QAAA,aAAA,CAAa,UAAU,MAAM,CAAC;GAChD;EACD;EACA,IAAI,OAAO,IAAI,QAAQ,GAAG;EAC1B,OAAO,IAAI,QAAQ;EACnB,QAAQ,KAAK,QAAQ;CACtB;CAEA,OAAO,QAAQ,SAAS,GAAG;EAC1B,MAAM,YAAY,QAAQ,IAAI;EAC9B,IAAI,cAAc,KAAA,GAAW;EAE7B,KAAK,MAAM,UAAA,GAAS,QAAA,YAAA,CAAY,WAAW,EAAE,eAAe,KAAK,CAAC,GAAG;GACpE,MAAM,QAAA,GAAO,UAAA,QAAA,CAAQ,WAAW,MAAM,IAAI;GAC1C,MAAM,UAAA,GAAS,QAAA,UAAA,CAAU,IAAI;GAC7B,IAAI,OAAO,eAAe,GAAG;GAE7B,MAAM,OAAA,GAAM,UAAA,SAAA,CAAS,MAAM,IAAI,CAAC,CAAC,MAAM,UAAA,GAAG,CAAC,CAAC,KAAK,GAAG;GACpD,IAAI,WAAW,KAAK,UAAU,GAAG;GAEjC,IAAI,OAAO,YAAY,GAAG;IACzB,MAAM,WAAW,QAAA,aAAa,OAAO,IAAI;IAIzC,IAHiB,iBAAiB,OAAA,GAAM,UAAA,SAAA,CAAS,MAAM,QAAQ,CAG3D,MAAa,KAAA,KAAa,OAAO,IAAI,QAAQ,GAChD;IAED,OAAO,IAAI,QAAQ;IACnB,QAAQ,KAAK,QAAQ;IACrB;GACD;GAEA,IACC,CAAC,OAAO,OAAO,KACd,SAAS,eAAe,KAAA,KACxB,CAAC,QAAQ,WAAW,MAAM,cAAc,MAAM,KAAK,SAAS,SAAS,CAAC,GAEvE;GAED,SAAS,IAAI,MAAA,GAAK,QAAA,aAAA,CAAa,MAAM,MAAM,CAAC;EAC7C;CACD;CAEA,OAAO,OAAO,YACb,MAAM,KAAK,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,WAAY,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI,CAAE,CAC1F;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,UAAU,KAAsB;CAC/C,IAAI;EACH,QAAQ,KAAK,KAAK,CAAC;CACpB,QAAQ;EACP,OAAO;CACR;CACA,IAAI,QAAQ,aAAa,SAAS,OAAO;CAGzC,IAAI;EACH,MAAM,UAAA,GAAS,QAAA,aAAA,CAAa,SAAS,OAAO,GAAG,EAAE,QAAQ,MAAM;EAC/D,MAAM,WAAW,OAAO,YAAY,IAAI;EACxC,OAAO,WAAW,KAAK,OAAO,MAAM,WAAW,GAAG,WAAW,CAAC,MAAM;CACrE,QAAQ;EACP,OAAO;CACR;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,eAAsB,mBAAmB,QAAgB,SAAsC;CAC9F,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,WAAW,SAAS,YAAY;CACtC,CAAA,GAAA,UAAA,YAAA,CAAY,UAAU,QAAQ,QAAQ;CAEtC,MAAM,SAAS,SAAS;CACxB,QAAQ,eAAe;CACvB,IAAI,OAAO,QAAQ;CAGnB,MAAM,SAAS,QAAQ,cAAuB;CAC9C,MAAM,SAAS,QAAQ,cAAqC;CAC5D,MAAM,SAAS,QAAQ,cAAqB;CAC5C,MAAM,UAAU,QAAQ,cAAqB;CAC7C,MAAM,eAAe,IAAI,gBAAgB;CACzC,OAAO,GAAG,SAAS,OAAO,OAAO;CACjC,OAAO,GAAG,SAAS,OAAO,OAAO;CACjC,QAAQ,iBAAiB,eAAe,QAAQ,OAAO,OAAO,MAAM,GAAG;EACtE,MAAM;EACN,QAAQ,aAAa;CACtB,CAAC;CACD,MAAM,QAAQ,iBAAiB;EAC9B,OAAO,uBAAO,IAAI,MAAM,+BAA+B,OAAO,GAAG,CAAC;CACnE,GAAG,MAAM;CAET,IAAI;EACH,MAAM,QAAQ,MAAM,QAAQ,KAAK;GAChC,OAAO,QAAQ,WAAW,KAAA,CAAS;GACnC,OAAO;GACP,OAAO;GACP,QAAQ;EACT,CAAC;EACD,IAAI,UAAU,KAAA,GAAW;EACzB,IAAI,MAAM,SAAS,cAAc,MAAM;EACvC,MAAM,QAAQ,KAAK;GAAC,OAAO;GAAS,OAAO;GAAS,QAAQ;EAAO,CAAC;CACrE,UAAU;EACT,aAAa,KAAK;EAClB,aAAa,MAAM;EACnB,OAAO,IAAI,SAAS,OAAO,OAAO;EAClC,OAAO,IAAI,SAAS,OAAO,OAAO;CACnC;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,eAAsB,eACrB,SACA,SACgB;CAChB,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,WAAW,SAAS,YAAY;CACtC,CAAA,GAAA,UAAA,YAAA,CAAY,WAAW,QAAQ,QAAQ;CAEvC,MAAM,QAAQ,YAAY,IAAI;CAC9B,IAAI;CACJ,OAAO,MAAM;EACZ,SAAS,QAAQ,eAAe;EAChC,IAAI;GACH,QAAQ,QAAQ;GAChB;EACD,SAAS,OAAO;GACf,UAAU;EACX;EAEA,IAAI,YAAY,IAAI,IAAI,SAAS,QAChC,MAAM,IAAI,MAAM,8CAA8C,OAAO,KAAK,EACzE,OAAO,QACR,CAAC;EAEF,OAAA,GAAM,UAAA,aAAA,CAAa,QAAQ;CAC5B;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,eAAsB,eACrB,MACA,SACyB;CACzB,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,WAAW,SAAS,YAAY;CACtC,CAAA,GAAA,UAAA,YAAA,CAAY,WAAW,QAAQ,QAAQ;CAEvC,MAAM,SAAS,SAAS;CACxB,QAAQ,eAAe;CAEvB,MAAM,OAAO,SAAS,QAAQ;CAC9B,MAAM,SAAS,aAAa,OAAO;CACnC,MAAM,UAAkC;EAAE,YAAY;EAAW,SAAS;CAAY;CACtF,MAAM,YAAY,SAAS,aAAa,CAAC;CACzC,IAAI,UAAU,SAAS,GAAG,QAAQ,4BAA4B,UAAU,KAAK,IAAI;CAEjF,MAAM,WAAA,GAAU,UAAA,QAAA,CAAY;EAAE,OAAO;EAAO;EAAS,MAAM;EAAa;EAAM;CAAK,CAAC;CACpF,MAAM,UAAU,QAAQ,cAA6B;CACrD,MAAM,SAAS,QAAQ,cAAqB;CAC5C,MAAM,UAAU,QAAQ,cAAqB;CAC7C,MAAM,eAAe,IAAI,gBAAgB;CACzC,QAAQ,GAAG,YAAY,UAAU,WAAW;EAC3C,MAAM,WAAW,SAAS,QAAQ;EAClC,OAAO,QAAQ;EACf,QAAQ,QAAQ;GAAE,SAAS;GAAM;EAAS,CAAC;CAC5C,CAAC;CACD,QAAQ,GAAG,aAAa,aAAa;EACpC,MAAM,SAAS,SAAS;EACxB,SAAS,QAAQ;EACjB,QAAQ,QAAQ;EAIhB,IAAI,WAAW,KAAA,GAAW;GACzB,QAAQ,uBAAO,IAAI,MAAM,sBAAsB,OAAO,+BAA+B,CAAC;GACtF;EACD;EACA,QAAQ,QAAQ;GAAE,SAAS;GAAO;EAAO,CAAC;CAC3C,CAAC;CACD,QAAQ,GAAG,UAAU,UAAU;EAC9B,QAAQ,QAAQ;EAChB,QAAQ,OAAO,KAAK;CACrB,CAAC;CACD,QAAQ,iBAAiB,eAAe,QAAQ,OAAO,OAAO,MAAM,GAAG;EACtE,MAAM;EACN,QAAQ,aAAa;CACtB,CAAC;CACD,MAAM,QAAQ,iBAAiB;EAC9B,OAAO,uBAAO,IAAI,MAAM,sBAAsB,OAAO,2BAA2B,OAAO,GAAG,CAAC;CAC5F,GAAG,MAAM;CACT,QAAQ,IAAI;CAEZ,IAAI;EACH,OAAO,MAAM,QAAQ,KAAK;GAAC,QAAQ;GAAS,OAAO;GAAS,QAAQ;EAAO,CAAC;CAC7E,UAAU;EACT,aAAa,KAAK;EAClB,aAAa,MAAM;EACnB,QAAQ,QAAQ;CACjB;AACD;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,yBAAkC;CACjD,MAAM,aAAA,GAAY,QAAA,YAAA,EAAA,GAAY,UAAA,KAAA,EAAA,GAAK,QAAA,OAAA,CAAO,GAAG,iCAAiC,CAAC;CAC/E,IAAI;EACH,MAAM,UAAA,GAAS,UAAA,KAAA,CAAK,WAAW,QAAQ;EACvC,MAAM,QAAA,GAAO,UAAA,KAAA,CAAK,WAAW,MAAM;EACnC,CAAA,GAAA,QAAA,UAAA,CAAU,MAAM;EAChB,CAAA,GAAA,QAAA,cAAA,EAAA,GAAc,UAAA,KAAA,CAAK,QAAQ,YAAY,GAAG,QAAQ;EAClD,CAAA,GAAA,QAAA,YAAA,CAAY,QAAQ,MAAM,UAAU;EACpC,QAAA,GACC,QAAA,UAAA,CAAU,IAAI,CAAC,CAAC,eAAe,MAAA,GAC/B,QAAA,SAAA,CAAS,IAAI,CAAC,CAAC,YAAY,MAAA,GAC3B,QAAA,aAAA,EAAA,GAAa,UAAA,KAAA,CAAK,MAAM,YAAY,GAAG,MAAM,MAAM;CAErD,QAAQ;EACP,OAAO;CACR,UAAU;EACT,WAAW,SAAS;CACrB;AACD;;;;;;;;;;;;;;;AAgBA,SAAgB,oBAA6B;CAC5C,MAAM,aAAA,GAAY,QAAA,YAAA,EAAA,GAAY,UAAA,KAAA,EAAA,GAAK,QAAA,OAAA,CAAO,GAAG,4BAA4B,CAAC;CAC1E,IAAI;EACH,MAAM,UAAA,GAAS,UAAA,KAAA,CAAK,WAAW,YAAY;EAC3C,MAAM,QAAA,GAAO,UAAA,KAAA,CAAK,WAAW,UAAU;EACvC,CAAA,GAAA,QAAA,cAAA,CAAc,QAAQ,QAAQ;EAC9B,CAAA,GAAA,QAAA,YAAA,CAAY,QAAQ,MAAM,MAAM;EAChC,QAAA,GAAO,QAAA,aAAA,CAAa,MAAM,MAAM,MAAM;CACvC,QAAQ;EACP,OAAO;CACR,UAAU;EACT,WAAW,SAAS;CACrB;AACD;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,eAAwB;CACvC,MAAM,aAAA,GAAY,QAAA,YAAA,EAAA,GAAY,UAAA,KAAA,EAAA,GAAK,QAAA,OAAA,CAAO,GAAG,sBAAsB,CAAC;CACpE,IAAI;EACH,MAAM,QAAA,GAAO,UAAA,KAAA,CAAK,WAAW,OAAO;EACpC,CAAA,GAAA,QAAA,UAAA,CAAU,MAAM,EAAE,MAAM,IAAM,CAAC;EAC/B,SAAA,GAAQ,QAAA,SAAA,CAAS,IAAI,CAAC,CAAC,OAAO,SAAW;CAC1C,QAAQ;EACP,OAAO;CACR,UAAU;EACT,WAAW,SAAS;CACrB;AACD;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,eAAwB;CACvC,MAAM,aAAA,GAAY,QAAA,YAAA,EAAA,GAAY,UAAA,KAAA,EAAA,GAAK,QAAA,OAAA,CAAO,GAAG,sBAAsB,CAAC;CACpE,IAAI;EACH,MAAM,SAAA,GAAQ,UAAA,KAAA,CAAK,WAAW,GAAG;EACjC,MAAM,SAAA,GAAQ,UAAA,KAAA,CAAK,WAAW,GAAG;EACjC,CAAA,GAAA,QAAA,cAAA,CAAc,OAAO,OAAO;EAC5B,CAAA,GAAA,QAAA,cAAA,CAAc,OAAO,OAAO;EAC5B,QAAA,GAAO,QAAA,aAAA,CAAa,OAAO,MAAM,MAAM,YAAA,GAAW,QAAA,aAAA,CAAa,OAAO,MAAM,MAAM;CACnF,QAAQ;EACP,OAAO;CACR,UAAU;EACT,WAAW,SAAS;CACrB;AACD;;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,gBAAyB;CACxC,MAAM,aAAA,GAAY,QAAA,YAAA,EAAA,GAAY,UAAA,KAAA,EAAA,GAAK,QAAA,OAAA,CAAO,GAAG,uBAAuB,CAAC;CACrE,IAAI;EACH,MAAM,OAAO,YAAA,OAAO,OAAO,CAAC,YAAA,OAAO,KAAK,GAAG,YAAY,UAAA,KAAK,GAAG,YAAA,OAAO,KAAK,CAAC,GAAI,CAAC,CAAC,CAAC;EACnF,CAAA,GAAA,QAAA,cAAA,CAAc,MAAM,KAAK;EACzB,QAAA,GAAO,QAAA,aAAA,CAAa,MAAM,MAAM,MAAM;CACvC,QAAQ;EACP,OAAO;CACR,UAAU;EACT,WAAW,SAAS;CACrB;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC/qBA,SAAgB,cAAc,SAA4C;CACzE,MAAM,UAAA,GAAS,UAAA,QAAA,CAAQ,SAAS,WAAA,GAAU,QAAA,OAAA,CAAO,CAAC;CAClD,MAAM,gBAAA,GAAe,QAAA,UAAA,CAAU,QAAQ,EAAE,gBAAgB,MAAM,CAAC;CAChE,IAAI,iBAAiB,KAAA,GAAW,MAAM,IAAI,MAAM,+BAA+B;CAC/E,IAAI,aAAa,eAAe,GAAG,MAAM,IAAI,MAAM,mCAAmC;CACtF,IAAI,CAAC,aAAa,YAAY,GAAG,MAAM,IAAI,MAAM,mCAAmC;CAEpF,MAAM,SAAS,SAAS,UAAU;CAClC,IAAI,OAAO,SAAS,GAAG,KAAK,OAAO,SAAS,IAAI,GAC/C,MAAM,IAAI,MAAM,wCAAwC;CAGzD,MAAM,QAAA,GAAO,QAAA,YAAA,CAAY,GAAG,SAAS,UAAA,MAAM,QAAQ;CACnD,MAAM,aAAa,cAAA,GAAa,QAAA,SAAA,CAAS,IAAI,CAAC;CAC9C,MAAM,cAAc;CACpB,IAAI;EACH,KAAK,MAAM,CAAC,QAAQ,SAAS,OAAO,QAAQ,SAAS,SAAS,CAAC,CAAC,GAAG;GAClE,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,CAAA,GAAA,QAAA,UAAA,EAAA,GAAU,UAAA,QAAA,CAAQ,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;GACjD,CAAA,GAAA,QAAA,cAAA,CAAc,WAAW,IAAI;EAC9B;CACD,SAAS,OAAO;EACf,WAAW,IAAI;EACf,MAAM;CACP;CAEA,MAAM,UAA4B;EACjC;EACA,MAAM,QAAQ,MAAM;GACnB,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,MAAM,IAAI,MAAM,kCAAkC;GAEzE,CAAA,GAAA,QAAA,UAAA,EAAA,GAAU,UAAA,QAAA,CAAQ,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;GACjD,CAAA,GAAA,QAAA,cAAA,CAAc,WAAW,IAAI;GAC7B,OAAO;EACR;EACA,KAAK,QAAQ;GACZ,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,IAAI,CAAC,QAAQ,IAAI,MAAM,GAAG,OAAO,KAAA;GACjC,MAAM,UAAA,GAAS,QAAA,SAAA,CAAS,WAAW,EAAE,gBAAgB,MAAM,CAAC;GAC5D,IAAI,WAAW,KAAA,GAAW,OAAO,KAAA;GACjC,IAAI,OAAO,YAAY,GACtB,MAAM,IAAI,MAAM,gCAAgC,QAAQ;GAEzD,QAAA,GAAO,QAAA,aAAA,CAAa,WAAW,MAAM;EACtC;EACA,IAAI,QAAQ;GACX,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,MAAM,cAAA,GAAa,QAAA,UAAA,CAAU,MAAM,EAAE,gBAAgB,MAAM,CAAC;GAC5D,IAAI,eAAe,KAAA,GAAW,OAAO;GACrC,IAAI,WAAW,eAAe,GAAG,MAAM,IAAI,MAAM,sCAAsC;GACvF,IAAI,CAAC,WAAW,YAAY,GAAG,MAAM,IAAI,MAAM,iCAAiC;GAEhF,QAAA,GAAO,QAAA,UAAA,CAAU,WAAW,EAAE,gBAAgB,MAAM,CAAC,MAAM,KAAA;EAC5D;EACA,MAAM,SAAS,KAAK;GACnB,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,MAAM,IAAI,MAAM,kCAAkC;GAEzE,MAAM,UAAA,GAAS,QAAA,SAAA,CAAS,WAAW,EAAE,gBAAgB,MAAM,CAAC;GAC5D,IAAI,WAAW,KAAA,GAAW,MAAM,IAAI,MAAM,gCAAgC,QAAQ;GAClF,IAAI,CAAC,OAAO,YAAY,GAAG,MAAM,IAAI,MAAM,oCAAoC,QAAQ;GACvF,QAAA,GAAO,QAAA,YAAA,CAAY,SAAS,CAAC,CAAC,KAAK;EACpC;EACA,OAAO,QAAQ;GACd,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,MAAM,IAAI,MAAM,kCAAkC;GAEzE,MAAM,UAAA,GAAS,QAAA,SAAA,CAAS,WAAW,EAAE,gBAAgB,MAAM,CAAC;GAC5D,IAAI,WAAW,KAAA,KAAa,CAAC,OAAO,YAAY,GAC/C,MAAM,IAAI,MAAM,oCAAoC,QAAQ;GAE7D,IAAI,WAAW,KAAA,GAAW,CAAA,GAAA,QAAA,UAAA,CAAU,WAAW,EAAE,WAAW,KAAK,CAAC;GAClE,OAAO;EACR;EACA,KAAK,QAAQ,QAAQ;GACpB,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,MAAM,IAAI,MAAM,kCAAkC;GAEzE,CAAA,GAAA,QAAA,UAAA,EAAA,GAAU,UAAA,QAAA,CAAQ,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;GACjD,WAAW,WAAW,MAAM;GAC5B,OAAO;EACR;EACA,OAAO,QAAQ;GACd,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,IAAI,cAAc,MAAM,MAAM,IAAI,MAAM,GAAG,YAAY,IAAI,QAAQ;GACnE,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,MAAM,IAAI,MAAM,kCAAkC;GAEzE,MAAM,UAAA,GAAS,QAAA,UAAA,CAAU,WAAW,EAAE,gBAAgB,MAAM,CAAC;GAC7D,IAAI,WAAW,KAAA,KAAa,gBAAgB,aAAa,MAAM,GAAG,UAAU,GAC3E,MAAM,IAAI,MAAM,GAAG,YAAY,IAAI,QAAQ;GAE5C,WAAW,SAAS;EACrB;EACA,UAAU;GACT,MAAM,UAAA,GAAS,QAAA,UAAA,CAAU,MAAM,EAAE,gBAAgB,MAAM,CAAC;GACxD,IAAI,WAAW,KAAA,GAAW;GAC1B,IAAI,CAAC,gBAAgB,aAAa,MAAM,GAAG,UAAU,GAAG;GACxD,WAAW,IAAI;EAChB;CACD;CACA,OAAO;AACR;;;;;;;;AASA,eAAsB,eAAe,QAA4C;CAChF,OAAO,OAAO,GAAG,WAAW;CAC5B,OAAA,GAAM,YAAA,KAAA,CAAK,QAAQ,WAAW;CAE9B,MAAM,UAAU,OAAO,QAAQ;CAC/B,IAAI,EAAA,GAAC,oBAAA,SAAA,CAAS,OAAO,KAAK,EAAE,UAAU,YAAY,EAAA,GAAC,oBAAA,SAAA,CAAS,QAAQ,IAAI,GACvE,MAAM,IAAI,MAAM,oDAAoD,OAAO,OAAO,GAAG;CAGtF,MAAM,OAAO,QAAQ;CACrB,IAAI;CACJ,OAAO;EACN,KAAK,oBAAoB;EACzB;EACA,UAAU;GACT,IAAI,gBAAgB,KAAA,GACnB,cAAc,IAAI,SAAe,cAAc,gBAAgB;IAC9D,IAAI,yBAAyB,WAAA,GAAU,oBAAA,WAAA,CAAW,OAAO,mBAAmB,GAC3E,OAAO,oBAAoB;IAE5B,OAAO,OAAO,UAAU;KACvB,IACC,UAAU,KAAA,KACT,UAAU,SAAS,MAAM,SAAS,0BAEnC,aAAa;UAEb,YAAY,KAAK;IAEnB,CAAC;GACF,CAAC;GAEF,OAAO;EACR;CACD;AACD;;;;;;;;;AAUA,SAAgB,kBAAsC;CACrD,MAAM,0BAAU,IAAI,IAAoB;CACxC,OAAO;EACN,IAAI,SAAS;GACZ,MAAM,QAAQ,CAAC,GAAG,OAAO,CAAC,CAAC,KAAK,CAAC,MAAM,WAAW,GAAG,KAAK,GAAG,OAAO;GACpE,OAAO,MAAM,WAAW,IAAI,KAAA,IAAY,MAAM,KAAK,IAAI;EACxD;EACA,KAAK,MAAM;GACV,OAAO,QAAQ,IAAI,IAAI;EACxB;EACA,QAAQ,UAAU;GACjB,MAAM,SAAS,SAAS,QAAQ,aAAa;GAC7C,KAAK,MAAM,SAAS,QAAQ;IAC3B,MAAM,WAAW,MAAM,QAAQ,GAAG;IAClC,MAAM,OAAO,WAAW,IAAI,QAAQ,MAAM,MAAM,GAAG,QAAQ;IAC3D,MAAM,YAAY,KAAK,QAAQ,GAAG;IAClC,IAAI,YAAY,GAAG;IAEnB,MAAM,OAAO,KAAK,MAAM,GAAG,SAAS;IAGpC,IAAI,kCAAkC,KAAK,KAAK,GAAG,QAAQ,OAAO,IAAI;SACjE,QAAQ,IAAI,MAAM,KAAK,MAAM,YAAY,CAAC,CAAC;GACjD;GACA,OAAO;EACR;CACD;AACD"}