Files
OmniRoute/tests/unit/topology-request-lifecycle-emit.test.ts
danscMax 18a8da6df7 fix(dashboard): topology reflects connection health + clears finished requests (#7672)
The provider topology only lit nodes from live/recent traffic, so between
requests (and right after a restart) it went blank even though 50+ connections
were healthy — which reads as "lost providers". Two root causes:

1. Stuck-green latch: request.completed/request.failed are declared in the
   dashboard event map and consumed by useLiveRequests to drain the active-request
   set, but they were never emitted (only request.started was). A node's green
   "active" pulse therefore only cleared on a page reload, and accumulated over a
   session. Emit the terminal event from persistAttemptLogs — keyed by the same
   traceId as request.started — through a pure resolveRequestLifecycleEvent()
   helper (2xx/3xx + no error => completed, else failed).

2. No at-rest state: the map had nothing to show when idle. Colour each node by
   connection health (green connected / red error / grey idle) as a base layer,
   with live/recent traffic still taking precedence and pulsing brighter on top.
   edgeStyle() gains an optional trailing `healthy` param (static dim green) and
   StatusDot a `pulse` prop (static dot for connected-at-rest); both backward
   compatible. Legend "Active" -> "Connected".

Tests: resolveRequestLifecycleEvent success/failure/token-alias units, edgeStyle
healthy variant + precedence, and source guards for the emit wiring (traceId
threaded into persistAttemptLogs) and the health-colour wiring.

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouzapw@users.noreply.github.com>
2026-07-19 20:52:43 -03:00

100 lines
3.8 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { resolveRequestLifecycleEvent } from "../../open-sse/handlers/chatCore/attemptLogging.ts";
// The live topology lights a node green on `request.started` and only clears it when a
// matching `request.completed`/`request.failed` arrives. Those terminal events were
// declared + consumed by the client but never emitted, so a node stayed green until a
// page reload (the "stuck green" latch). These tests guard the fix that emits them.
test("resolveRequestLifecycleEvent: 2xx success → request.completed keyed by traceId", () => {
const ev = resolveRequestLifecycleEvent({
traceId: "abc123",
status: 200,
error: null,
model: "gpt-5.6-sol",
provider: "codex",
comboName: "my-combo",
tokens: { input: 10, output: 5 },
latencyMs: 1234,
});
assert.equal(ev.name, "request.completed");
assert.equal(ev.payload.id, "abc123");
if (ev.name !== "request.completed") return;
assert.equal(ev.payload.status, "success");
assert.equal(ev.payload.provider, "codex");
assert.equal(ev.payload.model, "gpt-5.6-sol");
assert.equal(ev.payload.tokensInput, 10);
assert.equal(ev.payload.tokensOutput, 5);
assert.equal(ev.payload.latencyMs, 1234);
assert.equal(ev.payload.comboName, "my-combo");
});
test("resolveRequestLifecycleEvent: 5xx → request.failed keyed by the same traceId", () => {
const ev = resolveRequestLifecycleEvent({
traceId: "e1",
status: 500,
error: "boom",
model: "m",
provider: "p",
latencyMs: 7,
});
assert.equal(ev.name, "request.failed");
if (ev.name !== "request.failed") return;
assert.equal(ev.payload.id, "e1");
assert.equal(ev.payload.error, "boom");
assert.equal(ev.payload.statusCode, 500);
assert.equal(ev.payload.latencyMs, 7);
});
test("resolveRequestLifecycleEvent: a 2xx status carrying an error string is still a failure", () => {
const ev = resolveRequestLifecycleEvent({ traceId: "x", status: 200, error: "late error", latencyMs: 1 });
assert.equal(ev.name, "request.failed");
});
test("resolveRequestLifecycleEvent: tokens resolve from prompt_tokens/completion_tokens aliases", () => {
const ev = resolveRequestLifecycleEvent({
traceId: "t",
status: 201,
tokens: { prompt_tokens: 3, completion_tokens: 8 },
latencyMs: 0,
});
assert.equal(ev.name, "request.completed");
if (ev.name !== "request.completed") return;
assert.equal(ev.payload.tokensInput, 3);
assert.equal(ev.payload.tokensOutput, 8);
});
test("resolveRequestLifecycleEvent: missing/odd tokens degrade to zero, never NaN", () => {
const ev = resolveRequestLifecycleEvent({ traceId: "z", status: 200, tokens: "nope", latencyMs: 2 });
assert.equal(ev.name, "request.completed");
if (ev.name !== "request.completed") return;
assert.equal(ev.payload.tokensInput, 0);
assert.equal(ev.payload.tokensOutput, 0);
});
test("attemptLogging emits both terminal events through the dashboard event bus", () => {
const src = readFileSync(
fileURLToPath(new URL("../../open-sse/handlers/chatCore/attemptLogging.ts", import.meta.url)),
"utf8"
);
assert.match(src, /emit\("request\.completed"/, "success attempts must emit request.completed");
assert.match(src, /emit\("request\.failed"/, "failed attempts must emit request.failed");
assert.match(src, /resolveRequestLifecycleEvent/, "emit must go through the pure resolver");
});
test("chatCore threads traceId into the persistAttemptLogs context (pairs with request.started)", () => {
const src = readFileSync(
fileURLToPath(new URL("../../open-sse/handlers/chatCore.ts", import.meta.url)),
"utf8"
);
assert.match(
src,
/persistAttemptLogsFor\(args,\s*\{\s*traceId/,
"the terminal event id must be the same traceId emitted in request.started"
);
});