mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* feat(resilience): PR 2 dimension wiring — split reserveAdequacy + add sovereignFiscalBuffer Plan §3.4 follow-up to #3305 + #3319. Lands the scorer + dimension registration so the SWF seed from the Railway cron feeds a real score once the bake-in window closes. No weight rebalance yet (separate commit with Spearman sensitivity check), no health.js graduation yet (7-day ON_DEMAND window per feedback_health_required_key_needs_ railway_cron_first.md), no bootstrap wiring yet (follow-up PR). Shape of the change Retirement: - reserveAdequacy joins fuelStockDays in RESILIENCE_RETIRED_DIMENSIONS. The legacy scorer now mirrors scoreFuelStockDays: returns coverage=0 / imputationClass=null so the dimension is filtered out of the confidence / coverage averages via the registry filter in computeLowConfidence, computeOverallCoverage, and the widget's formatResilienceConfidence. Kept in RESILIENCE_DIMENSION_ORDER for structural continuity (tests, cached payload shape, registry membership). Indicator registry tier demoted to 'experimental'. Two new active dimensions: - liquidReserveAdequacy (replaces the liquid-reserves half of the retired reserveAdequacy). Same source (WB FI.RES.TOTL.MO, total reserves in months of imports) but re-anchored 1..12 months instead of 1..18. Twelve months ≈ IMF "full reserve adequacy" benchmark for a diversified emerging-market importer — the tighter ceiling prevents wealthy commodity-exporters from claiming outsized credit for on-paper reserve stocks that are not the relevant shock-absorption buffer. - sovereignFiscalBuffer. Reads resilience:recovery:sovereign-wealth:v1 (populated by scripts/seed-sovereign-wealth.mjs, landed in #3305 + wired into Railway cron in #3319). Computes the saturating transform: effectiveMonths = Σ [ aum/annualImports × 12 × access × liquidity × transparency ] score = 100 × (1 − exp(−effectiveMonths / 12)) Exponential saturation prevents Norway-type outliers (effective months in the 100s) from dominating the recovery pillar. Three code paths in scoreSovereignFiscalBuffer: 1. Seed key absent entirely → IMPUTE.recoverySovereignFiscalBuffer (score 50 / coverage 0.3 / unmonitored). Covers the Railway-cron bake-in window before the first successful tick. 2. Seed present, country NOT in manifest → score=0 with FULL coverage. Substantive absence, NOT imputation — per plan §3.4 "What happens to no-SWF countries." 0 × weight = 0 in the numerator, so the country correctly scores lower than SWF-holding peers on this dim. 3. Seed present, country in payload → saturating score, coverage derated by the partial-seed completeness signal (so a Mubadala or Temasek scrape drift on a multi-fund country shows up as lower confidence rather than a silently-understated total). Indicator registry: - Demoted recoveryReserveMonths (tied to retired reserveAdequacy) to tier='experimental'. - Added recoveryLiquidReserveMonths: WB FI.RES.TOTL.MO, anchors 1..12, tier='core', coverage=188. - Added recoverySovereignWealthEffectiveMonths: the new SWF signal, tier='experimental' for now because the manifest only has 8 funds (below the 180-core / 137-§3.6-gate threshold). Graduating to 'core' requires expanding the manifest past ~137 entries — a later PR. Tests updated - resilience-release-gate: 19→21 dim count; RETIRED_DIMENSIONS allow- list now includes reserveAdequacy alongside fuelStockDays. - resilience-dimension-scorers: scoreReserveAdequacy monotonicity + "high reserves score well" tests migrated to scoreLiquidReserve- Adequacy (same source, new 1..12 anchor). New retirement-shape test for scoreReserveAdequacy mirroring the PR 3 fuelStockDays retirement test. Four new scorer tests pin the three code paths of scoreSovereignFiscalBuffer (absent seed / no-SWF country / SWF country / partial-completeness derate). - resilience-scorers fixture: baseline 60.12→60.35, recovery-domain flat mean 47.33→48.75, overall 63.27→63.6. Each number commented with the driver (split adds liquidReserveAdequacy 18@1.0 + sovereign FiscalBuffer 50@0.3 at IMPUTE; retired reserveAdequacy drops out). - resilience-dimension-monotonicity: target scoreLiquidReserveAdequacy instead of scoreReserveAdequacy. - resilience-handlers: response-shape dim count 19→21. - resilience-indicator-registry: coverage 19→21 dimensions. - resilience-dimension-freshness: allowlisted the new sovereign-wealth seed-meta key in KNOWN_SEEDS_NOT_IN_HEALTH for the ON_DEMAND window. - resilience-methodology-lint HEADING_TO_DIMENSION: added the two new heading mappings. Methodology doc gets H4 sections for Liquid Reserve Adequacy and Sovereign Fiscal Buffer; Reserve Adequacy section is annotated as retired. - resilience-retired-dimensions-parity: client-side RESILIENCE_RETIRED_DIMENSION_IDS gets reserveAdequacy. Parser upgraded to strip inline `// …` comments from the array body so a future reviewer can drop a rationale next to an entry without breaking parity. - resilience-confidence-averaging: fixture updated to include both retired dims (reserveAdequacy + fuelStockDays) — confirms the registry filter correctly excludes BOTH from the visible coverage reading. Extraction harness (scripts/compare-resilience-current-vs-proposed.mjs): - recoveryLiquidReserveMonths: reads the same reserve-adequacy seed field as recoveryReserveMonths. - recoverySovereignWealthEffectiveMonths: reads the new SWF seed key on field totalEffectiveMonths. Absent-payload → 0 for correlation math (matches the substantive-no-SWF scorer branch). Out of scope for this commit (follow-ups) - Recovery-domain weight rebalance + Spearman sensitivity rerun against the PR 0 baseline. - health.js graduation (SEED_META entry + ON_DEMAND_KEYS removal) once Railway cron has ~7 days of clean runs. - api/bootstrap.js wiring once an RPC consumer needs the SWF data. - Manifest expansion past 137 countries so sovereignFiscalBuffer can graduate from tier='experimental' to tier='core'. Tests: 6573/6573 data-tier tests pass. Typecheck clean on both tsconfig configs. Biome clean on all touched files. * fix(resilience): PR 2 review — add widget labels for new dimensions P2 review finding on PR #3324. DIMENSION_LABELS in src/components/ resilience-widget-utils.ts covered only the old 19 dimension IDs, so the two new active dims (liquidReserveAdequacy, sovereignFiscalBuffer) would render with their raw internal IDs in the confidence grid for every country once the scorer started emitting them. The widget test at getResilienceDimensionLabel also asserted only the 19-label set, so the gap would have shipped silently. Fix: add user-facing short labels for both new dims. "Reserves" is already claimed by the retired reserveAdequacy, so the replacement disambiguates with "Liquid Reserves"; sovereignFiscalBuffer → "Sovereign Wealth" per the methodology doc H4 heading. Also added a regression guard — new test asserts EVERY id in RESILIENCE_DIMENSION_ORDER resolves to a non-id label. Any future dimension that ships without a matching DIMENSION_LABELS entry now fails CI loudly instead of leaking the ID into the UI. Tests: 502/502 resilience tests pass (+1 new coverage check). Typecheck clean on both configs. * fix(resilience): PR 2 review — remove dead IMPUTE.recoveryReserveAdequacy entry Greptile P2: the retired scoreReserveAdequacy stub no longer reads from IMPUTE (it hardcodes coverage=0 / imputationClass=null per the retirement pattern), making IMPUTE.recoveryReserveAdequacy dead code. Removed the entry + added a breadcrumb comment pointing at the replacement IMPUTE.recoveryLiquidReserveAdequacy. The second P2 (bootstrap.js not wired) is a deliberate non-goal — the reviewer explicitly flags "for visibility" since it's tracked in the PR body. No action this commit; bootstrap wiring lands alongside the SEED_META graduation after the ~7-day Railway-cron bake-in. Tests: 502/502 resilience tests still pass. Typecheck clean.
512 lines
22 KiB
TypeScript
512 lines
22 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
LOCKED_PREVIEW,
|
|
collectDimensionConfidences,
|
|
formatBaselineStress,
|
|
formatDimensionConfidence,
|
|
formatResilienceChange30d,
|
|
formatResilienceConfidence,
|
|
formatResilienceDataVersion,
|
|
getImputationClassIcon,
|
|
getImputationClassLabel,
|
|
getResilienceDimensionLabel,
|
|
getResilienceDomainLabel,
|
|
getResilienceTrendArrow,
|
|
getResilienceVisualLevel,
|
|
getStalenessLabel,
|
|
} from '../src/components/resilience-widget-utils';
|
|
import type { ResilienceScoreResponse } from '../src/services/resilience';
|
|
|
|
const baseResponse: ResilienceScoreResponse = {
|
|
countryCode: 'US',
|
|
overallScore: 73,
|
|
baselineScore: 82,
|
|
stressScore: 58,
|
|
stressFactor: 0.21,
|
|
level: 'high',
|
|
domains: [
|
|
{ id: 'economic', score: 80, weight: 0.22, dimensions: [
|
|
{ id: 'macroFiscal', score: 80, coverage: 0.9, observedWeight: 1, imputedWeight: 0 },
|
|
] },
|
|
],
|
|
trend: 'rising',
|
|
change30d: 2.4,
|
|
lowConfidence: false,
|
|
imputationShare: 0,
|
|
dataVersion: '2026-04-03',
|
|
};
|
|
|
|
test('getResilienceVisualLevel maps the score thresholds from the widget spec', () => {
|
|
assert.equal(getResilienceVisualLevel(80), 'very_high');
|
|
assert.equal(getResilienceVisualLevel(79), 'high');
|
|
assert.equal(getResilienceVisualLevel(60), 'high');
|
|
assert.equal(getResilienceVisualLevel(59), 'moderate');
|
|
assert.equal(getResilienceVisualLevel(20), 'low');
|
|
assert.equal(getResilienceVisualLevel(19), 'very_low');
|
|
assert.equal(getResilienceVisualLevel(Number.NaN), 'unknown');
|
|
});
|
|
|
|
test('getResilienceTrendArrow renders the expected glyphs', () => {
|
|
assert.equal(getResilienceTrendArrow('rising'), '↑');
|
|
assert.equal(getResilienceTrendArrow('falling'), '↓');
|
|
assert.equal(getResilienceTrendArrow('stable'), '→');
|
|
assert.equal(getResilienceTrendArrow('unknown'), '→');
|
|
});
|
|
|
|
test('getResilienceDomainLabel keeps the deep-dive shorthand labels stable', () => {
|
|
assert.equal(getResilienceDomainLabel('economic'), 'Economic');
|
|
assert.equal(getResilienceDomainLabel('infrastructure'), 'Infra & Supply');
|
|
assert.equal(getResilienceDomainLabel('energy'), 'Energy');
|
|
assert.equal(getResilienceDomainLabel('social-governance'), 'Social & Gov');
|
|
assert.equal(getResilienceDomainLabel('health-food'), 'Health & Food');
|
|
// Regression for the missing sixth-domain label. Before this pin, the
|
|
// recovery row rendered as the raw id "recovery" because DOMAIN_LABELS
|
|
// was a 5-entry map from the pre-recovery-domain era.
|
|
assert.equal(getResilienceDomainLabel('recovery'), 'Recovery');
|
|
assert.equal(getResilienceDomainLabel('custom-domain'), 'custom-domain');
|
|
});
|
|
|
|
test('formatResilienceConfidence shows sparse-data copy when low confidence is set', () => {
|
|
assert.equal(formatResilienceConfidence(baseResponse), 'Coverage 90% ✓');
|
|
assert.equal(
|
|
formatResilienceConfidence({ ...baseResponse, lowConfidence: true }),
|
|
'Low confidence — sparse data',
|
|
);
|
|
});
|
|
|
|
// PR 3 §3.5 follow-up: retired dimensions (fuelStockDays, post-PR-3)
|
|
// return coverage=0 structurally (by design, not by sparsity) and
|
|
// contribute zero weight to domain scoring. The widget's displayed
|
|
// coverage percentage must exclude them — otherwise a deliberate
|
|
// construct retirement would drag the user-facing confidence reading
|
|
// down for every country even though the dimension is not part of the
|
|
// score. Reviewer P1 anchor: US shows avgCoverage=0.8105 with retired
|
|
// dim included vs 0.8556 with retired excluded.
|
|
//
|
|
// Important: the filter is keyed on the retired-dim ID, NOT on
|
|
// `coverage === 0`. A non-retired dimension can legitimately emit
|
|
// coverage=0 on a genuinely sparse-data country (via weightedBlend
|
|
// fall-through), and those entries must continue to drag confidence
|
|
// down — that is the sparse-data signal lowConfidence exists to
|
|
// surface.
|
|
test('formatResilienceConfidence excludes retired dimensions by ID (not by coverage=0)', () => {
|
|
const withRetired: ResilienceScoreResponse = {
|
|
...baseResponse,
|
|
domains: [
|
|
{ id: 'economic', score: 80, weight: 0.22, dimensions: [
|
|
{ id: 'macroFiscal', score: 80, coverage: 0.9, observedWeight: 1, imputedWeight: 0 },
|
|
// Non-retired dim with coverage=0: must STAY in the average
|
|
// (genuine data sparsity, not a retirement).
|
|
{ id: 'currencyExternal', score: 50, coverage: 0, observedWeight: 0, imputedWeight: 0 },
|
|
] },
|
|
{ id: 'recovery', score: 65, weight: 1.0, dimensions: [
|
|
{ id: 'fiscalSpace', score: 72, coverage: 0.8, observedWeight: 0.8, imputedWeight: 0.2 },
|
|
// Retired dimension: coverage=0 is structural; must be excluded.
|
|
{ id: 'fuelStockDays', score: 50, coverage: 0, observedWeight: 0, imputedWeight: 0 },
|
|
] },
|
|
],
|
|
};
|
|
// Average over non-retired entries: (0.9 + 0 + 0.8) / 3 = 0.5667 → 57%.
|
|
// If fuelStockDays were included: (0.9 + 0 + 0.8 + 0) / 4 = 0.425 → 43%.
|
|
// If we filtered by coverage=0: (0.9 + 0.8) / 2 = 0.85 → 85% (the
|
|
// over-aggressive filter that would mask genuine sparsity).
|
|
assert.equal(formatResilienceConfidence(withRetired), 'Coverage 57% ✓');
|
|
});
|
|
|
|
test('formatResilienceChange30d preserves explicit sign formatting', () => {
|
|
assert.equal(formatResilienceChange30d(2.41), '30d +2.4');
|
|
assert.equal(formatResilienceChange30d(-1.26), '30d -1.3');
|
|
assert.equal(formatResilienceChange30d(0), '30d 0.0');
|
|
});
|
|
|
|
test('formatBaselineStress renders the expected breakdown string (no Impact)', () => {
|
|
assert.equal(formatBaselineStress(72.1, 58.3), 'Baseline: 72 | Stress: 58');
|
|
assert.equal(formatBaselineStress(80, 100), 'Baseline: 80 | Stress: 100');
|
|
assert.equal(formatBaselineStress(50, 0), 'Baseline: 50 | Stress: 0');
|
|
assert.equal(formatBaselineStress(NaN, 50), 'Baseline: 0 | Stress: 50');
|
|
});
|
|
|
|
// T1.4 Phase 1 of the country-resilience reference-grade upgrade plan.
|
|
// dataVersion is sourced from the Railway static-seed job's seed-meta key
|
|
// (fetchedAt → ISO date in _shared.ts buildResilienceScore). The widget
|
|
// renders a footer label so analysts can see how fresh the underlying
|
|
// source data is; a missing or malformed dataVersion returns an empty
|
|
// string so the caller skips rendering rather than showing a dangling label.
|
|
test('formatResilienceDataVersion renders a "Seed date" label for a valid ISO date', () => {
|
|
// Label narrowed from "Data" to "Seed date" in the review followup
|
|
// so it is clear the value reflects the static-seed bundle refresh,
|
|
// not the freshness of every live input feeding the score. Live
|
|
// inputs carry their own per-dimension freshness badges.
|
|
assert.equal(formatResilienceDataVersion('2026-04-11'), 'Seed date 2026-04-11');
|
|
assert.equal(formatResilienceDataVersion('2024-01-01'), 'Seed date 2024-01-01');
|
|
});
|
|
|
|
test('formatResilienceDataVersion returns empty for missing or malformed dataVersion', () => {
|
|
assert.equal(formatResilienceDataVersion(''), '');
|
|
assert.equal(formatResilienceDataVersion(null), '');
|
|
assert.equal(formatResilienceDataVersion(undefined), '');
|
|
// Guard against partially-formatted or non-ISO strings that the fallback
|
|
// path in _shared.ts should never emit but downstream code should still
|
|
// reject defensively:
|
|
assert.equal(formatResilienceDataVersion('2026-04'), '');
|
|
assert.equal(formatResilienceDataVersion('04/11/2026'), '');
|
|
assert.equal(formatResilienceDataVersion('not-a-date'), '');
|
|
});
|
|
|
|
test('formatResilienceDataVersion rejects regex-valid but calendar-invalid dates (PR #2943 review)', () => {
|
|
// Regex `/^\d{4}-\d{2}-\d{2}$/` accepts these strings but they are not
|
|
// real calendar dates. A stale or corrupted Redis key could emit one,
|
|
// and without the round-trip check the widget would render it unchecked.
|
|
assert.equal(formatResilienceDataVersion('9999-99-99'), '');
|
|
assert.equal(formatResilienceDataVersion('2024-13-45'), '');
|
|
assert.equal(formatResilienceDataVersion('2024-00-15'), '');
|
|
// February 30th parses as a real Date in JS but not the same string
|
|
// when round-tripped through toISOString; the round-trip check catches
|
|
// this slip, so `2024-02-30` silently rolling to `2024-03-01` is rejected.
|
|
assert.equal(formatResilienceDataVersion('2024-02-30'), '');
|
|
assert.equal(formatResilienceDataVersion('2024-02-31'), '');
|
|
// Legitimate calendar dates still pass.
|
|
assert.equal(formatResilienceDataVersion('2024-02-29'), 'Seed date 2024-02-29'); // leap year
|
|
assert.equal(formatResilienceDataVersion('2023-02-28'), 'Seed date 2023-02-28');
|
|
});
|
|
|
|
test('baseResponse includes dataVersion (regression for T1.4 wiring)', () => {
|
|
// Guards against a future change that accidentally drops the dataVersion
|
|
// field from the service response shape. The scorer writes it from the
|
|
// seed-meta key; the widget footer renders it via formatResilienceDataVersion.
|
|
assert.equal(typeof baseResponse.dataVersion, 'string');
|
|
assert.ok(baseResponse.dataVersion.length > 0, 'baseResponse should carry a non-empty dataVersion for regression coverage');
|
|
assert.equal(formatResilienceDataVersion(baseResponse.dataVersion), `Seed date ${baseResponse.dataVersion}`);
|
|
});
|
|
|
|
// T1.6 Phase 1 of the country-resilience reference-grade upgrade plan.
|
|
// Per-dimension confidence helpers. The widget renders a compact
|
|
// coverage grid below the 6-domain rows using these helpers; each
|
|
// scorer dimension must have a stable display label and a consistent
|
|
// status classification.
|
|
|
|
test('getResilienceDimensionLabel returns short stable labels for all 21 dimensions', () => {
|
|
assert.equal(getResilienceDimensionLabel('macroFiscal'), 'Macro');
|
|
assert.equal(getResilienceDimensionLabel('currencyExternal'), 'Currency');
|
|
assert.equal(getResilienceDimensionLabel('tradeSanctions'), 'Trade');
|
|
assert.equal(getResilienceDimensionLabel('cyberDigital'), 'Cyber');
|
|
assert.equal(getResilienceDimensionLabel('logisticsSupply'), 'Logistics');
|
|
assert.equal(getResilienceDimensionLabel('infrastructure'), 'Infra');
|
|
assert.equal(getResilienceDimensionLabel('energy'), 'Energy');
|
|
assert.equal(getResilienceDimensionLabel('governanceInstitutional'), 'Gov');
|
|
assert.equal(getResilienceDimensionLabel('socialCohesion'), 'Social');
|
|
assert.equal(getResilienceDimensionLabel('borderSecurity'), 'Border');
|
|
assert.equal(getResilienceDimensionLabel('informationCognitive'), 'Info');
|
|
assert.equal(getResilienceDimensionLabel('healthPublicService'), 'Health');
|
|
assert.equal(getResilienceDimensionLabel('foodWater'), 'Food');
|
|
assert.equal(getResilienceDimensionLabel('fiscalSpace'), 'Fiscal');
|
|
assert.equal(getResilienceDimensionLabel('reserveAdequacy'), 'Reserves');
|
|
assert.equal(getResilienceDimensionLabel('externalDebtCoverage'), 'Ext Debt');
|
|
assert.equal(getResilienceDimensionLabel('importConcentration'), 'Imports');
|
|
assert.equal(getResilienceDimensionLabel('stateContinuity'), 'Continuity');
|
|
assert.equal(getResilienceDimensionLabel('fuelStockDays'), 'Fuel');
|
|
// PR 2 §3.4 — new active dimensions. Retired reserveAdequacy's
|
|
// label stays ('Reserves'), and the live-data replacement
|
|
// disambiguates with 'Liquid Reserves'.
|
|
assert.equal(getResilienceDimensionLabel('liquidReserveAdequacy'), 'Liquid Reserves');
|
|
assert.equal(getResilienceDimensionLabel('sovereignFiscalBuffer'), 'Sovereign Wealth');
|
|
// Unknown dimension IDs fall through to the raw ID so the render
|
|
// never silently drops a row.
|
|
assert.equal(getResilienceDimensionLabel('unknownDim'), 'unknownDim');
|
|
});
|
|
|
|
// Every ID in RESILIENCE_DIMENSION_ORDER must have a display label —
|
|
// without this coverage the widget silently leaks raw internal IDs
|
|
// into the confidence grid for any new dimension that ships without
|
|
// a matching DIMENSION_LABELS entry (PR #3324 review-catch).
|
|
test('getResilienceDimensionLabel covers every dimension in RESILIENCE_DIMENSION_ORDER', async () => {
|
|
const { RESILIENCE_DIMENSION_ORDER } = await import('../server/worldmonitor/resilience/v1/_dimension-scorers.ts');
|
|
const leaks: string[] = [];
|
|
for (const id of RESILIENCE_DIMENSION_ORDER) {
|
|
const label = getResilienceDimensionLabel(id);
|
|
if (label === id) leaks.push(id);
|
|
}
|
|
assert.deepEqual(leaks, [],
|
|
`DIMENSION_LABELS missing entries for: ${leaks.join(', ')}. ` +
|
|
`Every new dimension must land its user-facing short label in src/components/resilience-widget-utils.ts.`);
|
|
});
|
|
|
|
test('formatDimensionConfidence classifies observed-heavy dimensions as observed', () => {
|
|
const result = formatDimensionConfidence({
|
|
id: 'macroFiscal',
|
|
coverage: 0.9,
|
|
observedWeight: 0.9,
|
|
imputedWeight: 0.1,
|
|
});
|
|
assert.equal(result.label, 'Macro');
|
|
assert.equal(result.coveragePct, 90);
|
|
assert.equal(result.status, 'observed');
|
|
assert.equal(result.absent, false);
|
|
});
|
|
|
|
test('formatDimensionConfidence classifies partial dimensions (mixed observed and imputed)', () => {
|
|
const result = formatDimensionConfidence({
|
|
id: 'currencyExternal',
|
|
coverage: 0.55,
|
|
observedWeight: 0.4,
|
|
imputedWeight: 0.6,
|
|
});
|
|
assert.equal(result.status, 'partial');
|
|
assert.equal(result.coveragePct, 55);
|
|
assert.equal(result.absent, false);
|
|
});
|
|
|
|
test('formatDimensionConfidence classifies all-imputed dimensions as imputed', () => {
|
|
const result = formatDimensionConfidence({
|
|
id: 'tradeSanctions',
|
|
coverage: 0.3,
|
|
observedWeight: 0,
|
|
imputedWeight: 1,
|
|
});
|
|
assert.equal(result.status, 'imputed');
|
|
assert.equal(result.coveragePct, 30);
|
|
assert.equal(result.absent, false);
|
|
});
|
|
|
|
test('formatDimensionConfidence handles absent dimensions (no data at all)', () => {
|
|
const result = formatDimensionConfidence({
|
|
id: 'borderSecurity',
|
|
coverage: 0,
|
|
observedWeight: 0,
|
|
imputedWeight: 0,
|
|
});
|
|
assert.equal(result.status, 'absent');
|
|
assert.equal(result.coveragePct, 0);
|
|
assert.equal(result.absent, true);
|
|
});
|
|
|
|
test('formatDimensionConfidence clamps out-of-range coverage and guards against NaN', () => {
|
|
// Coverage above 1 is clamped to 100%.
|
|
const high = formatDimensionConfidence({
|
|
id: 'energy',
|
|
coverage: 1.5,
|
|
observedWeight: 1,
|
|
imputedWeight: 0,
|
|
});
|
|
assert.equal(high.coveragePct, 100);
|
|
|
|
// Negative coverage is clamped to 0%.
|
|
const negative = formatDimensionConfidence({
|
|
id: 'energy',
|
|
coverage: -0.3,
|
|
observedWeight: 1,
|
|
imputedWeight: 0,
|
|
});
|
|
assert.equal(negative.coveragePct, 0);
|
|
|
|
// NaN fields fall through to 0 weight and absent status without throwing.
|
|
const nanResult = formatDimensionConfidence({
|
|
id: 'energy',
|
|
coverage: Number.NaN,
|
|
observedWeight: Number.NaN,
|
|
imputedWeight: Number.NaN,
|
|
});
|
|
assert.equal(nanResult.coveragePct, 0);
|
|
assert.equal(nanResult.status, 'absent');
|
|
assert.equal(nanResult.absent, true);
|
|
});
|
|
|
|
test('collectDimensionConfidences preserves scorer order across domains and dimensions', () => {
|
|
const domains = [
|
|
{
|
|
dimensions: [
|
|
{ id: 'macroFiscal', coverage: 0.9, observedWeight: 0.9, imputedWeight: 0.1 },
|
|
{ id: 'currencyExternal', coverage: 0.8, observedWeight: 0.75, imputedWeight: 0.25 },
|
|
],
|
|
},
|
|
{
|
|
dimensions: [
|
|
{ id: 'governanceInstitutional', coverage: 0.95, observedWeight: 1.0, imputedWeight: 0 },
|
|
],
|
|
},
|
|
];
|
|
const result = collectDimensionConfidences(domains);
|
|
assert.equal(result.length, 3);
|
|
assert.equal(result[0].id, 'macroFiscal');
|
|
assert.equal(result[1].id, 'currencyExternal');
|
|
assert.equal(result[2].id, 'governanceInstitutional');
|
|
// Labels are resolved for every entry.
|
|
assert.equal(result[0].label, 'Macro');
|
|
assert.equal(result[2].label, 'Gov');
|
|
});
|
|
|
|
test('collectDimensionConfidences returns an empty list for an empty response', () => {
|
|
assert.deepEqual(collectDimensionConfidences([]), []);
|
|
assert.deepEqual(collectDimensionConfidences([{ dimensions: [] }]), []);
|
|
});
|
|
|
|
// PR #2949 review followup: the gated LOCKED_PREVIEW must populate
|
|
// the per-dimension confidence grid so locked users see a blurred
|
|
// representative card instead of a blank gap between the domain rows
|
|
// and the footer. If a future edit accidentally drops a dimension
|
|
// from the preview, this regression test fails loudly.
|
|
test('LOCKED_PREVIEW populates all 19 dimensions for the gated preview (PR #2949 review)', () => {
|
|
const all = collectDimensionConfidences(LOCKED_PREVIEW.domains);
|
|
assert.equal(all.length, 19, `locked preview should carry all 19 dimensions, got ${all.length}`);
|
|
// Every cell should resolve to a short label (no raw IDs leaking through).
|
|
for (const dim of all) {
|
|
assert.ok(
|
|
dim.label.length > 0 && dim.label !== dim.id,
|
|
`${dim.id} should resolve to a short display label in the preview, got "${dim.label}"`,
|
|
);
|
|
}
|
|
// Every dimension in the preview should have non-absent status so
|
|
// the blurred grid renders a meaningful visual, never a row of empty
|
|
// "n/a" cells.
|
|
for (const dim of all) {
|
|
assert.notEqual(
|
|
dim.status,
|
|
'absent',
|
|
`${dim.id} should not be absent in the locked preview (all fixture values are populated)`,
|
|
);
|
|
}
|
|
});
|
|
|
|
// T1.6 full grid (PR 3 of 5): formatDimensionConfidence must surface
|
|
// the new imputationClass and freshness fields from PR 1 / PR 2 as
|
|
// typed nulls when unset or unknown, and the label/glyph helpers must
|
|
// map every four-class / three-level value without throwing.
|
|
|
|
test('formatDimensionConfidence normalizes imputationClass=stable-absence', () => {
|
|
const result = formatDimensionConfidence({
|
|
id: 'borderSecurity',
|
|
coverage: 0,
|
|
observedWeight: 0,
|
|
imputedWeight: 1,
|
|
imputationClass: 'stable-absence',
|
|
});
|
|
assert.equal(result.imputationClass, 'stable-absence');
|
|
});
|
|
|
|
test('formatDimensionConfidence coerces empty imputationClass to null', () => {
|
|
const result = formatDimensionConfidence({
|
|
id: 'macroFiscal',
|
|
coverage: 0.9,
|
|
observedWeight: 1,
|
|
imputedWeight: 0,
|
|
imputationClass: '',
|
|
});
|
|
assert.equal(result.imputationClass, null);
|
|
});
|
|
|
|
test('formatDimensionConfidence coerces unknown imputationClass to null (defensive)', () => {
|
|
const result = formatDimensionConfidence({
|
|
id: 'macroFiscal',
|
|
coverage: 0.9,
|
|
observedWeight: 1,
|
|
imputedWeight: 0,
|
|
imputationClass: 'lol-nope',
|
|
});
|
|
assert.equal(result.imputationClass, null);
|
|
});
|
|
|
|
test('formatDimensionConfidence normalizes freshness.staleness=fresh', () => {
|
|
const result = formatDimensionConfidence({
|
|
id: 'macroFiscal',
|
|
coverage: 0.9,
|
|
observedWeight: 1,
|
|
imputedWeight: 0,
|
|
freshness: { staleness: 'fresh', lastObservedAtMs: 1712000000000 },
|
|
});
|
|
assert.equal(result.staleness, 'fresh');
|
|
});
|
|
|
|
test('formatDimensionConfidence coerces empty freshness.staleness to null', () => {
|
|
const result = formatDimensionConfidence({
|
|
id: 'macroFiscal',
|
|
coverage: 0.9,
|
|
observedWeight: 1,
|
|
imputedWeight: 0,
|
|
freshness: { staleness: '', lastObservedAtMs: 1712000000000 },
|
|
});
|
|
assert.equal(result.staleness, null);
|
|
});
|
|
|
|
test('formatDimensionConfidence coerces freshness.lastObservedAtMs string to number', () => {
|
|
const result = formatDimensionConfidence({
|
|
id: 'macroFiscal',
|
|
coverage: 0.9,
|
|
observedWeight: 1,
|
|
imputedWeight: 0,
|
|
freshness: { staleness: 'fresh', lastObservedAtMs: '1712000000000' },
|
|
});
|
|
assert.equal(result.lastObservedAtMs, 1712000000000);
|
|
});
|
|
|
|
test('formatDimensionConfidence treats lastObservedAtMs=0 as null (no data)', () => {
|
|
const result = formatDimensionConfidence({
|
|
id: 'macroFiscal',
|
|
coverage: 0.9,
|
|
observedWeight: 1,
|
|
imputedWeight: 0,
|
|
freshness: { staleness: 'fresh', lastObservedAtMs: 0 },
|
|
});
|
|
assert.equal(result.lastObservedAtMs, null);
|
|
});
|
|
|
|
test('formatDimensionConfidence handles missing freshness and imputationClass fields', () => {
|
|
const result = formatDimensionConfidence({
|
|
id: 'macroFiscal',
|
|
coverage: 0.9,
|
|
observedWeight: 1,
|
|
imputedWeight: 0,
|
|
});
|
|
assert.equal(result.imputationClass, null);
|
|
assert.equal(result.staleness, null);
|
|
assert.equal(result.lastObservedAtMs, null);
|
|
});
|
|
|
|
test('getImputationClassIcon returns the correct glyph for each class', () => {
|
|
assert.equal(getImputationClassIcon('stable-absence'), '\u2713');
|
|
assert.equal(getImputationClassIcon('unmonitored'), '?');
|
|
assert.equal(getImputationClassIcon('source-failure'), '!');
|
|
assert.equal(getImputationClassIcon('not-applicable'), '\u2014');
|
|
assert.equal(getImputationClassIcon(null), '');
|
|
});
|
|
|
|
test('getImputationClassLabel returns a non-empty string for each class', () => {
|
|
for (const c of ['stable-absence', 'unmonitored', 'source-failure', 'not-applicable'] as const) {
|
|
const label = getImputationClassLabel(c);
|
|
assert.ok(label.length > 0, `${c} should have a tooltip label`);
|
|
}
|
|
// Null still returns a descriptive fallback (never an empty string)
|
|
// so the widget tooltip never breaks assembly.
|
|
assert.ok(getImputationClassLabel(null).length > 0);
|
|
});
|
|
|
|
test('getStalenessLabel returns a non-empty string for each level', () => {
|
|
for (const s of ['fresh', 'aging', 'stale'] as const) {
|
|
const label = getStalenessLabel(s);
|
|
assert.ok(label.length > 0, `${s} should have a tooltip label`);
|
|
}
|
|
assert.ok(getStalenessLabel(null).length > 0);
|
|
});
|
|
|
|
test('LOCKED_PREVIEW smoke: at least one dimension has imputationClass and one has staleness set (PR 3 / T1.6)', () => {
|
|
const all = collectDimensionConfidences(LOCKED_PREVIEW.domains);
|
|
const withClass = all.filter((d) => d.imputationClass != null);
|
|
const withStaleness = all.filter((d) => d.staleness != null);
|
|
assert.ok(
|
|
withClass.length >= 1,
|
|
`locked preview should exercise at least one imputation class, got ${withClass.length}`,
|
|
);
|
|
assert.ok(
|
|
withStaleness.length >= 1,
|
|
`locked preview should exercise at least one staleness level, got ${withStaleness.length}`,
|
|
);
|
|
// Non-fresh staleness should appear at least once so the preview
|
|
// visibly shows off the aging/stale color variants.
|
|
const nonFresh = withStaleness.filter((d) => d.staleness !== 'fresh');
|
|
assert.ok(
|
|
nonFresh.length >= 1,
|
|
`locked preview should exercise at least one non-fresh staleness level, got ${nonFresh.length}`,
|
|
);
|
|
});
|