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
9 changes: 4 additions & 5 deletions .github/workflows/ci-triage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
# every open PR — this has cost debugging time before).
# - needs investigation: everything else.
#
# SHIPPED INERT: this event-triggered workflow does nothing until the repo variable
# CI_TRIAGE_ENABLED == "true". It never runs PR-authored code — it only reads job
# metadata via the trusted default-branch checkout, and
# posts a comment with the built-in token.
# Enabled by default. Set repo variable CI_TRIAGE_ENABLED=false to disable.
# It never runs PR-authored code — it only reads job metadata via the trusted
# default-branch checkout, and posts a comment with the built-in token.
name: CI Triage

on:
Expand All @@ -32,7 +31,7 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 10
if: >
vars.CI_TRIAGE_ENABLED == 'true' &&
vars.CI_TRIAGE_ENABLED != 'false' &&
github.event.workflow_run.conclusion == 'failure' &&
github.event.workflow_run.event == 'pull_request'
steps:
Expand Down
60 changes: 31 additions & 29 deletions docs/branch-review-ledger.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/outstanding-issues.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/process-hardening.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ the durable index for the tooling; `docs/operator-backlog.md` tracks the human-o
(`INGESTION_AUTOPILOT_APPLY` unset → read-only); flip that repo var to `true` after a clean dry-run to
allow real recovery.
- **CI failure triage** (`.github/workflows/ci-triage.yml`): on PR CI failure, classifies each failed job
as main-side or needs-investigation. Inert until repo var `CI_TRIAGE_ENABLED=true` (now set). UI jobs use
as main-side or needs-investigation. Enabled by default; set repo var `CI_TRIAGE_ENABLED=false` to disable. UI jobs use
their uploaded JUnit classification and trace; job names alone never produce a known-flake verdict.
The workflow reads only trusted default-branch job metadata and never runs PR code.
- **PR metadata policy** (`.github/workflows/pr-policy.yml`, `scripts/pr-policy.mjs`): ready PRs to `main`
Expand Down
3 changes: 2 additions & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ The parser walks back over comma-continued selector lines, so a selector list sp
**Pixel baselines (`tests/ui-visual-baseline.spec.ts`) — advisory.** Run by
`playwright.visual.config.ts`, which also still runs the older attach-only
`ui-visual-artifacts.spec.ts`. Three constraints are deliberate: never `fullPage` (under CI load
Next.js leaves a hidden duplicate page root in the stream — ledger #093 — so a whole-page capture can
Next.js can leave a hidden duplicate page root in the stream — ledger #093, mitigated for interactive
tests via `visibleByTestId` in `tests/playwright-settlement.ts` — so a whole-page capture can still
contain the layout twice; every target is clipped to a locator), demo mode only (the Playwright
runner forces `NEXT_PUBLIC_DEMO_MODE` and offline providers, so content is stable between runs), and
motion off with carets hidden.
Expand Down
16 changes: 15 additions & 1 deletion tests/playwright-settlement.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, type Locator } from "playwright/test";
import { expect, type Locator, type Page } from "playwright/test";

/**
* Wait for a hydrating/portalling surface to converge to exactly one visible
Expand Down Expand Up @@ -29,3 +29,17 @@ export async function expectSingleSettledOwner(

return locator.first();
}

/**
* Scope a testid to the visible DOM owner (#093).
*
* Next streaming can leave a hidden duplicate page root in the tree under
* full-suite load. Bare `page.getByTestId(...)` then trips Playwright strict
* mode; bare `.first()` can pin the hidden clone. Prefer this helper (or
* pad-scoping under `mobile-composer-reserve-pad`) for page-root / shell
* surfaces. Use `expectSingleSettledOwner` when the duplicate must fully
* disappear rather than merely be ignored while hidden.
*/
export function visibleByTestId(page: Page, testId: string): Locator {
return page.getByTestId(testId).filter({ visible: true });
}
3 changes: 2 additions & 1 deletion tests/ui-accessibility.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, test, type Page, type TestInfo } from "playwright/test";
import { stubZeroTouchPoints } from "./helpers/zero-touch";
import { visibleByTestId } from "./playwright-settlement";

