/** * The semaphore, exercised through the only door it has: search(). * * These run against a deliberately unreachable CDP endpoint. Every search fails * fast at the connect step, which is exactly what is wanted — the questions here * are about slot bookkeeping, not about Google. Hitting the real browser to test * a counter would be slow, flaky, and rude to whoever is using it. */ import assert from "node:assert/strict"; import { test } from "node:test"; import { MAX_CONCURRENT_SEARCHES, disconnect, search } from "../src/search.ts"; const UNREACHABLE = "127.0.0.1:1"; // Nothing listens on port 1. const quiet = () => {}; const withUnreachableBrowser = async (body: () => Promise): Promise => { const saved = process.env["BU_CDP_HTTP"]; process.env["BU_CDP_HTTP"] = UNREACHABLE; disconnect(); try { await body(); } finally { if (saved === undefined) delete process.env["BU_CDP_HTTP"]; else process.env["BU_CDP_HTTP"] = saved; disconnect(); } }; const attempt = (query: string, signal: AbortSignal) => search({ query, numResults: 1 }, signal, quiet).then( () => "fulfilled" as const, (e: Error) => e.name, ); test("a failed search returns its slot, so the next one still runs", async () => { await withUnreachableBrowser(async () => { // Three times the cap, run sequentially. If a slot leaked on the failure // path, the run past the cap would hang until the test timed out. for (let i = 0; i < MAX_CONCURRENT_SEARCHES * 3; i++) { const name = await attempt(`q${i}`, AbortSignal.timeout(10_000)); assert.equal(name, "BrowserUnavailableError", `attempt ${i}`); } }); }); test("aborting while queued does not wedge the semaphore", async () => { await withUnreachableBrowser(async () => { // Saturate the cap and queue two more, then cancel the queued ones. The // bug this guards against: a cancelled waiter that is later handed a slot // resolves into nothing, and the slot is never counted back. const cancel = new AbortController(); const running = Array.from({ length: MAX_CONCURRENT_SEARCHES }, (_, i) => attempt(`running${i}`, AbortSignal.timeout(10_000)), ); const queued = Array.from({ length: 2 }, (_, i) => attempt(`queued${i}`, cancel.signal)); cancel.abort(); await Promise.all(running); for (const outcome of await Promise.all(queued)) { assert.ok( outcome === "SearchAbortedError" || outcome === "BrowserUnavailableError", `unexpected outcome ${outcome}`, ); } // The real assertion: the pool still works afterwards. A wedged semaphore // would leave this hanging rather than failing. for (let i = 0; i < MAX_CONCURRENT_SEARCHES + 1; i++) { assert.equal(await attempt(`after${i}`, AbortSignal.timeout(10_000)), "BrowserUnavailableError"); } }); }); test("an unreachable endpoint is reported, never silently swapped for a local browser", async () => { await withUnreachableBrowser(async () => { const error = await search({ query: "q", numResults: 1 }, AbortSignal.timeout(10_000), quiet).then( () => null, (e: Error) => e, ); assert.ok(error, "expected a throw"); assert.equal(error.name, "BrowserUnavailableError"); assert.match(error.message, new RegExp(UNREACHABLE.replace(".", "\\."))); }); });