From 97048b015387f24e7497915e317eb531e66d72c8 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 29 Oct 2026 23:12:58 -0500 Subject: [PATCH] Bind content projections to the observed reply text Retain each visible link label so the source content projection cannot attest to different text. Keep decorated-label support with exact destinations and negative coverage. Co-Authored-By: Paperclip --- tests/runner-e2e/connection-guidance.test.ts | 2 +- tests/runner-e2e/native-task-reply.test.ts | 5 +++-- tests/runner-e2e/native-task-reply.ts | 4 ++-- tests/runner-e2e/task-reply-content.ts | 9 ++++++++- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/runner-e2e/connection-guidance.test.ts b/tests/runner-e2e/connection-guidance.test.ts index dd642c13526..ef3254ec770 100644 --- a/tests/runner-e2e/connection-guidance.test.ts +++ b/tests/runner-e2e/connection-guidance.test.ts @@ -66,7 +66,7 @@ describe("neutral decline evidence", () => { const input = native(); const body = "Contacts are unavailable. See [Connection outcome](/QA/issues/QA-123#document-outcome)."; Object.assign(input.nativeFinalReply, { sourceBody: body, body: "Contacts are unavailable. See QA-123.", origin: "https://fixture.test", - content: [{ text: "Contacts are unavailable. See " }, { href: "/QA/issues/QA-123#document-outcome" }, { text: "." }] }); + content: [{ text: "Contacts are unavailable. See " }, { href: "/QA/issues/QA-123#document-outcome", label: "QA-123" }, { text: "." }] }); input.nativeFinalReply.finalEvent.body = body; expect(passes(input)).toBe(true); Object.assign(input.nativeFinalReply, { sourceBody: body.replace("Connection outcome", "PRIVATE_MARKER") }); diff --git a/tests/runner-e2e/native-task-reply.test.ts b/tests/runner-e2e/native-task-reply.test.ts index e877ab65976..086806702bc 100644 --- a/tests/runner-e2e/native-task-reply.test.ts +++ b/tests/runner-e2e/native-task-reply.test.ts @@ -80,11 +80,12 @@ it("binds complete Markdown content and the exact link destination despite a dec const published = { ...run, resultJson: { presentationDecision: { commentId: "selected" } } }; const bubble: TaskReplyObservation = { ...observed, anchor: "selected", origin: "https://fixture.test", body: "I couldn't retrieve the pages. See QA-123.", - content: [{ text: "I couldn't retrieve the pages. See " }, { href: "https://fixture.test/QA/issues/QA-123#document-outcome" }, { text: "." }] }; + content: [{ text: "I couldn't retrieve the pages. See " }, { href: "https://fixture.test/QA/issues/QA-123#document-outcome", label: "QA-123" }, { text: "." }] }; expect(taskReplyBelongsToRun(bubble, published, "owned-agent", [comment])).toBe(true); const copy = events(); copy[0]!.payload.prpEvent.payload.text = body; expect(taskReplyBelongsToRun({ ...bubble, anchor: "final-run:turn" }, run, "owned-agent", [], copy)).toBe(true); + expect(taskReplyBelongsToRun({ ...bubble, body: "Different visible response" }, published, "owned-agent", [comment])).toBe(false); for (const content of [undefined, [{ text: bubble.body }], [...bubble.content!, { text: " extra" }], - [{ text: "I couldn't retrieve the pages. See " }, { href: "/other-task" }, { text: "." }]]) + [{ text: "I couldn't retrieve the pages. See " }, { href: "/other-task", label: "QA-123" }, { text: "." }]]) expect(taskReplyBelongsToRun({ ...bubble, content }, published, "owned-agent", [comment])).toBe(false); }); diff --git a/tests/runner-e2e/native-task-reply.ts b/tests/runner-e2e/native-task-reply.ts index e7665a77f3a..b7bb80831fc 100644 --- a/tests/runner-e2e/native-task-reply.ts +++ b/tests/runner-e2e/native-task-reply.ts @@ -54,13 +54,13 @@ export function uniqueBoundTaskReply(all: TaskReplyObservation[], run: Row, agen export function observeTaskReplyElements(elements: Array): TaskReplyObservation[] { return elements.map(element => { - const content: Array<{ text: string } | { href: string }> = []; + const content: Array<{ text: string } | { href: string; label: string }> = []; const walk = (node: Node) => { if (node.nodeType === Node.TEXT_NODE) { content.push({ text: node.textContent ?? "" }); return; } if (!(node instanceof HTMLElement) || node.getAttribute("aria-hidden") === "true" || getComputedStyle(node).display === "none" || getComputedStyle(node).visibility === "hidden") return; if (node instanceof HTMLAnchorElement) { - if (node.getClientRects().length && (node.innerText.trim() || node.getAttribute("aria-label"))) content.push({ href: node.href }); + if (node.getClientRects().length && node.innerText.trim()) content.push({ href: node.href, label: node.innerText }); return; } const block = ["P", "DIV", "BR", "LI", "PRE", "BLOCKQUOTE", "H1", "H2", "H3", "H4", "H5", "H6"].includes(node.tagName); diff --git a/tests/runner-e2e/task-reply-content.ts b/tests/runner-e2e/task-reply-content.ts index 78455815e00..7fd9183cbb2 100644 --- a/tests/runner-e2e/task-reply-content.ts +++ b/tests/runner-e2e/task-reply-content.ts @@ -1,6 +1,6 @@ import { fromMarkdown } from "mdast-util-from-markdown"; -export type ReplyContentPart = { text: string } | { href: string }; +export type ReplyContentPart = { text: string } | { href: string; label?: string }; type MarkdownNode = { type: string; value?: string; url?: string; children?: MarkdownNode[] }; function normalized(parts: ReplyContentPart[], origin: string) { @@ -28,6 +28,13 @@ export function taskReplyContentMatches( observation: { body: string; content?: ReplyContentPart[]; origin?: string }, markdown: string, ) { + if (observation.content) { + // Bind the structured projection to the independently observed innerText. + // A destination alone cannot attest to arbitrary visible wording. + if (observation.content.some(part => "href" in part && (typeof part.label !== "string" || !part.label.trim()))) return false; + const displayed = observation.content.map(part => "text" in part ? part.text : part.label!).join("").replace(/\s+/g, " ").trim(); + if (displayed !== observation.body.replace(/\s+/g, " ").trim()) return false; + } const parts: ReplyContentPart[] = []; const inlineLabel = (node: MarkdownNode): boolean => ["text", "inlineCode", "strong", "emphasis"].includes(node.type) && (node.children ?? []).every(inlineLabel);