test(circuit-breakers): harden regression tests with try/finally and existence guards (#911)

- Wrap all 4 behavioral it() blocks in try/finally so clearAllCircuitBreakers()
  always runs on assertion failure (P2 — leaked breaker state between tests)
- Add assert.ok(fnStart !== -1) guards for fetchHapiSummary, fetchPositiveGdeltArticles,
  and fetchGdeltArticles so renames produce a clear diagnostic (P2 — silent false-positives)
- Fix misleading comment in seed-wb-indicators.mjs: WLD/EAS are 3-char codes and
  aren't filtered by iso3.length !== 3 (P3)
- Add timeout-minutes: 10 and permissions: contents: read to seed GHA workflow (P3)
This commit is contained in:
Elie Habib
2026-03-03 15:13:29 +04:00
committed by GitHub
parent d93db204e8
commit 6ec076c8d3
3 changed files with 92 additions and 78 deletions

View File

@@ -9,6 +9,9 @@ on:
jobs:
seed:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4

View File

@@ -185,7 +185,7 @@ async function fetchWbIndicator(indicatorId, dateRange) {
for (const entry of allEntries) {
if (entry.value === null || entry.value === undefined) continue;
const iso3 = entry.countryiso3code;
if (!iso3 || iso3.length !== 3) continue; // skip aggregates (WLD, EAS, etc.)
if (!iso3 || iso3.length !== 3) continue; // skip entries with missing or malformed country codes
const year = parseInt(entry.date, 10);
if (!latestByCountry[iso3] || year > latestByCountry[iso3].year) {

View File

@@ -37,6 +37,7 @@ describe('conflict/index.ts — per-country HAPI circuit breakers', () => {
// Scoped slices to avoid false positives from comments or unrelated code
const breakerSection = src.slice(src.indexOf('hapiBreakers'), src.indexOf('hapiBreakers') + 400);
const fnStart = src.indexOf('export async function fetchHapiSummary');
assert.ok(fnStart !== -1, 'fetchHapiSummary not found in conflict/index.ts — was it renamed?');
const fnBody = src.slice(fnStart, src.indexOf('\nexport ', fnStart + 1));
it('does NOT have a single shared hapiBreaker', () => {
@@ -89,8 +90,10 @@ describe('gdelt-intel.ts — dedicated circuit breakers per GDELT query type', (
// Scoped function body slices
const posStart = src.indexOf('export async function fetchPositiveGdeltArticles');
assert.ok(posStart !== -1, 'fetchPositiveGdeltArticles not found in gdelt-intel.ts — was it renamed?');
const posBody = src.slice(posStart, src.indexOf('\nexport ', posStart + 1));
const regStart = src.indexOf('export async function fetchGdeltArticles');
assert.ok(regStart !== -1, 'fetchGdeltArticles not found in gdelt-intel.ts — was it renamed?');
const regBody = src.slice(regStart, src.indexOf('\nexport ', regStart + 1));
it('has a dedicated positiveGdeltBreaker separate from gdeltBreaker', () => {
@@ -158,6 +161,7 @@ describe('CircuitBreaker isolation — HAPI per-country independence', () => {
clearAllCircuitBreakers();
try {
const breakerUS = createCircuitBreaker({ name: 'HDX HAPI:US', cacheTtlMs: 30 * 60 * 1000 });
const breakerRU = createCircuitBreaker({ name: 'HDX HAPI:RU', cacheTtlMs: 30 * 60 * 1000 });
@@ -176,8 +180,9 @@ describe('CircuitBreaker isolation — HAPI per-country independence', () => {
const goodData = { summary: { countryCode: 'RU', conflictEvents: 12, displacedPersons: 5000 } };
const result = await breakerRU.execute(async () => goodData, fallback);
assert.deepEqual(result, goodData, 'breakerRU should return live data unaffected by breakerUS cooldown');
} finally {
clearAllCircuitBreakers();
}
});
it('HAPI: different countries cache independently (no cross-country poisoning)', async () => {
@@ -187,6 +192,7 @@ describe('CircuitBreaker isolation — HAPI per-country independence', () => {
clearAllCircuitBreakers();
try {
const breakerUS = createCircuitBreaker({ name: 'HDX HAPI:US', cacheTtlMs: 30 * 60 * 1000 });
const breakerRU = createCircuitBreaker({ name: 'HDX HAPI:RU', cacheTtlMs: 30 * 60 * 1000 });
@@ -208,8 +214,9 @@ describe('CircuitBreaker isolation — HAPI per-country independence', () => {
'breakerRU cache must return RU data, not US data');
assert.notEqual(cachedUS.summary?.conflictEvents, cachedRU.summary?.conflictEvents,
'Cached conflict event counts must be independent per country');
} finally {
clearAllCircuitBreakers();
}
});
});
@@ -225,6 +232,7 @@ describe('CircuitBreaker isolation — GDELT split breaker independence', () =>
clearAllCircuitBreakers();
try {
const gdelt = createCircuitBreaker({ name: 'GDELT Intelligence', cacheTtlMs: 10 * 60 * 1000 });
const positive = createCircuitBreaker({ name: 'GDELT Positive', cacheTtlMs: 10 * 60 * 1000 });
@@ -243,8 +251,9 @@ describe('CircuitBreaker isolation — GDELT split breaker independence', () =>
const realArticles = { articles: [{ url: 'https://news.example/military', title: 'Conflict update' }], totalArticles: 1 };
const result = await gdelt.execute(async () => realArticles, fallback);
assert.deepEqual(result, realArticles, 'gdelt breaker should return live data unaffected by positive cooldown');
} finally {
clearAllCircuitBreakers();
}
});
it('GDELT: regular and positive breakers cache different data independently', async () => {
@@ -254,6 +263,7 @@ describe('CircuitBreaker isolation — GDELT split breaker independence', () =>
clearAllCircuitBreakers();
try {
const gdelt = createCircuitBreaker({ name: 'GDELT Intelligence', cacheTtlMs: 10 * 60 * 1000 });
const positive = createCircuitBreaker({ name: 'GDELT Positive', cacheTtlMs: 10 * 60 * 1000 });
@@ -282,7 +292,8 @@ describe('CircuitBreaker isolation — GDELT split breaker independence', () =>
cachedPositive.articles[0]?.url,
'Cached article URLs must be distinct per breaker (no cross-contamination)',
);
} finally {
clearAllCircuitBreakers();
}
});
});