From a891f7933be9e2a385b1f6172fa0db2c1fe30445 Mon Sep 17 00:00:00 2001 From: ejc3 Date: Sun, 25 Oct 2026 21:41:56 +0000 Subject: [PATCH] Review gate: read CodeRabbit's icon notice and Codex's commit-less receipt Two bot comments that hold no finding were counted as unanswered findings. CodeRabbit's "Review limit reached" summary notice has put an icon in front of each folded summary since 2026-10-23: a of two SVGs from storage.googleapis.com/coderabbit_public_assets/review-comment-icons-v1, then "Run configuration", "Commits", "Files selected for processing: N" or "Files ignored due to path filters: N". No such line was a listed shape. Of the 56 notices without a walkthrough on ejc3/fcvm PRs updated from 2026-10-01 to 2026-10-25, the gate on main claimed the 31 with icons; with this change it claims none. Codex's receipt can name no commit: on #1282 it was the receipt marker, the phrase and its sign-off, with no reviewed-commit line. It is a verdict that covers nothing. - scripts/check-review-threads.sh: - cr_icon_summary_re($icon; $name), and four payload shapes built from it, each pairing one icon with the name it labels. - is_verdict accepts the phrase line alone when the body's first line is "". verdict_sha returns "" for it, so it binds no head. Without the marker the bare phrase stays claimable (finding 21). - Comments describe both shapes. - scripts/test-check-review-threads.sh, finding 51: the #1285 notice (ids replaced) needs no disposition and covers no head; with a finding appended, a finding as an icon summary's name, or an icon URL changed, it is claimable. The #1282 receipt needs no disposition and covers no head; without its marker, with a finding appended, or from a human, it is claimable. - tests/test_log_scan.rs: the same two notice tests and two claim guards. The #1154 case from the issue already passes on main (c954079d, 6ddf1ce6 added "Delightful!" and the 2026-10 notice layout). The sign-off list stays enumerated: an open tail accepts "Didn't find any major issues. P1: ..." as a verdict (finding 21). All 148 receipt-shaped Codex comments on PRs updated since 2026-10-01 match. Tested: - bash scripts/test-check-review-threads.sh: passed=341 failed=0. On main's gate the two notice cases fail (UNANSWERED). With the marker check removed "the #1282 receipt without its marker is claimable" fails; with the icon name loosened to .* "a finding written as an icon summary's name is claimable" fails. - make test-unit FILTER="-E 'binary(test_log_scan)'": 40 passed. On main's gate the two notice tests fail; each mutation above fails its claim guard. - make lint: passes. --- scripts/check-review-threads.sh | 29 ++++-- scripts/test-check-review-threads.sh | 146 +++++++++++++++++++++++++++ tests/test_log_scan.rs | 120 ++++++++++++++++++++++ 3 files changed, 289 insertions(+), 6 deletions(-) diff --git a/scripts/check-review-threads.sh b/scripts/check-review-threads.sh index c2164700d..f9968cc43 100755 --- a/scripts/check-review-threads.sh +++ b/scripts/check-review-threads.sh @@ -621,7 +621,11 @@ TRIGGER_RE='\A[[:space:]]*@(codex[[:space:]]+(security[[:space:]]+)?review|coder # sign-offs, then "**Reviewed commit:** ``", then a folded "About Codex in GitHub" # block. Since 2026-10-15 the comment opens with "", an # HTML comment on its own line, which verdict_lines drops like any hidden comment -# (strip_hidden_comments below). A CodeRabbit review that produces no comments replies +# (strip_hidden_comments below). That receipt can also name no commit: on #1282 +# (2026-10-25) it was the phrase and sign-off alone, and the summary comment's row read "โ€”" +# where the commit goes. A receipt in that shape is a verdict that covers nothing, like +# CodeRabbit's reply below; the marker must be its first line, because the bare phrase +# without it is not a shape Codex posts (finding 21). A CodeRabbit review that produces no comments replies # "Full review finished." (or "Review finished." for an incremental one) folded under # "Action performed". Those are verdicts, not findings: they need no disposition. Codex's # also covers the commit it names; CodeRabbit's names nothing, and its coverage comes @@ -860,11 +864,14 @@ def verdict_lines: | map(select(length > 0 and . != cr_note))) end; def codex_line_re: "^Codex Review: Didn.t find any major issues\\.?( Bravo\\.| Keep it up!| Keep them coming!| Hooray!| Swish!| You.re on a roll\\.| :\\+1:| :rocket:| :tada:| Nice work[.!]| Great job[.!]| Breezy!| ๐Ÿ‘| Delightful!| What shall we delve into next\\?| More of your lovely PRs please\\.| Can.t wait for the next one!| Chef.s kiss\\.| Already looking forward to the next diff\\.| Another round soon, please!)?$"; def reviewed_re: "^\\*\\*Reviewed commit:\\*\\* `(?[0-9a-f]{7,40})`$"; +def codex_receipt_re: "\\A\n"; def is_verdict: - verdict_lines as $l + (. // "") as $b + | verdict_lines as $l | ($l == ["
", "โœ… Action performed", "Full review finished.", "
"]) or ($l == ["
", "โœ… Action performed", "Review finished.", "
"]) - or (($l | length) == 2 and ($l[0] | test(codex_line_re)) and ($l[1] | test(reviewed_re))); + or (($l | length) == 2 and ($l[0] | test(codex_line_re)) and ($l[1] | test(reviewed_re))) + or (($l | length) == 1 and ($l[0] | test(codex_line_re)) and ($b | test(codex_receipt_re))); def verdict_sha: verdict_lines as $l | if ($l | length) == 2 and ($l[1] | test(reviewed_re)) then ($l[1] | capture(reviewed_re) | .sha) else "" end; @@ -939,15 +946,25 @@ def cr_notice_heading_re: "^## (Review limit reached|Review skipped|Reviews paus # configuration as "- **Key**: value" (each key with the values it has shown, so a list # item holding other text is not a run setting) and links the limits docs at a new path (#1199); # every "Review limit reached" notice on ejc3/fcvm PRs on 2026-10-23 matched, except #811, -# which also carries a Finishing Touches block. A line outside it makes the comment claimable, which is what it was before the -# exemption existed: the cost of an unlisted shape is one disposition, and the cost of -# accepting one is a finding nobody has to answer. +# which also carries a Finishing Touches block. From 2026-10-23 each folded summary opens +# with an icon, a of two SVGs from the CodeRabbit icon set (cr_icon_summary_re, one +# shape per icon and the name it labels). Of the 56 notices without a walkthrough on ejc3/fcvm +# PRs updated from 2026-10-01 to 2026-10-25, the 31 with icons read as findings until these +# shapes were listed. A line outside this list makes the comment claimable, which is what it +# was before the exemption existed: the cost of an unlisted shape is one disposition, and +# the cost of accepting one is a finding nobody has to answer. +def cr_icon_summary_re($icon; $name): + "^\"\" \($name)$"; def cr_notice_payload_res: [ "^$" , "^$" , "^$" , "^(โš™๏ธ Run configuration|๐Ÿ“ฅ Commits|๐Ÿ“’ Files selected for processing: [0-9]+|โ›” Files ignored due to path filters: [0-9]+|View limit details)$" , "^(โš™๏ธ Run configuration|๐Ÿ“ฅ Commits|๐Ÿ“’ Files selected for processing \\([0-9]+\\)|โ›” Files ignored due to path filters \\([0-9]+\\)|How can I continue\\?|How do review limits work\\?|Review details|View limit details)$" + , cr_icon_summary_re("cog"; "Run configuration") + , cr_icon_summary_re("git-commit-horizontal"; "Commits") + , cr_icon_summary_re("file-plus-corner"; "Files selected for processing: [0-9]+") + , cr_icon_summary_re("file-minus-corner"; "Files ignored due to path filters: [0-9]+") , "^\\* `[^`]+`( is excluded by `[^`]+`)?$" , "^- \\[ \\] ๐Ÿ” Trigger review$" , "^\\*\\*(Configuration used|Review profile|Plan)\\*\\*: [A-Za-z0-9 +]+$" diff --git a/scripts/test-check-review-threads.sh b/scripts/test-check-review-threads.sh index 85a52aa42..316ab1760 100755 --- a/scripts/test-check-review-threads.sh +++ b/scripts/test-check-review-threads.sh @@ -2126,6 +2126,152 @@ run_case "a finding inside the Autofix block is claimable" \ run_case "a finding written as a run-configuration item is claimable" \ "$(wrap9 "$(cmt coderabbitai Bot 2026-01-01T00:00:00Z "$(jq -n --arg b "$CR_1199" '$b | sub("> - \\*\\*Commit\\*\\*: Unavailable"; "> - **Commit**: P1 this drops the last row")')" 2026-01-02T01:00:00Z)")" 1 "carry no disposition" +echo "== finding 51: the notice and receipt wordings the bots posted from 2026-10-23 ==" +# CodeRabbit's review-limit notice now puts an icon (a of two SVGs) in front of +# each folded summary: Run configuration, Commits, Files selected for processing, Files +# ignored due to path filters. No icon line was a listed shape, so from 2026-10-23 every +# "Review limit reached" notice on ejc3/fcvm (31 of them by 2026-10-25) read as an +# unanswered finding. CR_1285 is #1285's notice with its org, run and checkbox ids replaced +# and the range moved onto this harness's base and head. +IFS= read -r -d '' CR_1285_RAW <<'CR1285_EOF' + + + + + +> [!WARNING] +> ## Review limit reached +> +> You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. +> +> **Next included review available in 54 minutes.** +> +> [Check out review usage here](https://app.coderabbit.ai/dashboard/review-capacity?orgId=00000000-0000-4000-8000-000000000001). +> +>
+> View limit details +>
+>
+> +> **Limit details:** Youโ€™ve used the included review currently available. +> +> [Learn how review limits work](https://docs.coderabbit.ai/management/rate-limits). +> +> **Review configuration:** +> +>
+> Run configuration +>
+>
+> +> - **Source mode**: Auto +> - **Repository**: Unavailable +> - **Branch**: Unavailable +> - **File**: No configuration file from this repository applied +> - **Commit**: Unavailable +> - **Layers**: defaults +> - **Review profile**: CHILL +> - **Plan**: Advanced +> - **Run ID**: `00000000-0000-4000-8000-000000000002` +> +>
+>
+>
+> +>
+> Commits +>
+>
+> +> Reviewing files that changed from the base of the PR and between @BASE@ and @HEAD@. +> +>
+>
+>
+> +>
+> Files selected for processing: 2 +>
+>
+> +> * `.claude/CLAUDE.md` +> * `src/uffd/working_set.rs` +> +>
+>
+>
+> +>
+>
+>
+ + + + +- [ ] Autofix ยท Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts + + + + + +Thanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=ejc3/fcvm&utm_content=1285)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. + +
+โค๏ธ Share +
+
+ +- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai) +- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai) +- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai) +- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code) + +
+
+
+ + +Comment `@coderabbitai help` to get the list of available commands. + + +CR1285_EOF +CR_1285=${CR_1285_RAW//@BASE@/$BASE40}; CR_1285=${CR_1285//@HEAD@/$HEAD40} +cr1285() { jq -n --arg b "$CR_1285" --arg x "${1:-}" '$b + $x'; } +run_case "the #1285 review-limit notice with icon summaries needs no disposition" \ + "$(wrap9 "$(cmt coderabbitai Bot 2026-01-01T00:00:00Z "$(cr1285)" 2026-01-02T01:00:00Z)")" 0 "CLEAR" +# Guards: a notice covers no head; the same body with anything added, or with a finding +# written where an icon summary's name goes, is a claim; and an icon from anywhere but +# CodeRabbit's icon set is not a listed shape. +run_case "the #1285 review-limit notice quoting the head's range covers no head" \ + "$(wrap7 "[$(cmt coderabbitai Bot 2026-01-01T00:00:00Z "$(cr1285)" 2026-01-02T01:00:00Z)]")" 1 "UNREVIEWED HEAD" +run_case "the #1285 notice with a finding appended is claimable" \ + "$(wrap9 "$(cmt coderabbitai Bot 2026-01-01T00:00:00Z "$(cr1285 $'\n\nP1: this drops the last row')" 2026-01-02T01:00:00Z)")" 1 "carry no disposition" +run_case "a finding written as an icon summary's name is claimable" \ + "$(wrap9 "$(cmt coderabbitai Bot 2026-01-01T00:00:00Z "$(jq -n --arg b "$CR_1285" '$b | sub("
Commits"; "
P1: this drops the last row")')" 2026-01-02T01:00:00Z)")" 1 "carry no disposition" +run_case "an icon summary whose icon is not CodeRabbit's is claimable" \ + "$(wrap9 "$(cmt coderabbitai Bot 2026-01-01T00:00:00Z "$(jq -n --arg b "$CR_1285" '$b | sub("icons-v1/cog-dark\\.svg"; "icons-v1/cog-dark.svg?P1-drops-the-last-row")')" 2026-01-02T01:00:00Z)")" 1 "carry no disposition" +# Codex's receipt can also name no commit: on #1282 (2026-10-25) the receipt was the phrase +# and its sign-off with no reviewed-commit line, and the summary comment's row for it read +# "โ€”" where the commit goes. It reports no findings, so it needs no disposition, and it names +# nothing, so it covers nothing. The receipt marker is what tells it from the bare phrase +# finding 21 rejects; receipt_nocommit is the #1282 body. +receipt_nocommit() { jq -n --argjson b "$(receipt_body "$1" 0000000)" '$b | sub("\n\n\\*\\*Reviewed commit:\\*\\* `0000000`"; "")'; } +run_case "the #1282 receipt that names no commit needs no disposition" \ + "$(wrap9 "$(cmt "$CODEX" Bot 2026-01-02T01:00:00Z "$(receipt_nocommit " Can't wait for the next one!")")")" \ + 0 "CLEAR" +run_case "the #1282 receipt that names no commit covers no head" \ + "$(wrap6 "[$(cmt "$CODEX" Bot 2026-01-02T01:00:00Z "$(receipt_nocommit " Can't wait for the next one!")")]")" \ + 1 "UNREVIEWED HEAD" +run_case "the #1282 receipt without its marker is claimable" \ + "$(wrap9 "$(cmt "$CODEX" Bot 2026-01-02T01:00:00Z "$(jq -n --argjson b "$(receipt_nocommit " Can't wait for the next one!")" '$b | sub("\n\n"; "")')")")" \ + 1 "carry no disposition" +run_case "the #1282 receipt with a finding appended is claimable" \ + "$(wrap9 "$(cmt "$CODEX" Bot 2026-01-02T01:00:00Z "$(receipt_nocommit $' Can\'t wait for the next one!\n\nP1: the lease is dropped before the rename')")")" \ + 1 "carry no disposition" +run_case "a human posting the #1282 receipt is claimable" \ + "$(wrap9 "$(cmt helpful-human User 2026-01-02T01:00:00Z "$(receipt_nocommit " Can't wait for the next one!")")")" \ + 1 "carry no disposition" + echo echo "passed=$pass failed=$fail" [ "$fail" -eq 0 ] diff --git a/tests/test_log_scan.rs b/tests/test_log_scan.rs index 3f95e0dc1..8d92e2a60 100644 --- a/tests/test_log_scan.rs +++ b/tests/test_log_scan.rs @@ -1172,6 +1172,126 @@ fn review_gate_treats_the_coderabbit_1199_notice_with_additions_as_a_claim() { } } +/// CodeRabbit's "Review limit reached" summary notice as posted on #1285 (2026-10-25), with +/// its org, run and checkbox ids replaced. JSON string literal. +const CR_1285_NOTICE: &str = r#""\n\n\n\n\n> [!WARNING]\n> ## Review limit reached\n> \n> You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.\n> \n> **Next included review available in 54 minutes.**\n> \n> [Check out review usage here](https://app.coderabbit.ai/dashboard/review-capacity?orgId=00000000-0000-4000-8000-000000000001).\n> \n>
\n> View limit details\n>
\n>
\n> \n> **Limit details:** Youโ€™ve used the included review currently available.\n> \n> [Learn how review limits work](https://docs.coderabbit.ai/management/rate-limits).\n> \n> **Review configuration:**\n> \n>
\n> \"\" Run configuration\n>
\n>
\n> \n> - **Source mode**: Auto\n> - **Repository**: Unavailable\n> - **Branch**: Unavailable\n> - **File**: No configuration file from this repository applied\n> - **Commit**: Unavailable\n> - **Layers**: defaults\n> - **Review profile**: CHILL\n> - **Plan**: Advanced\n> - **Run ID**: `00000000-0000-4000-8000-000000000002`\n> \n>
\n>
\n>
\n> \n>
\n> \"\" Commits\n>
\n>
\n> \n> Reviewing files that changed from the base of the PR and between c89d4fa27958c7650314e7ba9f2c212656ecc142 and 2c2089a9ffe884eed3d19ef62272121c07ada5e6.\n> \n>
\n>
\n>
\n> \n>
\n> \"\" Files selected for processing: 2\n>
\n>
\n> \n> * `.claude/CLAUDE.md`\n> * `src/uffd/working_set.rs`\n> \n>
\n>
\n>
\n> \n>
\n>
\n>
\n\n\n\n\n- [ ] Autofix ยท Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts\n\n\n\n\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=ejc3/fcvm&utm_content=1285)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n
\nโค๏ธ Share\n
\n
\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n
\n
\n
\n\n\nComment `@coderabbitai help` to get the list of available commands.\n\n""#; + +/// CodeRabbit's review-limit notice with icons in its folded summaries needs no disposition +/// and covers no head. +/// +/// From 2026-10-23 the notice puts a of two SVG icons in front of each folded +/// summary (Run configuration, Commits, Files selected for processing, Files ignored due to +/// path filters). No such line was a listed shape, so every "Review limit reached" notice +/// posted on ejc3/fcvm from then on read as an unanswered finding. +#[test] +fn review_gate_treats_the_coderabbit_1285_icon_notice_as_a_notice() { + require_jq(); + let (code, out) = run_gate_on_bot_comment("cr1285", "coderabbitai", CR_1285_NOTICE); + assert!( + !out.contains("carry no disposition") && !out.contains("UNANSWERED"), + "the #1285 review-limit notice is not a finding.\n{out}" + ); + assert_eq!(code, Some(1), "{out}"); + assert!( + out.contains("UNREVIEWED HEAD") && !out.contains("HEAD COVERED"), + "a notice covers no head.\n{out}" + ); +} + +/// The #1285 notice with anything added, or with a finding written where an icon summary's +/// name goes, is a claim. +/// +/// Green on the gate before and after the icon shapes were listed; the second case goes red +/// if an icon summary accepts any name. +#[test] +fn review_gate_treats_the_coderabbit_1285_notice_with_additions_as_a_claim() { + require_jq(); + let appended = format!( + "{}\\n\\nP1: this drops the last row\"", + &CR_1285_NOTICE[..CR_1285_NOTICE.len() - 1] + ); + let as_icon_name = CR_1285_NOTICE.replace( + " Commits", + "
P1: this drops the last row", + ); + for (case, body) in [ + ("a finding appended", &appended), + ("a finding as an icon summary's name", &as_icon_name), + ] { + assert_ne!( + body.as_str(), + CR_1285_NOTICE, + "{case}: the fixture edit did not apply" + ); + let (code, out) = run_gate_on_bot_comment("cr1285-claim", "coderabbitai", body); + assert_eq!(code, Some(1), "{case}\n{out}"); + assert!( + out.contains("carry no disposition"), + "the #1285 notice with {case} is a claim.\n{out}" + ); + } +} + +/// Codex's receipt as posted on #1282 (2026-10-25): the marker, the phrase and its sign-off, +/// no reviewed-commit line, and the About Codex block. JSON string literal. +const CODEX_1282_RECEIPT: &str = r#""\n\nCodex Review: Didn't find any major issues. Can't wait for the next one!\n\n
โ„น๏ธ About Codex in GitHub\n
\n\n[Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you\n- Open a pull request for review\n- Mark a draft as ready\n- Comment \"@codex review\".\n\nIf Codex has suggestions, it will comment; otherwise it will react with ๐Ÿ‘.\n\n\n\n\nCodex can also answer questions or update the PR. Try commenting \"@codex address that feedback\".\n \n
""#; + +/// A Codex receipt that names no commit reports no findings and covers nothing. +/// +/// On #1282 Codex posted its receipt without a reviewed-commit line, and its summary +/// comment's row read "โ€”" where the commit goes. The gate counted the receipt as an +/// unanswered finding. +#[test] +fn review_gate_treats_a_codex_receipt_naming_no_commit_as_a_verdict_without_coverage() { + require_jq(); + let (code, out) = run_gate_on_bot_comment( + "receipt-nocommit", + "chatgpt-codex-connector", + CODEX_1282_RECEIPT, + ); + assert!( + !out.contains("carry no disposition") && !out.contains("UNANSWERED"), + "Codex's receipt naming no commit is not a finding.\n{out}" + ); + assert_eq!(code, Some(1), "{out}"); + assert!( + out.contains("UNREVIEWED HEAD") && !out.contains("HEAD COVERED"), + "a receipt naming no commit covers no head.\n{out}" + ); +} + +/// The #1282 receipt without its marker, or with a finding after the sign-off, is a claim. +/// +/// The marker is what separates the receipt from the bare phrase, which stays claimable. +/// Green on the gate before and after; the first case goes red if the marker is not +/// required. +#[test] +fn review_gate_treats_a_codex_receipt_naming_no_commit_with_changes_as_a_claim() { + require_jq(); + let unmarked = CODEX_1282_RECEIPT.replace(r"\n\n", ""); + let appended = CODEX_1282_RECEIPT.replace( + "Can't wait for the next one!", + r"Can't wait for the next one!\n\nP1: the lease is dropped before the rename", + ); + for (case, body) in [ + ("without its marker", &unmarked), + ("with a finding appended", &appended), + ] { + assert_ne!( + body.as_str(), + CODEX_1282_RECEIPT, + "{case}: the fixture edit did not apply" + ); + let (code, out) = + run_gate_on_bot_comment("receipt-nocommit-claim", "chatgpt-codex-connector", body); + assert_eq!(code, Some(1), "{case}\n{out}"); + assert!( + out.contains("carry no disposition"), + "the #1282 receipt {case} is a claim.\n{out}" + ); + } +} + /// A bot saying that its review did NOT run is not a finding, and covers no head. /// /// Codex out of quota posts "You have reached your Codex usage limits for code reviews" as