const readySetupChecks = [
{ id: "env", label: ".env.local configured", status: "ready", detail: "Test environment ready." },
Expand Down Expand Up @@ -459,7 +460,7 @@ test.describe("Clinical KB accessibility coverage", () => {
await expect(differentialSubmit).toBeEnabled({ timeout: 2_000 });
}).toPass({ timeout: 30_000 });
await differentialSubmit.click();
await expect(page.getByTestId("differentials-search-results")).toBeVisible();
await expect(visibleByTestId(page, "differentials-search-results")).toBeVisible();

const filterSelect = page.getByTestId("differential-result-type-select");
await expect(filterSelect).toBeVisible();
Expand Down
6 changes: 3 additions & 3 deletions tests/ui-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
readPrimaryScrollGeometry,
scrollPrimarySurface,
} from "./playwright-scroll";
import { expectSingleSettledOwner } from "./playwright-settlement";
import { expectSingleSettledOwner, visibleByTestId } from "./playwright-settlement";
import { answerThreadStorageKey } from "../src/lib/answer-thread-storage";
import { documentSummaryQuestion } from "../src/lib/answer-contract";
import { demoAnswer, demoDocuments, demoSummary, getDemoDocument, getDemoDocumentPayload } from "../src/lib/demo-data";
Expand Down Expand Up @@ -2938,7 +2938,7 @@ test.describe("Clinical KB UI smoke coverage", () => {
await mockDemoApi(page);
await gotoApp(page, "/differentials?q=acute+confusion&focus=1&run=1");

await expect(page.getByTestId("differentials-search-results")).toBeVisible({ timeout: 30_000 });
await expect(visibleByTestId(page, "differentials-search-results")).toBeVisible({ timeout: 30_000 });
await expect(page.getByRole("button", { name: "Mode Differentials" })).toBeVisible();
await expect(page.getByTestId("differentials-home")).toHaveCount(0);
});
Expand Down Expand Up @@ -3058,7 +3058,7 @@ test.describe("Clinical KB UI smoke coverage", () => {
await expect(page.getByRole("heading", { name: "Favourites command library" })).toBeVisible();
await expect(page.getByTestId("favourites-item-workspace")).toHaveCount(0);

await page.getByTestId("favourite-row-lithium-monitoring-guideline").locator("button[aria-pressed]").click();
await visibleByTestId(page, "favourite-row-lithium-monitoring-guideline").locator("button[aria-pressed]").click();
const workspace = page.getByTestId("favourites-item-workspace");
await expect(workspace).toBeVisible();
await expect(workspace.getByRole("heading", { name: "Lithium monitoring guideline", level: 3 })).toBeVisible();
Expand Down
26 changes: 13 additions & 13 deletions tests/ui-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
readPrimaryScrollGeometry,
scrollPrimarySurface,
} from "./playwright-scroll";
import { expectSingleSettledOwner } from "./playwright-settlement";
import { expectSingleSettledOwner, visibleByTestId } from "./playwright-settlement";

const readySetupChecks = [
{ id: "env", label: ".env.local configured", status: "ready", detail: "Test environment ready." },
Expand Down Expand Up @@ -568,7 +568,7 @@ test.describe("Clinical KB tools launcher", () => {
await formsMode.click();
await expect(page).toHaveURL(/\/forms$/, { timeout: 20_000 });
await expect(page.getByRole("button", { name: "Mode Forms" })).toBeVisible();
await expect(page.getByTestId("forms-home")).toBeVisible();
await expect(visibleByTestId(page, "forms-home")).toBeVisible();
await expect(page.getByTestId("form-search-results")).toHaveCount(0);
await expect(visibleGlobalSearchInput(page)).toHaveValue("");
await expectNoPageHorizontalOverflow(page);
Expand All @@ -590,14 +590,14 @@ test.describe("Clinical KB tools launcher", () => {

await expect(page).toHaveURL(/\/forms$/);
await expect(page.getByRole("button", { name: "Mode Forms" })).toBeVisible();
await expect(page.getByTestId("forms-home")).toBeVisible();
await expect(visibleByTestId(page, "forms-home")).toBeVisible();
await expect(page.getByTestId("form-search-results")).toHaveCount(0);
await expect(visibleGlobalSearchInput(page)).toHaveCount(1);
await expect(visibleGlobalSearchInput(page)).toHaveValue("");

await gotoLauncher(page, "/forms");
await expect(page.getByRole("button", { name: "Mode Forms" })).toBeVisible();
await expect(page.getByTestId("forms-home")).toBeVisible();
await expect(visibleByTestId(page, "forms-home")).toBeVisible();

menu = await openAppModeMenu(page, "Forms");
const servicesMode = menu.getByRole("menuitemradio", { name: /^Services\b/ });
Expand Down Expand Up @@ -1015,8 +1015,8 @@ test.describe("Clinical KB tools launcher", () => {
await expect(page.getByTestId("services-home").getByTestId("global-search-input")).toBeFocused();

await gotoLauncher(page, "/forms?focus=1");
await expect(page.getByTestId("forms-home").getByTestId("global-search-input")).toBeVisible();
await expect(page.getByTestId("forms-home").getByTestId("global-search-input")).toBeFocused();
await expect(visibleByTestId(page, "forms-home").getByTestId("global-search-input")).toBeVisible();
await expect(visibleByTestId(page, "forms-home").getByTestId("global-search-input")).toBeFocused();
});

test("services mode shows source-backed records in search results", async ({ page }) => {
Expand Down Expand Up @@ -1398,11 +1398,11 @@ test.describe("Clinical KB tools launcher", () => {

await expect(page).toHaveURL(/\/forms$/);
await expect(page.getByRole("button", { name: "Mode Forms" })).toBeVisible();
await expect(page.getByTestId("forms-home")).toBeVisible();
await expect(visibleByTestId(page, "forms-home")).toBeVisible();
await expect(page.getByRole("heading", { level: 1, name: "Forms" })).toBeVisible();
await expect(page.getByTestId("services-home")).toHaveCount(0);
await expect(page.getByTestId("global-search-input")).toHaveCount(1);
const formsHomeSearch = page.getByTestId("forms-home").getByTestId("global-search-input");
await expect(visibleByTestId(page, "forms-home").getByTestId("global-search-input")).toHaveCount(1);
const formsHomeSearch = visibleByTestId(page, "forms-home").getByTestId("global-search-input");
Comment thread
cursor[bot] marked this conversation as resolved.
await expect(formsHomeSearch).toBeVisible();
const formsSearchBox = await formsHomeSearch.boundingBox();
const formsHeadingBox = await page.getByRole("heading", { level: 1, name: "Forms" }).boundingBox();
Expand Down Expand Up @@ -1537,7 +1537,7 @@ test.describe("Clinical KB tools launcher", () => {

// Evidence arrived, so the results view renders — ranked from the imported
// differentials catalogue with a real query-matched result row.
await expect(page.getByTestId("differentials-search-results")).toBeVisible();
await expect(visibleByTestId(page, "differentials-search-results")).toBeVisible();
await expect(page.getByTestId("differentials-catalogue-notice")).toBeVisible();
await expect(page.getByText("Catalogue ranking").first()).toBeVisible();
await expect(page.getByRole("link", { name: "Delirium / Acute Confusion / Encephalopathy" }).first()).toBeVisible();
Expand Down Expand Up @@ -1594,7 +1594,7 @@ test.describe("Clinical KB tools launcher", () => {
await gotoLauncher(page, "/differentials");
await submitDifferentialSearch(page, "acute confusion");

await expect(page.getByTestId("differentials-search-results")).toBeVisible();
await expect(visibleByTestId(page, "differentials-search-results")).toBeVisible();
const typeSelect = page.getByTestId("differential-result-type-select");
await expect(typeSelect).toBeVisible();
await expect(typeSelect).toHaveAccessibleName("Filter by result type");
Expand Down Expand Up @@ -1695,7 +1695,7 @@ test.describe("Clinical KB tools launcher", () => {
await gotoLauncher(page, "/differentials");
await submitDifferentialSearch(page, "acute confusion");

await expect(page.getByTestId("differentials-search-results")).toBeVisible();
await expect(visibleByTestId(page, "differentials-search-results")).toBeVisible();
const typeSelect = page.getByTestId("differential-result-type-select");
await expect(typeSelect).toBeVisible();
await expect(typeSelect).toHaveAccessibleName("Filter by result type");
Expand Down Expand Up @@ -1824,7 +1824,7 @@ test.describe("Clinical KB tools launcher", () => {

const compareAction = page.getByTestId("differentials-compare-selected-mobile");
const dock = page.locator("form.answer-footer-search-dock");
const scrollport = page.getByTestId("differentials-search-results");
const scrollport = visibleByTestId(page, "differentials-search-results");
const mainContent = page.locator("#main-content");
await expect(scrollport).toBeVisible();
await expect(page.locator("#differentials-mobile-compare-addon-slot")).toHaveCount(1);
Expand Down
Loading