← back to La Socrata Ingester
test/layer-identity.test.mjs
95 lines
// TK-10955: zero-network, zero-database proof of the ArcGIS layer-identity guard.
//
// Regression under test: on 2026-09-05 LA re-indexed the NavigateLA MapServer and every
// layer id shifted +1. Because sources pinned bare numeric ids, 7 of 9 layers silently
// ingested a DIFFERENT dataset under our labels for 6 days with no error at all. The
// guard must make that class loud, and must never ingest a mismatched layer.
import assert from 'node:assert/strict';
import { assertLayerIdentity } from '../src/adapters/arcgis.js';
import { SOURCES } from '../src/sources.js';
const fakeFetch = (name) => async () => ({ name });
// 1. Matching name passes.
await assertLayerIdentity(
{ endpoint: 'x/419', expectName: 'Council Districts' },
fakeFetch('Council Districts')
);
// 2. Whitespace/case differences are not drift.
await assertLayerIdentity(
{ endpoint: 'x/419', expectName: 'Council Districts' },
fakeFetch(' council districts ')
);
// 3. The exact 2026-09-05 shift must THROW, not silently ingest.
await assert.rejects(
() => assertLayerIdentity(
{ endpoint: 'x/418', expectName: 'Council Districts' },
fakeFetch('Contract Administration Inspection Districts')
),
/layer identity drift/,
'a shifted layer id must fail loud'
);
// 4. A missing/unnamed layer (e.g. the Group Layer at old id 119) must THROW.
await assert.rejects(
() => assertLayerIdentity(
{ endpoint: 'x/119', expectName: 'Alquist Priolo Earthquake Fault Zones' },
async () => ({})
),
/layer identity drift/,
'an unnamed/group layer must fail loud'
);
// 5. Sources without expectName keep prior behaviour (opt-in guard, no network call).
let called = false;
await assertLayerIdentity({ endpoint: 'x/0' }, async () => { called = true; return {}; });
assert.equal(called, false, 'unpinned source must not preflight');
// 6. INVARIANT (not a config restatement): every source served off the NavigateLA
// MapServer must be ARMED — it must pin an expectName — or the identity guard is a
// no-op for it (assertLayerIdentity returns early when expectName is absent, check 5).
// Deliberately NOT asserting specific numeric ids: upstream layer ids are POSITIONAL
// and shift +1 whenever LA inserts a layer (twice in six days: 2026-09-05 and
// 2026-09-11, the latter caused by inserting 'Special Event Permits' at position 52).
// Asserting them would make every legitimate repoint a test edit while proving nothing
// about correctness — the id is upstream state, not our invariant. Ids as of
// 2026-09-11 for the record: zoning 73, council_districts 420, neighborhood_councils 441,
// hpoz 77, community_plan_areas 416, fault_zones 121, liquefaction 126, flood 256,
// fire_vhfhsz 360.
const NAV_PREFIX = 'https://maps.lacity.org/arcgis/rest/services/Mapping/NavigateLA/MapServer/';
const navSources = Object.entries(SOURCES).filter(
([, src]) => typeof src.endpoint === 'string' && src.endpoint.startsWith(NAV_PREFIX)
);
assert.ok(navSources.length >= 9, `expected >=9 NavigateLA sources, got ${navSources.length}`);
for (const [name, src] of navSources) {
assert.ok(
typeof src.expectName === 'string' && src.expectName.trim() !== '',
`${name} is served off NavigateLA but pins no expectName — the identity guard is DISARMED for it`
);
assert.match(src.endpoint.slice(NAV_PREFIX.length), /^\d+$/, `${name} must end in a numeric layer id`);
}
// 7b. Two sources must never point at the SAME numeric layer id either. The OLD check 6
// caught this ONLY as a side effect of asserting each source's exact id; dropping that
// assertion traded a static catch for a runtime-only one, so assert it directly. (Found by
// the contrarian red-team on TK-10955 — a real coverage regression, not a wash.)
const seenId = new Map();
for (const [name, src] of navSources) {
const id = src.endpoint.slice(NAV_PREFIX.length);
assert.ok(!seenId.has(id), `${name} and ${seenId.get(id)} both point at layer id ${id}`);
seenId.set(id, name);
}
// 7. Two sources must never expect the SAME upstream layer name — that would mean one
// of them is mislabelled, and a name-based repoint could not tell them apart.
const seen = new Map();
for (const [name, src] of navSources) {
const key = src.expectName.trim().toLowerCase().replace(/\s+/g, ' ');
assert.ok(!seen.has(key), `${name} and ${seen.get(key)} both expect "${src.expectName}"`);
seen.set(key, name);
}
console.log('✔ layer-identity guard: 8/8 checks pass (no network, no database)');