mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
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:
@@ -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',
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user