milestone 30: overview as a dashboard, explicit health contract, period aggregations
Gates / frontend (push) Successful in 1m32s
Gates / test (push) Successful in 1m54s
Gates / package (push) Successful in 5m28s
Gates / container (push) Successful in 14s
Gates / test-aarch64 (push) Failing after 3h10m0s
CI / gates (push) Failing after 3h11m55s

This commit is contained in:
2026-08-22 16:45:15 +02:00
parent 17422fac21
commit 648d9b4496
89 changed files with 7222 additions and 4239 deletions
@@ -0,0 +1,287 @@
/**
* The Pause control, migrated from the header PauseWidget it replaces. Every
* behaviour that widget pinned is pinned here, now driven by `Health.protection`
* rather than by a second poll of `/api/pause`.
*/
import { act } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import PauseControl from "@/features/pause/PauseControl";
import { formatClock } from "@/lib/format";
import { createQueryClient } from "@/lib/queryClient";
import { health } from "@/lib/healthFixture";
import { queryKeys } from "@/lib/queries";
import type { Health, PausePost, PauseState } from "@/lib/types";
let protection: Health["protection"];
let postBodies: PausePost[];
let postFailure: (() => Response) | null;
let healthFails: boolean;
function jsonResponse(payload: unknown): Response {
return new Response(JSON.stringify(payload), { status: 200, headers: { "content-type": "application/json" } });
}
beforeEach(() => {
protection = { state: "active", until: null };
postBodies = [];
postFailure = null;
healthFails = false;
vi.stubGlobal(
"fetch",
vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
if (url === "/api/health") {
if (healthFails) {
return new Response(JSON.stringify({ error: "nope" }), {
status: 400,
headers: { "content-type": "application/json" },
});
}
return jsonResponse(health({ protection }));
}
if (url === "/api/pause" && init?.method === "POST") {
const body = JSON.parse(String(init.body)) as PausePost;
postBodies.push(body);
if (postFailure !== null) return postFailure();
protection = body.paused
? {
state: "paused",
until:
body.duration_seconds == null
? null
: Math.floor(Date.now() / 1000) + body.duration_seconds,
}
: { state: "active", until: null };
const echo: PauseState = { paused: body.paused, until: protection.until };
return jsonResponse(echo);
}
return jsonResponse({ error: "not stubbed" });
}),
);
});
afterEach(() => {
vi.unstubAllGlobals();
vi.useRealTimers();
});
function renderControl(client?: QueryClient) {
render(
<QueryClientProvider client={client ?? createQueryClient()}>
<PauseControl />
</QueryClientProvider>,
);
}
async function findPauseTrigger(): Promise<HTMLButtonElement> {
await waitFor(() => {
const button = screen.getByRole("button", { name: "Pause" }) as HTMLButtonElement;
expect(button.disabled).toBe(false);
});
return screen.getByRole("button", { name: "Pause" }) as HTMLButtonElement;
}
test("unpaused: duration menu pauses with the picked duration_seconds", async () => {
renderControl();
const trigger = await findPauseTrigger();
expect(trigger.getAttribute("aria-expanded")).toBe("false");
fireEvent.click(trigger);
expect(trigger.getAttribute("aria-expanded")).toBe("true");
for (const label of ["60 seconds", "5 minutes", "30 minutes", "Indefinitely"]) {
expect(screen.getByRole("button", { name: label })).toBeTruthy();
}
fireEvent.click(screen.getByRole("button", { name: "5 minutes" }));
await waitFor(() => expect(postBodies).toEqual([{ paused: true, duration_seconds: 300 }]));
await screen.findByRole("button", { name: "Resume" });
});
test("indefinite pause sends no duration_seconds", async () => {
renderControl();
fireEvent.click(await findPauseTrigger());
fireEvent.click(screen.getByRole("button", { name: "Indefinitely" }));
await waitFor(() => expect(postBodies).toEqual([{ paused: true }]));
await screen.findByRole("button", { name: "Resume" });
});
test("resume posts paused false and returns to the Pause button", async () => {
protection = { state: "paused", until: null };
renderControl();
fireEvent.click(await screen.findByRole("button", { name: "Resume" }));
await waitFor(() => expect(postBodies).toEqual([{ paused: false }]));
await screen.findByRole("button", { name: "Pause" });
});
test("a timed pause says until when, beside the control that would end it", () => {
const nowSec = Math.floor(Date.now() / 1000);
const client = createQueryClient();
client.setQueryData(queryKeys.health, health({ protection: { state: "paused", until: nowSec + 90 } }));
renderControl(client);
// The same clock format the Diagnostics health strip writes, off the same
// reading, so the two cannot say different things about one pause.
expect(screen.getByText(`Paused until ${formatClock(nowSec + 90)}`)).toBeTruthy();
expect(screen.getByRole("button", { name: "Resume" })).toBeTruthy();
});
test("a pause with no end says so without inventing a time", () => {
const client = createQueryClient();
client.setQueryData(queryKeys.health, health({ protection: { state: "paused", until: null } }));
renderControl(client);
expect(screen.getByText("Paused")).toBeTruthy();
expect(screen.queryByText(/until/)).toBeNull();
});
test("an active resolver states nothing: the button already says Pause", async () => {
renderControl();
expect(await findPauseTrigger()).toBeTruthy();
expect(screen.queryByText(/^Paused/)).toBeNull();
});
test("escape closes the duration menu", async () => {
renderControl();
const trigger = await findPauseTrigger();
fireEvent.click(trigger);
expect(screen.getByRole("button", { name: "Indefinitely" })).toBeTruthy();
fireEvent.keyDown(trigger, { key: "Escape" });
expect(screen.queryByRole("button", { name: "Indefinitely" })).toBeNull();
});
test("failed pause with 429 shows a ticking retry countdown", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
postFailure = () =>
new Response(JSON.stringify({ error: "rate limited" }), {
status: 429,
headers: { "content-type": "application/json", "Retry-After": "30" },
});
renderControl();
fireEvent.click(await findPauseTrigger());
fireEvent.click(screen.getByRole("button", { name: "5 minutes" }));
const alert = await screen.findByRole("alert");
expect(alert.textContent).toBe("Rate limited. Try again in 30s.");
act(() => {
vi.advanceTimersByTime(1000);
});
expect(alert.textContent).toBe("Rate limited. Try again in 29s.");
});
test("failed pause with 503 shows the degraded message", async () => {
postFailure = () =>
new Response(JSON.stringify({ error: "unavailable" }), {
status: 503,
headers: { "content-type": "application/json" },
});
renderControl();
fireEvent.click(await findPauseTrigger());
fireEvent.click(screen.getByRole("button", { name: "60 seconds" }));
const alert = await screen.findByRole("alert");
expect(alert.textContent).toBe("The server is starting or degraded. Try again shortly.");
});
test("a successful pause clears the previous mutation error", async () => {
postFailure = () =>
new Response(JSON.stringify({ error: "unavailable" }), {
status: 503,
headers: { "content-type": "application/json" },
});
renderControl();
fireEvent.click(await findPauseTrigger());
fireEvent.click(screen.getByRole("button", { name: "5 minutes" }));
await screen.findByRole("alert");
postFailure = null;
fireEvent.click(screen.getByRole("button", { name: "Pause" }));
fireEvent.click(screen.getByRole("button", { name: "5 minutes" }));
await screen.findByRole("button", { name: "Resume" });
expect(screen.queryByRole("alert")).toBeNull();
});
test("no control at all while protection is unavailable", async () => {
protection = { state: "unavailable", until: null };
const client = createQueryClient();
renderControl(client);
await waitFor(() => expect(client.getQueryData(queryKeys.health)).toBeDefined());
expect(screen.queryByRole("button")).toBeNull();
});
test("a failed poll after a good one withdraws the control rather than acting on a stale state", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
renderControl();
await vi.waitFor(() =>
expect((screen.getByRole("button", { name: "Pause" }) as HTMLButtonElement).disabled).toBe(false),
);
healthFails = true;
await vi.advanceTimersByTimeAsync(11_000);
await vi.waitFor(() => expect(screen.queryByRole("button")).toBeNull());
vi.useRealTimers();
});
test("a menu left open when the control withdraws does not come back open", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
renderControl();
fireEvent.click(await findPauseTrigger());
expect(screen.getByRole("button", { name: "Indefinitely" })).toBeTruthy();
protection = { state: "unavailable", until: null };
await vi.advanceTimersByTimeAsync(11_000);
await vi.waitFor(() => expect(screen.queryByRole("button")).toBeNull());
protection = { state: "active", until: null };
await vi.advanceTimersByTimeAsync(11_000);
// Protection is back, and so is the trigger — but the menu is a thing the
// reader opened, and nobody opened this one.
const trigger = await vi.waitFor(() => screen.getByRole("button", { name: "Pause" }));
expect(trigger.getAttribute("aria-expanded")).toBe("false");
expect(screen.queryByRole("button", { name: "Indefinitely" })).toBeNull();
vi.useRealTimers();
});
test("a menu open when someone else pauses does not reopen when that pause ends", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
renderControl();
fireEvent.click(await findPauseTrigger());
expect(screen.getByRole("button", { name: "Indefinitely" })).toBeTruthy();
// Filtering is paused from somewhere else, and this browser learns it from
// the poll. The Resume rendering has no menu.
protection = { state: "paused", until: null };
await vi.advanceTimersByTimeAsync(11_000);
await vi.waitFor(() => expect(screen.getByRole("button", { name: "Resume" })).toBeTruthy());
protection = { state: "active", until: null };
await vi.advanceTimersByTimeAsync(11_000);
const trigger = await vi.waitFor(() => screen.getByRole("button", { name: "Pause" }));
expect(trigger.getAttribute("aria-expanded")).toBe("false");
expect(screen.queryByRole("button", { name: "Indefinitely" })).toBeNull();
vi.useRealTimers();
});
test("protection unknown offers no control at all, by the same rule as unavailable", () => {
// Health has not answered. Which of Pause and Resume applies is exactly what
// it has not said, so the control names neither.
renderControl();
expect(screen.queryByRole("button")).toBeNull();
});