Files
OmniRoute/tests/unit/routing-adaptive-e2e.test.ts
3g0r1ch d87b97a786 feat(routing): adaptive feedback loop v2 — operational/semantic quality, confidence, TTFT/ITL, end-to-end test (#10881)
Obrigado — feature substancial e bem estruturada: separa qualidade operacional (comportamento de wire: 4xx/5xx, 429, respostas malformadas, stream interrompido) de qualidade semântica (só setada por avaliadores externos, nunca inferida do sucesso HTTP), com confidence/sample-awareness para não deixar poucos sucessos de sorte dominarem o ranking. Instrumentação de streaming (TTFT/ITL) threaded até RoutingEvent, endpoint de explicabilidade, e teste E2E determinístico cobrindo degradação→recuperação→blip.

Validação (worktree própria a partir de origin/release/v3.8.50, merge limpo, 0 conflitos):
- typecheck:core limpo, complexity/cognitive-complexity dentro do baseline
- 59/59 testes passando (mlx-provider, routing-adaptive-e2e, routing-events(-concurrency), routing-otel, routing-quality, routing-scoring-quality, stream-timing, auto-combo-scoring-clamp)
2026-08-20 17:28:30 -03:00

200 lines
6.8 KiB
TypeScript

/**
* tests/unit/routing-adaptive-e2e.test.ts
*
* Deterministic end-to-end adaptive routing test (Phases 5 + 13).
*
* Exercises the REAL production routing path: the routing-event quality tracker
* → auto-combo scoring (`scoreAutoTargets` from open-sse/services/combo/autoStrategy.ts),
* which is what an "auto" combo uses to pick its preferred provider/model.
*
* Scenarios verified:
* 1. Healthy provider A outranks/ties backup B.
* 2. Degradation injected into A (sustained 5xx) → A's score falls below B → B preferred.
* 3. Recovery injected into A (sustained successes) → A's score recovers → A preferred again.
* 4. A cold provider C is neutral — it neither dominates nor is unfairly penalized.
* 5. One isolated failure does not overturn a healthy provider.
*
* The whole loop is deterministic: no network, no DB, only the real tracker +
* the real scorer.
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
resetQualityTracker,
recordQualityEvent,
} from "../../open-sse/services/routing/quality.ts";
import { qualityScoreFor } from "../../open-sse/services/routing/index.ts";
import { scoreAutoTargets } from "../../open-sse/services/combo/autoStrategy.ts";
import { DEFAULT_WEIGHTS } from "../../open-sse/services/autoCombo/scoring.ts";
import type { ResolvedComboTarget } from "../../open-sse/services/combo/types.ts";
import type { AutoProviderCandidate } from "../../open-sse/services/combo/types.ts";
function target(provider: string, model: string, weight = 1): ResolvedComboTarget {
const modelStr = `${provider}/${model}`;
return {
kind: "model",
stepId: modelStr,
executionKey: modelStr,
modelStr,
provider,
providerId: provider,
connectionId: null,
weight,
label: null,
};
}
function candidate(
target: ResolvedComboTarget,
quality: number,
extra: Partial<AutoProviderCandidate> = {}
): AutoProviderCandidate {
return {
stepId: target.stepId,
executionKey: target.executionKey,
modelStr: target.modelStr,
provider: target.provider,
model: target.modelStr.split("/")[1],
quotaRemaining: 100,
quotaTotal: 100,
circuitBreakerState: "CLOSED",
costPer1MTokens: 1,
p95LatencyMs: 100,
latencyStdDev: 10,
errorRate: 0,
accountTier: "standard",
quotaResetIntervalSecs: 86400,
contextAffinity: 0.5,
sessionAvailability: 1,
resetWindowAffinity: 0.5,
connectionPoolSize: 1,
connectionId: null,
quality,
...extra,
};
}
function record(
provider: string,
model: string,
partial: Partial<Parameters<typeof recordQualityEvent>[0]> = {}
): void {
recordQualityEvent({
provider,
model,
outcome: "success",
status: 200,
latencyMs: 100,
finishReason: "stop",
outputTokens: 5,
...partial,
});
}
function bestProvider(targets: ReturnType<typeof scoreAutoTargets>): string {
return targets[0].target.provider;
}
/** Derive the bare model id from a "provider/model" modelStr. */
function modelOf(t: ResolvedComboTarget): string {
return t.modelStr.slice(t.provider.length + 1);
}
/** Score a set of providers using their live quality-tracker signal. */
function scoreProviders(
ts: ResolvedComboTarget[],
weights = DEFAULT_WEIGHTS
): ReturnType<typeof scoreAutoTargets> {
return scoreAutoTargets(
ts,
ts.map((t) => candidate(t, qualityScoreFor(t.provider, modelOf(t)))),
"general",
weights
);
}
test("healthy provider A is preferred over backup B, cold C stays neutral", () => {
resetQualityTracker();
// Warm A to high confidence with solid success.
for (let i = 0; i < 100; i++) record("a", "m");
// B warm but mildly degraded.
for (let i = 0; i < 100; i++)
record("b", "m", {
outcome: i % 5 === 0 ? "error" : "success",
status: i % 5 === 0 ? 500 : 200,
});
const a = target("a", "m");
const b = target("b", "m");
const c = target("c", "m");
const scored = scoreProviders([a, b, c]);
assert.equal(bestProvider(scored), "a", "healthy A must be the top pick");
// Cold C must not be top (neutral 0.5 quality vs A's high quality).
assert.notEqual(bestProvider(scored), "c", "cold provider must not dominate");
// C's quality must be exactly neutral.
assert.equal(qualityScoreFor("c", "m"), 0.5);
});
test("degradation injected into A flips preference to B; recovery flips it back", () => {
resetQualityTracker();
// Phase 0: A and B are otherwise identical; both healthy. A is preferred via
// stable tie-break, and quality is the only differentiator.
for (let i = 0; i < 100; i++) record("a", "m");
for (let i = 0; i < 100; i++) record("b", "m");
const a = target("a", "m");
const b = target("b", "m");
const score = () => scoreProviders([a, b]);
const initial = score();
assert.equal(bestProvider(initial), "a", "initially A is preferred (tie-break on equal quality)");
// Phase 1: degrade A — sustained 5xx. A's quality collapses to ~0, so B
// (identical but healthy) becomes preferred. Gradual: the EWMA smooths the drop.
for (let i = 0; i < 60; i++) record("a", "m", { outcome: "error", status: 500 });
const during = score();
assert.equal(
bestProvider(during),
"b",
"sustained degradation must flip preference to B (gradual, not instant)"
);
const qualityA = qualityScoreFor("a", "m");
assert.ok(qualityA < 0.5, `A quality degraded below neutral, got ${qualityA}`);
// Phase 2: recover A — sustained successes. Quality recovers and A's
// preference is restored.
for (let i = 0; i < 200; i++) record("a", "m");
const after = score();
assert.equal(bestProvider(after), "a", "recovery must restore A's preference");
// Phase 3: one isolated failure must not destroy A — its quality stays healthy
// (well above neutral), it just falls marginally behind the now-equally-tied B.
record("a", "m", { outcome: "error", status: 500 });
const qualityAfterBlip = qualityScoreFor("a", "m");
assert.ok(
qualityAfterBlip > 0.7,
`one isolated failure must not destroy A's quality, got ${qualityAfterBlip}`
);
const afterBlip = score();
const aEntry = afterBlip.find((s) => s.target.provider === "a");
const bEntry = afterBlip.find((s) => s.target.provider === "b");
assert.ok(
Math.abs(aEntry!.score - bEntry!.score) < 0.02,
"A must remain competitive after one isolated failure (not destroyed)"
);
});
test("a provider with insufficient evidence does not dominate from optimistic init", () => {
resetQualityTracker();
// Solid warm provider.
for (let i = 0; i < 200; i++) record("solid", "m");
// Lucky cold provider: 7 flawless successes.
for (let i = 0; i < 7; i++) record("lucky", "m");
const s = target("solid", "m");
const l = target("lucky", "m");
const scored = scoreProviders([s, l]);
assert.equal(bestProvider(scored), "solid", "solid warm provider must beat a lucky cold one");
});