import { describe, expect, test } from "bun:test"; import { CommandSettlementError, waitForCommandSettlement, } from "./command-settlement.js"; import { HOST_EVENT_KEYS, toHostEventName } from "./constants.js"; /** * Avoid native `EventTarget.dispatchEvent`: other suites swap `globalThis` (e.g. SDK tests * set `window` + `CustomEvent` from happy-dom), so `new Event(...)` and `new EventTarget()` * can come from different realms and Bun rejects `dispatchEvent`. */ const createWindowRef = (): Window => { const listeners = new Map void>>(); return { addEventListener( type: string, listener: EventListenerOrEventListenerObject, _options?: boolean | AddEventListenerOptions, ): void { const handler = typeof listener === "function" ? listener : listener.handleEvent.bind(listener); let bucket = listeners.get(type); if (!bucket) { bucket = new Set(); listeners.set(type, bucket); } bucket.add(handler as (event: Event) => void); }, removeEventListener( type: string, listener: EventListenerOrEventListenerObject, _options?: boolean | EventListenerOptions, ): void { const handler = typeof listener === "function" ? listener : listener.handleEvent.bind(listener); listeners.get(type)?.delete(handler as (event: Event) => void); }, dispatchEvent(event: Event): boolean { const bucket = listeners.get(event.type); if (!bucket) { return true; } for (const handler of [...bucket]) { handler(event); } return true; }, } as unknown as Window; }; /** Same as {@link createWindowRef} but omits `removeEventListener` (protocol tolerates missing cleanup). */ const createWindowRefWithoutRemoveListener = (): Window => { const inner = createWindowRef(); return { addEventListener: inner.addEventListener.bind(inner), dispatchEvent: inner.dispatchEvent.bind(inner), removeEventListener: undefined, } as unknown as Window; }; const createSettledEvent = (detail: unknown): Event => { const event = new Event(settledEventName) as Event & { detail?: unknown }; Object.defineProperty(event, "detail", { value: detail }); return event; }; const settledEventName = toHostEventName( HOST_EVENT_KEYS.INSTANCE_COMMAND_SETTLED, ); describe("waitForCommandSettlement", () => { test("resolves when matching request settles successfully", async () => { const windowRef = createWindowRef(); const pending = waitForCommandSettlement({ requestId: "req_success_1", windowRef, }); windowRef.dispatchEvent( createSettledEvent({ requestId: "req_success_1", instanceId: "alpha", kind: "open", ok: true, }), ); await expect(pending).resolves.toEqual({ requestId: "req_success_1", instanceId: "alpha", kind: "open", ok: true, }); }); test("rejects when matching request settles with failure", async () => { const windowRef = createWindowRef(); const pending = waitForCommandSettlement({ requestId: "req_fail_1", windowRef, }); windowRef.dispatchEvent( createSettledEvent({ requestId: "req_fail_1", instanceId: "alpha", kind: "open", ok: false, error: { message: "boom", code: "INVALID_USAGE", }, }), ); await expect(pending).rejects.toThrow("boom"); await expect(pending).rejects.toBeInstanceOf(CommandSettlementError); await expect(pending).rejects.toMatchObject({ code: "INVALID_USAGE", requestId: "req_fail_1", kind: "open", instanceId: "alpha", }); }); test("ignores non-matching and invalid settlement events", async () => { const windowRef = createWindowRef(); const pending = waitForCommandSettlement({ requestId: "req_match_1", windowRef, }); let settled = false; void pending.then(() => { settled = true; }); windowRef.dispatchEvent( createSettledEvent({ requestId: "req_other", instanceId: "alpha", kind: "open", ok: true, }), ); windowRef.dispatchEvent( createSettledEvent({ requestId: "req_match_1", invalid: true, }), ); await Promise.resolve(); expect(settled).toBe(false); windowRef.dispatchEvent( createSettledEvent({ requestId: "req_match_1", instanceId: "alpha", kind: "open", ok: true, }), ); await expect(pending).resolves.toEqual({ requestId: "req_match_1", instanceId: "alpha", kind: "open", ok: true, }); }); test("rejects empty requestId input", async () => { const windowRef = createWindowRef(); await expect( waitForCommandSettlement({ requestId: "", windowRef }), ).rejects.toThrow("requestId must be a non-empty string"); }); test("does not normalize requestId input", async () => { const windowRef = createWindowRef(); const pending = waitForCommandSettlement({ requestId: " req_with_spaces ", windowRef, }); let settled = false; void pending.then(() => { settled = true; }); windowRef.dispatchEvent( createSettledEvent({ requestId: "req_with_spaces", instanceId: "alpha", kind: "open", ok: true, }), ); await Promise.resolve(); expect(settled).toBe(false); windowRef.dispatchEvent( createSettledEvent({ requestId: " req_with_spaces ", instanceId: "alpha", kind: "open", ok: true, }), ); await expect(pending).resolves.toEqual({ requestId: " req_with_spaces ", instanceId: "alpha", kind: "open", ok: true, }); }); test("rejects when settlement times out", async () => { const windowRef = createWindowRef(); await expect( waitForCommandSettlement({ requestId: "req_timeout_1", windowRef, timeoutMs: 1, }), ).rejects.toThrow("Timed out waiting for command settlement"); }); test("timeout path does not throw when window has no removeEventListener", async () => { const windowRef = createWindowRefWithoutRemoveListener(); await expect( waitForCommandSettlement({ requestId: "req_no_remove", windowRef, timeoutMs: 1, }), ).rejects.toThrow("Timed out waiting for command settlement"); }); test("settlement path does not throw when window has no removeEventListener", async () => { const windowRef = createWindowRefWithoutRemoveListener(); const pending = waitForCommandSettlement({ requestId: "req_settle_no_remove", windowRef, }); windowRef.dispatchEvent( createSettledEvent({ requestId: "req_settle_no_remove", instanceId: "alpha", kind: "open", ok: true, }), ); await expect(pending).resolves.toEqual({ requestId: "req_settle_no_remove", instanceId: "alpha", kind: "open", ok: true, }); }); });