464 lines
16 KiB
JavaScript
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);
|
|
});
|