Files
worldmonitor/tests/route-explorer-lane.test.mts
Elie Habib 822eef0fa6 feat(supply-chain): Sprint 1 — Route Explorer wrapper RPC (#2980)
* feat(supply-chain): Sprint 1 — Route Explorer wrapper RPC

Adds an internal wrapper around the vendor-only route-intelligence
compute so the upcoming Route Explorer UI can call it from a browser
PRO session instead of forcing an X-WorldMonitor-Key API gate.

Backend:
- New proto get-route-explorer-lane.proto with GetRouteExplorerLane{Request,Response}
- New handler server/worldmonitor/supply-chain/v1/get-route-explorer-lane.ts
- New static lookup tables _route-explorer-static-tables.ts:
  TRANSIT_DAYS_BY_ROUTE_ID, FREIGHT_USD_BY_CARGO_TYPE,
  BYPASS_CORRIDOR_GEOMETRY_BY_ID — covers all 5 land-bridge corridors
  plus every sea-alternative corridor with hand-curated coordinates
- Wired into supply-chain handler.ts service dispatcher
- Cache key ROUTE_EXPLORER_LANE_KEY in cache-keys.ts (NOT in BOOTSTRAP_KEYS)
- Gateway entry: PREMIUM_RPC_PATHS + RPC_CACHE_TIER 'slow-browser'
- Premium path entry in src/shared/premium-paths.ts so browser PRO auth attaches

Response contract enriches route-intelligence with:
- primaryRouteGeometry polyline from TRADE_ROUTES (lon/lat pairs)
- fromPort/toPort coords on every bypass option so the client can call
  MapContainer.setBypassRoutes directly without geometry lookups
- status: 'active' | 'proposed' | 'unavailable' derived from corridor notes
  to honestly label kra_canal_future and black_sea_western_ports
- estTransitDaysRange + estFreightUsdPerTeuRange from static tables
- noModeledLane: true when origin/destination clusters share no routes

Client wrapper fetchRouteExplorerLane added to src/services/supply-chain/index.ts.

Tests: tests/route-explorer-lane.test.mts — 30-query smoke matrix
(10 country pairs × 3 HS2 codes), structural assertions only, no
hard-coded transit/cost values. Test exposes a pure computeLane()
function with an injectable status map so it does not need Redis.

Gap report (from smoke run): 12 of 30 queries fall back to a synthetic
primaryRouteId because the destination's port cluster has no shared route
with the origin (US-JP, ZA-IN, CL-CN, TR-DE × 3 HS2 each). These pairs
return noModeledLane:true; Sprint 3 will render an empty-state for them.

Plan: docs/plans/2026-04-11-001-feat-worldwide-route-explorer-plan.md

* fix(route-explorer): address PR #2980 review findings

P1: bypass warRiskTier was hard-coded to WAR_RISK_TIER_NORMAL, dropping
the live risk signal from chokepoint status. Now derived from the
statusMap via the corridor's primaryChokepointId.

P2: freight fallback in emptyResponse and client-side empty payload used
a cargo-agnostic container range for all cargo types. Removed the ranges
entirely from fallback/noModeledLane responses; they are only present
when the lane is actually modeled.

Suggestion: when noModeledLane is true, the response now returns empty
primaryRouteId, empty geometry, empty exposures, empty bypasses, and
omits transit/freight ranges. Previously it returned plausible-looking
synthetic data from the origin's first route which could mislead the UI.

Tests updated to assert the noModeledLane contract: empty fields when
the flag is set, non-empty ranges only when the lane is modeled.

* fix(route-explorer): cargo-aware route ranking + bypass waypoint risk

P1: primary route selection was order-dependent, picking whichever
shared route the origin cluster listed first. Mixed clusters like
CN/JP could return an energy lane for a container request. Now ranks
shared routes by cargo-category compatibility (container→container,
tanker→energy, bulk→bulk, roro→container) before selecting.

P1: bypass warRiskTier was copied from the primary chokepoint instead
of derived from the corridor's own waypointChokepointIds. This
overstated risk for alternatives like Cape of Good Hope whose waypoints
may have a lower risk tier. Now uses max-tier across waypoint
chokepoints, matching get-bypass-options.ts logic.

Suggestion: placeholder corridors with addedTransitDays=0 (like
gibraltar_no_bypass, cape_of_good_hope_is_bypass) are now filtered out.
Previously they could surface as active alternatives.

Regression tests added:
- CN→JP tanker: asserts energy route is selected over container route
- CN→DE with faked Suez=CRITICAL / Cape=NORMAL: asserts Cape bypass
  shows NORMAL, not CRITICAL
- ES→EG: asserts zero-transit-day placeholders are excluded

* fix(route-explorer): scope exposures to primary route + narrow placeholder filter

P1: chokepointExposures and bypassOptions were computed from the full
sharedRoutes set, mixing data from energy/container corridors into a
single response. Now scoped to the cargo-ranked primaryRouteId only,
matching the proto contract that exposures are "on the primary route."

P2: the addedTransitDays === 0 filter was too broad and removed
kra_canal_future (a proposed bypass with real modeling). Narrowed to an
explicit PLACEHOLDER_CORRIDOR_IDS set (gibraltar_no_bypass,
cape_of_good_hope_is_bypass) so proposed zero-day corridors survive and
are surfaced with CORRIDOR_STATUS_PROPOSED.

Regression tests:
- chokepointExposures follow primaryRouteId (CN->JP container)
- kra_canal_future appears as CORRIDOR_STATUS_PROPOSED for Malacca routes
- placeholder filter still excludes explicit placeholders

* fix(route-explorer): address PR #2980 review comments

1. Unavailable corridors without waypoints (e.g. black_sea_western_ports)
   now derive WAR_RISK_TIER_WAR_ZONE from their CORRIDOR_STATUS_UNAVAILABLE
   status, instead of returning WAR_RISK_TIER_UNSPECIFIED. Corridors with
   waypointChokepointIds still use max-tier across those waypoints.

2. Added fixture test with non-empty status map (suez=75/HIGH,
   malacca=30/ELEVATED) so disruptionScore and warRiskTier assertions are
   not trivially satisfied by the empty-map default path.

3. Documented the single-chokepoint bypass design gap in the test gap report:
   bypassOptions only cover the primary chokepoint; multi-chokepoint routes
   show exposure for all but bypass guidance for only the top one. Sprint 3
   will decide whether to expand to top-N or add a UI hint.
2026-04-12 08:16:02 +04:00

326 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Smoke test matrix for `get-route-explorer-lane`.
*
* Calls the pure `computeLane` function (no Redis, no premium gate) for 30
* representative country pairs × HS2 codes and asserts on response *structure*
* — not hard-coded transit/cost values, which would drift as the underlying
* static tables change.
*
* The matrix also doubles as a gap report: any pair with empty
* `chokepointExposures` or `bypassOptions` is logged so Sprint 3/5 can plan
* empty-state work.
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { computeLane } from '../server/worldmonitor/supply-chain/v1/get-route-explorer-lane.ts';
import type { GetRouteExplorerLaneRequest } from '../src/generated/server/worldmonitor/supply_chain/v1/service_server.ts';
const PAIRS: Array<[string, string, string]> = [
['CN', 'DE', 'high-volume baseline'],
['US', 'JP', 'transpacific'],
['IR', 'CN', 'Hormuz-dependent'],
['BR', 'NL', 'Atlantic'],
['AU', 'KR', 'Pacific'],
['ZA', 'IN', 'Cape of Good Hope'],
['EG', 'IT', 'Mediterranean short-haul'],
['NG', 'CN', 'Africa to Asia crude'],
['CL', 'CN', 'South America to Asia copper'],
['TR', 'DE', 'semi-landlocked, tests land-bridge path'],
];
const HS2_CODES = ['27', '85', '10'];
const VALID_WAR_RISK_TIERS = new Set([
'WAR_RISK_TIER_UNSPECIFIED',
'WAR_RISK_TIER_NORMAL',
'WAR_RISK_TIER_ELEVATED',
'WAR_RISK_TIER_HIGH',
'WAR_RISK_TIER_CRITICAL',
'WAR_RISK_TIER_WAR_ZONE',
]);
const VALID_STATUSES = new Set([
'CORRIDOR_STATUS_UNSPECIFIED',
'CORRIDOR_STATUS_ACTIVE',
'CORRIDOR_STATUS_PROPOSED',
'CORRIDOR_STATUS_UNAVAILABLE',
]);
interface GapRow {
pair: string;
hs2: string;
primaryRouteId: string;
noModeledLane: boolean;
exposures: number;
bypasses: number;
reason: string;
}
const gapRows: GapRow[] = [];
describe('get-route-explorer-lane smoke matrix (30 queries)', () => {
for (const [fromIso2, toIso2, reason] of PAIRS) {
for (const hs2 of HS2_CODES) {
it(`${fromIso2} -> ${toIso2}, HS ${hs2} (${reason})`, async () => {
const req: GetRouteExplorerLaneRequest = {
fromIso2,
toIso2,
hs2,
cargoType: hs2 === '27' ? 'tanker' : hs2 === '10' ? 'bulk' : 'container',
};
// Pass an empty chokepoint-status map so the test does not depend on
// a live Redis cache. War risk + disruption come back as defaults.
const res = await computeLane(req, new Map());
// Echoed inputs
assert.equal(res.fromIso2, fromIso2);
assert.equal(res.toIso2, toIso2);
assert.equal(res.hs2, hs2);
// Cargo type echoed and valid
assert.match(res.cargoType, /^(container|tanker|bulk|roro)$/);
// primaryRouteId is either non-empty OR noModeledLane is set
if (!res.primaryRouteId) {
assert.equal(
res.noModeledLane,
true,
'empty primaryRouteId requires noModeledLane=true',
);
}
// primaryRouteGeometry is an array (may be empty when no modeled lane)
assert.ok(Array.isArray(res.primaryRouteGeometry));
for (const pt of res.primaryRouteGeometry) {
assert.equal(typeof pt.lon, 'number');
assert.equal(typeof pt.lat, 'number');
assert.ok(Number.isFinite(pt.lon));
assert.ok(Number.isFinite(pt.lat));
}
// chokepointExposures is an array of well-formed entries
assert.ok(Array.isArray(res.chokepointExposures));
for (const e of res.chokepointExposures) {
assert.equal(typeof e.chokepointId, 'string');
assert.equal(typeof e.chokepointName, 'string');
assert.equal(typeof e.exposurePct, 'number');
assert.ok(e.exposurePct >= 0 && e.exposurePct <= 100);
}
// bypassOptions is an array of well-formed entries
assert.ok(Array.isArray(res.bypassOptions));
for (const b of res.bypassOptions) {
assert.equal(typeof b.id, 'string');
assert.equal(typeof b.name, 'string');
assert.equal(typeof b.type, 'string');
assert.equal(typeof b.addedTransitDays, 'number');
assert.equal(typeof b.addedCostMultiplier, 'number');
assert.ok(VALID_STATUSES.has(b.status));
assert.ok(b.fromPort, 'bypass option must include fromPort');
assert.ok(b.toPort, 'bypass option must include toPort');
assert.equal(typeof b.fromPort.lon, 'number');
assert.equal(typeof b.fromPort.lat, 'number');
assert.equal(typeof b.toPort.lon, 'number');
assert.equal(typeof b.toPort.lat, 'number');
assert.ok(Number.isFinite(b.fromPort.lon));
assert.ok(Number.isFinite(b.toPort.lon));
}
// war risk tier is in the known enum set
assert.ok(
VALID_WAR_RISK_TIERS.has(res.warRiskTier),
`unexpected warRiskTier: ${res.warRiskTier}`,
);
// disruption score is a finite number in [0, 100]
assert.equal(typeof res.disruptionScore, 'number');
assert.ok(res.disruptionScore >= 0 && res.disruptionScore <= 100);
// transit + freight ranges: present and well-formed when lane is modeled;
// omitted when noModeledLane is true (no synthetic estimates)
if (!res.noModeledLane) {
assert.ok(res.estTransitDaysRange, 'modeled lane must include transit range');
assert.ok(res.estFreightUsdPerTeuRange, 'modeled lane must include freight range');
assert.ok(Number.isFinite(res.estTransitDaysRange.min));
assert.ok(Number.isFinite(res.estTransitDaysRange.max));
assert.ok(res.estTransitDaysRange.min <= res.estTransitDaysRange.max);
assert.ok(res.estFreightUsdPerTeuRange.min <= res.estFreightUsdPerTeuRange.max);
} else {
assert.equal(res.primaryRouteId, '', 'noModeledLane must have empty primaryRouteId');
assert.equal(res.primaryRouteGeometry.length, 0, 'noModeledLane must have empty geometry');
assert.equal(res.chokepointExposures.length, 0, 'noModeledLane must have empty exposures');
assert.equal(res.bypassOptions.length, 0, 'noModeledLane must have empty bypasses');
}
// fetchedAt is an ISO string
assert.equal(typeof res.fetchedAt, 'string');
assert.ok(res.fetchedAt.length > 0);
// Record gap-report metadata for the run summary
gapRows.push({
pair: `${fromIso2}->${toIso2}`,
hs2,
primaryRouteId: res.primaryRouteId,
noModeledLane: res.noModeledLane,
exposures: res.chokepointExposures.length,
bypasses: res.bypassOptions.length,
reason,
});
});
}
}
it('gap report summary (informational, never fails)', () => {
// Print a compact gap report so plan reviewers can see which pairs
// returned synthetic / empty data.
if (gapRows.length === 0) {
// No-op when run before the matrix above (test ordering is preserved)
return;
}
const noLane = gapRows.filter((r) => r.noModeledLane);
const emptyExposures = gapRows.filter((r) => r.exposures === 0);
const emptyBypasses = gapRows.filter((r) => r.bypasses === 0);
// eslint-disable-next-line no-console
console.log(
`\n[gap report] ${gapRows.length} queries | ${noLane.length} synthetic-fallback | ${emptyExposures.length} empty exposures | ${emptyBypasses.length} empty bypasses`,
);
if (noLane.length > 0) {
// eslint-disable-next-line no-console
console.log(' synthetic-fallback pairs:');
for (const r of noLane) {
// eslint-disable-next-line no-console
console.log(` ${r.pair} HS${r.hs2} -> ${r.primaryRouteId || '(none)'}`);
}
}
// eslint-disable-next-line no-console
console.log(
'\n[design gap] bypassOptions are only computed for the primary chokepoint (highest exposurePct).' +
'\nMulti-chokepoint routes (e.g. CN->DE via Malacca + Suez) show exposure data for both but' +
'\nbypass guidance only for the primary one. Sprint 3 should decide: expand to top-N chokepoints,' +
'\nor show a "see also" hint in the UI.',
);
// Always passes; informational only.
assert.ok(true);
});
it('cargo-aware route selection: CN->JP tanker picks energy route over container', async () => {
const res = await computeLane(
{ fromIso2: 'CN', toIso2: 'JP', hs2: '27', cargoType: 'tanker' },
new Map(),
);
if (!res.noModeledLane && res.primaryRouteId) {
const { TRADE_ROUTES } = await import('../src/config/trade-routes.ts');
const route = TRADE_ROUTES.find((r: { id: string }) => r.id === res.primaryRouteId);
assert.ok(route, `primaryRouteId ${res.primaryRouteId} not in TRADE_ROUTES`);
assert.equal(
route.category,
'energy',
`tanker request should prefer an energy route, got ${route.category} (${res.primaryRouteId})`,
);
}
});
it('bypass warRiskTier derives from waypoint chokepoints, not primary', async () => {
const fakeStatus = new Map<string, { id: string; warRiskTier: string }>([
['suez', { id: 'suez', warRiskTier: 'WAR_RISK_TIER_CRITICAL' }],
['cape_of_good_hope', { id: 'cape_of_good_hope', warRiskTier: 'WAR_RISK_TIER_NORMAL' }],
]);
const res = await computeLane(
{ fromIso2: 'CN', toIso2: 'DE', hs2: '85', cargoType: 'container' },
fakeStatus as Map<string, any>,
);
const capeBypass = res.bypassOptions.find((b) => b.id === 'suez_cape_of_good_hope');
if (capeBypass) {
assert.equal(
capeBypass.warRiskTier,
'WAR_RISK_TIER_NORMAL',
'Cape bypass should reflect its own waypoint risk (NORMAL), not the primary chokepoint (CRITICAL)',
);
}
});
it('placeholder corridors are excluded but proposed zero-day corridors survive', async () => {
const res = await computeLane(
{ fromIso2: 'ES', toIso2: 'EG', hs2: '85', cargoType: 'container' },
new Map(),
);
const placeholder = res.bypassOptions.find((b) =>
b.id === 'gibraltar_no_bypass' || b.id === 'cape_of_good_hope_is_bypass',
);
assert.equal(placeholder, undefined, 'explicit placeholder corridors should be filtered out');
});
it('kra_canal_future appears as CORRIDOR_STATUS_PROPOSED for Malacca routes', async () => {
const res = await computeLane(
{ fromIso2: 'CN', toIso2: 'DE', hs2: '85', cargoType: 'container' },
new Map(),
);
const kra = res.bypassOptions.find((b) => b.id === 'kra_canal_future');
if (kra) {
assert.equal(
kra.status,
'CORRIDOR_STATUS_PROPOSED',
'kra_canal_future should be surfaced as proposed, not filtered out',
);
}
});
it('disruptionScore and warRiskTier reflect injected status map', async () => {
const fakeStatus = new Map<string, { id: string; disruptionScore?: number; warRiskTier?: string }>([
['suez', { id: 'suez', disruptionScore: 75, warRiskTier: 'WAR_RISK_TIER_HIGH' }],
['malacca_strait', { id: 'malacca_strait', disruptionScore: 30, warRiskTier: 'WAR_RISK_TIER_ELEVATED' }],
]);
const res = await computeLane(
{ fromIso2: 'CN', toIso2: 'DE', hs2: '85', cargoType: 'container' },
fakeStatus as Map<string, any>,
);
if (res.noModeledLane) return;
assert.ok(res.disruptionScore > 0, 'disruptionScore should reflect injected data, not default to 0');
assert.notEqual(res.warRiskTier, 'WAR_RISK_TIER_NORMAL', 'warRiskTier should reflect injected data');
});
it('unavailable corridor without waypoints gets WAR_RISK_TIER_WAR_ZONE', async () => {
const fakeStatus = new Map<string, { id: string; warRiskTier?: string }>([
['kerch_strait', { id: 'kerch_strait', warRiskTier: 'WAR_RISK_TIER_WAR_ZONE' }],
]);
const res = await computeLane(
{ fromIso2: 'RU', toIso2: 'TR', hs2: '27', cargoType: 'tanker' },
fakeStatus as Map<string, any>,
);
const unavailable = res.bypassOptions.find((b) => b.status === 'CORRIDOR_STATUS_UNAVAILABLE');
if (unavailable) {
assert.equal(
unavailable.warRiskTier,
'WAR_RISK_TIER_WAR_ZONE',
'unavailable corridors without waypoints should derive WAR_ZONE from status',
);
}
});
it('chokepointExposures and bypassOptions follow the primaryRouteId', async () => {
const res = await computeLane(
{ fromIso2: 'CN', toIso2: 'JP', hs2: '85', cargoType: 'container' },
new Map(),
);
if (res.noModeledLane || !res.primaryRouteId) return;
const { TRADE_ROUTES } = await import('../src/config/trade-routes.ts');
const { CHOKEPOINT_REGISTRY } = await import('../server/_shared/chokepoint-registry.ts');
const route = TRADE_ROUTES.find((r: { id: string }) => r.id === res.primaryRouteId);
assert.ok(route, `primaryRouteId ${res.primaryRouteId} not in TRADE_ROUTES`);
const routeChokepointIds = new Set(
CHOKEPOINT_REGISTRY
.filter((cp: { routeIds: string[] }) => cp.routeIds.includes(res.primaryRouteId))
.map((cp: { id: string }) => cp.id),
);
for (const exp of res.chokepointExposures) {
assert.ok(
routeChokepointIds.has(exp.chokepointId),
`chokepoint ${exp.chokepointId} is not on the primary route ${res.primaryRouteId}`,
);
}
});
});