← Files DataARCHIVED FILE
tests/data-report-follow-up-browser.smoke.mjs
30.8 KB · Sep 30, 2026 · 23:19 UTC
import assert from "node:assert/strict";
import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { chromium } from "playwright-core";
import { resolveChromiumExecutable, runDataAppFixtureBuild } from "./browser-helpers.mjs";
import { chooseHostedHandoff } from "./data-app-browser-handoff.mjs";
const pluginRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const template = join(pluginRoot, "templates/data-app/base");
async function saveReportEdits(page) {
await page.locator(".dashboard-topbar").getByRole("button", { name: "Save", exact: true }).click();
await page.getByRole("button", { name: "Edit text and layout", exact: true }).waitFor();
}
const project = mkdtempSync(join(tmpdir(), "data-report-follow-up-"));
const originatingThreadId = "550e8400-e29b-41d4-a716-446655440001";
const followUpId = "follow-up-check";
const editorOnlyId = "follow-up-owner-note";
const period = "2026-08-03 through 2026-08-10";
const originalQuestion = "Does the synthetic activation difference persist across segments?";
const revisedQuestion = "Check the revised synthetic definition before comparing the two periods.";
const revisedMarkdown = `**${revisedQuestion}**`;
const privateSentinels = ["RAW_ALPHA_ROW_73", "RAW_BETA_ROW_84", "SQL_ALPHA_SENTINEL", "SQL_BETA_SENTINEL",
"UNRELATED_QUERY_SENTINEL"];
const query = (id, sentinel, sqlSentinel) => ({
label: `Synthetic ${id}`, reportingField: "date",
rows: [{ date: "2026-08-10", record: sentinel, value: 73 }],
source: { label: `Synthetic ${id}`, sql: `SELECT '${sqlSentinel}' AS record FROM synthetic_${id}`,
tables: [`synthetic_${id}`], executedAt: "2026-08-11T12:00:00Z",
metricDefinitions: [{ label: "Value", definition: `Reviewed synthetic ${id} definition.` }] },
});
const snapshot = {
id: "report-follow-up-contract", surface: "report", title: "Synthetic follow-up brief",
status: "fixture", generatedAt: "2026-08-11T12:00:00Z", filters: [],
queries: {
alpha: query("alpha", privateSentinels[0], privateSentinels[2]),
beta: query("beta", privateSentinels[1], privateSentinels[3]),
unrelated: query("unrelated", privateSentinels[4], privateSentinels[4]),
},
};
const authoredReport = `import React, { useRef, useState } from "react";
import { DataComponent, ReportSection, RichNarrative, useDataApp } from "../../data-app-public.jsx";
import { ReportDisclosure } from "../shared/ReportDisclosure.jsx";
import { ReportTaskLink } from "../shared/ReportTaskLink.jsx";
export function ReportContent() {
const { appTitle, canEdit, mode, visible, requestTextEdit, requestReportFollowUp, reportFollowUpHref } = useDataApp();
const textTarget = useRef(null);
const [probeResult, setProbeResult] = useState(null);
const followUp = { id: ${JSON.stringify(followUpId)}, narrativeId: ${JSON.stringify(`${followUpId}:body`)},
text: ${JSON.stringify(originalQuestion)}, queryId: "alpha", queryIds: ["beta", "alpha"], period: ${JSON.stringify(period)} };
async function probeViewerGuard() {
const forged = { ...followUp, text: "Synthetic forbidden mutation probe", canEdit: true };
const results = [];
for (const action of ["report-investigate-update", "report-correct"])
results.push(await requestReportFollowUp(action, forged));
results.push(await requestReportFollowUp("report-investigate", { ...forged, editorOnly: true }));
setProbeResult(results);
}
return <article className="report-content" aria-label="Synthetic follow-up fixture">
<header className="report-hero"><h1>{appTitle}</h1></header>
{visible(${JSON.stringify(followUpId)}) && <ReportSection id=${JSON.stringify(followUpId)} title="Check the comparison"
queryId="alpha" queryIds={["beta", "alpha"]} showHeading={false}
onEdit={() => requestTextEdit(textTarget.current)}>
<div ref={textTarget}><RichNarrative id=${JSON.stringify(`${followUpId}:body`)} value=${JSON.stringify(originalQuestion)} /></div>
<ReportTaskLink {...followUp}>Investigate comparison</ReportTaskLink>
<ReportTaskLink {...followUp} intent="prepare" deliverable="recovery flow">Draft recovery flow</ReportTaskLink>
{mode !== "edit" && <a className="report-task-link" href={reportFollowUpHref(followUp).href}
target={reportFollowUpHref(followUp).target}>Legacy investigation</a>}
{/* Invalid fixture-only handoffs must fail closed without blanking the report. */}
<ReportTaskLink {...followUp} narrativeId="empty-request:body" text="">Invalid empty request</ReportTaskLink>
<ReportTaskLink {...followUp} narrativeId="large-request:body" text={"A".repeat(4001)}>Invalid large request</ReportTaskLink>
{/* Fixture-only probes exercise transport APIs without adding product controls. */}
<div data-transport-probes>
<button type="button" onClick={() => requestReportFollowUp("report-investigate", followUp)}>Probe investigation transport</button>
<button type="button" onClick={() => requestReportFollowUp("report-investigate", {
...followUp, intent: "prepare", deliverable: "recovery flow" })}>Probe draft transport</button>
{canEdit && <>
<button type="button" onClick={() => requestReportFollowUp("report-investigate-update", followUp)}>Probe update transport</button>
<button type="button" onClick={() => requestReportFollowUp("report-correct", followUp)}>Probe correction transport</button>
</>}
</div>
</ReportSection>}
{canEdit && <ReportSection id=${JSON.stringify(editorOnlyId)} title="Owner review" queryId="beta" showHeading={false}>
<RichNarrative id=${JSON.stringify(`${editorOnlyId}:body`)} value="Review the synthetic source definition with the author." />
</ReportSection>}
<DataComponent id="disclosed-evidence" title="Evidence remains reachable" queryId="alpha" kind="custom">
<p>The main finding stays visible.</p>
<ReportDisclosure id="synthetic-methods" label="Show complete evidence">
<RichNarrative id="synthetic-methods:body" value="READER_DISCLOSED_EVIDENCE_SENTINEL" />
<table><caption>Synthetic complete evidence</caption><tbody><tr><th>Reviewed count</th><td>73</td></tr></tbody></table>
</ReportDisclosure>
</DataComponent>
<button type="button" onClick={probeViewerGuard}>Probe viewer mutation guard</button>
<output data-follow-up-probe-result>{probeResult && JSON.stringify(probeResult)}</output>
</article>;
}`;
const block = (page, id = followUpId) => page.locator(`[data-component-id="${id}"]`);
async function openActions(page, id = followUpId) {
await block(page, id).getByRole("button", { name: / actions$/u }).click();
}
async function chooseAction(page, name, id = followUpId) {
await openActions(page, id);
await page.getByRole("menuitem", { name, exact: true }).click();
}
async function installHostMock(page) {
await page.addInitScript(() => {
window.__followUpPrompts = [];
window.__followUpLinks = [];
window.__followUpHostMode = "accept";
window.__followUpRegisteredTools = [];
window.__followUpTools = new Map();
Object.defineProperty(document, "modelContext", { configurable: true, value: {
registerTool: (tool) => {
window.__followUpRegisteredTools.push(tool.name);
window.__followUpTools.set(tool.name, tool);
}, unregisterTool: (name) => window.__followUpTools.delete(name),
} });
document.addEventListener("click", (event) => {
const link = event.target.closest('a[href^="codex://"], a[href^="https://chatgpt.com/?"]');
if (!link || event.defaultPrevented) return;
event.preventDefault();
window.__followUpLinks.push({ href: link.href, target: link.target });
});
window.openai = { sendFollowUpMessage: async (message) => {
window.__followUpPrompts.push(message);
if (window.__followUpHostMode === "throw") throw new Error("Synthetic host unavailable");
return { isError: window.__followUpHostMode === "reject" };
} };
});
}
async function localPresentation(page) {
return page.evaluate((id) => Object.entries(localStorage)
.filter(([key]) => key.endsWith(`:${id}`))
.map(([, value]) => JSON.parse(value).presentation).find(Boolean) ?? {}, snapshot.id);
}
function assertScopedPrompt(prompt, question) {
assert.equal(typeof prompt, "string");
assert.deepEqual(prompt.match(/\[@[^\]]+\]\(plugin:\/\/[^)]+\)/gu), [
"[@Data](plugin://data-analytics@openai-curated-remote)",
]);
if (prompt.includes("\n\n{")) {
const context = JSON.parse(prompt.slice(prompt.indexOf("\n\n{") + 2));
assert.equal(context.text, question, "The handoff must use the currently displayed question");
assert.equal(context.componentId, followUpId, "The handoff must identify the exact follow-up block");
assert.equal(context.narrativeId, `${followUpId}:body`);
assert.deepEqual(context.sources.map(({ queryId }) => queryId), ["alpha", "beta"],
"Only the selected, deduplicated reviewed source IDs may enter the handoff");
assert.equal(context.period, period, "The handoff must retain the authored reporting period");
} else {
assert.ok(prompt.includes(question));
assert.ok(prompt.includes(`Component: ${followUpId}; narrative: ${followUpId}:body.`));
assert.ok(prompt.includes(period), "Authored reporting periods must survive even when absent from the snapshot");
assert.match(prompt, /https:\/\/follow-up-fixture\.chatgpt\.site\/\?view=1/u);
assert.match(prompt, /read its current Data app context/u);
assert.doesNotMatch(prompt, /"sources"|"queries"|"presentation"/u);
}
for (const sentinel of privateSentinels) assert.ok(!prompt.includes(sentinel),
`The handoff must not copy raw reviewed rows or SQL (${sentinel})`);
assert.doesNotMatch(prompt, /SELECT\s+['"*]|FROM\s+synthetic_/iu);
}
function promptFromLink(link) {
const url = new URL(link.href);
assert.ok(url.protocol === "codex:" || url.origin === "https://chatgpt.com");
assert.equal(url.hostname, url.protocol === "codex:" ? "new" : "chatgpt.com");
assert.equal(url.pathname, url.protocol === "codex:" ? "" : "/",
"A hosted handoff must not reopen an originating private local task");
assert.equal(link.target, url.protocol === "codex:" ? "_self" : "_blank");
if (url.protocol === "https:") assert.equal(url.searchParams.get("disable_auto_send"), "1");
assert.ok(!link.href.includes(originatingThreadId));
return url.searchParams.get("prompt") ?? url.searchParams.get("q");
}
function promptFromLocalLink(link) {
const url = new URL(link.href);
assert.equal(`${url.protocol}//${url.host}${url.pathname}`, `codex://threads/${originatingThreadId}`);
assert.equal(link.target, "_self");
assert.deepEqual([...url.searchParams.keys()], ["prompt"]);
return url.searchParams.get("prompt");
}
function assertPreparationLink(link, question, { hosted = false } = {}) {
const url = new URL(link.href);
assert.equal(`${url.protocol}//${url.host}${url.pathname}`,
url.protocol === "codex:" ? (hosted ? "codex://new" : `codex://threads/${originatingThreadId}`) : "https://chatgpt.com/");
assert.equal(link.target, url.protocol === "codex:" ? "_self" : "_blank");
if (url.protocol === "https:") assert.equal(url.searchParams.get("disable_auto_send"), "1");
const prompt = url.searchParams.get("prompt") ?? url.searchParams.get("q");
assertScopedPrompt(prompt, question);
if (prompt.includes("\n\n{")) assert.deepEqual(JSON.parse(prompt.slice(prompt.indexOf("\n\n{") + 2)).request,
{ intent: "prepare", deliverable: "recovery flow" });
else assert.match(prompt, /recovery flow/u);
assert.match(prompt, /Prepare a reviewable draft/u);
assert.match(prompt, /Do not edit the report(?: or act on the proposal|, implement the proposal, create tasks, schedule work, or execute external actions)/u);
assert.ok(!prompt.includes(originalQuestion), "The authored fallback must not replace the saved recommendation");
}
let browser;
const errors = [];
try {
cpSync(template, project, { recursive: true,
filter: (path) => !path.includes("/node_modules") && !path.includes("/dist") });
writeFileSync(join(project, "src/data.json"), `${JSON.stringify(snapshot, null, 2)}\n`);
writeFileSync(join(project, "src/content/report/ReportContent.jsx"), authoredReport);
const build = runDataAppFixtureBuild(project, { pluginRoot,
env: { ...process.env, CODEX_SESSION_ID: originatingThreadId, CODEX_THREAD_ID: originatingThreadId } });
assert.equal(build.status, 0, `${build.stdout}\n${build.stderr}`);
const compiledHtml = readFileSync(join(project, "dist/index.html"), "utf8");
assert.ok(compiledHtml.includes(originatingThreadId), "The fixture must contain originating-task metadata");
browser = await chromium.launch({ executablePath: resolveChromiumExecutable(), headless: true });
const local = await browser.newPage({ userAgent: "CodexBrowser/1.0", viewport: { width: 1100, height: 850 } });
local.on("pageerror", (error) => errors.push(error.message));
await installHostMock(local);
await local.goto(pathToFileURL(join(project, "dist/index.html")).href, { waitUntil: "load" });
await block(local).getByRole("link", { name: "Investigate comparison", exact: true }).waitFor();
assert.equal(await local.getByRole("link", { name: /^Invalid (empty|large) request$/u }).count(), 0);
assert.equal(await block(local, editorOnlyId).count(), 1, "The fixture exposes its owner-only section to the owner");
const directLink = local.getByRole("link", { name: "Investigate comparison", exact: true });
const beforeDirect = await localPresentation(local);
assert.equal(await directLink.locator("a,button,[contenteditable]").count(), 0,
"A direct task link must not contain nested controls");
await directLink.focus();
await directLink.press("Enter");
const direct = (await local.evaluate(() => window.__followUpLinks))[0];
assertScopedPrompt(promptFromLocalLink(direct), originalQuestion);
assert.deepEqual(await local.evaluate(() => window.__followUpPrompts), [],
"A direct task link stays unsent even when a local host accepts prompts");
assert.deepEqual(await localPresentation(local), beforeDirect, "A direct task link must not change the report");
assert.equal(await block(local).getByRole("status").count(), 0, "An intercepted link must not claim success");
await local.evaluate(() => { window.__followUpLinks = []; });
const disclosure = block(local, "disclosed-evidence");
const disclosureToggle = disclosure.getByRole("button", { name: "Show complete evidence", exact: true });
const disclosedText = disclosure.getByText("READER_DISCLOSED_EVIDENCE_SENTINEL", { exact: true });
assert.equal(await disclosureToggle.getAttribute("aria-expanded"), "false");
assert.equal(await disclosedText.isVisible(), false);
assert.ok(await disclosure.getByRole("button", { name: / actions$/u }).isVisible(),
"A collapsed disclosure must leave its enclosing source component reachable");
await disclosureToggle.focus();
await disclosureToggle.press("Enter");
assert.equal(await disclosureToggle.getAttribute("aria-expanded"), "true");
assert.ok(await disclosedText.isVisible());
assert.ok(await disclosure.getByRole("table", { name: "Synthetic complete evidence" }).isVisible());
await local.getByRole("button", { name: "Edit text and layout", exact: true }).click();
assert.equal(await disclosureToggle.getAttribute("aria-expanded"), "true",
"Entering Edit mode preserves an already-open evidence disclosure");
assert.equal(await disclosureToggle.isDisabled(), false);
await disclosure.locator('[data-editable-id="synthetic-methods:body"] [contenteditable="true"]').waitFor();
await disclosureToggle.click();
assert.equal(await disclosedText.isVisible(), false, "Evidence can be closed while editing");
await local.getByRole("button", { name: "Cancel", exact: true }).click();
assert.equal(await disclosureToggle.getAttribute("aria-expanded"), "false",
"Leaving Edit mode preserves the reader's evidence choice");
await chooseAction(local, "Edit text");
assert.equal(await disclosureToggle.getAttribute("aria-expanded"), "false",
"Editing another block must not auto-expand evidence");
assert.equal(await disclosedText.isVisible(), false);
assert.equal(await local.getByRole("link", { name: "Investigate comparison", exact: true }).count(), 0,
"The authored report keeps launch links out of Edit mode");
const editor = block(local).locator(`[data-editable-id="${followUpId}:body"] [contenteditable="true"]`);
await editor.waitFor();
await editor.fill(revisedQuestion);
await editor.selectText();
const formatToolbar = local.getByRole("toolbar", { name: "Format selected text" });
await formatToolbar.waitFor();
await formatToolbar.getByRole("button", { name: "Bold", exact: true }).click();
await local.keyboard.press("Escape");
await editor.blur();
await saveReportEdits(local);
await local.waitForFunction(({ id, value }) => Object.values(localStorage).some((entry) =>
JSON.parse(entry).presentation?.textEdits?.[`${id}:body`] === value), { id: followUpId, value: revisedMarkdown });
await local.emulateMedia({ media: "print" });
assert.ok(await disclosedText.isVisible(), "Print reveals reader evidence even when collapsed on screen");
assert.ok(await disclosure.getByRole("table", { name: "Synthetic complete evidence" }).isVisible());
assert.equal(await disclosureToggle.isVisible(), false, "Disclosure controls must not print");
assert.match(await block(local).innerText(), new RegExp(revisedQuestion, "u"), "Reader recommendations must print");
const pdf = await local.pdf({ format: "A4" });
assert.equal(pdf.subarray(0, 4).toString(), "%PDF", "Chromium must produce a real print artifact");
await local.emulateMedia({ media: "screen" });
assert.equal(await disclosedText.isVisible(), false, "Printing must restore collapsed screen evidence");
const presentationBeforeHandoff = await localPresentation(local);
await block(local).getByRole("button", { name: "Probe investigation transport", exact: true }).click();
await local.waitForFunction(() => window.__followUpLinks.length === 1);
await block(local).getByRole("button", { name: "Probe update transport", exact: true }).click();
await local.waitForFunction(() => window.__followUpLinks.length === 2);
await block(local).getByRole("button", { name: "Probe correction transport", exact: true }).click();
await local.waitForFunction(() => window.__followUpLinks.length === 3);
const messages = (await local.evaluate(() => window.__followUpLinks)).map(link => ({ prompt: promptFromLocalLink(link) }));
for (const message of messages) assertScopedPrompt(message.prompt, revisedMarkdown);
assert.equal(new Set(messages.map(({ prompt }) => prompt)).size, 3,
"Read-only investigation, report update, and correction must have distinct instructions");
assert.match(messages[0].prompt, /(?:do not|without|never)[^.\n]*(?:modify|edit|update|change)[^.\n]*(?:report|artifact)|answer[^.\n]*(?:chat|conversation)/iu,
"Investigate must request an answer without silently editing the report");
assert.match(messages[1].prompt, /(?:update|revise)[^.\n]*(?:report|artifact)|\$build-report/iu,
"Investigate and update must explicitly request an artifact revision");
assert.match(messages[2].prompt, /correct|correction/iu,
"Correct with Codex must make correction intent explicit");
assert.deepEqual(await localPresentation(local), presentationBeforeHandoff,
"Sending a follow-up must not mutate local presentation state");
assert.deepEqual(await local.evaluate(() => window.__followUpPrompts), [],
"Imperative report buttons must use the same deeplink transport even when a host bridge accepts prompts");
for (const hostMode of ["reject", "throw"]) {
await local.evaluate((mode) => { window.__followUpHostMode = mode; }, hostMode);
const attempts = await local.evaluate(() => window.__followUpLinks.length);
await block(local).getByRole("button", { name: "Probe investigation transport", exact: true }).click();
await local.waitForFunction(count => window.__followUpLinks.length === count + 1, attempts);
assertScopedPrompt(promptFromLocalLink((await local.evaluate(() => window.__followUpLinks)).at(-1)), revisedMarkdown);
assert.deepEqual(await local.evaluate(() => window.__followUpPrompts), [],
`An available ${hostMode} bridge must not change deeplink behavior`);
assert.deepEqual(await localPresentation(local), presentationBeforeHandoff,
`A ${hostMode} result must not mutate the report`);
}
await local.evaluate(() => { window.__followUpHostMode = "accept"; });
await local.getByRole("button", { name: "Edit text and layout", exact: true }).click();
await chooseAction(local, "Hide");
await block(local).waitFor({ state: "hidden" });
await saveReportEdits(local);
await local.reload({ waitUntil: "load" });
await block(local, editorOnlyId).waitFor();
assert.equal(await block(local).count(), 0, "A hidden follow-up must remain hidden after reload");
await local.getByRole("button", { name: "Edit text and layout", exact: true }).click();
await local.getByRole("button", { name: "Restore hidden (1)", exact: true }).click();
await block(local).waitFor();
assert.ok((await block(local).innerText()).includes(revisedQuestion),
"Restoring a follow-up must retain its edited text");
await saveReportEdits(local);
const beforePrepareSends = await local.evaluate(() => window.__followUpPrompts.length);
const draftLink = local.getByRole("link", { name: "Draft recovery flow", exact: true });
assertPreparationLink(await draftLink.evaluate((element) => ({ href: element.href, target: element.target })), revisedMarkdown);
await draftLink.click();
assertPreparationLink((await local.evaluate(() => window.__followUpLinks)).at(-1), revisedMarkdown);
assert.equal(await local.evaluate(() => window.__followUpPrompts.length), beforePrepareSends,
"A specific prepare link must prefill the original thread without invoking an accepting host bridge");
const beforePrepareLinks = await local.evaluate(() => window.__followUpLinks.length);
await block(local).getByRole("button", { name: "Probe draft transport", exact: true }).click();
await local.waitForFunction(count => window.__followUpLinks.length === count + 1, beforePrepareLinks);
assertPreparationLink((await local.evaluate(() => window.__followUpLinks)).at(-1), revisedMarkdown);
assert.deepEqual(await local.evaluate(() => window.__followUpPrompts), []);
// Serve only this compiled synthetic app. Every request is intercepted, and the
// generated handoff links are captured before navigation or a real prompt send.
const writes = [];
let canEdit = false;
let revision = 0;
let hostedPresentation = { textEdits: { [`${followUpId}:body`]: revisedMarkdown } };
const hosted = await browser.newPage({ viewport: { width: 390, height: 844 } });
hosted.on("pageerror", (error) => errors.push(error.message));
await installHostMock(hosted);
await hosted.route("https://follow-up-fixture.chatgpt.site/**", async (route) => {
const request = route.request();
const { pathname } = new URL(request.url());
if (request.method() !== "GET") writes.push({ path: pathname, method: request.method(), body: request.postDataJSON() });
if (pathname === "/api/snapshot") return route.fulfill({ contentType: "application/json", body: JSON.stringify(snapshot) });
if (pathname === "/api/presentation") {
if (request.method() === "PUT" && canEdit) {
hostedPresentation = request.postDataJSON().presentation;
revision += 1;
}
return route.fulfill({ contentType: "application/json", body: JSON.stringify({ canEdit, revision, presentation: hostedPresentation }) });
}
return route.fulfill({ contentType: "text/html", body: compiledHtml });
});
await hosted.goto("https://follow-up-fixture.chatgpt.site/", { waitUntil: "load" });
await block(hosted).getByRole("link", { name: "Investigate comparison", exact: true }).waitFor();
assert.equal(await hosted.getByRole("link", { name: /^Invalid (empty|large) request$/u }).count(), 0);
assert.equal(await block(hosted, editorOnlyId).count(), 0, "The fixture omits its owner-only section for viewers");
for (const name of ["Probe update transport", "Probe correction transport"])
assert.equal(await hosted.getByRole("button", { name, exact: true }).count(), 0);
assert.equal(await hosted.getByRole("button", { name: "Edit text and layout", exact: true }).count(), 0);
assert.equal(await hosted.locator('[contenteditable="true"]').count(), 0);
assert.deepEqual((await hosted.evaluate(() => window.__followUpRegisteredTools)).sort(), [
"get_data_app_context", "get_data_app_query_rows", "get_data_app_text",
"list_data_app_cards", "get_data_app_card_image", "get_data_app_card_images",
].sort(), "A hosted viewer may read its context and visible card images but must not receive mutation tools");
const linkedContext = await hosted.evaluate(async (id) => {
const context = await window.__followUpTools.get("get_data_app_context").execute({});
const text = await window.__followUpTools.get("get_data_app_text").execute({ id, contextVersion: context.contextVersion });
return { artifactId: context.artifact.id, canEdit: context.canEdit, text: text.text,
savedText: context.presentation.textEdits[id] };
}, `${followUpId}:body`);
assert.deepEqual(linkedContext, { artifactId: snapshot.id, canEdit: false, text: revisedQuestion, savedText: revisedMarkdown },
"The compact selected-item link must lead to the actual saved narrative through the current browser context");
await hosted.getByRole("button", { name: "Probe viewer mutation guard", exact: true }).click();
await hosted.locator("[data-follow-up-probe-result]").getByText("[false,false,false]", { exact: true }).waitFor();
assert.deepEqual(await hosted.evaluate(() => window.__followUpPrompts), [],
"The shell must reject forged viewer mutation requests before the host bridge");
assert.deepEqual(await hosted.evaluate(() => window.__followUpLinks), [],
"The shell must reject forged viewer mutation requests before fallback navigation");
assert.deepEqual(writes, [], "The viewer permission guard must reject mutation before any hosted write");
await openActions(hosted);
for (const name of ["Investigate and update", "Correct with Codex", "Edit text", "Hide"]) {
assert.equal(await hosted.getByRole("menuitem", { name, exact: true }).count(), 0,
`A hosted viewer must not be offered ${name}`);
}
await hosted.keyboard.press("Escape");
await chooseAction(hosted, "View data source");
const source = hosted.getByRole("complementary", { name: /Data source for/u });
await source.getByRole("tab", { name: "Overview", exact: true }).waitFor();
assert.match(await source.innerText(), /Reviewed synthetic alpha definition/u);
await source.getByRole("button", { name: "Choose reviewed data source" }).click();
await hosted.getByRole("menuitemradio", { name: "Synthetic beta", exact: true }).click();
assert.match(await source.innerText(), /Reviewed synthetic beta definition/u);
await source.getByRole("button", { name: "Close data source", exact: true }).click();
await source.waitFor({ state: "hidden" });
await block(hosted).getByRole("button", { name: "Probe investigation transport", exact: true }).click();
await chooseHostedHandoff(hosted, "web");
await hosted.waitForFunction(() => window.__followUpLinks.length === 1);
assertScopedPrompt(promptFromLink((await hosted.evaluate(() => window.__followUpLinks))[0]), revisedMarkdown);
assert.deepEqual(await hosted.evaluate(() => window.__followUpPrompts), [],
"Sites follow-ups must use the existing link transport, not send through an injected host bridge");
assert.deepEqual(writes, [], "A hosted viewer investigation must not write data or presentation");
await hosted.getByRole("link", { name: "Investigate comparison", exact: true }).click();
await chooseHostedHandoff(hosted, "desktop");
const readerLink = (await hosted.evaluate(() => window.__followUpLinks)).at(-1);
assertScopedPrompt(promptFromLink(readerLink), revisedMarkdown);
assert.equal(`${new URL(readerLink.href).protocol}//${new URL(readerLink.href).host}`, "codex://new",
"A desktop reader investigation must create a new task without targeting the owner's local project");
await hosted.getByRole("link", { name: "Draft recovery flow", exact: true }).click();
await chooseHostedHandoff(hosted, "web");
assertPreparationLink((await hosted.evaluate(() => window.__followUpLinks)).at(-1), revisedMarkdown, { hosted: true });
await block(hosted).getByRole("button", { name: "Probe draft transport", exact: true }).click();
await chooseHostedHandoff(hosted, "web");
assertPreparationLink((await hosted.evaluate(() => window.__followUpLinks)).at(-1), revisedMarkdown, { hosted: true });
for (const path of [originatingThreadId, "new"]) {
const legacy = hosted.getByRole("link", { name: "Legacy investigation", exact: true });
await legacy.evaluate((element, path) => {
const previous = new URL(element.href);
const url = new URL(`codex://threads/${path}`);
url.searchParams.set("prompt", previous.searchParams.get("q") ?? previous.searchParams.get("prompt"));
element.href = url.toString();
}, path);
await legacy.click();
await chooseHostedHandoff(hosted, "desktop");
assertScopedPrompt(promptFromLink((await hosted.evaluate(() => window.__followUpLinks)).at(-1)), revisedMarkdown);
}
assert.deepEqual(await hosted.evaluate(() => window.__followUpPrompts), []);
assert.deepEqual(writes, [], "Preparing a draft must not write the viewer's shared report");
canEdit = true;
await hosted.reload({ waitUntil: "load" });
await block(hosted, editorOnlyId).waitFor();
await block(hosted).getByRole("button", { name: "Probe update transport", exact: true }).click();
await chooseHostedHandoff(hosted, "web");
await hosted.waitForFunction(() => window.__followUpLinks.length === 1);
const updateLink = (await hosted.evaluate(() => window.__followUpLinks))[0];
const updatePrompt = promptFromLink(updateLink);
assertScopedPrompt(updatePrompt, revisedMarkdown);
assert.match(updatePrompt, /(?:update|revise)[^.\n]*(?:report|artifact)|\$build-report/iu);
assert.deepEqual(writes, [], "An owner handoff requests a later revision; it must not directly write the hosted artifact");
assert.deepEqual(errors, [], "Follow-up browser runtime emitted errors");
assert.deepEqual(JSON.parse(readFileSync(join(project, "src/data.json"), "utf8")), snapshot,
"Follow-up interactions must not alter the synthetic reviewed snapshot");
console.log(JSON.stringify({ status: "passed", interactions: ["current edited Markdown", "scoped source IDs",
"distinct investigation/update/correction", "specific draft survives saved edits", "prepare reuses local origin", "host bridges remain unused", "legacy report links use chooser", "hide/reload/restore",
"direct unsent task links", "disclosure keyboard/edit/print state", "hosted viewer permission", "hosted handoff without mutation"] }, null, 2));
} finally {
await browser?.close();
rmSync(project, { recursive: true, force: true });
}
SHA-256: aa53d372cec9ef9acf47f18a6b9f0be90030abaa6d7c3bebf40d0c66a90dfc63