mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 18:31:34 +01:00
fix(decisions): serialize triage updates
Lock each company/source triage identity before reading current state so concurrent partial updates preserve both fields and emit monotonic history versions. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
2b0b3b0634
commit
8a40f5ba61
2 files changed
+34
No files matched your search
@@ -396,6 +396,38 @@ describeEmbeddedPostgres("decision queue routes", () => {
|
||||
expect(current[0]?.setByUserId).toBe("override-user");
|
||||
});
|
||||
|
||||
it("serializes concurrent partial triage updates without losing state or reusing a version", async () => {
|
||||
const { companyId, issueId } = await seed();
|
||||
const board = boardActor(companyId);
|
||||
const [decideByResult, snoozeResult] = await Promise.all([
|
||||
request(app(board))
|
||||
.put(`/api/companies/${companyId}/decision-triage/review/${issueId}`)
|
||||
.send({ decideBy: "today" }),
|
||||
request(app(board))
|
||||
.put(`/api/companies/${companyId}/decision-triage/review/${issueId}`)
|
||||
.send({ snoozedUntil: "2026-08-03T12:00:00.000Z" }),
|
||||
]);
|
||||
|
||||
expect(decideByResult.status).toBe(200);
|
||||
expect(snoozeResult.status).toBe(200);
|
||||
expect([decideByResult.body.version, snoozeResult.body.version].sort()).toEqual([1, 2]);
|
||||
|
||||
const current = await request(app(board))
|
||||
.get(`/api/companies/${companyId}/decision-triage/review/${issueId}`)
|
||||
.expect(200);
|
||||
expect(current.body).toMatchObject({
|
||||
decideBy: "today",
|
||||
snoozedUntil: "2026-08-03T12:00:00.000Z",
|
||||
version: 2,
|
||||
});
|
||||
const events = await db.select().from(decisionTriageEvents).where(and(
|
||||
eq(decisionTriageEvents.companyId, companyId),
|
||||
eq(decisionTriageEvents.sourceKind, "review"),
|
||||
eq(decisionTriageEvents.sourceId, issueId),
|
||||
));
|
||||
expect(events.map((event) => event.details.version).sort()).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it("rejects task-bridge JWTs and returns the same 404 for missing and unauthorized sources", async () => {
|
||||
const { companyId, agentId, approvalId } = await seed();
|
||||
const bridge = {
|
||||
|
||||
@@ -597,6 +597,8 @@ export function decisionQueueService(db: Db) {
|
||||
await requireSourceRead(db, input.authActor, input.companyId, input.sourceKind, input.sourceId);
|
||||
return db.transaction(async (tx) => {
|
||||
const txDb = tx as unknown as Db;
|
||||
const lockKey = `decision-triage:${input.companyId}:${input.sourceKind}:${input.sourceId}`;
|
||||
await txDb.execute(sql`select pg_advisory_xact_lock(hashtextextended(${lockKey}, 0))`);
|
||||
const current = await txDb.select().from(decisionTriage).where(and(
|
||||
eq(decisionTriage.companyId, input.companyId),
|
||||
eq(decisionTriage.sourceKind, input.sourceKind),
|
||||
|
||||
Reference in new issue
Block a user