Handle incomplete task policies and preserve review limits (#15593)

Apply the reviewed change for Handle incomplete task policies and preserve review limits.

Validation: local typecheck/build and focused regression tests, passing exact-head CI, Greptile 5/5, and independent source review. The PR records full-suite evidence and any local environment limitations.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip authored and GitHub committed 2026-10-08 10:20:32 -07:00
1 parent 3d0e74e743
commit 317ed367d4
13 files changed
+452 -30

No files matched your search

@@ -0,0 +1,45 @@
import { randomUUID } from "node:crypto";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { companies, createDb, issues } from "@paperclipai/db";
import { buildExecutionPolicy } from "../../../ui/src/lib/issue-execution-policy.ts";
import { normalizeIssueExecutionPolicy } from "../services/issue-execution-policy.js";
import { issueService } from "../services/issues.js";
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
const support = await getEmbeddedPostgresTestSupport();
(support.supported ? describe : describe.skip)("edited execution policy persistence", () => {
let temporary: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>;
let db: ReturnType<typeof createDb>;
beforeAll(async () => {
temporary = await startEmbeddedPostgresTestDatabase("paperclip-policy-persistence-");
db = createDb(temporary.connectionString);
}, 30_000);
afterAll(async () => { await temporary?.cleanup(); });
it.each(["reviewer", "monitor"] as const)("retains the independent review limit after removing the last %s", async (removed) => {
const companyId = randomUUID();
const issueId = randomUUID();
const original = normalizeIssueExecutionPolicy({
maxReviewRounds: 4,
...(removed === "monitor"
? { monitor: { nextCheckAt: "2099-01-01T00:00:00.000Z" } }
: { stages: [{ type: "review", participants: [{ type: "user", userId: "board-reviewer" }] }] }),
});
await db.insert(companies).values({ id: companyId, name: "Policy fixture", issuePrefix: `P${companyId.slice(0, 6)}` });
await db.insert(issues).values({ id: issueId, companyId, title: "Review limit fixture", executionPolicy: original });
const edited = buildExecutionPolicy({ existingPolicy: original, reviewerValues: [], approverValues: [] })!;
// The monitor editor removes this key after building the unchanged stages.
const { monitor: _removedMonitor, ...withoutMonitor } = edited;
const normalized = normalizeIssueExecutionPolicy(removed === "monitor" ? withoutMonitor : edited);
const service = issueService(db);
await service.update(issueId, { executionPolicy: normalized, companyGuard: companyId });
const saved = await service.getById(issueId);
expect(saved?.executionPolicy).toEqual({ mode: "normal", commentRequired: true, stages: [], maxReviewRounds: 4 });
const restored = normalizeIssueExecutionPolicy(buildExecutionPolicy({
existingPolicy: saved?.executionPolicy,
reviewerValues: ["user:board-reviewer"], approverValues: [],
}));
expect(restored).toMatchObject({ maxReviewRounds: 4, stages: [{ type: "review" }] });
});
});
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { applyIssueExecutionPolicyTransition, normalizeIssueExecutionPolicy, parseIssueExecutionState } from "../services/issue-execution-policy.ts";
import type { IssueExecutionPolicy, IssueExecutionState } from "@paperclipai/shared";
import { buildExecutionPolicy } from "../../../ui/src/lib/issue-execution-policy.ts";
const coderAgentId = "11111111-1111-4111-8111-111111111111";
const qaAgentId = "22222222-2222-4222-8222-222222222222";
@@ -43,6 +44,23 @@ describe("normalizeIssueExecutionPolicy", () => {
expect(normalizeIssueExecutionPolicy({ stages: [] })).toBeNull();
});
it("retains an independent review limit through removing and restoring the last reviewer", () => {
const original = normalizeIssueExecutionPolicy({
maxReviewRounds: 4,
stages: [{ type: "review", participants: [{ type: "agent", agentId: qaAgentId }] }],
});
const edited = buildExecutionPolicy({ existingPolicy: original, reviewerValues: [], approverValues: [] });
const saved = normalizeIssueExecutionPolicy(edited);
expect(saved).toEqual({ mode: "normal", commentRequired: true, stages: [], maxReviewRounds: 4 });
expect(applyIssueExecutionPolicyTransition({
issue: { status: "in_progress", assigneeAgentId: coderAgentId },
policy: saved, requestedStatus: "done", requestedAssigneePatch: {}, actor: { agentId: coderAgentId },
})).toEqual({ patch: {} });
expect(normalizeIssueExecutionPolicy(buildExecutionPolicy({
existingPolicy: saved, reviewerValues: [`agent:${qaAgentId}`], approverValues: [],
}))).toMatchObject({ maxReviewRounds: 4, stages: [{ type: "review" }] });
});
it("throws when all participants are invalid (missing agentId)", () => {
expect(() =>
normalizeIssueExecutionPolicy({
@@ -401,7 +401,8 @@ export function normalizeIssueExecutionPolicy(input: unknown): IssueExecutionPol
const reviewPreset = parsed.data.reviewPreset;
const authorizationPolicy = parsed.data.authorizationPolicy;
if (stages.length === 0 && !monitor && !reviewPreset && !authorizationPolicy) return null;
if (stages.length === 0 && !monitor && !reviewPreset && !authorizationPolicy
&& parsed.data.maxReviewRounds == null) return null;
return {
mode: parsed.data.mode ?? "normal",