diff --git a/server/src/__tests__/decisions-service.test.ts b/server/src/__tests__/decisions-service.test.ts index febd4223aba..f03851919cb 100644 --- a/server/src/__tests__/decisions-service.test.ts +++ b/server/src/__tests__/decisions-service.test.ts @@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; -import { eq } from "drizzle-orm"; +import { eq, inArray } from "drizzle-orm"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { activityLog, @@ -84,6 +84,11 @@ describePg("decisionService", () => { options: [{ id: "yes", label: "Yes", effects: [{ type: "comment_on_issue", targetIssueId, staleness, bodyMarkdown: "hello" }] }], ...extra, }); + const expireDecisions = async (...ids: string[]) => { + await db.update(decisions) + .set({ expiresAt: new Date(Date.now() - 1_000) }) + .where(inArray(decisions.id, ids)); + }; it("returns the existing decision for concurrent idempotent creates", async () => { const input = { @@ -440,27 +445,27 @@ describePg("decisionService", () => { it("bounds expiration work to the configured batch size", async () => { process.env.PAPERCLIP_DECISIONS_SWEEP_BATCH_SIZE = "1"; - await createCommentDecision("lenient", { idempotencyKey: "batch-1", expiresAt: new Date(Date.now() + 5) }); - await createCommentDecision("lenient", { idempotencyKey: "batch-2", expiresAt: new Date(Date.now() + 5) }); - await new Promise((resolve) => setTimeout(resolve, 10)); + const first = await createCommentDecision("lenient", { idempotencyKey: "batch-1" }); + const second = await createCommentDecision("lenient", { idempotencyKey: "batch-2" }); + await expireDecisions(first.id, second.id); expect((await service().sweepExpired()).expired).toBe(1); expect((await service().sweepExpired()).expired).toBe(1); }); it("falls back to the default sweep batch size for invalid configuration", async () => { process.env.PAPERCLIP_DECISIONS_SWEEP_BATCH_SIZE = "not-a-number"; - await createCommentDecision("lenient", { idempotencyKey: "invalid-batch-1", expiresAt: new Date(Date.now() + 5) }); - await createCommentDecision("lenient", { idempotencyKey: "invalid-batch-2", expiresAt: new Date(Date.now() + 5) }); - await new Promise((resolve) => setTimeout(resolve, 10)); + const first = await createCommentDecision("lenient", { idempotencyKey: "invalid-batch-1" }); + const second = await createCommentDecision("lenient", { idempotencyKey: "invalid-batch-2" }); + await expireDecisions(first.id, second.id); await expect(service().sweepExpired()).resolves.toMatchObject({ expired: 2 }); }); it("expires TTL and target-gone decisions and wakes the origin agent", async () => { - const ttl = await createCommentDecision("lenient", { expiresAt: new Date(Date.now() + 5) }); + const ttl = await createCommentDecision("lenient"); const gone = await createCommentDecision("strict", { idempotencyKey: "gone" }); await db.update(issues).set({ status: "cancelled" }).where(eq(issues.id, targetIssueId)); - await new Promise((resolve) => setTimeout(resolve, 10)); + await expireDecisions(ttl.id); expect((await service().sweepExpired()).expired).toBe(2); const rows = await db.select().from(decisions); expect(rows.find((row) => row.id === ttl.id)?.metadata).toMatchObject({ expiredReason: "ttl" });