mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-08 08:11:17 -10:00
fix(review): proof-of-work markdown links open in the in-app drawer, not a full-page raw asset
FOUNDER UX fix (qitem-20260722234754-e8db7111): a proof-of-work file link in
the PROOF tab navigated the whole browser out of the SPA to the raw
/api/files/asset for guard/qa/rev1 .md verdicts. It now opens IN-APP in the
SharedDetailDrawer — rendering the C1 proof header (slice / candidate_sha /
artifact_type / verdict / money_evidence) + body — and closes cleanly back to
the review surface.
Fix (ProofTab.tsx only): the proof/ otherFiles list routes Markdown artifacts
(.md/.mdx, case-insensitive) through the existing FileLink primitive
(root=resolved.rootName, display path proof/<name>, readPath=<proofRel>/<name>)
instead of an <a target="_blank"> raw-asset anchor. Non-Markdown other files
(logs/video/binary) keep the browser-viewable raw-asset link; the image
gallery/lightbox, inline PROOF.md asset resolution, and lazy proof/ listing are
unchanged. The DELIVERED "see all proof" drill-in already opens in-drawer via
EvidenceOpener -> ArtifactsNavigator -> FileLink (no change needed there).
Tests (frozen pre-fix): proof-tab.test.tsx FX-1..FX-4 pin the in-app control,
the lazy slice-relative /read, the five-field C1 header + body, and
close-in-place with location/history unchanged; FX-5 preserves images/lightbox +
inline PROOF.md. artifacts-navigator.test.tsx pins the real delivered-see-all ->
EvidenceOpener('proof/') drill-in opening a C1 proof file in the drawer.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
1 parent
7d0997ddda
commit
44ef16c1b5
3 files changed
+343
-10
No files matched your search
@@ -24,6 +24,7 @@ import { useScopeMarkdown } from "../../hooks/useScopeMarkdown.js";
|
||||
import { MarkdownViewer } from "../markdown/MarkdownViewer.js";
|
||||
import { SectionHeader } from "../ui/section-header.js";
|
||||
import { EmptyState } from "../ui/empty-state.js";
|
||||
import { FileLink } from "../ui/FileLink.js";
|
||||
// OPR.0.4.4.20: Lightbox extracted (verbatim) for reuse by the Review surface.
|
||||
import { Lightbox } from "./Lightbox.js";
|
||||
|
||||
@@ -36,6 +37,11 @@ const VERDICT_TONE: Record<Verdict, string> = {
|
||||
};
|
||||
|
||||
const IMAGE_RE = /\.(png|jpe?g|gif|webp|avif|svg)$/i;
|
||||
// Markdown proof-of-work artifacts (guard/qa/rev1 verdicts) open IN-APP in the
|
||||
// SharedDetailDrawer via FileLink — rendering the C1 header + body — instead of a
|
||||
// full-page raw-asset navigation out of the SPA. Non-Markdown other files
|
||||
// (logs/video/binary) keep their existing browser-viewable raw-asset link.
|
||||
const MD_RE = /\.mdx?$/i;
|
||||
|
||||
/** Asset base for INLINE PROOF.md images — e.g. `` in the
|
||||
* Intent→Proof table, relative to the slice root where PROOF.md lives. MarkdownViewer
|
||||
@@ -212,14 +218,27 @@ function ProofSliceCard({
|
||||
<ul data-testid={`proof-files-${sliceId}`} className="mt-3 space-y-1 font-mono text-[10px]">
|
||||
{otherFiles.map((f) => (
|
||||
<li key={f.name}>
|
||||
<a
|
||||
href={fileAssetUrl(resolved!.rootName, `${proofRel}/${f.name}`)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-on-surface-variant underline decoration-outline-variant underline-offset-2 hover:text-on-surface"
|
||||
>
|
||||
proof/{f.name}
|
||||
</a>
|
||||
{MD_RE.test(f.name) ? (
|
||||
// Markdown proof artifact — open in the in-app drawer (C1 header +
|
||||
// body), not a full-page raw-asset navigation out of the SPA.
|
||||
<FileLink
|
||||
root={resolved!.rootName}
|
||||
path={`proof/${f.name}`}
|
||||
readPath={`${proofRel}/${f.name}`}
|
||||
className="text-on-surface-variant underline decoration-outline-variant underline-offset-2 hover:text-on-surface"
|
||||
>
|
||||
proof/{f.name}
|
||||
</FileLink>
|
||||
) : (
|
||||
<a
|
||||
href={fileAssetUrl(resolved!.rootName, `${proofRel}/${f.name}`)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-on-surface-variant underline decoration-outline-variant underline-offset-2 hover:text-on-surface"
|
||||
>
|
||||
proof/{f.name}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
// one is AC-3 (the lazy-load boundary — no eager file-body fetch, no tree pre-walk).
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, cleanup, waitFor, fireEvent } from "@testing-library/react";
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { render, screen, cleanup, waitFor, fireEvent, within } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ArtifactsNavigator } from "../src/components/project/ArtifactsNavigator.js";
|
||||
import { EvidenceOpener, type EvidenceContext } from "../src/components/review/EvidenceOpener.js";
|
||||
import { DrawerSelectionContext } from "../src/components/AppShell.js";
|
||||
import { SharedDetailDrawer, type DrawerSelection } from "../src/components/SharedDetailDrawer.js";
|
||||
|
||||
const mockFetch = vi.fn();
|
||||
globalThis.fetch = mockFetch as unknown as typeof fetch;
|
||||
@@ -33,6 +37,12 @@ const TREE: Record<string, Array<{ name: string; type: "dir" | "file" | "other";
|
||||
{ name: "README.md", type: "file", size: 4096, mtime: "2026-06-23T22:01:00.000Z" },
|
||||
{ name: "batch-1.change.diff", type: "file", size: 12288, mtime: "2026-06-23T22:52:00.000Z" },
|
||||
{ name: "03-story-dag.intent.png", type: "file", size: 129024, mtime: "2026-06-23T22:53:00.000Z" },
|
||||
{ name: "proof", type: "dir", size: null, mtime: "2026-06-23T22:52:00.000Z" },
|
||||
],
|
||||
// FOUNDER-FIX DELIVERED drill-in target — the slice proof/ folder EvidenceOpener
|
||||
// ('proof/') scopes ArtifactsNavigator to; its C1 file must open in the drawer.
|
||||
"missions/release-0.4.1/slices/15-workspace-ux/proof": [
|
||||
{ name: "guard.md", type: "file", size: 480, mtime: "2026-06-23T22:52:00.000Z" },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -55,10 +65,40 @@ function routeFiles({ rootsStatus = 200, rootsEmpty = false }: { rootsStatus?: n
|
||||
const path = u.searchParams.get("path") ?? "";
|
||||
return Promise.resolve(jsonResponse({ root: "work", path, entries: TREE[path] ?? [] }));
|
||||
}
|
||||
// File bodies — served ONLY when a file is opened (never on landing; AC-3 pins this).
|
||||
if (url.includes("/api/files/read")) {
|
||||
const u = new URL(url, "http://twin.local");
|
||||
const path = u.searchParams.get("path") ?? "";
|
||||
const content = FILE_BODIES[path];
|
||||
if (content == null) return Promise.resolve(jsonResponse({ error: "not found" }, 404));
|
||||
return Promise.resolve(jsonResponse({ root: "work", path, absolutePath: `/ws/${path}`, content, mtime: "2026-06-23T22:01:00.000Z", contentHash: "h", size: content.length }));
|
||||
}
|
||||
return Promise.resolve(jsonResponse({}, 404));
|
||||
};
|
||||
}
|
||||
|
||||
// C1 proof contract (docs/reference/sdlc-conventions.md §5) — five valid fields
|
||||
// (artifact_type in the closed set) + a distinctive body, so the DELIVERED drawer
|
||||
// render is asserted against real proof content, never a false green.
|
||||
const GUARD_C1 = [
|
||||
"---",
|
||||
"slice: slice-15-workspace-ux",
|
||||
"candidate_sha: 7d0997dddaab59f43bcc658fe2c0457128a64f53",
|
||||
"artifact_type: guard",
|
||||
"verdict: PASS",
|
||||
"money_evidence: delivered see-all proof/ drill-in opens guard.md in the drawer",
|
||||
"---",
|
||||
"",
|
||||
"# Guard Verdict",
|
||||
"",
|
||||
"DELIVERED-DRILL-IN-BODY: the proof file opened in the in-app drawer.",
|
||||
].join("\n");
|
||||
|
||||
// Opened-file bodies keyed by slice-relative read path (drawer content).
|
||||
const FILE_BODIES: Record<string, string> = {
|
||||
"missions/release-0.4.1/slices/15-workspace-ux/proof/guard.md": GUARD_C1,
|
||||
};
|
||||
|
||||
function listedPaths(): string[] {
|
||||
return calls
|
||||
.filter((c) => c.includes("/api/files/list"))
|
||||
@@ -168,4 +208,93 @@ describe("OPR.0.4.1.21 — Artifacts navigator", () => {
|
||||
// NOT the misleading out-of-scope state.
|
||||
expect(screen.queryByTestId("artifacts-navigator-no-scope")).toBeNull();
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// FOUNDER FIX (qitem-20260722234754-e8db7111) — DELIVERED proof/ drill-in leg.
|
||||
// The SECOND founder-named site: DELIVERED "see all proof" -> the REAL
|
||||
// EvidenceOpener('proof/') folder control -> ArtifactsNavigator scoped to the
|
||||
// slice proof/ dir, whose C1 file rows route through FileLink -> the drawer.
|
||||
// GREEN preservation: this caller already opens IN-APP, so no production change
|
||||
// is needed here — this pins it against regression through the real path.
|
||||
// -------------------------------------------------------------------------
|
||||
it("DELIVERED preservation: delivered-see-all -> EvidenceOpener('proof/') opens a proof C1 file IN the drawer, not a full-page asset", async () => {
|
||||
mockFetch.mockImplementation(routeFiles());
|
||||
const ctx: EvidenceContext = {
|
||||
root: "work",
|
||||
relPath: "missions/release-0.4.1/slices/15-workspace-ux",
|
||||
slicePath: "/ws/missions/release-0.4.1/slices/15-workspace-ux",
|
||||
};
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<DrawerHost>
|
||||
<EvidenceOpener evidenceRef="proof/" ctx={ctx} testId="delivered-see-all" />
|
||||
</DrawerHost>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
// pin the exact SPA location + history depth — a raw-asset navigation mutates these.
|
||||
const hrefBefore = window.location.href;
|
||||
const historyBefore = window.history.length;
|
||||
|
||||
// the DELIVERED "see all proof" folder control is the real founder-named caller.
|
||||
const folderBtn = screen.getByTestId("delivered-see-all-folder");
|
||||
expect(calls.some((c) => c.includes("/api/files/read"))).toBe(false); // lazy: nothing read yet
|
||||
fireEvent.click(folderBtn);
|
||||
|
||||
// the proof/ folder drills into the navigator; its C1 file row is an in-app control.
|
||||
const openCtrl = await screen.findByTestId("artifacts-file-open-guard.md");
|
||||
expect(openCtrl.closest("a")).toBeNull();
|
||||
expect(screen.queryByTestId("file-viewer")).toBeNull();
|
||||
|
||||
fireEvent.click(openCtrl);
|
||||
|
||||
const viewer = await screen.findByTestId("file-viewer");
|
||||
// reads the exact slice proof path under the work root — never the /asset escape.
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
calls.some(
|
||||
(c) =>
|
||||
c.includes("/api/files/read") &&
|
||||
c.includes("root=work") &&
|
||||
c.includes("15-workspace-ux%2Fproof%2Fguard.md"),
|
||||
),
|
||||
).toBe(true),
|
||||
);
|
||||
expect(calls.some((c) => c.includes("/api/files/asset"))).toBe(false);
|
||||
// valid C1 header (five fields) + distinctive body render in the drawer.
|
||||
const fm = within(viewer).getByTestId("markdown-frontmatter");
|
||||
for (const field of ["slice", "candidate_sha", "artifact_type", "verdict", "money_evidence"]) {
|
||||
expect(within(fm).getByText(field)).toBeTruthy();
|
||||
}
|
||||
expect(within(fm).getByText("guard")).toBeTruthy();
|
||||
expect(screen.getByText(/DELIVERED-DRILL-IN-BODY/)).toBeTruthy();
|
||||
|
||||
// close in place — the opener + drilled folder remain; location/history unmoved.
|
||||
fireEvent.pointerDown(screen.getByTestId("shared-detail-drawer-outside"));
|
||||
await waitFor(() => expect(screen.queryByTestId("file-viewer")).toBeNull());
|
||||
expect(screen.getByTestId("delivered-see-all-folder")).toBeTruthy();
|
||||
expect(screen.getByTestId("artifacts-file-open-guard.md")).toBeTruthy();
|
||||
expect(window.location.href).toBe(hrefBefore);
|
||||
expect(window.history.length).toBe(historyBefore);
|
||||
});
|
||||
});
|
||||
|
||||
// Minimal real drawer host — mirrors AppShell's DrawerSelection provider +
|
||||
// SharedDetailDrawer so a FileLink click actually opens the drawer end-to-end.
|
||||
function DrawerHost({ children }: { children: ReactNode }) {
|
||||
const [selection, setSelection] = useState<DrawerSelection>(null);
|
||||
return (
|
||||
<DrawerSelectionContext.Provider value={{ selection, setSelection }}>
|
||||
{children}
|
||||
<SharedDetailDrawer
|
||||
selection={selection}
|
||||
onClose={() => setSelection(null)}
|
||||
events={[]}
|
||||
selectedDiscoveredId={null}
|
||||
onSelectDiscoveredId={() => {}}
|
||||
placementTarget={null}
|
||||
onClearPlacement={() => {}}
|
||||
/>
|
||||
</DrawerSelectionContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -5,9 +5,12 @@
|
||||
// and read-only (no /write).
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, cleanup, waitFor } from "@testing-library/react";
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { render, screen, cleanup, waitFor, within, fireEvent } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ScopeProofRollup, SliceProofTab } from "../src/components/project/ProofTab.js";
|
||||
import { DrawerSelectionContext } from "../src/components/AppShell.js";
|
||||
import { SharedDetailDrawer, type DrawerSelection } from "../src/components/SharedDetailDrawer.js";
|
||||
|
||||
const mockFetch = vi.fn();
|
||||
globalThis.fetch = mockFetch as unknown as typeof fetch;
|
||||
@@ -45,6 +48,39 @@ const SLICES: Record<string, { proofMd?: string; proofEntries?: Array<{ name: st
|
||||
proofMd: "# PROOF — OPR.0.4.1.18 Queue summary\n\nClosed by: <seat> Date: <date> Verdict: <pass | pass-with-residue | ...>\n\n## What this proves\n\n<1-3 sentences>",
|
||||
proofEntries: [],
|
||||
},
|
||||
// FOUNDER-FIX drawer slice — a markdown proof-of-work file (guard.md) that must
|
||||
// open in the in-app drawer, alongside an image capture that stays in the gallery
|
||||
// + an inline PROOF.md image (both preserved on the /api/files/asset path).
|
||||
"missions/m/slices/20-drawer": {
|
||||
proofMd: "# OPR.0.4.1.20 Proof\n\n**Verdict: PASS** · 2026-07-22\n\n## Intent -> Proof\n\n",
|
||||
proofEntries: [
|
||||
{ name: "guard.md", type: "file", size: 480, mtime: "2026-07-22T22:00:00.000Z" },
|
||||
{ name: "shot.png", type: "file", size: 120000, mtime: "2026-07-22T22:01:00.000Z" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// C1 proof contract (docs/reference/sdlc-conventions.md §5): five frontmatter
|
||||
// fields + a distinctive body, so the drawer render is asserted against real
|
||||
// proof content (header AND body), never a false green.
|
||||
const GUARD_C1 = [
|
||||
"---",
|
||||
"slice: slice-04-review-tab-observability",
|
||||
"candidate_sha: 7d0997dddaab59f43bcc658fe2c0457128a64f53",
|
||||
"artifact_type: guard",
|
||||
"verdict: PASS",
|
||||
"money_evidence: proof/guard.md opens in the in-app drawer",
|
||||
"---",
|
||||
"",
|
||||
"# Guard Verdict — slice-04",
|
||||
"",
|
||||
"DISTINCTIVE-BODY-MARKER: the guard proof-of-work rendered inside the drawer.",
|
||||
].join("\n");
|
||||
|
||||
// Proof-of-work file bodies (proof/<name>, NOT PROOF.md) keyed by slice-relative
|
||||
// read path — served by the /api/files/read route below only when opened.
|
||||
const PROOF_FILES: Record<string, string> = {
|
||||
"missions/m/slices/20-drawer/proof/guard.md": GUARD_C1,
|
||||
};
|
||||
|
||||
function routeFiles(input: unknown) {
|
||||
@@ -55,6 +91,11 @@ function routeFiles(input: unknown) {
|
||||
}
|
||||
if (url.includes("/api/files/read")) {
|
||||
const path = new URL(url, "http://t.local").searchParams.get("path") ?? "";
|
||||
// proof-of-work file read (proof/<name>, not PROOF.md) — the drawer content.
|
||||
const proofFile = PROOF_FILES[path];
|
||||
if (proofFile != null) {
|
||||
return Promise.resolve(jsonResponse({ root: "work", path, absolutePath: `/ws/${path}`, content: proofFile, mtime: "2026-07-22T22:05:00.000Z", contentHash: "pf", size: proofFile.length }));
|
||||
}
|
||||
const slice = path.replace(/\/PROOF\.md$/, "");
|
||||
const md = SLICES[slice]?.proofMd;
|
||||
if (md == null) return Promise.resolve(jsonResponse({ error: "not found" }, 404));
|
||||
@@ -158,3 +199,147 @@ describe("OPR.0.4.1.23 — PROOF tab", () => {
|
||||
expect(screen.getByTestId("proof-verdict-OPR.0.4.1.16").textContent).toBe("PASS");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FOUNDER FIX (qitem-20260722234754-e8db7111): proof-of-work file links must
|
||||
// open IN-APP in the SharedDetailDrawer — rendering the C1 proof content — NOT
|
||||
// navigate the whole browser to the raw /api/files/asset (a full-page escape
|
||||
// out of the SPA). These tests mount the REAL drawer stack (DrawerSelection +
|
||||
// SharedDetailDrawer + FileViewer) so a click exercises the true in-app path,
|
||||
// and pin the preservation of images/lightbox + inline PROOF.md and the lazy
|
||||
// no-read-before-click boundary. RED against ProofTab.tsx's raw <a target=_blank>.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Minimal real drawer host — mirrors AppShell's DrawerSelection provider +
|
||||
// SharedDetailDrawer so a FileReferenceTrigger click actually opens the drawer.
|
||||
function DrawerHost({ children }: { children: ReactNode }) {
|
||||
const [selection, setSelection] = useState<DrawerSelection>(null);
|
||||
return (
|
||||
<DrawerSelectionContext.Provider value={{ selection, setSelection }}>
|
||||
{children}
|
||||
<SharedDetailDrawer
|
||||
selection={selection}
|
||||
onClose={() => setSelection(null)}
|
||||
events={[]}
|
||||
selectedDiscoveredId={null}
|
||||
onSelectDiscoveredId={() => {}}
|
||||
placementTarget={null}
|
||||
onClearPlacement={() => {}}
|
||||
/>
|
||||
</DrawerSelectionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const DRAWER_SLICE = { id: "OPR.0.4.1.20", path: "/ws/missions/m/slices/20-drawer" };
|
||||
// The canonical slice-relative proof path the drawer must read (URL-encoded).
|
||||
const GUARD_READ = "missions%2Fm%2Fslices%2F20-drawer%2Fproof%2Fguard.md";
|
||||
|
||||
function renderSliceInDrawer() {
|
||||
const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
return render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<DrawerHost>
|
||||
<SliceProofTab sliceId={DRAWER_SLICE.id} title="brief" slicePath={DRAWER_SLICE.path} />
|
||||
</DrawerHost>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("PROOF tab — proof file opens in the in-app drawer (founder fix e8db7111)", () => {
|
||||
beforeEach(() => {
|
||||
mockFetch.mockReset();
|
||||
mockFetch.mockImplementation(routeFiles);
|
||||
calls = [];
|
||||
});
|
||||
afterEach(() => cleanup());
|
||||
|
||||
it("FX-1 (RED): a markdown proof link is an in-app control, not a full-page raw-asset anchor", async () => {
|
||||
renderSliceInDrawer();
|
||||
const list = await screen.findByTestId(`proof-files-${DRAWER_SLICE.id}`);
|
||||
const label = within(list).getByText("proof/guard.md");
|
||||
// desired: an in-app button (FileReferenceTrigger), NOT an <a> that escapes the SPA.
|
||||
expect(label.closest("button")).toBeTruthy();
|
||||
expect(label.closest("a")).toBeNull();
|
||||
// no full-page raw-asset escape anywhere in the proof-files list.
|
||||
expect(list.querySelector('a[target="_blank"]')).toBeNull();
|
||||
expect(list.querySelector('a[href*="/api/files/asset"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("FX-2 (RED): the read is LAZY — clicking opens the drawer and reads the slice-relative proof path exactly", async () => {
|
||||
renderSliceInDrawer();
|
||||
const list = await screen.findByTestId(`proof-files-${DRAWER_SLICE.id}`);
|
||||
// lazy: no read of guard.md before the click (only PROOF.md + the proof/ listing so far).
|
||||
expect(calls.some((c) => c.includes("/api/files/read") && c.includes("guard.md"))).toBe(false);
|
||||
expect(screen.queryByTestId("file-viewer")).toBeNull();
|
||||
|
||||
fireEvent.click(within(list).getByText("proof/guard.md"));
|
||||
|
||||
const viewer = await screen.findByTestId("file-viewer");
|
||||
// reads the canonical slice-relative proof path under the work root — the money assertion.
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
calls.some((c) => c.includes("/api/files/read") && c.includes("root=work") && c.includes(GUARD_READ)),
|
||||
).toBe(true),
|
||||
);
|
||||
expect(within(viewer).getByTestId("file-viewer-root-path").textContent).toContain(
|
||||
"missions/m/slices/20-drawer/proof/guard.md",
|
||||
);
|
||||
// DISPLAY path stays the friendly slice-relative label proof/guard.md (the drawer
|
||||
// header), distinct from the resolved read path above — pins the FileLink display.
|
||||
expect(within(viewer).getByText("proof/guard.md")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("FX-3 (RED): the drawer renders the C1 proof contract — five frontmatter fields + the distinctive body", async () => {
|
||||
renderSliceInDrawer();
|
||||
const list = await screen.findByTestId(`proof-files-${DRAWER_SLICE.id}`);
|
||||
fireEvent.click(within(list).getByText("proof/guard.md"));
|
||||
await screen.findByTestId("file-viewer");
|
||||
|
||||
const fm = await screen.findByTestId("markdown-frontmatter");
|
||||
for (const field of ["slice", "candidate_sha", "artifact_type", "verdict", "money_evidence"]) {
|
||||
expect(within(fm).getByText(field)).toBeTruthy();
|
||||
}
|
||||
// distinctive field VALUES (not just keys) — prevents a false green on an empty header.
|
||||
expect(within(fm).getByText("guard")).toBeTruthy();
|
||||
expect(within(fm).getByText("7d0997dddaab59f43bcc658fe2c0457128a64f53")).toBeTruthy();
|
||||
// the BODY (not just the header) renders inside the drawer.
|
||||
expect(screen.getByText(/DISTINCTIVE-BODY-MARKER/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("FX-4 (RED): the drawer closes in place — same PROOF surface, location/history unchanged (no navigation)", async () => {
|
||||
renderSliceInDrawer();
|
||||
const list = await screen.findByTestId(`proof-files-${DRAWER_SLICE.id}`);
|
||||
// pin the exact SPA location + history depth BEFORE the interaction — a full-page
|
||||
// raw-asset navigation (the defect) is precisely what mutates these.
|
||||
const hrefBefore = window.location.href;
|
||||
const historyBefore = window.history.length;
|
||||
|
||||
fireEvent.click(within(list).getByText("proof/guard.md"));
|
||||
await screen.findByTestId("file-viewer");
|
||||
// opening the drawer is in-app: no navigation, no history push.
|
||||
expect(window.location.href).toBe(hrefBefore);
|
||||
expect(window.history.length).toBe(historyBefore);
|
||||
|
||||
fireEvent.pointerDown(screen.getByTestId("shared-detail-drawer-outside"));
|
||||
await waitFor(() => expect(screen.queryByTestId("file-viewer")).toBeNull());
|
||||
// still on the same slice proof card, and the location/history never moved.
|
||||
expect(screen.getByTestId(`proof-slice-${DRAWER_SLICE.id}`)).toBeTruthy();
|
||||
expect(screen.getByTestId(`proof-files-${DRAWER_SLICE.id}`)).toBeTruthy();
|
||||
expect(window.location.href).toBe(hrefBefore);
|
||||
expect(window.history.length).toBe(historyBefore);
|
||||
});
|
||||
|
||||
it("FX-5 (GREEN preservation): image captures + inline PROOF.md images still resolve via /api/files/asset (drawer is markdown-only)", async () => {
|
||||
const { container } = renderSliceInDrawer();
|
||||
await screen.findByTestId(`proof-slice-${DRAWER_SLICE.id}`);
|
||||
// the proof/ image capture stays in the gallery/lightbox path (browser-viewable asset), not the drawer.
|
||||
expect(screen.getByTestId(`proof-gallery-${DRAWER_SLICE.id}`)).toBeTruthy();
|
||||
expect(screen.getByTestId("proof-thumb-shot.png")).toBeTruthy();
|
||||
// inline PROOF.md image still resolves under /api/files/asset (guard fcf1126f invariant).
|
||||
const inline = container.querySelector('img[alt="cap"]') as HTMLImageElement | null;
|
||||
expect(inline).toBeTruthy();
|
||||
expect(inline!.getAttribute("src") ?? "").toContain("/api/files/asset");
|
||||
// and the guard.md control never triggered a read (drawer stays closed here).
|
||||
expect(calls.some((c) => c.includes("/api/files/read") && c.includes("guard.md"))).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user