Bind ambiguous-task clarification to visible linked question cards

This commit is contained in:
Dotta committed 2026-10-28 14:37:29 -05:00
1 parent bcdcd0b242
commit ed762ce875
6 files changed
+246 -8

No files matched your search

@@ -5637,7 +5637,8 @@
"chat-catalog-domains/internal-support.ts",
"chat-catalog-domains/internal-tasks.ts",
"chat-scenarios/tasks.yaml",
"chat-catalog-domains/internal-distinct-fixtures.ts"
"chat-catalog-domains/internal-distinct-fixtures.ts",
"chat-linked-question.ts"
],
"oracleVersion": "paperclip.chat-catalog.internal.v1",
"coverageGaps": [],
@@ -1,4 +1,5 @@
import { savedDesignInstruction, withRenderedCommentReference, validRenderedCommentReference } from "./internal-comment-reference.js";
import { linkedQuestionTakeover, observeLinkedTaskQuestion } from "../chat-linked-question.js";
import { withEditOwnership } from "./internal-edit-ownership.js";
import { verifyDistinctTaskFixtures } from "./internal-distinct-fixtures.js";
import { delegatedTaskCases } from "./internal-task-creation.js";
@@ -90,28 +91,37 @@ function ambiguousTask(): ChatCatalogCase {
await verifyDistinctTaskFixtures(ctx, [t, otherTask]);
return { project: p, task: t, otherProject, otherTask };
});
return { ...base, sourceFiles: [...base.sourceFiles, "chat-catalog-domains/internal-distinct-fixtures.ts"], expectedRunCount: 2, expectedHumanMessageCount: 2, attemptTimeoutMs: 420_000,
return { ...base, sourceFiles: [...base.sourceFiles, "chat-catalog-domains/internal-distinct-fixtures.ts", "chat-linked-question.ts"], expectedRunCount: 2, expectedHumanMessageCount: 2, attemptTimeoutMs: 420_000,
async act(ctx, state) {
const first = await pollUntil({ label: "Task ambiguity clarification", deadlineAt: Date.now() + 150_000,
const plain = await pollUntil({ label: "Task ambiguity clarification", deadlineAt: Date.now() + 150_000,
load: () => observeInternal(ctx, state), accept: o => o.runs.length === 1 && o.runs.every(row => row.status === "succeeded")
&& ctx.issue()?.conversationState === "waiting" });
const first = await observeLinkedTaskQuestion(ctx, state, plain);
state.clarification = { ...first, state: structuredClone(state) };
await ctx.input.evidence("chat-catalog-task-clarification.json", state.clarification);
const selected = entity(state, "task"); const other = entity(state, "otherTask");
if (!sameTask(selected, first.tasks.find(row => row.id === selected.id)) || !sameTask(other, first.tasks.find(row => row.id === other.id)))
throw new Error("Ambiguous task changed before the person chose a source");
const answer = `The Landing page task in Launch (${selected.identifier ?? selected.id}).`;
const textInput = ctx.input.page.getByTestId("question-text-answer-composer").last();
const card = first.linkedQuestionEvidence;
if (card) {
const fresh = (await observeLinkedTaskQuestion(ctx, state, plain)).linkedQuestionEvidence;
if (!fresh || fresh.interactionId !== card.interactionId || fresh.questionId !== card.questionId || fresh.payloadDigest !== card.payloadDigest)
throw new Error("The actual linked clarification question changed before the person's selection");
}
const answerRoot = card ? linkedQuestionTakeover(ctx.input.page, card) : ctx.input.page;
const textInput = answerRoot.getByTestId("question-text-answer-composer").last();
if (await textInput.isVisible()) {
await textInput.locator('[contenteditable="true"],textarea').first().fill(answer);
await ctx.input.page.getByRole("button", { name: /Submit answers|Submit|Continue/, exact: true }).last().click();
await answerRoot.getByRole("button", { name: /Submit answers|Submit|Continue/, exact: true }).last().click();
} else {
const otherChoice = ctx.input.page.getByRole("radio", { name: "Other", exact: true }).last();
const otherChoice = answerRoot.getByRole("radio", { name: "Other", exact: true }).last();
if (await otherChoice.isVisible()) {
await otherChoice.click();
await ctx.input.page.getByTestId("question-other-answer-composer").last().locator('[contenteditable="true"],textarea').first().fill(answer);
await ctx.input.page.getByRole("button", { name: /Submit answers|Submit|Continue/, exact: true }).last().click();
await answerRoot.getByTestId("question-other-answer-composer").last().locator('[contenteditable="true"],textarea').first().fill(answer);
await answerRoot.getByRole("button", { name: /Submit answers|Submit|Continue/, exact: true }).last().click();
} else {
if (card) throw new Error("The linked question has no supported explicit clarification input");
const { sendChatMessage } = await import("../chat-flow.js"); await sendChatMessage(ctx.input.page, answer);
}
}
@@ -0,0 +1,31 @@
import { expect, it } from "vitest";
import { linkedQuestionBinding } from "./chat-linked-question.js";
import { valid } from "./chat-linked-question.test-fixture.js";
import { grade_tasks } from "./chat-catalog-domains/internal-tasks.js";
import { createIssueThreadInteractionSchema } from "../../packages/shared/src/validators/issue.js";
it("accepts the actual public modern-question contract and its compatibility storage projection", () => {
const input = valid();
const parsed = createIssueThreadInteractionSchema.parse({ kind: "ask_user_questions", payload: input.interactions[0]!.payload });
expect(parsed.kind).toBe("ask_user_questions");
if (parsed.kind !== "ask_user_questions") throw new Error("Unexpected question contract");
expect(parsed.payload.questions[0]?.id).toBe("which-task");
input.interactions[0]!.payload = parsed.payload;
expect(linkedQuestionBinding(input)?.questionId).toBe("which-task");
});
it("preserves original no-early-write, exact person selection, mutation actor and final link checks", () => {
const input = valid(), binding = linkedQuestionBinding(input)!;
const evidence = { text: binding.prompt, links: binding.links.map(link => link.href) };
const first = { ...input.observation, reply: evidence.text, links: evidence.links };
const final = { ...structuredClone(input.observation), state: { ...input.observation.state, clarification: first },
tasks: [{ ...input.observation.tasks[0]!, priority: "high" }, structuredClone(input.observation.tasks[1]!)],
links: ["/RUN/issues/RUN-1"], activity: [{ id: "update", action: "issue.updated", entityId: "launch-task", agentId: "ceo" }] };
expect(grade_tasks({ ...final, state: { ...final.state, clarification: { ...first, reply: "", links: [] } } }).passed).toBe(false);
expect(grade_tasks(final).passed).toBe(true);
const early = structuredClone(final); early.state.clarification.tasks[0]!.priority = "high";
expect(grade_tasks(early).passed).toBe(false);
const wrong = structuredClone(final); wrong.tasks[0]!.priority = "medium"; wrong.tasks[1]!.priority = "high";
expect(grade_tasks(wrong).passed).toBe(false);
const actor = structuredClone(final); actor.activity[0]!.agentId = "other";
expect(grade_tasks(actor).passed).toBe(false);
expect(grade_tasks({ ...final, links: [] }).passed).toBe(false);
});
@@ -0,0 +1,25 @@
import type { linkedQuestionBinding } from "./chat-linked-question.js";
export type Input = Parameters<typeof linkedQuestionBinding>[0];
export function valid(): Input {
const task = { id: "launch-task", identifier: "RUN-1", companyId: "company", title: "Landing page", priority: "medium", projectId: "launch" };
const other = { ...task, id: "experiment-task", identifier: "RUN-2", projectId: "experiments" };
const query = "Mark the landing-page task as high priority.";
return { origin: "https://fixture.test", companyPrefix: "RUN", sourceId: "source", leadId: "ceo", query, tasks: [task, other],
comments: [{ id: "human", companyId: "company", issueId: "source", body: query, authorUserId: "person", authorAgentId: null }],
observation: { scenarioId: "tasks.ambiguous-task", origin: "https://fixture.test", companyId: "company", leadId: "ceo", state: { task, otherTask: other },
tasks: [structuredClone(task), structuredClone(other)], projects: [], goals: [], comments: [], documents: [], revisions: [], links: [], reply: "", humanQueries: [query],
runs: [{ id: "run", companyId: "company", agentId: "ceo", status: "succeeded", runtimeMode: "native", contextSnapshot: { issueId: "source" }, startedAt: "2026-10-28T12:00:00Z", finishedAt: "2026-10-28T12:01:00Z" }],
activity: [{ id: "action", companyId: "company", action: "issue.thread_interaction_created", entityType: "issue", entityId: "source", actorType: "agent", actorId: "ceo", agentId: "ceo", runId: "run", details: { interactionId: "question", interactionKind: "ask_user_questions" } } as never] },
interactions: [{ id: "question", companyId: "company", issueId: "source", kind: "ask_user_questions", status: "pending", createdByAgentId: "ceo", createdByUserId: null,
sourceRunId: "run", originCommentIds: ["human"], addresseeUserId: "person", addresseeAgentId: null, effectiveResolverPolicy: "human_only", createdAt: "2026-10-28T12:00:30Z",
payload: { version: 1, questionSet: { schema: "paperclip.question_set.v1", description: "Choose [Launch Landing page](/RUN/issues/RUN-1) or [Experiments Landing page](/RUN/issues/experiment-task) for High priority.",
questions: [{ id: "which-task", prompt: "Which Landing page task should be High priority?", answerMode: "text", required: true }] } } } as never] };
}
export function dom() {
return `<main data-testid="task-chat-thread"><div id="comment-human">Mark the landing-page task as high priority.</div>
<div data-thread-anchor="question" data-thread-item-kind="interaction"><button id="interaction-question" data-testid="task-chat-unanswered-question" aria-label="Answer question: Which Landing page task should be High priority?">Unanswered question</button></div>
<section data-testid="task-chat-composer-takeover"><div data-testid="task-chat-composer-takeover-body">
<div role="region" aria-label="Question context">Choose <a href="/RUN/issues/RUN-1">Launch Landing page</a> or <a href="/RUN/issues/experiment-task">Experiments Landing page</a> for High priority.</div>
<p id="question-which-task-prompt">Which Landing page task should be High priority?</p><div data-testid="question-text-answer-composer" aria-labelledby="question-which-task-prompt"></div><button>Submit answers</button>
</div></section></main>`;
}
@@ -0,0 +1,74 @@
// @vitest-environment jsdom
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import { linkedQuestionBinding, readLinkedQuestionDom } from "./chat-linked-question.js";
import { valid, dom, type Input } from "./chat-linked-question.test-fixture.js";
beforeEach(() => { document.body.innerHTML = dom(); vi.spyOn(Element.prototype, "getClientRects").mockReturnValue([{ width: 20, height: 20 }] as unknown as DOMRectList); });
afterEach(() => vi.restoreAllMocks());
const collect = (input = valid()) => { const bound = linkedQuestionBinding(input); return bound && readLinkedQuestionDom(bound); };
it("collects an actual production question takeover's two visible saved-task links with no ordinary reply", () => {
expect(document.querySelectorAll('[id^="comment-"] a')).toHaveLength(0);
const evidence = collect();
expect(evidence?.links).toEqual(["/RUN/issues/RUN-1", "/RUN/issues/experiment-task"]);
expect(evidence?.text).toBe("Which Landing page task should be High priority?");
expect(evidence).toMatchObject({ interactionId: "question", questionId: "which-task", anchorCommentId: "human", sourceRunId: "run", leadId: "ceo" });
});
it.each([
["wrong company", (v: Input) => { v.interactions[0]!.companyId = "foreign"; }],
["wrong conversation", (v: Input) => { v.interactions[0]!.issueId = "other-source"; }],
["wrong CEO", (v: Input) => { v.interactions[0]!.createdByAgentId = "another-agent"; }],
["human-created card", (v: Input) => { v.interactions[0]!.createdByUserId = "person"; }],
["wrong run", (v: Input) => { v.interactions[0]!.sourceRunId = "old-run"; }],
["stale card", (v: Input) => { v.interactions[0]!.createdAt = "2026-10-27T12:00:00Z"; }],
["future card", (v: Input) => { v.interactions[0]!.createdAt = "2026-10-29T12:00:00Z"; }],
["resolved card", (v: Input) => { v.interactions[0]!.status = "accepted"; }],
["dismissed card", (v: Input) => { v.interactions[0]!.dismissedAt = "2026-10-28T12:00:40Z"; }],
["wrong addressed person", (v: Input) => { v.interactions[0]!.addresseeUserId = "other"; }],
["agent addressed", (v: Input) => { v.interactions[0]!.addresseeAgentId = "ceo"; }],
["invalid resolver", (v: Input) => { v.interactions[0]!.effectiveResolverPolicy = "invalid" as never; }],
["wrong origin question", (v: Input) => { v.interactions[0]!.originCommentIds = ["old-human"]; }],
["foreign origin comment", (v: Input) => { v.interactions[0]!.originCommentIds!.push("foreign-comment"); }],
["wrong source comment", (v: Input) => { v.interactions[0]!.sourceCommentId = "other-human"; }],
["missing create action", (v: Input) => { v.observation.activity = []; }],
["wrong create actor", (v: Input) => { v.observation.activity[0]!.actorId = "other"; }],
["wrong create company", (v: Input) => { v.observation.activity[0]!.companyId = "foreign"; }],
["wrong action run", (v: Input) => { Object.assign(v.observation.activity[0]!, { runId: "old-run" }); }],
["wrong action resource kind", (v: Input) => { Object.assign(v.observation.activity[0]!, { entityType: "project" }); }],
["wrong action question", (v: Input) => { v.observation.activity[0]!.details!.interactionId = "other"; }],
["wrong action kind", (v: Input) => { v.observation.activity[0]!.details!.interactionKind = "request_confirmation"; }],
["legacy run", (v: Input) => { v.observation.runs[0]!.runtimeMode = "legacy"; }],
["unrelated run issue", (v: Input) => { v.observation.runs[0]!.contextSnapshot = { issueId: "other" }; }],
["foreign saved task", (v: Input) => { v.tasks[0]!.companyId = "foreign"; }],
["wrong actual task", (v: Input) => { v.observation.tasks[0]!.id = "other"; }],
["duplicate saved task", (v: Input) => { v.tasks[1] = v.tasks[0]!; }],
["old human request", (v: Input) => { v.comments[0]!.body = "another query"; }],
["nonhuman original request", (v: Input) => { v.comments[0]!.authorAgentId = "ceo"; }],
["missing linked context", (v: Input) => { v.interactions[0]!.payload.questionSet!.description = "Choose a task."; }],
["another native action", (v: Input) => { v.interactions.push(structuredClone(v.interactions[0]!)); }],
] as const)("rejects %s metadata rather than any question-card presence", (_, change) => {
const input = valid(); change(input); expect(collect(input)).toBeUndefined();
});
it.each([
["foreign company", "/OTHER/issues/RUN-1"], ["foreign origin", "https://foreign.test/RUN/issues/RUN-1"], ["wrong task", "/RUN/issues/wrong"],
["task search", "/RUN/issues?search=RUN-1"], ["fragment", "/RUN/issues/RUN-1#unrelated"],
] as const)("rejects %s saved question link", (_, href) => {
const input = valid(); input.interactions[0]!.payload.questionSet!.description = input.interactions[0]!.payload.questionSet!.description!.replace("/RUN/issues/RUN-1", href);
expect(collect(input)).toBeUndefined();
});
const domNegatives: Array<[string, () => void]> = [
["missing exact interaction", () => document.querySelector("button")!.id = "interaction-other"],
["wrong visible question", () => document.querySelector("p")!.textContent = "Another question?"],
["wrong question binding", () => document.querySelector("p")!.id = "question-other-prompt"],
["hidden takeover", () => document.querySelector("section")!.hidden = true],
["hidden prompt", () => document.querySelector("p")!.setAttribute("aria-hidden", "true")],
["hidden linked task", () => document.querySelector("a")!.setAttribute("style", "display:none")],
["wrong DOM link", () => document.querySelector("a")!.setAttribute("href", "/RUN/issues/wrong")],
["sidebar link only", () => document.body.append(document.querySelector("a")!)],
["unrelated conversation", () => document.querySelector("main")!.querySelector("#comment-human")!.remove()],
["ambiguous conversation", () => document.body.insertAdjacentHTML("beforeend", dom())],
["unrelated takeover", () => document.querySelector("section")!.setAttribute("data-testid", "another-takeover")],
];
it.each(domNegatives)("rejects %s DOM evidence", (_, change) => {
change();
expect(collect()).toBeUndefined();
});
+97
View File
@@ -0,0 +1,97 @@
import { createHash } from "node:crypto";
import type { Locator, Page } from "@playwright/test";
import type { AskUserQuestionsInteraction } from "../../packages/shared/src/types/issue.js";
import type { ChatCatalogContext } from "./chat-catalog-contract.js";
import type { InternalObservation, Entity } from "./chat-catalog-domains/internal-support.js";
interface QuestionInput {
origin: string; companyPrefix: string; sourceId: string; leadId: string; query: string;
observation: InternalObservation & { runs: Array<InternalObservation["runs"][number] & { finishedAt?: string | null }> };
comments: Entity[]; interactions: AskUserQuestionsInteraction[]; tasks: Entity[];
}
export function linkedQuestionBinding(input: QuestionInput) {
const o = input.observation, run = o.runs[0];
if (o.runs.length !== 1 || !run || run.companyId !== o.companyId || run.agentId !== input.leadId || run.runtimeMode !== "native" || run.status !== "succeeded"
|| run.contextSnapshot?.issueId !== input.sourceId || input.tasks.length !== 2 || new Set(input.tasks.map(task => task.id)).size !== 2
|| input.tasks.some(task => task.companyId !== o.companyId || !o.tasks.some(saved => saved.id === task.id && saved.companyId === task.companyId))) return;
const humans = input.comments.filter(comment => comment.companyId === o.companyId && comment.issueId === input.sourceId && !comment.authorAgentId
&& !!comment.authorUserId && comment.body === input.query);
if (humans.length !== 1 || o.humanQueries.length !== 1 || o.humanQueries[0] !== input.query) return;
const human = humans[0]!, start = Date.parse(run.startedAt ?? ""), end = Date.parse(run.finishedAt ?? "");
if (!Number.isFinite(start) || !Number.isFinite(end)) return;
const candidates = input.interactions.filter(card => card.kind === "ask_user_questions" && card.status === "pending" && !card.dismissedAt && !card.resolvedAt
&& card.companyId === o.companyId && card.issueId === input.sourceId && card.createdByAgentId === input.leadId && !card.createdByUserId
&& card.sourceRunId === run.id && !card.addresseeAgentId && (!card.addresseeUserId || card.addresseeUserId === human.authorUserId)
&& ["anyone", "human_only", "not_creator"].includes(card.effectiveResolverPolicy)
&& (card.originCommentIds?.includes(human.id) || card.sourceCommentId === human.id)
&& (card.originCommentIds ?? []).every(id => input.comments.some(comment => comment.id === id && comment.companyId === o.companyId && comment.issueId === input.sourceId))
&& (!card.sourceCommentId || card.sourceCommentId === human.id)
&& Date.parse(String(card.createdAt)) >= start && Date.parse(String(card.createdAt)) <= end
&& o.activity.some(event => event.companyId === o.companyId && event.action === "issue.thread_interaction_created"
&& (event as Entity & { entityType?: string }).entityType === "issue" && event.entityId === input.sourceId
&& event.actorType === "agent" && event.actorId === input.leadId && event.agentId === input.leadId
&& (event as Entity & { runId?: string }).runId === run.id && event.details?.interactionId === card.id && event.details?.interactionKind === "ask_user_questions"));
if (candidates.length !== 1) return;
const card = candidates[0]!, set = card.payload.questionSet, question = set?.questions[0];
if (set?.schema !== "paperclip.question_set.v1" || !set.description || !question?.id || !question.prompt) return;
const hrefs = [...set.description.matchAll(/\[[^\]]+\]\(([^)\s]+)\)/g)].map(match => match[1]!);
const links = hrefs.map(href => {
try {
const url = new URL(href, input.origin), parts = url.pathname.split("/").filter(Boolean).map(decodeURIComponent);
if (url.origin !== new URL(input.origin).origin || url.search || url.hash || parts.length !== 3 || parts[0] !== input.companyPrefix || parts[1] !== "issues") return;
const task = input.tasks.find(task => [task.id, task.identifier].includes(parts[2]));
return task ? { href, taskId: task.id } : undefined;
} catch { return; }
});
if (!links.length || links.some(link => !link) || new Set(links.map(link => link!.taskId)).size !== 2) return;
return { origin: input.origin, sourceId: input.sourceId, anchorCommentId: human.id, interactionId: card.id, questionId: question.id,
prompt: question.prompt, sourceRunId: run.id, leadId: input.leadId,
payloadDigest: createHash("sha256").update(JSON.stringify(card.payload)).digest("hex"), links: links as Array<{ href: string; taskId: string }> };
}
export function readLinkedQuestionDom(input: NonNullable<ReturnType<typeof linkedQuestionBinding>>) {
const visible = (element: Element) => {
for (let node: Element | null = element; node; node = node.parentElement) {
if (node.hasAttribute("hidden") || node.hasAttribute("inert") || node.getAttribute("aria-hidden") === "true") return false;
const style = window.getComputedStyle(node);
if (style.display === "none" || ["hidden", "collapse"].includes(style.visibility) || style.opacity === "0") return false;
}
return element.getClientRects().length > 0;
};
const selector = '[data-testid="task-chat-thread"]', clean = (value: string) => value.replace(/\s+/g, " ").trim();
const roots = Array.from(document.querySelectorAll(selector)).filter(root => visible(root) && Array.from(root.querySelectorAll("[id]")).some(node =>
node.id === `comment-${input.anchorCommentId}` && node.closest(selector) === root));
if (roots.length !== 1) return;
const root = roots[0]!, same = (node: Element) => node.closest(selector) === root;
const button = Array.from(root.querySelectorAll('[data-testid="task-chat-unanswered-question"]')).filter(node =>
node.id === `interaction-${input.interactionId}` && same(node) && visible(node) && node.getAttribute("aria-label") === `Answer question: ${input.prompt}`);
if (button.length !== 1) return;
const takeovers = Array.from(root.querySelectorAll('[data-testid="task-chat-composer-takeover"]')).filter(node => visible(node) && same(node));
if (takeovers.length !== 1) return;
const takeover = takeovers[0]!, prompts = Array.from(takeover.querySelectorAll("[id]")).filter(node => node.id === `${input.interactionId}-${input.questionId}-prompt`
&& visible(node) && same(node) && clean(node.textContent ?? "") === clean(input.prompt));
const contexts = Array.from(takeover.querySelectorAll('[role="region"][aria-label="Question context"]')).filter(node => visible(node) && same(node));
if (prompts.length !== 1 || contexts.length !== 1) return;
const links = Array.from(contexts[0]!.querySelectorAll("a[href]")).filter(node => visible(node) && same(node)).map(node => node.getAttribute("href")!);
if (!links.length || links.some(href => !input.links.some(link => link.href === href))
|| new Set(input.links.filter(link => links.includes(link.href)).map(link => link.taskId)).size !== 2) return;
return { ...input, links: [...new Set(links)], text: prompts[0]!.textContent ?? "" };
}
export type LinkedQuestionEvidence = NonNullable<ReturnType<typeof readLinkedQuestionDom>>;
export type ClarificationObservation = InternalObservation & { linkedQuestionEvidence?: LinkedQuestionEvidence };
export async function observeLinkedTaskQuestion(ctx: ChatCatalogContext, state: Record<string, unknown>, observation: InternalObservation): Promise<ClarificationObservation> {
const source = ctx.issue(), prefix = ctx.input.fixtures.company.issuePrefix;
if (!source || !prefix || ctx.scenario.id !== "tasks.ambiguous-task") return observation;
const [comments, interactions] = await Promise.all([
ctx.input.api.get<Entity[]>(`/api/issues/${source.id}/comments?order=asc`),
ctx.input.api.get<AskUserQuestionsInteraction[]>(`/api/issues/${source.id}/interactions`),
]);
const binding = linkedQuestionBinding({ origin: observation.origin, companyPrefix: prefix, sourceId: source.id, leadId: observation.leadId,
query: ctx.scenario.query, observation, comments, interactions, tasks: [state.task as Entity, state.otherTask as Entity] });
if (!binding) return observation;
const evidence = await ctx.input.page.evaluate(readLinkedQuestionDom, binding);
return evidence ? { ...observation, reply: [observation.reply, evidence.text].filter(Boolean).join("\n"), links: [...new Set([...observation.links, ...evidence.links])], linkedQuestionEvidence: evidence } : observation;
}
export function linkedQuestionTakeover(page: Page, evidence: LinkedQuestionEvidence): Locator {
return page.getByTestId("task-chat-thread").filter({ has: page.locator(`[id=${JSON.stringify(`comment-${evidence.anchorCommentId}`)}]`) })
.getByTestId("task-chat-composer-takeover").filter({ has: page.locator(`[id=${JSON.stringify(`${evidence.interactionId}-${evidence.questionId}-prompt`)}]`) });
}