test: preserve draft revisions while permitting successor progress

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-21 16:45:22 -05:00
1 parent c81357e4dc
commit cf6d4ae3a8
3 files changed
+14 -6

No files matched your search

+1 -1
View File
@@ -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] : []),
{
+6 -2
View File
@@ -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();
+7 -3
View File
@@ -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<Row>(`/api/issues/${task.id}`), boundary,
oldRun: await api.get<ChatRun>(`/api/heartbeat-runs/${boundary.id}`), runs: await context.allRuns(), successorId: second.id,
planBefore, planAfter: await api.get<Row>(`/api/issues/${task.id}/documents/plan`), draft,
draftRevisions: await api.get<Row[]>(`/api/issues/${task.id}/documents/${encodeURIComponent(draft.key)}/revisions`),
draftAfter: await api.get<Row>(`/api/issues/${task.id}/documents/${encodeURIComponent(draft.key)}`),
output: await readChatOutputDocument(api, task.id, marker), reference,
audit: await api.get<Row[]>(`/api/issues/${task.id}/activity`), taskIds: (await api.get<Row[]>(`${company}/issues`)).map(t => t.id) };