Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions src/review/unified-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,17 +608,29 @@ function nonRequiredFailingChecksBlock(readiness: MergeReadiness | undefined): s
return lines.join("\n");
}

/** The synthetic "Code review" row -- derived from the AI reviewers' own blocker count, never from
* `ctx.signals` -- so it always exists and is never subject to `review.fields` visibility (matches its
* pre-#6067 behavior as the signal table's unconditional first row). */
/** The synthetic "Code review" row -- its blocker count is NOT purely the AI reviewers' own findings:
* `buildDualReviewNotes` (FIX D1) folds the gate's own deterministic hard blockers in too, so a non-AI
* gate failure (a missing linked issue, a registry-deliverable check, a secret leak, ...) still populates
* a "Why this is blocked" list even when no AI reviewer ran at all. Never routed through `ctx.signals`, so
* this row always exists and is never subject to `review.fields` visibility (matches its pre-#6067
* behavior as the signal table's unconditional first row).
*
* #7491-class fix: when `reviewerCount` is 0, ANY blocker present can only have come from that deterministic
* fold-in -- a fresh `ai_consensus_defect` (the other blocker source) requires an actual review pass to
* exist at all. The evidence text used to read "No AI review summary" regardless, which next to a nonzero
* blocker count ("1 blocker (No AI review summary)") looked self-contradictory: as if an AI pass ran,
* found something, but produced no write-up -- not "AI review never ran; a separate check is what's
* blocking this." */
function codeReviewRow(input: UnifiedReviewInput): UnifiedSignalRow {
const blockerCount = (input.blockers ?? []).length;
const reviewerEvidence =
input.reviewerCount > 1
? `${input.reviewerCount} reviewers, synthesized`
: input.reviewerCount === 1
? "1 reviewer"
: "No AI review summary";
: blockerCount > 0
? "no AI review ran — blocker is from a non-AI gate check"
: "No AI review summary";
return {
label: "Code review",
state: blockerCount ? "fail" : "ok",
Expand Down
18 changes: 18 additions & 0 deletions test/unit/unified-comment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,24 @@ describe("renderUnifiedReviewComment", () => {
expect(md).not.toContain("readiness 93/100");
});

// #7491-class fix: a blocker present with reviewerCount 0 can only be a deterministic gate blocker folded
// in by buildDualReviewNotes (a fresh ai_consensus_defect needs an actual review pass to exist) -- the old
// "No AI review summary" evidence text, unconditional on reviewerCount, made this read as a silent/
// incomplete AI review rather than "AI review never ran; something else is blocking this."
it("does not claim a silent/incomplete AI review when a blocker is present but no reviewer ran", () => {
const md = renderUnifiedReviewComment(
{ ...base, reviewerCount: 0, recommendations: [], decision: "close", blockers: ["This PR's linked issue names a registry path it never touches."] },
ctx,
);
expect(md).toContain("- ❌ Code review — 1 blocker (no AI review ran — blocker is from a non-AI gate check)");
expect(md).not.toContain("1 blocker (No AI review summary)");
});

it("still reports a clean, non-contradictory 'no blockers' row when no reviewer ran and nothing is blocking", () => {
const md = renderUnifiedReviewComment({ ...base, reviewerCount: 0, recommendations: [] }, ctx);
expect(md).toContain("- ✅ Code review — No blockers (No AI review summary)");
});

it("held state uses the warning alert and amber bar", () => {
const md = renderUnifiedReviewComment({ ...base, decision: "manual", recommendations: ["manual_review"] }, ctx);
expect(md).toContain("> [!WARNING]");
Expand Down