core/workbench-ui/src/app/Shell.test.tsx
Shay 7cf78d97e0 feat(workbench): Wave M D1+D2 — guided determinism tour (provider-agnostic)
The "they'd want to use it" first-run surface: a curated narrative that makes
the engine's discipline legible to a newcomer — bring a claim from any model and
watch it get decided, refused, or replayed. Proposer authority ignored.

Honest by construction (the risk with a narrative surface is theater):
  - workbench/tour.py determinism_tour() is a curated ordered narrative BOUND to
    the real demo registry. Intro (provider-agnostic thesis) → three demo steps
    (deductive entailment decides only on engine+oracle agreement; epistemic
    truth-state refuses a wrong proposer; proof-carrying promotion ignores
    proposer authority) → payoff (replay-to-same-hash + the citable evidence
    bundle).
  - Each demo step's what_this_proves / what_this_does_not_prove cards and the
    demo title are PULLED FROM THE REAL DEMO SPEC — never re-authored — so the
    tour can never claim more than the demo it points at.
  - A step referencing a missing demo FAILS CLOSED (KeyError), not a dead link.
    Tests assert both (cards == spec; phantom demo raises).

Backend: schemas.py DeterminismTour/TourStep; GET /tour. schema-snapshot regen.
Frontend: /tour route registered in the registry (Determinism section, 14
routes; nav/palette/guide counts updated); TourRoute — thesis hero + ordered
step cards with the honesty cards + links to the real demos / replay.

Validation: 140 workbench Python tests incl. tour drift guards; 472/472 frontend
incl. routes/routeConformance/routes.docs (guide↔registry), Shell+palette counts
(14), schemaDrift, and the tour UI test; pnpm build clean; git diff --check
clean. No serving-path imports.
2026-06-13 19:39:41 -07:00

188 lines
6.2 KiB
TypeScript

import { render, screen, fireEvent } from "@testing-library/react";
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
import { MemoryRouter, Routes, Route } from "react-router-dom";
import { QueryClientProvider } from "@tanstack/react-query";
import { createTestQueryClient } from "../test/createTestQueryClient";
import { Shell } from "./Shell";
import { ChatRoute } from "../routes/ChatRoute";
import { ProposalsRoute } from "./proposals/ProposalsRoute";
import { setWorkbenchPref } from "./workbenchPrefs";
import type { RuntimeStatus } from "../types/api";
// Mock the API queries module
vi.mock("../api/queries", async (importOriginal) => {
const actual = await importOriginal<typeof import("../api/queries")>();
return {
...actual,
useRuntimeStatus: vi.fn(),
};
});
import { useRuntimeStatus } from "../api/queries";
const mockStatus: RuntimeStatus = {
backend: "numpy",
git_revision: "abcdef1234567890",
engine_state_present: true,
checkpoint_revision: "deadbeef12345678",
revision_warning: false,
active_session_id: null,
mutation_mode: "read_only",
};
function makeClient() {
return createTestQueryClient();
}
function renderShell(initialPath = "/chat") {
const client = makeClient();
return render(
<QueryClientProvider client={client}>
<MemoryRouter initialEntries={[initialPath]}>
<Routes>
<Route path="/" element={<Shell />}>
<Route path="chat" element={<ChatRoute />} />
<Route path="proposals" element={<ProposalsRoute />} />
</Route>
</Routes>
</MemoryRouter>
</QueryClientProvider>,
);
}
describe("Shell", () => {
afterEach(() => {
vi.unstubAllGlobals();
localStorage.clear();
});
beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(useRuntimeStatus).mockReturnValue({
data: mockStatus,
isLoading: false,
isError: false,
isPending: false,
isSuccess: true,
status: "success",
error: null,
} as any);
});
it("renders five named regions", () => {
renderShell();
expect(screen.getByRole("banner")).toBeInTheDocument(); // topbar is a header
expect(document.querySelector('[data-region="topbar"]')).toBeInTheDocument();
expect(document.querySelector('[data-region="leftnav"]')).toBeInTheDocument();
expect(document.querySelector('[data-region="main"]')).toBeInTheDocument();
expect(document.querySelector('[data-region="statusfooter"]')).toBeInTheDocument();
});
it("applies the persisted density mode to the shell", () => {
setWorkbenchPref("densityMode", "compact");
renderShell();
expect(document.querySelector('[data-density="compact"]')).toBeInTheDocument();
});
it("LeftNav has exactly 14 items in section-grouped order", () => {
renderShell();
const nav = document.querySelector('[data-region="leftnav"]')!;
const links = nav.querySelectorAll("a");
expect(links).toHaveLength(14);
const labels = Array.from(links).map((l) => l.textContent);
// Grouped by section (Converse → Cognition → Determinism → Evidence →
// Discipline → Substrate → Settings), derived from the route registry.
expect(labels).toEqual([
"Chat",
"Trace",
"Contemplation",
"Tour",
"Replay",
"Demos",
"Proposals",
"Runs",
"Vault",
"Audit",
"Evals",
"Calibration",
"Packs",
"Settings",
]);
});
it("clicking a nav item changes route (main shows new content)", () => {
vi.stubGlobal("fetch", vi.fn(() => new Promise(() => {})));
renderShell("/chat");
expect(screen.getByText("Ask CORE a question.")).toBeInTheDocument();
// Click Proposals nav link
const proposalsLink = screen.getByRole("link", { name: "Proposals" });
fireEvent.click(proposalsLink);
expect(screen.getByText("Loading proposal queue...")).toBeInTheDocument();
});
it("StatusFooter shows mutation_mode Read Only label for read_only", () => {
renderShell();
const footer = document.querySelector('[data-region="statusfooter"]')!;
expect(footer.textContent).toContain("Read Only");
expect(footer.textContent).not.toContain("Runtime Turn");
});
it("StatusFooter shows git_revision short SHA", () => {
renderShell();
const el = document.querySelector('[data-testid="git-revision"]');
expect(el).toBeInTheDocument();
expect(el!.textContent).toBe("abcdef12");
});
it("StatusFooter shows checkpoint_revision short SHA", () => {
renderShell();
const el = document.querySelector('[data-testid="checkpoint-revision"]');
expect(el).toBeInTheDocument();
expect(el!.textContent).toBe("deadbeef");
});
it("mutation_mode runtime_turn renders amber styling and Runtime Turn label", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(useRuntimeStatus).mockReturnValue({
data: { ...mockStatus, mutation_mode: "runtime_turn" },
isLoading: false,
isError: false,
isPending: false,
isSuccess: true,
status: "success",
error: null,
} as any);
renderShell();
const mutationEl = document.querySelector('[data-testid="mutation-mode"]')!;
expect(mutationEl.textContent).toBe("Runtime Turn");
// Should use warning color class (amber)
expect(mutationEl.className).toContain("warning");
});
it("revision_warning=true makes checkpoint_revision show warning attr, clicking expands note", () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
vi.mocked(useRuntimeStatus).mockReturnValue({
data: { ...mockStatus, revision_warning: true },
isLoading: false,
isError: false,
isPending: false,
isSuccess: true,
status: "success",
error: null,
} as any);
renderShell();
const cpBtn = document.querySelector('[data-testid="checkpoint-revision"]')!;
expect(cpBtn.getAttribute("data-warning")).toBe("true");
// Click to expand note
fireEvent.click(cpBtn);
const note = document.querySelector('[data-testid="revision-note"]');
expect(note).toBeInTheDocument();
expect(note!.textContent).toContain("ADR-0157 / ADR-0158");
});
});