diff --git a/tests/runner-e2e/chat-catalog-domains/identity-confirmation-workflows.test.ts b/tests/runner-e2e/chat-catalog-domains/identity-confirmation-workflows.test.ts index b9aebfda337..69f98c5228c 100644 --- a/tests/runner-e2e/chat-catalog-domains/identity-confirmation-workflows.test.ts +++ b/tests/runner-e2e/chat-catalog-domains/identity-confirmation-workflows.test.ts @@ -18,7 +18,8 @@ function valid(id: string): IdentityObservation { Object.assign(o.state, { humanId: "alex", worker: { id: "morgan" }, submitStatuses: [409, 200], actualSubmitRequests: 2, noTaskBeforeAnswers: true, bothTabsResolved: true }); o.activity = [{ id: "answer", action: "issue.thread_interaction_answered", actorType: "user", details: { interactionId: "request" } }, { id: "document", action: "issue.document_created", entityId: "draft", agentId: "morgan" }]; - o.documents = [{ id: "introduction", body: "Acorn helps small teams write clear product introductions.", latestRevisionId: "revision", createdByRunId: "write" }]; + o.documents = [{ id: "introduction", body: "Acorn helps small teams write clear product introductions.", latestRevisionId: "revision" }]; + o.revisions = [{ id: "revision", documentId: "introduction", createdByRunId: "write", createdByAgentId: "morgan" }]; o.runs = [{ id: "query", companyId: "acorn", agentId: "ceo", status: "succeeded" }, { id: "continue", companyId: "acorn", agentId: "ceo", status: "succeeded", contextSnapshot: { interactionId: "request" } }, { id: "write", companyId: "acorn", agentId: "morgan", status: "succeeded" }]; @@ -41,6 +42,10 @@ describe("conversational proposal and simultaneous human answer oracles", () => }); it("an empty task or unattributed document is not an artifact result", () => { const o = valid("questions-and-approvals.duplicate-answer"); o.documents = []; expect(gradeConfirmationWorkflow(o).passed).toBe(false); - const wrong = valid("questions-and-approvals.duplicate-answer"); wrong.documents[0]!.createdByRunId = "unrelated-run"; expect(gradeConfirmationWorkflow(wrong).passed).toBe(false); + const wrong = valid("questions-and-approvals.duplicate-answer"); wrong.revisions[0]!.createdByRunId = "unrelated-run"; expect(gradeConfirmationWorkflow(wrong).passed).toBe(false); + }); + it("rejects a real revision attributed to another agent or another document", () => { + const o = valid("questions-and-approvals.duplicate-answer"); o.revisions[0]!.createdByAgentId = "unrelated-agent"; expect(gradeConfirmationWorkflow(o).passed).toBe(false); + const wrong = valid("questions-and-approvals.duplicate-answer"); wrong.revisions[0]!.documentId = "unrelated-document"; expect(gradeConfirmationWorkflow(wrong).passed).toBe(false); }); }); diff --git a/tests/runner-e2e/chat-catalog-domains/identity-confirmation-workflows.ts b/tests/runner-e2e/chat-catalog-domains/identity-confirmation-workflows.ts index f0907f5e49d..79331be9517 100644 --- a/tests/runner-e2e/chat-catalog-domains/identity-confirmation-workflows.ts +++ b/tests/runner-e2e/chat-catalog-domains/identity-confirmation-workflows.ts @@ -1,5 +1,6 @@ import { expect, type Page, type Route } from "@playwright/test"; import { createIssueThreadInteractionSchema } from "../../../packages/shared/src/validators/issue.js"; +import type { IssueDocument, IssueDocumentSummary, DocumentRevision } from "../../../packages/shared/src/types/issue.js"; import type { ChatCatalogCase, ChatCatalogContext } from "../chat-catalog-contract.js"; import { pollUntil } from "../api.js"; import { sendChatMessage } from "../chat-flow.js"; @@ -41,7 +42,8 @@ export function gradeConfirmationWorkflow(value: unknown) { c.add("one-actual-answer-artifact", o.tasks.length === 1 && o.tasks[0]?.companyId === o.companyId && hasLink(o, "issues", o.tasks[0]) && o.documents.length === 1 && (o.documents[0]?.body ?? "").includes("Acorn") && !!o.documents[0]?.latestRevisionId && o.activity.some(row => row.action === "issue.document_created" && row.entityId === o.tasks[0]?.id && [o.leadId, (o.state.worker as Entity | undefined)?.id].includes(row.agentId ?? "")) - && o.runs.some(run => run.id === o.documents[0]?.createdByRunId && [o.leadId, (o.state.worker as Entity | undefined)?.id].includes(run.agentId)), + && o.revisions.some(revision => revision.id === o.documents[0]?.latestRevisionId && revision.documentId === o.documents[0]?.id + && o.runs.some(run => run.id === revision.createdByRunId && run.agentId === revision.createdByAgentId && [o.leadId, (o.state.worker as Entity | undefined)?.id].includes(run.agentId))), "The one continuation produces one linked ordinary draft task and one real saved introduction document using Acorn; an empty task or a prose promise is insufficient."); } else c.add("known-workflow", false, "This workflow has no independent oracle."); return c.result(); @@ -54,7 +56,10 @@ async function observeWorkflow(ctx: ChatCatalogContext, state: Record row.id === entity(state, "original").id)?.result?.commentId; if (ctx.scenario.id === "questions-and-approvals.duplicate-answer" && o.tasks.length === 1) { const target = o.tasks[0]!; state.task = target; - o.documents = await Promise.all((await ctx.input.api.get(`/api/issues/${target.id}/documents`)).map(row => ctx.input.api.get(`/api/issues/${target.id}/documents/${encodeURIComponent(row.key!)}`))); + const documents = await Promise.all((await ctx.input.api.get(`/api/issues/${target.id}/documents`)).map(row => ctx.input.api.get(`/api/issues/${target.id}/documents/${encodeURIComponent(row.key)}`))); + o.documents = documents.map(row => ({ id: row.id, issueId: row.issueId, key: row.key, body: row.body, latestRevisionId: row.latestRevisionId ?? undefined })); + const revisions = (await Promise.all(documents.map(row => ctx.input.api.get(`/api/issues/${target.id}/documents/${encodeURIComponent(row.key)}/revisions`)))).flat(); + o.revisions = revisions.map(row => ({ id: row.id, documentId: row.documentId, createdByRunId: row.createdByRunId, createdByAgentId: row.createdByAgentId })); } return o; } diff --git a/tests/runner-e2e/chat-catalog-domains/identity-plan-review.test.ts b/tests/runner-e2e/chat-catalog-domains/identity-plan-review.test.ts new file mode 100644 index 00000000000..00d3b103388 --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/identity-plan-review.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { planReviewIdentityCases } from "./identity-plan-review.js"; +import type { IdentityObservation, Interaction } from "./identity-support.js"; +function valid(scenarioId: string): IdentityObservation { + const review = scenarioId.endsWith("review-plan"); + const original = { id: "proposal", kind: "request_confirmation", status: "pending", payload: { target: { revisionId: "reviewed" } } }; + const task = { id: "task", companyId: "acorn", title: "Acorn research launch", description: "Acorn research launch", assigneeAgentId: "morgan", status: "in_progress" }; + const current = { ...original, status: review ? "accepted" : "rejected", resolvedByAgentId: "ceo", result: { commentId: "canonical" } } as unknown as Interaction; + const o: IdentityObservation = { scenarioId, origin: "http://fixture.invalid", companyId: "acorn", leadId: "ceo", company: { id: "acorn" }, companies: [], agents: [], interactions: [current], tasks: [task], projects: [], goals: [], comments: [], documents: [], revisions: [], + activity: [{ id: "created", action: "issue.created", entityId: "task", agentId: "ceo" }], links: ["/ACO/issues/task"], reply: review ? "The reviewed plan is approved; Morgan is starting the task." : "The publication was declined. Review the local draft.", humanQueries: [], + runs: [{ id: "lead-run", agentId: "ceo", companyId: "acorn", status: "succeeded" }, { id: "worker-run", agentId: "morgan", companyId: "acorn", status: "succeeded", contextSnapshot: { issueId: "task" } }], + state: { original, plan: { id: "plan", latestRevisionId: "reviewed" }, sourcePlanRevisionAfter: "reviewed", sourceId: "chat", worker: { id: "morgan" }, scopeMarker: "Acorn research launch", sourceRetainsConversation: true, canonicalCommentId: "canonical", + staleAttemptStatus: 409, staleErrorCode: "interaction_stale_target", newerRevisionVisible: true, newerRevisionId: "new", staleRevisionId: "old", staleExpired: true, rejectedReceiptVisible: true, noPublishedWorkProduct: true } }; + if (review) o.activity.push({ id: "approved", action: "issue.thread_interaction_accepted", details: { interactionId: "proposal", responseCommentId: "canonical" } }); + else { o.documents.push({ id: "draft", issueId: "task", key: "plan", body: "# Acorn research launch — local draft for review", latestRevisionId: "draft-version" }); + o.revisions.push({ id: "draft-version", documentId: "draft", createdByRunId: "worker-run", createdByAgentId: "morgan" }); } + return o; +} +describe("versioned approval and revised publication scope", () => { + for (const d of planReviewIdentityCases) { + it(`${d.scenarioId}: independent valid and missing evidence`, () => { const o = valid(d.scenarioId); expect(d.grade(o), JSON.stringify(d.grade(o))).toMatchObject({ passed: true }); expect(d.grade(undefined).passed).toBe(false); }); + it(`${d.scenarioId}: changed target revision rejected`, () => { const o = valid(d.scenarioId); (o.state.plan as { latestRevisionId: string }).latestRevisionId = "unreviewed"; expect(d.grade(o).passed).toBe(false); }); + it(`${d.scenarioId}: response promise without executed result rejected`, () => { const o = valid(d.scenarioId); o.runs = []; expect(d.grade(o).passed).toBe(false); }); + } + it("rejects acceptance of a stale revision despite successful current work", () => { const d = planReviewIdentityCases[0]!; const o = valid(d.scenarioId); o.state.staleAttemptStatus = 200; expect(d.grade(o).passed).toBe(false); }); + it("rejects reusing approval on the publication after the draft request", () => { const d = planReviewIdentityCases[1]!; const o = valid(d.scenarioId); o.interactions[0]!.status = "accepted"; expect(d.grade(o).passed).toBe(false); }); + it("rejects a published work product while keeping a real local draft", () => { const d = planReviewIdentityCases[1]!; const o = valid(d.scenarioId); o.state.noPublishedWorkProduct = false; expect(d.grade(o).passed).toBe(false); }); + it("allows actual attributed local chat draft without unnecessarily requiring delegation", () => { const d = planReviewIdentityCases[1]!; const o = valid(d.scenarioId); o.tasks = []; o.documents[0] = { ...o.documents[0]!, issueId: "chat" }; o.revisions[0] = { ...o.revisions[0]!, createdByRunId: "lead-run", createdByAgentId: "ceo" }; o.links = ["#document-plan"]; expect(d.grade(o).passed).toBe(true); o.links = []; expect(d.grade(o).passed).toBe(false); }); + it("rejects an otherwise real draft revision written by an unrelated run", () => { const d = planReviewIdentityCases[1]!; const o = valid(d.scenarioId); o.revisions[0]!.createdByRunId = "unrelated-run"; expect(d.grade(o).passed).toBe(false); }); +}); diff --git a/tests/runner-e2e/chat-catalog-domains/identity-plan-review.ts b/tests/runner-e2e/chat-catalog-domains/identity-plan-review.ts new file mode 100644 index 00000000000..a51db4a5f8c --- /dev/null +++ b/tests/runner-e2e/chat-catalog-domains/identity-plan-review.ts @@ -0,0 +1,118 @@ +import { expect } from "@playwright/test"; +import { createIssueThreadInteractionSchema } from "../../../packages/shared/src/validators/issue.js"; +import type { ChatCatalogCase } from "../chat-catalog-contract.js"; +import type { IssueWorkProduct } from "../../../packages/shared/src/types/work-product.js"; +import type { IssueDocument, IssueDocumentSummary, DocumentRevision } from "../../../packages/shared/src/types/issue.js"; +import { pollUntil } from "../api.js"; +import { checks, entity, hasLink, putDocument, verifyInternalResult, type Entity } from "./internal-support.js"; +import { identityDefinition, identityObservation, identityWorker, observeIdentity, openStandingChat, type Interaction } from "./identity-support.js"; + +interface PlanInteraction extends Interaction { payload: Interaction["payload"] & { target?: { revisionId?: string } } } +export function gradePlanReview(value: unknown) { + const c = checks(); const o = identityObservation(value); c.add("complete-versioned-workflow", !!o, "Actual versioned decisions, attributed ordinary work and browser receipts are required."); if (!o) return c.result(); + const original = o.state.original as PlanInteraction | undefined; const current = o.interactions.find(row => row.id === original?.id) as PlanInteraction | undefined; + const plan = o.state.plan as Entity | undefined; const saved = o.tasks[0]; const worker = o.state.worker as Entity | undefined; + c.add("same-request-version", !!original && !!current && typeof plan?.latestRevisionId === "string" && JSON.stringify(current.payload) === JSON.stringify(original.payload) + && original.payload.target?.revisionId === plan?.latestRevisionId, + "The actual current request retains the reviewed source document revision without authorizing another version."); + if (o.scenarioId === "questions-and-approvals.review-plan") { + c.add("ordinary-linked-execution", o.tasks.length === 1 && saved?.companyId === o.companyId && saved.id !== o.state.sourceId && saved.parentId == null + && saved.assigneeAgentId === worker?.id && hasLink(o, "issues", saved) && o.activity.some(row => row.action === "issue.created" && row.entityId === saved.id && row.agentId === o.leadId) + && o.state.sourceRetainsConversation === true && o.runs.some(run => run.agentId === worker?.id && (run.contextSnapshot?.issueId === saved.id || (run as unknown as { nativeIssueId?: string }).nativeIssueId === saved.id)), + "The agent creates one linked ordinary task assigned to the real Morgan, whose actual admitted execution is bound to that task; chat remains a conversation."); + c.add("canonical-human-approval-bound", current?.status === "accepted" && current.resolvedByAgentId === o.leadId + && o.state.sourcePlanRevisionAfter === plan?.latestRevisionId && typeof o.state.canonicalCommentId === "string" && current.result?.commentId === o.state.canonicalCommentId + && o.activity.some(row => row.action === "issue.thread_interaction_accepted" && row.details?.interactionId === original?.id && row.details?.responseCommentId === o.state.canonicalCommentId), + "The exact canonical conversational approval is attached to the real reviewed revision and decision audit."); + c.add("reviewed-scope-handed-off", !!saved && `${saved.title} ${saved.description}`.includes(String(o.state.scopeMarker)), "The actual ordinary task retains the substantive scope of the approved plan."); + c.add("stale-review-shows-new-version", o.state.staleAttemptStatus === 409 && o.state.staleErrorCode === "interaction_stale_target" && o.state.newerRevisionVisible === true + && o.state.newerRevisionId !== o.state.staleRevisionId && o.state.staleExpired === true, + "A separately opened real old revision becomes unapprovable after a public edit; the browser shows the actual newer version instead of approving unseen work."); + } else if (o.scenarioId === "questions-and-approvals.reject-and-revise") { + c.add("publication-decision-declined", ["rejected", "superseded"].includes(current?.status ?? "") && !o.activity.some(row => row.action === "issue.thread_interaction_accepted" && row.details?.interactionId === original?.id) + && o.state.rejectedReceiptVisible === true, "The old publication request remains visibly declined or superseded and is never accepted."); + c.add("real-local-review-draft", o.documents.length > 0 && o.documents.some(row => (row.body ?? "").includes(String(o.state.scopeMarker)) && /draft|review/i.test(row.body ?? "") + && !!row.latestRevisionId && row.latestRevisionId !== plan?.latestRevisionId && o.revisions.some(revision => revision.id === row.latestRevisionId && revision.documentId === row.id + && o.runs.some(run => run.id === revision.createdByRunId && run.agentId === revision.createdByAgentId && [o.leadId, worker?.id].includes(run.agentId))) + && (row.issueId === o.state.sourceId ? o.links.some(link => link.includes(`#document-${encodeURIComponent(row.key!)}`)) : o.tasks.some(task => task.id === row.issueId && hasLink(o, "issues", task)))) + && o.state.sourceRetainsConversation === true && o.state.noPublishedWorkProduct === true && !/\b(?:I|we)(?:'ve| have)?\s+(?:published|deployed)\b/i.test(o.reply), + "The revised scope produces an actual attributed local review document and linked task, without a published delivery record or claimed publication."); + } else c.add("known-plan-workflow", false, "No oracle exists for this workflow."); + return c.result(); +} +const sources = ["chat-catalog-domains/identity-plan-review.ts", "chat-catalog-domains/identity-support.ts", "chat-catalog-domains/internal-support.ts", "chat-scenarios/questions-and-approvals.yaml"]; +export const planReviewIdentityCases: readonly ChatCatalogCase[] = ["questions-and-approvals.review-plan", "questions-and-approvals.reject-and-revise"].map(scenarioId => identityDefinition(scenarioId, async ctx => { + const source = await openStandingChat(ctx); const worker = await identityWorker(ctx); const review = scenarioId.endsWith("review-plan"); + const scopeMarker = `Acorn research launch ${ctx.input.nonce}`; + const plan = await putDocument(ctx, source.id, `# ${scopeMarker}\n${review ? "Create a local implementation checklist for a research notes website. Begin the assigned checklist work after the current plan revision is approved." : "Proposed action: publish the research notes introduction page. Its subject is a research notebook product for small science teams."}\nUse the exact project context ${scopeMarker}.`); + const original = await ctx.input.api.post(`/api/issues/${source.id}/interactions`, createIssueThreadInteractionSchema.parse({ kind: "request_confirmation", title: review ? "Approve current research launch plan" : "Publish research launch introduction", + resolverPolicy: "anyone", continuationPolicy: "none", payload: { version: 1, prompt: review ? "Approve the current plan and start its linked ordinary work?" : "Publish the proposed introduction page?", + detailsMarkdown: `Review the actual attached plan revision ${plan.latestRevisionNumber}. ${review ? "Approval hands off one ordinary task to Morgan, not the conversation." : "A different requested draft scope must decline this publication proposal and create a local review draft instead. Publication requires a separate explicit accepted decision."}`, + supersedeOnUserComment: false, target: { type: "issue_document", issueId: source.id, documentId: plan.id, key: "plan", revisionId: plan.latestRevisionId, revisionNumber: plan.latestRevisionNumber } } })); + await ctx.input.page.reload({ waitUntil: "domcontentloaded" }); return { sourceId: source.id, worker, plan, original, scopeMarker }; +}, gradePlanReview, { + sourceFiles: sources, authentication: "human-session", expectedRunCount: 3, minimumRunCount: scenarioId.endsWith("review-plan") ? 2 : 1, attemptTimeoutMs: 600_000, + runPolicy: { kind: "source-settled-with-active-work", maximumActiveWorkerRuns: 1 }, + async act(ctx, state) { + await pollUntil({ label: "Plan decision source settled", deadlineAt: Date.now() + 180_000, + load: async () => { await ctx.refreshIssue(); return { source: ctx.issue(), runs: await ctx.allRuns() }; }, + accept: value => value.source?.conversationState === "waiting" && value.runs.filter(run => run.agentId === ctx.input.fixtures.agent.id).length === 1 + && value.runs.filter(run => run.agentId === ctx.input.fixtures.agent.id).every(run => run.status === "succeeded") }); + const original = entity(state, "original"); + if (scenarioId.endsWith("reject-and-revise")) { + const card = ctx.input.page.locator(`#interaction-${original.id}`); await expect(card).toContainText(/Declined|Superseded/i); state.rejectedReceiptVisible = true; return; + } + // A separate additional review opens before its actual public document edit. + // The first canonical approval's source plan is never rewritten by this check. + const target = await ctx.input.api.post(`/api/companies/${ctx.input.fixtures.company.id}/issues`, { title: "Separate stale review fixture", status: "backlog" }); + state.staleTaskId = target.id; + const old = await putDocument(ctx, target.id, "Old review: unrestricted publication."); + const stale = await ctx.input.api.post(`/api/issues/${target.id}/interactions`, createIssueThreadInteractionSchema.parse({ kind: "request_confirmation", title: "Review before edit", resolverPolicy: "human_only", continuationPolicy: "none", + payload: { version: 1, prompt: "Approve the currently opened older review?", supersedeOnUserComment: false, target: { type: "issue_document", issueId: target.id, documentId: old.id, key: "plan", revisionId: old.latestRevisionId, revisionNumber: old.latestRevisionNumber } } })); + const current = await putDocument(ctx, target.id, "Newer review: local draft only, do not publish.", old.latestRevisionId!); + const response = await ctx.input.page.context().request.post(`${ctx.input.api.baseURL}/api/issues/${target.id}/interactions/${stale.id}/accept`, { data: {}, headers: { Origin: ctx.input.api.baseURL } }); + state.staleAttemptStatus = response.status(); const body = await response.json() as { code?: string; details?: { code?: string } }; state.staleErrorCode = body.details?.code ?? body.code; + state.staleRevisionId = old.latestRevisionId; state.newerRevisionId = current.latestRevisionId; + const interactions = await ctx.input.api.get(`/api/issues/${target.id}/interactions`); state.staleExpired = interactions.find(row => row.id === stale.id)?.status === "expired"; + const source = ctx.input.page.url(); await ctx.input.page.goto(`/${ctx.input.fixtures.company.issuePrefix}/issues/${target.identifier ?? target.id}`, { waitUntil: "domcontentloaded" }); + // The supported plan deep link expands the real current document, not text fabricated by the harness. + await ctx.input.page.goto(`/${ctx.input.fixtures.company.issuePrefix}/issues/${target.identifier ?? target.id}#document-plan&viewer=full`, { waitUntil: "domcontentloaded" }); + await expect(ctx.input.page.getByText("Newer review: local draft only, do not publish.", { exact: false }).first()).toBeVisible(); state.newerRevisionVisible = true; + await ctx.input.page.goto(source, { waitUntil: "domcontentloaded" }); + await ctx.input.evidence("identity-stale-plan-review.json", { oldRevisionId: old.latestRevisionId, newRevisionId: current.latestRevisionId, staleAttemptStatus: state.staleAttemptStatus, staleErrorCode: state.staleErrorCode, newerRevisionVisible: true }); + }, + async observe(ctx, state) { + const o = await observeIdentity(ctx, state); o.tasks = o.tasks.filter(row => row.id !== state.staleTaskId); + const comments = await ctx.input.api.get(`/api/issues/${ctx.issue()!.id}/comments?order=asc`); state.canonicalCommentId = comments.find(row => row.authorUserId && row.body?.trim() === ctx.scenario.query)?.id; + state.sourcePlanRevisionAfter = (await ctx.input.api.get(`/api/issues/${ctx.issue()!.id}/documents/plan`)).latestRevisionId; + state.sourceRetainsConversation = ctx.issue()?.assigneeAgentId === ctx.input.fixtures.agent.id && ctx.issue()?.status === "in_review" && ctx.issue()?.conversationState === "waiting"; + o.documents = []; + let published = false; + const documentIssueIds = [...o.tasks.map(task => task.id), ...(scenarioId.endsWith("reject-and-revise") ? [ctx.issue()!.id] : [])]; + for (const issueId of documentIssueIds) { + const docs = await ctx.input.api.get(`/api/issues/${issueId}/documents`); + const full = await Promise.all(docs.map(doc => ctx.input.api.get(`/api/issues/${issueId}/documents/${encodeURIComponent(doc.key)}`))); + o.documents.push(...full.map(doc => ({ id: doc.id, issueId: doc.issueId, key: doc.key, body: doc.body, latestRevisionId: doc.latestRevisionId ?? undefined }))); + } + for (const issueId of [ctx.issue()!.id, ...o.tasks.map(task => task.id)]) { + const products = await ctx.input.api.get(`/api/issues/${issueId}/work-products`); + published ||= products.some(product => !["document", "artifact"].includes(product.type) || product.provider !== "paperclip" + || product.url != null && new URL(product.url, ctx.input.api.baseURL).origin !== ctx.input.api.baseURL); + } + const revisions = (await Promise.all(o.documents.map(row => ctx.input.api.get(`/api/issues/${row.issueId}/documents/${encodeURIComponent(row.key!)}/revisions`)))).flat(); + o.revisions = revisions.map(row => ({ id: row.id, documentId: row.documentId, createdByRunId: row.createdByRunId, createdByAgentId: row.createdByAgentId })); + state.noPublishedWorkProduct = !published; return o; + }, + async verifyResult(ctx, state, value) { + const o = identityObservation(value); + if (scenarioId.endsWith("review-plan") || o?.tasks.length) { await verifyInternalResult(ctx, state, value); return; } + const doc = o?.documents.find(row => o.revisions.some(revision => revision.id === row.latestRevisionId && revision.documentId === row.id && revision.createdByRunId) + && (row.body ?? "").includes(String(state.scopeMarker))); + if (!doc) throw new Error("Actual local review draft is missing"); + const href = await ctx.input.page.locator('[id^="comment-"] a[href]').evaluateAll(links => links.map(link => link.getAttribute("href")!)); + const reviewLink = href.find(link => link.includes(`#document-${encodeURIComponent(doc.key!)}`)); + if (!reviewLink) throw new Error("Actual draft review link missing from reply"); + await ctx.input.page.locator(`[id^="comment-"] a[href=${JSON.stringify(reviewLink)}]`).first().click(); + await expect(ctx.input.page.getByText(String(state.scopeMarker), { exact: false }).first()).toBeVisible(); + }, +}));