Files
worldmonitor/tests/bootstrap.test.mjs
Elie Habib 98d231595e perf: bootstrap endpoint + polling optimization (#495)
* perf: bootstrap endpoint + polling optimization (phases 3-4)

Replace 15+ individual RPC calls on startup with a single /api/bootstrap
batch call that fetches pre-cached data from Redis. Consolidate 6 panel
setInterval timers into the central RefreshScheduler for hidden-tab
awareness (10x multiplier) and adaptive backoff (up to 4x for unchanged
data). Convert IntelligenceGapBadge from 10s polling to event-driven
updates with 60s safety fallback.

* fix(bootstrap): inline Redis + cache keys in edge function

Vercel Edge Functions cannot resolve cross-directory TypeScript imports
from server/_shared/. Inline getCachedJsonBatch and BOOTSTRAP_CACHE_KEYS
directly in api/bootstrap.js. Add sync test to ensure inlined keys stay
in sync with the canonical server/_shared/cache-keys.ts registry.

* test: add Edge Function module isolation guard for all api/*.js files

Prevents any Edge Function from importing from ../server/ or ../src/
which breaks Vercel builds. Scans all 12 non-helper Edge Functions.

* fix(bootstrap): read unprefixed cache keys on all environments

Preview deploys set VERCEL_ENV=preview which caused getKeyPrefix() to
prefix Redis keys with preview:<sha>:, but handlers only write to
unprefixed keys on production. Bootstrap is a read-only consumer of
production cache — always read unprefixed keys.

* fix(bootstrap): wire sectors hydration + add coverage guard

- Wire getHydratedData('sectors') in data-loader to skip Yahoo Finance
  fetch when bootstrap provides sector data
- Add test ensuring every bootstrap key has a getHydratedData consumer
  — prevents adding keys without wiring them

* fix(server): resolve 25 TypeScript errors + add server typecheck to CI

- _shared.ts: remove unused `delay` variable
- list-etf-flows.ts: add missing `rateLimited` field to 3 return literals
- list-market-quotes.ts: add missing `rateLimited` field to 4 return literals
- get-cable-health.ts: add non-null assertions for regex groups and array access
- list-positive-geo-events.ts: add non-null assertion for array index
- get-chokepoint-status.ts: add required fields to request objects
- CI: run `typecheck:api` (tsconfig.api.json) alongside `typecheck` to catch
  server/ TS errors before merge
2026-02-28 08:25:25 +04:00

242 lines
9.7 KiB
JavaScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, readdirSync, statSync } from 'node:fs';
import { dirname, resolve, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = resolve(__dirname, '..');
describe('Bootstrap cache key registry', () => {
const cacheKeysPath = join(root, 'server', '_shared', 'cache-keys.ts');
const cacheKeysSrc = readFileSync(cacheKeysPath, 'utf-8');
const bootstrapSrc = readFileSync(join(root, 'api', 'bootstrap.js'), 'utf-8');
it('exports BOOTSTRAP_CACHE_KEYS with at least 10 entries', () => {
const matches = cacheKeysSrc.match(/^\s+\w+:\s+'[^']+'/gm);
assert.ok(matches && matches.length >= 10, `Expected ≥10 keys, found ${matches?.length ?? 0}`);
});
it('api/bootstrap.js inlined keys match server/_shared/cache-keys.ts', () => {
const extractKeys = (src) => {
const re = /(\w+):\s+'([a-z_]+(?::[a-z_-]+)+:v\d+)'/g;
const keys = {};
let m;
while ((m = re.exec(src)) !== null) keys[m[1]] = m[2];
return keys;
};
const canonical = extractKeys(cacheKeysSrc);
const inlined = extractKeys(bootstrapSrc);
assert.ok(Object.keys(canonical).length >= 10, 'Canonical registry too small');
for (const [name, key] of Object.entries(canonical)) {
assert.equal(inlined[name], key, `Key '${name}' mismatch: canonical='${key}', inlined='${inlined[name]}'`);
}
for (const [name, key] of Object.entries(inlined)) {
assert.equal(canonical[name], key, `Extra inlined key '${name}' not in canonical registry`);
}
});
it('every cache key matches a handler cache key pattern', () => {
const keyRe = /:\s+'([^']+)'/g;
let m;
const keys = [];
while ((m = keyRe.exec(cacheKeysSrc)) !== null) {
keys.push(m[1]);
}
for (const key of keys) {
assert.match(key, /^[a-z_]+(?::[a-z_-]+)+:v\d+$/, `Cache key "${key}" does not match expected pattern`);
}
});
it('has no duplicate cache keys', () => {
const keyRe = /:\s+'([^']+)'/g;
let m;
const keys = [];
while ((m = keyRe.exec(cacheKeysSrc)) !== null) {
keys.push(m[1]);
}
const unique = new Set(keys);
assert.equal(unique.size, keys.length, `Found duplicate cache keys: ${keys.filter((k, i) => keys.indexOf(k) !== i)}`);
});
it('has no duplicate logical names', () => {
const nameRe = /^\s+(\w+):/gm;
let m;
const names = [];
while ((m = nameRe.exec(cacheKeysSrc)) !== null) {
names.push(m[1]);
}
const unique = new Set(names);
assert.equal(unique.size, names.length, `Found duplicate names: ${names.filter((n, i) => names.indexOf(n) !== i)}`);
});
it('every cache key maps to a handler file with a matching cache key string', () => {
const keyRe = /:\s+'([^']+)'/g;
let m;
const keys = [];
while ((m = keyRe.exec(cacheKeysSrc)) !== null) {
keys.push(m[1]);
}
const handlerDirs = join(root, 'server', 'worldmonitor');
const handlerFiles = [];
function walk(dir) {
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) walk(full);
else if (entry.endsWith('.ts') && !entry.includes('service_server') && !entry.includes('service_client')) {
handlerFiles.push(full);
}
}
}
walk(handlerDirs);
const allHandlerCode = handlerFiles.map(f => readFileSync(f, 'utf-8')).join('\n');
for (const key of keys) {
assert.ok(
allHandlerCode.includes(key),
`Cache key "${key}" not found in any handler file`,
);
}
});
});
describe('Bootstrap endpoint (api/bootstrap.js)', () => {
const bootstrapPath = join(root, 'api', 'bootstrap.js');
const src = readFileSync(bootstrapPath, 'utf-8');
it('exports edge runtime config', () => {
assert.ok(src.includes("runtime: 'edge'"), 'Missing edge runtime config');
});
it('defines BOOTSTRAP_CACHE_KEYS inline', () => {
assert.ok(src.includes('BOOTSTRAP_CACHE_KEYS'), 'Missing BOOTSTRAP_CACHE_KEYS definition');
});
it('defines getCachedJsonBatch inline (self-contained, no server imports)', () => {
assert.ok(src.includes('getCachedJsonBatch'), 'Missing getCachedJsonBatch function');
assert.ok(!src.includes("from '../server/"), 'Should not import from server/ — Edge Functions cannot resolve cross-directory TS imports');
});
it('supports optional ?keys= query param for subset filtering', () => {
assert.ok(src.includes("'keys'"), 'Missing keys query param handling');
});
it('returns JSON with data and missing keys', () => {
assert.ok(src.includes('data'), 'Missing data field in response');
assert.ok(src.includes('missing'), 'Missing missing field in response');
});
it('sets Cache-Control header with s-maxage', () => {
assert.ok(src.includes('s-maxage=60'), 'Missing s-maxage=60 Cache-Control');
assert.ok(src.includes('stale-while-revalidate'), 'Missing stale-while-revalidate');
});
it('validates API key for desktop origins', () => {
assert.ok(src.includes('validateApiKey'), 'Missing API key validation');
});
it('handles CORS preflight', () => {
assert.ok(src.includes("'OPTIONS'"), 'Missing OPTIONS method handling');
assert.ok(src.includes('getCorsHeaders'), 'Missing CORS headers');
});
});
describe('Frontend hydration (src/services/bootstrap.ts)', () => {
const bootstrapClientPath = join(root, 'src', 'services', 'bootstrap.ts');
const src = readFileSync(bootstrapClientPath, 'utf-8');
it('exports getHydratedData function', () => {
assert.ok(src.includes('export function getHydratedData'), 'Missing getHydratedData export');
});
it('exports fetchBootstrapData function', () => {
assert.ok(src.includes('export async function fetchBootstrapData'), 'Missing fetchBootstrapData export');
});
it('uses consume-once pattern (deletes after read)', () => {
assert.ok(src.includes('.delete('), 'Missing delete in getHydratedData — consume-once pattern not implemented');
});
it('has a fast timeout cap to avoid regressing startup', () => {
const timeoutMatch = src.match(/AbortSignal\.timeout\((\d+)\)/);
assert.ok(timeoutMatch, 'Missing AbortSignal.timeout');
const ms = parseInt(timeoutMatch[1], 10);
assert.ok(ms <= 2000, `Timeout ${ms}ms too high — should be ≤2000ms to avoid regressing startup`);
});
it('fetches from /api/bootstrap', () => {
assert.ok(src.includes('/api/bootstrap'), 'Missing /api/bootstrap fetch URL');
});
it('handles fetch failure silently', () => {
assert.ok(src.includes('catch'), 'Missing error handling — panels should fall through to individual calls');
});
});
describe('Panel hydration consumers', () => {
const panels = [
{ name: 'ETFFlowsPanel', path: 'src/components/ETFFlowsPanel.ts', key: 'etfFlows' },
{ name: 'MacroSignalsPanel', path: 'src/components/MacroSignalsPanel.ts', key: 'macroSignals' },
{ name: 'ServiceStatusPanel (via infrastructure)', path: 'src/services/infrastructure/index.ts', key: 'serviceStatuses' },
{ name: 'Sectors (via data-loader)', path: 'src/app/data-loader.ts', key: 'sectors' },
];
for (const panel of panels) {
it(`${panel.name} checks getHydratedData('${panel.key}')`, () => {
const src = readFileSync(join(root, panel.path), 'utf-8');
assert.ok(src.includes('getHydratedData'), `${panel.name} missing getHydratedData import/usage`);
assert.ok(src.includes(`'${panel.key}'`), `${panel.name} missing hydration key '${panel.key}'`);
});
}
});
describe('Bootstrap key hydration coverage', () => {
it('every bootstrap key has a getHydratedData consumer in src/', () => {
const bootstrapSrc = readFileSync(join(root, 'api', 'bootstrap.js'), 'utf-8');
const keyRe = /(\w+):\s+'[a-z_]+(?::[a-z_-]+)+:v\d+'/g;
const keys = [];
let m;
while ((m = keyRe.exec(bootstrapSrc)) !== null) keys.push(m[1]);
// Gather all src/ .ts files
const srcFiles = [];
function walk(dir) {
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) walk(full);
else if (entry.endsWith('.ts') && !full.includes('/generated/')) srcFiles.push(full);
}
}
walk(join(root, 'src'));
const allSrc = srcFiles.map(f => readFileSync(f, 'utf-8')).join('\n');
for (const key of keys) {
assert.ok(
allSrc.includes(`getHydratedData('${key}')`),
`Bootstrap key '${key}' has no getHydratedData('${key}') consumer in src/ — data is fetched but never used`,
);
}
});
});
describe('Adaptive backoff adopters', () => {
it('ServiceStatusPanel.fetchStatus returns Promise<boolean>', () => {
const src = readFileSync(join(root, 'src/components/ServiceStatusPanel.ts'), 'utf-8');
assert.ok(src.includes('fetchStatus(): Promise<boolean>'), 'fetchStatus should return Promise<boolean> for adaptive backoff');
assert.ok(src.includes('lastServicesJson'), 'Missing lastServicesJson for change detection');
});
it('MacroSignalsPanel.fetchData returns Promise<boolean>', () => {
const src = readFileSync(join(root, 'src/components/MacroSignalsPanel.ts'), 'utf-8');
assert.ok(src.includes('fetchData(): Promise<boolean>'), 'fetchData should return Promise<boolean> for adaptive backoff');
assert.ok(src.includes('lastTimestamp'), 'Missing lastTimestamp for change detection');
});
it('StrategicRiskPanel.refresh returns Promise<boolean>', () => {
const src = readFileSync(join(root, 'src/components/StrategicRiskPanel.ts'), 'utf-8');
assert.ok(src.includes('refresh(): Promise<boolean>'), 'refresh should return Promise<boolean> for adaptive backoff');
assert.ok(src.includes('lastRiskFingerprint'), 'Missing lastRiskFingerprint for change detection');
});
});