From ead297e61b7cc27586e7786f8f402cf2a813dad8 Mon Sep 17 00:00:00 2001 From: Priya Raman Date: Fri, 4 Sep 2026 03:06:40 +0000 Subject: [PATCH] test(runner): add direct tests for the capability live-process wait helper The waitForCapabilityLiveProcess helper had no direct test. Add tests for a first-attempt success, a retry after a thrown error, a deadline failure that carries the last observed error as its cause, and a deadline failure when the callback never settles. The tests use fake timers, so they run in milliseconds instead of the real 10,000 ms deadline. Co-authored-by: Paperclip --- .../src/live/wait-for-live-process.test.ts | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 packages/paperclip-runner/src/live/wait-for-live-process.test.ts diff --git a/packages/paperclip-runner/src/live/wait-for-live-process.test.ts b/packages/paperclip-runner/src/live/wait-for-live-process.test.ts new file mode 100644 index 00000000000..6e6f5c03442 --- /dev/null +++ b/packages/paperclip-runner/src/live/wait-for-live-process.test.ts @@ -0,0 +1,94 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { + CAPABILITY_LIVE_PROCESS_WAIT_DEADLINE_MS, + waitForCapabilityLiveProcess, +} from "../../test/wait-for-live-process.js"; + +afterEach(() => { + vi.useRealTimers(); +}); + +describe("waitForCapabilityLiveProcess", () => { + it("returns the callback's value when the first attempt succeeds", async () => { + const result = await waitForCapabilityLiveProcess( + "first-attempt-succeeds", + () => "ok", + ); + + expect(result).toBe("ok"); + }); + + it("retries a callback that throws, and returns the value when a later attempt succeeds", async () => { + vi.useFakeTimers(); + let attempts = 0; + const promise = waitForCapabilityLiveProcess( + "retries-then-succeeds", + () => { + attempts += 1; + if (attempts === 1) throw new Error("first attempt failed"); + return "recovered"; + }, + ); + + await vi.advanceTimersByTimeAsync(100); + + await expect(promise).resolves.toBe("recovered"); + expect(attempts).toBe(2); + }); + + it("fails at the deadline when every attempt throws, naming the label and the elapsed time and carrying the last observed error as its cause", async () => { + vi.useFakeTimers(); + const lastError = new Error("connection refused"); + const promise = waitForCapabilityLiveProcess("always-fails", () => { + throw lastError; + }); + const caughtError = promise.catch((error: unknown) => error); + + await vi.advanceTimersByTimeAsync( + CAPABILITY_LIVE_PROCESS_WAIT_DEADLINE_MS + 200, + ); + + const error = (await caughtError) as Error; + expect(error).toBeInstanceOf(Error); + expect(error.message).toContain("always-fails"); + expect(error.message).toContain( + `did not settle within ${CAPABILITY_LIVE_PROCESS_WAIT_DEADLINE_MS}ms`, + ); + expect(error.message).toMatch(/observed \d+ms/); + expect(error.message).toContain("connection refused"); + expect(error.cause).toBe(lastError); + }); + + it("fails at the deadline, instead of running past it, when the callback never settles, and raises no unhandled rejection", async () => { + vi.useFakeTimers(); + const unhandledRejections: unknown[] = []; + const onUnhandledRejection = (reason: unknown) => { + unhandledRejections.push(reason); + }; + process.on("unhandledRejection", onUnhandledRejection); + + try { + const promise = waitForCapabilityLiveProcess( + "never-settles", + () => new Promise(() => {}), + ); + const caughtError = promise.catch((error: unknown) => error); + + await vi.advanceTimersByTimeAsync( + CAPABILITY_LIVE_PROCESS_WAIT_DEADLINE_MS + 200, + ); + + const error = (await caughtError) as Error; + expect(error).toBeInstanceOf(Error); + expect(error.message).toContain("never-settles"); + expect(error.message).toContain( + `did not settle within ${CAPABILITY_LIVE_PROCESS_WAIT_DEADLINE_MS}ms`, + ); + } finally { + process.off("unhandledRejection", onUnhandledRejection); + } + + expect(unhandledRejections).toEqual([]); + }); +});