test(compression): close F5.1 coverage gaps (replay reducer, live accumulator, StatusDot) (#4192)

Integrated into release/v3.8.29
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-18 16:32:36 -03:00
committed by GitHub
parent 4f3a113471
commit cd60eb20a9
5 changed files with 343 additions and 5 deletions

View File

@@ -27,28 +27,31 @@ export interface UseCompressionReplayReturn {
const BASE_STEP_MS = 400;
function stepMs(speed: ReplaySpeed): number {
/** Interval (ms) between frames for a given speed. Exported as a unit-test seam. */
export function stepMs(speed: ReplaySpeed): number {
return Math.round(BASE_STEP_MS / speed);
}
// ── Reducer ───────────────────────────────────────────────────────────────
// `ReplayState`, `ReplayAction`, `INITIAL_STATE` and `replayReducer` are exported
// purely as unit-test seams (the state machine is otherwise driven only via the hook).
interface ReplayState {
export interface ReplayState {
frameIndex: number; // -1 = not started
isPlaying: boolean;
speed: ReplaySpeed;
}
type ReplayAction =
export type ReplayAction =
| { type: "RESET" }
| { type: "PLAY"; frameIndex?: number }
| { type: "PAUSE" }
| { type: "TICK"; totalFrames: number }
| { type: "SET_SPEED"; speed: ReplaySpeed };
const INITIAL_STATE: ReplayState = { frameIndex: -1, isPlaying: false, speed: 1 };
export const INITIAL_STATE: ReplayState = { frameIndex: -1, isPlaying: false, speed: 1 };
function replayReducer(state: ReplayState, action: ReplayAction): ReplayState {
export function replayReducer(state: ReplayState, action: ReplayAction): ReplayState {
switch (action.type) {
case "RESET":
return { ...state, frameIndex: -1, isPlaying: false };
@@ -164,6 +167,10 @@ export function useCompressionReplay(
const handleSetSpeed = useCallback(
(s: ReplaySpeed) => {
// Update the cadence ref synchronously: startTick() below reads speedRef.current
// immediately, but the syncing effect only runs after render — too late for this
// in-flight restart. Without this, changing speed mid-play kept the old cadence.
speedRef.current = s;
dispatch({ type: "SET_SPEED", speed: s });
if (isPlaying) startTick();
},

View File

@@ -0,0 +1,89 @@
/**
* tests/unit/ui/compression-replay-reducer.test.ts
*
* F5.1 coverage gap (F3.2): the `useCompressionReplay` state machine was untested
* (only buildReplayFrames was covered). This pins the pure reducer + timing.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
replayReducer,
INITIAL_STATE,
stepMs,
type ReplayState,
} from "../../../src/app/(dashboard)/dashboard/compression/studio/useCompressionReplay.ts";
describe("replayReducer — compression replay state machine (F3.2)", () => {
it("starts idle: frameIndex -1, not playing, speed 1", () => {
assert.deepEqual(INITIAL_STATE, { frameIndex: -1, isPlaying: false, speed: 1 });
});
it("PLAY without frameIndex starts playing and keeps the current frame", () => {
const s = replayReducer({ frameIndex: 2, isPlaying: false, speed: 1 }, { type: "PLAY" });
assert.equal(s.isPlaying, true);
assert.equal(s.frameIndex, 2);
});
it("PLAY with frameIndex restarts from that frame (used for replay-from-start)", () => {
const s = replayReducer(INITIAL_STATE, { type: "PLAY", frameIndex: 0 });
assert.equal(s.isPlaying, true);
assert.equal(s.frameIndex, 0);
});
it("PAUSE stops playing but keeps the frame", () => {
const s = replayReducer({ frameIndex: 1, isPlaying: true, speed: 1 }, { type: "PAUSE" });
assert.equal(s.isPlaying, false);
assert.equal(s.frameIndex, 1);
});
it("TICK advances one frame while below the last", () => {
const start: ReplayState = { frameIndex: -1, isPlaying: true, speed: 1 };
const a = replayReducer(start, { type: "TICK", totalFrames: 3 });
assert.equal(a.frameIndex, 0);
assert.equal(a.isPlaying, true);
const b = replayReducer(a, { type: "TICK", totalFrames: 3 });
assert.equal(b.frameIndex, 1);
assert.equal(b.isPlaying, true);
});
it("TICK stops at the last frame and clears isPlaying (auto-stop at end)", () => {
const atSecondLast: ReplayState = { frameIndex: 1, isPlaying: true, speed: 1 };
const done = replayReducer(atSecondLast, { type: "TICK", totalFrames: 3 });
assert.equal(done.frameIndex, 2, "lands on the last frame index (totalFrames-1)");
assert.equal(done.isPlaying, false, "playback stops once the last frame is reached");
});
it("RESET returns to idle (-1, not playing) but preserves speed", () => {
const s = replayReducer({ frameIndex: 2, isPlaying: true, speed: 3 }, { type: "RESET" });
assert.equal(s.frameIndex, -1);
assert.equal(s.isPlaying, false);
assert.equal(s.speed, 3, "speed must survive a reset");
});
it("SET_SPEED changes speed without touching frame/play state", () => {
const s = replayReducer({ frameIndex: 1, isPlaying: true, speed: 1 }, {
type: "SET_SPEED",
speed: 3,
});
assert.equal(s.speed, 3);
assert.equal(s.frameIndex, 1);
assert.equal(s.isPlaying, true);
});
it("unknown action is a no-op (returns the same state reference)", () => {
const state: ReplayState = { frameIndex: 0, isPlaying: false, speed: 1 };
// @ts-expect-error — exercising the default branch with an invalid action
assert.equal(replayReducer(state, { type: "NOPE" }), state);
});
});
describe("stepMs — frame pacing by speed (F3.2)", () => {
it("maps speed to a shorter interval as speed rises", () => {
assert.equal(stepMs(1), 400);
assert.equal(stepMs(3), 133); // round(400/3)
assert.equal(stepMs(0.3), 1333); // round(400/0.3)
assert.ok(stepMs(3) < stepMs(1) && stepMs(1) < stepMs(0.3), "monotonic: faster = shorter");
});
});

View File

@@ -0,0 +1,98 @@
// @vitest-environment jsdom
/**
* F5.2 finding (HIGH): changing replay speed mid-play was silently ignored — `startTick`
* read `speedRef.current` synchronously but the ref was only synced in a post-render
* effect, so the new interval kept the OLD cadence. This drives the hook with fake timers
* to prove a mid-play speed change actually takes effect.
*/
import React, { act, useMemo, useEffect } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { useCompressionReplay } from "@/app/(dashboard)/dashboard/compression/studio/useCompressionReplay";
import { compressionEventToModel } from "@/app/(dashboard)/dashboard/compression/studio/compressionFlowModel";
import type {
CompressionCompletedPayload,
} from "@/lib/events/types";
// 3 engine steps → buildReplayFrames yields 3 frames (indices 0,1,2).
const PAYLOAD: CompressionCompletedPayload = {
requestId: "r1",
comboId: "c",
mode: "stacked",
originalTokens: 1000,
compressedTokens: 700,
savingsPercent: 30,
engineBreakdown: [
{ engine: "a", originalTokens: 1000, compressedTokens: 900, savingsPercent: 10, techniquesUsed: [], durationMs: 1 },
{ engine: "b", originalTokens: 900, compressedTokens: 800, savingsPercent: 11, techniquesUsed: [], durationMs: 1 },
{ engine: "c", originalTokens: 800, compressedTokens: 700, savingsPercent: 12, techniquesUsed: [], durationMs: 1 },
],
timestamp: 1718000000000,
};
let api: ReturnType<typeof useCompressionReplay> | null = null;
function Harness() {
// Memoize so the model identity is stable across re-renders (otherwise the
// model-change effect would RESET on every render).
const model = useMemo(() => compressionEventToModel(PAYLOAD), []);
const hook = useCompressionReplay(model);
// Capture in an effect (not during render) so we don't reassign a module-scoped
// variable mid-render. The effect runs after each commit, so `api` tracks the latest.
useEffect(() => {
api = hook;
});
return null;
}
const containers: HTMLElement[] = [];
function mount(): void {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(<Harness />);
});
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
while (containers.length > 0) containers.pop()?.remove();
document.body.innerHTML = "";
api = null;
});
describe("useCompressionReplay — mid-play speed change (F5.2)", () => {
it("applies a new speed to the running ticker (was silently ignored before the fix)", () => {
mount();
expect(api!.totalFrames).toBe(3);
// Play at speed 1 (interval 400ms): one tick advances to frame 0.
act(() => {
api!.play();
});
act(() => {
vi.advanceTimersByTime(400);
});
expect(api!.frameIndex).toBe(0);
// Speed up to 3 (interval ~133ms) WHILE playing, then advance only 133ms.
// With the fix the ticker restarts at the faster cadence and advances;
// without it the interval stayed at 400ms and 133ms would not tick.
act(() => {
api!.setSpeed(3);
});
act(() => {
vi.advanceTimersByTime(133);
});
expect(api!.frameIndex).toBe(1);
expect(api!.speed).toBe(3);
});
});

View File

@@ -0,0 +1,76 @@
/**
* tests/unit/ui/live-compression-accumulate.test.ts
*
* F5.1 coverage gap (F3.3): the `useLiveCompression` accumulator (`accumulateRun`)
* — which folds incoming `compression.completed` payloads into the run list — was
* exported "for unit tests" but never tested. This pins ordering + the cap.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { accumulateRun } from "../../../src/hooks/useLiveCompression.ts";
import type { CompressionRunModel } from "../../../src/app/(dashboard)/dashboard/compression/studio/compressionFlowModel.ts";
import type { CompressionCompletedPayload } from "../../../src/lib/events/types.ts";
function payload(requestId: string): CompressionCompletedPayload {
return {
requestId,
comboId: "my-combo",
mode: "stacked",
originalTokens: 1000,
compressedTokens: 600,
savingsPercent: 40,
engineBreakdown: [
{
engine: "rtk",
originalTokens: 1000,
compressedTokens: 600,
savingsPercent: 40,
techniquesUsed: ["tool-output-trim"],
durationMs: 3,
},
],
timestamp: 1718000000000,
};
}
describe("accumulateRun — live compression run accumulator (F3.3)", () => {
it("adds the first run derived from the payload (requestId carried through)", () => {
const runs = accumulateRun([], payload("r1"));
assert.equal(runs.length, 1);
assert.equal(runs[0].requestId, "r1");
assert.equal(runs[0].mode, "stacked");
});
it("prepends so the list is most-recent-first", () => {
let runs: CompressionRunModel[] = [];
runs = accumulateRun(runs, payload("r1"));
runs = accumulateRun(runs, payload("r2"));
runs = accumulateRun(runs, payload("r3"));
assert.deepEqual(
runs.map((r) => r.requestId),
["r3", "r2", "r1"],
);
});
it("caps the list at maxRuns, dropping the oldest", () => {
let runs: CompressionRunModel[] = [];
for (const id of ["a", "b", "c", "d"]) {
runs = accumulateRun(runs, payload(id), /* maxRuns */ 2);
}
assert.equal(runs.length, 2, "never grows beyond maxRuns");
assert.deepEqual(
runs.map((r) => r.requestId),
["d", "c"],
"keeps the 2 newest, newest-first; oldest (a, b) dropped",
);
});
it("does not mutate the previous array (returns a new list)", () => {
const prev = accumulateRun([], payload("r1"));
const next = accumulateRun(prev, payload("r2"));
assert.equal(prev.length, 1, "input array is left untouched");
assert.notEqual(prev, next);
});
});

