* fix(comtrade): retry on transient 5xx to stop silent reporter drops
Railway log 2026-04-14 bilateral-hs4 run: India (699) hit HTTP 503 on both
batches and was dropped entirely from the snapshot. Iran (364) hit 500
mid-batch. All three Comtrade seeders (bilateral-hs4, trade-flows,
recovery-import-hhi) retried only on 429; any 5xx = silent coverage gap.
Adds bounded 5xx retry (3 attempts, 5s then 15s backoff) in each seeder.
On giveup caller returns empty so resume cache picks the reporter up next
cycle. Exports isTransientComtrade + fetchBilateral for unit tests; 6 new
tests pin the contract.
* fix(comtrade): collapse 429+5xx into single classification loop (PR review)
Reviewer caught that the 429 branch bypassed the 5xx retry path: a 429 ->
503 sequence would return [] immediately after the 429-retry without
consuming any transient-5xx retries, leaving the silent-drop bug intact
for that specific sequence.
Both seeders now use a single while-loop that reclassifies each response:
- 429 (once, with full backoff)
- 5xx (up to 2 retries with 5s/15s or 5s/10s backoff)
- anything else -> break and return
Two new tests lock in the mixed case: 429 then 503 still consumes
transient retries; consecutive 429s cap at one wait. 8/8 pass.
* test(comtrade): inject sleep to drop retry-test runtime from 185s to 277ms
PR review flagged that the new mixed 429+5xx tests slept for the full
production backoffs (60s + 5s + 15s = 80s per test), making the unit
suite unnecessarily slow and CI-timeout-prone.
Add a module-local _retrySleep binding with __setSleepForTests(fn)
export. Production keeps the real sleep; tests swap in a no-op that
records requested delays. The sleepCalls array now pins the production
cadence so a future refactor that changes [60_000, 5_000, 15_000] has
to update the test too.
8/8 pass in 277ms (down from 185s).
* test(comtrade): update 60s-on-429 static-analysis regex for _retrySleep alias
The existing substring check 'sleep(60_000)' broke after the previous
commit renamed production calls to _retrySleep(60_000) for test injection.
Widen the regex to accept either the bare or injectable form; both
preserve the 60s production cadence.
* test(comtrade): extend retry coverage to trade-flows + recovery-import-hhi
Three P2 review findings addressed:
1. partnerCode 000 in the succeeds-on-third test was changed to real code
156. groupByProduct() filters 0/000 downstream, so the test was passing
while the user-visible seeder would still drop the row.
2. trade-flows and recovery-import-hhi had no unit coverage for their new
retry state machines. Adds 7 tests covering succeed-first, retry-then-
succeed, giveup-after-3, and mixed 429+5xx classification.
3. Both seeders now expose __setSleepForTests + export their fetch helper.
seed-trade-flows also gets an isMain guard so tests import without
triggering a real seed run. sleepCalls asserts pin the production
cadence.
15 retry tests pass in 183ms. Full suite 5198/5198.
* fix(trade-flows): per-reporter coverage gate blocks full-reporter flatline
PR #3084 review P1: the existing MIN_COVERAGE_RATIO=0.70 gate was
global-only. 6 reporters × 5 commodities = 30 pairs; losing one entire
reporter (e.g. the India/Taiwan silent-drop this PR is trying to stop)
is only 5/30 missing = 83% global coverage, which passed.
Adds a per-reporter coverage floor: each reporter must have ≥40% of
its commodities populated (2 of 5). Global gate kept as the broad-
outage catch; per-reporter gate catches the single-reporter flatline.
Extracts checkCoverage() as a pure function for unit testing — mocking
30+ fetches in fetchAllFlows is fragile, and the failure mode lives in
the gate, not the fetcher.
6 new tests cover: 30/30 ok; India flatline → reject at 83% global;
Taiwan flatline; broad outage → reject via global gate; healthy
80% global with 4/5 per-reporter → ok; per-reporter breakdown shape.
5204/5204 tests pass.