- normalize.ts num(): parseFloat truncated at the first comma, so "1,200"
silently became 1 (1000x error) and "5,5" became 5. Now strictly matches
either US thousands-grouping or a plain number spanning the whole string;
anything else (incl. ambiguous "5,5") returns null instead of a wrong value.
- labs.ts confirm handler: the pending-status check ran before the request
body was read, so two concurrent confirms could both pass it and
double-insert. Added a guarded UPDATE ... WHERE status = 'pending' as the
first statement inside the existing synchronous transaction; zero rows
affected throws and the route returns 409, with the fast-path check kept
for the common case.
- Added missing `await` on two rejects.toThrow assertions (llm.test.ts,
extract.test.ts) that were previously resolving before the assertion
settled.
- Bumped the 11th-failed-login rate-limit test to a 30s timeout — 10
sequential argon2id verifies can exceed bun:test's 5s default under load.
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>
- Add ConfirmDraftBody schema to shared types
- Implement GET /api/labs/drafts/:id (retrieves draft with extracted data)
- Implement POST /api/labs/drafts/:id/confirm (normalizes markers, inserts lab draw + biomarkers in transaction, marks draft confirmed)
- Implement POST /api/labs/drafts/:id/discard (marks draft discarded)
- Add comprehensive test suite with 3 new tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>