Files
worldmonitor/tests/edge-functions.test.mjs
Elie Habib 2f19d96357 feat(brief): route whyMatters through internal analyst-context endpoint (#3248)
* feat(brief): route whyMatters through internal analyst-context endpoint

The brief's "why this is important" callout currently calls Gemini on
only {headline, source, threatLevel, category, country} with no live
state. The LLM can't know whether a ceasefire is on day 2 or day 50,
that IMF flagged >90% gas dependency in UAE/Qatar/Bahrain, or what
today's forecasts look like. Output is generic prose instead of the
situational analysis WMAnalyst produces when given live context.

This PR adds an internal Vercel edge endpoint that reuses a trimmed
variant of the analyst context (country-brief, risk scores, top-3
forecasts, macro signals, market data — no GDELT, no digest-search)
and ships it through a one-sentence LLM call with the existing
WHY_MATTERS_SYSTEM prompt. The endpoint owns its own Upstash cache
(v3 prefix, 6h TTL), supports a shadow mode that runs both paths in
parallel for offline diffing, and is auth'd via RELAY_SHARED_SECRET.

Three-layer graceful degradation (endpoint → legacy Gemini-direct →
stub) keeps the brief shipping on any failure.

Env knobs:
- BRIEF_WHY_MATTERS_PRIMARY=analyst|gemini (default: analyst; typo → gemini)
- BRIEF_WHY_MATTERS_SHADOW=0|1 (default: 1; only '0' disables)
- BRIEF_WHY_MATTERS_SHADOW_SAMPLE_PCT=0..100 (default: 100)
- BRIEF_WHY_MATTERS_ENDPOINT_URL (Railway, optional override)

Cache keys:
- brief:llm:whymatters:v3:{hash16} — envelope {whyMatters, producedBy,
  at}, 6h TTL. Endpoint-owned.
- brief:llm:whymatters:shadow:v1:{hash16} — {analyst, gemini, chosen,
  at}, 7d TTL. Fire-and-forget.
- brief:llm:whymatters:v2:{hash16} — legacy. Cron's fallback path
  still reads/writes during the rollout window; expires in ≤24h.

Tests: 6022 pass (existing 5915 + 12 core + 36 endpoint + misc).
typecheck + typecheck:api + biome on changed files clean.

Plan (Codex-approved after 4 rounds):
docs/plans/2026-04-21-001-feat-brief-why-matters-analyst-endpoint-plan.md

* fix(brief): address /ce:review round 1 findings on PR #3248

Fixes 5 findings from multi-agent review, 2 of them P1:

- #241 P1: `.gitignore !api/internal/**` was too broad — it re-included
  `.env`, `.env.local`, and any future secret file dropped into that
  directory. Narrowed to explicit source extensions (`*.ts`, `*.js`,
  `*.mjs`) so parent `.env` / secrets rules stay in effect inside
  api/internal/.

- #242 P1: `Dockerfile.digest-notifications` did not COPY
  `shared/brief-llm-core.js` + `.d.ts`. Cron would have crashed at
  container start with ERR_MODULE_NOT_FOUND. Added alongside
  brief-envelope + brief-filter COPY lines.

- #243 P2: Cron dropped the endpoint's source/producedBy ground-truth
  signal, violating PR #3247's own round-3 memory
  (feedback_gate_on_ground_truth_not_configured_state.md). Added
  structured log at the call site: `[brief-llm] whyMatters source=<src>
  producedBy=<pb> hash=<h>`. Endpoint response now includes `hash` so
  log + shadow-record pairs can be cross-referenced.

- #244 P2: Defense-in-depth prompt-injection hardening. Story fields
  flowed verbatim into both LLM prompts, bypassing the repo's
  sanitizeForPrompt convention. Added sanitizeStoryFields helper and
  applied in both analyst and gemini paths.

- #245 P2: Removed redundant `validate` option from callLlmReasoning.
  With only openrouter configured in prod, a parse-reject walked the
  provider chain, then fell through to the other path (same provider),
  then the cron's own fallback (same model) — 3x billing on one reject.
  Post-call parseWhyMatters check already handles rejection cleanly.

Deferred to P3 follow-ups (todos 246-248): singleflight, v2 sunset,
misc polish (country-normalize LOC, JSDoc pruning, shadow waitUntil,
auto-sync mirror, context-assembly caching).

Tests: 6022 pass. typecheck + typecheck:api clean.

* fix(brief-why-matters): ctx.waitUntil for shadow write + sanitize legacy fallback

Two P2 findings on PR #3248:

1. Shadow record was fire-and-forget without ctx.waitUntil on an Edge
   function. Vercel can terminate the isolate after response return,
   so the background redisPipeline write completes unreliably — i.e.
   the rollout-validation signal the shadow keys were supposed to
   provide was flaky in production.

   Fix: accept an optional EdgeContext 2nd arg. Build the shadow
   promise up front (so it starts executing immediately) then register
   it with ctx.waitUntil when present. Falls back to plain unawaited
   execution when ctx is absent (local harness / tests).

2. scripts/lib/brief-llm.mjs legacy fallback path called
   buildWhyMattersPrompt(story) on raw fields with no sanitization.
   The analyst endpoint sanitizes before its own prompt build, but
   the fallback is exactly what runs when the endpoint misses /
   errors — so hostile headlines / sources reached the LLM verbatim
   on that path.

   Fix: local sanitizeStoryForPrompt wrapper imports sanitizeForPrompt
   from server/_shared/llm-sanitize.js (existing pattern — see
   scripts/seed-digest-notifications.mjs:41). Wraps story fields
   before buildWhyMattersPrompt. Cache key unchanged (hash is over raw
   story), so cache parity with the analyst endpoint's v3 entries is
   preserved.

Regression guard: new test asserts the fallback prompt strips
"ignore previous instructions", "### Assistant:" line prefixes, and
`<|im_start|>` tokens when injection-crafted fields arrive.

Typecheck + typecheck:api clean. 6023 / 6023 data tests pass.

* fix(digest-cron): COPY server/_shared/llm-sanitize into digest-notifications image

Reviewer P1 on PR #3248: my previous commit (4eee22083) added
`import sanitizeForPrompt from server/_shared/llm-sanitize.js` to
scripts/lib/brief-llm.mjs, but Dockerfile.digest-notifications cherry-
picks server/_shared/* files and doesn't copy llm-sanitize. Import is
top-level/static — the container would crash at module load with
ERR_MODULE_NOT_FOUND the moment seed-digest-notifications.mjs pulls in
scripts/lib/brief-llm.mjs. Not just on fallback — every startup.

Fix: add `COPY server/_shared/llm-sanitize.js server/_shared/llm-sanitize.d.ts`
next to the existing brief-render COPY line. Module is pure string
manipulation with zero transitive imports — nothing else needs to land.

Cites feedback_validation_docker_ship_full_scripts_dir.md in the comment
next to the COPY; the cherry-pick convention keeps biting when new
cross-dir imports land in scripts/lib/ or scripts/shared/.

Can't regression-test at build time from this branch without a
docker-build CI job, but the symptom is deterministic — local runs
remain green (they resolve against the live filesystem); only the
container crashes. Post-merge, Railway redeploy of seed-digest-
notifications should show a clean `Starting Container` log line
instead of the MODULE_NOT_FOUND crash my prior commit would have caused.
2026-04-21 14:03:27 +04:00

465 lines
18 KiB
JavaScript

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, readdirSync, existsSync, 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, '..');
const apiDir = join(root, 'api');
const apiOauthDir = join(root, 'api', 'oauth');
const sharedDir = join(root, 'shared');
const scriptsSharedDir = join(root, 'scripts', 'shared');
// All .js files in api/ except underscore-prefixed helpers (_cors.js, _api-key.js)
const edgeFunctions = readdirSync(apiDir)
.filter((f) => f.endsWith('.js') && !f.startsWith('_'))
.map((f) => ({ name: f, path: join(apiDir, f) }));
// Also include api/oauth/ subdir edge functions
const oauthEdgeFunctions = readdirSync(apiOauthDir)
.filter((f) => f.endsWith('.js') && !f.startsWith('_'))
.map((f) => ({ name: `oauth/${f}`, path: join(apiOauthDir, f) }));
const allEdgeFunctions = [...edgeFunctions, ...oauthEdgeFunctions];
// ALL .js AND .ts files under api/ (recursively) — used for node: built-in checks.
// Note: .ts edge functions are intentionally excluded from the
// module-isolation describe below because Vercel bundles them at build time, so
// imports from '../server/' are valid. The node: built-in check still applies
// regardless of depth, since Vercel Edge Runtime rejects node: imports at runtime.
function walkApi(dir, relPrefix = '') {
const out = [];
for (const entry of readdirSync(dir)) {
if (entry.startsWith('_')) continue; // underscore helpers are not routed
const full = join(dir, entry);
const rel = relPrefix ? `${relPrefix}/${entry}` : entry;
if (statSync(full).isDirectory()) {
out.push(...walkApi(full, rel));
} else if (entry.endsWith('.js') || entry.endsWith('.ts')) {
out.push({ name: rel, path: full });
}
}
return out;
}
const allApiFiles = walkApi(apiDir);
describe('scripts/shared/ stays in sync with shared/', () => {
// Historical scope: .json (data) + .cjs (helpers).
// Explicit additions (must be mirrored): edge-safe modules the cron consumes
// (e.g. brief-llm-core.js + its .d.ts). Other .js files in shared/ are
// client-only and intentionally NOT mirrored — grow this list only when a
// new file is imported from `scripts/`.
const explicitMirroredFiles = new Set([
'brief-llm-core.js',
'brief-llm-core.d.ts',
]);
const sharedFiles = readdirSync(sharedDir).filter(
(f) => f.endsWith('.json') || f.endsWith('.cjs') || explicitMirroredFiles.has(f),
);
for (const file of sharedFiles) {
it(`scripts/shared/${file} matches shared/${file}`, () => {
const srcPath = join(scriptsSharedDir, file);
assert.ok(existsSync(srcPath), `scripts/shared/${file} is missing — run: cp shared/${file} scripts/shared/`);
const original = readFileSync(join(sharedDir, file), 'utf8');
const copy = readFileSync(srcPath, 'utf8');
assert.strictEqual(copy, original, `scripts/shared/${file} is out of sync with shared/${file} — run: cp shared/${file} scripts/shared/`);
});
}
});
describe('Edge Function shared helpers resolve', () => {
it('_rss-allowed-domains.js re-exports shared domain list', async () => {
const mod = await import(join(apiDir, '_rss-allowed-domains.js'));
const domains = mod.default;
assert.ok(Array.isArray(domains), 'Expected default export to be an array');
assert.ok(domains.length > 200, `Expected 200+ domains, got ${domains.length}`);
assert.ok(domains.includes('feeds.bbci.co.uk'), 'Expected BBC feed domain in list');
});
});
describe('Edge Function no node: built-ins', () => {
for (const { name, path } of allApiFiles) {
it(`${name} does not import node: built-ins (unsupported in Vercel Edge Runtime)`, () => {
const src = readFileSync(path, 'utf-8');
const match = src.match(/from\s+['"]node:(\w+)['"]/);
assert.ok(
!match,
`${name}: imports node:${match?.[1]} — Vercel Edge Runtime does not support node: built-in modules. Use an edge-compatible alternative.`,
);
});
}
});
describe('Legacy api/*.js endpoint allowlist', () => {
const ALLOWED_LEGACY_ENDPOINTS = new Set([
'ais-snapshot.js',
'bootstrap.js',
'cache-purge.js',
'contact.js',
'download.js',
'fwdstart.js',
'geo.js',
'gpsjam.js',
'health.js',
'military-flights.js',
'og-story.js',
'opensky.js',
'oref-alerts.js',
'polymarket.js',
'product-catalog.js',
'register-interest.js',
'reverse-geocode.js',
'mcp-proxy.js',
'rss-proxy.js',
'satellites.js',
'seed-health.js',
'story.js',
'telegram-feed.js',
'sanctions-entity-search.js',
'version.js',
]);
const currentEndpoints = readdirSync(apiDir).filter(
(f) => f.endsWith('.js') && !f.startsWith('_'),
);
for (const file of currentEndpoints) {
it(`${file} is in the legacy endpoint allowlist`, () => {
assert.ok(
ALLOWED_LEGACY_ENDPOINTS.has(file),
`${file} is a new api/*.js endpoint not in the allowlist. ` +
'New data endpoints must use the sebuf protobuf RPC pattern ' +
'(proto definition → buf generate → handler in server/worldmonitor/{domain}/v1/ → wired in handler.ts). ' +
'If this is a non-data ops endpoint, add it to ALLOWED_LEGACY_ENDPOINTS in tests/edge-functions.test.mjs.',
);
});
}
it('allowlist has no stale entries (all listed files exist)', () => {
for (const file of ALLOWED_LEGACY_ENDPOINTS) {
assert.ok(
existsSync(join(apiDir, file)),
`${file} is in ALLOWED_LEGACY_ENDPOINTS but does not exist in api/ — remove it from the allowlist.`,
);
}
});
});
describe('reverse-geocode Redis write', () => {
const geocodePath = join(apiDir, 'reverse-geocode.js');
it('uses ctx.waitUntil for Redis write (non-blocking, survives isolate teardown)', () => {
const src = readFileSync(geocodePath, 'utf-8');
assert.ok(
src.includes('ctx.waitUntil('),
'reverse-geocode.js: Redis cache write must use ctx.waitUntil() so the response is not blocked by the write',
);
assert.ok(
!src.includes('await fetch(redisUrl'),
'reverse-geocode.js: Redis write must not be awaited before returning the response',
);
});
it('bounds the Redis write with AbortSignal.timeout', () => {
const src = readFileSync(geocodePath, 'utf-8');
assert.ok(
src.includes('AbortSignal.timeout'),
'reverse-geocode.js: Redis write must have AbortSignal.timeout to bound slow writes',
);
});
});
describe('oauth/authorize.js consent page safety', () => {
const authorizePath = join(apiOauthDir, 'authorize.js');
it('uses _js POST body field (not X-Requested-With header) for XHR detection — avoids CORS preflight', () => {
const src = readFileSync(authorizePath, 'utf-8');
assert.ok(
!src.includes("'X-Requested-With'"),
'authorize.js: must not send X-Requested-With header in fetch — it triggers CORS preflight which fails in WebView. Use _js POST body field instead.',
);
assert.ok(
src.includes("params.get('_js') === '1'"),
"authorize.js: must detect JS path via params.get('_js') === '1' from POST body.",
);
});
it('allows null origin for WebView compatibility', () => {
const src = readFileSync(authorizePath, 'utf-8');
assert.ok(
src.includes("origin !== 'null'"),
"authorize.js: origin check must allow the string 'null' (WebView opaque origin). Without this, Connectors UI gets 403.",
);
});
it('consent form includes _js hidden field set by inline script before FormData', () => {
const src = readFileSync(authorizePath, 'utf-8');
assert.ok(
src.includes('name="_js"'),
'authorize.js: consent form must include <input name="_js"> for JS-path detection.',
);
assert.ok(
src.includes("jf.value='1'"),
"authorize.js: inline script must set jf.value='1' before building FormData.",
);
});
it('OPTIONS response includes Access-Control-Allow-Headers', () => {
const src = readFileSync(authorizePath, 'utf-8');
assert.ok(
src.includes('Access-Control-Allow-Headers'),
'authorize.js: OPTIONS response must include Access-Control-Allow-Headers.',
);
});
});
describe('api/slack/oauth/start.ts safety', () => {
const startPath = join(root, 'api', 'slack', 'oauth', 'start.ts');
it('uses crypto.getRandomValues for CSRF state (not Math.random)', () => {
const src = readFileSync(startPath, 'utf-8');
assert.ok(
src.includes('crypto.getRandomValues'),
'start.ts: CSRF state must use crypto.getRandomValues — Math.random is predictable and exploitable',
);
assert.ok(
!src.includes('Math.random'),
'start.ts: must not use Math.random for state generation',
);
});
it('stores state in Upstash with EX TTL via pipeline (atomic)', () => {
const src = readFileSync(startPath, 'utf-8');
assert.ok(
src.includes("'EX'") || src.includes('"EX"'),
"start.ts: Upstash state entry must include 'EX' TTL to auto-expire unused tokens",
);
assert.ok(
src.includes('/pipeline'),
'start.ts: must use Upstash pipeline endpoint for atomic state storage',
);
});
it('uses AbortSignal.timeout on Upstash pipeline fetch', () => {
const src = readFileSync(startPath, 'utf-8');
assert.ok(
src.includes('AbortSignal.timeout'),
'start.ts: Upstash pipeline fetch must have AbortSignal.timeout to prevent hanging edge isolates',
);
});
it('validates bearer token before generating state', () => {
const src = readFileSync(startPath, 'utf-8');
// validateBearerToken must appear before getRandomValues
const validateIdx = src.indexOf('validateBearerToken');
const randomIdx = src.indexOf('getRandomValues');
assert.ok(validateIdx !== -1, 'start.ts: must call validateBearerToken');
assert.ok(randomIdx !== -1, 'start.ts: must call getRandomValues');
assert.ok(
validateIdx < randomIdx,
'start.ts: validateBearerToken must come before getRandomValues — generate state only for authenticated users',
);
});
});
describe('api/slack/oauth/callback.ts safety', () => {
const callbackPath = join(root, 'api', 'slack', 'oauth', 'callback.ts');
it("uses '*' as postMessage targetOrigin (works on all WM subdomains and previews)", () => {
const src = readFileSync(callbackPath, 'utf-8');
assert.ok(
src.includes("APP_ORIGIN = '*'"),
"callback.ts: postMessage targetOrigin must be '*' so it works on tech/finance/happy subdomains and " +
'preview deployments — a hardcoded origin would silently drop messages on all other origins. ' +
"Security comes from the e.origin check in the listener, not from targetOrigin.",
);
});
it('HTML-escapes the error param before embedding in response body (no XSS)', () => {
const src = readFileSync(callbackPath, 'utf-8');
assert.ok(
src.includes('escapeHtml(error)'),
'callback.ts: error param from Slack redirect must be HTML-escaped before embedding in response body — raw interpolation is a reflected XSS vector',
);
});
it('consumes CSRF state from Upstash after validation (prevents replay)', () => {
const src = readFileSync(callbackPath, 'utf-8');
const getIdx = src.indexOf('upstashGet');
const delIdx = src.indexOf('upstashDel');
assert.ok(getIdx !== -1, 'callback.ts: must call upstashGet to validate state');
assert.ok(delIdx !== -1, 'callback.ts: must call upstashDel to consume state after validation');
assert.ok(
getIdx < delIdx,
'callback.ts: must validate state (upstashGet) before consuming it (upstashDel)',
);
});
it('uses AbortSignal.timeout on all Upstash fetches', () => {
const src = readFileSync(callbackPath, 'utf-8');
// Both upstashGet and upstashDel must have timeouts — count occurrences
const timeoutCount = (src.match(/AbortSignal\.timeout/g) ?? []).length;
assert.ok(
timeoutCount >= 2,
`callback.ts: all Upstash fetches must have AbortSignal.timeout — found ${timeoutCount}, expected at least 2 (upstashGet + upstashDel)`,
);
});
it('does not redirect main window to Slack (dead-end fallback removed)', () => {
const src = readFileSync(callbackPath, 'utf-8');
assert.ok(
!src.includes('window.location.href'),
'callback.ts: must not redirect main window to Slack — without window.opener the user lands on a dead-end page. Show an allow-popups error instead.',
);
});
});
describe('vercel.json CSP: Slack OAuth callback has unsafe-inline override', () => {
const vercelJson = JSON.parse(readFileSync(join(root, 'vercel.json'), 'utf-8'));
it('vercel.json has a CSP override for /api/slack/oauth/callback allowing unsafe-inline scripts', () => {
const rule = vercelJson.headers?.find((r) => r.source === '/api/slack/oauth/callback');
assert.ok(rule, 'vercel.json: missing header rule for /api/slack/oauth/callback — the callback page serves inline JS (postMessage + window.close) which is blocked by the global CSP');
const csp = rule.headers?.find((h) => h.key === 'Content-Security-Policy');
assert.ok(csp, 'vercel.json: /api/slack/oauth/callback rule must include a Content-Security-Policy header');
assert.ok(
csp.value.includes("'unsafe-inline'"),
"vercel.json: /api/slack/oauth/callback CSP must include 'unsafe-inline' in script-src — the callback page uses an inline <script> to call postMessage and window.close()",
);
});
it('/api/slack/oauth/callback CSP override appears after the global CSP rule (must override it)', () => {
const headers = vercelJson.headers ?? [];
const globalIdx = headers.findIndex((r) => r.source === '/((?!docs).*)');
const callbackIdx = headers.findIndex((r) => r.source === '/api/slack/oauth/callback');
assert.ok(globalIdx !== -1, 'vercel.json: global CSP rule not found');
assert.ok(callbackIdx !== -1, 'vercel.json: callback CSP override not found');
assert.ok(
callbackIdx > globalIdx,
'vercel.json: /api/slack/oauth/callback CSP override must appear AFTER the global rule — Vercel applies rules in order and the last match wins',
);
});
});
describe('Edge Function module isolation', () => {
for (const { name, path } of allEdgeFunctions) {
it(`${name} does not import from ../server/ (Edge Functions cannot resolve cross-directory TS)`, () => {
const src = readFileSync(path, 'utf-8');
assert.ok(
!src.includes("from '../server/"),
`${name}: imports from ../server/ — Vercel Edge Functions cannot resolve cross-directory TS imports. Inline the code or move to a same-directory .js helper.`,
);
});
it(`${name} does not import from ../src/ (Edge Functions cannot resolve TS aliases)`, () => {
const src = readFileSync(path, 'utf-8');
assert.ok(
!src.includes("from '../src/"),
`${name}: imports from ../src/ — Vercel Edge Functions cannot resolve @/ aliases or cross-directory TS. Inline the code instead.`,
);
});
}
});
describe('Scenario run endpoint (api/scenario/v1/run.ts)', () => {
const runPath = join(root, 'api', 'scenario', 'v1', 'run.ts');
it('exports edge config with runtime: edge', () => {
const src = readFileSync(runPath, 'utf-8');
assert.ok(
src.includes("runtime: 'edge'") || src.includes('runtime: "edge"'),
'run.ts: must export config with runtime: edge',
);
});
it('has a default export handler function', () => {
const src = readFileSync(runPath, 'utf-8');
assert.ok(
src.includes('export default') && src.includes('function handler'),
'run.ts: must have a default export handler function',
);
});
it('returns 405 for non-POST requests', () => {
const src = readFileSync(runPath, 'utf-8');
assert.ok(
src.includes('405'),
'run.ts: must return 405 for non-POST requests',
);
assert.ok(
src.includes("!== 'POST'") || src.includes('!== "POST"'),
'run.ts: must check for POST method and reject other methods with 405',
);
});
it('validates scenarioId is required', () => {
const src = readFileSync(runPath, 'utf-8');
assert.ok(
src.includes('scenarioId'),
'run.ts: must validate scenarioId field',
);
assert.ok(
src.includes('400'),
'run.ts: must return 400 for invalid/missing scenarioId',
);
});
it('uses per-user rate limiting', () => {
const src = readFileSync(runPath, 'utf-8');
assert.ok(
src.includes('rate') && src.includes('429'),
'run.ts: must implement rate limiting with 429 response',
);
});
it('uses AbortSignal.timeout on Redis fetch', () => {
const src = readFileSync(runPath, 'utf-8');
assert.ok(
src.includes('AbortSignal.timeout'),
'run.ts: all Redis fetches must have AbortSignal.timeout to prevent hanging edge isolates',
);
});
});
describe('Scenario status endpoint (api/scenario/v1/status.ts)', () => {
const statusPath = join(root, 'api', 'scenario', 'v1', 'status.ts');
it('exports edge config with runtime: edge', () => {
const src = readFileSync(statusPath, 'utf-8');
assert.ok(
src.includes("runtime: 'edge'") || src.includes('runtime: "edge"'),
'status.ts: must export config with runtime: edge',
);
});
it('returns 400 for missing or invalid jobId', () => {
const src = readFileSync(statusPath, 'utf-8');
assert.ok(
src.includes('400'),
'status.ts: must return 400 for missing or invalid jobId',
);
assert.ok(
src.includes('jobId'),
'status.ts: must validate jobId query parameter',
);
});
it('validates jobId format to guard against path traversal', () => {
const src = readFileSync(statusPath, 'utf-8');
assert.ok(
src.includes('JOB_ID_RE') || src.includes('/^scenario:'),
'status.ts: must validate jobId against a regex to prevent path traversal attacks (e.g. ../../etc/passwd)',
);
});
it('uses AbortSignal.timeout on Redis fetch', () => {
const src = readFileSync(statusPath, 'utf-8');
assert.ok(
src.includes('AbortSignal.timeout'),
'status.ts: Redis fetch must have AbortSignal.timeout to prevent hanging edge isolates',
);
});
});