test: объявленный Baseline-Reviewed прогон проверяется отдельно от вершины ветки

`test/release-gate.test.mjs` строил ожидания из живого состояния репозитория, а
`localEvidence` читает `baselines.reviewedRun` из сообщения HEAD-коммита. Пока
вершиной был обычный коммит, поле оставалось `null` и всё сходилось. Как только
вершиной становился коммит приёмки эталонов, поле становилось непустым, и
`evaluateCiProof` законно требовал объявленный прогон — которого заглушка
`loadContext` не отдавала. Тест краснел на всей ветке, ради которой #573 и
писалась, и вместе с ним краснел бы `dev` сразу после слияния такой ветки.

Воспроизведено на `issue/594-form-kit-room@68bb7558`: 2776 pass / 1 fail, из-за
чего конвейер ревью не запустился вовсе.

Продуктовый код не менялся: контракт «объявленный прогон обязан существовать,
быть завершённым, не отменённым и быть Validate» — ровно то, что задумано, и он
намеренно не требует `success`, потому что приёмка эталонов по определению
ссылается на прогон, где golden покраснел.

Что изменено:

- зелёный путь теста считается на evidence с `baselines.reviewedRun: null` и
  больше не зависит от того, каким коммитом оказалась вершина;
- контракт объявленного прогона вынесен в отдельный тест и проверяется с трёх
  сторон: завершённый неотменённый Validate с `conclusion: 'failure'` — зелёно;
  прогона нет — отказ; прогон отменён — отказ;
- мутант `declared-baseline-review-run-never-checked` обнуляет объявление и
  краснеет на новом тесте.

Доказательство на фикстуре «вершина — приёмка эталонов»: старый тест 8 pass /
1 fail, новый 10 pass / 0 fail.

Issue: #595
User-Visible: no
This commit is contained in:
Claude
2026-09-19 06:12:46 +00:00
committed by claude[bot]
parent 2167d43968
commit 5b6d5d1d8f
2 changed files with 80 additions and 4 deletions
+12
View File
@@ -9937,6 +9937,18 @@ const MUTANT_DEFINITIONS = [
replace: ' const mismatch = evidenceMismatch(proof.evidence, proof.evidence);',
}],
},
{
id: 'declared-baseline-review-run-never-checked',
guard: 'node --test --test-name-pattern="#595" test/release-gate.test.mjs',
because: '#573/#595: коммит приёмки эталонов объявляет прогон, на кадры которого смотрели. '
+ 'Если объявление не резолвится, трейлер Baseline-Reviewed становится честным словом: '
+ 'сослаться можно на что угодно, включая отменённый или несуществующий прогон',
patches: [{
file: 'scripts/ci-proof.mjs',
find: ' const declared = proof.evidence.baselines?.reviewedRun ?? null;',
replace: ' const declared = null;',
}],
},
{
id: 'reused-marker-key-unchecked-against-candidate',
guard: 'node --test --test-name-pattern="#573" test/ci-proof.test.mjs',
+68 -4
View File
@@ -140,21 +140,33 @@ test('#573: ожидания считаются только на checkout ка
const own = candidateExpectations({ sha: head, root, log: (line) => notes.push(line) });
assert.deepEqual(own, localEvidence(root));
// #595: дальше ожидания строятся из `own`, но с ЯВНЫМ `baselines.reviewedRun`.
// Читать это поле из живого HEAD нельзя: `localEvidence` берёт его из сообщения
// последнего коммита, и на вершине-приёмке эталонов оно непустое. Тогда
// `evaluateCiProof` законно требует объявленный прогон, которого заглушка не
// отдавала, — и тест краснел на всей ветке, ради которой #573 и писалась.
const withoutReviewed = (evidence) => {
const copy = structuredClone(evidence);
copy.baselines.reviewedRun = null;
return copy;
};
const expected = withoutReviewed(own);
// 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,
runs: [legacy.run], repo: 'x/y', sha: SHA, tree: TREE, token: 'x', expected,
loadContext: async () => legacy.context,
});
assert.equal(verdict.status, 'missing', verdict.note);
const modern = proofContext({ id: 41 });
modern.context.proof.evidence = structuredClone(own);
modern.context.proof.evidence = structuredClone(expected);
const green = await classifyValidateProofs({
runs: [modern.run], repo: 'x/y', sha: SHA, tree: TREE, token: 'x', expected: own,
runs: [modern.run], repo: 'x/y', sha: SHA, tree: TREE, token: 'x', expected,
loadContext: async () => modern.context,
});
assert.equal(green.status, 'green', green.note);
const substituted = structuredClone(own);
const substituted = structuredClone(expected);
substituted.keys.golden = '0'.repeat(64);
const red = await classifyValidateProofs({
runs: [modern.run], repo: 'x/y', sha: SHA, tree: TREE, token: 'x', expected: substituted,
@@ -163,3 +175,55 @@ test('#573: ожидания считаются только на checkout ка
assert.equal(red.status, 'failed');
assert.match(red.note, /keys\.golden/);
});
// #595: объявленный `Baseline-Reviewed` прогон — отдельный контракт, и он обязан
// проверяться обеими сторонами, а не случайно попадать в проверку вместе с тем,
// каким коммитом оказалась вершина ветки.
test('#595: объявленный Baseline-Reviewed прогон обязан существовать, но не обязан быть зелёным', async () => {
const root = fileURLToPath(new URL('..', import.meta.url));
const head = execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
const base = candidateExpectations({ sha: head, root });
const expected = structuredClone(base);
expected.baselines.reviewedRun = 35407468491;
// Приёмка эталонов ПО ОПРЕДЕЛЕНИЮ ссылается на прогон, где golden покраснел:
// именно этот прогон и снял кадры-кандидаты. Поэтому `conclusion: 'failure'`
// здесь не поблажка, а рабочий случай — требуется лишь завершённость,
// неотменённость и то, что это Validate.
const accepted = proofContext({ id: 51 });
accepted.context.proof.evidence = structuredClone(expected);
accepted.context.reviewedRun = { run: {
databaseId: 35407468491, status: 'completed', conclusion: 'failure',
path: '.github/workflows/validate.yml',
} };
const green = await classifyValidateProofs({
runs: [accepted.run], repo: 'x/y', sha: SHA, tree: TREE, token: 'x', expected,
loadContext: async () => accepted.context,
});
assert.equal(green.status, 'green', green.note);
// А вот прогона нет вовсе — объявление не подтверждается ничем.
const orphan = proofContext({ id: 52 });
orphan.context.proof.evidence = structuredClone(expected);
orphan.context.reviewedRun = null;
const missing = await classifyValidateProofs({
runs: [orphan.run], repo: 'x/y', sha: SHA, tree: TREE, token: 'x', expected,
loadContext: async () => orphan.context,
});
assert.equal(missing.status, 'failed');
assert.match(missing.note, /Baseline-Reviewed run 35407468491/);
// И отменённый прогон объявлением тоже не считается.
const cancelled = proofContext({ id: 53 });
cancelled.context.proof.evidence = structuredClone(expected);
cancelled.context.reviewedRun = { run: {
databaseId: 35407468491, status: 'completed', conclusion: 'cancelled',
path: '.github/workflows/validate.yml',
} };
const dropped = await classifyValidateProofs({
runs: [cancelled.run], repo: 'x/y', sha: SHA, tree: TREE, token: 'x', expected,
loadContext: async () => cancelled.context,
});
assert.equal(dropped.status, 'failed');
assert.match(dropped.note, /Baseline-Reviewed run 35407468491/);
});