← back to Costa Rica
costa-rica: make the test suite deterministic — serialize + isolate real-DB tests (cycle 21) — TK-10346
b6fb0a1a7cd3cd200d7a3d044312ef6346660bf3 · 2026-09-24 02:19:19 -0700 · Steve
The suite intermittently false-red'd (~1/3 -> then rarer) under `node --test`'s
default file-level PARALLELISM. Root cause was NOT one data collision but
CONCURRENCY resource pressure surfacing in several unrelated places:
- booking-pay-idempotency created bookings on place_id=1 at CURRENT_DATE..+1,
colliding with a parallel file under the bookings_no_overlap_stay EXCLUDE.
- server-routes.test.js's q-floor asserted oneChar.total === totalAll (two
separately-timed COUNT queries) — a parallel test creating a throwaway ACTIVE
place (double-book) shifted the count between them.
- async-harden.test.js's HTTP-server E2E tests flaked under heavy parallel load
(event-loop / resource pressure), nothing to do with data.
Three unrelated flake sources => the fix is to remove the concurrency, not
whack-a-mole each collision. Definitive fix: `npm test` now runs
`node --test --test-concurrency=1` (files serial; tests within a file were already
sequential). Verified: 15 consecutive serial runs, 0 failures; a full serial run is
~3.3s (negligible vs the flakiness it removes).
Belt-and-suspenders data isolation (so a future accidental parallel run is also
safe): booking-pay-idempotency's 3 bookings now use distinct far-future date windows
(CURRENT_DATE + 3000/3100/3200, in the free 2000-5000 range, clear of payouts 1000+,
payments-race 6000+, reconcile 7000+); server-routes' q-floor uses a tolerance
(|oneChar-total| < 100, filtered << total-100) instead of exact equality, so a
concurrent place insert/delete can't false-red it while still proving "1-char q is
unfiltered, >=2-char filters".
Test-only, reversible, no prod/externality. Suite 197/197 (now deterministically).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
Files touched
M package.jsonM test/booking-pay-idempotency.test.jsM test/server-routes.test.js
Diff
commit b6fb0a1a7cd3cd200d7a3d044312ef6346660bf3
Author: Steve <steve@designerwallcoverings.com>
Date: Thu Sep 24 02:19:19 2026 -0700
costa-rica: make the test suite deterministic — serialize + isolate real-DB tests (cycle 21) — TK-10346
The suite intermittently false-red'd (~1/3 -> then rarer) under `node --test`'s
default file-level PARALLELISM. Root cause was NOT one data collision but
CONCURRENCY resource pressure surfacing in several unrelated places:
- booking-pay-idempotency created bookings on place_id=1 at CURRENT_DATE..+1,
colliding with a parallel file under the bookings_no_overlap_stay EXCLUDE.
- server-routes.test.js's q-floor asserted oneChar.total === totalAll (two
separately-timed COUNT queries) — a parallel test creating a throwaway ACTIVE
place (double-book) shifted the count between them.
- async-harden.test.js's HTTP-server E2E tests flaked under heavy parallel load
(event-loop / resource pressure), nothing to do with data.
Three unrelated flake sources => the fix is to remove the concurrency, not
whack-a-mole each collision. Definitive fix: `npm test` now runs
`node --test --test-concurrency=1` (files serial; tests within a file were already
sequential). Verified: 15 consecutive serial runs, 0 failures; a full serial run is
~3.3s (negligible vs the flakiness it removes).
Belt-and-suspenders data isolation (so a future accidental parallel run is also
safe): booking-pay-idempotency's 3 bookings now use distinct far-future date windows
(CURRENT_DATE + 3000/3100/3200, in the free 2000-5000 range, clear of payouts 1000+,
payments-race 6000+, reconcile 7000+); server-routes' q-floor uses a tolerance
(|oneChar-total| < 100, filtered << total-100) instead of exact equality, so a
concurrent place insert/delete can't false-red it while still proving "1-char q is
unfiltered, >=2-char filters".
Test-only, reversible, no prod/externality. Suite 197/197 (now deterministically).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TouFkmUGKHtwqpZwgReVic
---
package.json | 2 +-
test/booking-pay-idempotency.test.js | 6 +++---
test/server-routes.test.js | 13 +++++++++----
3 files changed, 13 insertions(+), 8 deletions(-)
diff --git a/package.json b/package.json
index e860890..ae73794 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"start": "node server.js",
"schema": "psql costa_rica_directory -f schema.sql",
"seed": "psql costa_rica_directory -f scripts/seed.sql",
- "test": "node --test test/*.test.js"
+ "test": "node --test --test-concurrency=1 test/*.test.js"
},
"dependencies": {
"dotenv": "^16.4.5",
diff --git a/test/booking-pay-idempotency.test.js b/test/booking-pay-idempotency.test.js
index 9618af9..11e7160 100644
--- a/test/booking-pay-idempotency.test.js
+++ b/test/booking-pay-idempotency.test.js
@@ -43,7 +43,7 @@ test('payment reorder: timeout createCharge leaves a pre-written "processing" ro
// Constraint: total = platform_fee + host_payout. Use 10% fee + 90% payout.
const { rows: [b] } = await pool.query(
`INSERT INTO bookings (traveler_id, place_id, code, status, currency, subtotal, total, platform_fee, host_payout, check_in, check_out)
- VALUES ($1,1,$2,'pending',$3,$4,$5,$6,$7,CURRENT_DATE,CURRENT_DATE+1) RETURNING id`,
+ VALUES ($1,1,$2,'pending',$3,$4,$5,$6,$7,CURRENT_DATE + 3000, CURRENT_DATE + 3001) RETURNING id`,
[u.id, `${SENT}-timeout-1`, 'USD', 10000, 12000, 1200, 10800]);
createdIds.bookings.push(b.id);
@@ -103,7 +103,7 @@ test('payment reorder: retry detects in-flight payment and reuses it (no double-
// Seed: a booking. Use place_id=1. Constraint: total = platform_fee + host_payout. Use 8% fee + 92% payout.
const { rows: [b] } = await pool.query(
`INSERT INTO bookings (traveler_id, place_id, code, status, currency, subtotal, total, platform_fee, host_payout, check_in, check_out)
- VALUES ($1,1,$2,'pending',$3,$4,$5,$6,$7,CURRENT_DATE,CURRENT_DATE+1) RETURNING id`,
+ VALUES ($1,1,$2,'pending',$3,$4,$5,$6,$7,CURRENT_DATE + 3100, CURRENT_DATE + 3101) RETURNING id`,
[u.id, `${SENT}-reuse-1`, 'USD', 23000, 25000, 2000, 23000]);
created.bookings.push(b.id);
@@ -145,7 +145,7 @@ test('payment reorder: booking stays pending on a timeout (no premature confirma
// Seed: a booking. Use place_id=1. Constraint: total = platform_fee + host_payout. Use 4% fee + 96% payout.
const { rows: [b] } = await pool.query(
`INSERT INTO bookings (traveler_id, place_id, code, status, currency, subtotal, total, platform_fee, host_payout, check_in, check_out)
- VALUES ($1,1,$2,'pending',$3,$4,$5,$6,$7,CURRENT_DATE,CURRENT_DATE+1) RETURNING id`,
+ VALUES ($1,1,$2,'pending',$3,$4,$5,$6,$7,CURRENT_DATE + 3200, CURRENT_DATE + 3201) RETURNING id`,
[u.id, `${SENT}-pending-1`, 'USD', 48000, 50000, 2000, 48000]);
created.bookings.push(b.id);
diff --git a/test/server-routes.test.js b/test/server-routes.test.js
index 735ddee..45f71ac 100644
--- a/test/server-routes.test.js
+++ b/test/server-routes.test.js
@@ -58,15 +58,20 @@ test('/api/places q>=2 floor: a 1-char q is IGNORED (returns the full unfiltered
const totalAll = all.json.total;
assert.ok(totalAll > 100, `expected the dev DB to have a large active-places count, got ${totalAll}`);
- // 1-char q: the guard skips the LIKE filter -> same total as no q.
+ // 1-char q: the guard skips the LIKE filter -> ~same total as no q. Use a tolerance,
+ // not exact equality: these are two separately-timed COUNT queries, and a parallel
+ // real-DB test file (e.g. double-book creates a throwaway ACTIVE place) can insert/
+ // delete a handful of places between them. The point is "1-char is UNFILTERED", i.e.
+ // the full-directory magnitude — not a filtered subset — which a tolerance captures.
const oneChar = await get('/api/places?q=a&limit=1');
assert.equal(oneChar.status, 200);
- assert.equal(oneChar.json.total, totalAll, 'a 1-char q must not filter (would be a full-directory scan); total == unfiltered');
+ assert.ok(Math.abs(oneChar.json.total - totalAll) < 100,
+ `a 1-char q must not filter (unfiltered ~${totalAll}, got ${oneChar.json.total})`);
- // A real >=2 nonsense term: the filter IS applied -> a strictly smaller total.
+ // A real >=2 nonsense term: the filter IS applied -> far fewer than the full directory.
const filtered = await get('/api/places?q=zzqxbyw&limit=1');
assert.equal(filtered.status, 200);
- assert.ok(filtered.json.total < totalAll, `a >=2-char filter must reduce the count (${filtered.json.total} < ${totalAll})`);
+ assert.ok(filtered.json.total < totalAll - 100, `a >=2-char filter drastically reduces the count (${filtered.json.total} << ${totalAll})`);
});
test('/api/places pagination: Link header carries rel="next" and the page respects limit', async () => {
← 1ca7bf7 cycle 20 docs: YOLO_NOTES ledger — stale-payment reconciler
·
back to Costa Rica
·
cycle 21 docs: YOLO_NOTES ledger — deterministic (serial) te ab1e4be →