import { describe, expect, it } from "bun:test"; import { createLazyHandleClient } from "./lazy-handle-client"; describe("lazy handle client resolved commands", () => { it("does not publish a handle invalidated while its command is active", async () => { const client = createLazyHandleClient({ getHandleIdFromSettlement: (settlement) => settlement.handleId, }); let resolveSettlement: | ((settlement: { handleId?: string }) => void) | undefined; const pendingCommand = client.runCommand( () => ({ kind: "open" }), () => new Promise((resolve) => { resolveSettlement = resolve; }), ); expect(client.recordHandleInvalidation("handle_pending")).toBeUndefined(); resolveSettlement?.({ handleId: "handle_pending" }); await pendingCommand; expect(client.getHandleId()).toBeUndefined(); }); it("publishes an active command handle after an unrelated invalidation", async () => { const client = createLazyHandleClient({ getHandleIdFromSettlement: (settlement) => settlement.handleId, }); let resolveSettlement: | ((settlement: { handleId?: string }) => void) | undefined; const pendingCommand = client.runCommand( () => ({ kind: "open" }), () => new Promise((resolve) => { resolveSettlement = resolve; }), ); client.recordHandleInvalidation("handle_unrelated"); resolveSettlement?.({ handleId: "handle_current" }); await pendingCommand; expect(client.getHandleId()).toBe("handle_current"); }); it("records invalidation during synchronous payload construction", async () => { const client = createLazyHandleClient({ getHandleIdFromSettlement: (settlement) => settlement.handleId, }); await client.runCommand( () => { client.recordHandleInvalidation("handle_sync"); return { kind: "open" }; }, async () => ({ handleId: "handle_sync" }), ); expect(client.getHandleId()).toBeUndefined(); }); it("allows a later epoch to reuse a handle invalidated by an earlier command", async () => { const client = createLazyHandleClient({ getHandleIdFromSettlement: (settlement) => settlement.handleId, }); let resolveSettlement: | ((settlement: { handleId?: string }) => void) | undefined; const pendingCommand = client.runCommand( () => ({ kind: "open" }), () => new Promise((resolve) => { resolveSettlement = resolve; }), ); client.recordHandleInvalidation("handle_reused"); client.reset(); resolveSettlement?.({ handleId: "handle_reused" }); await pendingCommand; await client.runCommand( () => ({ kind: "open" }), async () => ({ handleId: "handle_reused" }), ); expect(client.getHandleId()).toBe("handle_reused"); }); it("does not retry after retry preparation resets the command epoch", async () => { const client = createLazyHandleClient({ getHandleIdFromSettlement: (settlement) => settlement.handleId, }); let attempts = 0; const command = client.runCommandWithRetry( () => ({ kind: "open" }), async () => { attempts += 1; throw new Error("stale"); }, { shouldRetry: () => true, onBeforeRetry: () => client.reset(), }, ); await expect(command).rejects.toThrow("stale"); expect(attempts).toBe(1); }); it("skips queued resolved-handle work after a matching external clear", async () => { const client = createLazyHandleClient({ getHandleIdFromSettlement: (settlement) => settlement.handleId, }); await client.runCommand( () => ({ kind: "open" }), async () => ({ handleId: "handle_ready" }), ); let resolvePendingCommand: | ((settlement: { handleId?: string }) => void) | undefined; const pendingCommand = client.runCommand( (handleId) => handleId, () => new Promise((resolve) => { resolvePendingCommand = resolve; }), ); let didDispatch = false; const resolvedCommand = client.runResolvedHandleCommand( (handleId) => handleId, async () => { didDispatch = true; }, ); expect(client.clearHandleIfMatching("handle_ready")).toBe("handle_ready"); resolvePendingCommand?.({ handleId: "handle_ready" }); await pendingCommand; await resolvedCommand; expect(didDispatch).toBe(false); expect(client.getHandleId()).toBeUndefined(); }); it("allows a reset lifecycle to publish a previously invalidated handle id", async () => { const client = createLazyHandleClient({ getHandleIdFromSettlement: (settlement) => settlement.handleId, }); await client.runCommand( () => ({ kind: "open" }), async () => ({ handleId: "handle_reused" }), ); client.clearHandleIfMatching("handle_reused"); client.reset(); await client.runCommand( () => ({ kind: "open" }), async () => ({ handleId: "handle_reused" }), ); expect(client.getHandleId()).toBe("handle_reused"); }); it("rejects queued resolved-handle commands after invalidation", async () => { const client = createLazyHandleClient({ getHandleIdFromSettlement: (settlement) => settlement.handleId, }); await client.runCommand( () => ({ kind: "open" }), async () => ({ handleId: "handle_ready" }), ); let resolvePendingCommand: | ((settlement: { handleId?: string }) => void) | undefined; const pendingCommand = client.runCommand( (handleId) => handleId, () => new Promise((resolve) => { resolvePendingCommand = resolve; }), ); let didDispatch = false; const resolvedCommand = client.runResolvedHandleCommand( (handleId) => handleId, async () => { didDispatch = true; }, ); client.invalidate(); resolvePendingCommand?.({}); await pendingCommand; await expect(resolvedCommand).rejects.toThrow( "Handle client is invalidated", ); expect(didDispatch).toBe(false); }); it("continues queued execution after a resolved-handle command rejects", async () => { const client = createLazyHandleClient({ getHandleIdFromSettlement: (settlement) => settlement.handleId, }); await client.runCommand( () => ({ kind: "open" }), async () => ({ handleId: "handle_ready" }), ); const failedCommand = client.runResolvedHandleCommand( (handleId) => ({ handleId, kind: "close" }), async () => { throw new Error("close failed"); }, ); const seenHandles: Array = []; const followingCommand = client.runCommand( (handleId) => { seenHandles.push(handleId); return { kind: "open" }; }, async () => ({ handleId: "handle_recovered" }), ); await expect(failedCommand).rejects.toThrow("close failed"); await followingCommand; expect(seenHandles).toEqual(["handle_ready"]); expect(client.getHandleId()).toBe("handle_recovered"); }); });