Files
2026-07-13 13:39:12 +08:00

361 lines
13 KiB
TypeScript

/**
* tests/unit/ui/comboFlowModel.test.ts
*
* TDD for `comboFlowModel` — the pure reducer/model core of Tela B.
* Run: node --import tsx/esm --test tests/unit/ui/comboFlowModel.test.ts
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
classifyFailKind,
reduceComboEvent,
comboRunToFlow,
type ComboRunModel,
type ComboEventInput,
} from "../../../src/app/(dashboard)/dashboard/combos/live/comboFlowModel.ts";
import { FLOW_EDGE_COLORS } from "../../../src/shared/components/flow/edgeStyles.ts";
// ── helpers ───────────────────────────────────────────────────────────────
function mkAttempt(
targetIndex: number,
strategy = "priority",
provider = `prov${targetIndex}`,
model = `model${targetIndex}`
): ComboEventInput {
return {
comboName: "test-combo",
targetIndex,
provider,
model,
type: "attempt",
strategy,
timestamp: 1000 + targetIndex,
};
}
function mkFailed(
targetIndex: number,
error: string,
provider = `prov${targetIndex}`,
model = `model${targetIndex}`
): ComboEventInput {
return {
comboName: "test-combo",
targetIndex,
provider,
model,
type: "failed",
error,
latencyMs: 100,
timestamp: 2000 + targetIndex,
};
}
function mkSucceeded(
targetIndex: number,
provider = `prov${targetIndex}`,
model = `model${targetIndex}`
): ComboEventInput {
return {
comboName: "test-combo",
targetIndex,
provider,
model,
type: "succeeded",
latencyMs: 42,
timestamp: 3000 + targetIndex,
};
}
// Full 6-event cascade:
// attempt(0) → failed(0,"429 rate limited") → attempt(1) → failed(1,"circuit open") → attempt(2) → succeeded(2)
function buildFullRun(): ComboRunModel {
let run: ComboRunModel | null = null;
run = reduceComboEvent(run, mkAttempt(0));
run = reduceComboEvent(run, mkFailed(0, "429 rate limited"));
run = reduceComboEvent(run, mkAttempt(1));
run = reduceComboEvent(run, mkFailed(1, "circuit open"));
run = reduceComboEvent(run, mkAttempt(2));
run = reduceComboEvent(run, mkSucceeded(2));
return run;
}
// ── classifyFailKind ──────────────────────────────────────────────────────
describe("classifyFailKind", () => {
it("returns undefined when error is absent", () => {
assert.equal(classifyFailKind(undefined), undefined);
assert.equal(classifyFailKind(""), undefined);
});
it("returns 'rate-limit' for 429 messages", () => {
assert.equal(classifyFailKind("429 Too Many Requests"), "rate-limit");
assert.equal(classifyFailKind("upstream rate limit exceeded"), "rate-limit");
assert.equal(classifyFailKind("RATE_LIMIT_EXCEEDED"), "rate-limit");
});
it("returns 'circuit-open' for circuit breaker messages", () => {
assert.equal(classifyFailKind("circuit open"), "circuit-open");
assert.equal(classifyFailKind("Provider circuit breaker is open"), "circuit-open");
assert.equal(classifyFailKind("CIRCUIT_OPEN"), "circuit-open");
});
it("returns 'cooldown' for cooldown messages", () => {
assert.equal(classifyFailKind("connection cooldown active"), "cooldown");
assert.equal(classifyFailKind("cooldown period"), "cooldown");
});
it("returns 'other' for unrecognized error strings", () => {
assert.equal(classifyFailKind("Internal server error"), "other");
assert.equal(classifyFailKind("timeout"), "other");
assert.equal(classifyFailKind("some unknown failure"), "other");
});
it("circuit-open takes precedence over rate-limit when both match", () => {
// edge: a message with both circuit and rate — circuit wins by regex ordering
const result = classifyFailKind("circuit open after 429");
assert.equal(result, "circuit-open");
});
});
// ── reduceComboEvent ──────────────────────────────────────────────────────
describe("reduceComboEvent — basic events", () => {
it("creates a new run on first attempt event (null input)", () => {
const run = reduceComboEvent(null, mkAttempt(0));
assert.equal(run.comboName, "test-combo");
assert.equal(run.strategy, "priority");
assert.equal(run.outcome, "running");
assert.ok(run.startedAt > 0);
assert.equal(run.finishedAt, undefined);
assert.equal(run.targets.length, 1);
assert.equal(run.targets[0].targetIndex, 0);
assert.equal(run.targets[0].provider, "prov0");
assert.equal(run.targets[0].model, "model0");
assert.equal(run.targets[0].state, "attempting");
});
it("marks target as failed and sets failKind on failed event", () => {
let run = reduceComboEvent(null, mkAttempt(0));
run = reduceComboEvent(run, mkFailed(0, "429 rate limited"));
assert.equal(run.targets[0].state, "failed");
assert.equal(run.targets[0].failKind, "rate-limit");
assert.equal(run.targets[0].error, "429 rate limited");
assert.equal(run.outcome, "running"); // not done yet
});
it("marks target as succeeded and sets outcome + finishedAt on succeeded event", () => {
let run = reduceComboEvent(null, mkAttempt(0));
run = reduceComboEvent(run, mkSucceeded(0));
assert.equal(run.targets[0].state, "succeeded");
assert.equal(run.targets[0].latencyMs, 42);
assert.equal(run.outcome, "succeeded");
assert.ok(run.finishedAt != null);
});
});
describe("reduceComboEvent — full cascade (3 targets)", () => {
it("produces 3 targets ordered by targetIndex", () => {
const run = buildFullRun();
assert.equal(run.targets.length, 3);
assert.equal(run.targets[0].targetIndex, 0);
assert.equal(run.targets[1].targetIndex, 1);
assert.equal(run.targets[2].targetIndex, 2);
});
it("states are [failed, failed, succeeded]", () => {
const run = buildFullRun();
assert.equal(run.targets[0].state, "failed");
assert.equal(run.targets[1].state, "failed");
assert.equal(run.targets[2].state, "succeeded");
});
it("failKinds are [rate-limit, circuit-open, undefined]", () => {
const run = buildFullRun();
assert.equal(run.targets[0].failKind, "rate-limit");
assert.equal(run.targets[1].failKind, "circuit-open");
assert.equal(run.targets[2].failKind, undefined);
});
it("outcome is 'succeeded' and finishedAt is set", () => {
const run = buildFullRun();
assert.equal(run.outcome, "succeeded");
assert.ok(run.finishedAt != null);
});
it("strategy is set from the attempt payload", () => {
const run = buildFullRun();
assert.equal(run.strategy, "priority");
});
});
describe("reduceComboEvent — ordering", () => {
it("keeps targets sorted by targetIndex even if events arrive out of order", () => {
// Unusual but defensive: attempt(2) then attempt(0)
let run = reduceComboEvent(null, mkAttempt(2));
run = reduceComboEvent(run, mkAttempt(0));
assert.equal(run.targets[0].targetIndex, 0);
assert.equal(run.targets[1].targetIndex, 2);
});
it("idempotently applies a repeated attempt for the same target", () => {
let run = reduceComboEvent(null, mkAttempt(0));
run = reduceComboEvent(run, mkAttempt(0)); // duplicate
assert.equal(run.targets.length, 1);
assert.equal(run.targets[0].state, "attempting");
});
});
describe("reduceComboEvent — comboName key", () => {
it("ignores events for a different comboName when run already exists", () => {
let run = reduceComboEvent(null, mkAttempt(0));
// Event for a different combo — should be ignored, run returned unchanged
const alienEvent: ComboEventInput = {
comboName: "other-combo",
targetIndex: 99,
provider: "alien",
model: "alien",
type: "failed",
error: "some error",
timestamp: 9999,
};
run = reduceComboEvent(run, alienEvent);
assert.equal(run.targets.length, 1);
assert.equal(run.comboName, "test-combo");
});
});
// ── comboRunToFlow ────────────────────────────────────────────────────────
describe("comboRunToFlow", () => {
it("returns N+3 nodes for N targets (request + strategy + N targets + response)", () => {
const run = buildFullRun();
const { nodes } = comboRunToFlow(run);
// 3 targets → 1 request + 1 strategy + 3 targets + 1 response = 6 nodes
assert.equal(nodes.length, 6, `expected 6 nodes, got ${nodes.length}`);
});
it("node types: first=request, second=strategy, N middle=target, last=response", () => {
const run = buildFullRun();
const { nodes } = comboRunToFlow(run);
assert.equal(nodes[0].type, "request");
assert.equal(nodes[1].type, "strategy");
assert.equal(nodes[2].type, "target");
assert.equal(nodes[3].type, "target");
assert.equal(nodes[4].type, "target");
assert.equal(nodes[5].type, "response");
});
it("target nodes carry provider, model, state, failKind in data", () => {
const run = buildFullRun();
const { nodes } = comboRunToFlow(run);
const t0 = nodes[2];
assert.equal((t0.data as Record<string, unknown>).provider, "prov0");
assert.equal((t0.data as Record<string, unknown>).model, "model0");
assert.equal((t0.data as Record<string, unknown>).state, "failed");
assert.equal((t0.data as Record<string, unknown>).failKind, "rate-limit");
const t2 = nodes[4];
assert.equal((t2.data as Record<string, unknown>).state, "succeeded");
assert.equal((t2.data as Record<string, unknown>).failKind, undefined);
});
it("strategy node carries the strategy name in data", () => {
const run = buildFullRun();
const { nodes } = comboRunToFlow(run);
assert.equal((nodes[1].data as Record<string, unknown>).strategy, "priority");
assert.equal((nodes[1].data as Record<string, unknown>).targetCount, 3);
});
it("edges: N+3 sequential edges (one per node-to-node link)", () => {
const run = buildFullRun();
const { edges } = comboRunToFlow(run);
// 6 nodes → 5 edges: request→strategy, strategy→t0, t0→t1, t1→t2, t2→response
assert.equal(edges.length, 5, `expected 5 edges, got ${edges.length}`);
});
it("each edge connects consecutive nodes in order", () => {
const run = buildFullRun();
const { nodes, edges } = comboRunToFlow(run);
for (let i = 0; i < edges.length; i++) {
assert.equal(edges[i].source, nodes[i].id, `edge[${i}].source mismatch`);
assert.equal(edges[i].target, nodes[i + 1].id, `edge[${i}].target mismatch`);
}
});
it("failed edges are styled with error color", () => {
const run = buildFullRun();
const { edges } = comboRunToFlow(run);
// edge[2] is strategy→t0 (failed)… actually edges are: [0]req→strat, [1]strat→t0, [2]t0→t1, [3]t1→t2, [4]t2→resp
// edge for t0 (failed): index 1 (strategy→t0)
// The edge going INTO t0 should reflect t0 state
const t0IncomingEdge = edges[1]; // strategy→target0
const style = t0IncomingEdge.style as Record<string, unknown> | undefined;
assert.ok(style != null, "edge should have style");
assert.equal(style.stroke, FLOW_EDGE_COLORS.error, "failed target edge should be error color");
});
it("succeeded edge is styled with active/green color", () => {
const run = buildFullRun();
const { edges } = comboRunToFlow(run);
// edge[4] is t2→response (t2 succeeded)
const t2ResponseEdge = edges[4];
const style = t2ResponseEdge.style as Record<string, unknown> | undefined;
assert.ok(style != null);
assert.equal(
style.stroke,
FLOW_EDGE_COLORS.active,
"succeeded target edge should be active/green color"
);
});
it("idle/attempting edges are styled with idle or last-used color (not error/green)", () => {
// A run with one target still attempting
let run = reduceComboEvent(null, mkAttempt(0));
const { edges } = comboRunToFlow(run);
// edge[1] = strategy→t0 (attempting)
const attemptingEdge = edges[1];
const style = attemptingEdge.style as Record<string, unknown> | undefined;
assert.ok(style != null);
assert.notEqual(style.stroke, FLOW_EDGE_COLORS.error);
assert.notEqual(style.stroke, FLOW_EDGE_COLORS.active);
});
it("produces deterministic node IDs", () => {
const run = buildFullRun();
const { nodes } = comboRunToFlow(run);
assert.equal(nodes[0].id, "request");
assert.equal(nodes[1].id, "strategy");
assert.equal(nodes[2].id, "target-0");
assert.equal(nodes[3].id, "target-1");
assert.equal(nodes[4].id, "target-2");
assert.equal(nodes[5].id, "response");
});
});