diff --git a/docs/todo-summary.md b/docs/todo-summary.md index e062eea2..aea350e0 100644 --- a/docs/todo-summary.md +++ b/docs/todo-summary.md @@ -99,8 +99,6 @@ | 226 | 🚀 Tier 1 | **fmt baseline cleanup + `cargo fmt --check` gate 導入 + rustfmt 固定 (PR #224 セッション合意)** | todo13.md | M | なし (fmt enforcement がリポジトリに皆無で workspace 29 ファイルが rustfmt-clean でないドリフト蓄積。(A) `cargo fmt --all` 一括正規化 → (B) fmt --check を Stop/push gate に追加 → (C) rust-toolchain.toml で rustfmt 固定、の順。file_length plan と同型「clean baseline → gate」) | | 227 | 🚀 Tier 1 | **rule⑬: 非テストコードでの理由なし `#[allow(...)]` 禁止 custom lint (PR #224 セッション合意)** | todo13.md | S | なし (`#[allow]` = lint の握り潰し、既存 swallowed-error 系 rule③/④/⑩ と同 philosophy。justification マーカー無しの `#[allow(...)]` を warning 検出、test code 除外方式は着手時判断、rule_test_coverage_check で positive/negative test 機械強制) | | 228 | 🔧 Tier 2 | **`rate_limit_signal::cr_clean` の regression test (PR #224 post-merge-feedback T2-1 採用)** | todo13.md | S | なし (Fix 3 で拡張した `unresolved_threads` / `new_comments` / `actionable_comments` 3 field の clean 判定の回帰防止、None/境界ケース網羅、silent-clean 誤認の保護) | -| 229 | 🔧 Tier 2 | **共有 `env_override_lock` helper 抽出 — 並列 test の state file override 競合解消 (PR #224 post-merge-feedback T2-2 採用)** | todo13.md | S | なし (複数 test module の独立 static LOCK を単一化。memory `feedback_test_dry_antipattern` の per-module 複製原則との境界 = serialization primitive は単一必須、要ユーザー確認) | -| 230 | 🔧 Tier 2 | **`create_pr::tests` temp file collision 修正 (flaky test) — `tempfile` 移行 (PR #224 post-merge-feedback T2-4 採用)** | todo13.md | S | なし (並列 `cargo test` で `body_with_literal_newline_converted` が PID+ms 衝突で稀 fail、pre-existing。target は `cli-pr-monitor/src/stages/create_pr.rs` (report の cli-github は誤り)、CI 信頼性) | | 231 | 💎 Tier 3 | **ADR-022 拡張 — pre-create cleanup flow 例 + agent fmt スコープ指針 (PR #224 post-merge-feedback T3-1 採用)** | todo13.md | S | なし (CodeRabbit が `create_fix_commit` の空 findings 設計を bug 誤判定=却下 CR#2、agent 無差別 fmt の 2 事象を ADR-022 責務分離で codify、doc-only) | | 232 | 🔧 Tier 2 | **post-merge-feedback / workflow agent の repo 作業ツリー書込禁止 + 検知安全網 (PR #224 セッション合意)** | todo13.md | S-M | なし (merge 時に analyze-session agent が repo root に throwaway script (parse_transcript.py) を残した、日常工程ゆえ累積リスク = コンテキスト汚染。(1) feedback facets に repo 書込禁止 + jq/scratch 使用を明記 (2) post_steps/Stop hook で root 新規 untracked を warning 検知 (3) gitignore は補助) | diff --git a/docs/todo13.md b/docs/todo13.md index 91bd62dd..12bd05bf 100644 --- a/docs/todo13.md +++ b/docs/todo13.md @@ -137,54 +137,6 @@ --- -### 共有 `env_override_lock` helper 抽出 — 並列 test の state file override 競合解消 (PR #224 post-merge-feedback T2-2 採用) - -> **動機**: `PR_MONITOR_STATE_FILE_OVERRIDE` (process-global env var) 用の `static LOCK` を複数の test module (poll/mod.rs, rate_limit.rs, review_recheck.rs 等) が独立定義しており、並列 `cargo test` で state file override が競合するリスク。CodeRabbit outside-diff comment でも指摘。 -> -> **本タスクの位置づけ**: PR #224 post-merge-feedback Tier 2 #2 採用 (Effort S / Frequency Medium)。 -> -> **参照**: `.claude/feedback-reports/224.md` Tier 2 #2、各 test module の `env_override_lock()` / `static LOCK`、memory `feedback_test_dry_antipattern` (※下記の境界判断あり)。 -> -> **実行優先度**: 🔧 **Tier 2** — Effort S。 -> -> **重要 (規約整合)**: memory `feedback_test_dry_antipattern` は「test helper は共有 module 抽出せず per-module 複製」を原則とする。一方 env override LOCK は **process-global な serialization 用 static** で、複製すると各々が別 Mutex になり並列競合を防げない (= 複製が機能を壊す特殊ケース)。本タスクは「複製が正しい通常 helper」と「単一 static が必須な lock」を区別し、後者のみ共有化する。着手前にこの境界をユーザー確認。 - -#### 作業計画 - -- [ ] env override 用の単一 `static LOCK` を 1 箇所に集約し各 test module から参照 -- [ ] `feedback_test_dry_antipattern` との整合を明文化 (serialization primitive = 単一必須 / 通常 helper = 複製、の区別) -- [ ] 並列 `cargo test` で state file override 競合が起きないことを確認 -- [ ] 本 entry 削除 + todo-summary.md 行削除 - -#### 完了基準 - -- env override の serialization が単一 LOCK で保証され並列 test で競合しない。per-module 複製原則との境界がドキュメント化される。 - ---- - -### `create_pr::tests` temp file collision 修正 (flaky test) — `tempfile` 移行 (PR #224 post-merge-feedback T2-4 採用) - -> **動機**: 並列 `cargo test` で `create_pr::tests::body_with_literal_newline_converted` が稀に fail する (temp file 名が PID+ms timestamp で衝突)。PR-W2 分割以前から存在する pre-existing flaky で、PR #224 セッション中に実観測。flaky の常態化は test 全体の信頼性 (グリーンの価値) を損なう。 -> -> **本タスクの位置づけ**: PR #224 post-merge-feedback Tier 2 #4 採用 + セッション実観測。 -> -> **参照**: `.claude/feedback-reports/224.md` Tier 2 #4、`src/cli-pr-monitor/src/stages/create_pr.rs` `convert_body_to_file` (temp file 名生成。※ report の target `src/cli-github/...` は誤りで、実際は cli-pr-monitor)、`tempfile` crate (既に依存)。 -> -> **実行優先度**: 🔧 **Tier 2** — Effort S。CI 信頼性。 - -#### 作業計画 - -- [ ] `convert_body_to_file` の temp file 名生成を PID+ms timestamp から `tempfile::NamedTempFile` (一意保証) に移行 -- [ ] 繰り返し並列実行で衝突しないことを確認 -- [ ] `cargo test -p cli-pr-monitor` pass -- [ ] 本 entry 削除 + todo-summary.md 行削除 - -#### 完了基準 - -- `body_with_literal_newline_converted` 等が並列実行でも安定 pass、temp file 名衝突が構造的に解消。 - ---- - ### ADR-022 拡張 — pre-create cleanup flow の具体例 + agent fmt スコープ指針 (PR #224 post-merge-feedback T3-1 採用) > **動機**: PR #224 で CodeRabbit が `create_fix_commit` の「空 findings でも commit 作成」を bug と誤判定した (ADR-022 の意図的な pre-create 設計を知らなかったため、却下した CR#2)。また分割 agent が無差別 `cargo fmt` を実行した事象も ADR-022 の責務分離原則で説明可能。両事象とも将来再発が見込まれる。 diff --git a/src/cli-pr-monitor/Cargo.toml b/src/cli-pr-monitor/Cargo.toml index 6ce11f36..762b6f93 100644 --- a/src/cli-pr-monitor/Cargo.toml +++ b/src/cli-pr-monitor/Cargo.toml @@ -7,13 +7,15 @@ edition = "2021" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" toml = "0.8" +# tempfile は本番の convert_body_to_file が --body-file 用一時ファイルを +# 一意名 (O_EXCL + ランダム名) で生成するために使用 (順位 230 flaky 修正)。 +tempfile = "3" lib-jj-helpers = { path = "../lib-jj-helpers" } lib-pending-file = { path = "../lib-pending-file" } lib-report-formatter = { path = "../lib-report-formatter" } lib-subprocess = { path = "../lib-subprocess" } [dev-dependencies] -tempfile = "3" proptest = "1" # [profile.release] は workspace root (Cargo.toml) に集約 (ADR-026) diff --git a/src/cli-pr-monitor/src/stages/create_pr.rs b/src/cli-pr-monitor/src/stages/create_pr.rs index 4153e480..46f68c97 100644 --- a/src/cli-pr-monitor/src/stages/create_pr.rs +++ b/src/cli-pr-monitor/src/stages/create_pr.rs @@ -1,10 +1,12 @@ -use std::path::PathBuf; +use std::path::Path; + +use tempfile::TempPath; use crate::config::DEFAULT_STEP_TIMEOUT_SECS; use crate::log::log_info; use crate::runner::{run_cmd_direct, run_gh_quiet}; use crate::stages::monitor::start_monitoring; -use crate::state::{write_state, PrMonitorState}; +use crate::state::{state_file_path, write_state_to, PrMonitorState}; use crate::util::{ get_jj_bookmarks, get_pr_head_commit, get_pr_info, parse_pr_number_from_url, utc_now_iso8601, PrInfo, @@ -43,46 +45,45 @@ fn reassemble_split_body(args: &[String]) -> Vec { result } -// ─── --body -> --body-file 変換 (issue #1) ─── - -/// Drop 時に自動削除される一時ファイル -struct TempFile(PathBuf); - -impl Drop for TempFile { - fn drop(&mut self) { - let _ = std::fs::remove_file(&self.0); - } +/// `dir` 配下に一意な一時ファイルを作成し `content` を書き出す。 +/// +/// 返り値の `TempPath` は Drop 時に自動削除される (旧 `TempFile` struct の役割)。 +/// `tempfile` は `O_EXCL` + ランダム名 + 衝突時リトライでファイルを作るため、 +/// 並列 `cargo test` (同一プロセス・同一 ms) でも本番の並列実行 (別セッション / +/// サブエージェント) でも名前が構造的に衝突しない (順位 230 flaky 修正)。 +fn write_body_tempfile(dir: &Path, content: &str) -> std::io::Result { + let path = tempfile::Builder::new() + .prefix("gh-pr-body-") + .suffix(".md") + .tempfile_in(dir)? + .into_temp_path(); + std::fs::write(&path, content)?; + Ok(path) } /// --body 引数に改行が含まれる場合、一時ファイルに書き出して --body-file に差し替える。 -fn convert_body_to_file(args: &[String]) -> (Vec, Option) { +/// +/// `temp_dir` は生成先ディレクトリ。本番は `std::env::temp_dir()` を渡し、テストは +/// 各 test 専用の `tempfile::tempdir()` を注入して本番 namespace から分離する。 +fn convert_body_to_file(args: &[String], temp_dir: &Path) -> (Vec, Option) { let mut result = Vec::with_capacity(args.len()); let mut i = 0; - let mut temp_guard: Option = None; + let mut temp_guard: Option = None; while i < args.len() { if args[i] == "--body" && i + 1 < args.len() { let body = &args[i + 1]; if body.contains('\n') || body.contains("\\n") { - let filename = format!( - "gh-pr-body-{}-{}.md", - std::process::id(), - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() - ); - let path = std::env::temp_dir().join(filename); let resolved = body.replace("\\n", "\n"); - match std::fs::write(&path, &resolved) { - Ok(()) => { + match write_body_tempfile(temp_dir, &resolved) { + Ok(path) => { log_info(&format!( "--body に改行を検出 → --body-file に変換 ({})", path.display() )); result.push("--body-file".to_string()); result.push(path.to_string_lossy().to_string()); - temp_guard = Some(TempFile(path)); + temp_guard = Some(path); } Err(e) => { log_info(&format!( @@ -203,14 +204,14 @@ pub(crate) fn run_create_pr(gh_args: &[String]) -> i32 { fn write_early_reset_state() { let early_state = PrMonitorState::new(None, None, utc_now_iso8601()); - if let Err(e) = write_state(&early_state) { + if let Err(e) = write_state_to(&state_file_path(), &early_state) { log_info(&format!("[state] 早期 reset 書き込み失敗 (継続): {}", e)); } } -fn prepare_gh_pr_create_args(gh_args: &[String]) -> (Vec, Option) { +fn prepare_gh_pr_create_args(gh_args: &[String]) -> (Vec, Option) { let reassembled = reassemble_split_body(gh_args); - let (mut final_args, body_tempfile) = convert_body_to_file(&reassembled); + let (mut final_args, body_tempfile) = convert_body_to_file(&reassembled, &std::env::temp_dir()); if !has_head_flag(&final_args) { let bookmarks = get_jj_bookmarks(); @@ -380,46 +381,50 @@ mod tests { #[test] fn body_without_newline_unchanged() { + let dir = tempfile::tempdir().unwrap(); let args = vec![ "--title".into(), "test".into(), "--body".into(), "simple body".into(), ]; - let (result, temp) = convert_body_to_file(&args); + let (result, temp) = convert_body_to_file(&args, dir.path()); assert_eq!(result, args); assert!(temp.is_none()); } #[test] fn body_with_literal_newline_converted() { + let dir = tempfile::tempdir().unwrap(); let args = vec![ "--title".into(), "test".into(), "--body".into(), "line1\\nline2".into(), ]; - let (result, temp) = convert_body_to_file(&args); + let (result, temp) = convert_body_to_file(&args, dir.path()); assert_eq!(result[0], "--title"); assert_eq!(result[1], "test"); assert_eq!(result[2], "--body-file"); assert!(temp.is_some()); - let content = std::fs::read_to_string(&temp.as_ref().unwrap().0).unwrap(); + let content = std::fs::read_to_string(temp.as_ref().unwrap()).unwrap(); assert!(content.contains("line1\nline2")); } #[test] fn body_with_real_newline_converted() { + let dir = tempfile::tempdir().unwrap(); let args = vec!["--body".into(), "line1\nline2".into()]; - let (result, temp) = convert_body_to_file(&args); + let (result, temp) = convert_body_to_file(&args, dir.path()); assert_eq!(result[0], "--body-file"); assert!(temp.is_some()); } #[test] fn no_body_arg_unchanged() { + let dir = tempfile::tempdir().unwrap(); let args = vec!["--title".into(), "test".into()]; - let (result, temp) = convert_body_to_file(&args); + let (result, temp) = convert_body_to_file(&args, dir.path()); assert_eq!(result, args); assert!(temp.is_none()); } diff --git a/src/cli-pr-monitor/src/stages/monitor.rs b/src/cli-pr-monitor/src/stages/monitor.rs index 2fbe5afc..aabe6840 100644 --- a/src/cli-pr-monitor/src/stages/monitor.rs +++ b/src/cli-pr-monitor/src/stages/monitor.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use crate::config::load_config; use crate::fix_commit::{create_fix_commit, FixCommitState}; use crate::lock::{acquire as acquire_lock, LockResult}; @@ -6,7 +8,7 @@ use crate::stages::collect::collect_findings; use crate::stages::poll::run_poll_loop; use crate::stages::repush::execute_repush_flow; use crate::stages::takt::run_takt; -use crate::state::{read_state, write_state, PrMonitorState}; +use crate::state::{read_state_from, state_file_path, write_state_to, PrMonitorState}; use crate::util::{get_pr_info, utc_now_iso8601, PrInfo}; // ─── 監視開始 (sequential chain) ─── @@ -95,7 +97,7 @@ fn init_or_resume_state(pr_info: &PrInfo, is_wakeup: bool, pr_label: &str) { pr_info.push_time.clone().unwrap_or_else(utc_now_iso8601), ); init_state.fix_push_time = pr_info.fix_push_time.clone(); - if let Err(e) = write_state(&init_state) { + if let Err(e) = write_state_to(&state_file_path(), &init_state) { log_info(&format!("[state] 初期化書き込み失敗 (継続): {}", e)); } } @@ -206,7 +208,8 @@ pub(crate) fn run_monitor_only() -> i32 { resume_push_time )); pr_info.push_time = Some(resume_push_time.clone()); - pr_info.fix_push_time = resume_fix_push_time_or_started_at(&resume_push_time); + pr_info.fix_push_time = + resume_fix_push_time_or_started_at(&resume_push_time, &state_file_path()); start_monitoring_wakeup(&pr_info) } else { let now = utc_now_iso8601(); @@ -218,8 +221,11 @@ pub(crate) fn run_monitor_only() -> i32 { /// 順位 141: wakeup resume 経路で state から `fix_push_time` を取り出す。 /// legacy state (本フィールド未設定) では `started_at` に fallback して挙動を維持する。 -fn resume_fix_push_time_or_started_at(started_at_fallback: &str) -> Option { - read_state() +fn resume_fix_push_time_or_started_at( + started_at_fallback: &str, + state_path: &Path, +) -> Option { + read_state_from(state_path) .and_then(|s| s.fix_push_time) .or_else(|| Some(started_at_fallback.to_string())) } @@ -230,7 +236,7 @@ fn resume_fix_push_time_or_started_at(started_at_fallback: &str) -> Option Option { - let state = read_state()?; + let state = read_state_from(&state_file_path())?; let now_unix = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs() as i64) @@ -581,27 +587,16 @@ mod tests { assert_eq!(compute_verdict(&r), VERDICT_NO_PROBLEMS); } - /// PR_MONITOR_STATE_FILE_OVERRIDE は process-global env var のため、 - /// override 設定 / 解除を test 並行実行で race させない serial guard。 - fn env_override_lock() -> std::sync::MutexGuard<'static, ()> { - use std::sync::{Mutex, OnceLock}; - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())).lock().unwrap() - } - /// 順位 141: `resume_fix_push_time_or_started_at` Case A — /// state に `fix_push_time` が設定済みの場合、fallback の `started_at` ではなく /// state の値が返されることを検証する。 #[test] fn resume_returns_fix_push_time_from_state_when_set() { - let _guard = env_override_lock(); let tmp = tempfile::NamedTempFile::new().unwrap(); let mut s = PrMonitorState::new(Some(1), None, "t".into()); s.fix_push_time = Some("2026-05-22T06:06:00Z".into()); std::fs::write(tmp.path(), serde_json::to_string(&s).unwrap()).unwrap(); - std::env::set_var("PR_MONITOR_STATE_FILE_OVERRIDE", tmp.path()); - let result = resume_fix_push_time_or_started_at("2026-05-22T06:00:00Z"); - std::env::remove_var("PR_MONITOR_STATE_FILE_OVERRIDE"); + let result = resume_fix_push_time_or_started_at("2026-05-22T06:00:00Z", tmp.path()); assert_eq!( result.as_deref(), Some("2026-05-22T06:06:00Z"), diff --git a/src/cli-pr-monitor/src/stages/poll/iteration.rs b/src/cli-pr-monitor/src/stages/poll/iteration.rs index 99b82b51..bc4f6d06 100644 --- a/src/cli-pr-monitor/src/stages/poll/iteration.rs +++ b/src/cli-pr-monitor/src/stages/poll/iteration.rs @@ -1,3 +1,4 @@ +use std::path::Path; use std::time::Duration; use crate::classifier_runner::classify_findings; @@ -5,7 +6,7 @@ use crate::config::{ClassifierConfig, DEFAULT_CHECK_TIMEOUT_SECS}; use crate::log::{log_info, truncate_safe}; use crate::runner::run_cmd_direct; use crate::state::{ - read_state, update_state_from_check_result, write_state, CiState, CodeRabbitState, + read_state_from, update_state_from_check_result, write_state_to, CiState, CodeRabbitState, PrMonitorState, }; use crate::util::{utc_now_iso8601, PrInfo}; @@ -26,8 +27,9 @@ pub(super) fn run_one_iteration(ctx: &PollContext<'_>) -> Option { &result, ctx.skip_ci, ctx.skip_coderabbit, + ctx.state_path, ); - enrich_with_classifier(&mut state, ctx.classifier_config); + enrich_with_classifier(&mut state, ctx.classifier_config, ctx.state_path); log_info(&format!( "ポーリング: action={}, summary={}", state.action, state.summary @@ -43,6 +45,7 @@ pub(super) fn run_one_iteration(ctx: &PollContext<'_>) -> Option { ctx.pr_info, ctx.review_recheck_wait_secs, &result, + ctx.state_path, ) { return Some(terminal); } @@ -104,6 +107,7 @@ fn build_state_for_iteration( result: &serde_json::Value, skip_ci: bool, skip_coderabbit: bool, + state_path: &Path, ) -> PrMonitorState { let mut state = PrMonitorState::new( pr_info.pr_number, @@ -112,7 +116,7 @@ fn build_state_for_iteration( ); update_state_from_check_result(&mut state, result); - if let Some(existing) = read_state() { + if let Some(existing) = read_state_from(state_path) { state.notified = existing.notified; state.rate_limit_retries = existing.rate_limit_retries; state.rate_limit_last_retriggered_at = existing.rate_limit_last_retriggered_at; @@ -124,7 +128,7 @@ fn build_state_for_iteration( apply_skip_handling(&mut state, skip_ci, skip_coderabbit); state.last_checked = Some(utc_now_iso8601()); - if let Err(e) = write_state(&state) { + if let Err(e) = write_state_to(state_path, &state) { log_info(&format!("state 書き込み失敗 (skip 反映後、続行): {}", e)); } state @@ -135,7 +139,11 @@ fn build_state_for_iteration( /// `config.classifier.enabled = false` または findings が空のときは何もしない。 /// 実行成功時は state.classified_findings を populate して state file を再書き出す。 /// 失敗時は state.classified_findings は空のまま (caller は findings をそのまま使えばよい)。 -fn enrich_with_classifier(state: &mut PrMonitorState, config: &ClassifierConfig) { +fn enrich_with_classifier( + state: &mut PrMonitorState, + config: &ClassifierConfig, + state_path: &Path, +) { if !config.enabled || state.findings.is_empty() { return; } @@ -148,7 +156,7 @@ fn enrich_with_classifier(state: &mut PrMonitorState, config: &ClassifierConfig) classified.len() )); state.classified_findings = classified; - if let Err(e) = write_state(state) { + if let Err(e) = write_state_to(state_path, state) { log_info(&format!( "state 書き込み失敗 (classifier enrich 後、続行): {}", e @@ -312,8 +320,9 @@ mod tests { enabled: false, ..ClassifierConfig::default() }; + let dir = tempfile::tempdir().unwrap(); - enrich_with_classifier(&mut state, &disabled); + enrich_with_classifier(&mut state, &disabled, &dir.path().join("state.json")); assert_eq!( state.classified_findings, @@ -353,8 +362,9 @@ mod tests { enabled: true, ..ClassifierConfig::default() }; + let dir = tempfile::tempdir().unwrap(); - enrich_with_classifier(&mut state, &enabled); + enrich_with_classifier(&mut state, &enabled, &dir.path().join("state.json")); assert_eq!( state.classified_findings, diff --git a/src/cli-pr-monitor/src/stages/poll/mod.rs b/src/cli-pr-monitor/src/stages/poll/mod.rs index b6478dca..d69dfc07 100644 --- a/src/cli-pr-monitor/src/stages/poll/mod.rs +++ b/src/cli-pr-monitor/src/stages/poll/mod.rs @@ -30,6 +30,8 @@ pub(crate) struct PollResult { pub(super) struct PollContext<'a> { pub(super) checker: &'a std::path::Path, + /// state file の保存先 (順位 229: テストは自前 path を注入し env var 競合を排除)。 + pub(super) state_path: &'a std::path::Path, pub(super) push_time: &'a str, /// 順位 141: fresh push 時刻の固定値 (CR rate-limit detection bug 修正)。 /// 設定されていれば `build_checker_args` で `--push-time` に優先採用される。 @@ -70,8 +72,10 @@ pub(crate) fn run_poll_loop(full_config: &Config, pr_info: &PrInfo, is_wakeup: b return error_poll_result("check-ci-coderabbit.exe が見つかりません"); } + let state_path = crate::state::state_file_path(); let ctx = PollContext { checker: &checker, + state_path: &state_path, push_time: pr_info .push_time .as_deref() @@ -131,15 +135,7 @@ mod tests { use rate_limit::finalize_parked; use review_recheck::{finalize_initial_review_park, schedule_next_review_recheck_park}; - /// PR_MONITOR_STATE_FILE_OVERRIDE は process-global env var のため、 - /// override 設定 / 解除を test 並行実行で race させない serial guard。 - fn env_override_lock() -> std::sync::MutexGuard<'static, ()> { - use std::sync::{Mutex, OnceLock}; - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())).lock().unwrap() - } - - /// 書き込み先がディレクトリ不在のため write が必ず失敗する override path を返す。 + /// 書き込み先がディレクトリ不在のため write が必ず失敗する path を返す。 fn unwritable_state_path() -> std::path::PathBuf { std::env::temp_dir() .join(format!("pr-monitor-T2-2-{}", std::process::id())) @@ -147,7 +143,10 @@ mod tests { .join("state.json") } - fn invoke_finalize_parked_with_bad_path(pr_info: &crate::util::PrInfo) -> PollResult { + fn invoke_finalize_parked_with_bad_path( + pr_info: &crate::util::PrInfo, + state_path: &std::path::Path, + ) -> PollResult { let mut state = PrMonitorState::new(Some(1), Some("o/r".into()), "t".into()); let rl = RateLimitState { until_unix_secs: 1_775_088_000, @@ -156,10 +155,21 @@ mod tests { wait_seconds: 0, }; let result = serde_json::json!({}); - finalize_parked(&mut state, &rl, pr_info, 1_775_088_000, 3, &result) + finalize_parked( + &mut state, + &rl, + pr_info, + 1_775_088_000, + 3, + &result, + state_path, + ) } - fn invoke_review_park_with_bad_path(pr_info: &crate::util::PrInfo) -> PollResult { + fn invoke_review_park_with_bad_path( + pr_info: &crate::util::PrInfo, + state_path: &std::path::Path, + ) -> PollResult { let mut state = PrMonitorState::new(Some(1), Some("o/r".into()), "2026-05-01T00:00:00Z".into()); state.review_recheck_count = 1; @@ -168,6 +178,7 @@ mod tests { let classifier_config = ClassifierConfig::default(); let ctx = PollContext { checker: &checker_path, + state_path, push_time: "2026-05-01T00:00:00Z", fix_push_time: None, pr_info, @@ -186,12 +197,14 @@ mod tests { fn invoke_finalize_initial_review_park_with_bad_path( pr_info: &crate::util::PrInfo, + state_path: &std::path::Path, ) -> PollResult { let checker_path = std::path::PathBuf::from("dummy"); let rate_limit_config = RateLimitConfig::default(); let classifier_config = ClassifierConfig::default(); let ctx = PollContext { checker: &checker_path, + state_path, push_time: "2026-05-01T00:00:00Z", fix_push_time: None, pr_info, @@ -215,9 +228,7 @@ mod tests { /// invariant 維持を強制する。 #[test] fn finalize_park_siblings_have_symmetric_write_state_handling() { - let _guard = env_override_lock(); let bad_path = unwritable_state_path(); - std::env::set_var("PR_MONITOR_STATE_FILE_OVERRIDE", &bad_path); let pr_info = crate::util::PrInfo { pr_number: Some(1), @@ -227,11 +238,10 @@ mod tests { fix_push_time: None, }; - let outcome_rate_limit = invoke_finalize_parked_with_bad_path(&pr_info); - let outcome_review = invoke_review_park_with_bad_path(&pr_info); - let outcome_initial = invoke_finalize_initial_review_park_with_bad_path(&pr_info); - - std::env::remove_var("PR_MONITOR_STATE_FILE_OVERRIDE"); + let outcome_rate_limit = invoke_finalize_parked_with_bad_path(&pr_info, &bad_path); + let outcome_review = invoke_review_park_with_bad_path(&pr_info, &bad_path); + let outcome_initial = + invoke_finalize_initial_review_park_with_bad_path(&pr_info, &bad_path); assert_eq!( outcome_rate_limit.action, "action_required", diff --git a/src/cli-pr-monitor/src/stages/poll/rate_limit.rs b/src/cli-pr-monitor/src/stages/poll/rate_limit.rs index cde81aeb..2653ade4 100644 --- a/src/cli-pr-monitor/src/stages/poll/rate_limit.rs +++ b/src/cli-pr-monitor/src/stages/poll/rate_limit.rs @@ -10,10 +10,12 @@ //! signal 整形部分 (`format_park_signal` / shortcut signal / //! `format_posted_retrigger_review_park_signal`) は `rate_limit_signal.rs` に分離。 +use std::path::Path; + use crate::config::RateLimitConfig; use crate::log::log_info; use crate::runner::run_gh_quiet; -use crate::state::{write_state, PrMonitorState}; +use crate::state::{write_state_to, PrMonitorState}; use crate::util::PrInfo; use super::rate_limit_signal::{ @@ -33,6 +35,7 @@ pub(super) fn handle_rate_limit_branch( pr_info: &PrInfo, review_recheck_wait_secs: u64, result: &serde_json::Value, + state_path: &Path, ) -> Option { let rl = state.rate_limit.clone()?; let already_handled = @@ -65,6 +68,7 @@ pub(super) fn handle_rate_limit_branch( rate_limit_config.max_retries, review_recheck_wait_secs, result, + state_path, ) } @@ -75,11 +79,17 @@ fn dispatch_rate_limit_outcome( max_retries: u32, review_recheck_wait_secs: u64, result: &serde_json::Value, + state_path: &Path, ) -> Option { match handle_rate_limit_retry(rl, state, pr_info, max_retries) { - RateLimitOutcome::Posted => { - finalize_posted_retrigger(state, rl, pr_info, review_recheck_wait_secs, result) - } + RateLimitOutcome::Posted => finalize_posted_retrigger( + state, + rl, + pr_info, + review_recheck_wait_secs, + result, + state_path, + ), RateLimitOutcome::Parked { wakeup_at_unix } => Some(finalize_parked( state, rl, @@ -87,6 +97,7 @@ fn dispatch_rate_limit_outcome( wakeup_at_unix, max_retries, result, + state_path, )), RateLimitOutcome::Failed(e) => { log_info(&format!("[rate_limit] retrigger 失敗: {}", e)); @@ -108,6 +119,7 @@ pub(super) fn finalize_posted_retrigger( pr_info: &PrInfo, review_recheck_wait_secs: u64, result: &serde_json::Value, + state_path: &Path, ) -> Option { state.rate_limit_last_retriggered_at = Some(rl.comment_event_time.clone()); @@ -126,7 +138,7 @@ pub(super) fn finalize_posted_retrigger( review_recheck_wait_secs ); - if let Err(e) = write_state(state) { + if let Err(e) = write_state_to(state_path, state) { log_info(&format!( "[rate_limit] retrigger 後の state 永続化失敗、自動 retry を停止: {}", e @@ -154,6 +166,7 @@ pub(super) fn finalize_parked( wakeup_at_unix: i64, max_retries: u32, result: &serde_json::Value, + state_path: &Path, ) -> PollResult { state.action = "parked_rate_limit".into(); state.next_wakeup_at_unix = Some(wakeup_at_unix); @@ -163,7 +176,7 @@ pub(super) fn finalize_parked( "CodeRabbit rate-limit: wakeup を {}m{}s 後に予約 (PARK signal 参照)", rl.wait_minutes, rl.wait_seconds ); - if let Err(e) = write_state(state) { + if let Err(e) = write_state_to(state_path, state) { let msg = format!("park state 永続化失敗のため PARK signal を中止 ({})。手動で `@coderabbitai review` を投稿してください", e); return make_action_required_result(state, result, &msg); } @@ -441,15 +454,7 @@ mod tests { assert!(state.rate_limit_last_retriggered_at.is_none()); } - /// PR_MONITOR_STATE_FILE_OVERRIDE は process-global env var のため、 - /// override 設定 / 解除を test 並行実行で race させない serial guard。 - fn env_override_lock() -> std::sync::MutexGuard<'static, ()> { - use std::sync::{Mutex, OnceLock}; - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())).lock().unwrap() - } - - /// 書き込み先がディレクトリ不在のため write が必ず失敗する override path を返す。 + /// 書き込み先がディレクトリ不在のため write が必ず失敗する path を返す。 fn unwritable_state_path() -> std::path::PathBuf { std::env::temp_dir() .join(format!("pr-monitor-T2-2-{}", std::process::id())) @@ -461,9 +466,7 @@ mod tests { /// `action_required` を返却する fail-safe 経路を持つ (CodeRabbit Major #1 fix の固定化)。 #[test] fn finalize_parked_returns_action_required_when_write_state_fails() { - let _guard = env_override_lock(); let bad_path = unwritable_state_path(); - std::env::set_var("PR_MONITOR_STATE_FILE_OVERRIDE", &bad_path); let mut state = PrMonitorState::new(Some(42), Some("o/r".into()), "t".into()); let rl = RateLimitState { @@ -481,9 +484,15 @@ mod tests { }; let result = serde_json::json!({}); - let outcome = finalize_parked(&mut state, &rl, &pr_info, 1_775_088_000, 3, &result); - - std::env::remove_var("PR_MONITOR_STATE_FILE_OVERRIDE"); + let outcome = finalize_parked( + &mut state, + &rl, + &pr_info, + 1_775_088_000, + 3, + &result, + &bad_path, + ); assert_eq!( outcome.action, "action_required", @@ -519,16 +528,18 @@ mod tests { #[test] fn finalize_posted_retrigger_schedules_park_after_post() { - let _guard = env_override_lock(); let tmp = tempfile::tempdir().unwrap(); let state_path = tmp.path().join("state.json"); - std::env::set_var("PR_MONITOR_STATE_FILE_OVERRIDE", &state_path); let (mut state, rl, pr_info) = setup_posted_retrigger_fixture(); - let result = - finalize_posted_retrigger(&mut state, &rl, &pr_info, 300, &serde_json::Value::Null); - - std::env::remove_var("PR_MONITOR_STATE_FILE_OVERRIDE"); + let result = finalize_posted_retrigger( + &mut state, + &rl, + &pr_info, + 300, + &serde_json::Value::Null, + &state_path, + ); let park_result = result.expect("順位 80 fix: Posted 後は必ず park を返し silent exit を防ぐ"); @@ -553,9 +564,7 @@ mod tests { #[test] fn finalize_posted_retrigger_action_required_when_write_state_fails() { - let _guard = env_override_lock(); let bad_path = unwritable_state_path(); - std::env::set_var("PR_MONITOR_STATE_FILE_OVERRIDE", &bad_path); let mut state = PrMonitorState::new(Some(1), Some("o/r".into()), "t".into()); state.action = "continue_monitoring".into(); @@ -573,10 +582,14 @@ mod tests { fix_push_time: None, }; - let result = - finalize_posted_retrigger(&mut state, &rl, &pr_info, 300, &serde_json::Value::Null); - - std::env::remove_var("PR_MONITOR_STATE_FILE_OVERRIDE"); + let result = finalize_posted_retrigger( + &mut state, + &rl, + &pr_info, + 300, + &serde_json::Value::Null, + &bad_path, + ); assert!(result.is_some()); assert_eq!( diff --git a/src/cli-pr-monitor/src/stages/poll/review_recheck.rs b/src/cli-pr-monitor/src/stages/poll/review_recheck.rs index a79c6710..727fa43d 100644 --- a/src/cli-pr-monitor/src/stages/poll/review_recheck.rs +++ b/src/cli-pr-monitor/src/stages/poll/review_recheck.rs @@ -7,8 +7,10 @@ //! signal 整形部分 (`round_up_to_next_minute` / `compute_safe_minute_for_park_signal` / //! `format_review_park_signal`) は `review_recheck_signal.rs` に分離。 +use std::path::Path; + use crate::log::log_info; -use crate::state::{read_state, write_state, PrMonitorState}; +use crate::state::{read_state_from, write_state_to, PrMonitorState}; use super::rate_limit::make_action_required_result; use super::review_recheck_signal::format_review_park_signal; @@ -25,7 +27,7 @@ use super::{make_park_poll_result, PollContext, PollResult}; /// 食い違い、最悪 max 到達状態で park される)。 /// CR Major #1 fix: head_commit を state に保存し detect_wakeup_resume の比較対象とする。 pub(super) fn finalize_initial_review_park(ctx: &PollContext<'_>) -> PollResult { - let mut state = read_state().unwrap_or_else(|| { + let mut state = read_state_from(ctx.state_path).unwrap_or_else(|| { PrMonitorState::new( ctx.pr_info.pr_number, ctx.pr_info.repo.clone(), @@ -52,7 +54,7 @@ pub(super) fn finalize_initial_review_park(ctx: &PollContext<'_>) -> PollResult ctx.initial_review_wait_secs, ctx.max_review_rechecks ); - if let Err(e) = write_state(&state) { + if let Err(e) = write_state_to(ctx.state_path, &state) { log_info(&format!( "[review_recheck] initial park state 永続化失敗、action_required で抜ける: {}", e @@ -74,7 +76,7 @@ pub(super) fn finalize_initial_review_park(ctx: &PollContext<'_>) -> PollResult /// `action_required` で抜ける (review が想定時間内に未完了を通知)。 /// 未到達なら `review_recheck_wait_secs` 後の wakeup を予約して return。 pub(super) fn finalize_review_recheck_park(ctx: &PollContext<'_>) -> PollResult { - let mut state = read_state().unwrap_or_else(|| { + let mut state = read_state_from(ctx.state_path).unwrap_or_else(|| { PrMonitorState::new( ctx.pr_info.pr_number, ctx.pr_info.repo.clone(), @@ -87,7 +89,11 @@ pub(super) fn finalize_review_recheck_park(ctx: &PollContext<'_>) -> PollResult .or_else(|| ctx.fix_push_time.map(String::from)); if state.review_recheck_count >= ctx.max_review_rechecks { - return finalize_review_recheck_max_reached(&mut state, ctx.max_review_rechecks); + return finalize_review_recheck_max_reached( + &mut state, + ctx.state_path, + ctx.max_review_rechecks, + ); } schedule_next_review_recheck_park(&mut state, ctx) @@ -95,6 +101,7 @@ pub(super) fn finalize_review_recheck_park(ctx: &PollContext<'_>) -> PollResult fn finalize_review_recheck_max_reached( state: &mut PrMonitorState, + state_path: &Path, max_review_rechecks: u32, ) -> PollResult { log_info(&format!( @@ -109,7 +116,7 @@ fn finalize_review_recheck_max_reached( state.summary = summary.clone(); state.next_wakeup_at_unix = None; state.wakeup_reason = None; - if let Err(e) = write_state(state) { + if let Err(e) = write_state_to(state_path, state) { log_info(&format!( "state 書き込み失敗 (action_required 確定後、続行): {}", e @@ -135,7 +142,7 @@ pub(super) fn schedule_next_review_recheck_park( ctx.review_recheck_wait_secs, state.review_recheck_count, ctx.max_review_rechecks ); - if let Err(e) = write_state(state) { + if let Err(e) = write_state_to(ctx.state_path, state) { log_info(&format!( "[review_recheck] park state 永続化失敗、action_required で抜ける: {}", e @@ -155,23 +162,8 @@ pub(super) fn schedule_next_review_recheck_park( mod tests { use super::*; use crate::config::{ClassifierConfig, RateLimitConfig}; - use std::sync::{Mutex, OnceLock}; - - fn env_lock() -> std::sync::MutexGuard<'static, ()> { - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())) - .lock() - .unwrap_or_else(|e| e.into_inner()) - } - /// PR_MONITOR_STATE_FILE_OVERRIDE は process-global env var のため、 - /// override 設定 / 解除を test 並行実行で race させない serial guard。 - fn env_override_lock() -> std::sync::MutexGuard<'static, ()> { - static LOCK: OnceLock> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())).lock().unwrap() - } - - /// 書き込み先がディレクトリ不在のため write が必ず失敗する override path を返す。 + /// 書き込み先がディレクトリ不在のため write が必ず失敗する path を返す。 fn unwritable_state_path() -> std::path::PathBuf { std::env::temp_dir() .join(format!("pr-monitor-T2-2-{}", std::process::id())) @@ -199,12 +191,14 @@ mod tests { fn make_default_test_ctx<'a>( checker: &'a std::path::Path, + state_path: &'a std::path::Path, pr_info: &'a crate::util::PrInfo, rate_limit_config: &'a RateLimitConfig, classifier_config: &'a ClassifierConfig, ) -> PollContext<'a> { PollContext { checker, + state_path, push_time: "2026-05-01T00:00:00Z", fix_push_time: None, pr_info, @@ -225,10 +219,8 @@ mod tests { /// "wakeup は parked_* action のときのみスケジュールされる"。 #[test] fn finalize_review_recheck_max_reached_clears_wakeup_fields() { - let _guard = env_lock(); let tmp = tempfile::tempdir().unwrap(); let state_path = tmp.path().join("state.json"); - std::env::set_var("PR_MONITOR_STATE_FILE_OVERRIDE", &state_path); let mut state = PrMonitorState::new(Some(42), Some("o/r".into()), "t".into()); let stale_wakeup_unix: i64 = 9_999_999_999; @@ -238,9 +230,7 @@ mod tests { state.review_recheck_count = 3; state.action = "parked_review_recheck".into(); - finalize_review_recheck_max_reached(&mut state, 3); - - std::env::remove_var("PR_MONITOR_STATE_FILE_OVERRIDE"); + finalize_review_recheck_max_reached(&mut state, &state_path, 3); assert!( state.next_wakeup_at_unix.is_none(), @@ -262,9 +252,7 @@ mod tests { /// PARK signal emit を中止し `action_required` を返却する (sibling parity)。 #[test] fn schedule_next_review_recheck_park_returns_action_required_when_write_state_fails() { - let _guard = env_override_lock(); let bad_path = unwritable_state_path(); - std::env::set_var("PR_MONITOR_STATE_FILE_OVERRIDE", &bad_path); let mut state = PrMonitorState::new(Some(42), Some("o/r".into()), "2026-05-01T00:00:00Z".into()); @@ -281,6 +269,7 @@ mod tests { let classifier_config = ClassifierConfig::default(); let ctx = PollContext { checker: &checker_path, + state_path: &bad_path, push_time: "2026-05-01T00:00:00Z", fix_push_time: None, pr_info: &pr_info, @@ -297,8 +286,6 @@ mod tests { let outcome = schedule_next_review_recheck_park(&mut state, &ctx); - std::env::remove_var("PR_MONITOR_STATE_FILE_OVERRIDE"); - assert_eq!( outcome.action, "action_required", "T2-2 sibling parity: review park も write_state 失敗 → action_required で抜けること" @@ -310,25 +297,24 @@ mod tests { /// で残った state を持ち越さないことを machine-enforce する。 #[test] fn finalize_initial_review_park_resets_recheck_count() { - let _guard = env_override_lock(); - let tmp_path = std::env::temp_dir().join(format!( - "pr-monitor-CR-M2-{}-state.json", - std::process::id() - )); - std::env::set_var("PR_MONITOR_STATE_FILE_OVERRIDE", &tmp_path); - seed_stale_recheck_state(&tmp_path); + let tmp = tempfile::tempdir().unwrap(); + let state_path = tmp.path().join("state.json"); + seed_stale_recheck_state(&state_path); let pr_info = pr_info_for_initial_review_park_test(); let checker = std::path::PathBuf::from("dummy"); let rate_limit_config = RateLimitConfig::default(); let classifier_config = ClassifierConfig::default(); - let ctx = make_default_test_ctx(&checker, &pr_info, &rate_limit_config, &classifier_config); + let ctx = make_default_test_ctx( + &checker, + &state_path, + &pr_info, + &rate_limit_config, + &classifier_config, + ); let outcome = finalize_initial_review_park(&ctx); - let persisted = crate::state::read_state_from(&tmp_path).unwrap(); - - std::env::remove_var("PR_MONITOR_STATE_FILE_OVERRIDE"); - let _ = std::fs::remove_file(&tmp_path); + let persisted = crate::state::read_state_from(&state_path).unwrap(); assert_eq!(outcome.action, "parked_review_recheck"); assert_eq!( @@ -350,10 +336,8 @@ mod tests { /// or_else 被演算子の入れ替えバグを discriminate できる。 #[test] fn finalize_initial_review_park_preserves_existing_fix_push_time() { - let _guard = env_override_lock(); let tmp = tempfile::tempdir().unwrap(); let state_path = tmp.path().join("state.json"); - std::env::set_var("PR_MONITOR_STATE_FILE_OVERRIDE", &state_path); let mut seeded = PrMonitorState::new(Some(42), Some("o/r".into()), "2026-05-01T00:00:00Z".into()); @@ -370,14 +354,18 @@ mod tests { let checker = std::path::PathBuf::from("dummy"); let rate_limit_config = RateLimitConfig::default(); let classifier_config = ClassifierConfig::default(); - let mut ctx = - make_default_test_ctx(&checker, &pr_info, &rate_limit_config, &classifier_config); + let mut ctx = make_default_test_ctx( + &checker, + &state_path, + &pr_info, + &rate_limit_config, + &classifier_config, + ); let ctx_fix_push_time_must_lose = "2026-05-22T06:10:00Z"; ctx.fix_push_time = Some(ctx_fix_push_time_must_lose); finalize_initial_review_park(&ctx); let persisted = crate::state::read_state_from(&state_path).unwrap(); - std::env::remove_var("PR_MONITOR_STATE_FILE_OVERRIDE"); assert_eq!( persisted.fix_push_time.as_deref(), @@ -391,10 +379,8 @@ mod tests { /// `ctx.fix_push_time` の値で上書きしないことを検証する。 #[test] fn finalize_review_recheck_park_preserves_existing_fix_push_time() { - let _guard = env_override_lock(); let tmp = tempfile::tempdir().unwrap(); let state_path = tmp.path().join("state.json"); - std::env::set_var("PR_MONITOR_STATE_FILE_OVERRIDE", &state_path); let mut seeded = PrMonitorState::new(Some(42), Some("o/r".into()), "2026-05-01T00:00:00Z".into()); @@ -412,14 +398,18 @@ mod tests { let checker = std::path::PathBuf::from("dummy"); let rate_limit_config = RateLimitConfig::default(); let classifier_config = ClassifierConfig::default(); - let mut ctx = - make_default_test_ctx(&checker, &pr_info, &rate_limit_config, &classifier_config); + let mut ctx = make_default_test_ctx( + &checker, + &state_path, + &pr_info, + &rate_limit_config, + &classifier_config, + ); let ctx_fix_push_time_must_lose = "2026-05-22T06:10:00Z"; ctx.fix_push_time = Some(ctx_fix_push_time_must_lose); finalize_review_recheck_park(&ctx); let persisted = crate::state::read_state_from(&state_path).unwrap(); - std::env::remove_var("PR_MONITOR_STATE_FILE_OVERRIDE"); assert_eq!( persisted.fix_push_time.as_deref(), diff --git a/src/cli-pr-monitor/src/stages/poll/review_recheck_signal.rs b/src/cli-pr-monitor/src/stages/poll/review_recheck_signal.rs index ecaaed77..ad132d92 100644 --- a/src/cli-pr-monitor/src/stages/poll/review_recheck_signal.rs +++ b/src/cli-pr-monitor/src/stages/poll/review_recheck_signal.rs @@ -238,6 +238,7 @@ mod tests { let classifier_config = ClassifierConfig::default(); let ctx = PollContext { checker: &checker, + state_path: std::path::Path::new("unused"), push_time: "2026-05-01T00:00:00Z", fix_push_time: None, pr_info: &pr_info, @@ -283,6 +284,7 @@ mod tests { let classifier_config = ClassifierConfig::default(); let ctx = PollContext { checker: &checker, + state_path: std::path::Path::new("unused"), push_time: "2026-04-01T00:00:00Z", fix_push_time: None, pr_info: &pr_info, diff --git a/src/cli-pr-monitor/src/state.rs b/src/cli-pr-monitor/src/state.rs index 1bded3ee..78bcc3ba 100644 --- a/src/cli-pr-monitor/src/state.rs +++ b/src/cli-pr-monitor/src/state.rs @@ -153,15 +153,11 @@ impl PrMonitorState { } } -/// state file の保存パスを返す。 +/// state file の保存パスを返す (本番デフォルト = `/pr-monitor-state.json`)。 /// -/// 通常は `/pr-monitor-state.json`。 -/// 環境変数 `PR_MONITOR_STATE_FILE_OVERRIDE` がセットされていればそのパスを優先する -/// (T2-2 / fault injection test 用)。本番コードは env を設定しないため挙動変化なし。 +/// テストは override の env var ではなく、各関数に state path を直接注入する +/// (順位 229: 共有グローバル可変状態を排し並列テスト間の競合を構造的に解消)。 pub(crate) fn state_file_path() -> PathBuf { - if let Ok(path) = std::env::var("PR_MONITOR_STATE_FILE_OVERRIDE") { - return PathBuf::from(path); - } std::env::current_exe() .unwrap_or_default() .parent() @@ -184,14 +180,6 @@ pub(crate) fn read_state_from(path: &Path) -> Option { serde_json::from_str(&content).ok() } -pub(crate) fn write_state(state: &PrMonitorState) -> Result<(), String> { - write_state_to(&state_file_path(), state) -} - -pub(crate) fn read_state() -> Option { - read_state_from(&state_file_path()) -} - /// check-ci-coderabbit の JSON 出力から state を更新する pub(crate) fn update_state_from_check_result( state: &mut PrMonitorState,