ci: proof различает продуктовое дерево и overlay эталонов (#573)

Приёмка эталонов на beta.3 (`ad4000f9`) стоила второго полного Validate —
22 минуты, из них 17–22 на шард мутантов. Причина одна: корпус отпечатка
(`source-fingerprint.mjs`) называет `demo/golden` строкой-каталогом, а
замыкание входов раскрывало каталог во все текстовые файлы под ним, включая
`baselines-index.json`. Индекс становился входом smoke, performance_smoke и
каждого гарда через `serve.mjs`: на реальной паре C→B ключи smoke/perf были
DIFFERENT, отпечатки 181 из 183 браузерных свидетелей менялись, журнал их не
пропускал.

- `check-inputs.mjs`: `BASELINE_OVERLAY` — раскрытие каталога не выдаёт
  overlay; явный корень golden и явная ссылка на файл — как были. На паре
  C→B: ключи smoke/perf/parity/backend same, golden DIFFERENT; отпечатки
  743 из 744 равны; план мутантов B с журналом C — 0–1 на шард вместо 38–44
- `ci-proof.mjs`: составное evidence — product tree без overlay, overlay
  (tree, sha256 индекса, run из `Baseline-Reviewed`), content-ключи всех
  реюзных job (исполненных тоже); `evaluateCiProof({expected, reviewedRun})`
  сверяет с локальным расчётом, fail-closed на ключ, tree, индекс, reviewed
  run, маркер с чужим ключом; proof без evidence при ожиданиях — stale
- `release-gate.mjs` / `release-prerelease.mjs`: ожидания считаются на
  checkout кандидата (`candidateExpectations`), чужой checkout — notice
- мутанты: `baseline-overlay-leaks-into-every-key`,
  `proof-trusts-evidence-it-could-verify`,
  `reused-marker-key-unchecked-against-candidate`,
  `product-tree-identity-counts-baselines`; перенацелен
  `ci-proof-ignores-run-attempt`
- docs: TESTING (правило overlay), DEVELOPMENT (evidence в release proof),
  STATUS

Issue: #573
User-Visible: no
This commit is contained in:
Claude
2026-09-17 22:00:15 +03:00
parent 06bf9b6921
commit 47f36e571c
14 changed files with 578 additions and 29 deletions
+45 -3
View File
@@ -1,11 +1,11 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import {
CHECKS, CHECK_NAMES, NOT_AN_INPUT, REUSE_JOBS, checksAffectedBy, closure, coverage, globToRegExp,
inputsOf, isDeclaredNotAnInput, isExecutableInput, manifest, referencesOf, stripComments,
BASELINE_OVERLAY, CHECKS, CHECK_NAMES, NOT_AN_INPUT, REUSE_JOBS, checksAffectedBy, closure, coverage,
globToRegExp, inputsOf, isBaselineOverlay, isDeclaredNotAnInput, isExecutableInput, manifest, referencesOf,
stripComments,
} from '../scripts/check-inputs.mjs';
// Единый manifest входов (#492 §5). Две группы доказательств: чистая механика
@@ -103,6 +103,48 @@ test('замыкание: код транзитивно, данные — лис
assert.equal(parents.get('scripts/helper.mjs'), 'demo/compat.mjs');
});
// #573: overlay принятых эталонов принадлежит только golden. Строка-каталог
// `demo/golden` в корпусе отпечатка раскрывалась во ВСЕ текстовые файлы под
// ним, и индекс эталонов становился входом smoke, perf и каждого гарда через
// serve.mjs — приёмка 13 кадров на beta.3 перегнала 22 минуты чужой работы.
test('#573: раскрытие каталога не выдаёт overlay эталонов; явный корень и явная ссылка — выдают', () => {
const files = {
'demo/smoke_a.mjs': "import './serve.mjs';\n",
'demo/serve.mjs': "import '../scripts/source-fingerprint.mjs';\n",
'scripts/source-fingerprint.mjs': "const corpus = ['demo/fixtures', 'demo/golden'];\n",
'demo/golden/run.mjs': "const baselineRoot = 'demo/golden/baselines';\n",
'demo/golden/matrix.mjs': 'export const GOLDEN_SCENARIOS = [];\n',
'demo/golden/baselines/baselines-index.json': '{"scenarios":{}}',
'demo/golden/baselines/scene.png': 'binary',
'demo/golden/baselines/.gitkeep': '',
'test/golden-index.test.mjs': "const index = 'demo/golden/baselines/baselines-index.json';\n",
};
const tracked = Object.keys(files).sort();
const viaSmoke = closure('/virtual', ['demo/smoke_a.mjs'], { tracked, read: (f) => files[f] });
assert.ok(viaSmoke.includes('demo/golden/matrix.mjs'), 'код под demo/golden — по-прежнему вход');
assert.ok(!viaSmoke.some(isBaselineOverlay), `overlay не течёт через каталог: ${viaSmoke.join(', ')}`);
// golden сама называет каталог эталонов строкой — и всё равно получает их не
// раскрытием, а явным корнем manifest (CHECKS.golden.roots)
const viaGolden = closure('/virtual', ['demo/golden/run.mjs'], { tracked, read: (f) => files[f] });
assert.ok(!viaGolden.some(isBaselineOverlay), 'каталог overlay по строке — тоже не раскрывается');
assert.ok(CHECKS.golden.roots.includes('demo/golden/**'), 'эталоны входят в golden корнем');
// явная ссылка на файл overlay — честная зависимость, она остаётся
const viaTest = closure('/virtual', ['test/golden-index.test.mjs'], { tracked, read: (f) => files[f] });
assert.ok(viaTest.includes('demo/golden/baselines/baselines-index.json'));
assert.deepEqual(BASELINE_OVERLAY, ['demo/golden/baselines/**']);
assert.equal(isBaselineOverlay('demo/golden/baselines/baselines-index.json'), true);
assert.equal(isBaselineOverlay('demo/golden/matrix.mjs'), false);
});
test('#573: на живом дереве индекс эталонов — вход golden и ничьей другой реюзной job', () => {
// путь собран из кусков: литерал сделал бы индекс входом frontend через этот тест
const index = p('demo', 'golden', 'baselines', 'baselines-index.json');
for (const check of ['smoke', 'performance_smoke', 'geometry_parity', 'backend']) {
assert.ok(!MANIFEST[check].has(index), `${check} не читает эталоны`);
}
assert.ok(MANIFEST.golden.has(index));
});
test('замыкание останавливается на копиях бандла (класс D)', () => {
const files = {
'demo/smoke_a.mjs': "import '../custom_components/houseplan/frontend/houseplan-card.js';\n",
+146 -2
View File
@@ -2,10 +2,14 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import { deflateRawSync } from 'node:zlib';
import { fileURLToPath } from 'node:url';
import {
CI_PROOF_POLICIES, buildCiProof, evaluateCiProof, parseReuseMarker, readCiProofArtifact,
requiredCheckIds, selectCiProofVerdict,
CI_PROOF_POLICIES, baselineReviewedRun, buildCiProof, evaluateCiProof, localEvidence, parseReuseMarker,
productTreeId, readCiProofArtifact, requiredCheckIds, selectCiProofVerdict,
} from '../scripts/ci-proof.mjs';
import { REUSE_JOBS } from '../scripts/check-inputs.mjs';
import { reuseKey } from '../scripts/gate-reuse.mjs';
export const SHA = 'a'.repeat(40);
export const TREE = 'b'.repeat(40);
@@ -210,3 +214,143 @@ test('#541: uploaded deflated artifact is read without an external ZIP dependenc
const zip = Buffer.concat([local, name, compressed, central, name, eocd]);
assert.deepEqual(readCiProofArtifact(zip), { schema: 'test', ok: true });
});
// #573 — составное evidence. Proof называет отдельно продуктовое дерево,
// overlay принятых эталонов и content-ключи реюзных job; потребитель с
// checkout кандидата сверяет их, а не верит. Сценарий beta.3: кандидат C
// красный только по golden, baseline-only коммит B переиспользует зелёные
// job C и перегоняет golden.
const KEYS = Object.fromEntries(REUSE_JOBS.map((job, index) => [job, String(index + 1).repeat(64)]));
const evidenceOf = (over = {}) => ({
product: { tree: 'p'.repeat(64) },
baselines: { tree: 'c'.repeat(40), manifestSha256: 'd'.repeat(64), reviewedRun: 34853080375 },
keys: { ...KEYS },
...over,
});
/** B: smoke и performance_smoke reused из красного-по-golden C, golden исполнена. */
function baselineOnlyFixture() {
const fixture = proofFixture({ id: 20, attempt: 1 });
const evidence = evidenceOf();
const reuseFrom = (id) => ({
mode: 'reused', result: 'success',
reuse: { key: evidence.keys[id], sourceRun: 10, sourceAttempt: 1, sourceSha: 'e'.repeat(40) },
});
fixture.proof.checks.smoke = reuseFrom('smoke');
fixture.proof.checks.performance_smoke = reuseFrom('performance_smoke');
fixture.proof.checks.golden.key = evidence.keys.golden;
fixture.proof.checks.geometry_parity.key = evidence.keys.geometry_parity;
fixture.proof.checks.backend.key = evidence.keys.backend;
fixture.proof.executedChecks = fixture.proof.executedChecks.filter((id) => !['smoke', 'performance_smoke'].includes(id));
fixture.proof.reusedChecks = ['performance_smoke', 'smoke'];
fixture.proof.evidence = evidence;
fixture.jobs = fixture.jobs.filter((job) => !/Смоки|Перф-смок/.test(job.name));
// C: run красный (golden different), но smoke и perf — зелёные job
fixture.reuseRuns.set('10:1', {
run: { id: 10, run_attempt: 1, status: 'completed', conclusion: 'failure', head_sha: 'e'.repeat(40) },
jobs: [...smokeJobs(), success(names.smokeDone), success(names.performance),
{ name: names.golden, conclusion: 'failure' }, ...mutantJobs()],
});
fixture.reviewedRun = { run: { id: 34853080375, path: '.github/workflows/validate.yml', status: 'completed', conclusion: 'failure' } };
fixture.expected = evidenceOf();
return fixture;
}
test('#573 AC1: baseline-only коммит — reused smoke/perf из красного-по-golden кандидата, golden исполнена, proof green', () => {
const fixture = baselineOnlyFixture();
const verdict = evaluateCiProof({ ...fixture, policy: CI_PROOF_POLICIES.release });
assert.equal(verdict.status, 'green', verdict.note);
assert.deepEqual(fixture.proof.reusedChecks, ['performance_smoke', 'smoke']);
assert.ok(fixture.proof.executedChecks.includes('golden'), 'golden сравнивает с новыми эталонами и перегоняется всегда');
// те же семантики без ожиданий — review/merge потребители не ломаются
assert.equal(evaluateCiProof({ ...fixture, expected: null, reviewedRun: undefined, policy: CI_PROOF_POLICIES.merge }).status, 'green');
});
test('#573 AC3: красный smoke кандидата не прячется за зелёной golden — источник reuse обязан быть зелёной job', () => {
const fixture = baselineOnlyFixture();
fixture.reuseRuns.get('10:1').jobs = fixture.reuseRuns.get('10:1').jobs
.map((job) => (job.name.startsWith('Смоки в браузере (шард 2') ? { ...job, conclusion: 'failure' } : job));
const verdict = evaluateCiProof({ ...fixture, policy: CI_PROOF_POLICIES.release });
assert.equal(verdict.status, 'failed');
assert.match(verdict.note, /smoke: source run does not verify/);
});
test('#573 AC4: подмена content-ключа, product tree, overlay, индекса или reviewed run — fail-closed', () => {
const fixture = baselineOnlyFixture();
const withExpected = (over) => evaluateCiProof({
...fixture, expected: evidenceOf(over), policy: CI_PROOF_POLICIES.release,
});
assert.equal(withExpected({}).status, 'green');
assert.match(withExpected({ keys: { ...KEYS, smoke: 'f'.repeat(64) } }).note, /keys\.smoke/);
assert.equal(withExpected({ keys: { ...KEYS, smoke: 'f'.repeat(64) } }).status, 'failed');
assert.match(withExpected({ product: { tree: 'q'.repeat(64) } }).note, /product\.tree/);
assert.match(withExpected({ baselines: { tree: 'x'.repeat(40), manifestSha256: 'd'.repeat(64), reviewedRun: 34853080375 } }).note, /baselines\.tree/);
assert.match(withExpected({ baselines: { tree: 'c'.repeat(40), manifestSha256: 'y'.repeat(64), reviewedRun: 34853080375 } }).note, /manifestSha256/);
assert.match(withExpected({ baselines: { tree: 'c'.repeat(40), manifestSha256: 'd'.repeat(64), reviewedRun: 1 } }).note, /reviewedRun/);
// маркер реюза ссылается на ключ, отличный от ключа кандидата — внутренняя несогласованность
const tampered = structuredClone(fixture.proof);
tampered.checks.smoke.reuse.key = 'a'.repeat(64);
assert.match(evaluateCiProof({ ...fixture, proof: tampered, policy: CI_PROOF_POLICIES.release }).note, /smoke: reused marker key differs/);
// объявленный run просмотра кадров не существует / отменён / не Validate
assert.match(evaluateCiProof({ ...fixture, reviewedRun: null, policy: CI_PROOF_POLICIES.release }).note, /Baseline-Reviewed run 34853080375 is missing/);
assert.equal(evaluateCiProof({
...fixture, reviewedRun: { run: { id: 34853080375, path: '.github/workflows/validate.yml', status: 'completed', conclusion: 'cancelled' } },
policy: CI_PROOF_POLICIES.release,
}).status, 'failed');
assert.equal(evaluateCiProof({
...fixture, reviewedRun: { run: { id: 34853080375, path: '.github/workflows/nightly.yml', status: 'completed', conclusion: 'success' } },
policy: CI_PROOF_POLICIES.release,
}).status, 'failed');
// proof без evidence при наличии ожиданий — устарел, а не «сойдёт»
const legacy = structuredClone(fixture.proof);
delete legacy.evidence;
assert.equal(evaluateCiProof({ ...fixture, proof: legacy, policy: CI_PROOF_POLICIES.release }).status, 'stale');
});
test('#573: identity продуктового дерева не видит overlay эталонов, но видит всё остальное', () => {
const lines = [
'100644 blob 1111\tsrc/logic.ts',
'100644 blob 2222\tdemo/golden/matrix.mjs',
'100644 blob 3333\tdemo/golden/baselines/scene.png',
'100644 blob 4444\tdemo/golden/baselines/baselines-index.json',
];
const before = productTreeId(lines.join('\n'));
const accepted = productTreeId(lines.map((line) => line.replace('3333', '5555').replace('4444', '6666')).join('\n'));
assert.equal(accepted, before, 'приёмка эталонов — то же продуктовое дерево');
assert.notEqual(productTreeId(lines.map((line) => line.replace('1111', '9999')).join('\n')), before);
assert.notEqual(productTreeId(lines.map((line) => line.replace('2222', '9999')).join('\n')), before, 'сцены — продукт');
assert.throws(() => productTreeId(''), /empty/);
assert.equal(baselineReviewedRun('Accept frames\n\nBaseline-Reviewed: https://github.com/x/y/actions/runs/777\n'), 777);
assert.equal(baselineReviewedRun('no trailer'), null);
assert.throws(() => baselineReviewedRun('Baseline-Reviewed: somewhere-else\n'), /does not name an actions run/);
});
test('#573: buildCiProof пишет ключ исполненной реюзной job и отвергает маркер с чужим ключом', () => {
const evidence = evidenceOf();
const { proof } = proofFixture({ id: 30 });
const needs = { preflight: { result: 'success' }, changes: { result: 'success', outputs: { heavy: 'true', mutants_requested: 'true', frontend: 'true', backend: 'true', geometry_parity: 'true', integration: 'true' } },
reuse: { result: 'success', outputs: { smoke: 'true', smoke_key: evidence.keys.smoke, smoke_source_run: '10', smoke_source_attempt: '1', smoke_source_sha: 'e'.repeat(40) } },
frontend: { result: 'success' }, hacs: { result: 'success' }, hassfest: { result: 'success' }, changed_mutants: { result: 'success' },
smoke: { result: 'skipped' }, smoke_done: { result: 'skipped' }, golden: { result: 'success' }, performance_smoke: { result: 'success' },
geometry_parity: { result: 'success' }, backend: { result: 'success' } };
const built = buildCiProof({ candidateSha: SHA, candidateTree: TREE, runId: 30, attempt: 1, event: 'push', needs, evidence });
assert.equal(built.checks.golden.key, evidence.keys.golden, 'исполненная job несёт свой content-ключ');
assert.equal(built.checks.smoke.mode, 'reused');
assert.deepEqual(built.evidence, evidence);
assert.ok(!proof.evidence, 'без evidence блока нет — обратная совместимость записи');
const foreign = { ...needs, reuse: { ...needs.reuse, outputs: { ...needs.reuse.outputs, smoke_key: 'f'.repeat(64) } } };
assert.throws(() => buildCiProof({ candidateSha: SHA, candidateTree: TREE, runId: 30, attempt: 1, event: 'push', needs: foreign, evidence }),
/smoke: reuse marker key .* differs from the candidate key/);
});
test('#573: evidence живого дерева считается детерминированно и совпадает с ключами gate-reuse', () => {
const root = fileURLToPath(new URL('..', import.meta.url));
const first = localEvidence(root);
const second = localEvidence(root, { keys: first.keys });
assert.deepEqual(second, first);
assert.match(first.product.tree, /^[0-9a-f]{64}$/);
assert.match(first.baselines.tree, /^[0-9a-f]{40}$/);
assert.match(first.baselines.manifestSha256, /^[0-9a-f]{64}$/);
for (const job of REUSE_JOBS) assert.equal(first.keys[job], reuseKey(root, job));
assert.throws(() => localEvidence(root, { keys: { ...first.keys, smoke: 'f'.repeat(64) } }), /smoke: reuse job key/);
});
+28
View File
@@ -143,6 +143,34 @@ test('a behaviour input changes every browser key and a version bump changes all
}
});
// #573 — воспроизведение beta.3 на синтетическом дереве: кандидат C и
// baseline-only коммит B отличаются только overlay эталонов (PNG + индекс).
// Корпус отпечатка называет `demo/golden` каталогом ровно как настоящий
// source-fingerprint.mjs; до #573 это делало индекс входом smoke и perf.
test('#573: приёмка эталонов меняет только ключ golden — smoke, perf, parity и backend переиспользуются', () => {
const { dir, put } = makeTree();
try {
put('scripts/source-fingerprint.mjs', "const corpus = ['demo/fixtures', 'demo/golden'];\nexport const fp = 1;\n");
put('demo/bundle-freshness.mjs', "import '../scripts/source-fingerprint.mjs';\nexport const fresh = 1;\n");
put('demo/golden/baselines/baselines-index.json', '{"scenarios":{"one":"a"}}\n');
const candidate = keys(dir);
// B: 13 кадров и индекс переписаны, продукт не тронут
put('demo/golden/baselines/one.png', Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0x02]));
put('demo/golden/baselines/baselines-index.json', '{"scenarios":{"one":"b"},"acceptedAt":"later"}\n');
const accepted = keys(dir);
for (const job of JOBS) {
if (job === 'golden') assert.notEqual(accepted[job], candidate[job], 'golden сравнивает с эталонами и обязана перегоняться');
else assert.equal(accepted[job], candidate[job], `${job}: входы побайтово те же, что у кандидата`);
}
// но правка продукта вместе с эталонами перегоняет всё, что собирает бандл
put('src/card.ts', "export const CARD_VERSION = '1.0.0'; export const changed = true;\n");
const both = keys(dir);
for (const job of ['smoke', 'golden', 'performance_smoke']) assert.notEqual(both[job], accepted[job], `${job} видит правку исходника`);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('harness edits are isolated to their own job (#208)', () => {
const { dir, put } = makeTree();
const only = (changed) => {
+29
View File
@@ -446,6 +446,35 @@ test('#481 AC1: отпечаток свидетеля не меняется от
assert.notEqual(witnessFingerprint({ ...LEDGER_MUTANT, patches: [{ file: 'src/x.ts', find: 'a', replace: 'c' }] }, base), fp, 'объявление патча');
});
// #573. Гард-смок доходит до `source-fingerprint.mjs`, а тот называет корпус
// строкой-каталогом `demo/golden`. До #573 раскрытие каталога делало индекс
// эталонов входом каждого такого гарда: приёмка кадров на beta.3 сменила
// отпечатки 181 из 183 браузерных свидетелей, и журнал их не пропустил.
test('#573: приёмка эталонов не меняет отпечаток свидетеля со смок-гардом', () => {
const tree = (index) => ({
'src/x.ts': 'let a = 1;',
'demo/smoke_x.mjs': "import './serve.mjs';\n",
'demo/serve.mjs': "import '../scripts/source-fingerprint.mjs';\n",
'scripts/source-fingerprint.mjs': "const corpus = ['demo/fixtures', 'demo/golden'];\n",
'demo/golden/matrix.mjs': 'export const GOLDEN_SCENARIOS = [];\n',
'demo/golden/baselines/baselines-index.json': index,
});
const fsOf = (files) => ({
root: '/repo',
read: (file) => files[file] ?? '',
exists: (file) => file in files,
normalize: (text) => text,
inputsOf: (guard) => guardInputs(guard, { exists: (f) => f in files, read: (f) => files[f] ?? '', files: Object.keys(files).sort() }),
});
const mutant = { id: 'x', guard: 'node demo/smoke_x.mjs', patches: [{ file: 'src/x.ts', find: 'a', replace: 'b' }] };
const candidate = witnessFingerprint(mutant, fsOf(tree('{"scenarios":{"one":"a"}}')));
const accepted = witnessFingerprint(mutant, fsOf(tree('{"scenarios":{"one":"b"},"acceptedAt":"later"}')));
assert.equal(accepted, candidate, 'overlay эталонов — не вход смок-гарда');
const harness = tree('{"scenarios":{"one":"a"}}');
harness['demo/golden/matrix.mjs'] = 'export const GOLDEN_SCENARIOS = [1];\n';
assert.notEqual(witnessFingerprint(mutant, fsOf(harness)), candidate, 'код под demo/golden — по-прежнему вход');
});
// #518. Хост-файлы карты — тринадцать тысяч строк. Отпечаток и отбор по файлу
// целиком означали, что правка в одном их конце перегоняет свидетелей из
// другого: на #500 двенадцать изменённых строк тянули 53 мутанта из 75.
+41 -2
View File
@@ -1,10 +1,12 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import {
classifyValidateProofs, classifyValidateRuns, latestRelevantRun, workflowRunsUrl,
candidateExpectations, classifyValidateProofs, classifyValidateRuns, latestRelevantRun, workflowRunsUrl,
} from '../scripts/release-gate.mjs';
import { buildCiProof } from '../scripts/ci-proof.mjs';
import { buildCiProof, localEvidence } from '../scripts/ci-proof.mjs';
const SHA = 'a'.repeat(40);
const TREE = 'b'.repeat(40);
@@ -124,3 +126,40 @@ test('#541: the release documents describe proof semantics', () => {
const performance = readFileSync(new URL('../demo/performance/README.md', import.meta.url), 'utf8');
assert.match(performance, /latest\nnon-cancelled run on the SHA/);
});
// #573: гейт релиза стоит на checkout кандидата и сверяет составное evidence
// proof с тем, что считает сам; чужой checkout — честное «проверяю только по
// GitHub», а не молчаливый пропуск.
test('#573: ожидания считаются только на checkout кандидата и уходят в classifyValidateProofs', async () => {
const root = fileURLToPath(new URL('..', import.meta.url));
const head = execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
const notes = [];
const foreign = candidateExpectations({ sha: 'f'.repeat(40), root, log: (line) => notes.push(line) });
assert.equal(foreign, null);
assert.match(notes[0], /is not the candidate ffffffff — proof evidence is verified against GitHub only/);
const own = candidateExpectations({ sha: head, root, log: (line) => notes.push(line) });
assert.deepEqual(own, localEvidence(root));
// proof без evidence при наличии ожиданий — stale, а старее его нет → missing; с evidence и совпадением — green
const legacy = proofContext({ id: 40 });
const verdict = await classifyValidateProofs({
runs: [legacy.run], repo: 'x/y', sha: SHA, tree: TREE, token: 'x', expected: own,
loadContext: async () => legacy.context,
});
assert.equal(verdict.status, 'missing', verdict.note);
const modern = proofContext({ id: 41 });
modern.context.proof.evidence = structuredClone(own);
const green = await classifyValidateProofs({
runs: [modern.run], repo: 'x/y', sha: SHA, tree: TREE, token: 'x', expected: own,
loadContext: async () => modern.context,
});
assert.equal(green.status, 'green', green.note);
const substituted = structuredClone(own);
substituted.keys.golden = '0'.repeat(64);
const red = await classifyValidateProofs({
runs: [modern.run], repo: 'x/y', sha: SHA, tree: TREE, token: 'x', expected: substituted,
loadContext: async () => modern.context,
});
assert.equal(red.status, 'failed');
assert.match(red.note, /keys\.golden/);
});