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: [], caseRefs: ['meta-fixture/rejects-bad-input'], }, ], 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, extraArgs = []) { return spawnSync( process.execPath, [ verifier, '--root', root, '--manifest', 'gates/gates.manifest.json', '--skip-history', ...extraArgs, ], { 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('moving criterion bindings to unrelated cases is rejected', async () => { const root = await fixture('semantic-misbinding'); await writeGate(root); const manifest = baseManifest(); manifest.criteria.push({ id: 'META-CRIT-2', originalText: 'The fixture reports the second rejection reason.', currentText: 'The fixture reports the second rejection reason.', claimType: 'integrity', source: 'fixture', meaningChanges: [], caseRefs: ['meta-fixture/rejects-second-input'], }); manifest.gates[0].cases.push({ ...manifest.gates[0].cases[0], id: 'rejects-second-input', criterionIds: ['META-CRIT-1'], }); manifest.gates[0].cases[0].criterionIds = ['META-CRIT-2']; await writeManifest(root, manifest); const result = verify(root); assert.notEqual(result.status, 0); assert.match(output(result), /META-CRIT-1.*rejects-bad-input.*not bound/i); assert.match(output(result), /META-CRIT-2.*rejects-second-input.*not bound/i); }); test('moving meaning and prose criteria to an unrelated type error is rejected', async () => { const root = await fixture('real-manifest-misbinding'); const manifest = JSON.parse( await readFile(path.join(process.cwd(), 'gates', 'gates.manifest.json'), 'utf8'), ); for (const gate of manifest.gates) { for (const gateCase of gate.cases) { gateCase.criterionIds = gateCase.criterionIds.filter( (id) => !['RM02-MEANING-PROVENANCE', 'RM02-PROSE-CONTROL'].includes(id), ); } } const typeError = manifest.gates .find((gate) => gate.id === 'quality-typecheck') .cases.find((gateCase) => gateCase.id === 'type-error'); typeError.criterionIds.push('RM02-MEANING-PROVENANCE', 'RM02-PROSE-CONTROL'); await writeManifest(root, manifest); const result = verify(root, ['--structure-only']); assert.notEqual(result.status, 0); assert.match(output(result), /RM02-MEANING-PROVENANCE.*missing-meaning-provenance.*not bound/i); assert.match(output(result), /RM02-PROSE-CONTROL.*prose-claim-misbinding.*not bound/i); assert.match( output(result), /RM02-(?:MEANING-PROVENANCE|PROSE-CONTROL).*undeclared exercising case quality-typecheck\/type-error/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.criteria[0].caseRefs.push('meta-fixture/second-condition'); 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); });