mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 21:32:20 +03:00
Seven pointwise fixes on the Orchestration Canvas, each covered by a test: 1. Debounce x chip race: every chip/clear write in OrchestrationToolbar now cancels the pending search timer first. Left armed, it fired ~300ms later with a setParams closed over the pre-chip query string and silently reverted the chip. 2. The search input carries an aria-label (searchPlaceholder) — the placeholder alone is not an accessible name. 3. parseCsvSet trims each token, so `?state=running, failed` parses like the unpadded form instead of dropping the padded value. 4. toggleCsv was duplicated in the toolbar and the page client; both now import the single definition from the new model/urlParams.ts (pure, never mutates its inputs). 5. AgentsTab tells "nothing running" apart from "the filter matched nothing": with an active filter and no work node it renders noMatches + a clear-filters button instead of the setup CTAs, which would be wrong advice there. 6. Particle cap: orchestrationToFlow stamps `particles` on every edge and turns it off above PARTICLE_EDGE_CAP (40) simultaneously active edges — StatusEdge then renders the colored stroke without its 3 SMIL particles per edge. 7. The drawer's error banner clears when an action succeeds, so a recovered failure does not stay on screen. Only `noMatches` is added to en.json here; the other locales are task B4. Refs #12392
215 lines
8.9 KiB
TypeScript
215 lines
8.9 KiB
TypeScript
/** Run: node --import tsx/esm --test tests/unit/ui/orchestrationToFlow.test.ts */
|
|
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
PARTICLE_EDGE_CAP,
|
|
orchestrationToFlow,
|
|
} from "../../../src/app/(dashboard)/dashboard/orchestration/model/orchestrationToFlow.ts";
|
|
import type { OrchSnapshot } from "../../../src/app/(dashboard)/dashboard/orchestration/model/orchestrationTypes.ts";
|
|
|
|
const snap: OrchSnapshot = {
|
|
nodes: [
|
|
{ id: "orchestrator", kind: "orchestrator", label: "OmniRoute" },
|
|
{ id: "source:a2a", kind: "source", source: "a2a", label: "A2A" },
|
|
{ id: "a2a:t1", kind: "work", source: "a2a", state: "running", label: "smart-routing" },
|
|
{ id: "a2a:t2", kind: "work", source: "a2a", state: "failed", label: "cost-analysis" },
|
|
],
|
|
edges: [
|
|
{ id: "e1", from: "orchestrator", to: "source:a2a", kind: "owns", active: false },
|
|
{ id: "e2", from: "source:a2a", to: "a2a:t1", kind: "owns", active: true },
|
|
{ id: "e3", from: "source:a2a", to: "a2a:t2", kind: "owns", active: false },
|
|
],
|
|
sources: [],
|
|
generatedAt: "2026-08-30T12:00:00Z",
|
|
};
|
|
|
|
const multiSourceSnap: OrchSnapshot = {
|
|
nodes: [
|
|
{ id: "orchestrator", kind: "orchestrator", label: "OmniRoute" },
|
|
{ id: "source:a2a", kind: "source", source: "a2a", label: "A2A" },
|
|
{ id: "a2a:t1", kind: "work", source: "a2a", state: "running", label: "smart-routing" },
|
|
{
|
|
id: "a2a:t1:activity",
|
|
kind: "activity",
|
|
source: "a2a",
|
|
state: "running",
|
|
label: "thinking",
|
|
},
|
|
{ id: "source:cloud-agent", kind: "source", source: "cloud-agent", label: "Cloud Agent" },
|
|
{
|
|
id: "cloud-agent:t1",
|
|
kind: "work",
|
|
source: "cloud-agent",
|
|
state: "queued",
|
|
label: "build",
|
|
},
|
|
],
|
|
edges: [
|
|
{ id: "e1", from: "orchestrator", to: "source:a2a", kind: "owns", active: false },
|
|
{ id: "e2", from: "source:a2a", to: "a2a:t1", kind: "owns", active: true },
|
|
{ id: "e3", from: "a2a:t1", to: "a2a:t1:activity", kind: "owns", active: true },
|
|
{ id: "e4", from: "orchestrator", to: "source:cloud-agent", kind: "owns", active: false },
|
|
{ id: "e5", from: "source:cloud-agent", to: "cloud-agent:t1", kind: "owns", active: false },
|
|
],
|
|
sources: [],
|
|
generatedAt: "2026-08-30T12:00:00Z",
|
|
};
|
|
|
|
describe("orchestrationToFlow", () => {
|
|
it("puts each kind on its own Y layer and is deterministic", () => {
|
|
const a = orchestrationToFlow(snap);
|
|
const b = orchestrationToFlow(snap);
|
|
assert.deepEqual(
|
|
a.nodes.map((n) => n.position),
|
|
b.nodes.map((n) => n.position)
|
|
);
|
|
const ys = new Map(a.nodes.map((n) => [n.id, n.position.y]));
|
|
assert.equal(ys.get("orchestrator"), 0);
|
|
assert.equal(ys.get("source:a2a"), 150);
|
|
assert.equal(ys.get("a2a:t1"), 320);
|
|
});
|
|
it('edges carry type "status" and data.{state,active,mirror,particles}; no animated/style leak', () => {
|
|
const { edges } = orchestrationToFlow(snap);
|
|
const activeEdge = edges.find((e) => e.id === "e2");
|
|
assert.equal(activeEdge?.type, "status");
|
|
assert.deepEqual(activeEdge?.data, {
|
|
state: "running",
|
|
active: true,
|
|
mirror: false,
|
|
particles: true,
|
|
});
|
|
assert.equal((activeEdge as { animated?: boolean }).animated, undefined);
|
|
assert.equal((activeEdge as { style?: unknown }).style, undefined);
|
|
|
|
const edgeToFailed = edges.find((e) => e.id === "e3");
|
|
assert.equal(edgeToFailed?.type, "status");
|
|
assert.deepEqual(edgeToFailed?.data, {
|
|
state: "failed",
|
|
active: false,
|
|
mirror: false,
|
|
particles: true,
|
|
});
|
|
});
|
|
it("mirror edges carry data.mirror === true", () => {
|
|
const mirrorSnap: OrchSnapshot = {
|
|
...snap,
|
|
edges: [
|
|
...snap.edges,
|
|
{ id: "e4", from: "a2a:t1", to: "source:a2a", kind: "mirror", active: false },
|
|
],
|
|
};
|
|
const { edges } = orchestrationToFlow(mirrorSnap);
|
|
const mirrorEdge = edges.find((e) => e.id === "e4");
|
|
assert.equal(mirrorEdge?.type, "status");
|
|
assert.equal((mirrorEdge?.data as { mirror?: boolean })?.mirror, true);
|
|
const ownsEdge = edges.find((e) => e.id === "e2");
|
|
assert.equal((ownsEdge?.data as { mirror?: boolean })?.mirror, false);
|
|
});
|
|
it("fitKey only tracks the set of work ids", () => {
|
|
const k1 = orchestrationToFlow(snap).fitKey;
|
|
const stateChanged = {
|
|
...snap,
|
|
nodes: snap.nodes.map((n) => (n.id === "a2a:t1" ? { ...n, state: "succeeded" as const } : n)),
|
|
};
|
|
assert.equal(orchestrationToFlow(stateChanged).fitKey, k1);
|
|
const nodeRemoved = {
|
|
...snap,
|
|
nodes: snap.nodes.filter((n) => n.id !== "a2a:t2"),
|
|
edges: snap.edges.filter((e) => e.to !== "a2a:t2"),
|
|
};
|
|
assert.notEqual(orchestrationToFlow(nodeRemoved).fitKey, k1);
|
|
});
|
|
|
|
it("opts omitted preserves current behavior (all nodes/edges kept, no collapsed data)", () => {
|
|
const { nodes, edges, fitKey } = orchestrationToFlow(multiSourceSnap);
|
|
assert.equal(nodes.length, multiSourceSnap.nodes.length);
|
|
assert.equal(edges.length, multiSourceSnap.edges.length);
|
|
assert.ok(!fitKey.includes("::collapsed="));
|
|
const sourceA2a = nodes.find((n) => n.id === "source:a2a");
|
|
assert.equal((sourceA2a?.data as { collapsed?: boolean }).collapsed, undefined);
|
|
});
|
|
|
|
it("collapsing a source removes its work/activity nodes and their edges, keeps other sources", () => {
|
|
const { nodes, edges } = orchestrationToFlow(multiSourceSnap, {
|
|
collapsed: new Set(["a2a"]),
|
|
});
|
|
const ids = nodes.map((n) => n.id).sort();
|
|
assert.deepEqual(ids, ["cloud-agent:t1", "orchestrator", "source:a2a", "source:cloud-agent"]);
|
|
const edgeIds = edges.map((e) => e.id).sort();
|
|
assert.deepEqual(edgeIds, ["e1", "e4", "e5"]);
|
|
});
|
|
|
|
it("SourceNode for a collapsed source carries data.collapsed === true; others do not", () => {
|
|
const { nodes } = orchestrationToFlow(multiSourceSnap, { collapsed: new Set(["a2a"]) });
|
|
const sourceA2a = nodes.find((n) => n.id === "source:a2a");
|
|
const sourceCloudAgent = nodes.find((n) => n.id === "source:cloud-agent");
|
|
assert.equal((sourceA2a?.data as { collapsed?: boolean }).collapsed, true);
|
|
assert.equal((sourceCloudAgent?.data as { collapsed?: boolean }).collapsed, undefined);
|
|
});
|
|
|
|
it("fitKey changes when the collapsed set changes and is stable otherwise", () => {
|
|
const base = orchestrationToFlow(multiSourceSnap).fitKey;
|
|
const k1 = orchestrationToFlow(multiSourceSnap, { collapsed: new Set(["a2a"]) }).fitKey;
|
|
const k1Again = orchestrationToFlow(multiSourceSnap, { collapsed: new Set(["a2a"]) }).fitKey;
|
|
const k2 = orchestrationToFlow(multiSourceSnap, {
|
|
collapsed: new Set(["cloud-agent"]),
|
|
}).fitKey;
|
|
assert.equal(k1, k1Again);
|
|
assert.notEqual(k1, base);
|
|
assert.notEqual(k1, k2);
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Particle cap (task B3.6): above PARTICLE_EDGE_CAP simultaneously active edges the canvas
|
|
* would run 3 SMIL particles per edge, so `orchestrationToFlow` tells StatusEdge to render the
|
|
* plain stroke instead (`data.particles === false`).
|
|
*/
|
|
function busySnapshot(activeEdges: number): OrchSnapshot {
|
|
const nodes: OrchSnapshot["nodes"] = [
|
|
{ id: "orchestrator", kind: "orchestrator", label: "OmniRoute" },
|
|
{ id: "source:a2a", kind: "source", source: "a2a", label: "A2A" },
|
|
];
|
|
const edges: OrchSnapshot["edges"] = [];
|
|
for (let i = 0; i < activeEdges; i++) {
|
|
const id = `a2a:t${String(i).padStart(3, "0")}`;
|
|
nodes.push({ id, kind: "work", source: "a2a", state: "running", label: id });
|
|
edges.push({ id: `e${i}`, from: "source:a2a", to: id, kind: "owns", active: true });
|
|
}
|
|
return { nodes, edges, sources: [], generatedAt: "2026-09-07T00:00:00Z" };
|
|
}
|
|
|
|
describe("orchestrationToFlow — particle cap", () => {
|
|
it("PARTICLE_EDGE_CAP is 40", () => {
|
|
assert.equal(PARTICLE_EDGE_CAP, 40);
|
|
});
|
|
|
|
it("keeps particles on at exactly the cap", () => {
|
|
const { edges } = orchestrationToFlow(busySnapshot(PARTICLE_EDGE_CAP));
|
|
assert.equal(edges.length, PARTICLE_EDGE_CAP);
|
|
assert.ok(edges.every((e) => (e.data as { particles?: boolean }).particles === true));
|
|
});
|
|
|
|
it("turns particles off for every edge once the cap is exceeded", () => {
|
|
const { edges } = orchestrationToFlow(busySnapshot(PARTICLE_EDGE_CAP + 1));
|
|
assert.ok(edges.every((e) => (e.data as { particles?: boolean }).particles === false));
|
|
});
|
|
|
|
it("counts only ACTIVE edges — 41 idle edges stay under the cap", () => {
|
|
const snapWithIdle = busySnapshot(PARTICLE_EDGE_CAP + 1);
|
|
const allIdle: OrchSnapshot = {
|
|
...snapWithIdle,
|
|
edges: snapWithIdle.edges.map((e) => ({ ...e, active: false })),
|
|
};
|
|
const { edges } = orchestrationToFlow(allIdle);
|
|
assert.ok(edges.every((e) => (e.data as { particles?: boolean }).particles === true));
|
|
});
|
|
|
|
it("counts VISIBLE active edges only — collapsing the source drops it back under the cap", () => {
|
|
const { edges } = orchestrationToFlow(busySnapshot(PARTICLE_EDGE_CAP + 1), {
|
|
collapsed: new Set(["a2a"]),
|
|
});
|
|
assert.equal(edges.length, 0);
|
|
});
|
|
});
|