fix(labs): synchronous confirm transaction — async callback broke atomicity

bun-sqlite's Database.transaction is synchronous, but async callbacks return
a pending Promise at the first await, causing immediate COMMIT before the
entire callback completes. This allowed partial inserts with no rollback.

Fixed by:
- Remove async from transaction callback
- Add .run() to each insert/update to execute synchronously
- Add regression test proving atomicity: transaction that throws mid-loop
  rolls back all changes (both tables empty after failure)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
marcuspaico
2026-08-17 15:52:59 -07:00
parent 038c92af45
commit 96692163f1
2 changed files with 26 additions and 7 deletions

View File

@@ -5,7 +5,7 @@ import { join } from "node:path";
import { eq } from "drizzle-orm";
import { createApp } from "../src/app";
import { openDb } from "../src/db";
import { biomarkers, labDrafts } from "../src/db/schema";
import { biomarkers, labDrafts, labDraws } from "../src/db/schema";
import { loadOrCreateKey } from "../src/lib/crypto";
async function setup() {
@@ -83,4 +83,23 @@ describe("draft review", () => {
});
expect(bad.status).toBe(400);
});
test("sync transaction rolls back on mid-loop failure", () => {
const dir = mkdtempSync(join(tmpdir(), "helios-"));
const db = openDb(dir);
expect(() =>
db.transaction((tx) => {
tx.insert(labDraws).values({ id: "dX", collectedAt: "2026-01-01", labName: null, draftId: null, createdAt: 1 }).run();
tx.insert(biomarkers).values({ drawId: "dX", panel: "p", name: "n", marker: "m", analyteKey: null, value: "1", valueNum: 1, unit: null, referenceRange: null, flagged: 0, valueCanonical: null, canonicalUnit: null }).run();
throw new Error("boom");
}),
).toThrow("boom");
// Verify both tables are empty after rollback
const drawRows = db.select().from(labDraws).all();
const bioRows = db.select().from(biomarkers).all();
expect(drawRows).toHaveLength(0);
expect(bioRows).toHaveLength(0);
});
});