Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ test-results/
# UI kit / console (U1+)
node_modules/
ui/**/dist/

# Coverage reports are BUILD OUTPUT: regenerated by `pnpm test`, timestamped, never reviewed.
coverage/
4 changes: 3 additions & 1 deletion docs/rfc/goldpath-console.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ custom-develop ON, with the same kit, the same way they add features to the back
landing page: operators open consoles to answer "is anything wrong", not to browse.
- **D3 — Proof bar (UI is claims-are-proofs too).**
Kit: component tests (vitest) on the composites (keyset table paging, verb button's
refusal surface, state mapping). Console: the smoke drives THREE real apps — one open,
refusal surface, state mapping), under a COVERAGE FLOOR that CI enforces (kit 95/90,
console 97/85 statements/branches) — the floor exists because the gaps that hid real
bugs here were branches nobody had thought to exercise, not tests anyone had deleted. Console: the smoke drives THREE real apps — one open,
one behind the auth floor, one tenant-scoped — because how the console behaves when it
is REFUSED is as much a claim as how it behaves when it is welcome; plus a service that
dies mid-session. Runnable on demand (`.github/workflows/console-smoke.yml`), not only
Expand Down
13 changes: 7 additions & 6 deletions ui/console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"type": "module",
"main": "src/index.ts",
"scripts": {
"test": "vitest run",
"test": "vitest run --coverage --coverage.thresholds.statements=97 --coverage.thresholds.branches=85 --coverage.thresholds.functions=75 --coverage.thresholds.lines=97",
"typecheck": "tsc --noEmit",
"dev": "vite",
"e2e": "playwright test"
Expand All @@ -16,21 +16,22 @@
"react-dom": "^19.0.0"
},
"devDependencies": {
"@goldpath/kit": "link:../kit",
"@playwright/test": "1.50.1",
"@tailwindcss/vite": "^4.3.2",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "14.6.1",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "2.1.8",
"jsdom": "^25.0.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.0",
"vite": "^8.1.4",
"vitest": "^2.1.8",
"@goldpath/kit": "link:../kit",
"@tailwindcss/vite": "^4.3.2",
"tailwindcss": "^4.0.0",
"@playwright/test": "1.50.1"
"vitest": "^2.1.8"
}
}
414 changes: 414 additions & 0 deletions ui/console/pnpm-lock.yaml

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions ui/console/src/ArchivalPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -261,4 +261,45 @@ describe("the archival panel (chain health, retrieval, lifecycle)", () => {
// The previous "verifies" verdict is GONE — an unknown chain must not read as a proven one.
expect(screen.queryByTestId("chain-findings")).toBeNull();
});

it("a lifecycle verb that never reached the server is NOT reported as applied", async () => {
const { client: api } = client({ verb: { status: 503, body: {} } });
render(<ArchivalPanel client={api} now={NOW} />);

await retrieve();
await userEvent.click(await screen.findByRole("button", { name: "lift-hold" }));
await userEvent.click(within(screen.getByRole("alertdialog")).getByRole("button", { name: "lift-hold" }));

expect(await screen.findByText(/did not reach the server — it may not have been applied/)).toBeInTheDocument();
});

it("switching archive clears the open entry — a key means nothing outside its archive", async () => {
const { client: api } = client({
definitions: [
{ name: "policies", entries: 10, dueBacklog: 0, activeHolds: 0, chainHead: 10, purgedThrough: 0 },
{ name: "claims", entries: 4, dueBacklog: 0, activeHolds: 0, chainHead: 4, purgedThrough: 0 },
],
});
render(<ArchivalPanel client={api} now={NOW} />);

await retrieve();
await screen.findByTestId("archive-entry");

await userEvent.selectOptions(screen.getByLabelText("archive"), "claims");

// The policies entry must not linger under the claims archive's name.
expect(screen.queryByTestId("archive-entry")).toBeNull();
expect(screen.queryByText(/No entry for/)).toBeNull();
});

it("a lifted hold shows who lifted it and when — the row stays as evidence", async () => {
const { client: api } = client({
holds: [hold({ liftedBy: "legal@example.com", liftedAt: "2026-07-27T05:00:00Z" })],
});
render(<ArchivalPanel client={api} now={NOW} />);

const row = await screen.findByRole("row", { name: /CASE-2026-7/ });
expect(row).toHaveTextContent("lifted by legal@example.com at 2026-07-27T05:00:00Z");
expect(within(row).queryByText("Held")).toBeNull();
});
});
36 changes: 36 additions & 0 deletions ui/console/src/BulkPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,40 @@ describe("the bulk intake panel (upload → report → four-eyes gate)", () => {
await waitFor(() => expect(screen.queryByRole("button", { name: "approve" })).toBeNull());
expect(screen.getByText("approved")).toBeInTheDocument();
});

