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
8 changes: 8 additions & 0 deletions docs/rfc/goldpath-console.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ custom-develop ON, with the same kit, the same way they add features to the back
Red/overrun-predicted runs, repair-queue depths, gates awaiting four-eyes, DLQ depth —
each row deep-links into its panel. Fleet browsing is one click away, never the
landing page: operators open consoles to answer "is anything wrong", not to browse.
SHIPPED (U4, 2026-07-27). Three properties it must keep, because each was a decision:
(a) every number comes from the contract's own take-bounded lists — the console invents
no aggregate the API does not expose — so the screen PRINTS its scope instead of
implying completeness; (b) a surface the console cannot read (no ops role, or a call it
cannot scope) is itself a row: blindness during an incident is the most important thing
an operator can be told, and it is grouped per service so it cannot bury the estate's
real problems; (c) a surface that dies mid-read is a row too — triage never drops a
service quietly.
- **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), under a COVERAGE FLOOR that CI enforces (kit 95/90,
Expand Down
3 changes: 3 additions & 0 deletions ui/console/e2e/a11y.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { expect, test } from "@playwright/test";
const service = process.env.GOLDPATH_SERVICE_URL ?? "http://localhost:5310";

const PANELS = [
// The landing screen is checked too: it is the one an operator sees first, at 3am.
{ nav: "Today", ready: "triage-home" },
{ nav: "Runs", ready: "run-console" },
{ nav: "Bulk intake", ready: "bulk-panel" },
{ nav: "Campaigns", ready: "campaign-panel" },
Expand Down Expand Up @@ -40,6 +42,7 @@ test.describe("the console is operable without a mouse or a perfect screen", ()

test("a confirm dialog is opened, dismissed and handed back by keyboard alone", async ({ page }) => {
await page.goto(`/?base=${encodeURIComponent(service)}`);
await page.getByRole("button", { name: "Runs" }).click();
await expect(page.getByTestId("run-console")).toBeVisible();

// Opened from the KEYBOARD — the mouse is not assumed anywhere in this journey.
Expand Down
41 changes: 41 additions & 0 deletions ui/console/e2e/console.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ test.describe("the run console against a real Goldpath app", () => {

test("triggers a job, watches the run finish, and replays its repair item", async ({ page }) => {
await page.goto(`/?base=${encodeURIComponent(service)}`);
await page.getByRole("button", { name: "Runs" }).click();

// The fleet appears because the executor exists (zero-config discovery).
await expect(page.getByRole("button", { name: /console-smoke/ })).toBeVisible();
Expand All @@ -41,6 +42,7 @@ test.describe("the run console against a real Goldpath app", () => {
const smokeRun = page.getByRole("row", { name: /SmokeJob/ }).first();
await expect(async () => {
await page.goto(`/?base=${encodeURIComponent(service)}`);
await page.getByRole("button", { name: "Runs" }).click();
await expect(smokeRun).toContainText("Completed", { timeout: 5_000 });
}).toPass({ timeout: 60_000 });

Expand Down Expand Up @@ -323,10 +325,16 @@ test.describe("the run console against a real Goldpath app", () => {
test("behind the auth floor: every surface is NAMED as forbidden, never hidden", async ({ page }) => {
await page.goto(`/?base=${encodeURIComponent(secured)}`);

// TRIAGE says it first: the console cannot see this service, which during an incident
// is the most important thing an operator can be told.
await expect(page.getByTestId("triage-home")).toContainText("surfaces on");
await expect(page.getByTestId("triage-home")).toContainText("cannot be read");

// The modules ARE composed here; the operator simply has no principal. Hiding them
// would tell the operator this app has no admin surface, which is false.
await expect(page.getByRole("button", { name: "Runs" })).toBeVisible();
await expect(page.getByRole("button", { name: "Bulk intake" })).toBeVisible();
await page.getByRole("button", { name: "Runs" }).click();
await expect(page.getByRole("alert")).toContainText("lacks the ops role");
await expect(page.getByTestId("run-console")).toHaveCount(0);
await expect(page.getByText(/No Goldpath admin surface answered here/)).toHaveCount(0);
Expand All @@ -337,7 +345,9 @@ test.describe("the run console against a real Goldpath app", () => {

// R1: no ambient tenant → the app refuses (400). The console must repeat that, not
// silently downgrade a composed module to "absent".
await expect(page.getByTestId("triage-home")).toContainText("cannot be read");
await expect(page.getByRole("button", { name: "Runs" })).toBeVisible();
await page.getByRole("button", { name: "Runs" }).click();
const banner = page.getByRole("alert");
await expect(banner).toContainText("composed here but refused this request");
await expect(banner).toContainText(/tenant/i);
Expand All @@ -346,6 +356,7 @@ test.describe("the run console against a real Goldpath app", () => {

test("a service that dies MID-SESSION is reported, not papered over", async ({ page }) => {
await page.goto(`/?base=${encodeURIComponent(service)}`);
await page.getByRole("button", { name: "Runs" }).click();
await expect(page.getByTestId("run-console")).toBeVisible();

// The console discovered a healthy service; now the service stops answering.
Expand All @@ -365,6 +376,10 @@ test.describe("the run console against a real Goldpath app", () => {
// console reads theirs.
await page.goto("/");

// The landing screen is TODAY; the modules are one click away, never the front door.
await expect(page.getByTestId("triage-home")).toBeVisible();
await page.getByRole("button", { name: "Runs" }).click();

const picker = page.getByLabel(/service/i);
await expect(picker).toHaveValue("open");
await expect(page.getByTestId("run-console")).toBeVisible();
Expand All @@ -384,4 +399,30 @@ test.describe("the run console against a real Goldpath app", () => {
await picker.selectOption("open");
await expect(page.getByTestId("run-console")).toBeVisible();
});

test("triage: the first screen answers 'is anything wrong' and its rows are deep links", async ({ page }) => {
// Give the estate something to report: a batch waiting at the four-eyes gate.
const csv = "EndToEndId,Amount\nE2E-T1,10.00\nE2E-T2,20.00\n";
const uploaded = await page.request.post(`${service}/goldpath/admin/bulk/batches/payments?fileName=triage.csv`, {
headers: { "content-type": "application/octet-stream" },
data: csv,
});
expect(uploaded.ok()).toBeTruthy();

// The validate job is real; poll the TRIAGE screen until it reports the gate.
const gate = page.getByRole("button", { name: /awaiting approval in payments/ });
await expect(async () => {
await page.goto("/");
await expect(page.getByTestId("triage-home")).toBeVisible({ timeout: 5_000 });
await expect(gate).toBeVisible({ timeout: 5_000 });
}).toPass({ timeout: 90_000 });

// The screen says WHAT it read, so the numbers cannot be mistaken for the whole truth.
await expect(page.getByTestId("triage-home")).toContainText("most recent 50 rows");

// A row is a deep link: it opens the panel that owns it, on the service that owns it.
await gate.click();
await expect(page.getByTestId("bulk-panel")).toBeVisible();
await expect(page.getByLabel(/service/i)).toHaveValue("open");
});
});
105 changes: 0 additions & 105 deletions ui/console/src/Console.test.tsx

This file was deleted.

122 changes: 0 additions & 122 deletions ui/console/src/Console.tsx

This file was deleted.

Loading
Loading