Files
worldmonitor/tests/deploy-config.test.mjs
Elie Habib 39cf56dd4d perf: reduce ~14M uncached API calls/day via client caches + workbox fix + USNI Railway migration (#1605)
* perf: reduce uncached API calls via client-side circuit breaker caches

Add client-side circuit breaker caches with IndexedDB persistence to the
top 3 uncached API endpoints (CF analytics: 10.5M uncached requests/day):

- classify-events (5.37M/day): 6hr cache per normalized title, shouldCache
  guards against caching null/transient failures
- get-population-exposure (3.45M/day): 6hr cache per coordinate key
  (toFixed(4) for ~11m precision), 64-entry LRU
- summarize-article (1.68M/day): 2hr cache per headline-set hash via
  buildSummaryCacheKey, eliminates both cache-check and summarize RPCs

Fix workbox-*.js getting no-cache headers (3.62M/day): exclude from SPA
catch-all regex in vercel.json, add explicit immutable cache rule for
content-hashed workbox files.

Migrate USNI fleet fetch from Vercel edge to Railway relay (gold standard):
- Add seedUSNIFleet() loop to ais-relay.cjs (6hr interval, gzip support)
- Make server handler Redis-read-only (435 lines reduced to 38)
- Move usniFleet from ON_DEMAND to BOOTSTRAP_KEYS in health.js
- Add persistCache + shouldCache to client breaker

Estimated reduction: ~14.3M uncached requests/day.

* fix: address code review findings (P1 + P2)

P1: Include SummarizeOptions in summary cache key to prevent cross-option
cache pollution (e.g. cloud summary replayed after user disables cloud LLMs).

P2: Document that forceRefresh is intentionally ignored now that USNI
fetching moved to Railway relay (Vercel is Redis-read-only).

* fix: reject forceRefresh explicitly instead of silently ignoring it

Return an error response with explanation when forceRefresh=true is sent,
rather than silently returning cached data. Makes the behavior regression
visible to any caller instead of masking it.

* fix(build): set worker.format to 'es' for Vite 6 compatibility

Vite 6 defaults worker.format to 'iife' which fails with code-splitting
workers (analysis.worker.ts uses dynamic imports). Setting 'es' fixes
the Vercel production build.

* fix(test): update deploy-config test for workbox regex exclusion

The SPA catch-all regex test hard-coded the old pattern without the
workbox exclusion. Update to match the new vercel.json source pattern.
2026-03-15 00:52:10 +04:00

160 lines
6.5 KiB
JavaScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const vercelConfig = JSON.parse(readFileSync(resolve(__dirname, '../vercel.json'), 'utf-8'));
const viteConfigSource = readFileSync(resolve(__dirname, '../vite.config.ts'), 'utf-8');
const getCacheHeaderValue = (sourcePath) => {
const rule = vercelConfig.headers.find((entry) => entry.source === sourcePath);
const header = rule?.headers?.find((item) => item.key.toLowerCase() === 'cache-control');
return header?.value ?? null;
};
describe('deploy/cache configuration guardrails', () => {
it('disables caching for HTML entry routes on Vercel', () => {
const spaNoCache = getCacheHeaderValue('/((?!api|assets|blog|docs|favico|map-styles|data|textures|pro|sw\\.js|workbox-[a-f0-9]+\\.js|manifest\\.webmanifest|offline\\.html|robots\\.txt|sitemap\\.xml|llms\\.txt|llms-full\\.txt|\\.well-known).*)');
assert.equal(spaNoCache, 'no-cache, no-store, must-revalidate');
});
it('keeps immutable caching for hashed static assets', () => {
assert.equal(
getCacheHeaderValue('/assets/(.*)'),
'public, max-age=31536000, immutable'
);
});
it('keeps PWA precache glob free of HTML files', () => {
assert.match(
viteConfigSource,
/globPatterns:\s*\['\*\*\/\*\.\{js,css,ico,png,svg,woff2\}'\]/
);
assert.doesNotMatch(viteConfigSource, /globPatterns:\s*\['\*\*\/\*\.\{js,css,html/);
});
it('explicitly disables navigateFallback when HTML is not precached', () => {
assert.match(viteConfigSource, /navigateFallback:\s*null/);
assert.doesNotMatch(viteConfigSource, /navigateFallbackDenylist:\s*\[/);
});
it('uses network-only runtime caching for navigation requests', () => {
assert.match(viteConfigSource, /request\.mode === 'navigate'/);
assert.match(viteConfigSource, /handler:\s*'NetworkOnly'/);
});
it('contains variant-specific metadata fields used by html replacement and manifest', () => {
const variantMetaSource = readFileSync(resolve(__dirname, '../src/config/variant-meta.ts'), 'utf-8');
assert.match(variantMetaSource, /shortName:\s*'/);
assert.match(variantMetaSource, /subject:\s*'/);
assert.match(variantMetaSource, /classification:\s*'/);
assert.match(variantMetaSource, /categories:\s*\[/);
assert.match(
viteConfigSource,
/\.replace\(\/<meta name="subject" content="\.\*\?" \\\/>\/,\s*`<meta name="subject"/
);
assert.match(
viteConfigSource,
/\.replace\(\/<meta name="classification" content="\.\*\?" \\\/>\/,\s*`<meta name="classification"/
);
});
});
const getSecurityHeaders = () => {
const rule = vercelConfig.headers.find((entry) => entry.source === '/((?!docs).*)');
return rule?.headers ?? [];
};
const getHeaderValue = (key) => {
const headers = getSecurityHeaders();
const header = headers.find((h) => h.key.toLowerCase() === key.toLowerCase());
return header?.value ?? null;
};
describe('security header guardrails', () => {
it('includes all 5 required security headers on catch-all route', () => {
const required = [
'X-Content-Type-Options',
'Strict-Transport-Security',
'Referrer-Policy',
'Permissions-Policy',
'Content-Security-Policy',
];
const headerKeys = getSecurityHeaders().map((h) => h.key);
for (const name of required) {
assert.ok(headerKeys.includes(name), `Missing security header: ${name}`);
}
});
it('Permissions-Policy disables all expected browser APIs', () => {
const policy = getHeaderValue('Permissions-Policy');
const expectedDisabled = [
'camera=()',
'microphone=()',
'geolocation=(self)',
'accelerometer=()',
'bluetooth=()',
'display-capture=()',
'gyroscope=()',
'hid=()',
'idle-detection=()',
'magnetometer=()',
'midi=()',
'payment=()',
'screen-wake-lock=()',
'serial=()',
'usb=()',
'xr-spatial-tracking=()',
];
for (const directive of expectedDisabled) {
assert.ok(policy.includes(directive), `Permissions-Policy missing: ${directive}`);
}
});
it('Permissions-Policy delegates media APIs to allowed origins', () => {
const policy = getHeaderValue('Permissions-Policy');
// autoplay and encrypted-media delegate to self + YouTube
for (const api of ['autoplay', 'encrypted-media']) {
assert.match(
policy,
new RegExp(`${api}=\\(self "https://www\\.youtube\\.com" "https://www\\.youtube-nocookie\\.com"\\)`),
`Permissions-Policy should delegate ${api} to YouTube origins`
);
}
// picture-in-picture also includes Cloudflare challenges
assert.match(
policy,
/picture-in-picture=\(self "https:\/\/www\.youtube\.com" "https:\/\/www\.youtube-nocookie\.com" "https:\/\/challenges\.cloudflare\.com"\)/,
'Permissions-Policy should delegate picture-in-picture to YouTube + Cloudflare origins'
);
});
it('CSP connect-src does not allow unencrypted WebSocket (ws:)', () => {
const csp = getHeaderValue('Content-Security-Policy');
const connectSrc = csp.match(/connect-src\s+([^;]+)/)?.[1] ?? '';
assert.ok(!connectSrc.includes(' ws:'), 'CSP connect-src must not contain ws: (unencrypted WebSocket)');
assert.ok(connectSrc.includes('wss:'), 'CSP connect-src should keep wss: for secure WebSocket');
});
it('CSP connect-src does not contain localhost in production', () => {
const csp = getHeaderValue('Content-Security-Policy');
const connectSrc = csp.match(/connect-src\s+([^;]+)/)?.[1] ?? '';
assert.ok(!connectSrc.includes('http://localhost'), 'CSP connect-src must not contain http://localhost in production');
});
it('CSP script-src includes wasm-unsafe-eval for WebAssembly support', () => {
const csp = getHeaderValue('Content-Security-Policy');
const scriptSrc = csp.match(/script-src\s+([^;]+)/)?.[1] ?? '';
assert.ok(scriptSrc.includes("'wasm-unsafe-eval'"), 'CSP script-src must include wasm-unsafe-eval for WASM support');
assert.ok(scriptSrc.includes("'self'"), 'CSP script-src must include self');
});
it('security.txt exists in public/.well-known/', () => {
const secTxt = readFileSync(resolve(__dirname, '../public/.well-known/security.txt'), 'utf-8');
assert.match(secTxt, /^Contact:/m, 'security.txt must have a Contact field');
assert.match(secTxt, /^Expires:/m, 'security.txt must have an Expires field');
});
});