mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
166 lines
8.8 KiB
JavaScript
166 lines
8.8 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import {
|
||
REPORT_TITLE_MARKER, mutationGateReport, mutationShardEvidence, parseShardLogs,
|
||
telegramSummary, validateMutationShardEvidence,
|
||
} from '../scripts/mutation-gate-report.mjs';
|
||
|
||
// #472. Еженедельный полный прогон падал дважды подряд, и никто не смотрел:
|
||
// у отказа не было адресата. Отчёт обязан назвать сбежавших так, чтобы
|
||
// команда воспроизведения из письма работала, — а не выдумывать сущности из
|
||
// строк, которые выглядят похоже.
|
||
|
||
const KNOWN = ['alpha-mutant', 'beta-mutant', 'gamma-mutant'];
|
||
const guards = new Map([
|
||
['alpha-mutant', 'node --test test/a.test.mjs'],
|
||
['beta-mutant', 'node demo/smoke_b.mjs'],
|
||
['gamma-mutant', 'node --test test/g.test.mjs'],
|
||
]);
|
||
const meta = { runUrl: 'https://x/runs/1', ref: 'dev', sha: 'abcdef1234567890', date: '2026-09-08' };
|
||
const A = 'a'.repeat(40);
|
||
const B = 'b'.repeat(40);
|
||
const C = 'c'.repeat(40);
|
||
const expectedEvidence = {
|
||
materialSha: A, materialTree: B, workflowSha: C,
|
||
runId: 549, runAttempt: 2, shardCount: 4,
|
||
};
|
||
const evidenceRow = (shard, overrides = {}) => ({
|
||
file: `attempt-${overrides.runAttempt || 1}/shard-${shard}/evidence.json`,
|
||
evidence: mutationShardEvidence({
|
||
...expectedEvidence, shard, runAttempt: 1, ...overrides,
|
||
}),
|
||
});
|
||
|
||
test('#549: четыре шарда одного material образуют доказанный результат', () => {
|
||
const result = validateMutationShardEvidence(
|
||
[1, 2, 3, 4].map((shard) => evidenceRow(shard)), expectedEvidence,
|
||
);
|
||
assert.equal(result.ok, true);
|
||
assert.deepEqual(result.selected.map((row) => row.evidence.shard), [1, 2, 3, 4]);
|
||
});
|
||
|
||
test('#549: foreign SHA и отсутствующий шард отвергаются fail-closed', () => {
|
||
const rows = [evidenceRow(1), evidenceRow(2, { materialSha: 'd'.repeat(40) }), evidenceRow(4)];
|
||
const result = validateMutationShardEvidence(rows, expectedEvidence);
|
||
assert.equal(result.ok, false);
|
||
assert.ok(result.errors.some((error) => error.includes('foreign material SHA')));
|
||
assert.ok(result.errors.some((error) => error.includes('shard 3: evidence is missing')));
|
||
});
|
||
|
||
test('#549: partial retry выбирает новый attempt, но сохраняет единый material', () => {
|
||
const rows = [1, 2, 3, 4].map((shard) => evidenceRow(shard));
|
||
rows.push(evidenceRow(2, { runAttempt: 2 }));
|
||
const result = validateMutationShardEvidence(rows, expectedEvidence);
|
||
assert.equal(result.ok, true);
|
||
assert.equal(result.selected.find((row) => row.evidence.shard === 2).evidence.runAttempt, 2);
|
||
assert.equal(result.selected.find((row) => row.evidence.shard === 1).evidence.runAttempt, 1);
|
||
});
|
||
|
||
test('#549: partial retry с другим material не склеивается со старыми шардами', () => {
|
||
const rows = [1, 2, 3, 4].map((shard) => evidenceRow(shard));
|
||
rows.push(evidenceRow(2, { runAttempt: 2, materialSha: 'd'.repeat(40) }));
|
||
const result = validateMutationShardEvidence(rows, expectedEvidence);
|
||
assert.equal(result.ok, false);
|
||
assert.ok(result.errors.some((error) => error.includes('foreign material SHA')));
|
||
});
|
||
|
||
test('сбежавшие собираются из нескольких шардов без дублей и по порядку (#472 AC3)', () => {
|
||
const report = mutationGateReport({ ...meta, guards, logs: [
|
||
{ shard: 1, text: 'ok x\nFAIL beta-mutant: тест остался зелёным на сломанном коде\n guard: node demo/smoke_b.mjs\n' },
|
||
{ shard: 2, text: 'FAIL alpha-mutant: тест остался зелёным на сломанном коде\nFAIL beta-mutant: тест остался зелёным на сломанном коде\n' },
|
||
{ shard: 3, text: 'ok gamma-mutant: тест покраснел, как обязан\n' },
|
||
] });
|
||
assert.deepEqual(report.escaped, ['alpha-mutant', 'beta-mutant']);
|
||
assert.deepEqual(report.shards.map((s) => s.status), ['failed', 'failed', 'ok']);
|
||
assert.equal(report.failed, true);
|
||
assert.match(report.body, /--id=alpha-mutant/);
|
||
assert.match(report.body, /guard: `node demo\/smoke_b\.mjs`/);
|
||
});
|
||
|
||
test('красный гард без мутанта — не сбежавший мутант (#472 AC3, ревью r1)', () => {
|
||
// runCleanGuards пишет в тот же лог. Наивный парсер сделал бы мутанта «чистый».
|
||
const report = mutationGateReport({ ...meta, guards, logs: [
|
||
{ shard: 1, text: 'FAIL чистый прогон: node --test test/g.test.mjs красный без мутанта\n' },
|
||
] });
|
||
assert.deepEqual(report.escaped, []);
|
||
assert.deepEqual(report.redGuards, ['node --test test/g.test.mjs']);
|
||
assert.ok(!report.body.includes('--id=чистый'));
|
||
assert.match(report.body, /красные без мутанта/);
|
||
assert.equal(report.failed, true);
|
||
});
|
||
|
||
test('#550: setup/invalid/interruption reported as unverified, never escaped or caught', () => {
|
||
const report = mutationGateReport({ ...meta, guards, logs: [
|
||
{ shard: 1, text: [
|
||
'FAIL alpha-mutant: ошибка подготовки до заявленного теста',
|
||
'FAIL beta-mutant: неприменимый мутант',
|
||
'FAIL gamma-mutant: прерывание инфраструктуры',
|
||
'FAIL чистая подготовка: npx tsc -p tsconfig.test.json',
|
||
].join('\n') },
|
||
] });
|
||
assert.deepEqual(report.escaped, []);
|
||
assert.deepEqual(report.redGuards, []);
|
||
assert.equal(report.unverifiable.length, 4);
|
||
assert.equal(report.unparsed.length, 0);
|
||
assert.match(report.body, /Свидетели без доказательства \(4\)/);
|
||
assert.match(report.body, /не записывается в ledger/);
|
||
assert.ok(!report.body.includes('--id=alpha-mutant'));
|
||
assert.equal(report.failed, true);
|
||
});
|
||
|
||
test('id вне реестра не выдумывается, строка сохраняется как есть (#472 AC3)', () => {
|
||
const report = mutationGateReport({ ...meta, guards, logs: [
|
||
{ shard: 1, text: 'FAIL ghost-mutant: тест остался зелёным на сломанном коде\n' },
|
||
] });
|
||
assert.deepEqual(report.escaped, []);
|
||
assert.equal(report.unparsed.length, 1);
|
||
assert.match(report.unparsed[0].line, /ghost-mutant/);
|
||
assert.match(report.body, /Неразобранные строки FAIL/);
|
||
});
|
||
|
||
test('отсутствующий лог шарда — отказ, а не «сбежавших нет» (#472 AC3)', () => {
|
||
const report = mutationGateReport({ ...meta, guards, logs: [
|
||
{ shard: 1, text: 'ok alpha-mutant: тест покраснел, как обязан\n' },
|
||
{ shard: 2, text: null },
|
||
] });
|
||
assert.deepEqual(report.escaped, []);
|
||
assert.equal(report.shards[1].status, 'missing');
|
||
assert.equal(report.failed, true, 'нет артефакта — не знаем, что сбежало');
|
||
assert.match(report.body, /Артефакты шардов 2 не пришли/);
|
||
});
|
||
|
||
test('заголовок несёт постоянный маркер, тело — прогон, SHA и дату (#472 AC4)', () => {
|
||
const report = mutationGateReport({ ...meta, guards, logs: [{ shard: 1, text: 'FAIL alpha-mutant: тест остался зелёным на сломанном коде\n' }] });
|
||
assert.ok(report.title.startsWith(REPORT_TITLE_MARKER));
|
||
assert.match(report.body, /https:\/\/x\/runs\/1/);
|
||
assert.match(report.body, /abcdef123456/);
|
||
assert.match(report.body, /2026-09-08/);
|
||
});
|
||
|
||
test('зелёный набор логов даёт failed=false (#472)', () => {
|
||
const report = mutationGateReport({ ...meta, guards, logs: [
|
||
{ shard: 1, text: 'ok alpha-mutant: тест покраснел, как обязан\n' },
|
||
{ shard: 2, text: 'поймано 3 из 3\n' },
|
||
] });
|
||
assert.equal(report.failed, false);
|
||
assert.deepEqual(report.shards.map((s) => s.status), ['ok', 'ok']);
|
||
});
|
||
|
||
test('сводка для Telegram коротка и ведёт на issue (#472)', () => {
|
||
const report = mutationGateReport({ ...meta, guards, logs: [
|
||
{ shard: 1, text: 'FAIL alpha-mutant: тест остался зелёным на сломанном коде\n' },
|
||
{ shard: 2, text: null },
|
||
] });
|
||
const text = telegramSummary(report, 'https://x/issues/9');
|
||
assert.match(text, /alpha-mutant/);
|
||
assert.match(text, /2:missing/);
|
||
assert.match(text, /https:\/\/x\/issues\/9/);
|
||
assert.ok(text.length < 600);
|
||
});
|
||
|
||
test('parseShardLogs терпит CRLF и пустой ввод (#472)', () => {
|
||
const parsed = parseShardLogs([{ shard: 1, text: 'FAIL alpha-mutant: тест остался зелёным на сломанном коде\r\n' }], KNOWN);
|
||
assert.deepEqual(parsed.escaped, ['alpha-mutant']);
|
||
assert.deepEqual(parseShardLogs([], KNOWN).shards, []);
|
||
});
|