Files
gods-eye-view/src/data/cctvCards.test.mjs
T
Bilawal Sidhu b6da93b8ac release: v0.1.0 — one-click install, keyless boot, in-app Provider Settings (#142)
The first formal release.

- One-click install via Pinokio; keyless boot lands on a live Esri World
  Imagery satellite globe with keyless terrain, with automatic OSM fallback
  and graceful degradation when terrain is unavailable.
- Provider Settings (the POWER UP panel): add, replace, or remove API keys
  inside the app; credential files made owner-only before any secret is
  written; external keys shown read-only; the panel refuses shared or proxied
  servers.
- Keyless capability responses for the optional HUD summary and place search.
- Aircraft-identity voice answers cover operator, type, and route, and say so
  plainly when enrichment is unavailable.
- README rewritten keyless-first; concise 0.1.0 changelog section; CI
  workflow included.

Gates: 2,672 unit tests pass / 0 fail, build clean, tracking regression
108/108, map-source tray QA pass, setup doctor ready.
2026-08-31 17:49:45 -05:00

293 lines
13 KiB
JavaScript

// src/data/cctvCards.test.mjs
// Pure-helper tests for the ambient CCTV thumbnail-card overlay: greedy
// declutter, the no-flicker frame-persistence rule, retry pacing, and the
// bounded thumbnail-cache prune. Rendering geometry (placeCard/cardAlpha) is
// shared with firmsLabels.js and covered by firmsLabels.test.mjs; the radial
// keyhole edge fade is covered by the celestialRing suite.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import {
CCTV_CARD_FADE_END_M,
CCTV_CARD_FADE_START_M,
CCTV_CARD_FETCH_BURST_LIMIT,
CCTV_CARD_FETCH_BURST_SPACING_MS,
CCTV_CARD_FETCH_STEADY_SPACING_MS,
CCTV_CARD_MIN_SEP_PX,
CCTV_CARD_SCALE_AT_MID,
CCTV_CARD_SCALE_FULL_M,
CCTV_CARD_SCALE_MID_M,
CCTV_CARD_SCALE_MIN,
CCTV_FRAME_CACHE_MAX,
applyFrameResult,
cardFetchPolicy,
cardScaleForAltitude,
createCctvThumbnailOverlayEntry,
createFrameSlot,
declutterCctvCards,
frameFetchDue,
frameRetryDelayMs,
isCctvCardAnchorSafe,
planFrameCachePrune,
} from './cctvCards.js';
test('isCctvCardAnchorSafe protects the top HUD band for ambient cards', () => {
assert.equal(isCctvCardAnchorSafe({ sy: 100, viewH: 1000 }), false);
assert.equal(isCctvCardAnchorSafe({ sy: 150, viewH: 1000 }), true);
assert.equal(isCctvCardAnchorSafe({ sy: 100, viewH: 1000, pinned: true }), true);
assert.equal(isCctvCardAnchorSafe({ sy: NaN, viewH: 1000 }), false);
});
// ─── declutterCctvCards ──────────────────────────────────────────────────────
test('declutterCctvCards: nearest-first greedy accept with min separation', () => {
const kept = declutterCctvCards([
{ id: 'far', sx: 100, sy: 100, distanceKm: 5 },
{ id: 'near', sx: 110, sy: 110, distanceKm: 1 }, // wins the overlap: nearer
{ id: 'clear', sx: 400, sy: 400, distanceKm: 9 },
], { minSepPx: 130 });
assert.deepEqual(kept, ['near', 'clear']);
});
test('declutterCctvCards: separation is radial (Euclidean), matching the vessel pass', () => {
// 100px apart on one axis: inside a 130px separation, outside a 90px one.
const candidates = [
{ id: 'a', sx: 0, sy: 0, distanceKm: 1 },
{ id: 'b', sx: 100, sy: 0, distanceKm: 2 },
];
assert.deepEqual(declutterCctvCards(candidates, { minSepPx: 130 }), ['a']);
assert.deepEqual(declutterCctvCards(candidates, { minSepPx: 90 }), ['a', 'b']);
});
test('declutterCctvCards: respects the limit and drops malformed rows', () => {
const kept = declutterCctvCards([
{ id: 'a', sx: 0, sy: 0, distanceKm: 1 },
{ id: 'b', sx: 500, sy: 0, distanceKm: 2 },
{ id: 'c', sx: 1000, sy: 0, distanceKm: 3 },
{ id: '', sx: 200, sy: 200, distanceKm: 0 },
{ id: 'nan', sx: NaN, sy: 0, distanceKm: 0 },
null,
], { minSepPx: 130, limit: 2 });
assert.deepEqual(kept, ['a', 'b']);
});
test('thumbnail entry carries the exact shipped contract and stable frame-slot reference', () => {
const frameSlot = createFrameSlot();
const entry = createCctvThumbnailOverlayEntry({
id: 'cam-a',
position: { x: 1, y: 2, z: 3 },
title: 'Main & 5th',
frameSlot,
rank: 2,
});
assert.equal(entry.variant, 'thumbnail');
assert.equal(entry.collisionGroup, 'ambient-card');
assert.equal(entry.paintLane, 'thumbnail');
assert.equal(entry.interactive, true);
assert.equal(entry.image, frameSlot, 'host receives the source-owned stable slot by reference');
assert.equal(entry.thumbnailWidth, 96);
assert.equal(entry.thumbnailHeight, 54);
assert.equal(entry.thumbnailPadX, 4);
assert.equal(entry.thumbnailTitleHeight, 13);
assert.equal(entry.requireImage, true);
assert.equal(entry.edgeFade, 'keyhole');
assert.equal(entry.verticalOnly, true);
assert.equal(entry.gapPx, 22);
assert.equal(entry.leaderOffsetPx, 16);
// CCTV shipped as a stateless per-frame rebuild with an anchor-separation
// pass. Both are opt-ins on the shared host: without them the arbiter's
// min-lifetime/cooldown/sticky-corner hysteresis makes cards stick below
// their icon and take ~1.5 s to return, and rectangle overlap alone lets them
// stack about twice as densely as shipped.
assert.equal(entry.stateless, true);
assert.equal(entry.minAnchorSeparationPx, CCTV_CARD_MIN_SEP_PX);
});
test('active CCTV thumbnail is protected while ambient and pinned policies stay distinct', () => {
const base = { id: 'cam-a', position: { x: 1, y: 2, z: 3 }, title: 'A', frameSlot: createFrameSlot() };
const ambient = createCctvThumbnailOverlayEntry(base);
const pinned = createCctvThumbnailOverlayEntry({ ...base, pinned: true });
const active = createCctvThumbnailOverlayEntry({ ...base, active: true });
assert.equal(ambient.protected, false);
assert.equal(ambient.pinned, false);
assert.equal(pinned.pinned, true);
assert.equal(active.protected, true);
assert.ok(active.priority > pinned.priority && pinned.priority > ambient.priority);
});
test('CCTV card module cannot resurrect a canvas, projection, listener, or private hit store', () => {
const source = readFileSync(new URL('./cctvCards.js', import.meta.url), 'utf8');
const forbidden = [
/createElement\(\s*['"]canvas['"]\s*\)/,
/postRender\.addEventListener/,
/cartesianToCanvasCoordinates/,
/SceneTransforms/,
/hitTestCctvCard/,
/setCctvCardHitRects/,
];
for (const pattern of forbidden) assert.doesNotMatch(source, pattern);
});
// ─── cardScaleForAltitude — the owner-decided altitude curve (finding 5) ────
test('cardScaleForAltitude: full size and opacity at or below 1,800 m', () => {
assert.deepEqual(cardScaleForAltitude(0), { scale: 1, alpha: 1 });
assert.deepEqual(cardScaleForAltitude(CCTV_CARD_SCALE_FULL_M), { scale: 1, alpha: 1 });
assert.deepEqual(cardScaleForAltitude(NaN), { scale: 1, alpha: 1 });
});
test('cardScaleForAltitude: hits the owner-decided waypoints', () => {
const mid = cardScaleForAltitude(CCTV_CARD_SCALE_MID_M);
assert.ok(Math.abs(mid.scale - CCTV_CARD_SCALE_AT_MID) < 1e-9, 'scale ~0.45 at 6,000 m');
assert.equal(mid.alpha, 1);
const fadeStart = cardScaleForAltitude(CCTV_CARD_FADE_START_M);
assert.equal(fadeStart.alpha, 1, 'still opaque at 7,500 m');
assert.ok(fadeStart.scale < CCTV_CARD_SCALE_AT_MID, 'keeps shrinking slightly past 6,000 m');
const midFade = cardScaleForAltitude((CCTV_CARD_FADE_START_M + CCTV_CARD_FADE_END_M) / 2);
assert.ok(Math.abs(midFade.alpha - 0.5) < 1e-9, 'linear fade midpoint');
});
test('cardScaleForAltitude: fully hidden at and above the 9,500 m ceiling', () => {
assert.equal(cardScaleForAltitude(CCTV_CARD_FADE_END_M).alpha, 0);
assert.equal(cardScaleForAltitude(50_000).alpha, 0);
assert.equal(cardScaleForAltitude(50_000).scale, CCTV_CARD_SCALE_MIN);
});
test('cardScaleForAltitude: scale and alpha are monotonic non-increasing', () => {
let prev = cardScaleForAltitude(0);
for (let h = 100; h <= 12_000; h += 100) {
const cur = cardScaleForAltitude(h);
assert.ok(cur.scale <= prev.scale + 1e-12, `scale rises at ${h} m`);
assert.ok(cur.alpha <= prev.alpha + 1e-12, `alpha rises at ${h} m`);
assert.ok(cur.scale >= CCTV_CARD_SCALE_MIN - 1e-12);
prev = cur;
}
});
// ─── cardFetchPolicy — cold-fill burst pacing (finding 3) ───────────────────
test('cardFetchPolicy: cold fill bursts up to 4 in flight at 250 ms spacing', () => {
assert.deepEqual(
cardFetchPolicy({ coldFill: true, inFlight: 0, sinceLastLaunchMs: CCTV_CARD_FETCH_BURST_SPACING_MS }),
{ mode: 'burst', launch: true }
);
assert.deepEqual(
cardFetchPolicy({ coldFill: true, inFlight: CCTV_CARD_FETCH_BURST_LIMIT - 1, sinceLastLaunchMs: 260 }),
{ mode: 'burst', launch: true }
);
// Blocked at the concurrency cap and inside the launch spacing.
assert.equal(cardFetchPolicy({ coldFill: true, inFlight: CCTV_CARD_FETCH_BURST_LIMIT, sinceLastLaunchMs: 9_999 }).launch, false);
assert.equal(cardFetchPolicy({ coldFill: true, inFlight: 0, sinceLastLaunchMs: CCTV_CARD_FETCH_BURST_SPACING_MS - 1 }).launch, false);
});
test('cardFetchPolicy: steady state keeps the 1-fetch/s single-flight gate', () => {
assert.deepEqual(
cardFetchPolicy({ coldFill: false, inFlight: 0, sinceLastLaunchMs: CCTV_CARD_FETCH_STEADY_SPACING_MS }),
{ mode: 'steady', launch: true }
);
// An in-flight fetch blocks the tick; so does the 1 s spacing.
assert.equal(cardFetchPolicy({ coldFill: false, inFlight: 1, sinceLastLaunchMs: 9_999 }).launch, false);
assert.equal(cardFetchPolicy({ coldFill: false, inFlight: 0, sinceLastLaunchMs: 999 }).launch, false);
// First-ever launch (no prior fetch) is immediate.
assert.equal(cardFetchPolicy({ coldFill: false, inFlight: 0 }).launch, true);
});
// ─── applyFrameResult — the no-flicker persistence rule ─────────────────────
test('applyFrameResult: success replaces the frame and stamps it', () => {
const slot = createFrameSlot();
const next = applyFrameResult(slot, { ok: true, frame: 'FRAME-1' }, 5_000);
assert.deepEqual(next, { frame: 'FRAME-1', stamp: 5_000, failCount: 0, lastAttemptAt: 5_000 });
});
test('applyFrameResult: a FAILED fetch persists the drawn frame and stamp', () => {
const drawn = applyFrameResult(createFrameSlot(), { ok: true, frame: 'FRAME-1' }, 5_000);
const afterFail = applyFrameResult(drawn, { ok: false }, 65_000);
// The owner's zero-flicker rule: the card keeps rendering FRAME-1.
assert.equal(afterFail.frame, 'FRAME-1');
assert.equal(afterFail.stamp, 5_000);
assert.equal(afterFail.failCount, 1);
assert.equal(afterFail.lastAttemptAt, 65_000);
// A later success replaces it and clears the failure streak.
const recovered = applyFrameResult(afterFail, { ok: true, frame: 'FRAME-2' }, 130_000);
assert.equal(recovered.frame, 'FRAME-2');
assert.equal(recovered.stamp, 130_000);
assert.equal(recovered.failCount, 0);
});
test('applyFrameResult: an ok result without a frame counts as a failure', () => {
const drawn = applyFrameResult(createFrameSlot(), { ok: true, frame: 'FRAME-1' }, 5_000);
const next = applyFrameResult(drawn, { ok: true }, 9_000);
assert.equal(next.frame, 'FRAME-1');
assert.equal(next.failCount, 1);
});
// ─── frameRetryDelayMs / frameFetchDue ──────────────────────────────────────
test('frameRetryDelayMs: doubles per failure, capped at 5 minutes', () => {
assert.equal(frameRetryDelayMs(0), 15_000);
assert.equal(frameRetryDelayMs(1), 30_000);
assert.equal(frameRetryDelayMs(2), 60_000);
assert.equal(frameRetryDelayMs(10), 300_000);
});
test('frameFetchDue: an untouched slot is due immediately', () => {
assert.equal(frameFetchDue(createFrameSlot(), 300_000, 1_000), true);
assert.equal(frameFetchDue(null, 300_000, 1_000), false);
});
test('frameFetchDue: a fresh frame is not due until its cadence elapses', () => {
const slot = applyFrameResult(createFrameSlot(), { ok: true, frame: 'F' }, 10_000);
assert.equal(frameFetchDue(slot, 300_000, 10_000 + 299_999), false);
assert.equal(frameFetchDue(slot, 300_000, 10_000 + 300_000), true);
});
test('frameFetchDue: failures back off instead of hammering the source', () => {
let slot = applyFrameResult(createFrameSlot(), { ok: false }, 10_000);
// First retry waits the doubled delay (failCount=1 -> 30s).
assert.equal(frameFetchDue(slot, 300_000, 10_000 + 29_999), false);
assert.equal(frameFetchDue(slot, 300_000, 10_000 + 30_000), true);
// A slot with a PERSISTED frame still backs off between failed refreshes.
slot = applyFrameResult(createFrameSlot(), { ok: true, frame: 'F' }, 0);
slot = applyFrameResult(slot, { ok: false }, 300_000);
assert.equal(frameFetchDue(slot, 300_000, 300_000 + 20_000), false);
assert.equal(frameFetchDue(slot, 300_000, 300_000 + 30_000), true);
});
// ─── planFrameCachePrune ────────────────────────────────────────────────────
test('planFrameCachePrune: never drops live card ids (grace included)', () => {
const slots = [
{ id: 'live-1', stamp: 10 },
{ id: 'graced', stamp: 5 },
{ id: 'old', stamp: 1 },
];
const drops = planFrameCachePrune(slots, ['live-1', 'graced'], 2);
assert.deepEqual(drops, ['old']);
});
test('planFrameCachePrune: spare slots kept newest-first up to the cap', () => {
const slots = [
{ id: 'live', stamp: 100 },
{ id: 'spare-new', stamp: 90 },
{ id: 'spare-mid', stamp: 50 },
{ id: 'spare-old', stamp: 10 },
];
const drops = planFrameCachePrune(slots, ['live'], 3);
assert.deepEqual(drops.sort(), ['spare-old']);
assert.deepEqual(planFrameCachePrune(slots, ['live'], 1).sort(),
['spare-mid', 'spare-new', 'spare-old']);
});
test('planFrameCachePrune: default cap is the exported cache bound', () => {
const slots = Array.from({ length: CCTV_FRAME_CACHE_MAX + 5 }, (_, i) => ({
id: `cam-${String(i).padStart(3, '0')}`,
stamp: i,
}));
const drops = planFrameCachePrune(slots, []);
assert.equal(drops.length, 5);
// Oldest stamps go first.
assert.deepEqual(drops.sort(), ['cam-000', 'cam-001', 'cam-002', 'cam-003', 'cam-004']);
});