← back to La Socrata Ingester
test/auto-repoint.test.mjs
150 lines
// TK-10955: zero-network, zero-database proof of the VERIFIED auto-repoint policy
// (Steve 2026-09-11, option B/D). Positional NavigateLA layer ids shifted +1 twice in six
// days, so fail-loud-only meant a roughly weekly outage. The policy is now: find the layer
// by exact name, PROVE it on the authoritative per-layer plane, then ingest — and still
// fail loud on anything we cannot prove.
//
// Every check below is a NEGATIVE control except 1 and 2: the point of this suite is that
// the resolver REFUSES to move without proof, not merely that it moves when it can.
import assert from 'node:assert/strict';
import { resolveLayerIdentity, _resetCatalogCache } from '../src/adapters/arcgis.js';
const NAV = 'https://maps.lacity.org/arcgis/rest/services/Mapping/NavigateLA/MapServer';
const src = (id, extra = {}) => ({
endpoint: `${NAV}/${id}`, expectName: 'Council Districts',
nameFields: ['District_Name'], ...extra,
});
// A fake upstream: map of id -> layer metadata, plus the service catalog at '?f=json'.
const F = (name, { type = 'Feature Layer', fields = ['OBJECTID', 'District_Name'] } = {}) =>
({ name, type, fields: fields.map((n) => ({ name: n })) });
function upstream(layers, { catalog = true, catalogLayers = null } = {}) {
return async (url) => {
if (url === `${NAV}?f=json`) {
if (!catalog) throw new Error('catalog unavailable');
const src = catalogLayers ?? layers;
return { layers: Object.entries(src).map(([id, m]) => ({ id: Number(id), name: m.name, type: m.type })) };
}
const m = /\/(\d+)\?f=json$/.exec(url);
const hit = m && layers[m[1]];
if (!hit) throw new Error(`no layer ${m && m[1]}`);
return hit;
};
}
const run = (s, f) => { _resetCatalogCache(); return resolveLayerIdentity(s, f); };
// 1. Pinned id still correct → no repoint, endpoint untouched.
let r = await run(src(419), upstream({ 419: F('Council Districts') }));
assert.equal(r.repointed, false);
assert.equal(r.endpoint, `${NAV}/419`);
// 2. The real 2026-09-11 shift: pinned 419 drifted, truth moved to 420 → repoint via catalog.
r = await run(src(419), upstream({
419: F('Contract Administration Inspection Districts'),
420: F('Council Districts'),
}));
assert.equal(r.repointed, true);
assert.equal(r.to, 420);
assert.equal(r.via, 'catalog');
assert.equal(r.endpoint, `${NAV}/420`);
// 3. STALE CATALOG (measured live on 2026-09-11: catalog served the OLD index while
// per-layer endpoints served the new one). The catalog says 419, but 419 re-proves as
// something else, so the catalog hit must be REJECTED and the window scan must find 420.
r = await run(src(419), upstream(
{ 419: F('Contract Administration Inspection Districts'), 420: F('Council Districts') },
{ catalogLayers: { 419: F('Council Districts'), 420: F('Previous Council Districts') } }
));
assert.equal(r.repointed, true);
assert.equal(r.to, 420, 'a stale catalog must not decide the id');
assert.match(r.via, /^window/);
// 4. NEGATIVE — a GROUP LAYER with our exact name must never be repointed onto.
// (This is the documented layer-73 trap: same name, wrong type.)
await assert.rejects(
() => run(src(419), upstream({
419: F('Contract Administration Inspection Districts'),
420: F('Council Districts', { type: 'Group Layer' }),
})),
/Could not PROVE/,
'a same-named Group Layer is not a valid repoint target'
);
// 5. NEGATIVE — a same-named REPLACEMENT dataset lacking our fields must be refused.
await assert.rejects(
() => run(src(419), upstream({
419: F('Contract Administration Inspection Districts'),
420: F('Council Districts', { fields: ['FID', 'SOMETHING_ELSE'] }),
})),
/Could not PROVE/,
'a same-named layer missing oidField/nameFields is not ours'
);
// 6. NEGATIVE — AMBIGUITY (two proved matches) must fail loud, not pick one.
await assert.rejects(
() => run(src(419), upstream({
419: F('Contract Administration Inspection Districts'),
420: F('Council Districts'),
418: F('Council Districts'),
}, { catalog: false })),
/ambiguous|Could not PROVE/i,
'two matching layers must fail loud'
);
// 7. NEGATIVE — a genuine RENAME (our name exists nowhere) must fail loud.
await assert.rejects(
() => run(src(419), upstream({ 419: F('Something Entirely Different') }, { catalog: false })),
/Could not PROVE/,
'a rename must fail loud, never silently follow'
);
// 8. NEGATIVE — SCHEMA drift on the pinned id (right name, wrong shape) must fail loud and
// must NOT wander off looking for another layer.
await assert.rejects(
() => run(src(419), upstream({ 419: F('Council Districts', { fields: ['FID'] }) })),
/layer schema drift/,
'same name + wrong schema means the dataset was replaced'
);
// 9. The kill switch still works: LA_NO_AUTO_REPOINT=1 restores fail-loud-only.
process.env.LA_NO_AUTO_REPOINT = '1';
await assert.rejects(
() => run(src(419), upstream({ 419: F('Contract Administration Inspection Districts'), 420: F('Council Districts') })),
/repoint the layer id/,
'the kill switch must restore pure fail-loud'
);
delete process.env.LA_NO_AUTO_REPOINT;
// 10. Unpinned sources (no expectName) are untouched — the guard stays opt-in.
r = await run({ endpoint: `${NAV}/5` }, async () => { throw new Error('must not be called'); });
assert.equal(r.repointed, false);
assert.equal(r.endpoint, `${NAV}/5`);
// 11. A FAILED catalog fetch must NOT be cached. Caching it would let one transient blip on
// the first drifted source silently downgrade every LATER source in the same run to the
// window-scan path. Here the catalog throws once, then succeeds; the second resolve must
// use the catalog path, proving the failure was forgotten rather than memoised.
{
let catalogCalls = 0;
const layers = { 419: F('Contract Administration Inspection Districts'), 420: F('Council Districts') };
const flaky = async (url) => {
if (url === `${NAV}?f=json`) {
catalogCalls++;
if (catalogCalls === 1) throw new Error('transient catalog blip');
return { layers: Object.entries(layers).map(([id, m]) => ({ id: Number(id), name: m.name, type: m.type })) };
}
const m = /\/(\d+)\?f=json$/.exec(url);
const hit = m && layers[m[1]];
if (!hit) throw new Error(`no layer ${m && m[1]}`);
return hit;
};
_resetCatalogCache();
const first = await resolveLayerIdentity(src(419), flaky); // catalog throws -> window path
assert.equal(first.to, 420, 'window scan must still recover when the catalog is down');
assert.match(first.via, /^window/);
const second = await resolveLayerIdentity(src(419), flaky); // catalog retried -> catalog path
assert.equal(second.via, 'catalog', 'a failed catalog fetch must not be cached for the run');
assert.equal(catalogCalls, 2, 'the catalog must be retried, not memoised as null');
}
console.log('✔ verified auto-repoint: 11/11 checks pass (no network, no database)');