Files
OmniRoute/tests/unit/combo-target-timeout-runner.test.ts
Diego Rodrigues de Sa e Souza 373dd17ccb refactor(combo): extract buildTargetTimeoutRunner from handleComboChat (#6036)
Bloco J (hot-path decomposition), Task 1. Extract the per-target-timeout dispatch wrapper
(handleComboChat's handleSingleModelWithTimeout closure) verbatim into the leaf
combo/targetTimeoutRunner.ts as a factory buildTargetTimeoutRunner({handleSingleModel,
comboTargetTimeoutMs, log}). The per-model abort still comes from target.modelAbortSignal,
so the outer request signal is intentionally not a dependency. Host call-sites unchanged.

combo.ts shrinks ~60 LOC; leaf is 91 LOC (<800). Body byte-identical (verbatim), no cycle.
This is the first slice toward extracting the shared attempt-loop/success/error handlers
(Tasks 3-4) that de-duplicate handleComboChat and handleRoundRobinCombo. Adds a dedicated
test (5) so the failover path can be mutated independently. Consumer tests stay green
(combo-strategy-fallbacks 24, combo-499-abort 5, empty-content-failover 3, body-400-stop 1,
priority-quota-exhaustion 2, rr-streaming-lock 1, rr-session-stickiness 2).

Plan: _tasks/superpowers/plans/2026-07-03-blocoJ-combo-hotpath-decomposition.md
2026-07-03 00:56:45 -03:00

79 lines
2.6 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { buildTargetTimeoutRunner } from "../../open-sse/services/combo/targetTimeoutRunner.ts";
const noopLog = { warn() {}, info() {}, error() {}, debug() {} } as any;
test("timeout<=0: passthrough direto (sem timer)", async () => {
let called = false;
const runner = buildTargetTimeoutRunner({
handleSingleModel: async () => {
called = true;
return new Response("ok");
},
comboTargetTimeoutMs: 0,
log: noopLog,
});
const res = await runner({}, "m");
assert.equal(called, true);
assert.equal(await res.text(), "ok");
});
test("timeout<=0: erro do upstream vira errorResponse 502", async () => {
const runner = buildTargetTimeoutRunner({
handleSingleModel: async () => {
throw new Error("boom");
},
comboTargetTimeoutMs: 0,
log: noopLog,
});
const res = await runner({}, "m");
assert.equal(res.status, 502);
});
test("excede o limite: aborta e retorna 524 timed out", async () => {
const runner = buildTargetTimeoutRunner({
handleSingleModel: (_b, _m, target) =>
new Promise<Response>((resolve) => {
// resolve só se abortado (simula um upstream que respeita o signal)
const sig = (target as any)?.modelAbortSignal as AbortSignal | undefined;
sig?.addEventListener("abort", () => resolve(new Response(null, { status: 599 })));
}),
comboTargetTimeoutMs: 20,
log: noopLog,
});
const res = await runner({}, "slow-model");
assert.equal(res.status, 524);
const body = await res.json();
assert.match(JSON.stringify(body), /timed out/i);
});
test("sucesso rápido vence a corrida do timeout", async () => {
const runner = buildTargetTimeoutRunner({
handleSingleModel: async () => new Response("fast", { status: 200 }),
comboTargetTimeoutMs: 1000,
log: noopLog,
});
const res = await runner({}, "m");
assert.equal(res.status, 200);
assert.equal(await res.text(), "fast");
});
test("hedge do parent já abortado propaga o abort ao filho", async () => {
const parent = new AbortController();
parent.abort(new Error("hedge-cancelled"));
let sawAbort = false;
const runner = buildTargetTimeoutRunner({
handleSingleModel: (_b, _m, target) =>
new Promise<Response>((resolve) => {
const sig = (target as any)?.modelAbortSignal as AbortSignal | undefined;
if (sig?.aborted) sawAbort = true;
resolve(new Response("ok"));
}),
comboTargetTimeoutMs: 1000,
log: noopLog,
});
await runner({}, "m", { modelAbortSignal: parent.signal } as any);
assert.equal(sawAbort, true);
});