it("a decision that never reached the server is NOT reported as recorded", async () => {
// The verb envelope never came back — 503 is neither an ok nor a refusal, and the
// operator must not walk away believing the gate moved.
const { client: api } = client({ verb: { status: 503, body: {} } });
render(<BulkPanel client={api} />);

await openBatch();
await userEvent.click(await screen.findByRole("button", { name: "approve" }));
await userEvent.click(within(screen.getByRole("alertdialog")).getByRole("button", { name: "approve" }));

expect(await screen.findByText(/did not reach the server — it may not have been recorded/)).toBeInTheDocument();
});

it("a decided batch carries its evidence: who, when, and why", async () => {
const decided = batch({
state: "Rejected",
decidedAt: "2026-07-27T10:00:00Z",
decidedBy: "ops@example.com",
decisionNote: "line 3 is malformed",
});
const { client: api } = client({ batch: decided, batches: [decided] });
render(<BulkPanel client={api} />);

await openBatch();
const detail = await screen.findByTestId("batch-detail");
expect(detail).toHaveTextContent("Rejected by ops@example.com at 2026-07-27T10:00:00Z");
expect(detail).toHaveTextContent("line 3 is malformed");
});

it("a definition with nothing in it says so, instead of showing a bare name", async () => {
const { client: api } = client({ definitions: [definition({ batchesByState: {}, awaitingApproval: 0 })] });
render(<BulkPanel client={api} />);

expect(await screen.findByText("no batches yet")).toBeInTheDocument();
});
});
22 changes: 22 additions & 0 deletions ui/console/src/CampaignPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -298,4 +298,26 @@ describe("the campaign governor panel", () => {

await waitFor(() => expect(screen.getByTestId("campaign-detail").textContent).not.toBe(before));
});

it("a verb that never reached the server is NOT reported as applied", async () => {
const { client: api } = client({ verb: { status: 503, body: {} } });
render(<CampaignPanel client={api} />);

await open();
await userEvent.click(await screen.findByRole("button", { name: "pause" }));
await userEvent.click(within(screen.getByRole("alertdialog")).getByRole("button", { name: "pause" }));

expect(await screen.findByText(/did not reach the server — it may not have been applied/)).toBeInTheDocument();
});

it("a campaign with no window releases around the clock, and says exactly that", async () => {
const always = campaign({ windowStart: null, windowEnd: null, dailyQuota: null });
const { client: api } = client({ detail: always, list: [always] });
render(<CampaignPanel client={api} />);

await open();
const detail = await screen.findByTestId("campaign-detail");
expect(detail).toHaveTextContent("around the clock");
expect(detail).toHaveTextContent("Daily quotanone");
});
});
36 changes: 35 additions & 1 deletion ui/console/src/NotificationPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ function client(routes: Routes = {}) {
const json = (body: unknown) =>
new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } });

// The by-id route must be matched BEFORE the list route it is nested under.
if (/\/notifications\/[^/?]+$/.test(url)) {
const id = url.split("/").pop();
const all = [
notification(),
notification({ state: "Failed", id: "failed-1", attempts: 3, detail: "the relay refused: mailbox full", failedAt: "2026-07-27T06:02:00Z" }),
notification({ state: "Suppressed", id: "suppressed-1", detail: "suppressed by the MaySend hook — suppression is evidence too" }),
...(Array.isArray(routes.list) ? (routes.list as NotificationInfo[]) : []),
];
return json(all.find((row) => row.id === id) ?? notification());
}

if (url.includes("/failures")) return json(routes.failures ?? [notification({ state: "Failed", id: "failed-1", attempts: 3, detail: "the relay refused: mailbox full", failedAt: "2026-07-27T06:02:00Z" })]);
if (url.includes("/suppressions")) return json(routes.suppressions ?? [notification({ state: "Suppressed", id: "suppressed-1", detail: "suppressed by the MaySend hook — suppression is evidence too" })]);
if (url.includes("/notifications")) return json(routes.list ?? [notification()]);
Expand Down Expand Up @@ -157,7 +169,7 @@ describe("the notification evidence panel (read-only by contract)", () => {
});

