Files
AgentHub/client/src/components/__tests__/EventReplay.test.tsx
T
nntrivi2001 4aae61297f feat: analytics parity — tool cost, date ranges, budgets, query explorer, replay, snapshots (v2.4.0)
Analytics parity phases 2–7 (docs/plans/analytics-parity/), built on
138e706 and ported onto v2.3.1:
- Cost by AI tool (?tool= on cost/analytics/stats/workflows, by_tool,
  Codex fast/priority and flex tiers, agenthub_cost_usd_total{tool,model})
- Date ranges (from/to) on cost/analytics/workflows; Analytics range select
  with Today / This week (Monday) / This month (1st) / 7d / 30d / 90d / All /
  Custom; workflow filters carried into workflow-stats.js (read worker)
- Budgets and cost alerts as alert rule types (alert_rules rebuilt without
  its rule_type CHECK), GET /api/budgets/status, rule preview, spend pace,
  status-bar budget chip; rules stay editable in place
- Query Explorer (/query, allowlisted sort, CSV/JSON export) and scheduled
  reports (report_definitions/report_runs, Settings → Reports)
- Session Replay tab and immutable snapshots (session_snapshots, /snapshot/:token)
- Remote Codex sync, push ingest with a hashed token, privacy redaction and
  ignored folders (Settings → Data), Cursor sessions (no cost)
- Read-only MCP server, statusline, Grafana/Prometheus files

Also: resizable sidebar, pet run/wave animations, the workspace home keeps a
half-typed prompt, and the pre-commit hook runs only the related tests for a
small commit.
2026-10-06 17:12:28 +07:00

94 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file EventReplay.test.tsx
* @description The Replay tab's controls: step, jump, arrow keys, a lifecycle
* marker jumping the cursor, playback advancing on a timer, and the derived
* state following the cursor.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { describe, it, expect, vi, afterEach } from "vitest";
import { act, fireEvent, render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { EventReplay } from "../EventReplay";
import type { DashboardEvent } from "../../lib/types";
function ev(id: number, event_type: string, tool_name: string | null = null): DashboardEvent {
return {
id,
session_id: "s1",
agent_id: "a1",
event_type,
tool_name,
summary: `${event_type} ${tool_name ?? ""}`.trim(),
data: null,
created_at: new Date(Date.UTC(2026, 9, 1, 10, 0, id)).toISOString(),
} as DashboardEvent;
}
// Given newest first, as the events API answers: the replay orders them itself.
const events = [
ev(4, "Stop"),
ev(3, "PostToolUse", "Bash"),
ev(2, "PreToolUse", "Bash"),
ev(1, "SessionStart"),
];
function renderReplay() {
return render(
<MemoryRouter>
<EventReplay events={events} agentInfoById={new Map()} sessionNameById={new Map()} />
</MemoryRouter>
);
}
const position = () => screen.getByText(/^\d+ \/ 4/);
afterEach(() => vi.useRealTimers());
describe("EventReplay", () => {
it("starts at the first event and steps with the buttons", () => {
renderReplay();
expect(position().textContent).toMatch(/^1 \/ 4/);
fireEvent.click(screen.getByTitle("Step forward (→)"));
fireEvent.click(screen.getByTitle("Step forward (→)"));
expect(position().textContent).toMatch(/^3 \/ 4/);
expect(screen.getByText("Bash ×1")).toBeTruthy();
fireEvent.click(screen.getByTitle("Jump to end (End)"));
expect(position().textContent).toMatch(/^4 \/ 4/);
fireEvent.click(screen.getByTitle("Jump to start (Home)"));
expect(position().textContent).toMatch(/^1 \/ 4/);
});
it("moves on arrow keys and jumps from a lifecycle marker", () => {
renderReplay();
const root = screen.getByTestId("event-replay");
fireEvent.keyDown(root, { key: "ArrowRight" });
expect(position().textContent).toMatch(/^2 \/ 4/);
// The in-flight tool at the PreToolUse.
expect(screen.getByText("Running tool").nextSibling?.textContent).toBe("Bash");
fireEvent.keyDown(root, { key: "End" });
expect(position().textContent).toMatch(/^4 \/ 4/);
fireEvent.click(screen.getByRole("button", { name: "Session start · #1" }));
expect(position().textContent).toMatch(/^1 \/ 4/);
});
it("plays to the end at the chosen speed", () => {
vi.useFakeTimers();
renderReplay();
fireEvent.click(screen.getByRole("button", { name: "5×" }));
fireEvent.click(screen.getByTitle("Play (Space)"));
act(() => {
vi.advanceTimersByTime(100);
});
expect(position().textContent).toMatch(/^2 \/ 4/);
// One step per tick: each step schedules the next.
for (let i = 0; i < 3; i++) {
act(() => {
vi.advanceTimersByTime(100);
});
}
expect(position().textContent).toMatch(/^4 \/ 4/);
expect(screen.getByTitle("Play (Space)")).toBeTruthy();
});
});