import { waitForNativeReady } from "./native-readiness.mjs"; import { rm } from "node:fs/promises"; import { createServer } from "node:net"; async function bounded(promise, label) { let timer; try { return await Promise.race([ promise, new Promise((_, reject) => { timer = setTimeout( () => reject(new Error(`${label} cleanup timed out`)), 15_000, ); }), ]); } finally { clearTimeout(timer); } } export async function freePort() { const server = createServer(); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const { port } = server.address(); await new Promise((resolve) => server.close(resolve)); return port; } // Own native resources independently of the test body's suspended awaits. export class ExecutionLifetime { clients = []; phase = "setup"; completed = false; worker; persistence; #starting; #cleanupPromise; #stops = new WeakMap(); #reported = false; #readiness; constructor(t, group, events) { this.group = group; this.scenario = group; this.events = events; this.#followTest(t); } #followTest(t) { this.t = t; t.after(() => this.cleanup(), { timeout: 20_000 }); } async runAfterReady({ timeout, afterReady, startupOnly }, body) { const run = async (t) => { if (t !== this.t) this.#followTest(t); await afterReady?.({ eventOrigin: `http://127.0.0.1:${this.events.address().port}`, }); if (!startupOnly) await body(t); this.completed = true; }; try { // Keep setup under the outer deadline; measure native cancellation only // after startup has acquired resources and established Agent identity. if (timeout) await this.t.test( "native fixture lifecycle after readiness", { timeout }, run, ); else await run(this.t); } finally { await this.cleanup(); } } get closing() { return Boolean(this.#cleanupPromise); } closeClients() { for (const client of this.clients.splice(0)) client.close(); } stopWorker(instance = this.worker) { if (!this.#stops.has(instance)) this.#stops.set(instance, instance.stop()); return this.#stops.get(instance); } async start(createWorker) { this.t.signal.throwIfAborted(); this.phase = "Worker startup"; this.#starting = createWorker().then(async (instance) => { if (this.closing) { await this.stopWorker(instance); return; } this.worker = instance; }); await this.#starting; this.#starting = undefined; this.t.signal.throwIfAborted(); } async waitForReady(client, route) { this.phase = "native client readiness"; const state = { client, route }; this.#readiness = state; await waitForNativeReady(client, this.t.signal, state); if (this.#readiness === state) this.#readiness = undefined; } reportFailure() { if (this.#reported) return; this.#reported = true; const waiting = this.#readiness; const connection = waiting && { route: waiting.route, opens: waiting.opens, closes: waiting.closes, errors: waiting.errors, lastCloseCode: waiting.lastCloseCode, readyState: waiting.client.readyState, identified: waiting.client.identified, }; this.t.diagnostic( `Execution fixture interrupted: ${JSON.stringify({ group: this.group, scenario: this.scenario, phase: this.phase, connection })}`, ); } cleanup() { return (this.#cleanupPromise ??= (async () => { if (!this.completed) this.reportFailure(); this.closeClients(); this.phase = "resource disposal"; this.events.closeAllConnections(); const results = await Promise.allSettled([ bounded( new Promise((resolve) => this.events.close(resolve)), "events server", ), bounded( (async () => { if (this.worker) await this.stopWorker(); if (this.#starting) await this.#starting; })(), "Worker", ), ]); if (this.persistence) await rm(this.persistence, { recursive: true, force: true }); const failures = results.filter((result) => result.status === "rejected"); if (failures.length) { this.reportFailure(); throw new AggregateError( failures.map((result) => result.reason), "Execution fixture cleanup failed", ); } })()); } }