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", "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:
`` }));
- 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 {