diff --git a/tests/runner-e2e/catalog.ts b/tests/runner-e2e/catalog.ts index 10669b659f0..f53649fae2b 100644 --- a/tests/runner-e2e/catalog.ts +++ b/tests/runner-e2e/catalog.ts @@ -975,7 +975,7 @@ export const runnerSuites: readonly RunnerSuiteFixture[] = [ profiles: runnerProfiles.filter(profile => ["runner-codex", "runner-acpx-claude"].includes(profile.id)) .map(profile => productionStoryProfile(defaultPermissionProfile(profile))), environments: [localEnvironment], tasks: chatQualificationTasks, expectedMatrixSize: 6, - definitionMetadata: { version: 4, permissions: "production-defaults", instructions: "production", crashBoundary: "verified-native-worker-pid-at-file-wait", recovery: "user-visible-retry", answerGrading: "exact-grounded-propositions-plus-separate-semantic-review", scheduling: "explicit-only" }, + definitionMetadata: { version: 5, permissions: "production-defaults", instructions: "production", crashBoundary: "verified-native-worker-pid-at-file-wait", recovery: "user-visible-retry", answerGrading: "exact-grounded-propositions-plus-separate-semantic-review", scheduling: "explicit-only" }, }, ...(process.env.PAPERCLIP_RUNNER_E2E_CONNECTION_REVIEWS === "1" ? [connectionReviewSuite] : []), { diff --git a/tests/runner-e2e/chat-qualification.test.ts b/tests/runner-e2e/chat-qualification.test.ts index 12f498e48df..ed45d52d80b 100644 --- a/tests/runner-e2e/chat-qualification.test.ts +++ b/tests/runner-e2e/chat-qualification.test.ts @@ -9,7 +9,7 @@ const run = { id: "old", agentId: "original", companyId: "company", runtimeMode: const successor = { ...run, id: "next", agentId: "successor", status: "succeeded", startedAt: "2026-09-21T00:01:01Z" }; const before = { id: "task", assigneeAgentId: "original", description: "Preserve scope", projectId: null }; const plan = { body: "Friday REFERENCE", latestRevisionId: "revision" }; -const handoff = { before, after: { ...before, status: "done", assigneeAgentId: "successor" }, oldRun: run, boundary: { ...run, status: "running" }, runs: [run, successor], successorId: "successor", planBefore: plan, planAfter: plan, draft: { body: "REFERENCE" }, draftAfter: { body: "REFERENCE" }, output: { body: "REFERENCE", createdByAgentId: "successor" }, reference: "REFERENCE", audit: [{ action: "issue.reassigned", details: { source: "paperclip_runner_protocol" } }], taskIds: ["task"] }; +const handoff = { before, after: { ...before, status: "done", assigneeAgentId: "successor" }, oldRun: run, boundary: { ...run, status: "running" }, runs: [run, successor], successorId: "successor", planBefore: plan, planAfter: plan, draft: { id: "draft", latestRevisionId: "v1", body: "REFERENCE" }, draftAfter: { id: "draft", body: "REFERENCE" }, draftRevisions: [{ id: "v1", body: "REFERENCE" }], output: { body: "REFERENCE", createdByAgentId: "successor" }, reference: "REFERENCE", audit: [{ action: "issue.reassigned", details: { source: "paperclip_runner_protocol" } }], taskIds: ["task"] }; const recovery = { boundary: { ...run, status: "running" }, failed: { ...run, status: "failed" }, runs: [{ ...run, status: "failed" }, { ...successor, agentId: "original" }], issueId: "task", prompt: "Read my brief", comments: [{ body: "Read my brief" }, { body: "REFERENCE MARKER", authorAgentId: "original", createdByRunId: "next" }], reference: "REFERENCE", marker: "MARKER", planBefore: { body: "plan MARKER", latestRevisionId: "v1" }, planAfter: { body: "plan MARKER", latestRevisionId: "v1" } }; describe("remaining native chat qualification", () => { @@ -18,6 +18,8 @@ describe("remaining native chat qualification", () => { }); it("requires a stopped original worker before exactly one successor executes", () => { expect(() => assertActiveHandoff(handoff)).not.toThrow(); + expect(() => assertActiveHandoff({ ...handoff, draftAfter: { id: "draft", body: "Expanded REFERENCE" }, + output: { body: "Expanded REFERENCE", createdByAgentId: "original", updatedByAgentId: "successor" } })).not.toThrow(); for (const change of [ { boundary: { ...handoff.boundary, status: "succeeded" } }, { oldRun: { ...run, status: "succeeded" } }, @@ -28,7 +30,9 @@ describe("remaining native chat qualification", () => { { taskIds: ["replacement"] }, { planAfter: { ...plan, body: "rewritten" } }, { draft: { body: "no saved progress" } }, - { draftAfter: { body: "deleted progress" } }, + { draftAfter: { id: "replaced", body: "REFERENCE" } }, + { draftRevisions: [] }, + { draftRevisions: [{ id: "v1", body: "overwritten original" }] }, { output: { body: "REFERENCE", createdByAgentId: "original" } }, { audit: [] }, ]) expect(() => assertActiveHandoff({ ...handoff, ...change })).toThrow(); diff --git a/tests/runner-e2e/chat-qualification.ts b/tests/runner-e2e/chat-qualification.ts index 8122f15bf4b..7bf5610c6bf 100644 --- a/tests/runner-e2e/chat-qualification.ts +++ b/tests/runner-e2e/chat-qualification.ts @@ -27,7 +27,7 @@ export function assertWorkerIdentity(run: Row, command: string, environment: str export function assertActiveHandoff(e: { before: Row; after: Row; oldRun: ChatRun; boundary: ChatRun; runs: ChatRun[]; - successorId: string; planBefore: Row; planAfter: Row; draft: Row; draftAfter: Row; output: Row; + successorId: string; planBefore: Row; planAfter: Row; draft: Row; draftAfter: Row; draftRevisions: Row[]; output: Row; reference: string; audit: Row[]; taskIds: string[]; }) { expect(e.boundary).toMatchObject({ id: e.oldRun.id, status: "running", agentId: e.before.assigneeAgentId }); @@ -43,9 +43,12 @@ export function assertActiveHandoff(e: { expect(Date.parse(successor.startedAt!)).toBeGreaterThanOrEqual(oldEnd); expect(e.planAfter).toEqual(e.planBefore); expect(e.draft.body).toContain(e.reference); - expect(e.draftAfter).toEqual(e.draft); + expect(e.draftAfter.id).toBe(e.draft.id); + // Continuing a draft may create a new revision. Preservation means its exact + // saved revision remains retrievable, not that useful progress is forbidden. + expect(e.draftRevisions.find(r => r.id === e.draft.latestRevisionId)?.body).toBe(e.draft.body); expect(e.output.body).toContain(e.reference); - expect(e.output.createdByAgentId).toBe(e.successorId); + expect(e.output.updatedByAgentId ?? e.output.createdByAgentId).toBe(e.successorId); expect(e.audit.filter(a => a.action === "issue.reassigned")).toHaveLength(1); expect(e.audit.find(a => a.action === "issue.reassigned")?.details).toMatchObject({ source: "paperclip_runner_protocol" }); } @@ -115,6 +118,7 @@ export async function runActiveReassignment(context: Context) { const e = { before: task, after: await api.get(`/api/issues/${task.id}`), boundary, oldRun: await api.get(`/api/heartbeat-runs/${boundary.id}`), runs: await context.allRuns(), successorId: second.id, planBefore, planAfter: await api.get(`/api/issues/${task.id}/documents/plan`), draft, + draftRevisions: await api.get(`/api/issues/${task.id}/documents/${encodeURIComponent(draft.key)}/revisions`), draftAfter: await api.get(`/api/issues/${task.id}/documents/${encodeURIComponent(draft.key)}`), output: await readChatOutputDocument(api, task.id, marker), reference, audit: await api.get(`/api/issues/${task.id}/activity`), taskIds: (await api.get(`${company}/issues`)).map(t => t.id) };