148 lines
4.7 KiB
TypeScript
148 lines
4.7 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-499-"));
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "combo-499-test-secret";
|
|
|
|
const { handleComboChat } = await import("../../open-sse/services/combo.ts");
|
|
|
|
const noop = () => {};
|
|
const log = { info: noop, warn: noop, debug: noop, error: noop };
|
|
|
|
function makeCombo(strategy = "priority", models = ["a/model-1", "b/model-2", "c/model-3"]) {
|
|
return {
|
|
name: "test-combo-499",
|
|
strategy,
|
|
models: models.map((m) => ({ model: m })),
|
|
};
|
|
}
|
|
|
|
test("combo loop stops immediately when handleSingleModel returns 499 (client disconnect)", async () => {
|
|
let callCount = 0;
|
|
const handleSingleModel = async () => {
|
|
callCount++;
|
|
return new Response("Client disconnected", { status: 499 });
|
|
};
|
|
|
|
const result = await handleComboChat({
|
|
body: { model: "test", messages: [{ role: "user", content: "hi" }] },
|
|
combo: makeCombo("priority"),
|
|
handleSingleModel,
|
|
log,
|
|
settings: {},
|
|
allCombos: [],
|
|
});
|
|
|
|
// Should stop after the FIRST model — no fallback to model-2 or model-3
|
|
assert.equal(callCount, 1, "should only call handleSingleModel once on 499");
|
|
assert.equal(result.status, 499, "should return 499 status");
|
|
});
|
|
|
|
test("combo loop stops on signal.aborted before trying any model", async () => {
|
|
let callCount = 0;
|
|
const handleSingleModel = async () => {
|
|
callCount++;
|
|
return new Response("ok", { status: 200 });
|
|
};
|
|
|
|
const ac = new AbortController();
|
|
ac.abort(); // Pre-abort
|
|
|
|
const result = await handleComboChat({
|
|
body: { model: "test", messages: [{ role: "user", content: "hi" }] },
|
|
combo: makeCombo("priority"),
|
|
handleSingleModel,
|
|
log,
|
|
settings: {},
|
|
allCombos: [],
|
|
signal: ac.signal,
|
|
});
|
|
|
|
assert.equal(callCount, 0, "should NOT call handleSingleModel when signal is already aborted");
|
|
assert.equal(result.status, 499);
|
|
});
|
|
|
|
test("combo loop with 3 models: 499 on model-1 prevents trying model-2 and model-3", async () => {
|
|
const modelsCalled = [];
|
|
const handleSingleModel = async (_body, modelStr) => {
|
|
modelsCalled.push(modelStr);
|
|
return new Response("Client disconnected", { status: 499 });
|
|
};
|
|
|
|
const result = await handleComboChat({
|
|
body: { model: "test", messages: [{ role: "user", content: "hi" }] },
|
|
combo: makeCombo("priority", ["provider-a/fast", "provider-b/medium", "provider-c/large"]),
|
|
handleSingleModel,
|
|
log,
|
|
settings: {},
|
|
allCombos: [],
|
|
});
|
|
|
|
assert.equal(modelsCalled.length, 1);
|
|
assert.ok(
|
|
modelsCalled[0].includes("fast"),
|
|
`Expected first model to contain 'fast', got '${modelsCalled[0]}'`
|
|
);
|
|
assert.equal(result.status, 499);
|
|
});
|
|
|
|
test("combo loop does NOT stop on 502 (transient) — tries more than one model", async () => {
|
|
let callCount = 0;
|
|
const handleSingleModel = async () => {
|
|
callCount++;
|
|
return new Response("Bad Gateway", { status: 502 });
|
|
};
|
|
|
|
const result = await handleComboChat({
|
|
body: { model: "test", messages: [{ role: "user", content: "hi" }] },
|
|
combo: makeCombo("priority", ["a/m1", "b/m2", "c/m3"]),
|
|
handleSingleModel,
|
|
log,
|
|
settings: {},
|
|
allCombos: [],
|
|
});
|
|
|
|
// Key assertion: unlike 499, a 502 should try more than 1 model
|
|
assert.ok(callCount >= 2, `502 should attempt multiple models, but only tried ${callCount}`);
|
|
assert.equal(result.status, 502, "should still return 502 if all models fail");
|
|
});
|
|
|
|
test("signal abort during fallback wait interrupts immediately", async () => {
|
|
const ac = new AbortController();
|
|
let callCount = 0;
|
|
|
|
const handleSingleModel = async () => {
|
|
callCount++;
|
|
if (callCount === 1) {
|
|
// First call returns 502 (which triggers fallback wait)
|
|
return new Response("Bad Gateway", { status: 502 });
|
|
}
|
|
return new Response("ok", { status: 200 });
|
|
};
|
|
|
|
// Abort 50ms after start — should interrupt any wait
|
|
const timer = setTimeout(() => ac.abort(), 50);
|
|
|
|
const startMs = Date.now();
|
|
const result = await handleComboChat({
|
|
body: { model: "test", messages: [{ role: "user", content: "hi" }] },
|
|
combo: makeCombo("priority", ["a/m1", "b/m2"]),
|
|
handleSingleModel,
|
|
log,
|
|
settings: { fallbackDelayMs: 5000 }, // 5s delay would normally be slow
|
|
allCombos: [],
|
|
signal: ac.signal,
|
|
});
|
|
|
|
clearTimeout(timer);
|
|
const elapsed = Date.now() - startMs;
|
|
|
|
// Should complete in well under 5s — the abort interrupted the fallback wait
|
|
assert.ok(elapsed < 2000, `Expected fast abort, but took ${elapsed}ms`);
|
|
assert.equal(result.status, 499, "should return 499 after abort during wait");
|
|
});
|