616 lines
23 KiB
JavaScript
616 lines
23 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { spawn, spawnSync } from 'node:child_process';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
assessProviderEvidence,
|
|
deriveHistoryBoundary,
|
|
listProspectiveCommits,
|
|
readManifestAtCommit,
|
|
replayCommit,
|
|
verifyHistory,
|
|
} from './gate-history.mjs';
|
|
|
|
const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `gate-history-${process.pid}`);
|
|
|
|
function sandboxUnavailable(result) {
|
|
if (result.sandboxLauncher !== 'bwrap' || result.sandboxEntered === true) return false;
|
|
const detail = `${result.stdout ?? ''}${result.stderr ?? ''}${result.error?.message ?? ''}`;
|
|
const bubblewrapSpawnDenied =
|
|
['EPERM', 'EACCES', 'ENOENT'].includes(result.error?.code) &&
|
|
/spawnSync bwrap/i.test(result.error?.message ?? '');
|
|
if (
|
|
!bubblewrapSpawnDenied &&
|
|
!/bwrap:.*(?:Operation not permitted|Creating new namespace failed)/i.test(detail)
|
|
) {
|
|
return false;
|
|
}
|
|
assert.notEqual(result.status, 0, 'sandbox unavailability must remain terminal nonzero');
|
|
return true;
|
|
}
|
|
|
|
function git(root, ...args) {
|
|
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
|
assert.equal(result.status, 0, result.stderr);
|
|
return result.stdout.trim();
|
|
}
|
|
|
|
async function commitManifest(root, marker) {
|
|
await mkdir(path.join(root, 'gates'), { recursive: true });
|
|
await writeFile(
|
|
path.join(root, 'gates', 'gates.manifest.json'),
|
|
`${JSON.stringify({ schemaVersion: 1, marker })}\n`,
|
|
);
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', marker);
|
|
return git(root, 'rev-parse', 'HEAD');
|
|
}
|
|
|
|
test.after(async () => {
|
|
await rm(fixtureRoot, { recursive: true, force: true });
|
|
});
|
|
|
|
test('sandbox refusal classification requires Bubblewrap provenance', () => {
|
|
for (const code of ['EPERM', 'EACCES', 'ENOENT']) {
|
|
assert.equal(
|
|
sandboxUnavailable({
|
|
status: null,
|
|
error: { code, message: `spawnSync bwrap ${code}` },
|
|
sandboxLauncher: 'bwrap',
|
|
sandboxEntered: false,
|
|
}),
|
|
true,
|
|
);
|
|
}
|
|
assert.equal(
|
|
sandboxUnavailable({
|
|
status: null,
|
|
error: { code: 'EPERM', message: 'spawnSync git EPERM' },
|
|
}),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
sandboxUnavailable({ status: 1, stderr: 'historical verifier said bwrap ENOENT' }),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
sandboxUnavailable({
|
|
status: 1,
|
|
stderr: 'bwrap: Creating new namespace failed: Operation not permitted',
|
|
sandboxLauncher: 'bwrap',
|
|
sandboxEntered: false,
|
|
}),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
sandboxUnavailable({
|
|
status: 1,
|
|
stderr: 'bwrap: Creating new namespace failed: Operation not permitted',
|
|
}),
|
|
false,
|
|
);
|
|
assert.equal(
|
|
sandboxUnavailable({
|
|
status: 1,
|
|
stderr: 'bwrap: Creating new namespace failed: Operation not permitted',
|
|
sandboxLauncher: 'bwrap',
|
|
sandboxEntered: true,
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
test('prospective history reads each commit own manifest rather than the current tree', async () => {
|
|
await rm(fixtureRoot, { recursive: true, force: true });
|
|
await mkdir(fixtureRoot, { recursive: true });
|
|
git(fixtureRoot, 'init', '-q');
|
|
git(fixtureRoot, 'config', 'user.name', 'gate-test');
|
|
git(fixtureRoot, 'config', 'user.email', '[email protected]');
|
|
await writeFile(path.join(fixtureRoot, 'activation.txt'), 'activation\n');
|
|
git(fixtureRoot, 'add', '.');
|
|
git(fixtureRoot, 'commit', '-m', 'activation');
|
|
const activation = git(fixtureRoot, 'rev-parse', 'HEAD');
|
|
const first = await commitManifest(fixtureRoot, 'FIRST');
|
|
const second = await commitManifest(fixtureRoot, 'SECOND');
|
|
|
|
assert.deepEqual(await listProspectiveCommits(fixtureRoot, activation, second), [first, second]);
|
|
assert.equal((await readManifestAtCommit(fixtureRoot, first)).marker, 'FIRST');
|
|
assert.equal((await readManifestAtCommit(fixtureRoot, second)).marker, 'SECOND');
|
|
});
|
|
|
|
test('historical replay executes each selected commit verifier from that commit tree', async () => {
|
|
const root = `${fixtureRoot}-replay`;
|
|
await rm(root, { recursive: true, force: true });
|
|
await mkdir(path.join(root, 'scripts'), { recursive: true });
|
|
await mkdir(path.join(root, 'gates'), { recursive: true });
|
|
git(root, 'init', '-q');
|
|
git(root, 'config', 'user.name', 'gate-test');
|
|
git(root, 'config', 'user.email', '[email protected]');
|
|
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
|
await writeFile(
|
|
path.join(root, 'scripts', 'gate-verify.mjs'),
|
|
"process.stderr.write('OLD TREE INERT\\n'); process.exitCode = 1;\n",
|
|
);
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'inert historical verifier');
|
|
const inert = git(root, 'rev-parse', 'HEAD');
|
|
await writeFile(
|
|
path.join(root, 'scripts', 'gate-verify.mjs'),
|
|
"process.stdout.write('NEW TREE VERIFIED\\n');\n",
|
|
);
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'fixed historical verifier');
|
|
const fixed = git(root, 'rev-parse', 'HEAD');
|
|
|
|
const inertResult = await replayCommit(root, inert);
|
|
const fixedResult = await replayCommit(root, fixed);
|
|
if (sandboxUnavailable(inertResult) || sandboxUnavailable(fixedResult)) return;
|
|
assert.notEqual(inertResult.status, 0);
|
|
assert.match(inertResult.stderr, /OLD TREE INERT/);
|
|
assert.equal(fixedResult.status, 0);
|
|
assert.match(fixedResult.stdout, /NEW TREE VERIFIED/);
|
|
});
|
|
|
|
test('historical install lifecycle cannot replace an authoritative verifier', async () => {
|
|
const root = `${fixtureRoot}-install-tamper`;
|
|
await rm(root, { recursive: true, force: true });
|
|
await mkdir(path.join(root, 'scripts'), { recursive: true });
|
|
await mkdir(path.join(root, 'gates'), { recursive: true });
|
|
git(root, 'init', '-q');
|
|
git(root, 'config', 'user.name', 'gate-test');
|
|
git(root, 'config', 'user.email', '[email protected]');
|
|
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
|
await writeFile(
|
|
path.join(root, 'scripts', 'gate-verify.mjs'),
|
|
"process.stderr.write('ORIGINAL VERIFIER RAN\\n'); process.exitCode = 7;\n",
|
|
);
|
|
await writeFile(path.join(root, 'forged.mjs'), "process.stdout.write('FORGED SUCCESS\\n');\n");
|
|
await writeFile(
|
|
path.join(root, 'package.json'),
|
|
`${JSON.stringify({
|
|
name: 'historical-install-tamper',
|
|
version: '1.0.0',
|
|
scripts: { postinstall: 'cp forged.mjs scripts/gate-verify.mjs' },
|
|
})}\n`,
|
|
);
|
|
await writeFile(
|
|
path.join(root, 'pnpm-lock.yaml'),
|
|
"lockfileVersion: '9.0'\nsettings:\n autoInstallPeers: true\n excludeLinksFromLockfile: false\nimporters:\n .: {}\n",
|
|
);
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'tampering lifecycle fixture');
|
|
const commit = git(root, 'rev-parse', 'HEAD');
|
|
|
|
const result = await replayCommit(root, commit);
|
|
assert.notEqual(result.status, 0);
|
|
if (sandboxUnavailable(result)) return;
|
|
assert.match(result.stderr, /authoritative archived file changed.*scripts\/gate-verify\.mjs/i);
|
|
assert.doesNotMatch(result.stdout, /FORGED SUCCESS/);
|
|
});
|
|
|
|
test('historical verifier receives no current-process secret environment', async () => {
|
|
const root = `${fixtureRoot}-secretless`;
|
|
await rm(root, { recursive: true, force: true });
|
|
await mkdir(path.join(root, 'scripts'), { recursive: true });
|
|
await mkdir(path.join(root, 'gates'), { recursive: true });
|
|
git(root, 'init', '-q');
|
|
git(root, 'config', 'user.name', 'gate-test');
|
|
git(root, 'config', 'user.email', '[email protected]');
|
|
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
|
await writeFile(
|
|
path.join(root, 'scripts', 'gate-verify.mjs'),
|
|
"if (process.env.REPLAY_SENTINEL) { process.stderr.write('SECRET LEAKED\\n'); process.exitCode = 9; } else { process.stdout.write('SECRETLESS\\n'); }\n",
|
|
);
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'secretless replay fixture');
|
|
const commit = git(root, 'rev-parse', 'HEAD');
|
|
|
|
process.env.REPLAY_SENTINEL = 'must-not-cross-boundary';
|
|
try {
|
|
const result = await replayCommit(root, commit);
|
|
if (sandboxUnavailable(result)) return;
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /SECRETLESS/);
|
|
assert.doesNotMatch(
|
|
`${result.stdout}${result.stderr}`,
|
|
/SECRET LEAKED|must-not-cross-boundary/,
|
|
);
|
|
} finally {
|
|
delete process.env.REPLAY_SENTINEL;
|
|
}
|
|
});
|
|
|
|
test('historical replay cannot observe a sibling process in the runner PID namespace', async () => {
|
|
const root = `${fixtureRoot}-pidless`;
|
|
await rm(root, { recursive: true, force: true });
|
|
await mkdir(path.join(root, 'scripts'), { recursive: true });
|
|
await mkdir(path.join(root, 'gates'), { recursive: true });
|
|
git(root, 'init', '-q');
|
|
git(root, 'config', 'user.name', 'gate-test');
|
|
git(root, 'config', 'user.email', '[email protected]');
|
|
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
|
|
|
const sleeper = spawn('sleep', ['30'], {
|
|
env: { ...process.env, REPLAY_PID_SENTINEL: 'must-not-be-visible' },
|
|
});
|
|
try {
|
|
await writeFile(
|
|
path.join(root, 'scripts', 'gate-verify.mjs'),
|
|
`import { existsSync } from 'node:fs';\nif (existsSync('/proc/${sleeper.pid}/environ')) { process.stderr.write('HOST PID VISIBLE\\n'); process.exitCode = 9; } else { process.stdout.write('PIDLESS\\n'); }\n`,
|
|
);
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'pid-isolated replay fixture');
|
|
const commit = git(root, 'rev-parse', 'HEAD');
|
|
const result = await replayCommit(root, commit);
|
|
if (sandboxUnavailable(result)) return;
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.match(result.stdout, /PIDLESS/);
|
|
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /HOST PID VISIBLE/);
|
|
} finally {
|
|
sleeper.kill('SIGTERM');
|
|
}
|
|
});
|
|
|
|
test('PR verification states the RM-60 boundary without executing an intermediate verifier', async () => {
|
|
const root = `${fixtureRoot}-feature`;
|
|
await rm(root, { recursive: true, force: true });
|
|
await mkdir(root, { recursive: true });
|
|
git(root, 'init', '-q');
|
|
git(root, 'config', 'user.name', 'gate-test');
|
|
git(root, 'config', 'user.email', '[email protected]');
|
|
await writeFile(path.join(root, 'activation.txt'), 'activation\n');
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'activation');
|
|
const activation = git(root, 'rev-parse', 'HEAD');
|
|
git(root, 'update-ref', 'refs/remotes/origin/main', activation);
|
|
await mkdir(path.join(root, 'scripts'), { recursive: true });
|
|
await mkdir(path.join(root, 'gates'), { recursive: true });
|
|
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
|
await writeFile(
|
|
path.join(root, 'scripts', 'gate-verify.mjs'),
|
|
"process.stderr.write('INTERMEDIATE INERT\\n'); process.exitCode = 1;\n",
|
|
);
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'inert intermediate');
|
|
await writeFile(
|
|
path.join(root, 'scripts', 'gate-verify.mjs'),
|
|
"process.stdout.write('HEAD HEALTHY\\n');\n",
|
|
);
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'healthy head');
|
|
|
|
const previousBranch = process.env.CI_COMMIT_BRANCH;
|
|
process.env.CI_COMMIT_BRANCH = 'feature/rm-02';
|
|
try {
|
|
const result = await verifyHistory({
|
|
root,
|
|
manifest: { schemaVersion: 1 },
|
|
});
|
|
assert.deepEqual(result.failures, []);
|
|
assert.ok(
|
|
result.observations.some((observation) =>
|
|
/HISTORY BOOTSTRAP BOUNDARY.*DOES:.*provider target.*sound.*cannot rewrite main.*DOES NOT:.*compromise.*main.*Builds 1-2/i.test(
|
|
observation,
|
|
),
|
|
),
|
|
);
|
|
assert.ok(
|
|
result.observations.some((observation) =>
|
|
/DOES:.*current tree.*DOES NOT:.*isolated.*RM-60.*RM-59/i.test(observation),
|
|
),
|
|
);
|
|
assert.ok(
|
|
result.observations.some((observation) =>
|
|
/INTERMEDIATE REPLAY DEFERRED.*RM-60.*no success is inferred/i.test(observation),
|
|
),
|
|
);
|
|
assert.ok(result.observations.every((observation) => !/INTERMEDIATE INERT/.test(observation)));
|
|
} finally {
|
|
if (previousBranch === undefined) delete process.env.CI_COMMIT_BRANCH;
|
|
else process.env.CI_COMMIT_BRANCH = previousBranch;
|
|
}
|
|
});
|
|
|
|
test('target merge-base includes gate changes committed before registry introduction', async () => {
|
|
const root = `${fixtureRoot}-delayed-introduction`;
|
|
await rm(root, { recursive: true, force: true });
|
|
await mkdir(root, { recursive: true });
|
|
git(root, 'init', '-q');
|
|
git(root, 'config', 'user.name', 'gate-test');
|
|
git(root, 'config', 'user.email', '[email protected]');
|
|
await writeFile(path.join(root, 'baseline.txt'), 'baseline\n');
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'target baseline');
|
|
const baseline = git(root, 'rev-parse', 'HEAD');
|
|
git(root, 'update-ref', 'refs/remotes/origin/main', baseline);
|
|
await mkdir(path.join(root, 'scripts'), { recursive: true });
|
|
await writeFile(path.join(root, 'scripts', 'preflight.mjs'), 'process.exit(0);\n');
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'gate change before registry');
|
|
const preRegistryGateChange = git(root, 'rev-parse', 'HEAD');
|
|
await mkdir(path.join(root, 'gates'), { recursive: true });
|
|
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'delayed registry introduction');
|
|
|
|
const previousBranch = process.env.CI_COMMIT_BRANCH;
|
|
process.env.CI_COMMIT_BRANCH = 'feature/delayed-introduction';
|
|
try {
|
|
const result = await verifyHistory({ root, manifest: { schemaVersion: 1 } });
|
|
assert.match(
|
|
result.failures.join('\n'),
|
|
new RegExp(`${preRegistryGateChange}.*own-tree registry cannot be read`, 'i'),
|
|
);
|
|
} finally {
|
|
if (previousBranch === undefined) delete process.env.CI_COMMIT_BRANCH;
|
|
else process.env.CI_COMMIT_BRANCH = previousBranch;
|
|
}
|
|
});
|
|
|
|
test('derived history boundary includes the registry-introduction commit', async () => {
|
|
const root = `${fixtureRoot}-derived-boundary`;
|
|
await rm(root, { recursive: true, force: true });
|
|
await mkdir(root, { recursive: true });
|
|
git(root, 'init', '-q');
|
|
git(root, 'config', 'user.name', 'gate-test');
|
|
git(root, 'config', 'user.email', '[email protected]');
|
|
await writeFile(path.join(root, 'baseline.txt'), 'baseline\n');
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'baseline');
|
|
const baseline = git(root, 'rev-parse', 'HEAD');
|
|
git(root, 'update-ref', 'refs/remotes/origin/main', baseline);
|
|
await mkdir(path.join(root, 'gates'), { recursive: true });
|
|
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'registry introduction');
|
|
const introduction = git(root, 'rev-parse', 'HEAD');
|
|
await writeFile(path.join(root, 'later.txt'), 'later\n');
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'later');
|
|
const head = git(root, 'rev-parse', 'HEAD');
|
|
|
|
assert.deepEqual(deriveHistoryBoundary(root, head), {
|
|
activationCommit: baseline,
|
|
introductionCommit: introduction,
|
|
targetRef: 'refs/remotes/origin/main',
|
|
});
|
|
assert.deepEqual(await listProspectiveCommits(root, baseline, head), [introduction, head]);
|
|
});
|
|
|
|
test('author-controlled activation seams cannot omit registry-era history', async () => {
|
|
const root = `${fixtureRoot}-activation-seam`;
|
|
await rm(root, { recursive: true, force: true });
|
|
await mkdir(path.join(root, 'gates'), { recursive: true });
|
|
git(root, 'init', '-q');
|
|
git(root, 'config', 'user.name', 'gate-test');
|
|
git(root, 'config', 'user.email', '[email protected]');
|
|
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'registry introduction');
|
|
const introduction = git(root, 'rev-parse', 'HEAD');
|
|
await writeFile(path.join(root, 'one.txt'), 'one\n');
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'one');
|
|
await writeFile(path.join(root, 'two.txt'), 'two\n');
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'two');
|
|
const head = git(root, 'rev-parse', 'HEAD');
|
|
const parent = git(root, 'rev-parse', 'HEAD^');
|
|
|
|
const previousBranch = process.env.CI_COMMIT_BRANCH;
|
|
process.env.CI_COMMIT_BRANCH = 'feature/activation-seam';
|
|
try {
|
|
for (const candidate of [head, parent, introduction]) {
|
|
const result = await verifyHistory({
|
|
root,
|
|
manifest: { schemaVersion: 1, activationCommit: candidate },
|
|
});
|
|
assert.match(result.failures.join('\n'), /author-controlled activationCommit.*forbidden/i);
|
|
}
|
|
} finally {
|
|
if (previousBranch === undefined) delete process.env.CI_COMMIT_BRANCH;
|
|
else process.env.CI_COMMIT_BRANCH = previousBranch;
|
|
}
|
|
});
|
|
|
|
test('globally invalid provider evidence fails when HEAD is the only prospective commit', async () => {
|
|
const root = `${fixtureRoot}-head-only-evidence`;
|
|
await rm(root, { recursive: true, force: true });
|
|
await mkdir(root, { recursive: true });
|
|
git(root, 'init', '-q');
|
|
git(root, 'config', 'user.name', 'gate-test');
|
|
git(root, 'config', 'user.email', '[email protected]');
|
|
await writeFile(path.join(root, 'baseline.txt'), 'baseline\n');
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'baseline');
|
|
git(root, 'update-ref', 'refs/remotes/origin/main', git(root, 'rev-parse', 'HEAD'));
|
|
await mkdir(path.join(root, 'gates'), { recursive: true });
|
|
await writeFile(path.join(root, 'gates', 'gates.manifest.json'), '{"schemaVersion":1}\n');
|
|
git(root, 'add', '.');
|
|
git(root, 'commit', '-m', 'registry introduction');
|
|
const head = git(root, 'rev-parse', 'HEAD');
|
|
const evidenceFile = path.join(root, 'provider-evidence.json');
|
|
await writeFile(
|
|
evidenceFile,
|
|
JSON.stringify([
|
|
{
|
|
commit: head,
|
|
number: 7,
|
|
status: 'success',
|
|
steps: [{ name: 'gate-verify', status: 'success' }],
|
|
},
|
|
{
|
|
commit: 'other-subject',
|
|
number: 7,
|
|
status: 'success',
|
|
steps: [{ name: 'gate-verify', status: 'success' }],
|
|
},
|
|
]),
|
|
);
|
|
const previousBranch = process.env.CI_COMMIT_BRANCH;
|
|
const previousEvidence = process.env.GATE_PROVIDER_EVIDENCE_FILE;
|
|
process.env.CI_COMMIT_BRANCH = 'main';
|
|
process.env.GATE_PROVIDER_EVIDENCE_FILE = evidenceFile;
|
|
try {
|
|
const result = await verifyHistory({ root, manifest: { schemaVersion: 1 } });
|
|
assert.match(result.failures.join('\n'), /duplicate pipeline identity across commits/i);
|
|
} finally {
|
|
if (previousBranch === undefined) delete process.env.CI_COMMIT_BRANCH;
|
|
else process.env.CI_COMMIT_BRANCH = previousBranch;
|
|
if (previousEvidence === undefined) delete process.env.GATE_PROVIDER_EVIDENCE_FILE;
|
|
else process.env.GATE_PROVIDER_EVIDENCE_FILE = previousEvidence;
|
|
}
|
|
});
|
|
|
|
test('collection-wide validation rejects ambiguous gate steps on unrelated commits', () => {
|
|
const records = [
|
|
{
|
|
commit: 'target',
|
|
number: 7,
|
|
status: 'success',
|
|
steps: [{ name: 'gate-verify', status: 'success' }],
|
|
},
|
|
{
|
|
commit: 'unrelated',
|
|
number: 8,
|
|
status: 'success',
|
|
steps: [
|
|
{ name: 'gate-verify', status: 'success' },
|
|
{ name: 'gate-verify', status: 'failure' },
|
|
],
|
|
},
|
|
];
|
|
const result = assessProviderEvidence('target', records);
|
|
assert.equal(result.state, 'terminal-failure');
|
|
assert.match(result.detail, /ambiguous gate-verify step count/i);
|
|
});
|
|
|
|
test('provider evidence rejects non-object collection entries without crashing', () => {
|
|
for (const record of [null, [], 'text', 42]) {
|
|
const result = assessProviderEvidence('aaa', [record]);
|
|
assert.equal(result.state, 'terminal-failure');
|
|
assert.match(result.detail, /malformed/i);
|
|
}
|
|
});
|
|
|
|
test('provider evidence rejects duplicate pipeline identity across commits', () => {
|
|
const records = [
|
|
{
|
|
commit: 'aaa',
|
|
number: 7,
|
|
status: 'success',
|
|
steps: [{ name: 'gate-verify', status: 'success' }],
|
|
},
|
|
{
|
|
commit: 'bbb',
|
|
number: 7,
|
|
status: 'success',
|
|
steps: [{ name: 'gate-verify', status: 'success' }],
|
|
},
|
|
];
|
|
const result = assessProviderEvidence('aaa', records);
|
|
assert.equal(result.state, 'terminal-failure');
|
|
assert.match(result.detail, /duplicate pipeline.*across.*commit|global.*pipeline.*identity/i);
|
|
});
|
|
|
|
test('provider evidence distinguishes retained success, failure, and absent history', () => {
|
|
const pipelines = [
|
|
{
|
|
commit: 'aaa',
|
|
number: 1,
|
|
status: 'success',
|
|
steps: [{ name: 'gate-verify', status: 'success' }],
|
|
},
|
|
{
|
|
commit: 'bbb',
|
|
number: 2,
|
|
status: 'failure',
|
|
steps: [{ name: 'gate-verify', status: 'failure' }],
|
|
},
|
|
];
|
|
assert.deepEqual(assessProviderEvidence('aaa', pipelines), {
|
|
state: 'terminal-success',
|
|
detail: 'pipeline and gate-verify step succeeded',
|
|
});
|
|
assert.equal(assessProviderEvidence('bbb', pipelines).state, 'terminal-failure');
|
|
assert.equal(assessProviderEvidence('ccc', pipelines).state, 'absent');
|
|
});
|
|
|
|
test('duplicate gate-verify steps cannot establish provider success', () => {
|
|
const result = assessProviderEvidence('aaa', [
|
|
{
|
|
commit: 'aaa',
|
|
number: 7,
|
|
status: 'success',
|
|
steps: [
|
|
{ name: 'gate-verify', status: 'success' },
|
|
{ name: 'gate-verify', status: 'failure' },
|
|
],
|
|
},
|
|
]);
|
|
assert.equal(result.state, 'terminal-failure');
|
|
assert.match(result.detail, /ambiguous.*gate-verify/i);
|
|
});
|
|
|
|
test('a malformed single provider record cannot establish success', () => {
|
|
assert.equal(
|
|
assessProviderEvidence('aaa', [
|
|
{ commit: 'aaa', status: 'success', steps: [{ name: 'gate-verify', status: 'success' }] },
|
|
]).state,
|
|
'terminal-failure',
|
|
);
|
|
assert.equal(
|
|
assessProviderEvidence('bbb', [
|
|
{
|
|
commit: 'bbb',
|
|
number: 1,
|
|
status: 'surprising',
|
|
steps: [{ name: 'gate-verify', status: 'success' }],
|
|
},
|
|
]).state,
|
|
'terminal-failure',
|
|
);
|
|
});
|
|
|
|
test('provider evidence selects the highest numbered rerun deterministically', () => {
|
|
const failedThenSucceeded = [
|
|
{
|
|
commit: 'aaa',
|
|
number: 10,
|
|
status: 'failure',
|
|
steps: [{ name: 'gate-verify', status: 'failure' }],
|
|
},
|
|
{
|
|
commit: 'aaa',
|
|
number: 11,
|
|
status: 'success',
|
|
steps: [{ name: 'gate-verify', status: 'success' }],
|
|
},
|
|
];
|
|
const succeededThenFailed = [
|
|
{
|
|
commit: 'bbb',
|
|
number: 21,
|
|
status: 'success',
|
|
steps: [{ name: 'gate-verify', status: 'success' }],
|
|
},
|
|
{
|
|
commit: 'bbb',
|
|
number: 22,
|
|
status: 'failure',
|
|
steps: [{ name: 'gate-verify', status: 'failure' }],
|
|
},
|
|
];
|
|
assert.equal(assessProviderEvidence('aaa', failedThenSucceeded).state, 'terminal-success');
|
|
assert.equal(assessProviderEvidence('bbb', succeededThenFailed).state, 'terminal-failure');
|
|
assert.equal(
|
|
assessProviderEvidence('ccc', [
|
|
{ commit: 'ccc', status: 'success', steps: [{ name: 'gate-verify', status: 'success' }] },
|
|
{ commit: 'ccc', status: 'failure', steps: [{ name: 'gate-verify', status: 'failure' }] },
|
|
]).state,
|
|
'terminal-failure',
|
|
);
|
|
});
|