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
44 changes: 32 additions & 12 deletions src/review/unified-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,12 +391,15 @@ function plural(n: number, one: string): string {
return `${n} ${one}${n === 1 ? "" : "s"}`;
}

function statusChips(input: UnifiedReviewInput, ctx: UnifiedCommentContext): string {
function statusChips(input: UnifiedReviewInput, ctx: UnifiedCommentContext, status: UnifiedCommentStatus): string {
const chips: string[] = [`\`${plural(input.changedFiles, "file")}\``];
if (input.reviewerCount > 0) chips.push(`\`${plural(input.reviewerCount, "AI reviewer")}\``);
const blockerCount = (input.blockers ?? []).length;
chips.push(blockerCount ? `\`${plural(blockerCount, "blocker")}\`` : "`no blockers`");
if (typeof ctx.readinessScore === "number") chips.push(`\`readiness ${Math.round(ctx.readinessScore)}/100\``);
// The readiness score is advisory-only and NEVER feeds the gate (see deriveUnifiedStatus's own comments) —
// showing it next to a non-"ready" verdict reads as contradictory (e.g. "readiness 93/100" beside "fixes
// required"). Only surface the number when the verdict itself agrees with a high score.
if (status === "ready" && typeof ctx.readinessScore === "number") chips.push(`\`readiness ${Math.round(ctx.readinessScore)}/100\``);
if (input.readiness) {
const ci = input.readiness.ciState;
chips.push(ci === "passed" ? "`CI green`" : ci === "failed" ? "`CI failing`" : "`CI pending`");
Expand All @@ -408,6 +411,20 @@ function statusChips(input: UnifiedReviewInput, ctx: UnifiedCommentContext): str
return chips.join(" · ");
}

/** Nest a block one level deeper inside the outer alert blockquote (an extra `> ` per line). Gives the
* Suggested Action verdict — the single most load-bearing line in the comment — its own visually distinct
* bordered sub-block instead of a plain bold paragraph lost in the body flow. Pure markdown (a nested
* blockquote), no custom HTML/CSS — `asAlert` re-prefixes every line with its own `> ` afterward, so this
* produces ordinary two-deep blockquote nesting, which GitHub already renders with a second indent/border.
* Unlike `asAlert`, every caller-supplied line here is always non-empty (a bold verdict line, optionally
* followed by `- reason` bullets — never a blank separator), so no blank-line special case is needed. */
function nestedBox(text: string): string {
return text
.split("\n")
.map((l) => `> ${l}`)
.join("\n");
}

function verdictLine(status: UnifiedCommentStatus, input: UnifiedReviewInput, ctx: UnifiedCommentContext): string {
const icon = STATUS_META[status].icon;
const reasons = (defaultReason?: string) => {
Expand All @@ -416,21 +433,23 @@ function verdictLine(status: UnifiedCommentStatus, input: UnifiedReviewInput, ct
};
switch (status) {
case "ready":
return input.merged
? `**${icon} Suggested Action - Approve/Merge**${reasons("auto-merged")}`
: `**${icon} Suggested Action - Approve/Merge**${reasons("safe to merge")}`;
return nestedBox(
input.merged
? `**${icon} Suggested Action - Approve/Merge**${reasons("auto-merged")}`
: `**${icon} Suggested Action - Approve/Merge**${reasons("safe to merge")}`,
);
case "advisory":
return `**${icon} Suggested Action - Advisory Only**${reasons("no action taken")}`;
return nestedBox(`**${icon} Suggested Action - Advisory Only**${reasons("no action taken")}`);
case "held":
return `**${icon} Suggested Action - Manual Review**${reasons()}`;
return nestedBox(`**${icon} Suggested Action - Manual Review**${reasons()}`);
case "blocked":
if (ctx.neverClosed) {
return `**${icon} Suggested Action - Manual Review**${reasons()}`;
return nestedBox(`**${icon} Suggested Action - Manual Review**${reasons()}`);
}
if (input.decision === "close" && !ctx.neverClosed) {
return `**${icon} Suggested Action - Reject/Close**${reasons()}`;
return nestedBox(`**${icon} Suggested Action - Reject/Close**${reasons()}`);
}
return `**${icon} Suggested Action - Fix Blockers**${reasons()}`;
return nestedBox(`**${icon} Suggested Action - Fix Blockers**${reasons()}`);
}
}

Expand Down Expand Up @@ -653,10 +672,11 @@ export function renderUnifiedReviewComment(input: UnifiedReviewInput, ctx: Unifi
const collapsiblesOpen = verbosity === "detailed";

const blocks: string[] = [
meta.square.repeat(12),
// No repeated-square banner row here (dropped, #6066) — the alert blockquote below already renders a
// colored border + icon for the same status; a 12x-emoji row on top of that was pure decoration.
`### ${meta.icon} ${brand} result - ${headlineLabel(status, input, ctx)}${status === "ready" && input.merged ? " · auto-merged" : ""}`,
...(reviewTimestamp ? [`<sub>Review updated: ${reviewTimestamp}</sub>`] : []),
statusChips(input, ctx),
statusChips(input, ctx, status),
verdictLine(status, input, ctx),
];

Expand Down
28 changes: 28 additions & 0 deletions test/unit/unified-comment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ describe("renderUnifiedReviewComment", () => {
expect(md).toContain("`no blockers`");
expect(md).toContain("`readiness 93/100`");
expect(md).toContain("`CI green`");
// The Suggested Action verdict is nested one level deeper than the rest of the alert body (#6066) — a
// second `> ` on top of asAlert's own per-line prefix, giving it a visually distinct bordered sub-block.
expect(md).toContain("> > **✅ Suggested Action - Approve/Merge**");
expect(md).toContain("**Review summary**");
expect(md).toContain("| **Code review** | ✅ No blockers | 2 reviewers, synthesized |");
expect(md).toContain("| Linked issue | ✅ Linked | #1372 |");
Expand Down Expand Up @@ -226,13 +229,17 @@ describe("renderUnifiedReviewComment", () => {
expect(md).toContain("Why this is blocked");
expect(md).toContain("Introduces a hardcoded secret.");
expect(md).toContain("| **Code review** | ❌ 1 blocker |");
// #6066: the advisory-only readiness score is hidden on a non-"ready" verdict — "readiness 93/100" next
// to "fixes required" reads as contradictory, since the score never feeds the gate either way.
expect(md).not.toContain("readiness 93/100");
});

it("held state uses the warning alert and amber bar", () => {
const md = renderUnifiedReviewComment({ ...base, decision: "manual", recommendations: ["manual_review"] }, ctx);
expect(md).toContain("> [!WARNING]");
expect(md).toContain("🟨");
expect(md).toContain("Suggested Action - Manual Review");
expect(md).not.toContain("readiness 93/100"); // #6066: hidden outside the "ready" status
});

it("advisory state uses the note alert and blue bar", () => {
Expand All @@ -242,6 +249,27 @@ describe("renderUnifiedReviewComment", () => {
expect(md).toContain("Suggested Action - Advisory Only");
});

it("never renders the old repeated-square banner row (#6066 — redundant with the alert's own color/icon)", () => {
for (const decision of ["merge", "manual", "close", "comment"] as const) {
const md = renderUnifiedReviewComment({ ...base, decision }, ctx);
// Every STATUS_META square is exactly 2 code units (emoji + variation selector); a banner would show up
// as one square repeated 12x in a row with no other characters — the legend line below uses each square
// only ONCE per status name, so this pattern can only match a leftover banner.
expect(md).not.toMatch(/(🟩|🟦|🟨|🟥){12}/);
}
});

it("only shows the readiness-score chip when the verdict is ready, regardless of how high the score is", () => {
const readyMd = renderUnifiedReviewComment({ ...base, decision: "merge" }, ctx);
expect(readyMd).toContain("`readiness 93/100`");
const heldMd = renderUnifiedReviewComment({ ...base, decision: "manual" }, ctx);
expect(heldMd).not.toContain("readiness 93/100");
const blockedMd = renderUnifiedReviewComment({ ...base, decision: "close", blockers: ["x"] }, ctx);
expect(blockedMd).not.toContain("readiness 93/100");
const advisoryMd = renderUnifiedReviewComment({ ...base, decision: "comment", recommendations: [] }, ctx);
expect(advisoryMd).not.toContain("readiness 93/100");
});

it("dedupes repeated blockers and nits", () => {
const md = renderUnifiedReviewComment(
{ ...base, decision: "close", blockers: ["Same issue", "same issue", "Same issue"] },
Expand Down