mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 00:02:20 +03:00
fix(sse): replace timer-based waits with polling to fix flaky chatCore/SSE tests under CI load (#9567)
Closes #9567
This commit is contained in:
committed by
GitHub
parent
919f9acd80
commit
fad3539a69
1
changelog.d/fixes/9567-chatcore-sse-flaky.md
Normal file
1
changelog.d/fixes/9567-chatcore-sse-flaky.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(sse): replace timer-based waits with polling to fix flaky chatCore/SSE tests under CI load (#9567)
|
||||
@@ -618,12 +618,6 @@ export type CompatFilterOptions = {
|
||||
failOpen?: boolean;
|
||||
};
|
||||
|
||||
const HARD_COMPAT_REASONS = new Set(["tools", "vision", "structured_output"]);
|
||||
|
||||
function hasHardCapabilityFailure(reasons: string[]): boolean {
|
||||
return reasons.some((reason) => HARD_COMPAT_REASONS.has(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize a capability-filter exhaustion for a 400-class combo error (#8488).
|
||||
* Returns null when the empty pool is not attributable to hard requirements.
|
||||
@@ -727,7 +721,9 @@ export function filterTargetsByRequestCompatibility(
|
||||
|
||||
if (compatible.length === targets.length) return targets;
|
||||
if (compatible.length === 0) {
|
||||
const hardRejected = rejected.some((entry) => hasHardCapabilityFailure(entry.reasons));
|
||||
const hardRejected = rejected.some((entry) =>
|
||||
entry.reasons.some((r) => HARD_COMPAT_REASONS.has(r))
|
||||
);
|
||||
const failOpen = options?.failOpen === true;
|
||||
|
||||
log.debug?.(
|
||||
|
||||
@@ -29,14 +29,14 @@ function createSseResponse(events: string[]) {
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForAsyncSideEffects() {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
async function flushAsyncSideEffects() {
|
||||
// setImmediate rounds drain the event loop more reliably than setTimeout under CI load.
|
||||
for (let i = 0; i < 5; i++) await new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
|
||||
test.afterEach(async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
await waitForAsyncSideEffects();
|
||||
await flushAsyncSideEffects();
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
|
||||
@@ -250,7 +250,7 @@ test("chat completions route emits early keepalive while waiting for stream read
|
||||
await seedHealthyConnection();
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2200));
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return new Response(
|
||||
[
|
||||
`data: ${JSON.stringify({
|
||||
@@ -274,10 +274,7 @@ test("chat completions route emits early keepalive while waiting for stream read
|
||||
assert.match(response.headers.get("content-type") || "", /text\/event-stream/);
|
||||
|
||||
const body = await readAll(response);
|
||||
assert.match(
|
||||
body,
|
||||
/data: \{"id":"chatcmpl-keepalive","object":"chat\.completion\.chunk"/
|
||||
);
|
||||
assert.match(body, /data: \{"id":"chatcmpl-keepalive","object":"chat\.completion\.chunk"/);
|
||||
assert.match(body, /OK/);
|
||||
assert.match(body, /\[DONE\]/);
|
||||
});
|
||||
@@ -286,7 +283,7 @@ test("chat completions route returns JSON without early SSE framing when stream
|
||||
await seedHealthyConnection();
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2200));
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return Response.json({
|
||||
id: "chatcmpl-slow-json",
|
||||
choices: [
|
||||
|
||||
@@ -84,9 +84,9 @@ function ensureLegacyMemoryTable() {
|
||||
`);
|
||||
}
|
||||
|
||||
async function waitForAsyncMemoryFlush() {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
async function flushAsyncSideEffects() {
|
||||
// setImmediate rounds drain the event loop more reliably than setTimeout under CI load.
|
||||
for (let i = 0; i < 5; i++) await new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
|
||||
async function invokeChatCore({
|
||||
@@ -647,7 +647,7 @@ test("chatCore does not share or persist memories when apiKeyInfo is missing", a
|
||||
},
|
||||
});
|
||||
|
||||
await waitForAsyncMemoryFlush();
|
||||
await flushAsyncSideEffects();
|
||||
|
||||
const localMemoriesResult = await listMemories({ apiKeyId: "local" });
|
||||
const localMemories = Array.isArray(localMemoriesResult)
|
||||
@@ -751,7 +751,7 @@ test("chatCore extracts memories from Claude content arrays and Responses output
|
||||
|
||||
assert.equal(responsesResult.result.success, true);
|
||||
|
||||
await waitForAsyncMemoryFlush();
|
||||
await flushAsyncSideEffects();
|
||||
|
||||
const claudeMemoriesResult = await listMemories({ apiKeyId: claudeKeyId });
|
||||
const responsesMemoriesResult = await listMemories({ apiKeyId: responsesKeyId });
|
||||
@@ -819,7 +819,7 @@ test("chatCore request memory extraction for responses input ignores assistant i
|
||||
|
||||
assert.equal(responsesResult.result.success, true);
|
||||
|
||||
await waitForAsyncMemoryFlush();
|
||||
await flushAsyncSideEffects();
|
||||
|
||||
const memoriesResult = await listMemories({ apiKeyId: responsesKeyId });
|
||||
const memories = Array.isArray(memoriesResult) ? memoriesResult : (memoriesResult.data ?? []);
|
||||
|
||||
@@ -287,9 +287,9 @@ async function waitFor(fn, timeoutMs = 30000) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function waitForAsyncSideEffects() {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
async function flushAsyncSideEffects() {
|
||||
// setImmediate rounds drain the event loop more reliably than setTimeout under CI load.
|
||||
for (let i = 0; i < 5; i++) await new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
|
||||
async function getLatestCallLog() {
|
||||
@@ -363,7 +363,7 @@ async function invokeChatCore({
|
||||
onCredentialsRefreshed,
|
||||
onRequestSuccess,
|
||||
} as any);
|
||||
await waitForAsyncSideEffects();
|
||||
await flushAsyncSideEffects();
|
||||
|
||||
return { result, calls, call: calls.at(-1) };
|
||||
} finally {
|
||||
@@ -376,7 +376,7 @@ test.afterEach(async () => {
|
||||
restorePipelineCaptureEnv();
|
||||
clearPendingRequests();
|
||||
resetAccountSemaphores();
|
||||
await waitForAsyncSideEffects();
|
||||
await flushAsyncSideEffects();
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
@@ -385,7 +385,7 @@ test.after(async () => {
|
||||
restorePipelineCaptureEnv();
|
||||
clearPendingRequests();
|
||||
resetAccountSemaphores();
|
||||
await waitForAsyncSideEffects();
|
||||
await flushAsyncSideEffects();
|
||||
await resetStorage();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
@@ -443,7 +443,7 @@ test("chatCore times out upstream execution before provider response headers", a
|
||||
assert.equal(pendingDetail?.providerRequest?.model, "gpt-4o-mini");
|
||||
assert.deepEqual(pendingDetail?.providerRequest?.messages, body.messages);
|
||||
const result = await invocation;
|
||||
await waitForAsyncSideEffects();
|
||||
await flushAsyncSideEffects();
|
||||
|
||||
assert.equal(upstreamBodies[0]?.model, "gpt-4o-mini");
|
||||
assert.deepEqual(upstreamBodies[0]?.messages, body.messages);
|
||||
@@ -472,7 +472,7 @@ test("chatCore can disable pipeline stream chunk capture through environment", a
|
||||
|
||||
assert.equal(result.success, true);
|
||||
await result.response.text();
|
||||
await waitForAsyncSideEffects();
|
||||
await flushAsyncSideEffects();
|
||||
|
||||
const detail = await waitFor(getLatestCallLog);
|
||||
assert.ok(detail, "expected call log detail to be persisted");
|
||||
@@ -1702,7 +1702,7 @@ test("chatCore returns a semantic cache HIT for repeated deterministic requests"
|
||||
const payload = (await second.result.response.json()) as any;
|
||||
assert.equal(payload.choices[0].message.content, "cached-once");
|
||||
|
||||
await waitForAsyncSideEffects();
|
||||
await flushAsyncSideEffects();
|
||||
const semanticLog = await waitFor(async () => {
|
||||
const rows = await getCallLogs({ limit: 10 });
|
||||
const hit = rows.find((row) => row.cacheSource === "semantic");
|
||||
@@ -2632,7 +2632,7 @@ test("chatCore releases account semaphore slots when upstream execution throws",
|
||||
},
|
||||
});
|
||||
|
||||
await waitForAsyncSideEffects();
|
||||
await flushAsyncSideEffects();
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 502);
|
||||
@@ -2709,7 +2709,7 @@ test("chatCore caches streaming response and serves cache HIT on repeat", async
|
||||
assert.equal(first.result.success, true);
|
||||
// Consume the stream to trigger onStreamComplete and cache write
|
||||
await first.result.response.text();
|
||||
await waitForAsyncSideEffects();
|
||||
await flushAsyncSideEffects();
|
||||
|
||||
// Second request with same body should get cache HIT (JSON, not SSE)
|
||||
const second = await invokeChatCore({
|
||||
@@ -2762,7 +2762,7 @@ test("chatCore does not cache streaming response when temperature > 0", async ()
|
||||
});
|
||||
|
||||
await first.result.response.text();
|
||||
await waitForAsyncSideEffects();
|
||||
await flushAsyncSideEffects();
|
||||
|
||||
const second = await invokeChatCore({
|
||||
provider: "openai",
|
||||
@@ -2804,7 +2804,7 @@ test("chatCore skips streaming cache when X-OmniRoute-No-Cache header is set", a
|
||||
});
|
||||
|
||||
await first.result.response.text();
|
||||
await waitForAsyncSideEffects();
|
||||
await flushAsyncSideEffects();
|
||||
|
||||
// Verify nothing was cached
|
||||
const sig = generateSignature("gpt-4o-mini", sharedBody.messages, 0, 1);
|
||||
|
||||
@@ -143,9 +143,9 @@ async function resetStorage() {
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
async function waitForAsyncSideEffects() {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
async function flushAsyncSideEffects() {
|
||||
// setImmediate rounds drain the event loop more reliably than setTimeout under CI load.
|
||||
for (let i = 0; i < 5; i++) await new Promise((resolve) => setImmediate(resolve));
|
||||
}
|
||||
|
||||
async function invokeChatCore({
|
||||
@@ -192,7 +192,7 @@ async function invokeChatCore({
|
||||
},
|
||||
userAgent: "unit-test",
|
||||
});
|
||||
await waitForAsyncSideEffects();
|
||||
await flushAsyncSideEffects();
|
||||
return { result, calls, call: calls.at(-1) };
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
|
||||
@@ -382,6 +382,6 @@ test("aborting the client signal stops the keepalive stream (#2544)", async () =
|
||||
if (done) return true;
|
||||
}
|
||||
})();
|
||||
const timed = new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 500));
|
||||
const timed = new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 5000));
|
||||
assert.equal(await Promise.race([drained, timed]), true, "stream should close after abort");
|
||||
});
|
||||
|
||||
@@ -43,11 +43,6 @@ async function waitFor(fn, timeoutMs = 1500) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function waitForAsyncSideEffects() {
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
async function getLatestCallLog() {
|
||||
const rows = await getCallLogs({ limit: 5 });
|
||||
if (!Array.isArray(rows) || rows.length === 0) return null;
|
||||
@@ -85,7 +80,6 @@ test.afterEach(async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
clearPendingRequests();
|
||||
resetAccountSemaphores();
|
||||
await waitForAsyncSideEffects();
|
||||
await resetStorage();
|
||||
});
|
||||
|
||||
@@ -124,8 +118,8 @@ test("network failure persisted call log includes providerRequest in pipeline pa
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 502);
|
||||
|
||||
await waitForAsyncSideEffects();
|
||||
|
||||
// waitFor below polls for the exact DB state with 25ms intervals — no
|
||||
// unreliable fixed-delay timer needed, even under CI load contention.
|
||||
const detail = await waitFor(getLatestCallLog);
|
||||
assert.ok(detail, "expected a call log to be persisted");
|
||||
|
||||
@@ -188,7 +182,6 @@ test("network timeout persisted call log includes providerRequest in pipeline pa
|
||||
} as any);
|
||||
|
||||
const result = await invocation;
|
||||
await waitForAsyncSideEffects();
|
||||
|
||||
assert.equal(result.success, false);
|
||||
assert.ok(result.status === 504, `expected 504 timeout, got ${result.status}`);
|
||||
@@ -244,8 +237,6 @@ test("provider error response (HTTP 502) includes both providerRequest and provi
|
||||
assert.equal(result.success, false);
|
||||
assert.equal(result.status, 502);
|
||||
|
||||
await waitForAsyncSideEffects();
|
||||
|
||||
const detail = await waitFor(getLatestCallLog);
|
||||
assert.ok(detail, "expected a call log to be persisted");
|
||||
|
||||
@@ -312,8 +303,6 @@ test("successful response includes both providerRequest and providerResponse in
|
||||
|
||||
assert.equal(result.success, true);
|
||||
|
||||
await waitForAsyncSideEffects();
|
||||
|
||||
const detail = await waitFor(getLatestCallLog);
|
||||
assert.ok(detail, "expected a call log to be persisted");
|
||||
|
||||
@@ -391,7 +380,6 @@ test("streaming response preserves request headers in providerRequest pipeline p
|
||||
|
||||
assert.equal(result.success, true);
|
||||
await result.response.text();
|
||||
await waitForAsyncSideEffects();
|
||||
|
||||
const detail = await waitFor(getLatestCallLog);
|
||||
assert.ok(detail, "expected a call log to be persisted");
|
||||
@@ -475,7 +463,6 @@ test("CC-compatible providerRequest log keeps request beta headers and summarize
|
||||
|
||||
assert.equal(result.success, true);
|
||||
await result.response.json();
|
||||
await waitForAsyncSideEffects();
|
||||
|
||||
const detail = await waitFor(getLatestCallLog);
|
||||
assert.ok(detail, "expected a call log to be persisted");
|
||||
|
||||
Reference in New Issue
Block a user