From 6f141fe5a2ef517b734b4282c28382b7fa42fc3c Mon Sep 17 00:00:00 2001 From: Toni Alatalo Date: Wed, 30 Sep 2026 22:50:52 +0930 Subject: [PATCH 1/2] fix(ecc2): sandbox Config::config_path() under cfg(test) Dashboard tests that spawn sessions reach Config::save() through auto_split_layout_after_spawn, which wrote to the developer's real ~/Library/Application Support/ecc2/config.toml. The fixture's db_path pointed at a deleted temp dir, so the next `cargo run` failed with "unable to open database file". Under cfg(test), config_root() now returns a per-test-thread scratch directory that is removed when the thread exits, so no test can reach the real user config. Fixes #3276 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_011Y54kLgLGzLGqAxwLMFEc9 --- ecc2/src/config/mod.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/ecc2/src/config/mod.rs b/ecc2/src/config/mod.rs index d48bd9a67..df7b30c2f 100644 --- a/ecc2/src/config/mod.rs +++ b/ecc2/src/config/mod.rs @@ -517,6 +517,28 @@ impl Config { .context("deserialize merged ECC 2.0 config") } + #[cfg(test)] + fn config_root() -> PathBuf { + // Each test thread gets its own scratch root, so `Config::save()` can + // never overwrite the developer's real config. + struct TestConfigRoot(PathBuf); + + impl Drop for TestConfigRoot { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + thread_local! { + static ROOT: TestConfigRoot = TestConfigRoot( + std::env::temp_dir().join(format!("ecc2-test-config-{}", uuid::Uuid::new_v4())), + ); + } + + ROOT.with(|root| root.0.clone()) + } + + #[cfg(not(test))] fn config_root() -> PathBuf { dirs::config_dir().unwrap_or_else(|| { dirs::home_dir() @@ -1095,6 +1117,21 @@ focus_metrics = "e" let _ = std::fs::remove_dir_all(tempdir); } + #[test] + fn config_path_under_test_is_sandboxed_away_from_user_config() { + let path = Config::config_path(); + assert!( + path.starts_with(std::env::temp_dir()), + "tests must never resolve the real user config, got {}", + path.display() + ); + } + + #[test] + fn config_path_under_test_is_stable_within_a_test() { + assert_eq!(Config::config_path(), Config::config_path()); + } + #[test] fn primary_config_path_uses_xdg_style_location() { let path = Config::config_path(); From 708d621372501581ec4a0547defcea8542f72543 Mon Sep 17 00:00:00 2001 From: Toni Alatalo Date: Wed, 30 Sep 2026 23:08:55 +0930 Subject: [PATCH 2/2] fix(ecc2): also sandbox the legacy ~/.claude/ecc2.toml path under cfg(test) Addresses review feedback on #3277: the previous commit sandboxed the primary config path, but Config::global_config_paths() also reads the legacy $HOME/.claude/ecc2.toml. A test that called Config::load() would pick up (or fail on a malformed) developer-local legacy config. Under cfg(test), legacy_global_config_path() now resolves inside the same per-test-thread scratch root as config_path(). Proof of red (new test, before the fix): $ cargo test legacy_config_path_under_test running 1 test test config::tests::legacy_config_path_under_test_is_sandboxed_away_from_user_home ... FAILED failures: ---- config::tests::legacy_config_path_under_test_is_sandboxed_away_from_user_home stdout ---- tests must never read the real legacy user config, got /Users//.claude/ecc2.toml note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: config::tests::legacy_config_path_under_test_is_sandboxed_away_from_user_home test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 491 filtered out; finished in 0.00s error: test failed, to rerun pass `--bin ecc-tui` After the fix: $ cargo test legacy_config_path_under_test test config::tests::legacy_config_path_under_test_is_sandboxed_away_from_user_home ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 491 filtered out $ cargo test # full suite test result: ok. 491 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out Refs #3276 Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_011Y54kLgLGzLGqAxwLMFEc9 --- ecc2/src/config/mod.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ecc2/src/config/mod.rs b/ecc2/src/config/mod.rs index df7b30c2f..38fd467a1 100644 --- a/ecc2/src/config/mod.rs +++ b/ecc2/src/config/mod.rs @@ -547,6 +547,12 @@ impl Config { }) } + #[cfg(test)] + fn legacy_global_config_path() -> PathBuf { + Self::config_root().join(".claude").join("ecc2.toml") + } + + #[cfg(not(test))] fn legacy_global_config_path() -> PathBuf { dirs::home_dir() .unwrap_or_else(|| PathBuf::from(".")) @@ -1127,6 +1133,17 @@ focus_metrics = "e" ); } + #[test] + fn legacy_config_path_under_test_is_sandboxed_away_from_user_home() { + let path = Config::legacy_global_config_path(); + assert!( + path.starts_with(std::env::temp_dir()), + "tests must never read the real legacy user config, got {}", + path.display() + ); + assert!(path.ends_with(".claude/ecc2.toml")); + } + #[test] fn config_path_under_test_is_stable_within_a_test() { assert_eq!(Config::config_path(), Config::config_path());