From 7aa94ff745127a9f35abf0762e63131dfe521c28 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 29 Oct 2026 23:20:06 -0500 Subject: [PATCH] Preserve ordinary link labels and production route semantics Restrict renamed labels to existing product chips. Reuse the company routing helper and current task URL, with browser regressions for prefixes, fragments and wrong task/company targets. Co-Authored-By: Paperclip --- tests/runner-e2e/PRODUCT-JOURNEYS.md | 2 ++ tests/runner-e2e/connection-guidance.test.ts | 2 +- tests/runner-e2e/native-task-reply.spec.ts | 22 ++++++++++++++-- tests/runner-e2e/native-task-reply.test.ts | 2 +- tests/runner-e2e/native-task-reply.ts | 9 ++++--- tests/runner-e2e/task-reply-content.test.ts | 9 +++++++ tests/runner-e2e/task-reply-content.ts | 27 +++++++++++++------- 7 files changed, 56 insertions(+), 17 deletions(-) diff --git a/tests/runner-e2e/PRODUCT-JOURNEYS.md b/tests/runner-e2e/PRODUCT-JOURNEYS.md index 549c08cdd2a..2a23d7dbbb7 100644 --- a/tests/runner-e2e/PRODUCT-JOURNEYS.md +++ b/tests/runner-e2e/PRODUCT-JOURNEYS.md @@ -118,6 +118,8 @@ thread anchor and observed text; classic protocol comment matchers stay strict. Markdown replies compare complete text and exact link destinations after parsing CommonMark. Whitespace and inline formatting can change during rendering. A document or issue chip can replace a link label, but must retain its destination. +Ordinary links retain their source label. Destination resolution uses the existing +production company-route helper and the current task URL for fragments. Receipts retain both source Markdown and observed content. Unsupported constructs such as images, raw HTML and GFM-only formatting fail closed. Exact marker checks remain exact, and decline checks inspect both visible explanations and source diff --git a/tests/runner-e2e/connection-guidance.test.ts b/tests/runner-e2e/connection-guidance.test.ts index ef3254ec770..8044605ed54 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", label: "QA-123" }, { text: "." }] }); + content: [{ text: "Contacts are unavailable. See " }, { href: "/QA/issues/QA-123#document-outcome", label: "QA-123", decorated: true }, { 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.spec.ts b/tests/runner-e2e/native-task-reply.spec.ts index 19392fa938f..c255db6ba49 100644 --- a/tests/runner-e2e/native-task-reply.spec.ts +++ b/tests/runner-e2e/native-task-reply.spec.ts @@ -2,13 +2,15 @@ import { expect, test } from "@playwright/test"; import { readBoundTaskFinalReply } from "./native-task-reply.js"; const markdown = "I **couldn't retrieve** the pages. See [Connection outcome](/QA/issues/QA-123#document-outcome)."; -const rendered = "I couldn't retrieve the pages. See QA-123."; +const rendered = "I couldn't retrieve the pages. See QA-123."; const run = { id: "final-run", companyId: "company", agentId: "agent", status: "succeeded", runtimeMode: "native", resultJson: { presentationDecision: { commentId: "selected" } } }; const comment = { id: "selected", authorAgentId: "agent", createdByRunId: run.id, body: markdown }; for (const [name, content, passes] of [ ["decorated document link and bold text", rendered, true], + ["ordinary link with its source label", rendered.replace(" data-mention-kind='issue'", "").replace(">QA-123", ">Connection outcome"), true], + ["renamed ordinary link", rendered.replace(" data-mention-kind='issue'", ""), false], ["wrong destination", rendered.replace("#document-outcome", "#document-other"), false], ["missing link", rendered.replace(//, "QA-123"), false], ["extra visible text", rendered + " Extra.", false], @@ -23,7 +25,23 @@ for (const [name, content, passes] of [ const receipt = await result; expect(receipt.source).toBe("posted-comment"); expect(receipt.sourceBody).toBe(markdown); - expect(receipt.body).toBe("I couldn't retrieve the pages. See QA-123."); + expect(receipt.body).toBe(content.includes(">Connection outcome") ? "I couldn't retrieve the pages. See Connection outcome." : "I couldn't retrieve the pages. See QA-123."); expect(receipt.observedAfterReload).toBe(true); } else await expect(result).rejects.toThrow(); }); + +for (const [sourceHref, renderedHref, passes] of [ + ["/issues/QA-123#document-outcome", "/QA/issues/QA-123#document-outcome", true], + ["#document-outcome", "/QA/issues/QA-123#document-outcome", true], + ["/issues/QA-123#document-outcome", "/OTHER/issues/QA-123#document-outcome", false], + ["#document-outcome", "/QA/issues/QA-999#document-outcome", false], +] as const) test(`reloaded link ${sourceHref} to ${renderedHref}`, async ({ page }) => { + const url = "https://fixture.test/QA/issues/QA-123"; + await page.route(url, route => route.fulfill({ contentType: "text/html", body: + `
Read Outcome
` })); + await page.goto(url); + const result = readBoundTaskFinalReply({ page, runId: run.id, agentId: "agent", deadlineAt: Date.now() + (passes ? 3000 : 300), + load: async () => ({ run, comments: [{ ...comment, body: `Read [Outcome](${sourceHref})` }], events: [] }) }); + if (passes) expect((await result).pageUrl).toBe(url); + else await expect(result).rejects.toThrow(); +}); diff --git a/tests/runner-e2e/native-task-reply.test.ts b/tests/runner-e2e/native-task-reply.test.ts index 086806702bc..870a377c650 100644 --- a/tests/runner-e2e/native-task-reply.test.ts +++ b/tests/runner-e2e/native-task-reply.test.ts @@ -80,7 +80,7 @@ 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", label: "QA-123" }, { text: "." }] }; + content: [{ text: "I couldn't retrieve the pages. See " }, { href: "https://fixture.test/QA/issues/QA-123#document-outcome", label: "QA-123", decorated: true }, { 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); diff --git a/tests/runner-e2e/native-task-reply.ts b/tests/runner-e2e/native-task-reply.ts index b7bb80831fc..f668577299b 100644 --- a/tests/runner-e2e/native-task-reply.ts +++ b/tests/runner-e2e/native-task-reply.ts @@ -5,7 +5,7 @@ import { taskReplyContentMatches, type ReplyContentPart } from "./task-reply-con type Row = Record; export interface TaskReplyObservation { body: string; anchor: string | null; settled: boolean; nativeFinal: boolean; visible: boolean; - content?: ReplyContentPart[]; origin?: string; + content?: ReplyContentPart[]; origin?: string; pageUrl?: string; } export type BoundTaskFinalReply = Omit>, "sourceBody"> & { sourceBody?: string }; @@ -54,13 +54,14 @@ 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; label: string }> = []; + const content: Array<{ text: string } | { href: string; label: string; decorated: boolean }> = []; 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()) content.push({ href: node.href, label: node.innerText }); + if (node.getClientRects().length && node.innerText.trim()) content.push({ href: node.href, label: node.innerText, + decorated: node.matches('[data-mention-kind="issue"], [data-sidebar-document-link]') }); return; } const block = ["P", "DIV", "BR", "LI", "PRE", "BLOCKQUOTE", "H1", "H2", "H3", "H4", "H5", "H6"].includes(node.tagName); @@ -71,7 +72,7 @@ export function observeTaskReplyElements(elements: Array