diff --git a/src/anime-bridge/bridge-client.test.ts b/src/anime-bridge/bridge-client.test.ts index eb170648..9f0da069 100644 --- a/src/anime-bridge/bridge-client.test.ts +++ b/src/anime-bridge/bridge-client.test.ts @@ -55,6 +55,20 @@ test('isReady requires every capability the client depends on', async () => { assert.equal(await partial.isReady(), false); }); +test('isReady caps the probe at the deadline the caller passes', async () => { + // A bridge that accepts the socket and then stalls: only the abort ends it. + const fetchImpl = (async (_input: RequestInfo | URL, init?: RequestInit) => { + await new Promise((resolve) => init?.signal?.addEventListener('abort', resolve)); + throw new Error('aborted'); + }) as typeof fetch; + const client = new AnimeBridgeClient({ baseUrl: 'http://127.0.0.1:9', fetchImpl }); + + const started = Date.now(); + assert.equal(await client.isReady(50), false); + // Well under the 5s default, so the per-call deadline is what applied. + assert.ok(Date.now() - started < 1000, 'probe outlived the caller deadline'); +}); + test('isReady reports false instead of throwing when the bridge is down', async () => { const client = new AnimeBridgeClient({ baseUrl: 'http://127.0.0.1:9', diff --git a/src/anime-bridge/bridge-client.ts b/src/anime-bridge/bridge-client.ts index 2a20a076..c8ba6e20 100644 --- a/src/anime-bridge/bridge-client.ts +++ b/src/anime-bridge/bridge-client.ts @@ -74,9 +74,13 @@ export class AnimeBridgeClient { this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS; } - async getCapabilities(): Promise { + /** + * `timeoutMs` lets a caller with its own deadline (the readiness loop) cap the + * probe below the default, so a short readiness budget is actually honored. + */ + async getCapabilities(timeoutMs = CAPABILITIES_TIMEOUT_MS): Promise { const response = await this.fetchImpl(`${this.baseUrl}/capabilities`, { - signal: AbortSignal.timeout(Math.min(CAPABILITIES_TIMEOUT_MS, this.requestTimeoutMs)), + signal: AbortSignal.timeout(Math.max(0, Math.min(timeoutMs, this.requestTimeoutMs))), }); if (!response.ok) { throw new Error(`Anime bridge capabilities check failed (${response.status}).`); @@ -85,9 +89,9 @@ export class AnimeBridgeClient { } /** True once the bridge is up and reports the features this client needs. */ - async isReady(): Promise { + async isReady(timeoutMs?: number): Promise { try { - const capabilities = await this.getCapabilities(); + const capabilities = await this.getCapabilities(timeoutMs); return ( capabilities.mangatanMihonBridge === 1 && capabilities.sourceFactory === true && diff --git a/src/anime-bridge/sidecar-process.test.ts b/src/anime-bridge/sidecar-process.test.ts index 0d9ac72b..d9b292b7 100644 --- a/src/anime-bridge/sidecar-process.test.ts +++ b/src/anime-bridge/sidecar-process.test.ts @@ -11,9 +11,16 @@ const binaries: BundleBinaries = { }; /** A ChildProcess stand-in: an EventEmitter with the bits startSidecar touches. */ -function fakeChild(): ChildProcess { +function fakeChild(onKill?: (child: EventEmitter) => void): ChildProcess { const child = new EventEmitter(); - Object.assign(child, { stdout: null, stderr: null, kill: () => true }); + Object.assign(child, { + stdout: null, + stderr: null, + kill: () => { + onKill?.(child); + return true; + }, + }); return child as unknown as ChildProcess; } @@ -32,6 +39,18 @@ test('a failed spawn rejects instead of throwing an unhandled error event', asyn ); }); +test('a readiness timeout shuts the child down and reports the timeout', async () => { + const port = await allocatePort(); + // Never becomes ready, but does go down on the first signal. + const child = fakeChild((emitter) => queueMicrotask(() => emitter.emit('exit', 0, 'SIGTERM'))); + const spawnImpl = (() => child) as unknown as typeof spawnType; + + await assert.rejects( + () => startSidecar({ binaries, port, readyTimeoutMs: 50, spawnImpl }), + /did not become ready within 50ms/, + ); +}); + test('an early exit is reported with its code rather than waiting out the deadline', async () => { const port = await allocatePort(); const child = fakeChild(); diff --git a/src/anime-bridge/sidecar-process.ts b/src/anime-bridge/sidecar-process.ts index 4675b410..3423406a 100644 --- a/src/anime-bridge/sidecar-process.ts +++ b/src/anime-bridge/sidecar-process.ts @@ -107,6 +107,11 @@ export async function startSidecar(options: StartSidecarOptions): Promise {}); throw new Error( `Anime bridge did not become ready within ${options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS}ms.`, );