From 78c3a8f762d6514ed54cf8f9c7be76635b3f7fcf Mon Sep 17 00:00:00 2001 From: aloekun Date: Thu, 7 May 2026 21:22:47 +0900 Subject: [PATCH] =?UTF-8?q?feat(cli-pr-monitor):=20verdict=20guard=20for?= =?UTF-8?q?=20review=5Fstate=20not=5Ffound/pending=20+=20transition=20matr?= =?UTF-8?q?ix=20tests=20(=E9=A0=86=E4=BD=8D=2085+86=20/=20Bundle=20g-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit §A-2 Phase 5 dogfood P-1 (Bundle g-1)。3 PR 連続観測 (PR #119/#120/#121) で 発覚した monitor の誤 approved 判定を fix。 順位 85 (Tier 1, T1-1): - compute_verdict() に review_state guard 追加。CodeRabbit が未投稿 (review_state: not_found) もしくは進行中 (pending) のときは findings の有無に関わらず判定保留。空 findings を 'no problems' と誤同一視する false negative を防止。 順位 86 (Tier 2, T2-4): - mod tests に (action, review_state, findings) → verdict transition matrix を 12 unit test で網羅: - parked_rate_limit / parked_review_recheck (action 優先) - not_found + 空 / pending + 空 / not_found + findings (順位 85 fix) - success + 空 / minor / critical / high / major - skipped (skip_coderabbit 経路) - coderabbit field None (初期 state) - 既存 should_resume_wakeup_* テスト 7 件と並存、no regression build + deploy 済 (.claude/cli-pr-monitor.exe を release build で再生成)。 --- src/cli-pr-monitor/src/stages/monitor.rs | 142 +++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/src/cli-pr-monitor/src/stages/monitor.rs b/src/cli-pr-monitor/src/stages/monitor.rs index 59abc10..10b7fce 100644 --- a/src/cli-pr-monitor/src/stages/monitor.rs +++ b/src/cli-pr-monitor/src/stages/monitor.rs @@ -300,6 +300,12 @@ fn compute_verdict(result: &crate::stages::poll::PollResult) -> &'static str { _ => {} } + if let Some(cr) = &result.coderabbit { + if cr.review_state == "not_found" || cr.review_state == "pending" { + return "CodeRabbit review が未完了のため、判定を保留します"; + } + } + let critical_major = result .findings .iter() @@ -425,4 +431,140 @@ mod tests { let pr_info = make_pr_info(42, "o/r", Some("abc1234")); assert!(!should_resume_wakeup(&state, &pr_info, 200)); } + + use crate::stages::poll::PollResult; + use crate::state::CodeRabbitState; + use lib_report_formatter::Finding; + + fn poll_result( + action: &str, + review_state: Option<&str>, + findings: Vec, + ) -> PollResult { + PollResult { + action: action.into(), + summary: "test".into(), + ci: None, + coderabbit: review_state.map(|rs| CodeRabbitState { + review_state: rs.into(), + new_comments: 0, + actionable_comments: None, + unresolved_threads: None, + }), + findings, + check_output: None, + rate_limit: None, + } + } + + fn finding(severity: &str) -> Finding { + Finding { + severity: severity.into(), + file: "f.rs".into(), + line: "1".into(), + issue: "test issue".into(), + suggestion: "test suggestion".into(), + source: "test".into(), + } + } + + const VERDICT_PARK_RATE_LIMIT: &str = + "CodeRabbit rate-limit のため wakeup を予約 (上記 PARK signal 参照)"; + const VERDICT_PARK_REVIEW: &str = "review 完了待ちのため wakeup を予約 (上記 PARK signal 参照)"; + const VERDICT_REVIEW_PENDING: &str = "CodeRabbit review が未完了のため、判定を保留します"; + const VERDICT_NO_PROBLEMS: &str = "問題は見つかりませんでした"; + const VERDICT_MINOR: &str = "重大な問題は見つかりませんでした。軽微な改善提案があります"; + const VERDICT_CRITICAL: &str = "修正が必要な指摘があります"; + + #[test] + fn verdict_park_rate_limit_takes_precedence_over_review_state() { + let r = poll_result("parked_rate_limit", Some("not_found"), vec![]); + assert_eq!(compute_verdict(&r), VERDICT_PARK_RATE_LIMIT); + } + + #[test] + fn verdict_park_review_recheck_takes_precedence_over_findings() { + let r = poll_result("parked_review_recheck", Some("not_found"), vec![finding("critical")]); + assert_eq!(compute_verdict(&r), VERDICT_PARK_REVIEW); + } + + #[test] + fn verdict_pending_when_review_not_found_with_no_findings() { + let r = poll_result("continue_monitoring", Some("not_found"), vec![]); + assert_eq!(compute_verdict(&r), VERDICT_REVIEW_PENDING); + } + + #[test] + fn verdict_pending_when_review_pending_with_no_findings() { + let r = poll_result("continue_monitoring", Some("pending"), vec![]); + assert_eq!(compute_verdict(&r), VERDICT_REVIEW_PENDING); + } + + #[test] + fn verdict_pending_when_review_not_found_even_with_findings() { + let r = poll_result( + "continue_monitoring", + Some("not_found"), + vec![finding("major")], + ); + assert_eq!(compute_verdict(&r), VERDICT_REVIEW_PENDING); + } + + #[test] + fn verdict_no_problems_when_review_success_with_no_findings() { + let r = poll_result("stop_monitoring_success", Some("success"), vec![]); + assert_eq!(compute_verdict(&r), VERDICT_NO_PROBLEMS); + } + + #[test] + fn verdict_minor_when_review_success_with_low_severity_findings() { + let r = poll_result( + "stop_monitoring_success", + Some("success"), + vec![finding("minor")], + ); + assert_eq!(compute_verdict(&r), VERDICT_MINOR); + } + + #[test] + fn verdict_critical_when_review_success_with_critical_findings() { + let r = poll_result( + "stop_monitoring_success", + Some("success"), + vec![finding("critical")], + ); + assert_eq!(compute_verdict(&r), VERDICT_CRITICAL); + } + + #[test] + fn verdict_critical_when_severity_is_high() { + let r = poll_result( + "stop_monitoring_success", + Some("success"), + vec![finding("high")], + ); + assert_eq!(compute_verdict(&r), VERDICT_CRITICAL); + } + + #[test] + fn verdict_critical_when_severity_is_major() { + let r = poll_result( + "stop_monitoring_success", + Some("success"), + vec![finding("major")], + ); + assert_eq!(compute_verdict(&r), VERDICT_CRITICAL); + } + + #[test] + fn verdict_no_problems_when_review_skipped() { + let r = poll_result("stop_monitoring_success", Some("skipped"), vec![]); + assert_eq!(compute_verdict(&r), VERDICT_NO_PROBLEMS); + } + + #[test] + fn verdict_no_problems_when_coderabbit_state_absent() { + let r = poll_result("stop_monitoring_success", None, vec![]); + assert_eq!(compute_verdict(&r), VERDICT_NO_PROBLEMS); + } }