234 lines
9.1 KiB
JavaScript
234 lines
9.1 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import test from 'node:test';
|
|
|
|
const root = process.cwd();
|
|
const verifierPath = path.join(root, 'scripts', 'gate-verify.mjs');
|
|
const manifestPath = path.join(root, 'gates', 'gates.manifest.json');
|
|
const requiredGateId = 'hook-pre-push';
|
|
|
|
function output(result) {
|
|
return `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
|
}
|
|
|
|
function shrinkManifest(manifest, removedGateId) {
|
|
const removedGate = manifest.gates.find((gate) => gate.id === removedGateId);
|
|
assert.ok(removedGate, `fixture gate ${removedGateId} must exist`);
|
|
const removedCaseRefs = new Set(
|
|
removedGate.cases.map((gateCase) => `${removedGateId}/${gateCase.id}`),
|
|
);
|
|
const removedCriterionIds = new Set(
|
|
manifest.criteria
|
|
.filter(
|
|
(criterion) =>
|
|
criterion.caseRefs.length > 0 &&
|
|
criterion.caseRefs.every((caseRef) => removedCaseRefs.has(caseRef)),
|
|
)
|
|
.map((criterion) => criterion.id),
|
|
);
|
|
|
|
manifest.gates = manifest.gates.filter((gate) => gate.id !== removedGateId);
|
|
manifest.criteria = manifest.criteria
|
|
.filter((criterion) => !removedCriterionIds.has(criterion.id))
|
|
.map((criterion) => ({
|
|
...criterion,
|
|
caseRefs: criterion.caseRefs.filter((caseRef) => !removedCaseRefs.has(caseRef)),
|
|
...(criterion.gateRefs
|
|
? { gateRefs: criterion.gateRefs.filter((gateId) => gateId !== removedGateId) }
|
|
: {}),
|
|
}));
|
|
manifest.proseClaims = manifest.proseClaims.filter(
|
|
(claim) => !removedCriterionIds.has(claim.criterionId) && !removedCaseRefs.has(claim.caseRef),
|
|
);
|
|
manifest.compatibilityScenarios = manifest.compatibilityScenarios
|
|
.map((scenario) => ({
|
|
...scenario,
|
|
caseRefs: scenario.caseRefs.filter((caseRef) => !removedCaseRefs.has(caseRef)),
|
|
}))
|
|
.filter((scenario) => scenario.caseRefs.length > 0);
|
|
for (const gate of manifest.gates) {
|
|
for (const gateCase of gate.cases) {
|
|
gateCase.criterionIds = gateCase.criterionIds.filter(
|
|
(criterionId) => !removedCriterionIds.has(criterionId),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
test('shrinking the verifier inventory and manifest together is rejected by an independent baseline', async () => {
|
|
const fixture = await mkdtemp(path.join(os.tmpdir(), 'rm02-shrink-both-'));
|
|
try {
|
|
await mkdir(path.join(fixture, 'scripts'), { recursive: true });
|
|
await mkdir(path.join(fixture, 'gates'), { recursive: true });
|
|
const verifier = await readFile(verifierPath, 'utf8');
|
|
const inventoryEntry = " ['hook-pre-push', '.husky/pre-push'],\n";
|
|
assert.equal(verifier.split(inventoryEntry).length - 1, 1, 'source inventory fixture drifted');
|
|
await writeFile(
|
|
path.join(fixture, 'scripts', 'gate-verify.mjs'),
|
|
verifier.replace(inventoryEntry, ''),
|
|
);
|
|
await copyFile(
|
|
path.join(root, 'gates', 'required-gates.baseline.json'),
|
|
path.join(fixture, 'gates', 'required-gates.baseline.json'),
|
|
);
|
|
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
shrinkManifest(manifest, requiredGateId);
|
|
await writeFile(
|
|
path.join(fixture, 'gates', 'gates.manifest.json'),
|
|
`${JSON.stringify(manifest)}\n`,
|
|
);
|
|
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[
|
|
path.join(fixture, 'scripts', 'gate-verify.mjs'),
|
|
'--root',
|
|
fixture,
|
|
'--manifest',
|
|
'gates/gates.manifest.json',
|
|
'--structure-only',
|
|
],
|
|
{ cwd: fixture, encoding: 'utf8' },
|
|
);
|
|
assert.notEqual(result.status, 0, 'shrinking source anchor and manifest together must go red');
|
|
assert.match(output(result), /independent required-gate baseline.*hook-pre-push/i);
|
|
} finally {
|
|
await rm(fixture, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('shrinking the independent baseline and manifest together is rejected by verifier inventory', async () => {
|
|
const fixture = await mkdtemp(path.join(os.tmpdir(), 'rm02-shrink-baseline-manifest-'));
|
|
try {
|
|
await mkdir(path.join(fixture, 'scripts'), { recursive: true });
|
|
await mkdir(path.join(fixture, 'gates'), { recursive: true });
|
|
await copyFile(verifierPath, path.join(fixture, 'scripts', 'gate-verify.mjs'));
|
|
const baseline = JSON.parse(
|
|
await readFile(path.join(root, 'gates', 'required-gates.baseline.json'), 'utf8'),
|
|
);
|
|
baseline.gates = baseline.gates.filter((gate) => gate.id !== requiredGateId);
|
|
await writeFile(
|
|
path.join(fixture, 'gates', 'required-gates.baseline.json'),
|
|
`${JSON.stringify(baseline)}\n`,
|
|
);
|
|
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
shrinkManifest(manifest, requiredGateId);
|
|
await writeFile(
|
|
path.join(fixture, 'gates', 'gates.manifest.json'),
|
|
`${JSON.stringify(manifest)}\n`,
|
|
);
|
|
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[
|
|
path.join(fixture, 'scripts', 'gate-verify.mjs'),
|
|
'--root',
|
|
fixture,
|
|
'--manifest',
|
|
'gates/gates.manifest.json',
|
|
'--structure-only',
|
|
],
|
|
{ cwd: fixture, encoding: 'utf8' },
|
|
);
|
|
assert.notEqual(result.status, 0, 'shrinking baseline and manifest together must go red');
|
|
assert.match(output(result), /verifier inventory.*hook-pre-push.*baseline/i);
|
|
} finally {
|
|
await rm(fixture, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('every gate carries an evidence-side subject distinct from its definition', async () => {
|
|
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
for (const gate of manifest.gates) {
|
|
assert.ok(gate.cases.length > 0, `${gate.id} must have consumable evidence`);
|
|
for (const gateCase of gate.cases) {
|
|
assert.equal(
|
|
gateCase.evidence?.subject,
|
|
gate.id,
|
|
`${gate.id}/${gateCase.id} must source its subject from the evidence record`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('evidence population control depends on production result consumption wiring', async () => {
|
|
const fixture = await mkdtemp(path.join(os.tmpdir(), 'rm02-evidence-consumer-inert-'));
|
|
try {
|
|
await mkdir(path.join(fixture, 'scripts'), { recursive: true });
|
|
await mkdir(path.join(fixture, 'gates'), { recursive: true });
|
|
const verifier = await readFile(verifierPath, 'utf8');
|
|
const consumer = ` const subjectFailure = consumeEvidenceSubject(gate, result.evidence);\n if (subjectFailure) failures.push(subjectFailure);\n`;
|
|
assert.equal(verifier.split(consumer).length - 1, 1, 'consumer fixture drifted');
|
|
await writeFile(
|
|
path.join(fixture, 'scripts', 'gate-verify.mjs'),
|
|
verifier.replace(consumer, ''),
|
|
);
|
|
await copyFile(
|
|
path.join(root, 'scripts', 'gate-population-control.mjs'),
|
|
path.join(fixture, 'scripts', 'gate-population-control.mjs'),
|
|
);
|
|
await copyFile(manifestPath, path.join(fixture, 'gates', 'gates.manifest.json'));
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[path.join(fixture, 'scripts', 'gate-population-control.mjs'), 'evidence-subject'],
|
|
{ cwd: fixture, encoding: 'utf8' },
|
|
);
|
|
assert.equal(result.status, 0, output(result));
|
|
assert.match(output(result), /did not reject every registered gate/i);
|
|
} finally {
|
|
await rm(fixture, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('production verification has a closed current-tree observation renderer', async () => {
|
|
const verifier = await readFile(verifierPath, 'utf8');
|
|
assert.doesNotMatch(verifier, /from ['"]\.\/gate-history\.mjs['"]/);
|
|
assert.doesNotMatch(verifier, /\bverifyHistory\s*\(/);
|
|
assert.doesNotMatch(verifier, /history[-_ ]?provenance/i);
|
|
const attacks = [
|
|
[
|
|
'history wording',
|
|
' /^META-NEGATIVE-CONTROL /,',
|
|
' /^HISTORY PROVENANCE VERIFIED /,\n /^META-NEGATIVE-CONTROL /,',
|
|
],
|
|
[
|
|
'renamed ancestry wording',
|
|
' /^META-NEGATIVE-CONTROL /,',
|
|
' /^COMMIT ANCESTRY VERIFIED /,\n /^META-NEGATIVE-CONTROL /,',
|
|
],
|
|
[
|
|
'provider lineage wording',
|
|
' /^META-NEGATIVE-CONTROL /,',
|
|
' /^PROVIDER LINEAGE SUCCESS /,\n /^META-NEGATIVE-CONTROL /,',
|
|
],
|
|
['renderer bypass', ' assertCurrentTreeObservation(observation);\n', ''],
|
|
];
|
|
for (const [name, find, replace] of attacks) {
|
|
const fixture = await mkdtemp(path.join(os.tmpdir(), 'rm02-history-renderer-'));
|
|
try {
|
|
await mkdir(path.join(fixture, 'scripts'), { recursive: true });
|
|
assert.equal(verifier.split(find).length - 1, 1, `${name}: fixture drifted`);
|
|
await writeFile(
|
|
path.join(fixture, 'scripts', 'gate-verify.mjs'),
|
|
verifier.replace(find, replace),
|
|
);
|
|
await copyFile(
|
|
path.join(root, 'scripts', 'gate-history-exclusion-control.mjs'),
|
|
path.join(fixture, 'scripts', 'gate-history-exclusion-control.mjs'),
|
|
);
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[path.join(fixture, 'scripts', 'gate-history-exclusion-control.mjs')],
|
|
{ cwd: fixture, encoding: 'utf8' },
|
|
);
|
|
assert.equal(result.status, 79, `${name}: ${output(result)}`);
|
|
assert.match(output(result), /HISTORY_PROVENANCE_FORBIDDEN/);
|
|
} finally {
|
|
await rm(fixture, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|