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