Skip to content
Merged
31 changes: 17 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 @@ -2656,6 +2657,7 @@ test("renders Runtime telemetry as visual snapshot panels with details on demand
device_id: null,
connection_generation: null,
},
lifecycle_state: "active",
status: "observed",
reason: null,
allocation_created_at: baseline - 8_500,
Expand All @@ -2680,7 +2682,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 +2694,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 +2730,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")).toContainText("10%");
await cpuChart.focus();
await cpuChart.press("ArrowRight");
await expect(cpuCard.locator(".dashboard-runtime-trend-tooltip")).toContainText("Pinned");
Expand Down Expand Up @@ -2760,7 +2762,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 @@ -2838,6 +2840,7 @@ test("restores retained Runtime history after a Dashboard reload", async ({ page
id: sessionId, object: "agent.runtime_observation", session_id: sessionId, environment_id: environmentId,
mode: "openai_hosted", provider_type: "docker",
instance: { kind: "managed_allocation", allocation_id: allocationId, device_id: null, connection_generation: null },
lifecycle_state: "active",
status: "observed", reason: null, allocation_created_at: now - 600, resolved_at: now,
observed_at: now - 1, started_at: now - 600,
cpu: { usage_seconds_total: 120, capacity_cores: 2, usage_cores: null, utilization_ratio: null },
Expand Down Expand Up @@ -2910,19 +2913,21 @@ 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.getByRole("heading", { name: "Active sandboxes", exact: true })).toBeVisible();
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 +3042,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
5 changes: 4 additions & 1 deletion apps/web/src/features/dashboard/DashboardView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ describe("Dashboard loaded-result presentation", () => {
device_id: null,
connection_generation: null,
},
lifecycle_state: "active",
status: "observed",
reason: null,
allocation_created_at: 1_700_000_000,
Expand Down Expand Up @@ -256,7 +257,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 Expand Up @@ -342,6 +344,7 @@ describe("Dashboard loaded-result presentation", () => {
mode: "openai_hosted",
provider_type: "docker",
instance: { kind: "managed_allocation", allocation_id: "33333333-3333-4333-8333-333333333333", device_id: null, connection_generation: null },
lifecycle_state: "active",
status: "observed",
reason: null,
allocation_created_at: 1_700_000_000,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ export function RuntimeObservabilityContent({
return (
<>
<div className="dashboard-runtime-summary" aria-label={t("runtime.resourceSnapshot")}>
<RuntimeMetric icon={<Server size={17} />} label={t("runtime.metrics.active")} value={summary.observedRuntimeCount.toLocaleString(locale)} detail={t("runtime.metrics.activeDetail", { managed: summary.managedRuntimeCount, unavailable: summary.unavailableRuntimeCount })} />
<RuntimeMetric icon={<Server size={17} />} label={t("runtime.metrics.sandboxState")} value={t("runtime.metrics.sandboxStateValue", { active: summary.activeSandboxCount.toLocaleString(locale), sleeping: summary.sleepingSandboxCount.toLocaleString(locale) })} detail={t("runtime.metrics.sandboxStateDetail", { total: summary.sandboxTotalCount.toLocaleString(locale), transitioning: (summary.transitioningSandboxCount + summary.pendingSandboxCount).toLocaleString(locale) })} />
<RuntimeMetric icon={<Cpu size={17} />} label={t("runtime.metrics.cpu")} value={summary.cpuUsageSecondsTotal === null && summary.cpuCapacityCores === null ? t("runtime.metrics.noSample") : `${summary.cpuUsageSecondsTotal === null ? t("runtime.filters.unavailable") : formatDashboardDuration(summary.cpuUsageSecondsTotal)} / ${summary.cpuCapacityCores === null ? "—" : t("runtime.cores", { value: summary.cpuCapacityCores.toLocaleString(locale) })}`} detail={t("runtime.metrics.cpuDetail", { covered: summary.cpuCoverageCount, total: summary.observedRuntimeCount })} />
<RuntimeMetric icon={<MemoryStick size={17} />} label={t("runtime.metrics.memory")} value={summary.memoryUsageBytes === null && summary.memoryLimitBytes === null ? t("runtime.metrics.noSample") : `${summary.memoryUsageBytes === null ? t("runtime.filters.unavailable") : formatDashboardBytes(summary.memoryUsageBytes)} / ${summary.memoryLimitBytes === null ? t("runtime.filters.unavailable") : formatDashboardBytes(summary.memoryLimitBytes)}`} detail={t("runtime.metrics.memoryDetail", { covered: summary.memoryCoverageCount, total: summary.observedRuntimeCount })} />
<RuntimeMetric icon={<Gauge size={17} />} label={t("runtime.metrics.tokens")} value={summary.totalTokens === null ? t("runtime.filters.unavailable") : formatDashboardTokens(summary.totalTokens, locale)} detail={t("runtime.metrics.tokenDetail", { covered: summary.tokenCoverageCount, total: summary.sessionCount })} />
Expand Down
52 changes: 40 additions & 12 deletions apps/web/src/features/dashboard/RuntimeTrendCharts.test.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";

import { RuntimeTrendCharts, runtimeChartCaption, runtimeChartShowsSparsePoints } from "./RuntimeTrendCharts";
import { integerTickRatios, RuntimeTrendCharts, runtimeChartCaption, runtimeChartShowsSparsePoints } from "./RuntimeTrendCharts";
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",
cpuRatio,
uptimeSeconds: 120,
}],
cpuCandidates: [],
memoryUsageBytes: 512,
Expand All @@ -23,6 +23,11 @@ function sample(sampledAt: number, cpuRatio: number | null): RuntimeTrendSample
}

describe("Runtime live-window chart accessibility", () => {
it("uses exact integer y-axis positions for Sandbox counts", () => {
expect(integerTickRatios(5).map((ratio) => ratio * 5)).toEqual([5, 3, 2, 0]);
expect(integerTickRatios(17).map((ratio) => ratio * 17)).toEqual([17, 11, 6, 0]);
});

it("shows isolated or sparse values as points without inventing continuity", () => {
expect(runtimeChartShowsSparsePoints([null, 512, null])).toBe(true);
expect(runtimeChartShowsSparsePoints([512, 768])).toBe(true);
Expand All @@ -38,23 +43,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 +80,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 +94,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 +118,42 @@ 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", () => {
it("renders one summed active-Sandbox series instead of one series per Session", () => {
const latest = { ...sample(120_000, .5), activeSandboxCount: 3 };
const html = renderToStaticMarkup(
<RuntimeTrendCharts samples={[sample(60_000, .25)]} source="durable" showDurableUptimePlaceholder />,
<RuntimeTrendCharts samples={[sample(60_000, .25), latest]} />,
);

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");
expect(html).toContain("active</th><td>3</td><td>0</td>");
expect(html.match(/aria-label="Hide active series"/g)).toHaveLength(1);
});

it("renders the Session view as one binary Runtime-active series", () => {
const latest = { ...sample(120_000, .5), activeSandboxCount: 3 };
const html = renderToStaticMarkup(
<RuntimeTrendCharts samples={[sample(60_000, .25), latest]} activeDisplay="binary" />,
);

expect(html).toContain("Runtime active");
expect(html).toContain("1 active / 0 inactive");
expect(html).toContain("Runtime</th><td>Active</td><td>0</td>");
expect(html).not.toContain("Active sandboxes");
});
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");
});
});
Loading
Loading