From 6eba7fffb4c1ee37779b7c2282e0f3517c57dd55 Mon Sep 17 00:00:00 2001 From: Dotta Date: Thu, 29 Oct 2026 23:28:39 -0500 Subject: [PATCH] Match renderer link routing and keep browser callbacks self-contained Prefix only parsed issue references, preserve ordinary root-relative anchors, and avoid tsx name helpers inside the serialized DOM callback. Verify with the exact QA Node24/tsx path and positive/negative browser cases. Co-Authored-By: Paperclip --- tests/runner-e2e/connection-guidance.test.ts | 2 +- tests/runner-e2e/native-task-reply.spec.ts | 19 +++++++++++++++++-- tests/runner-e2e/native-task-reply.test.ts | 2 +- tests/runner-e2e/native-task-reply.ts | 19 ++++++++++--------- tests/runner-e2e/task-reply-content.test.ts | 4 +++- tests/runner-e2e/task-reply-content.ts | 4 +++- 6 files changed, 35 insertions(+), 15 deletions(-) diff --git a/tests/runner-e2e/connection-guidance.test.ts b/tests/runner-e2e/connection-guidance.test.ts index 8044605ed54..6beade64324 100644 --- a/tests/runner-e2e/connection-guidance.test.ts +++ b/tests/runner-e2e/connection-guidance.test.ts @@ -65,7 +65,7 @@ describe("neutral decline evidence", () => { it("checks both the visible explanation and preserved native Markdown when links become chips", () => { 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", + Object.assign(input.nativeFinalReply, { sourceBody: body, body: "Contacts are unavailable. See QA-123.", origin: "https://fixture.test", pageUrl: "https://fixture.test/QA/issues/QA-123", 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); diff --git a/tests/runner-e2e/native-task-reply.spec.ts b/tests/runner-e2e/native-task-reply.spec.ts index c255db6ba49..b075aa115fd 100644 --- a/tests/runner-e2e/native-task-reply.spec.ts +++ b/tests/runner-e2e/native-task-reply.spec.ts @@ -1,4 +1,6 @@ import { expect, test } from "@playwright/test"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; import { readBoundTaskFinalReply } from "./native-task-reply.js"; const markdown = "I **couldn't retrieve** the pages. See [Connection outcome](/QA/issues/QA-123#document-outcome)."; @@ -7,6 +9,17 @@ const run = { id: "final-run", companyId: "company", agentId: "agent", status: " resultJson: { presentationDecision: { commentId: "selected" } } }; const comment = { id: "selected", authorAgentId: "agent", createdByRunId: run.id, body: markdown }; +test("the QA tsx loader produces a self-contained browser callback", async ({ page }) => { + const moduleUrl = new URL("./native-task-reply.ts", import.meta.url).href; + const loader = fileURLToPath(new URL("../../cli/node_modules/tsx/dist/loader.mjs", import.meta.url)); + const serialized = JSON.parse(execFileSync(process.execPath, ["--import", loader, "--input-type=module", "-e", + `import { observeTaskReplyElements } from ${JSON.stringify(moduleUrl)}; console.log(JSON.stringify(observeTaskReplyElements.toString()));`], { encoding: "utf8" })); + await page.setContent(`
${rendered}
`); + const observations: any = await page.evaluate(`(${serialized})(Array.from(document.querySelectorAll('[data-testid="reply"]')))`); + expect(observations[0].body).toBe("I couldn't retrieve the pages. See QA-123."); + expect(observations[0].content.some((part: any) => part.label === "QA-123" && part.decorated)).toBe(true); +}); + 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], @@ -16,9 +29,9 @@ for (const [name, content, passes] of [ ["extra visible text", rendered + " Extra.", false], ["hidden source text", rendered.replace("couldn't retrieve", ""), false], ] as const) test(`reloaded task reply: ${name}`, async ({ page }) => { - await page.route("https://fixture.test/task", route => route.fulfill({ contentType: "text/html", body: + await page.route("https://fixture.test/QA/issues/QA-123", route => route.fulfill({ contentType: "text/html", body: `
${content}
` })); - await page.goto("https://fixture.test/task"); + await page.goto("https://fixture.test/QA/issues/QA-123"); const result = readBoundTaskFinalReply({ page, runId: run.id, agentId: "agent", deadlineAt: Date.now() + (passes ? 3000 : 300), load: async () => ({ run, comments: [comment], events: [] }) }); if (passes) { @@ -35,6 +48,8 @@ for (const [sourceHref, renderedHref, passes] of [ ["#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], + ["/projects/example", "/projects/example", true], + ["/projects/example", "/QA/projects/example", 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: diff --git a/tests/runner-e2e/native-task-reply.test.ts b/tests/runner-e2e/native-task-reply.test.ts index 870a377c650..5e7d7635bf0 100644 --- a/tests/runner-e2e/native-task-reply.test.ts +++ b/tests/runner-e2e/native-task-reply.test.ts @@ -78,7 +78,7 @@ it("binds complete Markdown content and the exact link destination despite a dec const body = "I **couldn't retrieve** the pages. See [Connection outcome](/QA/issues/QA-123#document-outcome)."; const comment = { id: "selected", authorAgentId: "owned-agent", createdByRunId: "final-run", body }; const published = { ...run, resultJson: { presentationDecision: { commentId: "selected" } } }; - const bubble: TaskReplyObservation = { ...observed, anchor: "selected", origin: "https://fixture.test", + const bubble: TaskReplyObservation = { ...observed, anchor: "selected", origin: "https://fixture.test", pageUrl: "https://fixture.test/QA/issues/QA-123", 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", decorated: true }, { text: "." }] }; expect(taskReplyBelongsToRun(bubble, published, "owned-agent", [comment])).toBe(true); diff --git a/tests/runner-e2e/native-task-reply.ts b/tests/runner-e2e/native-task-reply.ts index f668577299b..0cd7074f7c5 100644 --- a/tests/runner-e2e/native-task-reply.ts +++ b/tests/runner-e2e/native-task-reply.ts @@ -55,21 +55,22 @@ 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; decorated: boolean }> = []; - const walk = (node: Node) => { - if (node.nodeType === Node.TEXT_NODE) { content.push({ text: node.textContent ?? "" }); return; } + const nodes: Array = [element]; + while (nodes.length) { + const node = nodes.pop()!; + if (typeof node === "string") { content.push({ text: node }); continue; } + if (node.nodeType === Node.TEXT_NODE) { content.push({ text: node.textContent ?? "" }); continue; } if (!(node instanceof HTMLElement) || node.getAttribute("aria-hidden") === "true" - || getComputedStyle(node).display === "none" || getComputedStyle(node).visibility === "hidden") return; + || getComputedStyle(node).display === "none" || getComputedStyle(node).visibility === "hidden") continue; if (node instanceof HTMLAnchorElement) { 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; + continue; } const block = ["P", "DIV", "BR", "LI", "PRE", "BLOCKQUOTE", "H1", "H2", "H3", "H4", "H5", "H6"].includes(node.tagName); - if (block) content.push({ text: " " }); - node.childNodes.forEach(walk); - if (block) content.push({ text: " " }); - }; - walk(element); + if (block) { content.push({ text: " " }); nodes.push(" "); } + for (let index = node.childNodes.length - 1; index >= 0; index--) nodes.push(node.childNodes[index]!); + } return { body: (element as HTMLElement).innerText.trim(), content, origin: location.origin, pageUrl: location.href, diff --git a/tests/runner-e2e/task-reply-content.test.ts b/tests/runner-e2e/task-reply-content.test.ts index 87852a2e0d0..5e08a9bf072 100644 --- a/tests/runner-e2e/task-reply-content.test.ts +++ b/tests/runner-e2e/task-reply-content.test.ts @@ -18,6 +18,8 @@ it("uses the production company prefix and current task URL without accepting a content: [{ text: "Read " }, { href: "/QA/issues/QA-123#document-outcome", label: "Outcome" }] }; for (const href of ["/issues/QA-123#document-outcome", "#document-outcome"]) expect(taskReplyContentMatches(observation, `Read [Outcome](${href})`)).toBe(true); - for (const href of ["/OTHER/issues/QA-123#document-outcome", "/QA/issues/QA-999#document-outcome", "#document-other"]) + for (const href of ["/QA/issues/QA-999#document-outcome", "#document-other"]) expect(taskReplyContentMatches(observation, `Read [Outcome](${href})`)).toBe(false); + expect(taskReplyContentMatches({ ...observation, content: [{ text: "Read " }, + { href: "/OTHER/issues/QA-123#document-outcome", label: "Outcome" }] }, "Read [Outcome](/issues/QA-123#document-outcome)")).toBe(false); }); diff --git a/tests/runner-e2e/task-reply-content.ts b/tests/runner-e2e/task-reply-content.ts index 745252d3e09..a2ee32ecfc9 100644 --- a/tests/runner-e2e/task-reply-content.ts +++ b/tests/runner-e2e/task-reply-content.ts @@ -1,5 +1,6 @@ import { fromMarkdown } from "mdast-util-from-markdown"; import { applyCompanyPrefix, extractCompanyPrefixFromPath } from "../../ui/src/lib/company-routes.js"; +import { parseIssueReferenceFromHref } from "../../ui/src/lib/issue-reference.js"; export type ReplyContentPart = { text: string } | { href: string; label?: string; decorated?: boolean }; type MarkdownNode = { type: string; value?: string; url?: string; children?: MarkdownNode[] }; @@ -9,7 +10,8 @@ function normalized(parts: ReplyContentPart[], pageUrl: string, source = false) for (const part of parts) { if ("href" in part) { try { - const href = source ? applyCompanyPrefix(part.href, extractCompanyPrefixFromPath(new URL(pageUrl).pathname)) : part.href; + const issueRef = source ? parseIssueReferenceFromHref(part.href) : null; + const href = issueRef ? applyCompanyPrefix(issueRef.href, extractCompanyPrefixFromPath(new URL(pageUrl).pathname)) : part.href; result.push({ href: new URL(href, pageUrl).href, label: part.label?.replace(/\s+/g, " ").trim(), decorated: part.decorated }); } catch { return null; } } else {