GenerAI / worldmonitor /tests /analysis-geo-convergence.test.mjs
amogaddy's picture
Fix build: ripristinate blog-site/tests/e2e/pro-test/convex (referenziate dagli script di build) (part 3)
ec8acdf verified
Raw
History Blame Contribute Delete
14.4 kB
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { describe, it } from 'node:test';
import {
GEO_CONVERGENCE_THRESHOLD,
GEO_CONVERGENCE_WINDOW_MS,
GEO_EVENT_TYPE_LABELS,
GEO_NEARBY_MIN_TYPES,
GeoConvergenceEngine,
geoConvergenceToSignal,
getCellId,
getLocationName,
haversineKm,
scoreGeoCell,
} from '../shared/analysis-geo-convergence.ts';
const HOUR_MS = 60 * 60 * 1000;
/** Engine bound to a mutable clock so every window/prune assertion is deterministic. */
function makeEngine(startMs = 1_700_000_000_000, options = {}) {
const clock = { now: startMs };
const engine = new GeoConvergenceEngine({ now: () => clock.now, ...options });
return { engine, clock };
}
describe('geo convergence cells', () => {
it('buckets coordinates into whole-degree cells', () => {
assert.equal(getCellId(32.4, 44.9), '32,44');
assert.equal(getCellId(32.9, 44.1), '32,44');
assert.equal(getCellId(33.0, 45.0), '33,45');
// Math.floor, not truncation: negatives round away from zero.
assert.equal(getCellId(-0.5, -0.5), '-1,-1');
assert.equal(getCellId(-33.2, -70.7), '-34,-71');
});
it('places the cell centroid at the middle of its degree square', () => {
const { engine } = makeEngine();
engine.ingest(32.4, 44.9, 'protest');
const [cell] = engine.snapshot();
assert.equal(cell.id, '32,44');
assert.equal(cell.lat, 32.5);
assert.equal(cell.lon, 44.5);
});
it('accumulates repeat events per type and tracks the latest timestamp', () => {
const { engine, clock } = makeEngine();
engine.ingest(32.4, 44.9, 'protest');
clock.now += HOUR_MS;
engine.ingest(32.1, 44.1, 'protest');
const [cell] = engine.snapshot();
assert.equal(cell.events.length, 1);
assert.equal(cell.events[0].type, 'protest');
assert.equal(cell.events[0].count, 2);
assert.equal(cell.events[0].lastSeen, clock.now);
// firstSeen is pinned at cell creation, not refreshed by later events.
assert.equal(cell.firstSeen, clock.now - HOUR_MS);
});
it('reports cell count and clears without pruning', () => {
const { engine, clock } = makeEngine();
engine.ingest(32.4, 44.9, 'protest');
engine.ingest(10.0, 10.0, 'earthquake');
assert.equal(engine.cellCount(), 2);
// Well past the window: counting/snapshotting must NOT prune (debug helpers stay cheap).
clock.now += GEO_CONVERGENCE_WINDOW_MS * 2;
assert.equal(engine.cellCount(), 2);
assert.equal(engine.snapshot().length, 2);
engine.clear();
assert.equal(engine.cellCount(), 0);
});
it('ingests bulk events with a per-event timestamp, falling back to the clock', () => {
const { engine, clock } = makeEngine();
engine.ingestEvents(
[
{ lat: 32.4, lon: 44.9, time: clock.now - HOUR_MS },
{ lat: 32.6, lon: 44.2 },
],
'protest',
);
const [cell] = engine.snapshot();
assert.equal(cell.events[0].count, 2);
assert.equal(cell.events[0].lastSeen, clock.now);
assert.equal(cell.firstSeen, clock.now - HOUR_MS);
});
});
describe('geo convergence detection', () => {
it('stays silent below the 3-domain threshold and fires at it', () => {
const { engine } = makeEngine();
engine.ingest(32.4, 44.9, 'protest');
engine.ingest(32.5, 44.5, 'military_flight');
assert.deepEqual(engine.detect(new Set()), []);
engine.ingest(32.1, 44.1, 'military_vessel');
const alerts = engine.detect(new Set());
assert.equal(alerts.length, 1);
assert.equal(alerts[0].cellId, '32,44');
assert.equal(alerts[0].lat, 32.5);
assert.equal(alerts[0].lon, 44.5);
assert.deepEqual(alerts[0].types, ['protest', 'military_flight', 'military_vessel']);
assert.equal(alerts[0].totalEvents, 3);
});
it('scores by domain count plus a capped event-volume boost', () => {
// 3 domains x 25 = 75, plus min(25, 3 events x 2) = 6 -> 81.
assert.equal(scoreGeoCell(3, 3), 81);
assert.equal(scoreGeoCell(2, 2), 54);
// Volume boost saturates at 25 well below the overall ceiling: 50 + 25.
assert.equal(scoreGeoCell(2, 20), 75);
assert.equal(scoreGeoCell(2, 2000), 75);
// Total score saturates at 100.
assert.equal(scoreGeoCell(3, 40), 100);
assert.equal(scoreGeoCell(4, 4), 100);
const { engine } = makeEngine();
engine.ingest(32.4, 44.9, 'protest');
engine.ingest(32.5, 44.5, 'military_flight');
engine.ingest(32.1, 44.1, 'military_vessel');
assert.equal(engine.detect(new Set())[0].score, 81);
});
it('suppresses cells already present in seenAlerts and records new ones', () => {
const { engine } = makeEngine();
engine.ingest(32.4, 44.9, 'protest');
engine.ingest(32.5, 44.5, 'military_flight');
engine.ingest(32.1, 44.1, 'military_vessel');
const seen = new Set();
assert.equal(engine.detect(seen).length, 1);
assert.ok(seen.has('32,44'), 'detect must record the alert it emitted');
assert.deepEqual(engine.detect(seen), [], 'a seen cell must not re-alert');
const preSeeded = new Set(['32,44']);
assert.deepEqual(engine.detect(preSeeded), []);
});
it('sorts alerts by descending score', () => {
const { engine } = makeEngine();
// Weaker cell ingested first so insertion order cannot explain the result.
engine.ingest(10.4, 10.4, 'protest');
engine.ingest(10.4, 10.4, 'military_flight');
engine.ingest(10.4, 10.4, 'military_vessel');
engine.ingest(32.4, 44.9, 'protest');
engine.ingest(32.4, 44.9, 'military_flight');
engine.ingest(32.4, 44.9, 'military_vessel');
engine.ingest(32.4, 44.9, 'earthquake');
const alerts = engine.detect(new Set());
assert.deepEqual(
alerts.map((a) => [a.cellId, a.score]),
[
['32,44', 100],
['10,10', 81],
],
);
});
it('prunes event types older than the 24h window before detecting', () => {
const { engine, clock } = makeEngine();
engine.ingest(32.4, 44.9, 'protest');
clock.now += GEO_CONVERGENCE_WINDOW_MS + 1;
engine.ingest(32.5, 44.5, 'military_flight');
engine.ingest(32.1, 44.1, 'military_vessel');
// protest aged out -> only 2 live domains -> below threshold.
assert.deepEqual(engine.detect(new Set()), []);
const [cell] = engine.snapshot();
assert.deepEqual(
cell.events.map((e) => e.type),
['military_flight', 'military_vessel'],
);
});
it('drops the whole cell once every event type has aged out', () => {
const { engine, clock } = makeEngine();
engine.ingest(32.4, 44.9, 'protest');
engine.ingest(32.5, 44.5, 'military_flight');
engine.ingest(32.1, 44.1, 'military_vessel');
clock.now += GEO_CONVERGENCE_WINDOW_MS + 1;
assert.deepEqual(engine.detect(new Set()), []);
assert.equal(engine.cellCount(), 0);
});
it('keeps an event that is exactly at the window edge', () => {
const { engine, clock } = makeEngine();
engine.ingest(32.4, 44.9, 'protest');
engine.ingest(32.5, 44.5, 'military_flight');
engine.ingest(32.1, 44.1, 'military_vessel');
clock.now += GEO_CONVERGENCE_WINDOW_MS;
assert.equal(engine.detect(new Set()).length, 1, 'cutoff is exclusive: lastSeen === cutoff survives');
});
it('honours a custom window and threshold', () => {
const { engine, clock } = makeEngine(0, { windowMs: HOUR_MS, convergenceThreshold: 2 });
engine.ingest(32.4, 44.9, 'protest');
engine.ingest(32.5, 44.5, 'military_flight');
assert.equal(engine.detect(new Set()).length, 1, 'threshold of 2 fires on 2 domains');
clock.now += HOUR_MS + 1;
assert.deepEqual(engine.detect(new Set()), [], 'custom 1h window prunes both domains');
});
});
describe('geo convergence proximity lookup', () => {
it('returns the strongest cell within the radius', () => {
const { engine } = makeEngine();
engine.ingest(32.4, 44.9, 'protest');
engine.ingest(32.4, 44.9, 'military_flight');
const near = engine.alertsNear(32.5, 44.5, 100);
assert.deepEqual(near, { score: 54, types: 2 });
});
it('excludes cells outside the radius', () => {
const { engine } = makeEngine();
engine.ingest(32.4, 44.9, 'protest');
engine.ingest(32.4, 44.9, 'military_flight');
// Cell centroid is (32.5, 44.5); ~5 degrees of latitude away is ~555km.
assert.equal(engine.alertsNear(37.5, 44.5, 100), null);
assert.deepEqual(engine.alertsNear(37.5, 44.5, 600), { score: 54, types: 2 });
});
it('ignores single-domain cells', () => {
const { engine } = makeEngine();
engine.ingest(32.4, 44.9, 'protest');
engine.ingest(32.6, 44.6, 'protest');
assert.equal(GEO_NEARBY_MIN_TYPES, 2);
assert.equal(engine.alertsNear(32.5, 44.5, 500), null);
});
it('picks the highest scoring cell when several are in range', () => {
const { engine } = makeEngine();
engine.ingest(32.4, 44.9, 'protest');
engine.ingest(32.4, 44.9, 'military_flight');
engine.ingest(33.4, 44.9, 'protest');
engine.ingest(33.4, 44.9, 'military_flight');
engine.ingest(33.4, 44.9, 'military_vessel');
assert.deepEqual(engine.alertsNear(32.9, 44.5, 400), { score: 81, types: 3 });
});
it('prunes stale events before answering', () => {
const { engine, clock } = makeEngine();
engine.ingest(32.4, 44.9, 'protest');
engine.ingest(32.4, 44.9, 'military_flight');
clock.now += GEO_CONVERGENCE_WINDOW_MS + 1;
assert.equal(engine.alertsNear(32.5, 44.5, 100), null);
});
});
describe('geo reverse geocoding', () => {
const places = {
conflictZones: [
{ name: 'Sudan Civil War', center: [30, 15] },
{ name: 'Iran Conflict', center: [53, 32] },
],
waterways: [{ name: 'STRAIT OF HORMUZ', lat: 26.5, lon: 56.5 }],
hotspots: [
{ name: 'Tehran', lat: 35.7, lon: 51.4 },
{ name: 'Qom', lat: 34.6, lon: 50.9 },
],
};
it('measures great-circle distance in km', () => {
assert.equal(Math.round(haversineKm(0, 0, 0, 0)), 0);
// One degree of latitude is ~111km anywhere on the globe.
assert.ok(Math.abs(haversineKm(0, 0, 1, 0) - 111) < 1);
});
it('prefers conflict zones and strips the boilerplate suffix', () => {
// Zone centers are [lon, lat] — the reversed order is load-bearing.
assert.equal(getLocationName(15.5, 30.5, places), 'Sudan');
assert.equal(getLocationName(32.2, 53.2, places), 'Iran');
});
it('falls back to strategic waterways when no conflict zone is close', () => {
assert.equal(getLocationName(26.8, 56.6, places), 'STRAIT OF HORMUZ');
});
it('falls back to the nearest intel hotspot, not merely the first in range', () => {
// Both hotspots are within 150km; Qom is closer.
assert.equal(getLocationName(34.8, 51.0, places), 'Qom');
});
it('falls back to a coarse region when nothing named is near', () => {
assert.equal(getLocationName(33, 44, {}), 'Middle East');
assert.equal(getLocationName(35, 120, {}), 'East Asia');
assert.equal(getLocationName(5, 100, {}), 'Southeast Asia');
assert.equal(getLocationName(48, 10, {}), 'Europe');
assert.equal(getLocationName(55, 100, {}), 'Russia');
assert.equal(getLocationName(-5, 20, {}), 'Africa');
assert.equal(getLocationName(40, -100, {}), 'North America');
assert.equal(getLocationName(-20, -60, {}), 'South America');
});
it('falls back to formatted coordinates outside every region box', () => {
assert.equal(getLocationName(80.12, -170.98, {}), '80.1°, -171.0°');
});
it('treats omitted datasets as empty', () => {
assert.equal(getLocationName(33, 44), 'Middle East');
});
});
describe('geo convergence signal projection', () => {
const alert = {
cellId: '32,44',
lat: 32.5,
lon: 44.5,
types: ['protest', 'military_flight', 'military_vessel'],
totalEvents: 7,
score: 89,
};
it('projects an alert into a deterministic correlation signal', () => {
const signal = geoConvergenceToSignal(alert, {
places: {},
generateId: () => 'sig-test',
now: () => new Date(0),
});
assert.equal(signal.id, 'sig-test');
assert.equal(signal.type, 'geo_convergence');
assert.equal(signal.title, 'Geographic Convergence (3 types)');
assert.equal(
signal.description,
'protests, military flights, naval vessels in Middle East - 7 events/24h',
);
assert.equal(signal.confidence, 0.89);
assert.equal(signal.timestamp.getTime(), 0);
assert.deepEqual(signal.data, {
newsVelocity: 7,
relatedTopics: ['protest', 'military_flight', 'military_vessel'],
});
});
it('labels every event type', () => {
assert.deepEqual(GEO_EVENT_TYPE_LABELS, {
protest: 'protests',
military_flight: 'military flights',
military_vessel: 'naval vessels',
earthquake: 'seismic activity',
});
});
it('mints an id and timestamp when none are injected', () => {
const signal = geoConvergenceToSignal(alert);
assert.match(signal.id, /^sig-[0-9a-f-]{36}$/);
assert.ok(signal.timestamp instanceof Date);
});
});
describe('client shim stays pinned to the shared core', () => {
it('publishes the same convergence threshold the docs guard reads', () => {
// tests/docs-signal-alignment.test.mts asserts this literal in the client shim
// and cross-checks it against docs/geographic-convergence.mdx; the shared core
// owns the default, so the two must not drift.
const shim = readFileSync(new URL('../src/services/geo-convergence.ts', import.meta.url), 'utf8');
const literal = shim.match(/const CONVERGENCE_THRESHOLD = (\d+);/);
assert.ok(literal, 'src/services/geo-convergence.ts must declare CONVERGENCE_THRESHOLD');
assert.equal(Number(literal[1]), GEO_CONVERGENCE_THRESHOLD);
});
it('imports the core instead of re-implementing it', () => {
const shim = readFileSync(new URL('../src/services/geo-convergence.ts', import.meta.url), 'utf8');
assert.match(shim, /from '\.\.\/\.\.\/shared\/analysis-geo-convergence'/);
});
it('keeps the shared core free of client-only imports', () => {
const core = readFileSync(new URL('../shared/analysis-geo-convergence.ts', import.meta.url), 'utf8');
assert.doesNotMatch(core, /from '@\//, 'core must not use the src/ path alias');
assert.doesNotMatch(core, /import\.meta/, 'core must stay bundler-agnostic');
});
});