import type { WorkerLike, WorkerRequest, WorkerResponse, } from "../../src/worker/worker-host.types.js"; type MessageListener = (ev: MessageEvent) => void; type ErrorListener = (ev: ErrorEvent) => void; /** * Deterministic `Worker` shim. Tests script the response sequence via the * `script` array; each posted request consumes the next handler. Out-of-script * requests are echoed back as `ok: false` with a fixed stderr so the host * surfaces the matching error code. */ export class FakeWorker implements WorkerLike { public posted: WorkerRequest[] = []; public terminated = 0; public messageListeners: MessageListener[] = []; public errorListeners: ErrorListener[] = []; public script: Array<(req: WorkerRequest) => WorkerResponse | "drop"> = []; public postMessage(message: unknown): void { const req = message as WorkerRequest; this.posted.push(req); const handler = this.script.shift(); if (!handler) { queueMicrotask(() => { this.emitMessage({ id: req.id, ok: false, stderr: "no-handler", }); }); return; } const result = handler(req); if (result === "drop") return; queueMicrotask(() => this.emitMessage(result)); } public emitMessage(data: unknown): void { const ev = { data } as MessageEvent; for (const l of this.messageListeners) l(ev); } public emitError(message: string, error?: Error): void { const ev = { message, error } as unknown as ErrorEvent; for (const l of this.errorListeners) l(ev); } public addEventListener( type: "message" | "error", listener: MessageListener | ErrorListener ): void { if (type === "message") this.messageListeners.push(listener as MessageListener); else this.errorListeners.push(listener as ErrorListener); } public removeEventListener( type: "message" | "error", listener: MessageListener | ErrorListener ): void { if (type === "message") this.messageListeners = this.messageListeners.filter( (l) => l !== listener ); else this.errorListeners = this.errorListeners.filter( (l) => l !== listener ); } public terminate(): void { this.terminated++; } } /** * Standard success script: `load` resolves, `writeFile` resolves, `run` * resolves with the provided output bytes and stderr, `readFile` resolves * with those bytes, both `deleteFile` calls resolve. Drop into the worker's * `script` array. */ export function successScript( outputBytes: Uint8Array, stderr = "" ): Array<(req: WorkerRequest) => WorkerResponse> { return [ (req) => ({ id: req.id, ok: true }), // load (req) => ({ id: req.id, ok: true }), // writeFile (req) => ({ id: req.id, ok: true, value: { stderr } }), // run (req) => ({ id: req.id, ok: true, value: outputBytes }), // readFile (req) => ({ id: req.id, ok: true }), // deleteFile input (req) => ({ id: req.id, ok: true }), // deleteFile output ]; } /** * Build a deterministic `Float32Array` interleaved into `Uint8Array` form * for use as the `readFile` payload. */ export function buildPcmBytes( frames: number, channels: number, pattern: (frame: number, channel: number) => number = (f, c) => Math.sin((f * (c + 1)) / 100) * 0.5 ): Uint8Array { const interleaved = new Float32Array(frames * channels); for (let f = 0; f < frames; f++) { for (let c = 0; c < channels; c++) { interleaved[f * channels + c] = pattern(f, c); } } return new Uint8Array( interleaved.buffer, interleaved.byteOffset, interleaved.byteLength ); }