core/workbench-ui/src/app/EvidenceChainRail.test.tsx
Shay 9344f11415 feat(workbench): design mastery pass — chain rail, typography, doctrine-as-tests (Wave R R1)
- EvidenceChainRail: the spine's seven stages rendered per subject with
  the honesty contract — lit/hollow/dim derive ONLY from carried fields
  (a trace hash lights 'replay' as RECORDED, never as verified); pure
  deriveStages() with meaningfully-fail tests (remove one field -> exactly
  its stage hollows).
- Hash display standard: DigestBadge default 12-char + copy everywhere;
  CopyableHash consolidated away (2 call sites swapped, file deleted).
- Typography: tabular-nums on MetadataTable values + eval metrics;
  text-wrap balance on Panel titles + EmptyState statements.
- Selection unification: new --color-selected-bg/-border tokens; selected
  (persistent) vs focused (transient) now visually distinct across
  Proposals/Replay/Evals lists; focus-ring no longer doubles as selection.
- EmptyState gains a deterministic monochrome inline-SVG glyph.
- Panel adopted in Evals lane list + Proposals queue.
- Doctrine-as-tests: hexScan (no palette literals outside tokens.css/
  generated tokens.ts) + schemaDrift gate (scripts/dump-schemas.py AST
  walk -> schema-snapshot.json -> every dataclass field mirrored in
  types/api.ts; explicit shrink-only NOT_YET_MIRRORED debt list).
  The drift gate caught real drift on first run: ChatTurnResult.turn_id
  existed in Python, missing from TS — now mirrored.

Verified: build green; vitest 36 files / 279 tests EXIT=0; playwright
12/12; dump-schemas.py runs clean.
2026-06-12 13:18:44 -07:00

107 lines
4.2 KiB
TypeScript

import { render } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { EvidenceChainRail, deriveStages } from "./EvidenceChainRail";
import type { EvidenceSubject } from "./evidenceContext";
import type { ChatTurnResult, ProposalDetail } from "../types/api";
const FULL_TURN: ChatTurnResult = {
prompt: "What is alpha?",
surface: "alpha causes beta",
articulation_surface: "alpha causes beta",
walk_surface: "alpha -> beta",
grounding_source: "teaching",
epistemic_state: "decoded",
normative_clearance: "cleared",
normative_detail: "",
trace_hash: "sha256:abc123",
refusal_emitted: false,
hedge_injected: false,
mutation_mode: "runtime_turn",
identity_verdict: null,
safety_verdict: null,
ethics_verdict: null,
proposal_candidates: [],
turn_cost_ms: 17,
checkpoint_emitted: true,
};
function turnSubject(data: ChatTurnResult | undefined): EvidenceSubject {
return { kind: "turn", turnId: 1, data } as EvidenceSubject;
}
function statusOf(subject: EvidenceSubject, stageId: string): string {
const stages = deriveStages(subject);
const found = stages?.find((s) => s.id === stageId);
if (!found) throw new Error(`stage not derived: ${stageId}`);
return found.status;
}
describe("EvidenceChainRail honesty contract", () => {
it("a fully-evidenced turn lights exactly the applicable stages", () => {
const subject = turnSubject(FULL_TURN);
expect(statusOf(subject, "intent")).toBe("lit");
expect(statusOf(subject, "subject")).toBe("lit");
expect(statusOf(subject, "provenance")).toBe("lit");
expect(statusOf(subject, "admissibility")).toBe("lit");
expect(statusOf(subject, "replay")).toBe("lit");
expect(statusOf(subject, "authority")).toBe("lit");
expect(statusOf(subject, "action")).toBe("dim");
});
it("MEANINGFULLY FAILS: removing a single field hollows exactly its stage", () => {
// This is the test that breaks if anyone makes the rail infer status
// instead of deriving it from the named field.
const noHash = turnSubject({ ...FULL_TURN, trace_hash: "" });
expect(statusOf(noHash, "replay")).toBe("hollow");
expect(statusOf(noHash, "provenance")).toBe("lit");
const noGrounding = turnSubject({ ...FULL_TURN, grounding_source: "" as ChatTurnResult["grounding_source"] });
expect(statusOf(noGrounding, "provenance")).toBe("hollow");
expect(statusOf(noGrounding, "replay")).toBe("lit");
});
it("identity-only subject (detail not loaded): applicable stages are hollow, never lit", () => {
const subject = turnSubject(undefined);
for (const id of ["intent", "provenance", "admissibility", "replay", "authority"]) {
expect(statusOf(subject, id)).toBe("hollow");
}
expect(statusOf(subject, "subject")).toBe("lit");
});
it("proposal: replay_equivalent=false is still recorded evidence (lit), null is hollow", () => {
const base: ProposalDetail = {
proposal_id: "p-1",
state: "pending",
source_kind: "contemplation",
replay_equivalent: false,
created_at: null,
downstream_effect: "unknown",
proposed_chain: [],
provenance: null,
suggested_cli: null,
} as unknown as ProposalDetail;
const recorded: EvidenceSubject = { kind: "proposal", proposalId: "p-1", data: base } as EvidenceSubject;
expect(statusOf(recorded, "replay")).toBe("lit");
const unrecorded: EvidenceSubject = {
kind: "proposal",
proposalId: "p-1",
data: { ...base, replay_equivalent: null },
} as EvidenceSubject;
expect(statusOf(unrecorded, "replay")).toBe("hollow");
});
it("renders no rail for kind=none", () => {
const { container } = render(<EvidenceChainRail subject={{ kind: "none" }} />);
expect(container.querySelector('[data-testid="evidence-chain-rail"]')).toBeNull();
});
it("renders all seven stages with status data attributes", () => {
const { container } = render(<EvidenceChainRail subject={turnSubject(FULL_TURN)} />);
const items = container.querySelectorAll("[data-stage]");
expect(items.length).toBe(7);
expect(container.querySelectorAll('[data-status="lit"]').length).toBe(6);
expect(container.querySelectorAll('[data-status="dim"]').length).toBe(1);
});
});