Files
stack/scripts/gate-verify.test.mjs
T

464 lines
16 KiB
JavaScript

import assert from 'node:assert/strict';
import { chmod, copyFile, mkdir, readFile, rm, symlink, 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 verifier = path.join(process.cwd(), 'scripts', 'gate-verify.mjs');
const fixtureBase = path.join(process.cwd(), '.mosaic-test-work', `gate-verify-${process.pid}`);
async function fixture(name = 'case') {
const root = path.join(fixtureBase, name);
await rm(root, { recursive: true, force: true });
await mkdir(path.join(root, 'gates'), { recursive: true });
return root;
}
function baseManifest() {
return {
schemaVersion: 1,
activationCommit: null,
gateRoots: ['gates'],
governingClaimFiles: [],
coverageBoundary: { included: ['meta fixture'], excluded: [], trackedBy: 'RM-54' },
criteria: [
{
id: 'META-CRIT-1',
originalText: 'The fixture rejects its bad input.',
currentText: 'The fixture rejects its bad input.',
claimType: 'integrity',
source: 'fixture',
meaningChanges: [],
},
],
compatibilityScenarios: [],
proseClaims: [],
gates: [
{
id: 'meta-fixture',
source: 'gates/meta-fixture.sh',
invocation: ['gates/meta-fixture.sh'],
deployment: { kind: 'none', reason: 'test fixture only' },
inertMutation: {
file: 'gates/meta-fixture.sh',
find: 'exit 7',
replace: 'exit 0',
expected: { exitCode: 0 },
},
cases: [
{
id: 'rejects-bad-input',
criterionIds: ['META-CRIT-1'],
mustFail: true,
invocation: ['gates/meta-fixture.sh'],
required: { exitCode: 7 },
actual: { exitCode: 7 },
reasonPattern: 'META_REJECT',
},
],
},
],
};
}
async function writeGate(root, contents = '#!/bin/sh\necho META_REJECT >&2\nexit 7\n') {
const target = path.join(root, 'gates', 'meta-fixture.sh');
await writeFile(target, contents);
await chmod(target, 0o755);
}
async function writeManifest(root, manifest) {
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), `${JSON.stringify(manifest)}\n`);
}
function verify(root) {
return spawnSync(
process.execPath,
[verifier, '--root', root, '--manifest', 'gates/gates.manifest.json', '--skip-history'],
{ cwd: root, encoding: 'utf8', env: { ...process.env, HOME: os.homedir() } },
);
}
function output(result) {
return `${result.stdout}\n${result.stderr}`;
}
test.after(async () => {
await rm(fixtureBase, { recursive: true, force: true });
});
test('an externally inerted failure branch makes verification nonzero and names the gate', async () => {
const root = await fixture('external-inert');
await writeGate(root, '#!/bin/sh\nexit 0\n');
await writeManifest(root, baseManifest());
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture/);
});
test('the verifier applies the declared inert mutation and observes its own control red', async () => {
const root = await fixture('internal-meta');
await writeGate(root);
await writeManifest(root, baseManifest());
const result = verify(root);
assert.equal(result.status, 0, output(result));
assert.match(output(result), /META-NEGATIVE-CONTROL.*meta-fixture.*observed red/i);
});
test('a mutation crash is rejected instead of counted as an observed-red control', async () => {
const root = await fixture('mutation-crash');
await writeGate(root);
const manifest = baseManifest();
manifest.gates[0].inertMutation.replace = 'this is not valid shell (';
manifest.gates[0].inertMutation.expected = { exitCode: 0 };
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture.*mutation.*unexpected outcome/i);
assert.doesNotMatch(output(result), /META-NEGATIVE-CONTROL.*observed red/i);
});
test('a stale declared mutation case id is rejected instead of falling back', async () => {
const root = await fixture('stale-case-id');
await writeGate(root);
const manifest = baseManifest();
manifest.gates[0].inertMutation.caseId = 'case-that-does-not-exist';
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture.*case-that-does-not-exist.*not found/i);
});
test('duplicate stable ids and unsupported schema versions are rejected', async () => {
const root = await fixture('closed-schema');
await writeGate(root);
const manifest = baseManifest();
manifest.schemaVersion = 99;
manifest.criteria.push({ ...manifest.criteria[0] });
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /unsupported schemaVersion 99/i);
assert.match(output(result), /duplicate criterion id META-CRIT-1/i);
});
test('manifest-controlled fixture paths cannot escape the sandbox', async () => {
const root = await fixture('path-traversal');
await writeGate(root);
const manifest = baseManifest();
manifest.gates[0].cases[0].fixture = {
writeFiles: [{ path: '../../escaped-by-manifest', content: 'bad' }],
};
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /path escapes sandbox/i);
});
test('fixture writes reject a final symlink and preserve its outside target', async () => {
const root = await fixture('final-symlink');
await writeGate(root);
const outside = path.join(fixtureBase, 'outside-sentinel');
await writeFile(outside, 'preserve-me\n');
const linked = path.join(root, 'linked-sentinel');
await symlink(outside, linked);
const manifest = baseManifest();
manifest.gates[0].cases[0].fixture = {
writeFiles: [{ path: 'linked-sentinel', content: 'overwritten\n' }],
};
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /fixture write.*symbolic link/i);
assert.equal(await readFile(outside, 'utf8'), 'preserve-me\n');
});
test('an executable below a gate root without an entry is rejected', async () => {
const root = await fixture('unregistered');
await writeGate(root);
const extra = path.join(root, 'gates', 'forgotten.sh');
await writeFile(extra, '#!/bin/sh\nexit 1\n');
await chmod(extra, 0o755);
await writeManifest(root, baseManifest());
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /unregistered gate.*forgotten\.sh/i);
});
test('a must-fail case without a reason diagnostic is rejected', async () => {
const root = await fixture('missing-reason');
await writeGate(root);
const manifest = baseManifest();
manifest.gates[0].cases[0].reasonPattern = '';
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture.*rejects-bad-input.*reasonPattern/i);
});
test('a gate with zero must-fail cases is rejected', async () => {
const root = await fixture('no-negative');
await writeGate(root, '#!/bin/sh\nexit 0\n');
const manifest = baseManifest();
manifest.gates[0].inertMutation = {
file: 'gates/meta-fixture.sh',
find: 'exit 0',
replace: 'exit 1',
};
manifest.gates[0].cases = [
{
id: 'positive',
criterionIds: ['META-CRIT-1'],
mustFail: false,
invocation: ['gates/meta-fixture.sh'],
required: { exitCode: 0 },
actual: { exitCode: 0 },
reasonPattern: '',
},
];
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture.*no negative control/i);
});
test('reordered but equivalent outcome fields do not create a false behavior delta', async () => {
const root = await fixture('reordered-outcomes');
await writeGate(root);
const manifest = baseManifest();
manifest.gates[0].cases[0].required = { exitCode: 7, outputPattern: 'META_REJECT' };
manifest.gates[0].cases[0].actual = { outputPattern: 'META_REJECT', exitCode: 7 };
await writeManifest(root, manifest);
const result = verify(root);
assert.equal(result.status, 0, output(result));
assert.doesNotMatch(output(result), /behavior delta requires|DEFECT \(owner:/);
});
test('a required-versus-actual delta without a tracked owner is rejected', async () => {
const root = await fixture('ownerless-delta');
await writeGate(root, '#!/bin/sh\nexit 0\n');
const manifest = baseManifest();
manifest.gates[0].inertMutation.find = 'exit 0';
manifest.gates[0].inertMutation.replace = 'exit 7';
manifest.gates[0].cases[0].actual.exitCode = 0;
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture.*delta.*tracked owner/i);
});
test('a criterion with no bound case is rejected', async () => {
const root = await fixture('unbound-criterion');
await writeGate(root);
const manifest = baseManifest();
manifest.criteria.push({
id: 'ORPHAN',
originalText: 'This criterion is not exercised.',
currentText: 'This criterion is not exercised.',
claimType: 'quality',
source: 'fixture',
meaningChanges: [],
});
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /ORPHAN.*no bound case/i);
});
test('an unbound governing prose marker is rejected', async () => {
const root = await fixture('unbound-prose');
await writeGate(root);
await mkdir(path.join(root, 'docs'), { recursive: true });
await writeFile(path.join(root, 'docs', 'governing.md'), 'GATE-CLAIM:UNBOUND\n');
const manifest = baseManifest();
manifest.governingClaimFiles = ['docs/governing.md'];
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /GATE-CLAIM:UNBOUND.*unbound/i);
});
test('directly contradictory modeled scenarios are rejected', async () => {
const root = await fixture('conflict');
await writeGate(root);
const manifest = baseManifest();
manifest.compatibilityScenarios = [
{
id: 'ONE',
construction: 'same-input',
caseRefs: ['meta-fixture/rejects-bad-input'],
invocation: ['sh', '-c', 'exit 0'],
expected: { exitCode: 0 },
},
{
id: 'TWO',
construction: 'same-input',
caseRefs: ['meta-fixture/rejects-bad-input'],
invocation: ['sh', '-c', 'exit 1'],
expected: { exitCode: 1 },
},
];
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /ONE.*TWO.*conflict/i);
});
test('compatibility scenarios execute referenced conditions as one construction', async () => {
const root = await fixture('combined-compatibility');
await writeGate(root);
const manifest = baseManifest();
manifest.gates[0].cases.push({
id: 'second-condition',
criterionIds: ['META-CRIT-1'],
mustFail: true,
invocation: ['sh', '-c', 'echo "$SECOND_REASON" >&2; exit 7'],
fixture: { writeFiles: [{ path: 'conditions/second', content: 'present\n' }] },
required: { exitCode: 7 },
actual: { exitCode: 7 },
reasonPattern: 'SECOND_REASON',
environment: { SECOND_REASON: 'SECOND_REASON' },
});
manifest.gates[0].cases[0].fixture = {
writeFiles: [{ path: 'conditions/first', content: 'present\n' }],
};
manifest.compatibilityScenarios = [
{
id: 'BOTH-CONDITIONS',
construction: 'both-fixtures',
caseRefs: ['meta-fixture/rejects-bad-input', 'meta-fixture/second-condition'],
invocation: ['sh', '-c', 'test -f conditions/first && test -f conditions/second'],
expected: { exitCode: 0 },
},
];
await writeManifest(root, manifest);
const result = verify(root);
assert.equal(result.status, 0, output(result));
assert.match(output(result), /COMPATIBILITY BOTH-CONDITIONS: observed expected outcome/i);
});
test('a restated criterion without provenance is rejected', async () => {
const root = await fixture('provenance');
await writeGate(root);
const manifest = baseManifest();
manifest.criteria[0].currentText = 'The fixture rejects only malformed input.';
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /META-CRIT-1.*meaning-change provenance/i);
});
test('a security criterion bound only to a positive case is unregistered in substance', async () => {
const root = await fixture('positive-only-criterion');
await writeGate(root);
const manifest = baseManifest();
manifest.criteria.push({
id: 'POSITIVE-ONLY',
originalText: 'A security property.',
currentText: 'A security property.',
claimType: 'security',
source: 'fixture',
meaningChanges: [],
});
manifest.gates[0].cases.push({
id: 'positive-only',
criterionIds: ['POSITIVE-ONLY'],
mustFail: false,
invocation: ['gates/meta-fixture.sh'],
required: { exitCode: 7 },
actual: { exitCode: 7 },
reasonPattern: '',
});
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /POSITIVE-ONLY.*no must-fail case/i);
});
test('a registered prose claim whose marker is absent is rejected', async () => {
const root = await fixture('missing-prose-marker');
await writeGate(root);
await mkdir(path.join(root, 'docs'), { recursive: true });
await writeFile(path.join(root, 'docs', 'governing.md'), 'No marker here.\n');
const manifest = baseManifest();
manifest.governingClaimFiles = ['docs/governing.md'];
manifest.proseClaims = [{ id: 'MISSING-MARKER', criterionId: 'META-CRIT-1' }];
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /GATE-CLAIM:MISSING-MARKER.*missing/i);
});
test('source-versus-deployed byte drift is rejected and names the gate', async () => {
const root = await fixture('deployment-drift');
await writeGate(root);
await mkdir(path.join(root, 'deployed'), { recursive: true });
await writeFile(path.join(root, 'deployed', 'meta-fixture.sh'), '#!/bin/sh\nexit 0\n');
const manifest = baseManifest();
manifest.gates[0].deployment = { kind: 'file', path: 'deployed/meta-fixture.sh' };
await writeManifest(root, manifest);
const result = verify(root);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture.*source.*deployed.*drift/i);
});
test('deployment drift meta-control fails if the shared comparator is made inert', async () => {
const root = await fixture('deployment-comparator-inert');
await writeGate(root);
await mkdir(path.join(root, 'deployed'), { recursive: true });
await copyFile(path.join(root, 'gates', 'meta-fixture.sh'), path.join(root, 'deployed', 'meta-fixture.sh'));
const manifest = baseManifest();
manifest.gates[0].deployment = { kind: 'file', path: 'deployed/meta-fixture.sh' };
await writeManifest(root, manifest);
const alteredScripts = path.join(root, 'verifier-scripts');
await mkdir(alteredScripts, { recursive: true });
const verifierSource = await readFile(verifier, 'utf8');
const inertSource = verifierSource.replace(
'return source.equals(deployed);',
'return true; // deliberate test-only inert comparator',
);
assert.notEqual(inertSource, verifierSource, 'shared deployment comparator mutation went stale');
await writeFile(path.join(alteredScripts, 'gate-verify.mjs'), inertSource);
await copyFile(
path.join(process.cwd(), 'scripts', 'gate-history.mjs'),
path.join(alteredScripts, 'gate-history.mjs'),
);
const result = spawnSync(
process.execPath,
[
path.join(alteredScripts, 'gate-verify.mjs'),
'--root',
root,
'--manifest',
'gates/gates.manifest.json',
'--skip-history',
],
{ cwd: root, encoding: 'utf8', env: { ...process.env, HOME: os.homedir() } },
);
assert.notEqual(result.status, 0);
assert.match(output(result), /meta-fixture.*drift negative control was ineffective/i);
});