View File

@@ -0,0 +1,68 @@
// @vitest-environment jsdom
/**
* F5.1 coverage gap (F0.2): the StatusDot U0 sub-component had no dedicated test.
* Pins the error-override + sizeClass behavior.
*/
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { StatusDot } from "@/shared/components/flow/StatusDot";
import { FLOW_EDGE_COLORS } from "@/shared/components/flow/edgeStyles";
const containers: HTMLElement[] = [];
function mount(ui: React.ReactElement): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
containers.push(container);
const root = createRoot(container);
act(() => {
root.render(ui);
});
return container;
}
/** Normalize a CSS color the way jsdom does (hex → rgb(...)) for stable comparison. */
function normalizeColor(c: string): string {
const probe = document.createElement("span");
probe.style.backgroundColor = c;
return probe.style.backgroundColor;
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (containers.length > 0) containers.pop()?.remove();
document.body.innerHTML = "";
});
describe("StatusDot (U0 — pulsing presence indicator)", () => {
it("uses the provided color when not errored", () => {
const container = mount(<StatusDot color="rgb(1, 2, 3)" />);
const dots = container.querySelectorAll<HTMLElement>("span[style]");
expect(dots.length).toBe(2); // ping halo + solid dot
for (const d of dots) expect(d.style.backgroundColor).toBe("rgb(1, 2, 3)");
});
it("overrides color with the error color when error=true", () => {
const container = mount(<StatusDot color="rgb(1, 2, 3)" error />);
const dots = container.querySelectorAll<HTMLElement>("span[style]");
const expected = normalizeColor(FLOW_EDGE_COLORS.error);
for (const d of dots) {
expect(d.style.backgroundColor).toBe(expected);
expect(d.style.backgroundColor).not.toBe("rgb(1, 2, 3)");
}
});
it("applies the sizeClass to the wrapper (defaults to size-1.5)", () => {
const dflt = mount(<StatusDot color="rgb(1, 2, 3)" />);
expect(dflt.querySelector(".size-1\\.5")).toBeTruthy();
const custom = mount(<StatusDot color="rgb(1, 2, 3)" sizeClass="size-3" />);
expect(custom.querySelector(".size-3")).toBeTruthy();
});
});