mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 06:59:46 +00:00
fix(ci): give nightly mutation shards headroom and report cancellation accurately
Split the full registry into ten shards without increasing the one-hour limit. Preserve summary and step outcome separately so cancellation after 194/194 remains red without claiming the summary was missing. Issue: #795 User-Visible: no
This commit is contained in:
@@ -158,8 +158,8 @@ test('#604: лог без итоговой строки — прерван, а
|
||||
assert.deepEqual(report.shards.map((s) => s.status), ['ok', 'interrupted']);
|
||||
assert.equal(report.failed, true, 'обрыв — отказ: до сбежавших могли не дойти');
|
||||
assert.deepEqual(report.escaped, [], 'сбежавших из обрывка не выдумывается');
|
||||
assert.match(report.body, /\| 2 \| \*\*прерван — лог без итоговой строки \(таймаут или отмена\)\*\* \|/);
|
||||
assert.match(report.body, /Шарды 2 прерваны до итоговой строки/);
|
||||
assert.match(report.body, /\| 2 \| \*\*прерван — лог без итоговой строки; успешное завершение не доказано\*\* \|/);
|
||||
assert.match(report.body, /Прерванные шарды не дают полного доказательства/);
|
||||
assert.match(telegramSummary(report, 'https://x/issues/9'), /2:interrupted/);
|
||||
});
|
||||
|
||||
@@ -176,6 +176,43 @@ test('#604: исход шага cancelled прерывает шард даже
|
||||
assert.deepEqual(parsed.shards.map((s) => s.status), ['interrupted', 'interrupted', 'failed', 'failed', 'ok', 'ok']);
|
||||
});
|
||||
|
||||
test('#795: cancelled с итогами 194/194 остаётся отказом с точной причиной', () => {
|
||||
const report = mutationGateReport({ ...meta, guards, logs: [
|
||||
{ shard: 4, text: 'поймано 194 из 194\n', outcome: 'cancelled' },
|
||||
] });
|
||||
assert.equal(report.failed, true);
|
||||
assert.equal(report.shards[0].status, 'interrupted');
|
||||
assert.match(report.body, /шаг cancelled \(таймаут или отмена\)/);
|
||||
assert.match(report.body, /итог в логе 194\/194 не подтверждает успешное завершение/);
|
||||
assert.doesNotMatch(report.body, /без итоговой строки|до итоговой строки|Сбежавших там не разобрано/);
|
||||
assert.match(telegramSummary(report, 'https://x/issues/795'), /4:interrupted/);
|
||||
});
|
||||
|
||||
test('#795: причины прерывания не смешивают отмену, пропуск и отсутствие итогов', () => {
|
||||
for (const [outcome, text, expected] of [
|
||||
['cancelled', 'ok alpha-mutant: заявленный тест покраснел на мутанте\n', /шаг cancelled \(таймаут или отмена\); лог без итоговой строки/],
|
||||
['skipped', 'поймано 3 из 3\n', /шаг skipped; итог в логе 3\/3 не подтверждает успешное завершение/],
|
||||
['success', '', /лог без итоговой строки; успешное завершение не доказано/],
|
||||
]) {
|
||||
const report = mutationGateReport({ ...meta, guards, logs: [{ shard: 1, text, outcome }] });
|
||||
assert.equal(report.failed, true);
|
||||
assert.match(report.body, expected);
|
||||
if (outcome !== 'cancelled') assert.doesNotMatch(report.body, /таймаут или отмена/);
|
||||
}
|
||||
});
|
||||
|
||||
test('#795: десять evidence обязательны — девять или чужой десятый не доказывают весь реестр', () => {
|
||||
const expected = { ...expectedEvidence, shardCount: 10 };
|
||||
const rows = Array.from({ length: 10 }, (_, i) => evidenceRow(i + 1, { shardCount: 10, outcome: 'success' }));
|
||||
assert.equal(validateMutationShardEvidence(rows, expected).ok, true);
|
||||
const missing = validateMutationShardEvidence(rows.slice(0, 9), expected);
|
||||
assert.equal(missing.ok, false);
|
||||
assert.ok(missing.errors.includes('shard 10: evidence is missing'));
|
||||
const foreign = validateMutationShardEvidence([...rows.slice(0, 9), evidenceRow(10, { shardCount: 10, materialSha: 'd'.repeat(40) })], expected);
|
||||
assert.equal(foreign.ok, false);
|
||||
assert.ok(foreign.errors.includes('shard 10: foreign material SHA'));
|
||||
});
|
||||
|
||||
test('#604: evidence несёт исход шага, агрегатор отвергает прерванный шард как неполный', () => {
|
||||
const rows = [1, 2, 3, 4].map((shard) => evidenceRow(shard, { outcome: shard === 2 ? 'cancelled' : 'success' }));
|
||||
assert.equal(rows[1].evidence.outcome, 'cancelled');
|
||||
|
||||
@@ -442,23 +442,26 @@ test('#472 AC2 / #549: каждый шард сохраняет лог и identi
|
||||
assert.match(mutationWorkflow, /--write-evidence=.*evidence\.json/);
|
||||
});
|
||||
|
||||
// #604. Шард 2/4 снят по timeout-minutes: реестр вырос до 810 мутантов, ~203 на
|
||||
// шард, 55–61 мин при потолке 60. Шардов шесть, и делитель повторяется в
|
||||
// четырёх местах workflow — тест не даёт им разойтись при следующем делении.
|
||||
test('#604: делитель шардов ночного прогона один во всех местах workflow, шардов шесть', () => {
|
||||
// #795: на шести шардах ночь снова дошла до часового потолка. Контракт
|
||||
// проверяет не только одинаковые делители, но и полный охват реальной раскладкой.
|
||||
test('#795: десять ночных шардов согласованы с runner/evidence и покрывают реестр ровно один раз', () => {
|
||||
const matrix = /shard: \[([0-9, ]+)\]/.exec(mutationWorkflow);
|
||||
assert.ok(matrix, 'matrix.shard объявлена списком');
|
||||
const shards = matrix[1].split(',').map((n) => Number(n.trim()));
|
||||
assert.deepEqual(shards, [1, 2, 3, 4, 5, 6]);
|
||||
assert.deepEqual(shards, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
const n = shards.length;
|
||||
assert.match(mutationWorkflow, new RegExp(`\\(шард \\$\\{\\{ matrix\\.shard \\}\\} из ${n}\\)`), 'имя job');
|
||||
assert.match(mutationWorkflow, new RegExp(`--shard=\\$\\{\\{ matrix\\.shard \\}\\}/${n} `), 'делитель раннера');
|
||||
// Только строки команд: комментарий про `--shards=6` — не место вызова.
|
||||
// Только строки команд, не комментарии.
|
||||
const shardsArgs = mutationWorkflow.split('\n').filter((line) => !/^\s*#/.test(line))
|
||||
.flatMap((line) => line.match(/--shards=\d+/g) || []);
|
||||
assert.equal(shardsArgs.length, 3, 'evidence шарда, агрегатор, отчёт');
|
||||
assert.ok(shardsArgs.every((arg) => arg === `--shards=${n}`), shardsArgs.join(' '));
|
||||
assert.ok(!/--shard=\$\{\{ matrix\.shard \}\}\/4\b|--shards=4\b|из 4\)/.test(mutationWorkflow), 'старый делитель 4 не остался');
|
||||
const scheduled = shards.flatMap((shard) => shardMutants(MUTANTS, shard, n).map((m) => m.id));
|
||||
assert.deepEqual(scheduled.sort(), MUTANTS.map((m) => m.id).sort(), 'нет пропусков и повторов');
|
||||
for (const shard of shards) {
|
||||
assert.ok(shardMutants(MUTANTS, shard, n).length <= Math.ceil(MUTANTS.length / n));
|
||||
}
|
||||
assert.match(mutationWorkflow, /timeout-minutes: 60/, 'потолок остаётся стражем от зависшего Chromium');
|
||||
});
|
||||
|
||||
@@ -488,7 +491,7 @@ test('#549: moving ref фиксируется один раз, а каждый
|
||||
assert.ok(!mutants.includes("inputs.ref || 'dev'"), 'шарды не должны независимо читать moving ref');
|
||||
});
|
||||
|
||||
test('#549: агрегатор требует четыре evidence одного material и report не перечитывает dev', () => {
|
||||
test('#549: агрегатор требует evidence одного material и report не перечитывает dev', () => {
|
||||
const evidence = mutationWorkflow.slice(mutationWorkflow.indexOf(' evidence:'), mutationWorkflow.indexOf(' report:'));
|
||||
const report = mutationWorkflow.slice(mutationWorkflow.indexOf('\n report:\n'));
|
||||
assert.match(evidence, /--verify-only/);
|
||||
|
||||
Reference in New Issue
Block a user