mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-30 13:20:54 +01:00
test: preserve draft revisions while permitting successor progress
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
c81357e4dc
commit
cf6d4ae3a8
3 files changed
+14
-6
No files matched your search
@@ -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] : []),
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) };
|
||||
|
||||
Reference in new issue
Block a user