← back to La Socrata Ingester

test/run-policy.test.mjs

122 lines

// TK-10955: zero-network, zero-database proof of aggregate exit semantics.
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { classifyRunResults } from '../src/run-policy.js';
import { SOURCES } from '../src/sources.js';

const sources = {
  gis_zoning: {
    allowFailure: {
      since: '2026-08-11',
      scope: 'aggregate-all-only',
      reason: 'mock known upstream failure',
    },
  },
  healthy_source: {},
};

const knownFailure = [{ name: 'gis_zoning', error: 'mock HTTP 502 after retries' }];
const unexpectedFailure = [{ name: 'healthy_source', error: 'mock schema regression' }];

const tolerated = classifyRunResults(knownFailure, sources, 'all');
assert.equal(tolerated.exitCode, 0, 'known gis_zoning failure must not fail aggregate all');
assert.deepEqual(tolerated.tolerated.map((r) => r.name), ['gis_zoning']);
assert.equal(tolerated.unexpected.length, 0);

const unexpected = classifyRunResults(unexpectedFailure, sources, 'all');
assert.equal(unexpected.exitCode, 1, 'unexpected source failure must fail aggregate all');
assert.deepEqual(unexpected.unexpected.map((r) => r.name), ['healthy_source']);

const directKnown = classifyRunResults(knownFailure, sources, 'gis_zoning');
assert.equal(directKnown.exitCode, 1, 'direct source runs must remain strict');
assert.equal(directKnown.tolerated.length, 0);

const mixed = classifyRunResults([...knownFailure, ...unexpectedFailure], sources, 'all');
assert.equal(mixed.exitCode, 1, 'a tolerated failure must never mask an unexpected failure');
assert.deepEqual(mixed.tolerated.map((r) => r.name), ['gis_zoning']);
assert.deepEqual(mixed.unexpected.map((r) => r.name), ['healthy_source']);

console.log('PASS run policy: known aggregate failure exits0; direct/unexpected/mixed failures exit1');

// Exercise the OS process boundary without importing cli.js (which would open PG).
// The child receives only mocked results plus real audited source metadata, then exits
// with the exact code the CLI consumes.
const policyUrl = new URL('../src/run-policy.js', import.meta.url).href;
function mockedProcessStatus(results, targetToken) {
  const child = spawnSync(process.execPath, [
    '--input-type=module',
    '--eval',
    `import { classifyRunResults } from ${JSON.stringify(policyUrl)};
     const sources = ${JSON.stringify(SOURCES)};
     const results = ${JSON.stringify(results)};
     process.exit(classifyRunResults(results, sources, ${JSON.stringify(targetToken)}).exitCode);`,
  ], { encoding: 'utf8' });
  assert.equal(child.signal, null, child.stderr);
  return child.status;
}

assert.equal(mockedProcessStatus(knownFailure, 'all'), 0, 'known aggregate failure OS exit must be 0');
assert.equal(mockedProcessStatus(unexpectedFailure, 'all'), 1, 'unexpected aggregate failure OS exit must be 1');
assert.equal(mockedProcessStatus(knownFailure, 'gis_zoning'), 1, 'direct known-source failure OS exit must be 1');
console.log('PASS process boundary: mocked no-DB child exits 0/1/1 as required');

// ── TK-10955 (contrarian hole #2): an allowFailure exemption must be scoped to the
// AUDITED failure, not to the source name. Otherwise the one source carrying an
// exemption is the one source whose layer-identity drift is silently swallowed.
const zoningLike = {
  gis_zoning: {
    allowFailure: { scope: 'aggregate-all-only', match: ['HTTP 502', 'HTTP 503', 'HTTP 504'] },
  },
};
const audited502 = classifyRunResults(
  [{ name: 'gis_zoning', error: 'HTTP 502 after 5 tries: https://maps.lacity.org/...' }],
  zoningLike, 'all'
);
assert.equal(audited502.exitCode, 0, 'the audited 502 must stay tolerated');

const drift = classifyRunResults(
  [{ name: 'gis_zoning', error: 'ArcGIS layer identity drift: .../MapServer/72 is now "Foo" but this source expects "Generalized Zoning".' }],
  zoningLike, 'all'
);
assert.equal(drift.exitCode, 1, 'layer-identity drift must NOT be swallowed by the 502 exemption');
assert.deepEqual(drift.unexpected.map((r) => r.name), ['gis_zoning']);
assert.equal(drift.tolerated.length, 0);

// Real config must actually carry the scoping (not just the mock).
assert.ok(Array.isArray(SOURCES.gis_zoning.allowFailure.match), 'gis_zoning must scope its exemption');
assert.equal(
  classifyRunResults([{ name: 'gis_zoning', error: 'ArcGIS layer identity drift: ...' }], SOURCES, 'all').exitCode,
  1, 'live SOURCES config must also refuse to tolerate identity drift'
);

console.log('PASS allowFailure is scoped to the audited failure; identity drift still exits 1');

// ── TK-10955 (2026-09-12 recurrence): assessor_parcels' audited page-1 400 must be
// tolerated in aggregate `all`, but a genuinely different assessor_parcels failure (a
// real bad query, or an upstream schema change) must still fail loud — same discipline
// as gis_zoning's exemption above, now proven on a second source.
const auditedAssessor400 = classifyRunResults(
  [{ name: 'assessor_parcels', error: 'ArcGIS error: {"code":400,"message":"Cannot perform query. Invalid query parameters.","details":["Unable to perform query. Please check your parameters."]}' }],
  SOURCES, 'all'
);
assert.equal(auditedAssessor400.exitCode, 0, 'the audited assessor_parcels page-1 400 must stay tolerated in aggregate all');
assert.deepEqual(auditedAssessor400.tolerated.map((r) => r.name), ['assessor_parcels']);

const unrelatedAssessorError = classifyRunResults(
  [{ name: 'assessor_parcels', error: 'ArcGIS error: {"code":400,"message":"Invalid field: RollYear2"}' }],
  SOURCES, 'all'
);
assert.equal(unrelatedAssessorError.exitCode, 1, 'a different assessor_parcels 400 must NOT be swallowed by the audited exemption');
assert.deepEqual(unrelatedAssessorError.unexpected.map((r) => r.name), ['assessor_parcels']);
assert.equal(unrelatedAssessorError.tolerated.length, 0);

const directAssessor400 = classifyRunResults(
  [{ name: 'assessor_parcels', error: 'ArcGIS error: {"code":400,"message":"Cannot perform query. Invalid query parameters.","details":["Unable to perform query. Please check your parameters."]}' }],
  SOURCES, 'assessor_parcels'
);
assert.equal(directAssessor400.exitCode, 1, 'a direct assessor_parcels run must remain strict (aggregate-all-only scope)');

assert.ok(Array.isArray(SOURCES.assessor_parcels.allowFailure.match), 'assessor_parcels must scope its exemption');

console.log('PASS assessor_parcels audited page-1 400 tolerated in aggregate all; unrelated/direct failures still exit 1');