Files
OmniRoute/tests/unit/account-rotation.test.ts
Dizzle 1058426120 fix(executors): rotate on upstream 400 empty-body rejections (opencode) (#11158)
Validated on the combined batch board over tip 92ef3c71: static gates clean (changelog, file-size, complexity 2624<=2774, cognitive 1182<=1223, dead-code 411<=416), typecheck:core clean, focused tests green.

Empty-envelope 400 (no error field, empty content, finish_reason null) now rotates/retries instead of propagating as success; 200/streaming path never buffered; real-error 400s untouched. account-rotation + new rotation suite 34/34 on the board. Thank you @maxmad64bis!
2026-08-22 21:51:22 -03:00

216 lines
9.1 KiB
TypeScript

import { describe, it } from "node:test";
import assert from "node:assert";
import {
isAccountReady,
pickAccount,
markCooldown,
markSuccess,
maskAccountId,
isNetworkErrorRotatable,
isEmptyUpstreamRejection,
extractChatcmplId,
type RotatableAccount,
} from "../../open-sse/executors/accountRotation.ts";
function account(overrides: Partial<RotatableAccount> = {}): RotatableAccount {
return {
fingerprint: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
cooldownUntil: 0,
consecutiveFails: 0,
proxy: null,
...overrides,
};
}
describe("accountRotation", () => {
it("isAccountReady is true when cooldownUntil is in the past", () => {
assert.strictEqual(isAccountReady(account({ cooldownUntil: Date.now() - 1000 })), true);
});
it("isAccountReady is false when cooldownUntil is in the future", () => {
assert.strictEqual(isAccountReady(account({ cooldownUntil: Date.now() + 60_000 })), false);
});
it("markCooldown increments consecutiveFails and sets a future cooldownUntil", () => {
const acct = account();
markCooldown(acct);
assert.strictEqual(acct.consecutiveFails, 1);
assert.ok(acct.cooldownUntil > Date.now());
});
it("markCooldown backs off exponentially with consecutive failures", () => {
const acct = account();
markCooldown(acct);
const firstCooldown = acct.cooldownUntil;
markCooldown(acct);
assert.strictEqual(acct.consecutiveFails, 2);
// Second backoff (base*2^1) must be strictly larger than the first
// (base*2^0), modulo the shared jitter window — compare the floor.
assert.ok(acct.cooldownUntil - Date.now() > firstCooldown - Date.now() - 1000);
});
it("markCooldown uses the same magnitude regardless of why it was called (429 or network throw)", () => {
// No `short`/severity parameter: proxy-attributable failures (429, dead
// proxy) and shared-egress network throws use the identical formula —
// the repo's own established "transient, not clearly attributable"
// cooldown (errorConfig.ts TRANSIENT_COOLDOWN_MS/transientMax) already
// covers both cases at the same magnitude. The behavioral fix for
// shared-egress accounts lives in the caller's skip logic, not here.
const a = account();
const b = account();
markCooldown(a);
markCooldown(b);
// Both draw from the same base backoff ± up to 1s jitter — same formula,
// no separate "short" magnitude for either call site.
assert.ok(
Math.abs(a.cooldownUntil - b.cooldownUntil) <= 1000,
"same account state must produce cooldowns within the shared jitter window"
);
});
it("markSuccess resets consecutiveFails to 0", () => {
const acct = account({ consecutiveFails: 5 });
markSuccess(acct);
assert.strictEqual(acct.consecutiveFails, 0);
});
it("maskAccountId masks a real fingerprint to its first 8 chars + ellipsis", () => {
assert.strictEqual(maskAccountId("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), "aaaaaaaa…");
});
it("maskAccountId reports the empty/default fingerprint as 'direct'", () => {
assert.strictEqual(maskAccountId(""), "direct");
});
it("pickAccount skips accounts in cooldown and rotates nextAccountIdx", () => {
const a = account({ fingerprint: "a", cooldownUntil: Date.now() + 60_000 });
const b = account({ fingerprint: "b", cooldownUntil: 0 });
const state = { nextAccountIdx: 0 };
const picked = pickAccount([a, b], state);
assert.strictEqual(picked.fingerprint, "b", "must skip the account still in cooldown");
});
it("pickAccount falls back to the next index when every account is in cooldown", () => {
const a = account({ fingerprint: "a", cooldownUntil: Date.now() + 60_000 });
const b = account({ fingerprint: "b", cooldownUntil: Date.now() + 60_000 });
const state = { nextAccountIdx: 0 };
const picked = pickAccount([a, b], state);
assert.strictEqual(picked.fingerprint, "a", "must still return an account, not throw/hang");
});
it("pickAccount accepts a custom isReady predicate (e.g. JWT-freshness-aware)", () => {
const a = account({ fingerprint: "a", cooldownUntil: 0 });
const b = account({ fingerprint: "b", cooldownUntil: 0 });
const state = { nextAccountIdx: 0 };
// Custom predicate rejects "a" for a reason cooldown alone wouldn't catch.
const picked = pickAccount([a, b], state, (acct: RotatableAccount) => acct.fingerprint !== "a");
assert.strictEqual(picked.fingerprint, "b");
});
it("isNetworkErrorRotatable is true only when the account has a configured proxy", () => {
const withProxy = account({
proxy: { type: "http", host: "127.0.0.1", port: 8080 },
});
const withoutProxy = account({ proxy: null });
assert.strictEqual(isNetworkErrorRotatable(withProxy), true);
assert.strictEqual(isNetworkErrorRotatable(withoutProxy), false);
});
});
describe("isEmptyUpstreamRejection", () => {
it("matches the observed malformed completion envelope (no error field, empty content, null finish_reason)", () => {
const observed =
'{"id":"chatcmpl_44fn2g6e7kk","object":"chat.completion","created":1787419957,"model":"muse-spark-1.2-contributor-free","choices":[{"index":0,"message":{"role":"assistant"},"finish_reason":null}]}';
assert.strictEqual(isEmptyUpstreamRejection(400, observed), true);
});
it("does not match a non-400 status", () => {
const observed =
'{"id":"chatcmpl_44fn2g6e7kk","object":"chat.completion","created":1787419957,"model":"muse-spark-1.2-contributor-free","choices":[{"index":0,"message":{"role":"assistant"},"finish_reason":null}]}';
assert.strictEqual(isEmptyUpstreamRejection(200, observed), false);
assert.strictEqual(isEmptyUpstreamRejection(429, observed), false);
assert.strictEqual(isEmptyUpstreamRejection(502, observed), false);
});
it("does not match when an error field is present", () => {
const withError = JSON.stringify({
error: { message: "bad request", type: "invalid_request_error" },
});
assert.strictEqual(isEmptyUpstreamRejection(400, withError), false);
const emptyError = JSON.stringify({ error: {} });
assert.strictEqual(isEmptyUpstreamRejection(400, emptyError), false);
});
it("does not match when content is non-empty or tool_calls present", () => {
const nonEmpty = JSON.stringify({
choices: [{ message: { role: "assistant", content: "hi" }, finish_reason: "stop" }],
});
assert.strictEqual(isEmptyUpstreamRejection(400, nonEmpty), false);
const toolCalls = JSON.stringify({
choices: [
{ message: { role: "assistant", tool_calls: [{ id: "x" }] }, finish_reason: "tool_calls" },
],
});
assert.strictEqual(isEmptyUpstreamRejection(400, toolCalls), false);
});
it("does not match when content is a non-string non-null value (number, block array)", () => {
const numericContent = JSON.stringify({
choices: [{ message: { role: "assistant", content: 123 }, finish_reason: null }],
});
assert.strictEqual(
isEmptyUpstreamRejection(400, numericContent),
false,
"non-string non-null content is not eligible"
);
const reasoningContent = JSON.stringify({
choices: [
{ message: { role: "assistant", reasoning_content: "thinking" }, finish_reason: null },
],
});
assert.strictEqual(isEmptyUpstreamRejection(400, reasoningContent), false);
});
it("does not match when choices or message are absent", () => {
const noChoices = JSON.stringify({ id: "chatcmpl_x", model: "muse" });
assert.strictEqual(isEmptyUpstreamRejection(400, noChoices), false);
const noMessage = JSON.stringify({ choices: [{ finish_reason: null }] });
assert.strictEqual(isEmptyUpstreamRejection(400, noMessage), false);
});
it("does not match when finish_reason is a literal value (not null)", () => {
const stopReason = JSON.stringify({
choices: [{ message: { role: "assistant" }, finish_reason: "stop" }],
});
assert.strictEqual(isEmptyUpstreamRejection(400, stopReason), false);
});
it("matches an empty string content (treated as eligible)", () => {
const emptyContent = JSON.stringify({
choices: [{ message: { role: "assistant", content: "" }, finish_reason: null }],
});
assert.strictEqual(isEmptyUpstreamRejection(400, emptyContent), true);
});
it("returns false for unparseable JSON rather than throwing", () => {
assert.strictEqual(isEmptyUpstreamRejection(400, "not json"), false);
assert.strictEqual(isEmptyUpstreamRejection(400, ""), false);
});
});
describe("extractChatcmplId", () => {
it("extracts the chatcmpl id from an observed envelope", () => {
const observed =
'{"id":"chatcmpl_44fn2g6e7kk","object":"chat.completion","created":1787419957,"model":"muse-spark-1.2-contributor-free","choices":[{"index":0,"message":{"role":"assistant"},"finish_reason":null}]}';
assert.strictEqual(extractChatcmplId(observed), "chatcmpl_44fn2g6e7kk");
});
it("falls back to 'unknown' when no id is present", () => {
assert.strictEqual(extractChatcmplId("{choices:[]}"), "unknown");
assert.strictEqual(extractChatcmplId(""), "unknown");
assert.strictEqual(extractChatcmplId("not json"), "unknown");
});
});