Files
kandev/apps/web/e2e/tests/office/agent-dashboard.spec.ts
T
2026-06-18 18:36:48 +01:00

181 lines
6.7 KiB
TypeScript

import { test, expect } from "../../fixtures/office-fixture";
/**
* E2E coverage for the new agent dashboard sub-page
* (`/office/agents/[id]/dashboard`). Seeds a small fixture of runs +
* activity + cost events, then walks the rendered charts, recent
* tasks list, and costs section.
*
* The page is server-rendered: one navigation produces the full HTML
* including the chart segment values, so the assertions don't need
* to wait for client-side hydration to inspect the data.
*/
test.describe("Agent dashboard", () => {
test("renders chart values, latest run, recent tasks, and costs", async ({
testPage,
apiClient,
officeApi,
officeSeed,
}) => {
// Seed three tasks the agent has touched.
const task1 = await apiClient.createTask(officeSeed.workspaceId, "Dashboard Task A", {
workflow_id: officeSeed.workflowId,
});
const task2 = await apiClient.createTask(officeSeed.workspaceId, "Dashboard Task B", {
workflow_id: officeSeed.workflowId,
});
const now = Date.now();
const isoMinusDays = (n: number, hours = 0) =>
new Date(now - n * 24 * 3600_000 - hours * 3600_000).toISOString();
// Seed runs across 5 days: 3 succeeded + 1 failed.
await apiClient.seedRun({
agentProfileId: officeSeed.agentId,
status: "finished",
taskId: task1.id,
requestedAt: isoMinusDays(0, 1),
finishedAt: isoMinusDays(0, 0),
});
await apiClient.seedRun({
agentProfileId: officeSeed.agentId,
status: "finished",
taskId: task1.id,
requestedAt: isoMinusDays(1, 1),
finishedAt: isoMinusDays(1, 0),
});
await apiClient.seedRun({
agentProfileId: officeSeed.agentId,
status: "finished",
taskId: task2.id,
requestedAt: isoMinusDays(3, 1),
finishedAt: isoMinusDays(3, 0),
});
const failedRun = await apiClient.seedRun({
agentProfileId: officeSeed.agentId,
status: "failed",
taskId: task2.id,
errorMessage: "boom",
requestedAt: isoMinusDays(4, 1),
finishedAt: isoMinusDays(4, 0),
});
// Most-recent activity rows for the recent-tasks list. Must use
// the agent id as actor_id so the dashboard's WHERE clause picks
// them up. The dashboard service treats both target_type='task'
// rows as activity for that task.
await apiClient.seedActivity({
workspaceId: officeSeed.workspaceId,
actorType: "agent",
actorId: officeSeed.agentId,
action: "task.update",
targetType: "task",
targetId: task1.id,
createdAt: isoMinusDays(0, 1),
});
await apiClient.seedActivity({
workspaceId: officeSeed.workspaceId,
actorType: "agent",
actorId: officeSeed.agentId,
action: "task.update",
targetType: "task",
targetId: task2.id,
createdAt: isoMinusDays(0, 2),
});
// Cost events for the costs section.
await apiClient.seedCostEvent({
agentProfileId: officeSeed.agentId,
taskId: task1.id,
tokensIn: 1000,
tokensOut: 500,
tokensCachedIn: 200,
costSubcents: 1200,
occurredAt: isoMinusDays(0, 0),
});
await apiClient.seedCostEvent({
agentProfileId: officeSeed.agentId,
taskId: task1.id,
tokensIn: 200,
tokensOut: 100,
tokensCachedIn: 50,
costSubcents: 300,
occurredAt: isoMinusDays(0, 0),
});
await testPage.goto(`/office/agents/${officeSeed.agentId}/dashboard`);
// Latest run card carries the most recent run's status. The most
// recent seeded row by requested_at is the day-0 finished run.
const latestCard = testPage.getByTestId("latest-run-card");
await expect(latestCard).toBeVisible({ timeout: 10_000 });
await expect(testPage.getByTestId("latest-run-status")).toHaveText("finished");
// Run activity chart has the right number of bars (14-day window)
// and shows the seeded counts. We don't pin specific dates in the
// bars because the test runs in real time; instead we sum every
// segment's data-segment-value.
const runActivityCard = testPage.getByTestId("run-activity-card");
const runBars = runActivityCard.getByTestId("stacked-bar");
await expect(runBars).toHaveCount(14);
const succeededTotal = await runActivityCard
.locator('[data-segment-key="succeeded"]')
.evaluateAll((nodes) =>
nodes.reduce((sum, n) => sum + Number(n.getAttribute("data-segment-value") ?? "0"), 0),
);
const failedTotal = await runActivityCard
.locator('[data-segment-key="failed"]')
.evaluateAll((nodes) =>
nodes.reduce((sum, n) => sum + Number(n.getAttribute("data-segment-value") ?? "0"), 0),
);
expect(succeededTotal).toBe(3);
expect(failedTotal).toBe(1);
// Recent tasks lists both seeded tasks. Order: most-recent
// last_active_at first (task1 by 1h vs task2 by 2h).
const recentRows = testPage.getByTestId("recent-task-row");
await expect(recentRows).toHaveCount(2);
await expect(recentRows.first()).toHaveAttribute("data-task-id", task1.id);
// Costs aggregate sums match (1200 / 600 / 250 / 15c).
await expect(testPage.getByTestId("agg-input")).toHaveText("1.2k");
await expect(testPage.getByTestId("agg-output")).toHaveText("600");
await expect(testPage.getByTestId("agg-cached")).toHaveText("250");
await expect(testPage.getByTestId("agg-total-cost")).toHaveText("$0.15");
// Defensive: fixture-bound API check that the failed run id is
// among the returned summary so future changes that drop runs
// from the cost rollup still flag here.
void failedRun;
void officeApi;
});
test("dashboard renders chart primitives and latest-run card after navigation", async ({
testPage,
apiClient,
officeSeed,
}) => {
const task = await apiClient.createTask(officeSeed.workspaceId, "SSR Probe Task", {
workflow_id: officeSeed.workflowId,
});
await apiClient.seedRun({
agentProfileId: officeSeed.agentId,
status: "finished",
taskId: task.id,
requestedAt: new Date(Date.now() - 3600_000).toISOString(),
finishedAt: new Date().toISOString(),
});
// The route loader pre-fetches the summary before rendering the
// dashboard view. The view receives the snapshot synchronously into
// useState, so the chart primitives and latest-run card are present in
// the rendered DOM with no waiting on a client-side fetch.
await testPage.goto(`/office/agents/${officeSeed.agentId}/dashboard`);
await expect(testPage.getByTestId("latest-run-card")).toBeVisible({ timeout: 15_000 });
await expect(testPage.getByTestId("stacked-bar").first()).toBeVisible({ timeout: 15_000 });
await expect(testPage.locator('[data-segment-key="succeeded"]').first()).toBeAttached();
});
});