mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
#620. Mutants were ~70 % of CI machine time. 1. mutation-gate.yml: a green full run (aggregator verified all six shards) leaves a cache marker keyed by material tree + workflow SHA. A scheduled night with the same tree and workflow, marker not older than 7 days, skips the shards and writes "reused from run N" to the run summary. Red runs leave no marker, so the next night runs again and still files the issue (#472). Manual dispatch always runs the full registry. Decision is a pure function in scripts/mutation-nightly-reuse.mjs. 2. changed_mutants: the shard plan (already computed before setup, #518) now names the environment of its guards (plan-browser=/plan-python=, from scripts/mutation-environment.mjs). Python + backend deps only for shards with pytest guards, Chromium only for shards whose guards reach Playwright; pip wheels cached. Every shard still runs and reports, so the six mutant jobs of the review proof are unchanged. Item 3 of the issue (smoke guards -> node --test) is out of scope here. Issue: #620 User-Visible: no
99 lines
5.3 KiB
JavaScript
99 lines
5.3 KiB
JavaScript
// #620: ночной полный реестр не гоняется повторно на уже доказанном дереве.
|
|
// Решение — чистая функция; workflow-проводку держит test/mutation-gate.test.mjs.
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
MAX_REUSE_AGE_DAYS, REUSE_MARKER_SCHEMA, decideNightlyReuse, main, reuseMarker,
|
|
} from '../scripts/mutation-nightly-reuse.mjs';
|
|
|
|
const TREE = 'a'.repeat(40);
|
|
const OTHER_TREE = 'b'.repeat(40);
|
|
const SHA = 'c'.repeat(40);
|
|
const WORKFLOW = 'd'.repeat(40);
|
|
const NOW = Date.parse('2026-09-24T01:05:00Z');
|
|
const DAY = 24 * 60 * 60 * 1000;
|
|
|
|
const marker = (overrides = {}) => ({
|
|
...reuseMarker({ tree: TREE, sha: SHA, workflowSha: WORKFLOW, runId: 1234, runAttempt: 1, event: 'schedule', now: NOW - DAY }),
|
|
...overrides,
|
|
});
|
|
const decide = (overrides = {}) => decideNightlyReuse({
|
|
event: 'schedule', tree: TREE, workflowSha: WORKFLOW, marker: marker(), now: NOW, ...overrides,
|
|
});
|
|
|
|
test('#620 AC1: неизменённое дерево с зелёным маркером — прогон переиспользуется с номером', () => {
|
|
assert.deepEqual(decide(), { reuse: true, reason: `tree ${TREE} already proved green`, runId: '1234' });
|
|
});
|
|
|
|
test('#620: маркер другого дерева не переиспользуется', () => {
|
|
const result = decide({ marker: marker({ tree: OTHER_TREE }) });
|
|
assert.equal(result.reuse, false);
|
|
assert.match(result.reason, /another tree/);
|
|
assert.equal(decide({ tree: OTHER_TREE }).reuse, false, 'дерево текущей ночи другое');
|
|
assert.equal(decide({ tree: 'HEAD' }).reuse, false, 'tree не полный SHA');
|
|
});
|
|
|
|
test('#620: маркер другого workflow не переиспользуется', () => {
|
|
assert.equal(decide({ workflowSha: 'e'.repeat(40) }).reuse, false);
|
|
assert.equal(decide({ workflowSha: '' }).reuse, false);
|
|
});
|
|
|
|
test('#620: ручной dispatch гоняет реестр всегда', () => {
|
|
const result = decide({ event: 'workflow_dispatch' });
|
|
assert.equal(result.reuse, false);
|
|
assert.match(result.reason, /manual dispatch/);
|
|
});
|
|
|
|
test('#620: без маркера, с чужой схемой или без номера прогона — полный прогон', () => {
|
|
assert.equal(decide({ marker: null }).reuse, false);
|
|
assert.equal(decide({ marker: marker({ schema: 'houseplan-mutation-green/v0' }) }).reuse, false);
|
|
assert.equal(decide({ marker: marker({ runId: '' }) }).reuse, false);
|
|
assert.equal(decide({ marker: marker({ runId: '0' }) }).reuse, false);
|
|
});
|
|
|
|
test(`#620: маркер старше ${MAX_REUSE_AGE_DAYS} суток или из будущего не принимается`, () => {
|
|
const at = (ms) => marker({ provenAt: new Date(ms).toISOString() });
|
|
assert.equal(decide({ marker: at(NOW - (MAX_REUSE_AGE_DAYS * DAY - 60_000)) }).reuse, true, 'в пределах недели');
|
|
const stale = decide({ marker: at(NOW - (MAX_REUSE_AGE_DAYS * DAY + 60_000)) });
|
|
assert.equal(stale.reuse, false);
|
|
assert.match(stale.reason, /older than/);
|
|
assert.equal(decide({ marker: at(NOW + DAY) }).reuse, false, 'из будущего');
|
|
assert.equal(decide({ marker: marker({ provenAt: 'вчера' }) }).reuse, false, 'время не читается');
|
|
});
|
|
|
|
test('#620: маркер не пишется с неполной identity', () => {
|
|
assert.equal(marker().schema, REUSE_MARKER_SCHEMA);
|
|
assert.throws(() => reuseMarker({ tree: TREE, sha: SHA, workflowSha: WORKFLOW, runId: 1, runAttempt: 1 }), /identity/);
|
|
assert.throws(() => reuseMarker({ tree: 'short', sha: SHA, workflowSha: WORKFLOW, runId: 1, runAttempt: 1, event: 'schedule' }), /identity/);
|
|
});
|
|
|
|
test('#620 AC1: CLI пишет маркер, решает по нему и оставляет «reused from run N» в сводке', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'hp-620-'));
|
|
try {
|
|
const file = join(dir, 'green', 'marker.json');
|
|
const summary = join(dir, 'summary.md');
|
|
const quiet = () => {};
|
|
assert.equal(main([`--write-marker=${file}`, `--tree=${TREE}`, `--sha=${SHA}`, `--workflow-sha=${WORKFLOW}`,
|
|
'--run-id=777', '--run-attempt=2', '--event=schedule'], { now: NOW - DAY, warn: quiet }), 0);
|
|
assert.equal(JSON.parse(readFileSync(file, 'utf8')).runId, '777');
|
|
const out = [];
|
|
assert.equal(main(['--decide', '--event=schedule', `--tree=${TREE}`, `--workflow-sha=${WORKFLOW}`,
|
|
`--marker=${file}`, `--summary=${summary}`, '--run-url-base=https://example.test/runs'],
|
|
{ now: NOW, log: (line) => out.push(line), warn: quiet }), 0);
|
|
assert.deepEqual(out, ['reuse=true', 'reused_run=777']);
|
|
assert.match(readFileSync(summary, 'utf8'), /reused from run 777 \(https:\/\/example\.test\/runs\/777\)/);
|
|
// Битый маркер — не доказательство.
|
|
writeFileSync(file, '{');
|
|
const broken = [];
|
|
main(['--decide', '--event=schedule', `--tree=${TREE}`, `--workflow-sha=${WORKFLOW}`, `--marker=${file}`],
|
|
{ now: NOW, log: (line) => broken.push(line), warn: quiet });
|
|
assert.deepEqual(broken, ['reuse=false']);
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|