213 lines
7.8 KiB
TypeScript
213 lines
7.8 KiB
TypeScript
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
compressionEventToModel,
|
|
compressionRunToFlow,
|
|
buildReplayFrames,
|
|
type CompressionRunModel,
|
|
} from "../../../src/app/(dashboard)/dashboard/compression/studio/compressionFlowModel.ts";
|
|
import type { CompressionCompletedPayload } from "../../../src/lib/events/types.ts";
|
|
|
|
// ── fixture ───────────────────────────────────────────────────────────────
|
|
|
|
const THREE_ENGINE_PAYLOAD: CompressionCompletedPayload = {
|
|
requestId: "abc123",
|
|
comboId: "my-combo",
|
|
mode: "stacked",
|
|
originalTokens: 1000,
|
|
compressedTokens: 600,
|
|
savingsPercent: 40,
|
|
engineBreakdown: [
|
|
{
|
|
engine: "lite",
|
|
originalTokens: 1000,
|
|
compressedTokens: 900,
|
|
savingsPercent: 10,
|
|
techniquesUsed: ["whitespace"],
|
|
durationMs: 2,
|
|
},
|
|
{
|
|
engine: "caveman",
|
|
originalTokens: 900,
|
|
compressedTokens: 750,
|
|
savingsPercent: 16.7,
|
|
techniquesUsed: ["filler", "dedup"],
|
|
durationMs: 5,
|
|
},
|
|
{
|
|
engine: "rtk",
|
|
originalTokens: 750,
|
|
compressedTokens: 600,
|
|
savingsPercent: 20,
|
|
techniquesUsed: ["tool-output-trim"],
|
|
rulesApplied: ["max-lines"],
|
|
durationMs: 8,
|
|
},
|
|
],
|
|
timestamp: 1718000000000,
|
|
};
|
|
|
|
// ── compressionEventToModel ───────────────────────────────────────────────
|
|
|
|
describe("compressionEventToModel", () => {
|
|
it("builds the model from a 3-engine payload", () => {
|
|
const model: CompressionRunModel = compressionEventToModel(THREE_ENGINE_PAYLOAD);
|
|
|
|
assert.equal(model.requestId, "abc123");
|
|
assert.equal(model.comboId, "my-combo");
|
|
assert.equal(model.mode, "stacked");
|
|
assert.equal(model.originalTokens, 1000);
|
|
assert.equal(model.compressedTokens, 600);
|
|
assert.equal(model.savingsPercent, 40);
|
|
assert.equal(model.steps.length, 3);
|
|
});
|
|
|
|
it("maps each engine step correctly", () => {
|
|
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
|
|
|
|
assert.equal(model.steps[0].engine, "lite");
|
|
assert.equal(model.steps[0].originalTokens, 1000);
|
|
assert.equal(model.steps[0].compressedTokens, 900);
|
|
assert.equal(model.steps[0].savingsPercent, 10);
|
|
assert.deepEqual(model.steps[0].techniquesUsed, ["whitespace"]);
|
|
assert.equal(model.steps[0].durationMs, 2);
|
|
|
|
assert.equal(model.steps[2].engine, "rtk");
|
|
assert.deepEqual(model.steps[2].rulesApplied, ["max-lines"]);
|
|
});
|
|
|
|
it("handles null comboId gracefully", () => {
|
|
const payload = { ...THREE_ENGINE_PAYLOAD, comboId: null };
|
|
const model = compressionEventToModel(payload);
|
|
assert.equal(model.comboId, null);
|
|
});
|
|
|
|
it("handles empty engineBreakdown", () => {
|
|
const payload = { ...THREE_ENGINE_PAYLOAD, engineBreakdown: [] };
|
|
const model = compressionEventToModel(payload);
|
|
assert.equal(model.steps.length, 0);
|
|
});
|
|
});
|
|
|
|
// ── compressionRunToFlow ──────────────────────────────────────────────────
|
|
|
|
describe("compressionRunToFlow", () => {
|
|
it("returns N+2 nodes for N engine steps (input + N + output)", () => {
|
|
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
|
|
const flow = compressionRunToFlow(model);
|
|
|
|
// 3 engine steps → 1 input + 3 engine + 1 output = 5 nodes
|
|
assert.equal(flow.nodes.length, 5, `expected 5 nodes, got ${flow.nodes.length}`);
|
|
});
|
|
|
|
it("node types: first=input, last=output, middle=engine", () => {
|
|
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
|
|
const flow = compressionRunToFlow(model);
|
|
|
|
assert.equal(flow.nodes[0].type, "input");
|
|
assert.equal(flow.nodes[flow.nodes.length - 1].type, "output");
|
|
assert.equal(flow.nodes[1].type, "engine");
|
|
assert.equal(flow.nodes[2].type, "engine");
|
|
assert.equal(flow.nodes[3].type, "engine");
|
|
});
|
|
|
|
it("edges connect nodes in sequence (N+1 edges for N+2 nodes)", () => {
|
|
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
|
|
const flow = compressionRunToFlow(model);
|
|
|
|
// 5 nodes → 4 sequential edges
|
|
assert.equal(flow.edges.length, 4, `expected 4 edges, got ${flow.edges.length}`);
|
|
});
|
|
|
|
it("each edge source/target connects consecutive nodes", () => {
|
|
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
|
|
const flow = compressionRunToFlow(model);
|
|
|
|
for (let i = 0; i < flow.edges.length; i++) {
|
|
assert.equal(
|
|
flow.edges[i].source,
|
|
flow.nodes[i].id,
|
|
`edge ${i} source should be node[${i}].id`
|
|
);
|
|
assert.equal(
|
|
flow.edges[i].target,
|
|
flow.nodes[i + 1].id,
|
|
`edge ${i} target should be node[${i + 1}].id`
|
|
);
|
|
}
|
|
});
|
|
|
|
it("engine nodes carry per-step data", () => {
|
|
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
|
|
const flow = compressionRunToFlow(model);
|
|
|
|
// nodes[1] is the first engine step (lite)
|
|
const liteNode = flow.nodes[1];
|
|
assert.equal((liteNode.data as Record<string, unknown>).engine, "lite");
|
|
assert.equal((liteNode.data as Record<string, unknown>).originalTokens, 1000);
|
|
assert.equal((liteNode.data as Record<string, unknown>).compressedTokens, 900);
|
|
assert.equal((liteNode.data as Record<string, unknown>).savingsPercent, 10);
|
|
});
|
|
|
|
it("handles zero-engine model (only input + output)", () => {
|
|
const payload = { ...THREE_ENGINE_PAYLOAD, engineBreakdown: [] };
|
|
const model = compressionEventToModel(payload);
|
|
const flow = compressionRunToFlow(model);
|
|
|
|
assert.equal(flow.nodes.length, 2);
|
|
assert.equal(flow.edges.length, 1);
|
|
assert.equal(flow.nodes[0].type, "input");
|
|
assert.equal(flow.nodes[1].type, "output");
|
|
});
|
|
});
|
|
|
|
// ── buildReplayFrames ─────────────────────────────────────────────────────
|
|
|
|
describe("buildReplayFrames", () => {
|
|
it("returns progressive snapshots (1 engine applied, 2 applied, ...)", () => {
|
|
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
|
|
const frames = buildReplayFrames(model);
|
|
|
|
// 3 engines → 3 frames
|
|
assert.equal(frames.length, 3, `expected 3 frames, got ${frames.length}`);
|
|
});
|
|
|
|
it("frame[0] has 1 step, frame[1] has 2 steps, frame[2] has 3 steps", () => {
|
|
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
|
|
const frames = buildReplayFrames(model);
|
|
|
|
assert.equal(frames[0].steps.length, 1);
|
|
assert.equal(frames[1].steps.length, 2);
|
|
assert.equal(frames[2].steps.length, 3);
|
|
});
|
|
|
|
it("frames are independent objects (mutations don't bleed)", () => {
|
|
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
|
|
const frames = buildReplayFrames(model);
|
|
|
|
// Mutating frame[0].steps should not affect frame[1]
|
|
frames[0].steps.push({ engine: "fake" } as (typeof frames)[0]["steps"][0]);
|
|
assert.equal(frames[1].steps.length, 2);
|
|
});
|
|
|
|
it("each frame carries the correct compressedTokens up to that step", () => {
|
|
const model = compressionEventToModel(THREE_ENGINE_PAYLOAD);
|
|
const frames = buildReplayFrames(model);
|
|
|
|
// Frame 0: after lite — compressedTokens = 900
|
|
assert.equal(frames[0].compressedTokens, 900);
|
|
// Frame 1: after caveman — compressedTokens = 750
|
|
assert.equal(frames[1].compressedTokens, 750);
|
|
// Frame 2: after rtk — compressedTokens = 600
|
|
assert.equal(frames[2].compressedTokens, 600);
|
|
});
|
|
|
|
it("returns empty array for model with no steps", () => {
|
|
const payload = { ...THREE_ENGINE_PAYLOAD, engineBreakdown: [] };
|
|
const model = compressionEventToModel(payload);
|
|
const frames = buildReplayFrames(model);
|
|
assert.equal(frames.length, 0);
|
|
});
|
|
});
|