mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
fix: сохранять диагностику при сбое восстановления review-метки (#555)
Issue: #555 User-Visible: no
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
alreadyReported, decideReconciliation, latestReviewRequest, markerFor,
|
||||
parseProcessRun, preparedEvidenceError, reconciliationKey,
|
||||
alreadyReported, applyReconciliationDecision, decideReconciliation, latestReviewRequest, markerFor,
|
||||
parseProcessRun, preparedEvidenceError, reconcileAll, reconciliationKey, relabel,
|
||||
} from '../scripts/process-reconcile.mjs';
|
||||
|
||||
const NOW = Date.parse('2026-09-13T12:00:00Z');
|
||||
@@ -137,7 +137,7 @@ test('#555 repeat reconciliation has a stable dedupe marker', () => {
|
||||
...issue(),
|
||||
comments: [{
|
||||
createdAt: '2026-09-13T10:05:00Z',
|
||||
body: `${markerFor(key)}\nАвтосверка процесса повторно разбудила \`S7-code-review\``,
|
||||
body: `${markerFor(key)}\nАвтосверка процесса пытается повторно разбудить \`S7-code-review\``,
|
||||
}],
|
||||
};
|
||||
const retriedRequest = { ...request, id: 'event-8', at: '2026-09-13T10:04:59Z' };
|
||||
@@ -146,6 +146,64 @@ test('#555 repeat reconciliation has a stable dedupe marker', () => {
|
||||
assert.match(stopped.reason, /one automatic retry/);
|
||||
});
|
||||
|
||||
test('#555 write path persists the marker before relabel and never mutates after comment failure', async () => {
|
||||
const decision = decideReconciliation({ issue: issue(), request, runs: [], now: NOW });
|
||||
const key = reconciliationKey(issue(), request, decision);
|
||||
const calls = [];
|
||||
await assert.rejects(applyReconciliationDecision({
|
||||
repo: 'owner/repo', issue: issue(), request, decision, key,
|
||||
ops: {
|
||||
comment: async () => { calls.push('comment'); throw new Error('comment unavailable'); },
|
||||
relabel: async () => { calls.push('relabel'); },
|
||||
},
|
||||
}), /comment unavailable/);
|
||||
assert.deepEqual(calls, ['comment']);
|
||||
|
||||
calls.length = 0;
|
||||
await applyReconciliationDecision({
|
||||
repo: 'owner/repo', issue: issue(), request, decision, key,
|
||||
ops: {
|
||||
comment: async (_repo, _issue, body) => { calls.push('comment'); assert.match(body, new RegExp(markerFor(key))); },
|
||||
relabel: async () => { calls.push('relabel'); },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(calls, ['comment', 'relabel']);
|
||||
});
|
||||
|
||||
test('#555 relabel checks the bounded restore attempt instead of throwing the first error blindly', () => {
|
||||
const calls = [];
|
||||
let adds = 0;
|
||||
const recovered = (args) => {
|
||||
calls.push(args.at(-2));
|
||||
if (args.includes('--remove-label')) return { status: 0, stderr: '' };
|
||||
adds++;
|
||||
return adds === 1 ? { status: 1, stderr: 'transient' } : { status: 0, stderr: '' };
|
||||
};
|
||||
assert.doesNotThrow(() => relabel('owner/repo', issue(), 'S7-code-review', recovered));
|
||||
assert.equal(adds, 2);
|
||||
assert.equal(calls.length, 3);
|
||||
|
||||
const broken = (args) => args.includes('--remove-label')
|
||||
? { status: 0, stderr: '' } : { status: 1, stderr: 'still broken' };
|
||||
assert.throws(() => relabel('owner/repo', issue(), 'S7-code-review', broken), /still broken/);
|
||||
});
|
||||
|
||||
test('#555 one write failure stays in machine summary and does not abort the snapshot', async () => {
|
||||
const snapshot = { issue: issue(), request, runs: [] };
|
||||
const runtime = {
|
||||
openReviewIssues: async () => [snapshot.issue],
|
||||
processRuns: async () => [],
|
||||
snapshot: async () => snapshot,
|
||||
applyDecision: async () => { throw new Error('write failed after durable diagnosis'); },
|
||||
};
|
||||
const summary = await reconcileAll({ repo: 'owner/repo', apply: true, now: NOW, runtime });
|
||||
assert.equal(summary.failures, 1);
|
||||
assert.equal(summary.mutations, 0);
|
||||
assert.equal(summary.records.length, 1);
|
||||
assert.equal(summary.records[0].applied, false);
|
||||
assert.match(summary.records[0].error, /write failed/);
|
||||
});
|
||||
|
||||
test('#555 a fresh label/run completion stays inside grace instead of duplicating work', () => {
|
||||
assert.equal(decideReconciliation({
|
||||
issue: issue(),
|
||||
|
||||
@@ -786,6 +786,8 @@ test('#555: bounded reconciler wakes only lost review requests and emits one mac
|
||||
assert.match(workflow, /secrets\.HP_PROCESS_TOKEN/);
|
||||
assert.match(workflow, /node scripts\/process-reconcile\.mjs[\s\S]*--apply="\$APPLY"/);
|
||||
assert.match(workflow, /--max-actions=5/);
|
||||
assert.match(workflow, /- name: Опубликовать компактный machine-readable итог\n\s+if: always\(\)/,
|
||||
'summary artifact survives a failed write or reconciler exit');
|
||||
assert.match(workflow, /process-reconcile-\$\{\{ github\.run_id \}\}-\$\{\{ github\.run_attempt \}\}/);
|
||||
assert.match(process, /houseplan-process-reconcile\/v1/);
|
||||
assert.match(process, /второй вызов модели или S8 по догадке запрещены/);
|
||||
|
||||
Reference in New Issue
Block a user