Files
worldmonitor/tests/energy-shock-seed.test.mts
Elie Habib 6e401ad02f feat(supply-chain): Global Shipping Intelligence — Sprint 0 + Sprint 1 (#2870)
* feat(supply-chain): Sprint 0 — chokepoint registry, HS2 sectors, war_risk_tier

- src/config/chokepoint-registry.ts: single source of truth for all 13
  canonical chokepoints with displayName, relayName, portwatchName,
  corridorRiskName, baselineId, shockModelSupported, routeIds, lat/lon
- src/config/hs2-sectors.ts: static dictionary for all 99 HS2 chapters
  with category, shockModelSupported (true only for HS27), cargoType
- server/worldmonitor/supply-chain/v1/_chokepoint-ids.ts: migrated to
  derive CANONICAL_CHOKEPOINTS from chokepoint-registry; no data duplication
- src/config/geo.ts + src/types/index.ts: added chokepointId field to
  StrategicWaterway interface and all 13 STRATEGIC_WATERWAYS entries
- src/components/MapPopup.ts: switched chokepoint matching from fragile
  name.toLowerCase() to direct chokepointId === id comparison
- server/worldmonitor/intelligence/v1/_shock-compute.ts: migrated from old
  IDs (hormuz/malacca/babelm) to canonical IDs (hormuz_strait/malacca_strait/
  bab_el_mandeb); same for CHOKEPOINT_LNG_EXPOSURE
- proto/worldmonitor/supply_chain/v1/supply_chain_data.proto: added
  WarRiskTier enum + war_risk_tier field (field 16) on ChokepointInfo
- get-chokepoint-status.ts: populates warRiskTier from ChokepointConfig.threatLevel
  via new threatLevelToWarRiskTier() helper (FREE field, no PRO gate)

* feat(supply-chain): Sprint 1 — country chokepoint exposure index + sector ring

S1.1: scripts/shared/country-port-clusters.json
  ~130 country → {nearestRouteIds, coastSide} mappings derived from trade route
  waypoints; covers all 6 seeded Comtrade reporters plus major trading nations.

S1.2: scripts/seed-hs2-chokepoint-exposure.mjs
  Daily cron seeder. Pure computation — reads country-port-clusters.json,
  scores each country against CHOKEPOINT_REGISTRY route overlap, writes
  supply-chain:exposure:{iso2}:{hs2}:v1 keys + seed-meta (24h TTL).

S1.3: RPC get-country-chokepoint-index (PRO-gated, request-varying)
  - proto: GetCountryChokepointIndexRequest/Response + ChokepointExposureEntry
  - handler: isCallerPremium gate; cachedFetchJson 24h; on-demand for any iso2
  - cache-keys.ts: CHOKEPOINT_EXPOSURE_KEY(iso2, hs2) constant
  - health.js: chokepointExposure SEED_META entry (48h threshold)
  - gateway.ts: slow-browser cache tier
  - service client: fetchCountryChokepointIndex() exported

S1.4: Chokepoint popup HS2 sector ring chart (PRO-gated)
  Static trade-sector breakdown (IEA/UNCTAD estimates) per 9 major chokepoints.
  SVG donut ring + legend shown for PRO users; blurred lockout + gate-hit
  analytics for free users. Wired into renderWaterwayPopup().

🤖 Generated with Claude Sonnet 4.6 via Claude Code (https://claude.com/claude-code) + Compound Engineering v2.49.0

Co-Authored-By: Claude Sonnet 4.6 (200K context) <noreply@anthropic.com>

* fix(tests): update energy-shock-v2 tests to use canonical chokepoint IDs

CHOKEPOINT_EXPOSURE and CHOKEPOINT_LNG_EXPOSURE keys were migrated from
short IDs (hormuz, malacca, babelm) to canonical registry IDs
(hormuz_strait, malacca_strait, bab_el_mandeb) in Sprint 0.
Test fixtures were not updated at the time; fix them now.

* fix(tests): update energy-shock-seed chokepoint ID to canonical form

VALID_CHOKEPOINTS changed to canonical IDs in Sprint 0; the seed test
that checks valid IDs was not updated alongside it.

* fix(cache-keys): reword JSDoc comment to avoid confusing bootstrap test regex

The comment "NOT in BOOTSTRAP_CACHE_KEYS" caused the bootstrap.test.mjs
regex to match the comment rather than the actual export declaration,
resulting in 0 entries found. Rephrase to "excluded from bootstrap".

* fix(supply-chain): address P1 review findings for chokepoint exposure index

- Add get-country-chokepoint-index to PREMIUM_RPC_PATHS (CDN bypass)
- Validate iso2/hs2 params before Redis key construction (cache injection)
- Fix seeder TTL to 172800s (2× interval) and extend TTL on skipped lock
- Fix CHOKEPOINT_EXPOSURE_SEED_META_KEY to match seeder write key
- Render placeholder sectors behind blur gate (DOM data leakage)
- Document get-country-chokepoint-index in widget agent system prompts

* fix(lint): resolve Biome CI failures

- Add biome.json overrides to silence noVar in HTML inline scripts,
  disable linting for public/ vendor/build artifacts and pro-test/
- Remove duplicate NG and MW keys from country-port-clusters.json
- Use import attributes (with) instead of deprecated assert syntax

* fix(build): drop JSON import attribute — esbuild rejects `with` syntax

---------

Co-authored-by: Claude Sonnet 4.6 (200K context) <noreply@anthropic.com>
2026-04-09 17:06:03 +04:00

221 lines
8.3 KiB
TypeScript

/**
* Unit tests for computeEnergyShockScenario handler logic.
*
* Tests the pure computation functions imported from _shock-compute.ts (no Redis dependency).
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
clamp,
computeGulfShare,
computeEffectiveCoverDays,
buildAssessment,
GULF_PARTNER_CODES,
CHOKEPOINT_EXPOSURE,
VALID_CHOKEPOINTS,
} from '../server/worldmonitor/intelligence/v1/_shock-compute.js';
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe('energy shock scenario computation', () => {
describe('chokepoint validation', () => {
it('accepts all valid chokepoint IDs', () => {
for (const id of ['hormuz_strait', 'malacca_strait', 'suez', 'bab_el_mandeb']) {
assert.ok(VALID_CHOKEPOINTS.has(id), `Expected ${id} to be valid`);
}
});
it('rejects invalid chokepoint IDs', () => {
for (const id of ['panama', 'taiwan', '', 'xyz']) {
assert.ok(!VALID_CHOKEPOINTS.has(id), `Expected ${id} to be invalid`);
}
});
it('CHOKEPOINT_EXPOSURE contains all valid chokepoints', () => {
for (const id of VALID_CHOKEPOINTS) {
assert.ok(id in CHOKEPOINT_EXPOSURE, `Expected CHOKEPOINT_EXPOSURE to have key ${id}`);
}
});
});
describe('disruption_pct clamping', () => {
it('clamps disruption_pct below 10 to 10', () => {
assert.equal(clamp(Math.round(5), 10, 100), 10);
assert.equal(clamp(Math.round(0), 10, 100), 10);
});
it('clamps disruption_pct above 100 to 100', () => {
assert.equal(clamp(Math.round(150), 10, 100), 100);
assert.equal(clamp(Math.round(200), 10, 100), 100);
});
it('passes through valid disruption_pct values unchanged', () => {
for (const v of [10, 25, 50, 75, 100]) {
assert.equal(clamp(v, 10, 100), v);
}
});
});
describe('gulf crude share calculation', () => {
it('returns hasData=false when no flows provided', () => {
const result = computeGulfShare([]);
assert.equal(result.share, 0);
assert.equal(result.hasData, false);
});
it('returns hasData=false when all flows have zero/negative tradeValueUsd', () => {
const flows = [
{ partnerCode: '682', tradeValueUsd: 0 },
{ partnerCode: '784', tradeValueUsd: -100 },
];
const result = computeGulfShare(flows);
assert.equal(result.share, 0);
assert.equal(result.hasData, false);
});
it('returns hasData=false when country has no Comtrade data (no numeric code mapping)', () => {
const ISO2_TO_COMTRADE: Record<string, string> = {
US: '842', CN: '156', RU: '643', IR: '364', IN: '356', TW: '158',
};
const unsupportedCountries = ['DE', 'FR', 'JP', 'KR', 'BR', 'SA'];
for (const code of unsupportedCountries) {
assert.equal(ISO2_TO_COMTRADE[code], undefined, `${code} should not have Comtrade mapping`);
}
});
it('GULF_PARTNER_CODES contains expected Gulf country codes', () => {
assert.ok(GULF_PARTNER_CODES.has('682'), 'SA should be in Gulf set');
assert.ok(GULF_PARTNER_CODES.has('784'), 'AE should be in Gulf set');
assert.ok(GULF_PARTNER_CODES.has('368'), 'IQ should be in Gulf set');
assert.ok(GULF_PARTNER_CODES.has('414'), 'KW should be in Gulf set');
assert.ok(GULF_PARTNER_CODES.has('364'), 'IR should be in Gulf set');
assert.ok(!GULF_PARTNER_CODES.has('643'), 'RU should NOT be in Gulf set');
});
it('returns share=1.0 and hasData=true when all imports are from Gulf partners', () => {
const flows = [
{ partnerCode: '682', tradeValueUsd: 1000 }, // SA
{ partnerCode: '784', tradeValueUsd: 500 }, // AE
];
const result = computeGulfShare(flows);
assert.equal(result.share, 1.0);
assert.equal(result.hasData, true);
});
it('returns share=0 and hasData=true when no imports are from Gulf partners', () => {
const flows = [
{ partnerCode: '124', tradeValueUsd: 1000 }, // Canada
{ partnerCode: '643', tradeValueUsd: 500 }, // Russia (not in Gulf set)
];
const result = computeGulfShare(flows);
assert.equal(result.share, 0);
assert.equal(result.hasData, true);
});
it('computes fractional Gulf share correctly', () => {
const flows = [
{ partnerCode: '682', tradeValueUsd: 300 }, // SA (Gulf)
{ partnerCode: '124', tradeValueUsd: 700 }, // Canada (non-Gulf)
];
const result = computeGulfShare(flows);
assert.equal(result.share, 0.3);
assert.equal(result.hasData, true);
});
it('ignores flows with zero or negative tradeValueUsd', () => {
const flows = [
{ partnerCode: '682', tradeValueUsd: 0 }, // Gulf but zero
{ partnerCode: '784', tradeValueUsd: -100 }, // Gulf but negative
{ partnerCode: '124', tradeValueUsd: 500 }, // Non-Gulf positive
];
const result = computeGulfShare(flows);
assert.equal(result.share, 0);
assert.equal(result.hasData, true);
});
it('accepts numeric partnerCode values', () => {
const flows = [
{ partnerCode: 682, tradeValueUsd: 1000 }, // SA as number
];
const result = computeGulfShare(flows);
assert.equal(result.share, 1.0);
assert.equal(result.hasData, true);
});
});
describe('effective cover days computation', () => {
it('returns -1 for net exporters', () => {
assert.equal(computeEffectiveCoverDays(90, true, 100, 500), -1);
});
it('returns raw daysOfCover when crudeLossKbd is 0', () => {
assert.equal(computeEffectiveCoverDays(90, false, 0, 500), 90);
});
it('returns raw daysOfCover when crudeImportsKbd is 0', () => {
assert.equal(computeEffectiveCoverDays(90, false, 50, 0), 90);
});
it('scales cover days by the loss ratio', () => {
// 90 days cover, 50% loss of 200 kbd imports = ratio 0.5
// effectiveCoverDays = round(90 / 0.5) = 180
const result = computeEffectiveCoverDays(90, false, 100, 200);
assert.equal(result, 180);
});
it('produces shorter cover days for higher loss ratios', () => {
// 90 days cover, 90% disruption of 200 kbd = 180 kbd loss, ratio 0.9
// effectiveCoverDays = round(90 / 0.9) = 100
const result = computeEffectiveCoverDays(90, false, 180, 200);
assert.equal(result, 100);
});
});
describe('assessment string branches', () => {
it('uses insufficient data message when dataAvailable is false', () => {
const assessment = buildAssessment('XZ', 'suez', false, 0, 0, 0, 50, []);
assert.ok(assessment.includes('Insufficient import data'));
assert.ok(assessment.includes('XZ'));
assert.ok(assessment.includes('suez'));
});
it('uses net-exporter branch when effectiveCoverDays === -1', () => {
const assessment = buildAssessment('SA', 'hormuz', true, 0.8, -1, 0, 50, []);
assert.ok(assessment.includes('net oil exporter'));
});
it('net-exporter branch takes priority over low-Gulf-share branch', () => {
const assessment = buildAssessment('NO', 'hormuz', true, 0.05, -1, 0, 50, []);
assert.ok(assessment.includes('net oil exporter'));
assert.ok(!assessment.includes('low Gulf crude dependence'));
});
it('uses low-dependence branch when gulfCrudeShare < 0.1', () => {
const assessment = buildAssessment('DE', 'hormuz', true, 0.05, 180, 90, 50, []);
assert.ok(assessment.includes('low Gulf crude dependence'));
assert.ok(assessment.includes('5%'));
});
it('uses IEA cover branch when effectiveCoverDays > 90', () => {
const assessment = buildAssessment('US', 'hormuz', true, 0.4, 180, 90, 50, []);
assert.ok(assessment.includes('bridge'));
assert.ok(assessment.includes('180 days'));
});
it('uses deficit branch when dataAvailable, gulfShare >= 0.1, effectiveCoverDays <= 90', () => {
const products = [
{ product: 'Diesel', deficitPct: 25.0 },
{ product: 'Jet fuel', deficitPct: 20.0 },
];
const assessment = buildAssessment('IN', 'malacca', true, 0.5, 60, 30, 75, products);
assert.ok(assessment.includes('faces'));
assert.ok(assessment.includes('25.0% diesel deficit'));
assert.ok(assessment.includes('25.0%'));
});
});
});