test: eliminate decision sweep timing races

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-08-01 22:54:01 +00:00
1 parent cffe8efd85
commit 2c6e14dffb
1 file changed
+14 -9
+14 -9
View File
@@ -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" });