diff --git a/tests/runner-e2e/FIXTURES.md b/tests/runner-e2e/FIXTURES.md index 636068a71c8..d1571426ec0 100644 --- a/tests/runner-e2e/FIXTURES.md +++ b/tests/runner-e2e/FIXTURES.md @@ -100,7 +100,7 @@ canonical Plan revision, capture its pending UI, approve in the browser, and prove exactly two successful runs. `warm_three_turn` provides exactly two browser follow-up messages, preserves one project/execution-workspace scope, verifies host file contents after every turn, and finishes within three -ten-minute turn deadlines. +ten-minute turn deadlines. Native warm turns 1 and 2 create a real confirmation card with `request_human_input` before reporting `needs_review`; the pending card supplies the required review gate. A disposition string alone does not create that gate. Turn 3 reports Done without another card. Every selected case runs in its own isolated Paperclip process, and independent cases may run concurrently. Follow-up turns inside one case retain their shared diff --git a/tests/runner-e2e/catalog.test.ts b/tests/runner-e2e/catalog.test.ts index 955368c0ad6..c4eb357793f 100644 --- a/tests/runner-e2e/catalog.test.ts +++ b/tests/runner-e2e/catalog.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { createIssueThreadInteractionSchema } from "../../packages/shared/src/validators/issue.js"; import { connectionReviewSuite, runnerEnvironments, @@ -25,6 +26,30 @@ import { } from "./selectors.js"; describe("runner E2E catalog", () => { + it("gives native warm turns a real review card before reporting needs_review", () => { + const prompts = [daytonaWarmContinuityTask.buildPrompt("nonce"), ...daytonaWarmContinuityTask.buildFollowupMessages!("nonce")]; + for (const [index, prompt] of prompts.entries()) { + const match = prompt.match(/call request_human_input with (\{.*\})\. Reuse/); + if (index === 2) { + expect(match).toBeNull(); + continue; + } + expect(match, `turn ${index + 1} must create its review interaction`).not.toBeNull(); + const request = JSON.parse(match![1]); + const interaction = createIssueThreadInteractionSchema.parse({ + kind: "request_confirmation", idempotencyKey: request.idempotencyKey, + title: request.title, summary: request.prompt, continuationPolicy: request.continuationPolicy, + payload: { ...request.payload, prompt: request.prompt }, + }); + expect(interaction).toMatchObject({ + kind: "request_confirmation", continuationPolicy: "wake_assignee", + payload: { target: { type: "custom", revisionId: `nonce-T${index + 1}` }, rejectRequiresReason: true }, + }); + expect(request.interactionKind).toBe("confirmation"); + expect(prompt.indexOf("call request_human_input with")).toBeLessThan(prompt.indexOf("then call paperclip_finish")); + } + }); + it("defines sixteen local connection-review journeys without expanding the default matrix", () => { expect(connectionReviewSuite.expectedMatrixSize).toBe(16); expect(new Set(connectionReviewSuite.profiles.map(profile => profile.id))).toEqual(new Set(["runner-codex", "runner-acpx-claude", "legacy-codex", "legacy-claude"])); diff --git a/tests/runner-e2e/catalog.ts b/tests/runner-e2e/catalog.ts index c19dd0a8e37..bbceb51d7a9 100644 --- a/tests/runner-e2e/catalog.ts +++ b/tests/runner-e2e/catalog.ts @@ -811,6 +811,22 @@ function warmTurnInstructions(turn: 1 | 2 | 3, nonce: string) { ); const marker = warmTurnMarker(turn, nonce); const finalTurn = turn === 3; + const nativeReviewRequest = { + interactionKind: "confirmation", + idempotencyKey: `daytona-warm-review-T${turn}-${nonce}`, + title: `Warm continuity turn ${turn}`, + prompt: `Is this warm continuity task ready to complete after turn ${turn}?`, + continuationPolicy: "wake_assignee", + payload: { + version: 1, + acceptLabel: "Approve completion", + rejectLabel: "Continue work", + rejectRequiresReason: true, + allowDeclineReason: true, + supersedeOnUserComment: false, + target: { type: "custom", key: `daytona_warm_turn_${turn}`, revisionId: `${nonce}-T${turn}`, label: `Warm continuity turn ${turn}` }, + }, + }; const legacyCompletion = finalTurn ? `In a legacy runner, make exactly one public-API completion write after verification: PATCH /api/issues/$PAPERCLIP_TASK_ID with {"status":"done","comment":"${marker}"}. Include Authorization and X-Paperclip-Run-Id. Do not POST a separate comment.` : `In a legacy runner, after verification POST exactly one request_confirmation to /api/issues/$PAPERCLIP_TASK_ID/interactions with {"kind":"request_confirmation","idempotencyKey":"daytona-warm-review-T${turn}-${nonce}","resolverPolicy":"human_only","title":"Warm continuity turn ${turn}","summary":"Review completed warm continuity turn ${turn}.","continuationPolicy":"wake_assignee","payload":{"version":1,"prompt":"Is this warm continuity task ready to complete after turn ${turn}?","acceptLabel":"Approve completion","rejectLabel":"Continue work","rejectRequiresReason":true,"allowDeclineReason":true,"supersedeOnUserComment":false,"target":{"type":"custom","key":"daytona_warm_turn_${turn}","revisionId":"${nonce}-T${turn}","label":"Warm continuity turn ${turn}"}}}. Capture the returned interaction id. Then make exactly one issue PATCH with {"status":"in_review","comment":"${marker}","reviewInteractionId":""}. Include Authorization and X-Paperclip-Run-Id on both writes. If the issue PATCH fails, retry only that PATCH and never create another interaction. Do not POST a separate comment. After both writes succeed, end the response and heartbeat immediately; do not wait or poll because the reviewer action will start the next turn.`; @@ -820,6 +836,7 @@ function warmTurnInstructions(turn: 1 | 2 | 3, nonce: string) { ? `Create ${file} with exactly this one line followed by a newline: ${lines[0]}` : `Before changing anything, read ${file} and verify its content is exactly ${lines.slice(0, -1).join("\\n")} followed by a newline. Then append exactly ${lines.at(-1)} followed by a newline.`, `After the write, verify ${file} contains exactly these lines, once each and in order: ${lines.join(" | ")}.`, + ...(!finalTurn ? [`In a native runner, after verification and before reporting needs_review, call request_human_input with ${JSON.stringify(nativeReviewRequest)}. Reuse its idempotencyKey on a retry; do not create another card. The pending card is the review gate; do not report done while it is waiting.`] : []), `In a native runner, use the current turn completion revision and criterion IDs (CURRENT_TURN_REVISION below is a placeholder, not a literal), then call paperclip_finish exactly once with {reportedWorkDisposition:"${finalTurn ? "done" : "needs_review"}",summary:"${marker}",completionClaim:{contractRevision:CURRENT_TURN_REVISION,objectiveSatisfied:true,criteria:[{criterionId:"objective",status:"satisfied",evidenceRefs:[]}],remainingWork:[]},evidence:[],verification:[{commandOrCheck:"read ${file}",status:"passed"}]}. Wait for that tool call to succeed, then emit exactly ${marker} once as the complete user-facing final response.`, legacyCompletion, `In a legacy runner, the PATCH comment is the complete visible response. After its 2xx response, finish silently: do not print, echo, or emit ${marker} again as assistant text.`,