fix: address second round of code-review findings

actions.js:
- Add assertValidRepo/assertValidIssueNumber guards at the top of all
  action handlers (applyClaim, applySync, applyValidate, applyPublish,
  applyReview, applyDecompose, applyUnblock) for fast-fail validation
- applyValidate: fix status transition — set 'validated' unconditionally
  when ok=true instead of preserving 'blocked' (was inconsistent with
  projectState becoming 'ready')

gh-api.js:
- runGh: preserve GITHUB_TOKEN by default; only delete when caller
  explicitly sets options.stripGithubToken=true (was deleting by
  default, breaking CI)

parsing.js:
- extractCoordinationState: throw SyntaxError on malformed JSON instead
  of silently returning null — lets callers distinguish bad JSON from
  absent marker
- normalizeBodyForComparison: fix regex to match JSON-quoted form
  "lastSyncAt": ... instead of bare lastSyncAt: ...

policy.js:
- loadPolicy: validate that parsed JSON is a plain object before
  spreading; coerce nested fields (labels, review, validation,
  branchModel, project, fieldNames) to objects before merging

state.js:
- assertIssueClaimable: block re-claim on status alone (not status AND
  owner) to prevent {status:'claimed', owner:null} bypass; use
  state.owner || 'unknown' in error message
- getCoordinationState: catch SyntaxError from extractCoordinationState,
  log warning to stderr, fall back to default state

tests/lib:
- Update malformed-JSON test to expect SyntaxError throw instead of null

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Victor Casado
2026-06-11 14:25:58 -04:00
parent d4486a7a29
commit 33f2219307
6 changed files with 57 additions and 32 deletions
+9 -3
View File
@@ -36,7 +36,13 @@ function defaultCoordinationState(issue, policy = DEFAULT_POLICY) {
function getCoordinationState(issue, policy = DEFAULT_POLICY) {
const { extractCoordinationState } = require('./parsing'); // lazy to avoid circular init order
const existing = extractCoordinationState(issue && issue.body, policy);
let existing;
try {
existing = extractCoordinationState(issue && issue.body, policy);
} catch (error) {
process.stderr.write(`[github-coordination] Warning: ${error.message} (issue #${issue && issue.number})\n`);
existing = null;
}
if (existing) {
return {
...defaultCoordinationState(issue, policy),
@@ -205,8 +211,8 @@ function assertIssueClaimable(issue, state) {
throw new Error(`Issue #${issue.number} is not open`);
}
if (state.status === 'claimed' && state.owner) {
throw new Error(`Issue #${issue.number} is already claimed by ${state.owner}`);
if (state.status === 'claimed') {
throw new Error(`Issue #${issue.number} is already claimed by ${state.owner || 'unknown'}`);
}
}