mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* feat(economic): add National Debt Clock panel with IMF + Treasury data - Proto: GetNationalDebt RPC in EconomicService with NationalDebtEntry message - Seed: seed-national-debt.mjs fetches IMF WEO (debt%, GDP, deficit%) + US Treasury FiscalData in parallel; filters aggregates/territories; sorts by total debt; 35-day TTL for monthly Railway cron - Handler: get-national-debt.ts reads seeded Redis cache key economic:national-debt:v1 - Registry: nationalDebt key added to cache-keys.ts, bootstrap.js (SLOW tier), health.js (maxStaleMin=10080), gateway.ts (daily cache tier) - Service: getNationalDebtData() in economic/index.ts with bootstrap hydration + RPC fallback - Panel: NationalDebtPanel.ts with sort tabs (Total/Debt-GDP/1Y Growth), search, live ticking via direct DOM manipulation (avoids setContent debounce) - Tests: 10 seed formula tests + 8 ticker math tests; all 2064 suite tests green * fix(economic): address code review findings for national debt clock * fix(economic): guard runSeed() call to prevent process.exit in test imports seed-national-debt.mjs called runSeed() at module top-level. When imported by tests (to access computeEntries), the seed ran, hit missing Redis creds in CI, and called process.exit(1), failing the entire test suite. Guard with isMain check so runSeed() only fires on direct execution.
79 lines
3.1 KiB
TypeScript
79 lines
3.1 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { describe, it } from 'node:test';
|
|
|
|
const BASELINE_TS = Date.UTC(2024, 0, 1);
|
|
const SECONDS_PER_YEAR = 365.25 * 86400;
|
|
|
|
function getCurrentDebt(entry: { debtUsd: number; perSecondRate: number; baselineTs: number }, nowMs: number): number {
|
|
const secondsElapsed = (nowMs - entry.baselineTs) / 1000;
|
|
return entry.debtUsd + entry.perSecondRate * secondsElapsed;
|
|
}
|
|
|
|
function formatDebt(usd: number): string {
|
|
if (!Number.isFinite(usd) || usd <= 0) return '$0';
|
|
if (usd >= 1e12) return `$${(usd / 1e12).toFixed(1)}T`;
|
|
if (usd >= 1e9) return `$${(usd / 1e9).toFixed(1)}B`;
|
|
if (usd >= 1e6) return `$${(usd / 1e6).toFixed(1)}M`;
|
|
return `$${Math.round(usd).toLocaleString()}`;
|
|
}
|
|
|
|
describe('getCurrentDebt ticking math', () => {
|
|
it('returns base debt at baseline_ts', () => {
|
|
const entry = { debtUsd: 33_600_000_000_000, perSecondRate: 10000, baselineTs: BASELINE_TS };
|
|
const result = getCurrentDebt(entry, BASELINE_TS);
|
|
assert.ok(Math.abs(result - 33_600_000_000_000) < 1, `Expected base debt, got ${result}`);
|
|
});
|
|
|
|
it('accrues correctly after 1 hour', () => {
|
|
const perSecondRate = 50_000;
|
|
const entry = { debtUsd: 33_600_000_000_000, perSecondRate, baselineTs: BASELINE_TS };
|
|
const oneHourLater = BASELINE_TS + 3600 * 1000;
|
|
const result = getCurrentDebt(entry, oneHourLater);
|
|
const expected = 33_600_000_000_000 + perSecondRate * 3600;
|
|
assert.ok(Math.abs(result - expected) < 1, `Expected ${expected}, got ${result}`);
|
|
});
|
|
|
|
it('accrues correctly after 1 year', () => {
|
|
const deficitPct = 5;
|
|
const gdpUsd = 28_000_000_000_000;
|
|
const perSecondRate = (deficitPct / 100) * gdpUsd / SECONDS_PER_YEAR;
|
|
const entry = { debtUsd: 33_600_000_000_000, perSecondRate, baselineTs: BASELINE_TS };
|
|
const oneYearLater = BASELINE_TS + Math.round(SECONDS_PER_YEAR * 1000);
|
|
const result = getCurrentDebt(entry, oneYearLater);
|
|
const expectedAccrual = (deficitPct / 100) * gdpUsd;
|
|
const accrued = result - entry.debtUsd;
|
|
assert.ok(Math.abs(accrued - expectedAccrual) < 1000, `Accrued ${accrued}, expected ~${expectedAccrual}`);
|
|
});
|
|
|
|
it('zero perSecondRate keeps debt flat', () => {
|
|
const entry = { debtUsd: 1_000_000_000_000, perSecondRate: 0, baselineTs: BASELINE_TS };
|
|
const later = BASELINE_TS + 86400_000;
|
|
const result = getCurrentDebt(entry, later);
|
|
assert.ok(Math.abs(result - 1_000_000_000_000) < 1, 'Debt should be flat with zero rate');
|
|
});
|
|
});
|
|
|
|
describe('formatDebt', () => {
|
|
it('formats trillions', () => {
|
|
assert.equal(formatDebt(33_600_000_000_000), '$33.6T');
|
|
assert.equal(formatDebt(1_000_000_000_000), '$1.0T');
|
|
assert.equal(formatDebt(100_000_000_000_000), '$100.0T');
|
|
});
|
|
|
|
it('formats billions', () => {
|
|
assert.equal(formatDebt(913_200_000_000), '$913.2B');
|
|
assert.equal(formatDebt(1_000_000_000), '$1.0B');
|
|
});
|
|
|
|
it('formats millions', () => {
|
|
assert.equal(formatDebt(12_300_000), '$12.3M');
|
|
assert.equal(formatDebt(1_000_000), '$1.0M');
|
|
});
|
|
|
|
it('handles zero and non-finite', () => {
|
|
assert.equal(formatDebt(0), '$0');
|
|
assert.equal(formatDebt(NaN), '$0');
|
|
assert.equal(formatDebt(-1), '$0');
|
|
});
|
|
});
|