fix(labs): strict numeric parsing, atomic confirm guard, test integrity
- 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.
This commit is contained in:
@@ -24,9 +24,27 @@ export interface NormMarker {
|
||||
canonicalUnit: string | null;
|
||||
}
|
||||
|
||||
const THOUSANDS_GROUPED = /^\d{1,3}(,\d{3})+(\.\d+)?$/;
|
||||
const PLAIN_NUMBER = /^-?\d+(\.\d+)?$/;
|
||||
|
||||
// Strict numeric parsing: parseFloat alone stops at the first non-numeric
|
||||
// character, so "1,200" silently became 1 (a 1000x error) and "5,5" (a
|
||||
// European decimal) silently became 5. Instead: strip comparators/whitespace,
|
||||
// then only accept (a) US thousands-grouping, comma-stripped, or (b) a plain
|
||||
// number that spans the ENTIRE remaining string. Anything else — including
|
||||
// ambiguous "5,5" — returns null so no canonical value is computed rather
|
||||
// than a silently wrong one.
|
||||
const num = (v: string): number | null => {
|
||||
const n = parseFloat(v.replace(/[<>≤≥]/g, "").trim());
|
||||
return Number.isFinite(n) ? n : null;
|
||||
const stripped = v.replace(/[<>≤≥\s]/g, "");
|
||||
if (THOUSANDS_GROUPED.test(stripped)) {
|
||||
const n = parseFloat(stripped.replace(/,/g, ""));
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
if (PLAIN_NUMBER.test(stripped)) {
|
||||
const n = parseFloat(stripped);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export function normalizeMarker(raw: RawMarker): NormMarker {
|
||||
|
||||
Reference in New Issue
Block a user