mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-05 22:29:05 +00:00
test: the real waitValidate is exercised against a cancelled dispatch
Code review r2 (M1): the cancelled-run filter in merge-candidate's waitValidate had no test or mutant — every test replaced ops.waitValidate with a fake. realOps now takes an injectable `exec` (default: the same spawnSync wrapper) so the real implementation runs against scripted `gh run list` answers: a cancelled dispatch is skipped and its replacement followed; a lone cancelled run ends in `missing`, never red. Mutant: merge-trusts-cancelled-dispatch. Issue: #510 User-Visible: no
This commit is contained in:
@@ -101,9 +101,9 @@ const sh = (cmd, args, opts = {}) => {
|
||||
return { status: r.status ?? 1, stdout: (r.stdout || '').trim(), stderr: (r.stderr || '').trim() };
|
||||
};
|
||||
|
||||
export function realOps({ repo, token, workflow = 'validate.yml', sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = Date.now }) {
|
||||
export function realOps({ repo, token, workflow = 'validate.yml', sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = Date.now, exec = sh }) {
|
||||
const pushUrl = `https://x-access-token:${token}@github.com/${repo}`;
|
||||
const git = (...args) => sh('git', args);
|
||||
const git = (...args) => exec('git', args);
|
||||
const must = (r, what) => { if (r.status !== 0) throw new Error(`${what}: ${r.stderr || r.stdout}`); return r.stdout; };
|
||||
return {
|
||||
fetch: (...refs) => must(git('fetch', '-q', 'origin', ...refs), 'git fetch'),
|
||||
@@ -131,14 +131,14 @@ export function realOps({ repo, token, workflow = 'validate.yml', sleep = (ms) =
|
||||
// новое дерево, поэтому слияние запускает Validate с мутантами само и ждёт
|
||||
// именно этот dispatch-прогон; push-прогон на том же SHA их не содержит.
|
||||
dispatchValidate: (ref) => {
|
||||
const r = sh('gh', ['workflow', 'run', workflow, '--repo', repo, '--ref', ref, '-f', 'full=false', '-f', 'mutants=true']);
|
||||
const r = exec('gh', ['workflow', 'run', workflow, '--repo', repo, '--ref', ref, '-f', 'full=false', '-f', 'mutants=true']);
|
||||
if (r.status !== 0) throw new Error(`gh workflow run ${workflow}: ${r.stderr || r.stdout}`);
|
||||
},
|
||||
waitValidate: async (sha, { event = 'workflow_dispatch' } = {}) => {
|
||||
const started = now();
|
||||
let runId = null;
|
||||
while (now() - started < VALIDATE_TOTAL_MS) {
|
||||
const r = sh('gh', ['run', 'list', '--repo', repo, '--workflow', workflow, '--commit', sha, '--json', 'databaseId,status,conclusion,url,event', '--limit', '10']);
|
||||
const r = exec('gh', ['run', 'list', '--repo', repo, '--workflow', workflow, '--commit', sha, '--json', 'databaseId,status,conclusion,url,event', '--limit', '10']);
|
||||
const all = r.status === 0 && r.stdout ? JSON.parse(r.stdout) : [];
|
||||
// Отменённый прогон ничего не доказывает (#511): его заменил следующий
|
||||
// dispatch на той же ветке — ждём его, а не красим кандидата.
|
||||
|
||||
@@ -8158,6 +8158,17 @@ const MUTANT_DEFINITIONS = [
|
||||
replace: " if (false) { // mutant: cancelled counts as red",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'merge-trusts-cancelled-dispatch',
|
||||
guard: 'node --test test/merge-candidate.test.mjs',
|
||||
because: 'the real waitValidate must skip a dispatch cancelled by its replacement; reading it as red '
|
||||
+ 'fails the merge candidate for nothing (#510 review r2 M1, #511)',
|
||||
patches: [{
|
||||
file: 'scripts/merge-candidate.mjs',
|
||||
find: " const runs = all.filter((x) => (!event || x.event === event) && x.conclusion !== 'cancelled');",
|
||||
replace: " const runs = all.filter((x) => (!event || x.event === event)); // mutant: cancelled is red",
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'review-trusts-push-run-without-mutants',
|
||||
guard: 'node --test test/validate-gate.test.mjs',
|
||||
|
||||
@@ -226,3 +226,42 @@ test('на настоящем git: чистый ребейз с равным pat
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- #510 code review r2 M1: the real waitValidate, not a fake ----------
|
||||
|
||||
/** gh scripted by call: each `gh run list` answer is the next snapshot. */
|
||||
function scriptedExec(snapshots) {
|
||||
let calls = 0;
|
||||
return {
|
||||
exec: (cmd, args) => {
|
||||
if (cmd === 'gh' && args[0] === 'run' && args[1] === 'list') {
|
||||
const s = snapshots[Math.min(calls, snapshots.length - 1)];
|
||||
calls += 1;
|
||||
return { status: 0, stdout: JSON.stringify(s), stderr: '' };
|
||||
}
|
||||
throw new Error(`unexpected ${cmd} ${args.join(' ')}`);
|
||||
},
|
||||
calls: () => calls,
|
||||
};
|
||||
}
|
||||
|
||||
test('#510 r2 M1: realOps.waitValidate ignores a cancelled dispatch and follows its replacement', async () => {
|
||||
const cancelled = { databaseId: 1, status: 'completed', conclusion: 'cancelled', url: 'https://run/1', event: 'workflow_dispatch' };
|
||||
const push = { databaseId: 2, status: 'completed', conclusion: 'success', url: 'https://run/2', event: 'push' };
|
||||
const replacement = { databaseId: 3, status: 'completed', conclusion: 'success', url: 'https://run/3', event: 'workflow_dispatch' };
|
||||
const gh = scriptedExec([[cancelled, push], [cancelled, push], [replacement, cancelled, push]]);
|
||||
let clock = 0;
|
||||
const ops = realOps({ repo: 'x/y', token: 'none', exec: gh.exec, sleep: async (ms) => { clock += ms; }, now: () => clock });
|
||||
const r = await ops.waitValidate('c'.repeat(40), { event: 'workflow_dispatch' });
|
||||
assert.deepEqual(r, { result: 'green', url: 'https://run/3' });
|
||||
assert.equal(gh.calls(), 3, 'kept polling past the cancelled run instead of returning red on the first answer');
|
||||
});
|
||||
|
||||
test('#510 r2 M1: realOps.waitValidate with only a cancelled dispatch reports missing after the appear window, never red', async () => {
|
||||
const cancelled = { databaseId: 1, status: 'completed', conclusion: 'cancelled', url: 'https://run/1', event: 'workflow_dispatch' };
|
||||
const gh = scriptedExec([[cancelled]]);
|
||||
let clock = 0;
|
||||
const ops = realOps({ repo: 'x/y', token: 'none', exec: gh.exec, sleep: async (ms) => { clock += ms; }, now: () => clock });
|
||||
const r = await ops.waitValidate('c'.repeat(40), { event: 'workflow_dispatch' });
|
||||
assert.equal(r.result, 'missing');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user