Files
OmniRoute/tests/unit/routing-events.test.ts
Praveen K Palaniswamy 65e81158ab fix(ollama): route models by advertised capability (#11088)
Landed with the design call resolved per the owner's pick — **option 1**: the synced store is now endpoint-agnostic (persistDiscoveredModels and managedModelImport no longer drop non-chat models at write time), and chat selectability moved to read time (auto-pool expansion in autoStrategy applies filterChatSelectableModels; the models-route projection already had its chatOnly filter). Your discovery test now passes end-to-end (3/3): /api/show capabilities persist per connection and image/embedding requests route through the advertising host.

Reconciliation notes: conflicted areas merged onto the current tip (adobe discovery import, requestedModel preflight signature, resolvedProvider fast-path coexists with the synced-route override — explicit resolution wins); carried base-red drains (#10055 memoization, #11071 test variants) dropped as already-landed; the managed-model-import exclusion test was propagated to the new contract (image/video models persist; the read filter still hides them from chat pickers — pinned by a new assertion). Full battery: 205/206 focused (the one red is a confirmed periodic-timer timing flake on the loaded devbox — 20/20 isolated), autoCombo vitest 30/30, combo suites 46/46, gates + typecheck clean.

Thank you @yourspraveen — the capability probe + routing design was right; it just needed the store contract opened up. Fixes #11087.
2026-08-23 11:45:01 -03:00

142 lines
4.2 KiB
TypeScript

/**
* tests/unit/routing-events.test.ts
*
* Routing feedback foundation (open-sse/services/routing/events.ts):
* - createRoutingEvent normalizes defaults
* - outcomeFromStatus classifies HTTP statuses
* - MemoryRoutingEventStore is bounded and returns newest-first
* - dispatchRoutingEvent fans out to sinks and isolates a throwing sink
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
MemoryRoutingEventStore,
createRoutingEvent,
outcomeFromStatus,
dispatchRoutingEvent,
registerRoutingEventSink,
listRoutingEventSinks,
clearRoutingEventSinks,
type RoutingEvent,
type RoutingEventSink,
} from "../../open-sse/services/routing/events.ts";
function event(partial: Partial<RoutingEvent> = {}): RoutingEvent {
return createRoutingEvent({
requestId: "req-1",
provider: "openai",
model: "gpt-4o",
strategy: "auto",
latencyMs: 120,
outcome: "success",
status: 200,
...partial,
});
}
test("createRoutingEvent fills observability defaults", () => {
const e = createRoutingEvent({
requestId: "req-x",
provider: "anthropic",
model: "claude-4",
latencyMs: 50,
outcome: "error",
status: 500,
});
assert.equal(e.strategy, "direct");
assert.equal(e.ttftMs, null);
assert.equal(e.inputTokens, null);
assert.equal(e.outputTokens, null);
assert.equal(e.cost, null);
assert.equal(e.retries, 0);
assert.equal(e.fallbackUsed, false);
assert.equal(e.finishReason, null);
assert.equal(e.connectionId, null);
assert.ok(e.ts > 0);
assert.equal(e.status, 500);
});
test("outcomeFromStatus classifies statuses", () => {
assert.equal(outcomeFromStatus(200), "success");
assert.equal(outcomeFromStatus(201), "success");
assert.equal(outcomeFromStatus(429), "rate_limited");
assert.equal(outcomeFromStatus(408), "timeout");
assert.equal(outcomeFromStatus(504), "timeout");
assert.equal(outcomeFromStatus(500), "error");
assert.equal(outcomeFromStatus(400), "error");
assert.equal(outcomeFromStatus(null), "error");
assert.equal(outcomeFromStatus(undefined), "error");
});
test("MemoryRoutingEventStore returns newest-first within capacity", () => {
const store = new MemoryRoutingEventStore(5);
for (let i = 0; i < 5; i++) store.record(event({ requestId: `r-${i}` }));
const recent = store.recent(5);
assert.equal(recent.length, 5);
assert.equal(recent[0].requestId, "r-4");
assert.equal(recent[4].requestId, "r-0");
});
test("MemoryRoutingEventStore is bounded and still newest-first after overflow", () => {
const store = new MemoryRoutingEventStore(3);
for (let i = 0; i < 10; i++) store.record(event({ requestId: `r-${i}` }));
assert.equal(store.size, 3);
const recent = store.recent(3);
assert.deepEqual(
recent.map((e) => e.requestId),
["r-9", "r-8", "r-7"]
);
store.clear();
assert.equal(store.size, 0);
assert.deepEqual(store.recent(), []);
});
test("dispatchRoutingEvent fans out to every registered sink", () => {
const seen: string[] = [];
const sink: RoutingEventSink = {
name: "test-a",
record: (e) => void seen.push(e.requestId),
};
const unsub = registerRoutingEventSink(sink);
try {
dispatchRoutingEvent(event({ requestId: "fan-1" }));
dispatchRoutingEvent(event({ requestId: "fan-2" }));
assert.deepEqual(seen, ["fan-1", "fan-2"]);
} finally {
unsub();
}
});
test("dispatchRoutingEvent isolates a throwing sink", () => {
const badSink: RoutingEventSink = {
name: "test-throw",
record: () => {
throw new Error("boom");
},
};
const goodSeen: string[] = [];
const goodSink: RoutingEventSink = {
name: "test-good",
record: (e) => void goodSeen.push(e.requestId),
};
registerRoutingEventSink(badSink);
registerRoutingEventSink(goodSink);
try {
dispatchRoutingEvent(event({ requestId: "isolated" }));
assert.deepEqual(goodSeen, ["isolated"]);
} finally {
clearRoutingEventSinks();
}
});
test("listRoutingEventSinks reports registered names", () => {
clearRoutingEventSinks();
assert.deepEqual(listRoutingEventSinks(), []);
const unsub = registerRoutingEventSink({ name: "probe", record: () => {} });
try {
assert.deepEqual(listRoutingEventSinks(), ["probe"]);
} finally {
unsub();
}
});