diff --git a/tests/runner-e2e/cursor-native-cases.test.ts b/tests/runner-e2e/cursor-native-cases.test.ts index d0bdb73f752..63100304b1a 100644 --- a/tests/runner-e2e/cursor-native-cases.test.ts +++ b/tests/runner-e2e/cursor-native-cases.test.ts @@ -1,3 +1,6 @@ +import { parsePaperclipQuestionResponse } from "../../packages/paperclip-runner/src/contracts/question-set.js"; +import { normalizeCursorPlanRequest } from "../../packages/paperclip-runner/src/drivers/acpx/cursor-extensions.js"; +import { cursorNativePlanResponse } from "./cursor-native-flow.js"; import { cursorDeniedCommand, type CursorToolNotice } from "./cursor-native-evidence.js"; import { describe, expect, it } from "vitest"; import { cursorNativeCaseDesigns, cursorNativePrompt, hasDeliveredCursorNativeRequest, hasCursorPlanDecision, hasCursorDenialBoundary, CURSOR_DENIAL_SAMPLE_PHASES, hasExactCursorNativeResponse } from "./cursor-native-cases.js"; @@ -62,3 +65,23 @@ it("requires exact delivered answer bytes rather than a saved or different answe expect(hasExactCursorNativeResponse({ ...input, response: { ...response, answers: { q: { selectedOptionIds: ["other"] } } } })).toBe(false); expect(hasExactCursorNativeResponse({ ...input, events: rows.slice(0, 1) })).toBe(false); }); + + +it.each(["accept", "cancel", "reject"] as const)("recognizes canonical delivered native plan %s without an empty optional answer", decision => { + const native = normalizeCursorPlanRequest({ toolCallId: "native-plan", plan: "Verify the revised plan.", todos: [] }); + const planId = native.questionSet.questions[0]!.id; + const response = cursorNativePlanResponse(planId, decision, " Keep exact feedback 漢字\nSecond line "); + const delivered = parsePaperclipQuestionResponse(native.questionSet, response); + const rows = proof(); + (rows[0]!.payload.prpEvent.payload as any).request.origin.method = "cursor/create_plan"; + (rows[1]!.payload.prpEvent.payload as any).response = delivered; + const input = { events: rows, runId: "run", turnId: "turn", requestId: "request", method: "cursor/create_plan" as const, action: "submit" as const, response }; + expect(hasCursorPlanDecision(native.questionSet, response, decision)).toBe(true); + expect(hasExactCursorNativeResponse(input)).toBe(true); + expect(native.resolve(delivered).outcome.outcome).toBe({ accept: "accepted", cancel: "cancelled", reject: "rejected" }[decision]); + if (decision === "reject") expect(delivered.answers.reason?.text).toBe(" Keep exact feedback 漢字\nSecond line "); + else { + expect(delivered.answers).not.toHaveProperty("reason"); + expect(hasExactCursorNativeResponse({ ...input, response: { ...response, answers: { ...response.answers, reason: {} } } })).toBe(false); + } +}); diff --git a/tests/runner-e2e/cursor-native-flow.ts b/tests/runner-e2e/cursor-native-flow.ts index 32e84dd349f..c55013cc6d0 100644 --- a/tests/runner-e2e/cursor-native-flow.ts +++ b/tests/runner-e2e/cursor-native-flow.ts @@ -14,6 +14,11 @@ import type { MatrixExecution } from "./types.js"; type Row = Record; type Check = { id: string; passed: boolean; detail: string }; +/** Match canonical omission of an unanswered optional feedback field. */ +export function cursorNativePlanResponse(planId: string, decision: "accept" | "reject" | "cancel", feedback: string) { + return { schema: "paperclip.question_response.v1", answers: { [planId]: { selectedOptionIds: [decision] }, ...(decision === "reject" ? { reason: { text: feedback } } : {}) } }; +} + /** Independent bounded snapshot; symlinks are rejected without following them. */ export async function cursorNativeWorkspaceSnapshot(root: string): Promise> { const files: Record = {}; let bytes = 0; @@ -183,7 +188,7 @@ export async function runCursorNativeFlow(input: { await page.getByRole("radio", { name: decision === "accept" ? "Accept plan" : decision === "reject" ? "Reject plan" : "Cancel plan request", exact: true }).last().click(); await page.getByRole("button", { name: "Next", exact: true }).last().click(); if (decision === "reject") await page.getByTestId("question-text-answer-composer").last().locator('[contenteditable="true"],textarea').first().fill(feedback); - const response = { schema: "paperclip.question_response.v1", answers: { [plan.id]: { selectedOptionIds: [decision] }, reason: decision === "reject" ? { text: feedback } : {} } }; + const response = cursorNativePlanResponse(plan.id, decision, feedback); check("revision-bound-decision", hasCursorPlanDecision(set, response, decision), "Decision addresses precisely the displayed native plan revision"); await page.getByRole("button", { name: set.submitLabel ?? "Submit answers", exact: true }).last().click(); await delivery(card, event, response); seen.add(card.id); previousRevision = plan.id;