Files
OmniRoute/tests/unit/combo/execute-target-attempt.test.ts
Bob.Hou c75e293a47 fix(api): thread X-OmniRoute-Fallback-Attempts through combo chat (#12339) (#13038)
Right call not to reuse the combo loop's `fallbackCount`: it only increments after a leg fails or is skipped, so the second target would still report 0 at dispatch. Stamping the ordered index (or round-robin offset) at the gate is the only place the number is actually known. This PR also carries the batch's file-size rebaseline, since it merges first and the ceiling has to cover every intermediate state.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:27:33 -03:00

356 lines
11 KiB
TypeScript

/**
* Characterization for executeTarget retry loop + classify
* (open-sse/services/combo/executeTargetAttempt.ts,
* open-sse/services/combo/executeTargetClassify.ts).
*
* Plan Task 3. RED until those modules exist.
*/
import test from "node:test";
import assert from "node:assert/strict";
import type {
AttemptLoopDeps,
AttemptLoopState,
} from "../../../open-sse/services/combo/attemptLoopTypes.ts";
import type { ResolvedComboTarget } from "../../../open-sse/services/combo/types.ts";
function emptyState(overrides: Partial<AttemptLoopState> = {}): AttemptLoopState {
return {
orderedTargets: [],
fallbackCount: 0,
recordedAttempts: 0,
comboErrors: [],
lastError: null,
lastStatus: null,
earliestRetryAfter: null,
comboExpired: false,
exhaustedProviders: new Set(),
exhaustedConnections: new Set(),
transientRateLimitedProviders: new Set(),
abortControllers: new Map([[0, new AbortController()]]),
dispatchedTargets: new Set(),
targetFailureTrust: new Map(),
comboAttemptOrder: [],
skippedForCircuitOpen: false,
earliestCircuitOpenRetryMs: 0,
globalAttempts: 0,
observedFailure: false,
allObservedFailuresQuota: true,
observeFailure() {},
...overrides,
};
}
function baseDeps(overrides: Partial<AttemptLoopDeps> = {}): AttemptLoopDeps {
const handleSingleModelWithTimeout = async () => {
throw new Error("handleSingleModel must be stubbed");
};
return {
strategy: "priority",
combo: { name: "t", models: [] },
config: {},
log: { info() {}, warn() {}, debug() {}, error() {} },
settings: null,
resilienceSettings: {
providerCooldown: { enabled: false },
} as AttemptLoopDeps["resilienceSettings"],
sticky: { targets: [], messageHash: null, stuck: false },
effectiveSessionId: null,
preScreenMap: new Map(),
quotaCutoffResetWindowConfig: {} as AttemptLoopDeps["quotaCutoffResetWindowConfig"],
maxRetries: 0,
traceInvocationId: "inv-attempt",
clientRequestedStream: false,
handleSingleModelWithTimeout,
body: { messages: [{ role: "user", content: "hi" }] },
startTime: Date.now(),
releaseStickyPinOnFailure() {},
clearStaleLKGP() {},
...overrides,
};
}
function modelTarget(overrides: Partial<ResolvedComboTarget> = {}): ResolvedComboTarget {
return {
kind: "model",
stepId: "s1",
executionKey: "ek-1",
modelStr: "openai/gpt-4o",
provider: "openai",
providerId: null,
connectionId: "c-fail",
weight: 1,
label: null,
...overrides,
};
}
function emptyContent200(connectionId = "c-fail"): Response {
return new Response(
JSON.stringify({ choices: [{ message: { role: "assistant", content: "" } }] }),
{
status: 200,
headers: {
"content-type": "application/json",
"x-omniroute-selected-connection-id": connectionId,
},
}
);
}
// "invalid message format" — same fixture as combo-body-specific-400-stop-4279.test.ts
function bodySpecific400(): Response {
return new Response(
JSON.stringify({
detail: "Invalid message format: the request body is malformed.",
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
test("remainderIsHomogeneous is true only when remaining targets share modelStr", async () => {
const { remainderIsHomogeneous } =
await import("../../../open-sse/services/combo/executeTargetClassify.ts");
const same = [
{ modelStr: "openai/gpt-4o" },
{ modelStr: "openai/gpt-4o" },
{ modelStr: "openai/gpt-4o" },
];
assert.equal(remainderIsHomogeneous(same, 0, "openai/gpt-4o"), true);
const mixed = [{ modelStr: "openai/gpt-4o" }, { modelStr: "anthropic/claude" }];
assert.equal(remainderIsHomogeneous(mixed, 0, "openai/gpt-4o"), false);
assert.equal(remainderIsHomogeneous(same, 2, "openai/gpt-4o"), true);
});
test("shouldAbortOnInputBoundFailure requires homogeneous remainder", async () => {
const { shouldAbortOnInputBoundFailure } =
await import("../../../open-sse/services/combo/executeTargetClassify.ts");
const structured = { code: "context_length_exceeded" };
assert.equal(
shouldAbortOnInputBoundFailure({ structuredError: structured, remainderIsHomogeneous: true }),
true
);
assert.equal(
shouldAbortOnInputBoundFailure({ structuredError: structured, remainderIsHomogeneous: false }),
false
);
assert.equal(
shouldAbortOnInputBoundFailure({
structuredError: { code: "rate_limit" },
remainderIsHomogeneous: true,
}),
false
);
});
test("shouldSurfaceBodySpecific400 matches #4279 invalid-format 400, not model-scoped", async () => {
const { shouldSurfaceBodySpecific400 } =
await import("../../../open-sse/services/combo/executeTargetClassify.ts");
assert.equal(
shouldSurfaceBodySpecific400({
status: 400,
errorText: "Invalid message format: the request body is malformed.",
shouldFallback: true,
}),
true
);
assert.equal(
shouldSurfaceBodySpecific400({
status: 400,
errorText: "The requested model is not supported",
shouldFallback: true,
}),
false
);
assert.equal(
shouldSurfaceBodySpecific400({
status: 429,
errorText: "Invalid message format: the request body is malformed.",
shouldFallback: true,
}),
false
);
});
test("quality-rejected 200 calls releaseStickyPinOnFailure and records kind quality", async () => {
const { executeTargetAttempt } =
await import("../../../open-sse/services/combo/executeTargetAttempt.ts");
let released: string | null = null;
const target = modelTarget({ connectionId: "c-fail" });
const deps = baseDeps({
maxRetries: 0,
clientRequestedStream: false,
releaseStickyPinOnFailure(_hash, id) {
released = String(id);
},
handleSingleModelWithTimeout: async () => emptyContent200("c-fail"),
sticky: { targets: [], messageHash: "h", stuck: true },
});
const state = emptyState({
orderedTargets: [target],
abortControllers: new Map([[0, new AbortController()]]),
});
const result = await executeTargetAttempt({
index: 0,
state,
deps,
targetForAttempt: target,
profile: {},
protectedPriorityTarget: false,
});
assert.equal(released, "c-fail");
assert.equal(
state.comboErrors.some((e) => e.kind === "quality"),
true
);
assert.equal(result, null);
});
test("injection: missing releaseStickyPinOnFailure forwarding goes red on quality fail", async () => {
const { executeTargetAttempt } =
await import("../../../open-sse/services/combo/executeTargetAttempt.ts");
let callCount = 0;
const target = modelTarget({ connectionId: "c-fail" });
const deps = baseDeps({
maxRetries: 0,
clientRequestedStream: false,
releaseStickyPinOnFailure() {
callCount += 1;
},
handleSingleModelWithTimeout: async () => emptyContent200("c-fail"),
sticky: { targets: [], messageHash: "h", stuck: true },
});
const state = emptyState({
orderedTargets: [target],
abortControllers: new Map([[0, new AbortController()]]),
});
await executeTargetAttempt({
index: 0,
state,
deps,
targetForAttempt: target,
profile: {},
protectedPriorityTarget: false,
});
assert.equal(callCount, 1);
});
test("499 surfaces {ok:false,response} and does not continue retries", async () => {
const { executeTargetAttempt } =
await import("../../../open-sse/services/combo/executeTargetAttempt.ts");
let calls = 0;
const target = modelTarget({ connectionId: "c1" });
const deps = baseDeps({
maxRetries: 3,
handleSingleModelWithTimeout: async () => {
calls += 1;
return new Response("disconnected", { status: 499 });
},
});
const state = emptyState({
orderedTargets: [target],
abortControllers: new Map([[0, new AbortController()]]),
});
const result = await executeTargetAttempt({
index: 0,
state,
deps,
targetForAttempt: target,
profile: {},
protectedPriorityTarget: false,
});
assert.equal(result?.ok, false);
assert.equal(result?.response?.status, 499);
assert.equal(calls, 1);
});
test("body-specific 400 surfaces via {ok,response} not null", async () => {
const { executeTargetAttempt } =
await import("../../../open-sse/services/combo/executeTargetAttempt.ts");
const target = modelTarget({ connectionId: "c1", modelStr: "codex/gpt-5.2" });
const deps = baseDeps({
maxRetries: 0,
handleSingleModelWithTimeout: async () => bodySpecific400(),
});
const state = emptyState({
orderedTargets: [target],
abortControllers: new Map([[0, new AbortController()]]),
});
const result = await executeTargetAttempt({
index: 0,
state,
deps,
targetForAttempt: target,
profile: {},
protectedPriorityTarget: false,
});
assert.equal(result?.ok, false);
assert.equal(result?.response?.status, 400);
});
test("spreads stamped fallbackAttempts onto the handleSingleModel target", async () => {
const { executeTargetAttempt } =
await import("../../../open-sse/services/combo/executeTargetAttempt.ts");
let seen: unknown;
const target = {
...modelTarget({ connectionId: "c1" }),
fallbackAttempts: 2,
} as ResolvedComboTarget & { fallbackAttempts: number };
const deps = baseDeps({
maxRetries: 0,
handleSingleModelWithTimeout: async (_body, _model, dispatched) => {
seen = dispatched;
return new Response(JSON.stringify({ choices: [{ message: { content: "ok" } }] }), {
status: 200,
headers: { "content-type": "application/json" },
});
},
});
const state = emptyState({
orderedTargets: [target],
abortControllers: new Map([[0, new AbortController()]]),
});
const result = await executeTargetAttempt({
index: 0,
state,
deps,
targetForAttempt: target,
profile: {},
protectedPriorityTarget: false,
});
assert.equal(result?.ok, true);
assert.equal((seen as { fallbackAttempts?: number } | undefined)?.fallbackAttempts, 2);
});
test("injection: dropping fallbackAttempts from the dispatch target goes red", async () => {
const { executeTargetAttempt } =
await import("../../../open-sse/services/combo/executeTargetAttempt.ts");
let seen: unknown;
const target = {
...modelTarget({ connectionId: "c1" }),
fallbackAttempts: 2,
} as ResolvedComboTarget & { fallbackAttempts: number };
const deps = baseDeps({
maxRetries: 0,
handleSingleModelWithTimeout: async (_body, _model, dispatched) => {
seen = dispatched;
return new Response(JSON.stringify({ choices: [{ message: { content: "ok" } }] }), {
status: 200,
headers: { "content-type": "application/json" },
});
},
});
const state = emptyState({
orderedTargets: [target],
abortControllers: new Map([[0, new AbortController()]]),
});
await executeTargetAttempt({
index: 0,
state,
deps,
targetForAttempt: target,
profile: {},
protectedPriorityTarget: false,
});
assert.equal(Object.prototype.hasOwnProperty.call(seen as object, "fallbackAttempts"), true);
});