Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions apps/web/e2e/agents-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2462,8 +2462,9 @@ test("presents Dashboard page-chain results and System boundaries without extra
await page.getByRole("button", { name: "Dashboard", exact: true }).click();

await dashboard.getByRole("table", { name: "Recent Sessions" }).getByRole("button", { name: "Lifecycle Agent" }).click();
await expect(page.locator(".session-page")).toBeVisible();
await expect(page.getByText("Lifecycle Agent", { exact: true }).first()).toBeVisible();
const sessionPage = page.locator(".session-page");
await expect(sessionPage).toBeVisible();
await expect(sessionPage.getByText("Lifecycle Agent", { exact: true }).first()).toBeVisible();

await page.getByRole("button", { name: "System", exact: true }).click();
const system = page.locator(".system-page");
Expand Down Expand Up @@ -2680,7 +2681,7 @@ test("renders Runtime telemetry as visual snapshot panels with details on demand
await expect(dashboard.locator(".dashboard-runtime-sample-count")).toContainText("1 sample ·");
await expect(dashboard.getByRole("heading", { name: "CPU usage" })).toBeVisible();
await expect(dashboard.getByRole("heading", { name: "Memory usage" })).toBeVisible();
await expect(dashboard.getByRole("heading", { name: "Compute uptime" })).toBeVisible();
await expect(dashboard.getByRole("heading", { name: "Active Sandboxes" })).toBeVisible();
await expect(dashboard.getByRole("heading", { name: "Token throughput" })).toBeVisible();
await expect(dashboard.getByLabel("Live Runtime sampling every 30 seconds")).toBeVisible();
const liveRange = dashboard.getByRole("group", { name: "Runtime live range" });
Expand All @@ -2692,7 +2693,7 @@ test("renders Runtime telemetry as visual snapshot panels with details on demand
await refresh.click();
await expect(dashboard.getByLabel("CPU usage: 3 live samples")).toBeVisible();
await expect(dashboard.getByLabel("Memory usage: 3 live samples")).toBeVisible();
await expect(dashboard.getByLabel("Compute uptime: 3 live samples")).toBeVisible();
await expect(dashboard.getByLabel("Active Sandboxes: 3 live samples")).toBeVisible();
await expect(dashboard.getByLabel("Token throughput: 3 live samples")).toBeVisible();
await expect(dashboard.locator(".dashboard-runtime-sample-count")).toContainText("3 samples");
await expect(dashboard.getByText("CPU usage live trend available")).toBeAttached();
Expand Down Expand Up @@ -2728,7 +2729,7 @@ test("renders Runtime telemetry as visual snapshot panels with details on demand
await expect(cpuCard.locator(".dashboard-runtime-trend-tooltip")).toBeVisible();
await cpuChart.click({ position: { x: 260, y: 90 } });
await expect(cpuCard.locator(".dashboard-runtime-trend-tooltip")).toContainText("Pinned");
await expect(cpuCard.locator(".dashboard-runtime-trend-tooltip")).toContainText("Unavailable");
await expect(cpuCard.locator(".dashboard-runtime-trend-tooltip")).not.toContainText("Unavailable");
await cpuChart.focus();
await cpuChart.press("ArrowRight");
await expect(cpuCard.locator(".dashboard-runtime-trend-tooltip")).toContainText("Pinned");
Expand Down Expand Up @@ -2760,7 +2761,7 @@ test("renders Runtime telemetry as visual snapshot panels with details on demand
const keyboardSelectedAt = Number(await cpuChart.getAttribute("data-selected-at"));
expect(keyboardSelectedAt).toBeGreaterThanOrEqual(zoomedViewStart);
expect(keyboardSelectedAt).toBeLessThanOrEqual(zoomedViewEnd);
for (const chartName of ["Memory usage", "Compute uptime", "Token throughput"]) {
for (const chartName of ["Memory usage", "Active Sandboxes", "Token throughput"]) {
await expect(dashboard.getByLabel(`${chartName}: 3 live samples`)).toHaveAttribute("data-view-start", String(initialViewStart));
await expect(dashboard.getByLabel(`${chartName}: 3 live samples`)).toHaveAttribute("data-view-end", String(initialViewEnd));
}
Expand Down Expand Up @@ -2910,19 +2911,20 @@ test("restores retained Runtime history after a Dashboard reload", async ({ page
await expect(dashboard.getByLabel(/Durable · 30s; 1 Runtime targets/)).toBeVisible();
await expect(dashboard.getByLabel("Runtime durable-history charts")).toBeVisible();
await expect(dashboard.getByRole("heading", { name: "Compute uptime", exact: true })).toHaveCount(0);
await expect(dashboard.locator('[data-chart-engine="uplot"]')).toHaveCount(3);
await expect(dashboard.locator('[data-chart-engine="uplot"]')).toHaveCount(4);
await expect(dashboard.getByText("CPU usage durable trend available")).toBeAttached();
await expect(dashboard).toContainText("120 buckets");
await expect(dashboard).toContainText("119/120 observations");
await expect(dashboard.getByText("Active Sandboxes durable trend available")).toBeAttached();
await expect(dashboard.getByText("Token throughput durable trend available")).toBeAttached();
const durableCpuChart = dashboard.getByLabel("CPU usage: 120 retained buckets");
await expect(dashboard.getByRole("region", { name: "CPU usage durable history chart" })).toBeVisible();
const durableCpuCard = durableCpuChart.locator("xpath=ancestor::section[contains(@class, 'dashboard-runtime-trend-card')]");
await durableCpuChart.focus();
await durableCpuChart.press("ArrowLeft");
await expect(durableCpuCard.locator(".dashboard-runtime-trend-tooltip")).toContainText("Unavailable");
await expect(durableCpuCard.locator(".dashboard-runtime-trend-tooltip")).toContainText("0%");
await durableCpuChart.press("ArrowLeft");
await expect(durableCpuCard.locator(".dashboard-runtime-trend-tooltip")).toContainText("Unavailable");
await expect(durableCpuCard.locator(".dashboard-runtime-trend-tooltip")).toContainText("0%");
const durableMemoryCard = dashboard.getByRole("region", { name: "Memory usage durable history chart" });
const durableMemorySpan = await durableMemoryCard.locator("canvas").evaluate((canvas: HTMLCanvasElement) => {
const context = canvas.getContext("2d");
Expand Down Expand Up @@ -3037,18 +3039,16 @@ test("publishes Dashboard counts only after every top-level Agent and Session pa
await expect(dashboard.locator(".dashboard-source-badge").filter({ hasText: "Runtime" })).toContainText("Unavailable");
expect(sessionAfters).toEqual([null, "session_snapshot", null, "session_snapshot"]);
await page.getByRole("button", { name: "Sessions", exact: true }).click();
await expect.poll(() => sessionAfters.length).toBe(6);
await expect.poll(() => sessionAfters.length).toBe(4);
await page.getByRole("button", { name: "Dashboard", exact: true }).click();
await expect(dashboard.locator(".dashboard-summary > div").filter({ hasText: "Agents" })).toContainText("3");
await expect(dashboard.locator(".dashboard-summary > div").filter({ hasText: "Sessions" })).toContainText("2");
expect(agentAfters).toEqual([null, "agent_b"]);
// Session collection loads once; the unavailable Runtime snapshot is retried
// on entry to Sessions and again on return to Dashboard. Each reads both pages.
// The cached Dashboard and shared Session collection stay mounted across
// navigation, so no extra page-chain read occurs on either transition.
await expect.poll(() => sessionAfters).toEqual([
null, "session_snapshot",
null, "session_snapshot",
null, "session_snapshot",
null, "session_snapshot",
]);
});

Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/features/dashboard/DashboardView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,8 @@ describe("Dashboard loaded-result presentation", () => {
expect(html).toContain('aria-pressed="true">1h</button>');
expect(html).toContain("CPU usage");
expect(html).toContain("Memory usage");
expect(html).toContain("Compute uptime");
expect(html).not.toContain("Compute uptime");
expect(html).toContain("Active Sandboxes");
expect(html).toContain("Token throughput");
expect(html).not.toContain("No retained CPU samples");
expect(html).toContain("Latest value");
Expand Down
34 changes: 18 additions & 16 deletions apps/web/src/features/dashboard/RuntimeTrendCharts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { RuntimeTrendSample } from "./runtime-trends";
function sample(sampledAt: number, cpuRatio: number | null): RuntimeTrendSample {
return {
sampledAt,
activeSandboxCount: 1,
targets: [{
seriesId: "session-1:allocation-1",
label: "Runtime worker",
Expand Down Expand Up @@ -38,23 +39,32 @@ describe("Runtime live-window chart accessibility", () => {
allSeriesHidden: true,
validPoints: 0,
sampleCount: 24,
emptyMessage: "No complete retained memory samples",
emptyMessage: "No retained observed memory samples",
})).toBe("Memory usage all series hidden; use the legend to show a series");
});

it("renders an unavailable current value as zero without retaining a stale value", () => {
const unavailable = {
...sample(120_000, null),
activeSandboxCount: 0,
memoryUsageBytes: null,
memoryLimitBytes: null,
} satisfies RuntimeTrendSample;
const html = renderToStaticMarkup(
<RuntimeTrendCharts samples={[sample(60_000, .5), sample(120_000, null)]} />,
<RuntimeTrendCharts samples={[sample(60_000, .5), unavailable]} />,
);

expect(html).toContain("Runtime worker</th><td>0%</td><td>0</td>");
expect(html).not.toContain("Runtime worker</th><td>50%</td><td>1</td>");
expect(html).toContain("used</th><td>0 B</td><td>0</td>");
expect(html).toContain("active</th><td>0</td><td>0</td>");
});

it("renders empty retained buckets as continuous zero-value chart series", () => {
const empty = (sampledAt: number): RuntimeTrendSample => ({
...sample(sampledAt, null),
targets: [],
activeSandboxCount: 0,
memoryUsageBytes: null,
memoryLimitBytes: null,
inputTokensPerMinute: null,
Expand All @@ -66,6 +76,7 @@ describe("Runtime live-window chart accessibility", () => {

expect(html).toContain("usage</th><td>0%</td><td>0</td>");
expect(html).toContain("used</th><td>0 B</td><td>0</td>");
expect(html).toContain("active</th><td>0</td><td>0</td>");
expect(html).toContain("input</th><td>0/min</td><td>0</td>");
expect(html).not.toContain("No retained CPU samples");
expect(html).not.toContain("No complete retained memory samples");
Expand All @@ -79,7 +90,7 @@ describe("Runtime live-window chart accessibility", () => {

expect(html.match(/data-chart-engine="uplot"/g)).toHaveLength(4);
expect(html).not.toContain("Collecting live samples");
expect(html).toContain("Compute uptime");
expect(html).toContain("Active Sandboxes");
});

it("exposes interactive series, point selection, and Grafana-style in-plot range selection", () => {
Expand All @@ -103,29 +114,20 @@ describe("Runtime live-window chart accessibility", () => {
);

expect(html).toContain('aria-label="CPU usage durable history chart"');
expect(html.match(/data-chart-engine="uplot"/g)).toHaveLength(3);
expect(html.match(/data-chart-engine="uplot"/g)).toHaveLength(4);
expect(html).not.toContain("Compute uptime");
expect(html).toContain('aria-label="CPU usage: 2 retained buckets"');
expect(html).toContain('aria-label="Active Sandboxes durable history chart"');
expect(html).toContain("active</th><td>1</td><td>0</td>");
expect(html).not.toContain('aria-label="CPU usage: 2 live samples"');
});

it("can retain an honest uptime card when a consumer requires four metric panels", () => {
const html = renderToStaticMarkup(
<RuntimeTrendCharts samples={[sample(60_000, .25)]} source="durable" showDurableUptimePlaceholder />,
);

expect(html.match(/data-chart-engine="uplot"/g)).toHaveLength(4);
expect(html).toContain("Compute uptime");
expect(html).toContain("Live-only metric");
expect(html).toContain("Select Live to inspect current Runtime uptime");
});

it("announces an isolated durable value as sparse rather than empty", () => {
const html = renderToStaticMarkup(
<RuntimeTrendCharts samples={[sample(60_000, .25)]} source="durable" />,
);

expect(html).toContain("Memory usage durable trend has 1 sparse valid point; a line requires consecutive buckets");
expect(html).not.toContain("Memory usage No complete retained memory samples");
expect(html).not.toContain("Memory usage No retained observed memory samples");
});
});
33 changes: 15 additions & 18 deletions apps/web/src/features/dashboard/RuntimeTrendCharts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import uPlot from "uplot";
import "uplot/dist/uPlot.min.css";

import { formatDashboardBytes, formatDashboardDuration, formatDashboardTokens } from "./dashboard-model";
import { formatDashboardBytes, formatDashboardTokens } from "./dashboard-model";
import { tokenThroughput, type RuntimeTrendSample } from "./runtime-trends";

interface TrendPoint {
Expand Down Expand Up @@ -481,7 +481,7 @@ function TrendChart({
);
}

function targetIds(samples: readonly RuntimeTrendSample[], field: "cpuRatio" | "uptimeSeconds"): string[] {
function targetIds(samples: readonly RuntimeTrendSample[], field: "cpuRatio"): string[] {
const latest = new Map<string, number>();
for (const sample of samples) {
for (const target of sample.targets) {
Expand All @@ -507,17 +507,14 @@ export function RuntimeTrendCharts({
source = "live",
rangeStart,
rangeEnd,
showDurableUptimePlaceholder = false,
}: {
samples: readonly RuntimeTrendSample[];
source?: RuntimeTrendSource;
rangeStart?: number;
rangeEnd?: number;
showDurableUptimePlaceholder?: boolean;
}) {
const charts = useMemo(() => {
const cpuIds = targetIds(samples, "cpuRatio");
const uptimeIds = targetIds(samples, "uptimeSeconds");
const cpu = cpuIds.map((id, index): TrendSeries => ({
id,
label: targetLabel(samples, id),
Expand All @@ -529,23 +526,20 @@ export function RuntimeTrendCharts({
}
const memoryUsed = samples.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.memoryUsageBytes ?? 0 }));
const memoryLimit = samples.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.memoryLimitBytes ?? 0 }));
const uptime = uptimeIds.map((id, index): TrendSeries => ({
id,
label: targetLabel(samples, id),
tone: tones[(index + 2) % tones.length] ?? "blue",
points: samples.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.targets.find((target) => target.seriesId === id)?.uptimeSeconds ?? 0 })),
}));
if (uptime.length === 0 && samples.length > 0) {
uptime.push({ id: "uptime", label: "Runtime", tone: "blue", points: samples.map((sample) => ({ sampledAt: sample.sampledAt, value: 0 })) });
}
const active = [{
id: "active",
label: "active",
tone: "green",
points: samples.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.activeSandboxCount ?? 0 })),
}] satisfies TrendSeries[];
const throughput = tokenThroughput(samples);
return {
cpu,
memory: [
{ id: "used", label: "used", tone: "purple", points: memoryUsed },
{ id: "limit", label: "configured limit", tone: "green", points: memoryLimit },
] satisfies TrendSeries[],
uptime,
active,
tokens: [
{ id: "input", label: "input", tone: "orange", points: throughput.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.inputPerMinute ?? 0 })) },
{ id: "output", label: "output", tone: "green", points: throughput.map((sample) => ({ sampledAt: sample.sampledAt, value: sample.outputPerMinute ?? 0 })) },
Expand All @@ -554,7 +548,10 @@ export function RuntimeTrendCharts({
}, [samples]);
const cpuMaximum = Math.max(100, ...finite(charts.cpu.flatMap((series) => series.points.map((point) => point.value))));
const memoryMaximum = Math.max(1, ...finite(charts.memory.flatMap((series) => series.points.map((point) => point.value))));
const uptimeMaximum = Math.max(1, ...finite(charts.uptime.flatMap((series) => series.points.map((point) => point.value))));
const activeMaximum = Math.max(1, ...finite(charts.active.flatMap((series) => series.points.map((point) => point.value))));
const activeTicks = activeMaximum <= 4
? Array.from({ length: activeMaximum + 1 }, (_, index) => (activeMaximum - index) / activeMaximum)
: [1, .66, .33, 0];
const tokenMaximum = Math.max(1, ...finite(charts.tokens.flatMap((series) => series.points.map((point) => point.value))));
const newest = rangeEnd ?? samples.at(-1)?.sampledAt ?? Date.now();
const oldest = rangeStart ?? samples[0]?.sampledAt ?? newest - 60 * 60 * 1_000;
Expand All @@ -563,8 +560,8 @@ export function RuntimeTrendCharts({
return (
<div className="dashboard-runtime-trend-grid" aria-label={durable ? "Runtime durable-history charts" : "Runtime live-window charts"}>
<TrendChart title="CPU usage" subtitle={durable ? "bucketed cumulative-delta utilization · durable history" : "reported or cumulative-delta utilization · live window"} samples={samples} series={charts.cpu} maximum={cpuMaximum} formatValue={(value) => `${Math.round(value)}%`} rangeStart={oldest} rangeEnd={newest} source={source} bands={[{ from: 0, to: 30, tone: "safe" }, { from: 30, to: 70, tone: "warning" }, { from: 70, to: 100, tone: "danger" }]} ticks={[1, .7, .3, 0]} emptyMessage={durable ? "No retained CPU samples" : undefined} />
<TrendChart title="Memory usage" subtitle={durable ? "complete target aggregate / configured limit · durable history" : "working set / configured limit · live window"} samples={samples} series={charts.memory} maximum={memoryMaximum} formatValue={(value) => formatDashboardBytes(Math.round(value))} rangeStart={oldest} rangeEnd={newest} source={source} emptyMessage={durable ? "No complete retained memory samples" : undefined} />
{!durable || showDurableUptimePlaceholder ? <TrendChart title="Compute uptime" subtitle={durable ? "current Runtime measurement · not retained in durable history" : "provider started_at → observed_at · allocation series"} samples={samples} series={durable ? [] : charts.uptime} maximum={uptimeMaximum} formatValue={(value) => formatDashboardDuration(value)} rangeStart={oldest} rangeEnd={newest} source={source} emptyMessage={durable ? "Live-only metric" : undefined} emptyDetail={durable ? "Select Live to inspect current Runtime uptime" : undefined} /> : null}
<TrendChart title="Memory usage" subtitle={durable ? "observed Sandbox aggregate / configured limit · durable history" : "observed Sandbox working set / configured limit · live window"} samples={samples} series={charts.memory} maximum={memoryMaximum} formatValue={(value) => formatDashboardBytes(Math.round(value))} rangeStart={oldest} rangeEnd={newest} source={source} emptyMessage={durable ? "No retained observed memory samples" : undefined} />
<TrendChart title="Active Sandboxes" subtitle={durable ? "confirmed observed allocations per bucket · durable history" : "confirmed observed allocations per snapshot · live window"} samples={samples} series={charts.active} maximum={activeMaximum} formatValue={(value) => `${Math.round(value)}`} rangeStart={oldest} rangeEnd={newest} source={source} ticks={activeTicks} emptyMessage={durable ? "No confirmed active Sandbox samples" : undefined} />
<TrendChart title="Token throughput" subtitle={durable ? "canonical Session Usage deltas · durable history" : "Session Usage deltas · missing usage excluded"} samples={samples} series={charts.tokens} maximum={tokenMaximum} formatValue={(value) => `${formatDashboardTokens(Math.round(value))}/min`} rangeStart={oldest} rangeEnd={newest} source={source} emptyMessage={durable ? "No retained token samples" : undefined} />
</div>
);
Expand Down
Loading
Loading