mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
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:
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",
|
||||
|
||||
Reference in new issue
Block a user