mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-02 21:01:21 +00:00
176 lines
9.6 KiB
JavaScript
176 lines
9.6 KiB
JavaScript
// #775: real incident identities; list-window/order variants below are synthetic
|
|
// witnesses, not a claim about GitHub's unsaved historical API responses.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { decideReconciliation, pendingEvidenceError, processRuns } from '../scripts/process-reconcile.mjs';
|
|
import { decideResume, resume } from '../scripts/process-resume.mjs';
|
|
|
|
const LABEL = 'S7-code-review';
|
|
const incidents = [
|
|
[740, 'stairs-floor-switch', 36877086418, 36877204584, 'f23cca8f65a9641f9cc409b749347ba167cf9c65', '14:32:05', 'success'],
|
|
[748, 'canon-reconcile', 36883983363, 36884081445, '395210117e242b5b16118cdfd1985f9a3d620cef', '15:24:18', 'success'],
|
|
[744, 'floor-geometry-key', 36875649994, 36875756451, '62bd32538132462d4f5222a36db3bdf1e660b80b', '14:21:07', 'failure'],
|
|
];
|
|
function fixture(row = incidents[0]) {
|
|
const [issue, slug, id, validateId, sha, time, conclusion] = row;
|
|
const branch = `issue/${issue}-${slug}`;
|
|
const createdAt = `2026-10-01T${time}Z`;
|
|
const run = { id, attempt: 1, issue, label: LABEL, stage: 'code', status: 'completed', conclusion: 'success', createdAt };
|
|
const request = { id: 'request-1', label: LABEL, at: createdAt };
|
|
const pending = { schema: 1, issue: String(issue), run_id: String(id), run_attempt: '1', stage: 'code', branch,
|
|
material_sha: sha, validate_run_id: String(validateId) };
|
|
return { issue, branch, sha, headSha: sha, request, labels: [LABEL], run, pending,
|
|
runs: [run], validateRun: { id: validateId, event: 'workflow_dispatch', status: 'completed', headSha: sha, conclusion },
|
|
pendingOf: (candidate) => candidate.id === id ? pending : null };
|
|
}
|
|
const raw = (run, extra = {}) => ({ ...run, event: 'issues',
|
|
display_title: `process #${run.issue} · ${run.label} · fixture`, created_at: run.createdAt, ...extra });
|
|
|
|
test('#775 all three incident markers resume, including red Validate (ordinary prepare owns S6/comment)', () => {
|
|
for (const row of incidents) assert.equal(decideResume(fixture(row)).action, 'resume', `#${row[0]}`);
|
|
});
|
|
|
|
test('#775 history is paged to the request, not capped at 200 or filtered-search 1000 runs', () => {
|
|
for (const row of incidents) {
|
|
const f = fixture(row);
|
|
const unrelated = Array.from({ length: 1100 }, (_, n) => raw({ ...f.run, id: n + 1, issue: 999,
|
|
createdAt: '2026-10-01T16:00:00Z' }, { display_title: 'process #999 · P2 · unrelated label' }));
|
|
const rows = [...unrelated, raw(f.run), raw({ ...f.run, id: 2, createdAt: '2026-09-30T00:00:00Z' })];
|
|
const calls = [];
|
|
const runs = processRuns('o/r', [{ number: f.issue }], { since: f.request.at, getJson: (args) => {
|
|
const url = args[1];
|
|
calls.push(url);
|
|
assert.ok(!url.includes('event='), 'unfiltered endpoint avoids the search-result cap');
|
|
const page = Number(new URL(`https://api.test/${url}`).searchParams.get('page'));
|
|
return { workflow_runs: rows.slice((page - 1) * 100, page * 100) };
|
|
} });
|
|
assert.equal(calls.length, 12);
|
|
assert.equal(decideResume({ ...f, runs }).action, 'resume', `#${f.issue}`);
|
|
}
|
|
});
|
|
|
|
test('#775 history errors/repeated pages are not silently interpreted as a lost request', () => {
|
|
const f = fixture();
|
|
const page = Array.from({ length: 100 }, (_, n) => raw({ ...f.run, id: n + 1 }));
|
|
let calls = 0;
|
|
assert.throws(() => processRuns('o/r', [], { since: f.request.at, getJson: () => {
|
|
if (++calls === 2) throw new Error('API unavailable');
|
|
return { workflow_runs: page };
|
|
} }), /API unavailable/);
|
|
assert.throws(() => processRuns('o/r', [], { since: f.request.at,
|
|
getJson: () => ({ workflow_runs: page }) }), /pagination/);
|
|
});
|
|
|
|
test('#775 stop at the request time, deduplicate overlapping pages, ignore non-issue workflow events', () => {
|
|
const f = fixture();
|
|
const first = Array.from({ length: 100 }, (_, n) => raw({ ...f.run, id: n + 1, createdAt: '2026-10-01T16:00:00Z' }));
|
|
const second = [first[99], raw(f.run), raw({ ...f.run, id: 3, createdAt: '2026-09-30T00:00:00Z' }),
|
|
raw({ ...f.run, id: 200 }, { event: 'workflow_dispatch' })];
|
|
let count = 0;
|
|
const runs = processRuns('o/r', [], { since: f.request.at,
|
|
getJson: () => ({ workflow_runs: [first, second][count++] }) });
|
|
assert.equal(count, 2);
|
|
assert.equal(runs.length, 101);
|
|
assert.equal(new Set(runs.map((run) => run.id)).size, runs.length);
|
|
});
|
|
|
|
test('#775 skipped noise can be ignored but newer attempted reviews are barriers', () => {
|
|
const f = fixture();
|
|
const newer = { ...f.run, id: f.run.id + 1, createdAt: '2026-10-01T16:00:00Z' };
|
|
assert.equal(decideResume({ ...f, runs: [{ ...newer, conclusion: 'skipped' }, f.run] }).action, 'resume');
|
|
for (const conclusion of ['failure', 'cancelled', 'success']) {
|
|
assert.equal(decideResume({ ...f, runs: [{ ...newer, conclusion }, f.run] }).action, 'noop', conclusion);
|
|
}
|
|
assert.equal(decideResume({ ...f, runs: [{ ...newer, status: 'in_progress' }, f.run] }).action, 'noop');
|
|
});
|
|
|
|
test('#775 a new request, branch head, different dispatch or damaged identity never wakes the old round', () => {
|
|
const f = fixture();
|
|
for (const overrides of [
|
|
{ request: null }, { request: { ...f.request, at: '2026-10-01T16:00:00Z' } },
|
|
{ headSha: 'b'.repeat(40) }, { issue: 999 }, { branch: 'issue/740-other-branch' },
|
|
{ validateRun: { ...f.validateRun, id: f.validateRun.id + 1 } },
|
|
{ labels: [LABEL, 'S4-spec-review'] },
|
|
]) assert.equal(decideResume({ ...f, ...overrides }).action, 'noop', JSON.stringify(overrides));
|
|
for (const overrides of [{ schema: 2 }, { issue: '999' }, { run_id: '1' }, { run_attempt: '2' },
|
|
{ stage: 'spec' }, { branch: 'other' }, { validate_run_id: '' }]) {
|
|
assert.equal(decideResume({ ...f, pendingOf: () => ({ ...f.pending, ...overrides }) }).action, 'noop', JSON.stringify(overrides));
|
|
}
|
|
});
|
|
|
|
test('#775 write path rechecks current request, stop labels, head and runs; duplicates cannot relabel twice', async () => {
|
|
const f = fixture();
|
|
for (const changed of [{ labels: [] }, { labels: [LABEL, 'blocked'] }, { headSha: 'b'.repeat(40) },
|
|
{ request: { ...f.request, id: 'request-2' } }]) {
|
|
let reads = 0;
|
|
let writes = 0;
|
|
const ops = { state: () => ++reads === 1 ? f : { ...f, ...changed }, runs: () => f.runs,
|
|
pendingOf: f.pendingOf, relabel: () => writes++ };
|
|
const result = await resume({ ...f, repo: 'o/r', ops, sleep: async () => {} });
|
|
assert.equal(result.applied, false);
|
|
assert.equal(writes, 0);
|
|
}
|
|
let reads = 0;
|
|
let writes = 0;
|
|
let state = f;
|
|
const ops = { state: () => state, runs: () => { reads++; return f.runs; }, pendingOf: f.pendingOf,
|
|
relabel: () => { writes++; state = { ...f, request: { ...f.request, id: 'request-2', at: '2026-10-01T17:00:00Z' } }; } };
|
|
assert.equal((await resume({ ...f, repo: 'o/r', ops, sleep: async () => {} })).applied, true);
|
|
assert.equal((await resume({ ...f, repo: 'o/r', ops, sleep: async () => {} })).applied, false);
|
|
assert.equal(writes, 1);
|
|
assert.ok(reads >= 2);
|
|
});
|
|
|
|
test('#775 transient missing/old/no-marker snapshots recover without waiting for the schedule', async () => {
|
|
for (const row of incidents) {
|
|
const f = fixture(row);
|
|
let reads = 0;
|
|
let writes = 0;
|
|
const waits = [];
|
|
const ops = { state: () => f, runs: () => ++reads === 1
|
|
? (f.issue === 744 ? [] : [{ ...f.run, id: 1, createdAt: f.issue === 748 ? '2026-09-30T00:00:00Z' : f.run.createdAt }])
|
|
: f.runs, pendingOf: f.pendingOf, relabel: () => writes++ };
|
|
const result = await resume({ ...f, repo: 'o/r', ops, sleep: async (ms) => waits.push(ms) });
|
|
assert.equal(result.applied, true, `#${f.issue}`);
|
|
assert.equal(writes, 1);
|
|
assert.deepEqual(waits, [5000]);
|
|
}
|
|
});
|
|
|
|
test('#775 absent evidence gets a bounded grace, never a guessed relabel', async () => {
|
|
const f = fixture();
|
|
let reads = 0;
|
|
const waits = [];
|
|
const result = await resume({ ...f, repo: 'o/r', sleep: async (ms) => waits.push(ms),
|
|
ops: { state: () => f, runs: () => { reads++; return []; }, pendingOf: f.pendingOf,
|
|
relabel: () => assert.fail('no evidence') } });
|
|
assert.equal(result.action, 'noop');
|
|
assert.equal(result.applied, false);
|
|
assert.equal(reads, 3);
|
|
assert.deepEqual(waits, [5000, 5000]);
|
|
});
|
|
|
|
test('#775 reconciler uses the same request boundary, skipped-run handling and sealed identity', () => {
|
|
const f = fixture();
|
|
const now = Date.parse('2026-10-01T17:00:00Z');
|
|
const base = { issue: { number: f.issue, labels: [LABEL] }, request: f.request, now };
|
|
const ready = { ...f.run, pending: f.pending, pendingValidate: 'completed' };
|
|
const skipped = { ...f.run, id: 999, createdAt: '2026-10-01T16:00:00Z', conclusion: 'skipped' };
|
|
assert.equal(decideReconciliation({ ...base, runs: [skipped, ready] }).action, 'retry');
|
|
assert.equal(decideReconciliation({ ...base, runs: [{ ...ready, resultArtifact: true }] }).action, 'escalate');
|
|
const justReapplied = { ...f.request, at: '2026-10-01T14:32:06Z' };
|
|
assert.equal(decideReconciliation({ ...base, request: justReapplied, now: Date.parse(justReapplied.at), runs: [ready] }).action, 'wait',
|
|
'previous run one second before the new label cannot be recovered as the new request');
|
|
assert.equal(pendingEvidenceError(f.pending, { issue: f.issue, run: f.run }), null);
|
|
assert.match(pendingEvidenceError({ ...f.pending, run_attempt: '2' }, { issue: f.issue, run: f.run }), /attempt/);
|
|
});
|
|
|
|
test('#775 a run starting between snapshots prevents the write', async () => {
|
|
const f = fixture();
|
|
let reads = 0;
|
|
const ops = { state: () => f, runs: () => ++reads === 1 ? f.runs
|
|
: [{ ...f.run, id: f.run.id + 1, status: 'in_progress', createdAt: '2026-10-01T16:00:00Z' }, f.run],
|
|
pendingOf: f.pendingOf, relabel: () => assert.fail('new process already running') };
|
|
assert.equal((await resume({ ...f, repo: 'o/r', ops })).applied, false);
|
|
});
|