it("says when the body was deleted — the retention promise, kept and recorded", async () => {
const { client: api } = client({ list: [notification({ bodyDeletedAt: "2026-10-25T00:00:00Z" })] });
const { client: api } = client({ list: [notification({ id: "deleted-1", bodyDeletedAt: "2026-10-25T00:00:00Z" })] });
render(<NotificationPanel client={api} />);

await userEvent.click(await screen.findByRole("button", { name: "a***@e***" }));
Expand All @@ -171,4 +183,26 @@ describe("the notification evidence panel (read-only by contract)", () => {
// The table reports its own failure too — the panel-level banner is the first one.
expect((await screen.findAllByRole("alert"))[0]).toHaveTextContent(/notification templates could not be loaded/i);
});

it("tenant and correlation id appear when the row carries them — and never as empty labels", async () => {
const { client: api } = client({
list: [notification({ id: "traced-1", tenant: "acme", correlationId: "0af7651916cd43dd8448eb211c80319c" })],
});
render(<NotificationPanel client={api} />);

await userEvent.click(await screen.findByRole("button", { name: "a***@e***" }));
const detail = await screen.findByTestId("notification-detail");
expect(detail).toHaveTextContent("acme");
expect(detail).toHaveTextContent("0af7651916cd43dd8448eb211c80319c");
});

it("a row WITHOUT them shows no tenant or correlation block at all", async () => {
const { client: api } = client();
render(<NotificationPanel client={api} />);

await userEvent.click(await screen.findByRole("button", { name: "a***@e***" }));
const detail = await screen.findByTestId("notification-detail");
expect(within(detail).queryByText("Tenant")).toBeNull();
expect(within(detail).queryByText("Correlation")).toBeNull();
});
});
27 changes: 23 additions & 4 deletions ui/console/src/NotificationPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function NotificationPanel({ client }: NotificationPanelProps) {
const [lens, setLens] = useState<Lens>("all");
const [state, setState] = useState<string>("");
const [template, setTemplate] = useState<string>("");
const [selectedId, setSelectedId] = useState<string | null>(null);
const [selected, setSelected] = useState<NotificationInfo | null>(null);
const [error, setError] = useState<string | null>(null);
const [refreshToken, setRefreshToken] = useState(0);
Expand Down Expand Up @@ -77,6 +78,24 @@ export function NotificationPanel({ client }: NotificationPanelProps) {
[client, lens, state, template, refreshToken],
);

// The open row RE-READS by id: a notification is evidence in MOTION (Requested → Sent
// or Failed), so a row captured minutes ago is not what the operator should judge.
useEffect(() => {
if (!selectedId) {
setSelected(null);
return;
}

let live = true;
client
.notification(selectedId)
.then((found) => live && setSelected(found))
.catch(() => live && setError(`notification ${selectedId} could not be read`));
return () => {
live = false;
};
}, [client, selectedId, refreshToken]);

const waiting = (templates ?? []).filter((entry) => (entry.oldestRequestedSeconds ?? 0) > 0);

return (
Expand Down Expand Up @@ -127,7 +146,7 @@ export function NotificationPanel({ client }: NotificationPanelProps) {
}`}
onClick={() => {
setLens(entry.key);
setSelected(null);
setSelectedId(null);
}}
>
{entry.label}
Expand All @@ -145,7 +164,7 @@ export function NotificationPanel({ client }: NotificationPanelProps) {
value={state}
onChange={(event) => {
setState(event.target.value);
setSelected(null);
setSelectedId(null);
}}
>
<option value="">all states</option>
Expand All @@ -165,7 +184,7 @@ export function NotificationPanel({ client }: NotificationPanelProps) {
value={template}
onChange={(event) => {
setTemplate(event.target.value);
setSelected(null);
setSelectedId(null);
}}
>
<option value="">all templates</option>
Expand All @@ -192,7 +211,7 @@ export function NotificationPanel({ client }: NotificationPanelProps) {
{
header: "Recipient (masked)",
cell: (row) => (
<button className="font-mono text-xs underline-offset-2 hover:underline" onClick={() => setSelected(row)}>
<button className="font-mono text-xs underline-offset-2 hover:underline" onClick={() => setSelectedId(row.id)}>
{row.maskedRecipient}
</button>
),
Expand Down
42 changes: 42 additions & 0 deletions ui/console/src/RunConsole.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,46 @@ describe("the run console (console RFC §2 — a client of the frozen contract)"

expect(await screen.findByRole("alert")).toHaveTextContent(/fleet list could not be loaded/i);
});

it("choosing another fleet loads ITS jobs and closes the run that belonged to the first", async () => {
const fetcher = (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
const json = (body: unknown) => new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } });
if (init?.method === "POST") return json({ ok: true, message: "done" });
if (url.includes("/jobs/runs/")) return json(detail());
if (url.includes("/runs?")) return json(url.includes("night-cluster") ? [] : [run()]);
if (url.endsWith("/jobs")) return json(url.includes("night-cluster") ? [{ name: "nightly-sweep" }] : [{ name: "eod-reconciliation" }]);
return json([
{ schedulerName: "it-cluster", jobCount: 1, nodes: [{ instanceId: "node-a" }] },
{ schedulerName: "night-cluster", jobCount: 1, nodes: [] },
]);
}) as typeof fetch;

render(<RunConsole client={new AdminClient({ fetcher })} />);

await userEvent.click(await screen.findByRole("button", { name: "run-9f21" }));
await screen.findByTestId("run-detail");

await userEvent.click(screen.getByRole("button", { name: /night-cluster/ }));

// The open run belonged to the other fleet: keeping it on screen would put one
// fleet's job list next to another fleet's run.
await waitFor(() => expect(screen.queryByTestId("run-detail")).toBeNull());
expect(await screen.findByText("nightly-sweep")).toBeInTheDocument();
});

it("a PAUSED job offers resume, and resuming posts the frozen route", async () => {
const { client: api, posted } = client({ jobs: [{ name: "eod-reconciliation", paused: true }] });
render(<RunConsole client={api} />);

// A paused job must never offer "pause" — the console reads the job's own flag.
expect(await screen.findByRole("button", { name: "resume" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "pause" })).toBeNull();

await userEvent.click(screen.getByRole("button", { name: "resume" }));
await userEvent.click(screen.getByRole("alertdialog").querySelector("button")!);

await waitFor(() => expect(posted).toHaveLength(1));
expect(posted[0].url).toContain("/goldpath/admin/jobs/fleets/it-cluster/jobs/eod-reconciliation/resume");
});
});
37 changes: 37 additions & 0 deletions ui/console/src/adminClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,41 @@ describe("the admin client (the console's only door — the FROZEN contract)", (

expect(calls[0].init?.credentials).toBe("include");
});

it("every campaign verb posts ITS frozen route — including the ones the panel shows rarely", async () => {
const { fetcher, calls } = fakeFetch({ "/campaign/": json({ ok: true, message: "done" }) });
const client = new AdminClient({ fetcher });

await client.resumeCampaign("c-1");
await client.pauseCampaign("c-1");

expect(calls[0].url).toContain("/goldpath/admin/campaign/c-1/resume");
expect(calls[1].url).toContain("/goldpath/admin/campaign/c-1/pause");
});

it("ids with slashes and spaces are ENCODED — a key is data, never part of the path", async () => {
const { fetcher, calls } = fakeFetch({ "/goldpath/admin/": json({}) });
const client = new AdminClient({ fetcher });

await client.notification("a/b c");
await client.archiveEntry("policies", "P/42 x");

expect(calls[0].url).toContain("/notifications/a%2Fb%20c");
expect(calls[1].url).toContain("/entries/policies/P%2F42%20x");
});

it("rerun posts the run's own route — the repair verbs are all one shape", async () => {
const { fetcher, calls } = fakeFetch({ "/jobs/runs/": json({ ok: true, message: "rerun queued" }) });

const result = await new AdminClient({ fetcher }).rerun("run-9f21");

expect(calls[0].url).toContain("/goldpath/admin/jobs/runs/run-9f21/rerun");
expect(result).toEqual({ ok: true, message: "rerun queued" });
});

it("a verb answered with an unexpected status THROWS — it is never read as an outcome", async () => {
const { fetcher } = fakeFetch({ "/jobs/runs/": new Response("", { status: 503 }) });

await expect(new AdminClient({ fetcher }).rerun("run-9f21")).rejects.toThrow(/503/);
});
});
7 changes: 7 additions & 0 deletions ui/console/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ export default defineConfig({
},
test: {
environment: "jsdom",
coverage: {
// Only the SOURCE is judged: the gallery and the dev entry point are worked
// examples, and a barrel file has nothing to test.
include: ["src/**/*.{ts,tsx}"],
exclude: ["src/index.ts", "src/test-setup.ts", "src/**/*.{test,spec}.{ts,tsx}"],
},

globals: true,
setupFiles: ["./src/test-setup.ts"],
// The e2e suite belongs to Playwright (`pnpm e2e`); vitest owns src/ only.
Expand Down
Loading
Loading