mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-05 06:08:59 +00:00
ci(process): раунд ревью ждёт Validate событием, а не сном раннера (#636)
Стадия prepare спала ≈ 28 минут на раунд, пока шёл Validate с мутантами на материале (модель работает 10–12); за неделю ≈ 420–500 job-минут простоя и потолок бюджета стадии 55 минут. - validate-gate.mjs: `--no-wait` — гейт диспатчит прогон, убеждается, что тот встал на материал (#539 сохранён), и возвращает `pending` (код 2) вместо ожидания; завершённый зелёный/красный отдаёт сразу, как прежде. - process.yml prepare: третий исход `proceed=pending`: запечатанный маркер `review-pending-<issue>-<run>-<attempt>` (issue, stage, branch, material_sha, validate run) и выход; модель и интеграция не запускаются; возврат автору — только на явном `false`. - process-resume.yml + scripts/process-resume.mjs: на `workflow_run: completed` Validate по ветке issue/* — если метка S7 стоит, активного прогона нет и последний прогон оставил маркер на этот SHA, переставить S7 (HP_PROCESS_TOKEN); новый прогон находит завершённый dispatch сразу. Без маркера не будит. - process-reconcile.mjs: читает маркер и состояние Validate на материале; идёт — wait, завершился/пропал без продолжения — retry; без маркера — прежний escalate. Общий loadSealedArtifact, экспорт processRuns/artifactNames. - preflight сверяет process-resume.yml между main и dev наравне с process.yml. - Тесты: validate-gate (4), process-resume (8, включая контракт трёх workflow), process-reconcile (2); мутанты gate-no-wait-still-sleeps, resume-wakes-round-without-marker, resume-ignores-active-run, reconcile-wakes-pending-while-validate-active. PROCESS.md §10.4, AGENTS.md. Issue: #636 User-Visible: no
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
validateStateOnMaterial,
|
||||
alreadyReported, applyReconciliationDecision, decideReconciliation, latestReviewRequest, markerFor,
|
||||
parseProcessRun, preparedEvidenceError, reconcileAll, reconciliationKey, relabel,
|
||||
} from '../scripts/process-reconcile.mjs';
|
||||
@@ -35,7 +36,7 @@ test('#555 maps a stable process run-name to issue and stage', () => {
|
||||
id: 70, attempt: 2, issue: 555, label: 'S7-code-review', stage: 'code',
|
||||
status: 'in_progress', conclusion: '', createdAt: '2026-09-13T10:00:01Z',
|
||||
updatedAt: null, url: null, prepared: null, preparedArtifact: false,
|
||||
resultArtifact: false, evidenceError: null,
|
||||
resultArtifact: false, pending: null, pendingValidate: null, evidenceError: null,
|
||||
});
|
||||
assert.equal(parseProcessRun({ display_title: 'unrelated label event' }), null);
|
||||
});
|
||||
@@ -215,3 +216,41 @@ test('#555 a fresh label/run completion stays inside grace instead of duplicatin
|
||||
runs: [run({ conclusion: 'cancelled', updatedAt: '2026-09-13T11:58:00Z' })], now: NOW,
|
||||
}).action, 'wait');
|
||||
});
|
||||
|
||||
// #636: подготовка вышла успешно, оставив маркер ожидания — Validate на
|
||||
// материале ещё шёл. Это не «успешный прогон без вердикта»: пока Validate идёт,
|
||||
// reconcile ждёт; завершился или пропал, а событие раунд не разбудило —
|
||||
// повторная метка. Маркер обязателен: без него правило прежнее (escalate),
|
||||
// иначе любой успешный прогон без вердикта будил бы модель второй раз.
|
||||
test('#636 pending marker: running Validate waits, finished Validate retries, no marker still escalates', () => {
|
||||
const pending = { schema: 1, issue: 636, material_sha: 'a'.repeat(40) };
|
||||
const waiting = decideReconciliation({
|
||||
issue: issue(), request, runs: [run({ conclusion: 'success', pending, pendingValidate: 'active' })], now: NOW,
|
||||
});
|
||||
assert.equal(waiting.action, 'wait');
|
||||
assert.match(waiting.reason, /still running/);
|
||||
|
||||
for (const state of ['completed', 'missing']) {
|
||||
const done = decideReconciliation({
|
||||
issue: issue(), request, runs: [run({ conclusion: 'success', pending, pendingValidate: state })], now: NOW,
|
||||
});
|
||||
assert.equal(done.action, 'retry', state);
|
||||
assert.match(done.reason, /was not resumed/);
|
||||
}
|
||||
|
||||
const noMarker = decideReconciliation({ issue: issue(), request, runs: [run({ conclusion: 'success' })], now: NOW });
|
||||
assert.equal(noMarker.action, 'escalate');
|
||||
const parsed = parseProcessRun({ display_title: 'process #555 · S7-code-review · x', id: 1, pending, pendingValidate: 'active' });
|
||||
assert.equal(parsed.pending, pending);
|
||||
assert.equal(parsed.pendingValidate, 'active');
|
||||
});
|
||||
|
||||
test('#636 validateStateOnMaterial reads only dispatch runs and prefers active over completed', () => {
|
||||
assert.equal(validateStateOnMaterial([]), 'missing');
|
||||
assert.equal(validateStateOnMaterial([{ event: 'push', status: 'completed' }]), 'missing');
|
||||
assert.equal(validateStateOnMaterial([{ event: 'workflow_dispatch', status: 'completed' }]), 'completed');
|
||||
assert.equal(validateStateOnMaterial([
|
||||
{ event: 'workflow_dispatch', status: 'completed' }, { event: 'workflow_dispatch', status: 'in_progress' },
|
||||
]), 'active');
|
||||
assert.equal(validateStateOnMaterial([{ event: 'workflow_dispatch', status: 'queued' }]), 'active');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user