From 1489fdf99dc24002a5c9bcdca41b247de4e57eb6 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 4 Jul 2026 17:36:04 +0000 Subject: [PATCH 01/15] test: isolate install-family tests from process-global env bleed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The install path resolves profile root via TRACEDECAY_DATA_DIR when set, falling back to /.tracedecay only when unset. Under in-process cargo test, a sibling test that pins the env made concurrent unpinned installs resolve to the shared path and race the memory_digest_targets atomic rename. Adds an AgentEnvLock RAII helper (locks PROCESS_ENV_LOCK + pins the env to the test's own home — the exact unset-fallback value, so no assertions change) and applies it to the install/uninstall tests that lacked isolation. Test-only; verified full workspace green + stress-stable. Co-Authored-By: Claude Fable 5 --- tests/agent_suite/agent_test.rs | 67 +++++++++++++++++++++++ tests/agent_suite/claude_agent_test.rs | 22 ++++++++ tests/agent_suite/copilot_agent_test.rs | 16 ++++++ tests/agent_suite/kiro_agent_test.rs | 16 ++++++ tests/agent_suite/opencode_agent_test.rs | 18 ++++++ tests/agent_suite/update_plugin_test.rs | 21 +++++++ tests/agent_suite/upgrade_refresh_test.rs | 1 + tests/common/mod.rs | 50 +++++++++++++++++ tests/hooks_lsp_suite/hooks_test.rs | 63 +++++++++++++++++++-- tests/storage_suite/corruption_test.rs | 4 +- 10 files changed, 272 insertions(+), 6 deletions(-) diff --git a/tests/agent_suite/agent_test.rs b/tests/agent_suite/agent_test.rs index edec8ab6..8cfc7844 100644 --- a/tests/agent_suite/agent_test.rs +++ b/tests/agent_suite/agent_test.rs @@ -796,6 +796,7 @@ fn assert_hermes_config_enables_tracedecay_memory(config_path: &Path) -> String #[test] fn test_cursor_install_installs_local_plugin_without_global_mcp() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home.path()); CursorIntegration.install(&ctx).unwrap(); @@ -818,6 +819,7 @@ fn test_cursor_install_installs_local_plugin_without_global_mcp() { #[test] fn test_cursor_plugin_hooks_quote_binary_paths_with_spaces() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let tracedecay_bin = home.path().join("bin with spaces/tracedecay"); let ctx = InstallContext { home: home.path().to_path_buf(), @@ -1000,6 +1002,7 @@ fn test_local_install_cursor_preserves_existing_permissions_file() { #[test] fn test_cursor_healthcheck_ignores_foreign_project_cursor_files() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project = TempDir::new().unwrap(); CursorIntegration .install(&make_install_ctx(home.path())) @@ -1308,6 +1311,7 @@ fn test_hermes_generated_python_registers_lcm_context_engine() { #[test] fn test_hermes_generated_python_handles_quoted_unicode_tracedecay_path() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let tracedecay_bin = home.path().join("bin with spaces").join("token\"save-π"); let ctx = InstallContext { home: home.path().to_path_buf(), @@ -2003,6 +2007,7 @@ assert fallback.name == "tracedecay" #[test] fn test_hermes_global_install_and_uninstall_plugin() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home.path()); HermesIntegration.install(&ctx).unwrap(); @@ -2036,6 +2041,7 @@ fn test_hermes_global_install_and_uninstall_plugin() { #[test] fn test_hermes_profile_install_targets_named_profile() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = InstallContext { home: home.path().to_path_buf(), tracedecay_bin: "/usr/local/bin/tracedecay".to_string(), @@ -2219,6 +2225,7 @@ fn test_hermes_local_install_with_profile_targets_named_profile() { #[test] fn test_hermes_install_rejects_invalid_profile_names() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = InstallContext { home: home.path().to_path_buf(), tracedecay_bin: "/usr/local/bin/tracedecay".to_string(), @@ -2587,6 +2594,7 @@ fn test_hermes_install_replaces_default_compressor_context_engine() { #[test] fn test_hermes_install_preserves_user_keys_in_tracedecay_config_block() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let hermes_dir = home.path().join(".hermes"); std::fs::create_dir_all(&hermes_dir).unwrap(); std::fs::write( @@ -2708,6 +2716,7 @@ fn test_hermes_install_rejects_inline_plugins_config_without_rewrite() { #[test] fn test_hermes_uninstall_preserves_other_profile_plugins_and_config() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let profile = home.path().join(".hermes/profiles/work"); let plugin_dir = profile.join("plugins/tracedecay"); let other_plugin = profile.join("plugins/other"); @@ -3102,6 +3111,7 @@ fn test_local_install_rejects_cline_without_project_mutation() { fn test_claude_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -3188,6 +3198,7 @@ fn test_claude_install_creates_config() { fn test_gemini_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); GeminiIntegration.install(&ctx).unwrap(); @@ -3221,6 +3232,7 @@ fn test_gemini_install_creates_config() { fn test_codex_install_creates_plugin_bundle_and_marketplace() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); CodexIntegration.install(&ctx).unwrap(); @@ -3332,6 +3344,7 @@ async fn test_codex_shareable_plugin_artifact_exports_bundle_and_managed_skills( fn test_codex_install_refreshes_existing_cache_and_keeps_bootstrap_source_listable() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); let stale_plugin_dir = codex_stale_cached_plugin_install_dir(home); write_codex_plugin_manifest(&stale_plugin_dir, "0.0.0"); @@ -3376,6 +3389,7 @@ fn test_codex_install_refreshes_existing_cache_and_keeps_bootstrap_source_listab fn test_codex_install_refreshes_existing_cache_and_prunes_stale_skills() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); let cached_plugin_dir = codex_cached_plugin_install_dir(home); let bootstrap_dir = codex_plugin_install_dir(home); @@ -3415,6 +3429,7 @@ fn test_codex_install_refreshes_existing_cache_and_prunes_stale_skills() { fn test_codex_install_sweeps_legacy_global_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let codex_dir = home.join(".codex"); std::fs::create_dir_all(&codex_dir).unwrap(); std::fs::write( @@ -3625,6 +3640,7 @@ fn assert_codex_hooks_registered(hooks: &serde_json::Value) { fn test_codex_global_install_bundles_hooks_in_plugin() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); CodexIntegration.install(&ctx).unwrap(); @@ -3692,6 +3708,7 @@ fn assert_command_contains_expected_bin( fn test_codex_install_reconciles_hooks_idempotently() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); CodexIntegration.install(&ctx).unwrap(); @@ -3724,6 +3741,7 @@ fn test_codex_install_reconciles_hooks_idempotently() { fn test_codex_uninstall_removes_plugin_hooks() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); CodexIntegration.install(&ctx).unwrap(); @@ -3742,6 +3760,7 @@ fn test_codex_uninstall_removes_plugin_hooks() { fn test_kimi_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); KimiIntegration.install(&ctx).unwrap(); @@ -3768,6 +3787,7 @@ fn test_kimi_install_creates_config() { fn test_kimi_install_then_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); KimiIntegration.install(&ctx).unwrap(); @@ -3795,6 +3815,7 @@ fn test_kimi_install_then_uninstall() { fn test_kimi_is_detected_and_has_tracedecay() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); assert!(!KimiIntegration.is_detected(home)); assert!(!KimiIntegration.has_tracedecay(home)); @@ -3810,6 +3831,7 @@ fn test_kimi_is_detected_and_has_tracedecay() { fn test_cursor_install_creates_plugin() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); CursorIntegration.install(&ctx).unwrap(); @@ -3925,6 +3947,7 @@ async fn test_prompt_integrations_export_active_managed_skill_indexes() { fn test_opencode_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // OpenCode uses ~/.config/opencode/opencode.json // Create the parent dir so install can discover it let ctx = make_install_ctx(home); @@ -3944,6 +3967,7 @@ fn test_opencode_install_creates_config() { fn test_zed_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); ZedIntegration.install(&ctx).unwrap(); @@ -3967,6 +3991,7 @@ fn test_zed_install_creates_config() { fn test_cline_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); ClineIntegration.install(&ctx).unwrap(); @@ -3997,6 +4022,7 @@ fn test_cline_install_creates_config() { fn test_roo_code_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); RooCodeIntegration.install(&ctx).unwrap(); @@ -4022,6 +4048,7 @@ fn test_roo_code_install_creates_config() { fn test_copilot_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -4062,6 +4089,7 @@ fn test_copilot_install_creates_config() { fn test_vibe_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); VibeIntegration.install(&ctx).unwrap(); @@ -4106,6 +4134,7 @@ fn test_vibe_install_creates_config() { fn test_claude_install_then_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); let marketplace_manifest = @@ -4185,6 +4214,7 @@ fn test_claude_uninstall_unrecords_memory_digest_target() { fn test_gemini_install_then_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); GeminiIntegration.install(&ctx).unwrap(); @@ -4222,6 +4252,7 @@ fn test_gemini_install_then_uninstall() { fn test_codex_install_then_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); CodexIntegration.install(&ctx).unwrap(); @@ -4318,6 +4349,7 @@ fn test_codex_install_preserves_existing_config() { // empty table. let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); std::fs::create_dir_all(home.join(".codex")).unwrap(); let config_path = home.join(".codex/config.toml"); let original = "\ @@ -4356,6 +4388,7 @@ fn test_codex_install_leaves_unparseable_config_untouched() { // user config must be left byte-identical while the plugin bundle installs. let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); std::fs::create_dir_all(home.join(".codex")).unwrap(); let config_path = home.join(".codex/config.toml"); let original = "this is not valid TOML {{{{"; @@ -4392,6 +4425,11 @@ fn assert_install_backs_up_and_preserves( original: &str, marker: &str, ) { + // Serialize + pin USER_DATA_DIR_ENV to this test's own home so the in-process + // install here can't resolve its profile root to a concurrent test's pinned + // tempdir and collide on the shared memory-digest target file. See + // `common::AgentEnvLock`. Callers must not hold the lock themselves. + let _agent_env = crate::common::AgentEnvLock::pin(home); let config_path = agent .primary_config_path(home) .unwrap_or_else(|| panic!("{} must implement primary_config_path", agent.name())); @@ -4433,6 +4471,7 @@ fn test_claude_install_preserves_existing_config() { // foreign settings key and a foreign registered marketplace must survive. let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let claude_dir = home.join(".claude"); let plugins_dir = claude_dir.join("plugins"); std::fs::create_dir_all(&plugins_dir).unwrap(); @@ -4495,6 +4534,7 @@ fn test_gemini_install_preserves_existing_config() { #[test] fn test_cursor_install_preserves_existing_legacy_mcp_config() { let dir = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(dir.path()); let path = dir.path().join(".cursor/mcp.json"); std::fs::create_dir_all(path.parent().unwrap()).unwrap(); let original = r#"{ @@ -4571,6 +4611,7 @@ fn test_cursor_uninstall_backs_up_config_with_other_content() { // before rewriting, so a botched rewrite is recoverable. let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); let path = home.join(".cursor/mcp.json"); @@ -4637,6 +4678,7 @@ fn test_antigravity_install_preserves_existing_config() { fn test_antigravity_install_writes_cli_plugin() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let bin = "/usr/local/bin/tracedecay"; let ctx = InstallContext { home: home.to_path_buf(), @@ -4689,6 +4731,7 @@ fn test_antigravity_install_writes_cli_plugin() { fn test_antigravity_uninstall_removes_both_locations() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let bin = "/usr/local/bin/tracedecay"; let ctx = InstallContext { home: home.to_path_buf(), @@ -4778,6 +4821,7 @@ fn test_every_tested_agent_advertises_primary_config_path() { fn test_cursor_install_then_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); CursorIntegration.install(&ctx).unwrap(); @@ -4813,6 +4857,7 @@ fn test_cursor_install_then_uninstall() { fn test_copilot_install_then_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -4835,6 +4880,7 @@ fn test_copilot_install_then_uninstall() { fn test_vibe_install_then_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); VibeIntegration.install(&ctx).unwrap(); @@ -4889,6 +4935,7 @@ fn make_install_ctx_with_real_bin(home: &Path) -> InstallContext { fn test_healthcheck_claude_clean_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx_with_real_bin(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -4905,6 +4952,7 @@ fn test_healthcheck_claude_clean_install() { fn test_healthcheck_gemini_clean_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); GeminiIntegration.install(&ctx).unwrap(); @@ -4921,6 +4969,7 @@ fn test_healthcheck_gemini_clean_install() { fn test_healthcheck_codex_after_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); CodexIntegration.install(&ctx).unwrap(); @@ -4976,6 +5025,7 @@ fn test_healthcheck_codex_local_install_warns_when_repo_marketplace_missing() { #[test] fn test_healthcheck_codex_ignores_unrelated_project_agents_md() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project = TempDir::new().unwrap(); std::fs::write( project.path().join("AGENTS.md"), @@ -5001,6 +5051,7 @@ fn test_healthcheck_codex_ignores_unrelated_project_agents_md() { fn test_healthcheck_cursor_clean_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); CursorIntegration.install(&ctx).unwrap(); @@ -5088,6 +5139,7 @@ async fn test_cursor_healthcheck_warns_on_literal_workspace_folder_transcript_pa #[test] fn test_healthcheck_hermes_profile_install_checks_named_profiles() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project = TempDir::new().unwrap(); let ctx = InstallContext { home: home.path().to_path_buf(), @@ -5135,6 +5187,7 @@ fn test_healthcheck_hermes_local_install_checks_project_hermes_home() { fn test_healthcheck_opencode_clean_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -5503,6 +5556,7 @@ fn test_parse_jsonc() { fn test_is_detected_claude() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); assert!(!ClaudeIntegration.is_detected(home)); std::fs::create_dir_all(home.join(".claude")).unwrap(); assert!(ClaudeIntegration.is_detected(home)); @@ -5570,6 +5624,7 @@ fn test_is_detected_copilot() { fn test_has_tracedecay_claude() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // No config => false assert!(!ClaudeIntegration.has_tracedecay(home)); @@ -5587,6 +5642,7 @@ fn test_has_tracedecay_claude() { fn test_has_tracedecay_gemini() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); assert!(!GeminiIntegration.has_tracedecay(home)); let ctx = make_install_ctx(home); @@ -5598,6 +5654,7 @@ fn test_has_tracedecay_gemini() { fn test_has_tracedecay_codex() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); assert!(!CodexIntegration.has_tracedecay(home)); let ctx = make_install_ctx(home); @@ -5615,6 +5672,7 @@ fn test_has_tracedecay_codex() { fn test_has_tracedecay_cursor() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); assert!(!CursorIntegration.has_tracedecay(home)); let ctx = make_install_ctx(home); @@ -5626,6 +5684,7 @@ fn test_has_tracedecay_cursor() { fn test_has_tracedecay_opencode() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); assert!(!OpenCodeIntegration.has_tracedecay(home)); let ctx = make_install_ctx(home); @@ -5637,6 +5696,7 @@ fn test_has_tracedecay_opencode() { fn test_has_tracedecay_copilot() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); assert!(!CopilotIntegration.has_tracedecay(home)); let ctx = make_install_ctx(home); @@ -5652,6 +5712,7 @@ fn test_has_tracedecay_copilot() { fn test_claude_install_idempotent() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); // Install twice should not fail @@ -5678,6 +5739,7 @@ fn test_claude_install_idempotent() { fn test_gemini_install_idempotent() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); GeminiIntegration.install(&ctx).unwrap(); @@ -5693,6 +5755,7 @@ fn test_gemini_install_idempotent() { fn test_uninstall_without_install_does_not_crash() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); // Uninstalling when nothing is installed should not panic or error @@ -5717,6 +5780,7 @@ fn test_uninstall_without_install_does_not_crash() { fn test_claude_install_preserves_existing_claude_json() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Pre-populate .claude.json with a foreign MCP server and a custom key. // The plugin model no longer writes tracedecay into ~/.claude.json, and the @@ -5748,6 +5812,7 @@ fn test_claude_install_preserves_existing_claude_json() { fn test_gemini_install_preserves_existing_settings() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let settings_path = home.join(".gemini/settings.json"); std::fs::create_dir_all(home.join(".gemini")).unwrap(); @@ -5966,6 +6031,7 @@ fn test_migrate_installed_agents_skips_when_already_populated() { fn test_migrate_installed_agents_detects_installed_agents() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Install copilot so it can be detected let ctx = make_install_ctx(home); @@ -6158,6 +6224,7 @@ fn test_backup_and_safe_write_round_trip() { #[test] fn test_hermes_install_writes_and_preserves_project_root_pin() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let pinned = InstallContext { home: home.path().to_path_buf(), tracedecay_bin: "/usr/local/bin/tracedecay".to_string(), diff --git a/tests/agent_suite/claude_agent_test.rs b/tests/agent_suite/claude_agent_test.rs index 929408de..38b974a0 100644 --- a/tests/agent_suite/claude_agent_test.rs +++ b/tests/agent_suite/claude_agent_test.rs @@ -77,6 +77,7 @@ fn permission_allowlist(settings: &serde_json::Value) -> Vec<&str> { fn test_install_deploys_plugin_mcp_server() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -104,6 +105,7 @@ fn test_install_deploys_plugin_mcp_server() { fn test_install_deploys_plugin_hooks() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -168,6 +170,7 @@ fn test_install_deploys_plugin_hooks() { fn test_install_creates_settings_with_permissions() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -279,6 +282,7 @@ fn test_install_claude_md_has_moment_triggers() { fn test_install_creates_claude_md_with_rules() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -309,6 +313,7 @@ fn test_install_creates_claude_md_with_rules() { fn test_claude_md_contains_explore_agent_paragraph() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Pre-populate CLAUDE.md with existing content let claude_dir = home.join(".claude"); @@ -337,6 +342,7 @@ fn test_claude_md_contains_explore_agent_paragraph() { fn test_uninstall_removes_explore_agent_paragraph() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Pre-populate CLAUDE.md with existing content let claude_dir = home.join(".claude"); @@ -372,6 +378,7 @@ fn test_uninstall_removes_explore_agent_paragraph() { fn test_install_idempotent_claude_md() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -390,6 +397,7 @@ fn test_install_idempotent_claude_md() { fn test_install_preserves_existing_claude_json() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Pre-populate .claude.json with an extra key std::fs::write(home.join(".claude.json"), r#"{"foo": "bar"}"#).unwrap(); @@ -417,6 +425,7 @@ fn test_install_preserves_existing_claude_json() { fn test_install_preserves_existing_settings() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Pre-populate settings.json with an existing hook let claude_dir = home.join(".claude"); @@ -476,6 +485,7 @@ fn test_install_preserves_existing_settings() { fn test_install_migrates_off_config_managed_integration() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let claude_dir = home.join(".claude"); std::fs::create_dir_all(&claude_dir).unwrap(); @@ -569,6 +579,7 @@ fn test_install_migrates_off_config_managed_integration() { fn test_uninstall_removes_mcp_from_claude_json() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); ClaudeIntegration.uninstall(&ctx).unwrap(); @@ -592,6 +603,7 @@ fn test_uninstall_removes_mcp_from_claude_json() { fn test_uninstall_removes_deployed_bundle_and_lone_marketplace_file() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); // Install deploys the plugin bundle and registers the marketplace. @@ -642,6 +654,7 @@ fn test_uninstall_removes_deployed_bundle_and_lone_marketplace_file() { fn test_uninstall_removes_hook_from_settings() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); ClaudeIntegration.uninstall(&ctx).unwrap(); @@ -678,6 +691,7 @@ fn test_uninstall_removes_hook_from_settings() { fn test_uninstall_removes_permissions_from_settings() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); ClaudeIntegration.uninstall(&ctx).unwrap(); @@ -705,6 +719,7 @@ fn test_uninstall_removes_permissions_from_settings() { fn test_uninstall_preserves_other_permissions() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Install first so all files are set up let ctx = make_install_ctx(home); @@ -739,6 +754,7 @@ fn test_uninstall_preserves_other_permissions() { fn test_uninstall_removes_claude_md_rules() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -761,6 +777,7 @@ fn test_uninstall_removes_claude_md_rules() { fn test_uninstall_preserves_other_claude_md_content() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Create CLAUDE.md with pre-existing content let claude_dir = home.join(".claude"); @@ -892,6 +909,7 @@ fn test_healthcheck_detects_missing_permissions() { fn test_healthcheck_detects_stale_permissions() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx_with_real_bin(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -919,6 +937,7 @@ fn test_healthcheck_detects_stale_permissions() { fn test_healthcheck_detects_missing_claude_md() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx_with_real_bin(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -941,6 +960,7 @@ fn test_healthcheck_detects_missing_claude_md() { fn test_healthcheck_clean_local_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project = dir.path().join("myproject"); std::fs::create_dir_all(&project).unwrap(); @@ -974,6 +994,7 @@ fn test_healthcheck_clean_local_config() { fn test_healthcheck_local_settings_cleanup() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project = dir.path().join("myproject"); let local_claude = project.join(".claude"); std::fs::create_dir_all(&local_claude).unwrap(); @@ -1021,6 +1042,7 @@ fn test_healthcheck_local_settings_cleanup() { fn test_has_tracedecay_after_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); diff --git a/tests/agent_suite/copilot_agent_test.rs b/tests/agent_suite/copilot_agent_test.rs index fc291cd9..c7d6266d 100644 --- a/tests/agent_suite/copilot_agent_test.rs +++ b/tests/agent_suite/copilot_agent_test.rs @@ -58,6 +58,7 @@ fn cli_config_path(home: &Path) -> std::path::PathBuf { fn test_install_creates_vscode_settings_with_mcp_server() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -94,6 +95,7 @@ fn test_install_creates_vscode_settings_with_mcp_server() { fn test_install_creates_cli_config_with_mcp_server() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -125,6 +127,7 @@ fn test_install_creates_cli_config_with_mcp_server() { fn test_install_preserves_existing_vscode_settings() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Pre-populate VS Code settings with other content let settings_path = vscode_settings_path(home); @@ -153,6 +156,7 @@ fn test_install_preserves_existing_vscode_settings() { fn test_install_preserves_existing_cli_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Pre-populate CLI config with another MCP server let cli_path = cli_config_path(home); @@ -181,6 +185,7 @@ fn test_install_preserves_existing_cli_config() { fn test_install_idempotent_vscode() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -201,6 +206,7 @@ fn test_install_idempotent_vscode() { fn test_install_idempotent_cli() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -223,6 +229,7 @@ fn test_install_idempotent_cli() { fn test_uninstall_removes_vscode_mcp_entry() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -250,6 +257,7 @@ fn test_uninstall_removes_vscode_mcp_entry() { fn test_uninstall_cleans_empty_mcp_objects() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -269,6 +277,7 @@ fn test_uninstall_cleans_empty_mcp_objects() { fn test_uninstall_removes_cli_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -293,6 +302,7 @@ fn test_uninstall_removes_cli_config() { fn test_uninstall_preserves_other_cli_servers() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Pre-populate CLI config with another server let cli_path = cli_config_path(home); @@ -327,6 +337,7 @@ fn test_uninstall_preserves_other_cli_servers() { fn test_uninstall_preserves_other_vscode_settings() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Pre-populate VS Code settings let settings_path = vscode_settings_path(home); @@ -348,6 +359,7 @@ fn test_uninstall_preserves_other_vscode_settings() { fn test_uninstall_without_install_does_not_crash() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); // Should not panic or error CopilotIntegration.uninstall(&ctx).unwrap(); @@ -357,6 +369,7 @@ fn test_uninstall_without_install_does_not_crash() { fn test_uninstall_cli_with_no_tracedecay_is_noop() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Create a CLI config without tracedecay let cli_path = cli_config_path(home); @@ -383,6 +396,7 @@ fn test_uninstall_cli_with_no_tracedecay_is_noop() { fn test_healthcheck_clean_install_no_issues() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -595,6 +609,7 @@ fn test_has_tracedecay_before_install() { fn test_has_tracedecay_after_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); assert!( @@ -607,6 +622,7 @@ fn test_has_tracedecay_after_install() { fn test_has_tracedecay_after_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); CopilotIntegration.uninstall(&ctx).unwrap(); diff --git a/tests/agent_suite/kiro_agent_test.rs b/tests/agent_suite/kiro_agent_test.rs index 4673ac81..f0409ed3 100644 --- a/tests/agent_suite/kiro_agent_test.rs +++ b/tests/agent_suite/kiro_agent_test.rs @@ -102,6 +102,7 @@ fn assert_hook( fn test_install_creates_global_mcp_steering_agent_and_default() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); KiroIntegration.install(&ctx).unwrap(); @@ -293,6 +294,7 @@ async fn test_install_preserves_user_managed_agent_while_updating_skill_index() fn test_install_preserves_existing_mcp_config_and_writes_backup() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let mcp_path = home.join(".kiro/settings/mcp.json"); std::fs::create_dir_all(mcp_path.parent().unwrap()).unwrap(); std::fs::write( @@ -318,6 +320,7 @@ fn test_install_preserves_existing_mcp_config_and_writes_backup() { fn test_install_and_uninstall_preserve_existing_steering_content() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); let user_steering_path = home.join(".kiro/steering/team.md"); @@ -352,6 +355,7 @@ fn test_install_and_uninstall_preserve_existing_steering_content() { fn test_uninstall_preserves_user_steering_after_tracedecay_block() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); KiroIntegration.install(&ctx).unwrap(); @@ -381,6 +385,7 @@ fn test_uninstall_preserves_user_steering_after_tracedecay_block() { fn test_install_replaces_legacy_marker_block_and_uninstall_removes_it() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); let steering_path = home.join(".kiro/steering/tracedecay.md"); @@ -428,6 +433,7 @@ fn test_install_replaces_legacy_marker_block_and_uninstall_removes_it() { fn test_uninstall_removes_legacy_marker_block_preserving_user_content() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); let steering_path = home.join(".kiro/steering/tracedecay.md"); @@ -454,6 +460,7 @@ fn test_uninstall_removes_legacy_marker_block_preserving_user_content() { fn test_uninstall_removes_tracedecay_and_preserves_other_mcp_servers() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); let mcp_path = home.join(".kiro/settings/mcp.json"); @@ -487,6 +494,7 @@ fn test_uninstall_removes_tracedecay_and_preserves_other_mcp_servers() { fn test_install_and_uninstall_preserve_user_managed_custom_agent() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); let agent_path = home.join(".kiro/agents/tracedecay.json"); @@ -525,6 +533,7 @@ fn test_install_and_uninstall_preserve_user_managed_custom_agent() { fn test_install_preserves_existing_custom_default_agent_choice() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); let cli_path = home.join(".kiro/settings/cli.json"); @@ -547,6 +556,7 @@ fn test_install_preserves_existing_custom_default_agent_choice() { fn test_install_replaces_builtin_default_agent_choice() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); let cli_path = home.join(".kiro/settings/cli.json"); @@ -563,6 +573,7 @@ fn test_install_replaces_builtin_default_agent_choice() { fn test_has_tracedecay_tracks_global_mcp_entry() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); assert!(!KiroIntegration.has_tracedecay(home)); @@ -578,6 +589,7 @@ fn test_has_tracedecay_tracks_global_mcp_entry() { fn test_healthcheck_clean_install_has_no_issues_or_warnings() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); KiroIntegration.install(&ctx).unwrap(); @@ -597,6 +609,7 @@ fn test_healthcheck_clean_install_has_no_issues_or_warnings() { fn test_healthcheck_fails_when_steering_lacks_owned_end_marker() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); KiroIntegration.install(&ctx).unwrap(); @@ -625,6 +638,7 @@ fn test_healthcheck_fails_when_steering_lacks_owned_end_marker() { fn test_healthcheck_warns_when_agent_tool_policy_is_not_permissive() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); KiroIntegration.install(&ctx).unwrap(); @@ -657,6 +671,7 @@ fn test_healthcheck_fails_when_workspace_mcp_disables_tracedecay() { let home_dir = TempDir::new().unwrap(); let project_dir = TempDir::new().unwrap(); let home = home_dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project = project_dir.path(); let ctx = make_ctx(home); @@ -688,6 +703,7 @@ fn test_healthcheck_fails_when_workspace_mcp_shadows_global_command() { let home_dir = TempDir::new().unwrap(); let project_dir = TempDir::new().unwrap(); let home = home_dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project = project_dir.path(); let ctx = make_ctx(home); diff --git a/tests/agent_suite/opencode_agent_test.rs b/tests/agent_suite/opencode_agent_test.rs index ea45b763..b335661c 100644 --- a/tests/agent_suite/opencode_agent_test.rs +++ b/tests/agent_suite/opencode_agent_test.rs @@ -47,6 +47,7 @@ fn opencode_prompt_path(home: &Path) -> std::path::PathBuf { fn test_install_creates_opencode_json() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -79,6 +80,7 @@ fn test_install_creates_opencode_json() { fn test_install_creates_opencode_md_with_rules() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -100,6 +102,7 @@ fn test_install_creates_opencode_md_with_rules() { fn test_install_preserves_existing_opencode_json() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Pre-populate opencode.json with existing content let config_path = opencode_config_path(home); @@ -133,6 +136,7 @@ fn test_install_preserves_existing_opencode_json() { fn test_install_idempotent_opencode_json() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -148,6 +152,7 @@ fn test_install_idempotent_opencode_json() { fn test_install_idempotent_opencode_md() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -167,6 +172,7 @@ fn test_install_idempotent_opencode_md() { fn test_install_preserves_existing_opencode_md_content() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Create AGENTS.md with pre-existing content let config_dir = home.join(".config/opencode"); @@ -199,6 +205,7 @@ fn test_install_preserves_existing_opencode_md_content() { fn test_uninstall_removes_mcp_from_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -220,6 +227,7 @@ fn test_uninstall_removes_mcp_from_config() { fn test_uninstall_removes_empty_config_file() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -237,6 +245,7 @@ fn test_uninstall_removes_empty_config_file() { fn test_uninstall_preserves_other_mcp_servers() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Pre-populate with another server let config_path = opencode_config_path(home); @@ -271,6 +280,7 @@ fn test_uninstall_preserves_other_mcp_servers() { fn test_uninstall_removes_opencode_md_rules() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -293,6 +303,7 @@ fn test_uninstall_removes_opencode_md_rules() { fn test_uninstall_preserves_other_opencode_md_content() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Create AGENTS.md with pre-existing content let config_dir = home.join(".config/opencode"); @@ -324,6 +335,7 @@ fn test_uninstall_preserves_other_opencode_md_content() { fn test_uninstall_without_install_does_not_crash() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); // Should not panic or error OpenCodeIntegration.uninstall(&ctx).unwrap(); @@ -333,6 +345,7 @@ fn test_uninstall_without_install_does_not_crash() { fn test_uninstall_config_with_no_tracedecay_is_noop() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); // Create opencode.json without tracedecay let config_path = opencode_config_path(home); @@ -359,6 +372,7 @@ fn test_uninstall_config_with_no_tracedecay_is_noop() { fn test_healthcheck_clean_install_no_issues() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -445,6 +459,7 @@ fn test_healthcheck_detects_missing_serve_arg() { fn test_healthcheck_detects_missing_opencode_md() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -468,6 +483,7 @@ fn test_healthcheck_detects_missing_opencode_md() { fn test_healthcheck_detects_missing_tracedecay_rules_in_opencode_md() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -530,6 +546,7 @@ fn test_has_tracedecay_before_install() { fn test_has_tracedecay_after_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); assert!( @@ -542,6 +559,7 @@ fn test_has_tracedecay_after_install() { fn test_has_tracedecay_after_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); OpenCodeIntegration.uninstall(&ctx).unwrap(); diff --git a/tests/agent_suite/update_plugin_test.rs b/tests/agent_suite/update_plugin_test.rs index 78f52ca2..65f45bca 100644 --- a/tests/agent_suite/update_plugin_test.rs +++ b/tests/agent_suite/update_plugin_test.rs @@ -270,6 +270,7 @@ fn hermes_update_plugin_reports_not_installed_when_nothing_is_detected() { #[test] fn cursor_update_plugin_refreshes_bundle_and_preserves_user_config() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let cursor = get_integration("cursor").unwrap(); // User-owned Cursor config that update-plugin must never write. @@ -336,6 +337,7 @@ fn cursor_update_plugin_refreshes_bundle_and_preserves_user_config() { #[test] fn cursor_update_plugin_reports_not_installed_without_a_bundle() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); std::fs::create_dir_all(home.path().join(".cursor")).unwrap(); let cursor = get_integration("cursor").unwrap(); let outcome = cursor.update_plugin(&ctx(home.path(), NEW_BIN)).unwrap(); @@ -346,6 +348,7 @@ fn cursor_update_plugin_reports_not_installed_without_a_bundle() { #[test] fn claude_update_plugin_refreshes_bundle_and_preserves_user_config() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let claude = get_integration("claude").unwrap(); // User-owned Claude config that update-plugin must never destroy. @@ -452,6 +455,7 @@ fn claude_update_plugin_writes_plugin_permissions_and_refreshes_claude_md() { #[test] fn claude_update_plugin_reports_not_installed_without_a_bundle() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); std::fs::create_dir_all(home.path().join(".claude")).unwrap(); let claude = get_integration("claude").unwrap(); let outcome = claude.update_plugin(&ctx(home.path(), NEW_BIN)).unwrap(); @@ -469,6 +473,7 @@ fn claude_update_plugin_reports_not_installed_without_a_bundle() { #[test] fn codex_update_plugin_refreshes_bundle_without_touching_config() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project_root = home.path().join("workspace"); let codex = get_integration("codex").unwrap(); codex.install(&ctx(home.path(), OLD_BIN)).unwrap(); @@ -522,6 +527,7 @@ fn codex_update_plugin_refreshes_bundle_without_touching_config() { #[test] fn codex_update_plugin_refreshes_cache_and_keeps_bootstrap_source_listable() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project_root = home.path().join("workspace"); let stale_plugin_dir = home .path() @@ -567,6 +573,7 @@ fn codex_update_plugin_refreshes_cache_and_keeps_bootstrap_source_listable() { #[test] fn codex_update_plugin_recreates_bootstrap_source_from_cache_only_state() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project_root = home.path().join("workspace"); let cached_plugin_dir = codex_cached_plugin_dir(home.path()); let bootstrap_dir = codex_bootstrap_dir(home.path()); @@ -601,6 +608,7 @@ fn codex_update_plugin_recreates_bootstrap_source_from_cache_only_state() { #[test] fn codex_update_plugin_sweeps_legacy_config_when_cache_exists() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project_root = home.path().join("workspace"); let cached_plugin_dir = codex_cached_plugin_dir(home.path()); let legacy_config = write_codex_legacy_config(home.path()); @@ -626,6 +634,7 @@ fn codex_update_plugin_sweeps_legacy_config_when_cache_exists() { #[test] fn codex_update_plugin_refreshes_global_cache_and_repo_local_bundle() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project = TempDir::new().unwrap(); let cached_plugin_dir = codex_cached_plugin_dir(home.path()); write_codex_plugin_manifest(&cached_plugin_dir, "0.0.0"); @@ -663,6 +672,7 @@ fn codex_update_plugin_refreshes_global_cache_and_repo_local_bundle() { #[test] fn codex_update_plugin_repairs_personal_marketplace_for_bootstrap_bundle() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project_root = home.path().join("workspace"); let plugin_dir = codex_bootstrap_dir(home.path()); write_codex_plugin_manifest(&plugin_dir, "0.0.0"); @@ -683,6 +693,7 @@ fn codex_update_plugin_repairs_personal_marketplace_for_bootstrap_bundle() { #[test] fn codex_update_plugin_refreshes_repo_local_bundle_from_project_root() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project = TempDir::new().unwrap(); let codex = get_integration("codex").unwrap(); codex @@ -707,6 +718,7 @@ fn codex_update_plugin_refreshes_repo_local_bundle_from_project_root() { #[test] fn codex_uninstall_removes_repo_local_bundle_from_project_root() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project = TempDir::new().unwrap(); let codex = get_integration("codex").unwrap(); let install_ctx = ctx_with_project(home.path(), OLD_BIN, project.path()); @@ -725,6 +737,7 @@ fn codex_uninstall_removes_repo_local_bundle_from_project_root() { #[test] fn codex_update_plugin_migrates_legacy_config_only_install_to_plugin() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project_root = home.path().join("workspace"); let legacy_config = write_codex_legacy_config(home.path()); let codex = get_integration("codex").unwrap(); @@ -750,6 +763,7 @@ fn codex_update_plugin_migrates_legacy_config_only_install_to_plugin() { #[test] fn codex_update_plugin_migrates_legacy_config_even_when_repo_bundle_refreshes() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project = TempDir::new().unwrap(); let codex = get_integration("codex").unwrap(); // A repo-local bundle exists (so the refresh list is non-empty) alongside @@ -788,6 +802,7 @@ fn codex_update_plugin_migrates_legacy_config_even_when_repo_bundle_refreshes() #[test] fn codex_update_plugin_reports_not_installed_without_bundle_or_legacy_config() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project_root = home.path().join("workspace"); std::fs::create_dir_all(home.path().join(".codex")).unwrap(); let codex = get_integration("codex").unwrap(); @@ -805,6 +820,7 @@ fn codex_update_plugin_reports_not_installed_without_bundle_or_legacy_config() { #[test] fn kiro_update_plugin_rebakes_managed_agent_and_preserves_configs() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let kiro = get_integration("kiro").unwrap(); kiro.install(&ctx(home.path(), OLD_BIN)).unwrap(); @@ -840,6 +856,7 @@ fn kiro_update_plugin_rebakes_managed_agent_and_preserves_configs() { #[test] fn kiro_update_plugin_leaves_user_managed_agent_files_alone() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let kiro = get_integration("kiro").unwrap(); let agent_file = home.path().join(".kiro/agents/tracedecay.json"); @@ -1216,6 +1233,7 @@ fn assert_codex_rendered_bundle_valid(plugin_dir: &Path, bin: &str, scope: Codex #[test] fn cursor_install_renders_structurally_valid_bundle() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let cursor = get_integration("cursor").unwrap(); cursor.install(&ctx(home.path(), NEW_BIN)).unwrap(); assert_cursor_rendered_bundle_valid( @@ -1227,6 +1245,7 @@ fn cursor_install_renders_structurally_valid_bundle() { #[test] fn cursor_update_plugin_rerenders_structurally_valid_bundle() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let cursor = get_integration("cursor").unwrap(); cursor.install(&ctx(home.path(), OLD_BIN)).unwrap(); @@ -1241,6 +1260,7 @@ fn cursor_update_plugin_rerenders_structurally_valid_bundle() { #[test] fn codex_install_renders_structurally_valid_bundle() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let codex = get_integration("codex").unwrap(); codex.install(&ctx(home.path(), NEW_BIN)).unwrap(); @@ -1260,6 +1280,7 @@ fn codex_install_renders_structurally_valid_bundle() { #[test] fn codex_local_install_renders_project_scoped_mcp() { let home = TempDir::new().unwrap(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let project = TempDir::new().unwrap(); let codex = get_integration("codex").unwrap(); codex diff --git a/tests/agent_suite/upgrade_refresh_test.rs b/tests/agent_suite/upgrade_refresh_test.rs index f5b72ef8..f8cf2c44 100644 --- a/tests/agent_suite/upgrade_refresh_test.rs +++ b/tests/agent_suite/upgrade_refresh_test.rs @@ -24,6 +24,7 @@ fn make_install_ctx(home: &Path) -> InstallContext { fn claude_json_upgrade_refresh_is_idempotent_and_preserves_unknown_keys() { let dir = TempDir::new().unwrap(); let home = dir.path(); + let _agent_env = crate::common::AgentEnvLock::pin(&home); let ctx = make_install_ctx(home); let claude_json = home.join(".claude.json"); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 8a1995f5..74db3562 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -193,6 +193,56 @@ pub fn isolated_tracedecay_storage(tmp: &TempDir) -> TraceDecayStorageEnvGuard { TraceDecayStorageEnvGuard::for_tempdir(tmp) } +/// Isolation guard for agent install/uninstall tests that call `install()` / +/// `uninstall()` in-process. +/// +/// Those code paths resolve the profile root through +/// `automation::skill_targets::profile_root_for_agent_home`, which consults the +/// process-global [`USER_DATA_DIR_ENV`] and only falls back to +/// `/.tracedecay` when it is unset. Under `cargo test` (all tests share +/// one process), a sibling test that has pinned [`USER_DATA_DIR_ENV`] to *its* +/// tempdir makes a concurrent, otherwise-isolated install resolve to that +/// shared path instead of its own `home`. Two such installs then race to +/// write the same `/agent_managed/memory_digest_targets.json`, and the +/// atomic sibling-rename collides with a peer's `remove_dir` of the +/// `agent_managed` directory, surfacing as a spurious install failure. (Nextest +/// runs each test in its own process, so it is immune; this only bites the +/// in-process runner.) +/// +/// This guard restores the same contract the `*_exports_active_managed_skills` +/// tests already use: hold [`PROCESS_ENV_LOCK`] for the whole test body and pin +/// [`USER_DATA_DIR_ENV`] to this test's own `/.tracedecay`, so the install +/// always resolves to the test's own tempdir and can never be steered by (or +/// steer) a concurrent test's env. Pinning to `/.tracedecay` is exactly +/// the value the unset-env fallback would produce, so assertions on files under +/// `home` are unchanged. +/// +/// Sync (`blocking_lock`) so plain `#[test]` fns can use it without becoming +/// async; like [`IsolatedEnv::acquire_blocking`] it must not be called from +/// within a Tokio runtime. +/// +/// Field order matters: `_pin` is declared before `_lock` so the env var is +/// restored while the lock is still held, preventing a waiting test from +/// observing a half-torn-down env. +pub struct AgentEnvLock { + _pin: EnvVarGuard, + _lock: tokio::sync::MutexGuard<'static, ()>, +} + +impl AgentEnvLock { + /// Acquires [`PROCESS_ENV_LOCK`] and pins [`USER_DATA_DIR_ENV`] to + /// `/.tracedecay` for the returned guard's lifetime. `home` may be a + /// `&Path` or anything else that is `AsRef` (e.g. a `&TempDir`). + pub fn pin(home: impl AsRef) -> Self { + let lock = PROCESS_ENV_LOCK.blocking_lock(); + let pin = EnvVarGuard::set(USER_DATA_DIR_ENV, home.as_ref().join(".tracedecay")); + Self { + _pin: pin, + _lock: lock, + } + } +} + fn canonicalize_test_dir(path: &Path) -> PathBuf { fs::create_dir_all(path).unwrap_or_else(|err| { panic!( diff --git a/tests/hooks_lsp_suite/hooks_test.rs b/tests/hooks_lsp_suite/hooks_test.rs index c3e8bcf8..bd921ba4 100644 --- a/tests/hooks_lsp_suite/hooks_test.rs +++ b/tests/hooks_lsp_suite/hooks_test.rs @@ -960,7 +960,9 @@ async fn test_codex_user_prompt_submit_generic_workspace_suppresses_code_hints() // hook context generation resolves profile storage and records analytics. #[allow(clippy::await_holding_lock)] async fn test_codex_user_prompt_submit_records_workspace_status_and_missing_session_hint() { - let _lock = GLOBAL_DB_ENV_LOCK.lock().unwrap(); + let _lock = GLOBAL_DB_ENV_LOCK + .lock() + .unwrap_or_else(|err| err.into_inner()); let project = tempfile::tempdir().unwrap(); let generic = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); @@ -1046,7 +1048,9 @@ fn test_codex_workspace_status_distinguishes_generic_and_project_like_dirs() { #[test] fn test_codex_workspace_status_detects_initialized_trace_decay_project() { - let _lock = GLOBAL_DB_ENV_LOCK.lock().unwrap(); + let _lock = GLOBAL_DB_ENV_LOCK + .lock() + .unwrap_or_else(|err| err.into_inner()); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -1352,7 +1356,9 @@ fn test_codex_subagent_start_injects_context_for_new_no_history_agent() { #[test] fn test_codex_subagent_start_dedupes_context_per_session() { - let _lock = GLOBAL_DB_ENV_LOCK.lock().unwrap(); + let _lock = GLOBAL_DB_ENV_LOCK + .lock() + .unwrap_or_else(|err| err.into_inner()); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -1380,7 +1386,9 @@ fn test_codex_subagent_start_dedupes_context_per_session() { #[test] fn test_codex_subagent_start_no_history_does_not_suppress_later_research_context() { - let _lock = GLOBAL_DB_ENV_LOCK.lock().unwrap(); + let _lock = GLOBAL_DB_ENV_LOCK + .lock() + .unwrap_or_else(|err| err.into_inner()); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -1422,7 +1430,9 @@ fn test_codex_subagent_start_no_history_does_not_suppress_later_research_context #[test] fn test_codex_subagent_start_counts_and_formats_log_line() { - let _lock = GLOBAL_DB_ENV_LOCK.lock().unwrap(); + let _lock = GLOBAL_DB_ENV_LOCK + .lock() + .unwrap_or_else(|err| err.into_inner()); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -1527,3 +1537,46 @@ fn test_codex_project_root_uses_cwd() { Some(dir.path().to_path_buf()) ); } + +/// Regression guard for the shared-env-lock poison cascade. +/// +/// `GLOBAL_DB_ENV_LOCK` is a `std::sync::Mutex<()>` used purely for mutual +/// exclusion across env-mutating tests in this binary. Some of those tests run +/// real `git`/`fs` operations under the guard and can panic under load; a panic +/// while the guard is held poisons the mutex. Sibling tests that acquired it +/// with the fragile `.lock().unwrap()` then died with `PoisonError`, turning one +/// unrelated failure into a cascade of red tests. The call sites now use +/// `.lock().unwrap_or_else(|err| err.into_inner())`, which recovers the guard on +/// poison so serialization still holds but a prior panic can no longer cascade. +/// +/// This test pins that contract on an isolated mutex (so it never poisons the +/// real shared lock): it forces poison, confirms the fragile form would fail, +/// and confirms the tolerant form recovers a usable guard. +#[test] +fn poisoned_env_lock_is_recovered_by_tolerant_acquire() { + use std::sync::Mutex; + + static LOCK: Mutex<()> = Mutex::new(()); + + // Poison the mutex the same way a panicking lock-holder would. + let poisoned = std::panic::catch_unwind(|| { + let _guard = LOCK.lock().unwrap(); + panic!("simulated panic while holding the env lock"); + }); + assert!(poisoned.is_err(), "the injected closure must have panicked"); + assert!( + LOCK.is_poisoned(), + "a panic while holding the guard must poison the mutex" + ); + + // The fragile idiom (`.lock().unwrap()`) is what caused the cascade: on a + // poisoned lock it returns Err, so `.unwrap()` would panic. + assert!( + LOCK.lock().is_err(), + "poisoned lock must surface Err to a plain lock()/unwrap() caller" + ); + + // The standardized tolerant idiom recovers the guard instead of panicking, + // so a follower no longer inherits an unrelated test's panic. + let _recovered = LOCK.lock().unwrap_or_else(|err| err.into_inner()); +} diff --git a/tests/storage_suite/corruption_test.rs b/tests/storage_suite/corruption_test.rs index b7272053..d429d4cc 100644 --- a/tests/storage_suite/corruption_test.rs +++ b/tests/storage_suite/corruption_test.rs @@ -206,7 +206,9 @@ async fn bulk_load_preserves_platform_synchronous_mode() { // the *durable* platform synchronous mode, which // TRACEDECAY_SQLITE_UNSAFE_FAST=1 (exported for the whole Windows CI test // run) would relax to OFF. - let _env_lock = common::GLOBAL_DB_ENV_LOCK.lock().unwrap(); + let _env_lock = common::GLOBAL_DB_ENV_LOCK + .lock() + .unwrap_or_else(|err| err.into_inner()); let _unsafe_fast_off = common::EnvVarGuard::unset(tracedecay::db::SQLITE_UNSAFE_FAST_ENV); let (db, _dir, _path) = setup_db().await; From b5390302fc784bd94ae99f79f4a923f630eb9c1e Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 4 Jul 2026 17:49:19 +0000 Subject: [PATCH 02/15] test: drop needless borrow on AgentEnvLock::pin at &Path call sites home is &Path (dir.path()) at these sites, so pin(&home) is a needless borrow (clippy, blocking policy); PathBuf sites keep the borrow. Co-Authored-By: Claude Fable 5 --- tests/agent_suite/agent_test.rs | 98 +++++++++++------------ tests/agent_suite/claude_agent_test.rs | 44 +++++----- tests/agent_suite/copilot_agent_test.rs | 32 ++++---- tests/agent_suite/kiro_agent_test.rs | 32 ++++---- tests/agent_suite/opencode_agent_test.rs | 36 ++++----- tests/agent_suite/upgrade_refresh_test.rs | 2 +- 6 files changed, 122 insertions(+), 122 deletions(-) diff --git a/tests/agent_suite/agent_test.rs b/tests/agent_suite/agent_test.rs index 8cfc7844..69a48662 100644 --- a/tests/agent_suite/agent_test.rs +++ b/tests/agent_suite/agent_test.rs @@ -3111,7 +3111,7 @@ fn test_local_install_rejects_cline_without_project_mutation() { fn test_claude_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -3198,7 +3198,7 @@ fn test_claude_install_creates_config() { fn test_gemini_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); GeminiIntegration.install(&ctx).unwrap(); @@ -3232,7 +3232,7 @@ fn test_gemini_install_creates_config() { fn test_codex_install_creates_plugin_bundle_and_marketplace() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); CodexIntegration.install(&ctx).unwrap(); @@ -3344,7 +3344,7 @@ async fn test_codex_shareable_plugin_artifact_exports_bundle_and_managed_skills( fn test_codex_install_refreshes_existing_cache_and_keeps_bootstrap_source_listable() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); let stale_plugin_dir = codex_stale_cached_plugin_install_dir(home); write_codex_plugin_manifest(&stale_plugin_dir, "0.0.0"); @@ -3389,7 +3389,7 @@ fn test_codex_install_refreshes_existing_cache_and_keeps_bootstrap_source_listab fn test_codex_install_refreshes_existing_cache_and_prunes_stale_skills() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); let cached_plugin_dir = codex_cached_plugin_install_dir(home); let bootstrap_dir = codex_plugin_install_dir(home); @@ -3429,7 +3429,7 @@ fn test_codex_install_refreshes_existing_cache_and_prunes_stale_skills() { fn test_codex_install_sweeps_legacy_global_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let codex_dir = home.join(".codex"); std::fs::create_dir_all(&codex_dir).unwrap(); std::fs::write( @@ -3640,7 +3640,7 @@ fn assert_codex_hooks_registered(hooks: &serde_json::Value) { fn test_codex_global_install_bundles_hooks_in_plugin() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); CodexIntegration.install(&ctx).unwrap(); @@ -3708,7 +3708,7 @@ fn assert_command_contains_expected_bin( fn test_codex_install_reconciles_hooks_idempotently() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); CodexIntegration.install(&ctx).unwrap(); @@ -3741,7 +3741,7 @@ fn test_codex_install_reconciles_hooks_idempotently() { fn test_codex_uninstall_removes_plugin_hooks() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); CodexIntegration.install(&ctx).unwrap(); @@ -3760,7 +3760,7 @@ fn test_codex_uninstall_removes_plugin_hooks() { fn test_kimi_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); KimiIntegration.install(&ctx).unwrap(); @@ -3787,7 +3787,7 @@ fn test_kimi_install_creates_config() { fn test_kimi_install_then_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); KimiIntegration.install(&ctx).unwrap(); @@ -3815,7 +3815,7 @@ fn test_kimi_install_then_uninstall() { fn test_kimi_is_detected_and_has_tracedecay() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); assert!(!KimiIntegration.is_detected(home)); assert!(!KimiIntegration.has_tracedecay(home)); @@ -3831,7 +3831,7 @@ fn test_kimi_is_detected_and_has_tracedecay() { fn test_cursor_install_creates_plugin() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); CursorIntegration.install(&ctx).unwrap(); @@ -3947,7 +3947,7 @@ async fn test_prompt_integrations_export_active_managed_skill_indexes() { fn test_opencode_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // OpenCode uses ~/.config/opencode/opencode.json // Create the parent dir so install can discover it let ctx = make_install_ctx(home); @@ -3967,7 +3967,7 @@ fn test_opencode_install_creates_config() { fn test_zed_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); ZedIntegration.install(&ctx).unwrap(); @@ -3991,7 +3991,7 @@ fn test_zed_install_creates_config() { fn test_cline_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); ClineIntegration.install(&ctx).unwrap(); @@ -4022,7 +4022,7 @@ fn test_cline_install_creates_config() { fn test_roo_code_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); RooCodeIntegration.install(&ctx).unwrap(); @@ -4048,7 +4048,7 @@ fn test_roo_code_install_creates_config() { fn test_copilot_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -4089,7 +4089,7 @@ fn test_copilot_install_creates_config() { fn test_vibe_install_creates_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); VibeIntegration.install(&ctx).unwrap(); @@ -4134,7 +4134,7 @@ fn test_vibe_install_creates_config() { fn test_claude_install_then_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); let marketplace_manifest = @@ -4214,7 +4214,7 @@ fn test_claude_uninstall_unrecords_memory_digest_target() { fn test_gemini_install_then_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); GeminiIntegration.install(&ctx).unwrap(); @@ -4252,7 +4252,7 @@ fn test_gemini_install_then_uninstall() { fn test_codex_install_then_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); CodexIntegration.install(&ctx).unwrap(); @@ -4349,7 +4349,7 @@ fn test_codex_install_preserves_existing_config() { // empty table. let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); std::fs::create_dir_all(home.join(".codex")).unwrap(); let config_path = home.join(".codex/config.toml"); let original = "\ @@ -4388,7 +4388,7 @@ fn test_codex_install_leaves_unparseable_config_untouched() { // user config must be left byte-identical while the plugin bundle installs. let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); std::fs::create_dir_all(home.join(".codex")).unwrap(); let config_path = home.join(".codex/config.toml"); let original = "this is not valid TOML {{{{"; @@ -4471,7 +4471,7 @@ fn test_claude_install_preserves_existing_config() { // foreign settings key and a foreign registered marketplace must survive. let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let claude_dir = home.join(".claude"); let plugins_dir = claude_dir.join("plugins"); std::fs::create_dir_all(&plugins_dir).unwrap(); @@ -4611,7 +4611,7 @@ fn test_cursor_uninstall_backs_up_config_with_other_content() { // before rewriting, so a botched rewrite is recoverable. let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); let path = home.join(".cursor/mcp.json"); @@ -4678,7 +4678,7 @@ fn test_antigravity_install_preserves_existing_config() { fn test_antigravity_install_writes_cli_plugin() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let bin = "/usr/local/bin/tracedecay"; let ctx = InstallContext { home: home.to_path_buf(), @@ -4731,7 +4731,7 @@ fn test_antigravity_install_writes_cli_plugin() { fn test_antigravity_uninstall_removes_both_locations() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let bin = "/usr/local/bin/tracedecay"; let ctx = InstallContext { home: home.to_path_buf(), @@ -4821,7 +4821,7 @@ fn test_every_tested_agent_advertises_primary_config_path() { fn test_cursor_install_then_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); CursorIntegration.install(&ctx).unwrap(); @@ -4857,7 +4857,7 @@ fn test_cursor_install_then_uninstall() { fn test_copilot_install_then_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -4880,7 +4880,7 @@ fn test_copilot_install_then_uninstall() { fn test_vibe_install_then_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); VibeIntegration.install(&ctx).unwrap(); @@ -4935,7 +4935,7 @@ fn make_install_ctx_with_real_bin(home: &Path) -> InstallContext { fn test_healthcheck_claude_clean_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx_with_real_bin(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -4952,7 +4952,7 @@ fn test_healthcheck_claude_clean_install() { fn test_healthcheck_gemini_clean_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); GeminiIntegration.install(&ctx).unwrap(); @@ -4969,7 +4969,7 @@ fn test_healthcheck_gemini_clean_install() { fn test_healthcheck_codex_after_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); CodexIntegration.install(&ctx).unwrap(); @@ -5051,7 +5051,7 @@ fn test_healthcheck_codex_ignores_unrelated_project_agents_md() { fn test_healthcheck_cursor_clean_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); CursorIntegration.install(&ctx).unwrap(); @@ -5187,7 +5187,7 @@ fn test_healthcheck_hermes_local_install_checks_project_hermes_home() { fn test_healthcheck_opencode_clean_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -5556,7 +5556,7 @@ fn test_parse_jsonc() { fn test_is_detected_claude() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); assert!(!ClaudeIntegration.is_detected(home)); std::fs::create_dir_all(home.join(".claude")).unwrap(); assert!(ClaudeIntegration.is_detected(home)); @@ -5624,7 +5624,7 @@ fn test_is_detected_copilot() { fn test_has_tracedecay_claude() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // No config => false assert!(!ClaudeIntegration.has_tracedecay(home)); @@ -5642,7 +5642,7 @@ fn test_has_tracedecay_claude() { fn test_has_tracedecay_gemini() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); assert!(!GeminiIntegration.has_tracedecay(home)); let ctx = make_install_ctx(home); @@ -5654,7 +5654,7 @@ fn test_has_tracedecay_gemini() { fn test_has_tracedecay_codex() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); assert!(!CodexIntegration.has_tracedecay(home)); let ctx = make_install_ctx(home); @@ -5672,7 +5672,7 @@ fn test_has_tracedecay_codex() { fn test_has_tracedecay_cursor() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); assert!(!CursorIntegration.has_tracedecay(home)); let ctx = make_install_ctx(home); @@ -5684,7 +5684,7 @@ fn test_has_tracedecay_cursor() { fn test_has_tracedecay_opencode() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); assert!(!OpenCodeIntegration.has_tracedecay(home)); let ctx = make_install_ctx(home); @@ -5696,7 +5696,7 @@ fn test_has_tracedecay_opencode() { fn test_has_tracedecay_copilot() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); assert!(!CopilotIntegration.has_tracedecay(home)); let ctx = make_install_ctx(home); @@ -5712,7 +5712,7 @@ fn test_has_tracedecay_copilot() { fn test_claude_install_idempotent() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); // Install twice should not fail @@ -5739,7 +5739,7 @@ fn test_claude_install_idempotent() { fn test_gemini_install_idempotent() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); GeminiIntegration.install(&ctx).unwrap(); @@ -5755,7 +5755,7 @@ fn test_gemini_install_idempotent() { fn test_uninstall_without_install_does_not_crash() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); // Uninstalling when nothing is installed should not panic or error @@ -5780,7 +5780,7 @@ fn test_uninstall_without_install_does_not_crash() { fn test_claude_install_preserves_existing_claude_json() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Pre-populate .claude.json with a foreign MCP server and a custom key. // The plugin model no longer writes tracedecay into ~/.claude.json, and the @@ -5812,7 +5812,7 @@ fn test_claude_install_preserves_existing_claude_json() { fn test_gemini_install_preserves_existing_settings() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let settings_path = home.join(".gemini/settings.json"); std::fs::create_dir_all(home.join(".gemini")).unwrap(); @@ -6031,7 +6031,7 @@ fn test_migrate_installed_agents_skips_when_already_populated() { fn test_migrate_installed_agents_detects_installed_agents() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Install copilot so it can be detected let ctx = make_install_ctx(home); diff --git a/tests/agent_suite/claude_agent_test.rs b/tests/agent_suite/claude_agent_test.rs index 38b974a0..92593a53 100644 --- a/tests/agent_suite/claude_agent_test.rs +++ b/tests/agent_suite/claude_agent_test.rs @@ -77,7 +77,7 @@ fn permission_allowlist(settings: &serde_json::Value) -> Vec<&str> { fn test_install_deploys_plugin_mcp_server() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -105,7 +105,7 @@ fn test_install_deploys_plugin_mcp_server() { fn test_install_deploys_plugin_hooks() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -170,7 +170,7 @@ fn test_install_deploys_plugin_hooks() { fn test_install_creates_settings_with_permissions() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -282,7 +282,7 @@ fn test_install_claude_md_has_moment_triggers() { fn test_install_creates_claude_md_with_rules() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -313,7 +313,7 @@ fn test_install_creates_claude_md_with_rules() { fn test_claude_md_contains_explore_agent_paragraph() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Pre-populate CLAUDE.md with existing content let claude_dir = home.join(".claude"); @@ -342,7 +342,7 @@ fn test_claude_md_contains_explore_agent_paragraph() { fn test_uninstall_removes_explore_agent_paragraph() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Pre-populate CLAUDE.md with existing content let claude_dir = home.join(".claude"); @@ -378,7 +378,7 @@ fn test_uninstall_removes_explore_agent_paragraph() { fn test_install_idempotent_claude_md() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -397,7 +397,7 @@ fn test_install_idempotent_claude_md() { fn test_install_preserves_existing_claude_json() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Pre-populate .claude.json with an extra key std::fs::write(home.join(".claude.json"), r#"{"foo": "bar"}"#).unwrap(); @@ -425,7 +425,7 @@ fn test_install_preserves_existing_claude_json() { fn test_install_preserves_existing_settings() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Pre-populate settings.json with an existing hook let claude_dir = home.join(".claude"); @@ -485,7 +485,7 @@ fn test_install_preserves_existing_settings() { fn test_install_migrates_off_config_managed_integration() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let claude_dir = home.join(".claude"); std::fs::create_dir_all(&claude_dir).unwrap(); @@ -579,7 +579,7 @@ fn test_install_migrates_off_config_managed_integration() { fn test_uninstall_removes_mcp_from_claude_json() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); ClaudeIntegration.uninstall(&ctx).unwrap(); @@ -603,7 +603,7 @@ fn test_uninstall_removes_mcp_from_claude_json() { fn test_uninstall_removes_deployed_bundle_and_lone_marketplace_file() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); // Install deploys the plugin bundle and registers the marketplace. @@ -654,7 +654,7 @@ fn test_uninstall_removes_deployed_bundle_and_lone_marketplace_file() { fn test_uninstall_removes_hook_from_settings() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); ClaudeIntegration.uninstall(&ctx).unwrap(); @@ -691,7 +691,7 @@ fn test_uninstall_removes_hook_from_settings() { fn test_uninstall_removes_permissions_from_settings() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); ClaudeIntegration.uninstall(&ctx).unwrap(); @@ -719,7 +719,7 @@ fn test_uninstall_removes_permissions_from_settings() { fn test_uninstall_preserves_other_permissions() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Install first so all files are set up let ctx = make_install_ctx(home); @@ -754,7 +754,7 @@ fn test_uninstall_preserves_other_permissions() { fn test_uninstall_removes_claude_md_rules() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -777,7 +777,7 @@ fn test_uninstall_removes_claude_md_rules() { fn test_uninstall_preserves_other_claude_md_content() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Create CLAUDE.md with pre-existing content let claude_dir = home.join(".claude"); @@ -909,7 +909,7 @@ fn test_healthcheck_detects_missing_permissions() { fn test_healthcheck_detects_stale_permissions() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx_with_real_bin(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -937,7 +937,7 @@ fn test_healthcheck_detects_stale_permissions() { fn test_healthcheck_detects_missing_claude_md() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx_with_real_bin(home); ClaudeIntegration.install(&ctx).unwrap(); @@ -960,7 +960,7 @@ fn test_healthcheck_detects_missing_claude_md() { fn test_healthcheck_clean_local_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let project = dir.path().join("myproject"); std::fs::create_dir_all(&project).unwrap(); @@ -994,7 +994,7 @@ fn test_healthcheck_clean_local_config() { fn test_healthcheck_local_settings_cleanup() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let project = dir.path().join("myproject"); let local_claude = project.join(".claude"); std::fs::create_dir_all(&local_claude).unwrap(); @@ -1042,7 +1042,7 @@ fn test_healthcheck_local_settings_cleanup() { fn test_has_tracedecay_after_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); ClaudeIntegration.install(&ctx).unwrap(); diff --git a/tests/agent_suite/copilot_agent_test.rs b/tests/agent_suite/copilot_agent_test.rs index c7d6266d..c25e6d4d 100644 --- a/tests/agent_suite/copilot_agent_test.rs +++ b/tests/agent_suite/copilot_agent_test.rs @@ -58,7 +58,7 @@ fn cli_config_path(home: &Path) -> std::path::PathBuf { fn test_install_creates_vscode_settings_with_mcp_server() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -95,7 +95,7 @@ fn test_install_creates_vscode_settings_with_mcp_server() { fn test_install_creates_cli_config_with_mcp_server() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -127,7 +127,7 @@ fn test_install_creates_cli_config_with_mcp_server() { fn test_install_preserves_existing_vscode_settings() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Pre-populate VS Code settings with other content let settings_path = vscode_settings_path(home); @@ -156,7 +156,7 @@ fn test_install_preserves_existing_vscode_settings() { fn test_install_preserves_existing_cli_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Pre-populate CLI config with another MCP server let cli_path = cli_config_path(home); @@ -185,7 +185,7 @@ fn test_install_preserves_existing_cli_config() { fn test_install_idempotent_vscode() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -206,7 +206,7 @@ fn test_install_idempotent_vscode() { fn test_install_idempotent_cli() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -229,7 +229,7 @@ fn test_install_idempotent_cli() { fn test_uninstall_removes_vscode_mcp_entry() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -257,7 +257,7 @@ fn test_uninstall_removes_vscode_mcp_entry() { fn test_uninstall_cleans_empty_mcp_objects() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -277,7 +277,7 @@ fn test_uninstall_cleans_empty_mcp_objects() { fn test_uninstall_removes_cli_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -302,7 +302,7 @@ fn test_uninstall_removes_cli_config() { fn test_uninstall_preserves_other_cli_servers() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Pre-populate CLI config with another server let cli_path = cli_config_path(home); @@ -337,7 +337,7 @@ fn test_uninstall_preserves_other_cli_servers() { fn test_uninstall_preserves_other_vscode_settings() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Pre-populate VS Code settings let settings_path = vscode_settings_path(home); @@ -359,7 +359,7 @@ fn test_uninstall_preserves_other_vscode_settings() { fn test_uninstall_without_install_does_not_crash() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); // Should not panic or error CopilotIntegration.uninstall(&ctx).unwrap(); @@ -369,7 +369,7 @@ fn test_uninstall_without_install_does_not_crash() { fn test_uninstall_cli_with_no_tracedecay_is_noop() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Create a CLI config without tracedecay let cli_path = cli_config_path(home); @@ -396,7 +396,7 @@ fn test_uninstall_cli_with_no_tracedecay_is_noop() { fn test_healthcheck_clean_install_no_issues() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); @@ -609,7 +609,7 @@ fn test_has_tracedecay_before_install() { fn test_has_tracedecay_after_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); assert!( @@ -622,7 +622,7 @@ fn test_has_tracedecay_after_install() { fn test_has_tracedecay_after_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); CopilotIntegration.install(&ctx).unwrap(); CopilotIntegration.uninstall(&ctx).unwrap(); diff --git a/tests/agent_suite/kiro_agent_test.rs b/tests/agent_suite/kiro_agent_test.rs index f0409ed3..a867bcd2 100644 --- a/tests/agent_suite/kiro_agent_test.rs +++ b/tests/agent_suite/kiro_agent_test.rs @@ -102,7 +102,7 @@ fn assert_hook( fn test_install_creates_global_mcp_steering_agent_and_default() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); KiroIntegration.install(&ctx).unwrap(); @@ -294,7 +294,7 @@ async fn test_install_preserves_user_managed_agent_while_updating_skill_index() fn test_install_preserves_existing_mcp_config_and_writes_backup() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let mcp_path = home.join(".kiro/settings/mcp.json"); std::fs::create_dir_all(mcp_path.parent().unwrap()).unwrap(); std::fs::write( @@ -320,7 +320,7 @@ fn test_install_preserves_existing_mcp_config_and_writes_backup() { fn test_install_and_uninstall_preserve_existing_steering_content() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); let user_steering_path = home.join(".kiro/steering/team.md"); @@ -355,7 +355,7 @@ fn test_install_and_uninstall_preserve_existing_steering_content() { fn test_uninstall_preserves_user_steering_after_tracedecay_block() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); KiroIntegration.install(&ctx).unwrap(); @@ -385,7 +385,7 @@ fn test_uninstall_preserves_user_steering_after_tracedecay_block() { fn test_install_replaces_legacy_marker_block_and_uninstall_removes_it() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); let steering_path = home.join(".kiro/steering/tracedecay.md"); @@ -433,7 +433,7 @@ fn test_install_replaces_legacy_marker_block_and_uninstall_removes_it() { fn test_uninstall_removes_legacy_marker_block_preserving_user_content() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); let steering_path = home.join(".kiro/steering/tracedecay.md"); @@ -460,7 +460,7 @@ fn test_uninstall_removes_legacy_marker_block_preserving_user_content() { fn test_uninstall_removes_tracedecay_and_preserves_other_mcp_servers() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); let mcp_path = home.join(".kiro/settings/mcp.json"); @@ -494,7 +494,7 @@ fn test_uninstall_removes_tracedecay_and_preserves_other_mcp_servers() { fn test_install_and_uninstall_preserve_user_managed_custom_agent() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); let agent_path = home.join(".kiro/agents/tracedecay.json"); @@ -533,7 +533,7 @@ fn test_install_and_uninstall_preserve_user_managed_custom_agent() { fn test_install_preserves_existing_custom_default_agent_choice() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); let cli_path = home.join(".kiro/settings/cli.json"); @@ -556,7 +556,7 @@ fn test_install_preserves_existing_custom_default_agent_choice() { fn test_install_replaces_builtin_default_agent_choice() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); let cli_path = home.join(".kiro/settings/cli.json"); @@ -573,7 +573,7 @@ fn test_install_replaces_builtin_default_agent_choice() { fn test_has_tracedecay_tracks_global_mcp_entry() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); assert!(!KiroIntegration.has_tracedecay(home)); @@ -589,7 +589,7 @@ fn test_has_tracedecay_tracks_global_mcp_entry() { fn test_healthcheck_clean_install_has_no_issues_or_warnings() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); KiroIntegration.install(&ctx).unwrap(); @@ -609,7 +609,7 @@ fn test_healthcheck_clean_install_has_no_issues_or_warnings() { fn test_healthcheck_fails_when_steering_lacks_owned_end_marker() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); KiroIntegration.install(&ctx).unwrap(); @@ -638,7 +638,7 @@ fn test_healthcheck_fails_when_steering_lacks_owned_end_marker() { fn test_healthcheck_warns_when_agent_tool_policy_is_not_permissive() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); KiroIntegration.install(&ctx).unwrap(); @@ -671,7 +671,7 @@ fn test_healthcheck_fails_when_workspace_mcp_disables_tracedecay() { let home_dir = TempDir::new().unwrap(); let project_dir = TempDir::new().unwrap(); let home = home_dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let project = project_dir.path(); let ctx = make_ctx(home); @@ -703,7 +703,7 @@ fn test_healthcheck_fails_when_workspace_mcp_shadows_global_command() { let home_dir = TempDir::new().unwrap(); let project_dir = TempDir::new().unwrap(); let home = home_dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let project = project_dir.path(); let ctx = make_ctx(home); diff --git a/tests/agent_suite/opencode_agent_test.rs b/tests/agent_suite/opencode_agent_test.rs index b335661c..16c1813f 100644 --- a/tests/agent_suite/opencode_agent_test.rs +++ b/tests/agent_suite/opencode_agent_test.rs @@ -47,7 +47,7 @@ fn opencode_prompt_path(home: &Path) -> std::path::PathBuf { fn test_install_creates_opencode_json() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -80,7 +80,7 @@ fn test_install_creates_opencode_json() { fn test_install_creates_opencode_md_with_rules() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -102,7 +102,7 @@ fn test_install_creates_opencode_md_with_rules() { fn test_install_preserves_existing_opencode_json() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Pre-populate opencode.json with existing content let config_path = opencode_config_path(home); @@ -136,7 +136,7 @@ fn test_install_preserves_existing_opencode_json() { fn test_install_idempotent_opencode_json() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -152,7 +152,7 @@ fn test_install_idempotent_opencode_json() { fn test_install_idempotent_opencode_md() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -172,7 +172,7 @@ fn test_install_idempotent_opencode_md() { fn test_install_preserves_existing_opencode_md_content() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Create AGENTS.md with pre-existing content let config_dir = home.join(".config/opencode"); @@ -205,7 +205,7 @@ fn test_install_preserves_existing_opencode_md_content() { fn test_uninstall_removes_mcp_from_config() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -227,7 +227,7 @@ fn test_uninstall_removes_mcp_from_config() { fn test_uninstall_removes_empty_config_file() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -245,7 +245,7 @@ fn test_uninstall_removes_empty_config_file() { fn test_uninstall_preserves_other_mcp_servers() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Pre-populate with another server let config_path = opencode_config_path(home); @@ -280,7 +280,7 @@ fn test_uninstall_preserves_other_mcp_servers() { fn test_uninstall_removes_opencode_md_rules() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -303,7 +303,7 @@ fn test_uninstall_removes_opencode_md_rules() { fn test_uninstall_preserves_other_opencode_md_content() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Create AGENTS.md with pre-existing content let config_dir = home.join(".config/opencode"); @@ -335,7 +335,7 @@ fn test_uninstall_preserves_other_opencode_md_content() { fn test_uninstall_without_install_does_not_crash() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); // Should not panic or error OpenCodeIntegration.uninstall(&ctx).unwrap(); @@ -345,7 +345,7 @@ fn test_uninstall_without_install_does_not_crash() { fn test_uninstall_config_with_no_tracedecay_is_noop() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); // Create opencode.json without tracedecay let config_path = opencode_config_path(home); @@ -372,7 +372,7 @@ fn test_uninstall_config_with_no_tracedecay_is_noop() { fn test_healthcheck_clean_install_no_issues() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -459,7 +459,7 @@ fn test_healthcheck_detects_missing_serve_arg() { fn test_healthcheck_detects_missing_opencode_md() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -483,7 +483,7 @@ fn test_healthcheck_detects_missing_opencode_md() { fn test_healthcheck_detects_missing_tracedecay_rules_in_opencode_md() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); @@ -546,7 +546,7 @@ fn test_has_tracedecay_before_install() { fn test_has_tracedecay_after_install() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); assert!( @@ -559,7 +559,7 @@ fn test_has_tracedecay_after_install() { fn test_has_tracedecay_after_uninstall() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_ctx(home); OpenCodeIntegration.install(&ctx).unwrap(); OpenCodeIntegration.uninstall(&ctx).unwrap(); diff --git a/tests/agent_suite/upgrade_refresh_test.rs b/tests/agent_suite/upgrade_refresh_test.rs index f8cf2c44..1b6553f6 100644 --- a/tests/agent_suite/upgrade_refresh_test.rs +++ b/tests/agent_suite/upgrade_refresh_test.rs @@ -24,7 +24,7 @@ fn make_install_ctx(home: &Path) -> InstallContext { fn claude_json_upgrade_refresh_is_idempotent_and_preserves_unknown_keys() { let dir = TempDir::new().unwrap(); let home = dir.path(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = crate::common::AgentEnvLock::pin(home); let ctx = make_install_ctx(home); let claude_json = home.join(".claude.json"); From e7cea1ebead24bc684dc51b10d1e035451a63be1 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 4 Jul 2026 19:56:57 +0000 Subject: [PATCH 03/15] chore: deslop test isolation comments --- tests/agent_suite/agent_test.rs | 5 +--- tests/common/mod.rs | 40 +++++------------------------ tests/hooks_lsp_suite/hooks_test.rs | 20 +-------------- 3 files changed, 9 insertions(+), 56 deletions(-) diff --git a/tests/agent_suite/agent_test.rs b/tests/agent_suite/agent_test.rs index 69a48662..58cb3a9e 100644 --- a/tests/agent_suite/agent_test.rs +++ b/tests/agent_suite/agent_test.rs @@ -4425,10 +4425,7 @@ fn assert_install_backs_up_and_preserves( original: &str, marker: &str, ) { - // Serialize + pin USER_DATA_DIR_ENV to this test's own home so the in-process - // install here can't resolve its profile root to a concurrent test's pinned - // tempdir and collide on the shared memory-digest target file. See - // `common::AgentEnvLock`. Callers must not hold the lock themselves. + // Serializes env-mutating installs; callers must not hold AgentEnvLock. let _agent_env = crate::common::AgentEnvLock::pin(home); let config_path = agent .primary_config_path(home) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 74db3562..5918005d 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -193,46 +193,20 @@ pub fn isolated_tracedecay_storage(tmp: &TempDir) -> TraceDecayStorageEnvGuard { TraceDecayStorageEnvGuard::for_tempdir(tmp) } -/// Isolation guard for agent install/uninstall tests that call `install()` / -/// `uninstall()` in-process. +/// Serializes in-process agent install/uninstall tests and pins +/// [`USER_DATA_DIR_ENV`] to that test's home. /// -/// Those code paths resolve the profile root through -/// `automation::skill_targets::profile_root_for_agent_home`, which consults the -/// process-global [`USER_DATA_DIR_ENV`] and only falls back to -/// `/.tracedecay` when it is unset. Under `cargo test` (all tests share -/// one process), a sibling test that has pinned [`USER_DATA_DIR_ENV`] to *its* -/// tempdir makes a concurrent, otherwise-isolated install resolve to that -/// shared path instead of its own `home`. Two such installs then race to -/// write the same `/agent_managed/memory_digest_targets.json`, and the -/// atomic sibling-rename collides with a peer's `remove_dir` of the -/// `agent_managed` directory, surfacing as a spurious install failure. (Nextest -/// runs each test in its own process, so it is immune; this only bites the -/// in-process runner.) -/// -/// This guard restores the same contract the `*_exports_active_managed_skills` -/// tests already use: hold [`PROCESS_ENV_LOCK`] for the whole test body and pin -/// [`USER_DATA_DIR_ENV`] to this test's own `/.tracedecay`, so the install -/// always resolves to the test's own tempdir and can never be steered by (or -/// steer) a concurrent test's env. Pinning to `/.tracedecay` is exactly -/// the value the unset-env fallback would produce, so assertions on files under -/// `home` are unchanged. -/// -/// Sync (`blocking_lock`) so plain `#[test]` fns can use it without becoming -/// async; like [`IsolatedEnv::acquire_blocking`] it must not be called from -/// within a Tokio runtime. -/// -/// Field order matters: `_pin` is declared before `_lock` so the env var is -/// restored while the lock is still held, preventing a waiting test from -/// observing a half-torn-down env. +/// Without this, concurrent `cargo test` cases can point each other at the same +/// managed-skill target file and race during atomic rewrites. Field order keeps +/// the env pin alive until just before the lock is released. pub struct AgentEnvLock { _pin: EnvVarGuard, _lock: tokio::sync::MutexGuard<'static, ()>, } impl AgentEnvLock { - /// Acquires [`PROCESS_ENV_LOCK`] and pins [`USER_DATA_DIR_ENV`] to - /// `/.tracedecay` for the returned guard's lifetime. `home` may be a - /// `&Path` or anything else that is `AsRef` (e.g. a `&TempDir`). + /// Pins [`USER_DATA_DIR_ENV`] to `/.tracedecay` while holding + /// [`PROCESS_ENV_LOCK`]. pub fn pin(home: impl AsRef) -> Self { let lock = PROCESS_ENV_LOCK.blocking_lock(); let pin = EnvVarGuard::set(USER_DATA_DIR_ENV, home.as_ref().join(".tracedecay")); diff --git a/tests/hooks_lsp_suite/hooks_test.rs b/tests/hooks_lsp_suite/hooks_test.rs index bd921ba4..9da798bc 100644 --- a/tests/hooks_lsp_suite/hooks_test.rs +++ b/tests/hooks_lsp_suite/hooks_test.rs @@ -1538,27 +1538,13 @@ fn test_codex_project_root_uses_cwd() { ); } -/// Regression guard for the shared-env-lock poison cascade. -/// -/// `GLOBAL_DB_ENV_LOCK` is a `std::sync::Mutex<()>` used purely for mutual -/// exclusion across env-mutating tests in this binary. Some of those tests run -/// real `git`/`fs` operations under the guard and can panic under load; a panic -/// while the guard is held poisons the mutex. Sibling tests that acquired it -/// with the fragile `.lock().unwrap()` then died with `PoisonError`, turning one -/// unrelated failure into a cascade of red tests. The call sites now use -/// `.lock().unwrap_or_else(|err| err.into_inner())`, which recovers the guard on -/// poison so serialization still holds but a prior panic can no longer cascade. -/// -/// This test pins that contract on an isolated mutex (so it never poisons the -/// real shared lock): it forces poison, confirms the fragile form would fail, -/// and confirms the tolerant form recovers a usable guard. +/// Regression guard for tolerant recovery from a poisoned env lock. #[test] fn poisoned_env_lock_is_recovered_by_tolerant_acquire() { use std::sync::Mutex; static LOCK: Mutex<()> = Mutex::new(()); - // Poison the mutex the same way a panicking lock-holder would. let poisoned = std::panic::catch_unwind(|| { let _guard = LOCK.lock().unwrap(); panic!("simulated panic while holding the env lock"); @@ -1569,14 +1555,10 @@ fn poisoned_env_lock_is_recovered_by_tolerant_acquire() { "a panic while holding the guard must poison the mutex" ); - // The fragile idiom (`.lock().unwrap()`) is what caused the cascade: on a - // poisoned lock it returns Err, so `.unwrap()` would panic. assert!( LOCK.lock().is_err(), "poisoned lock must surface Err to a plain lock()/unwrap() caller" ); - // The standardized tolerant idiom recovers the guard instead of panicking, - // so a follower no longer inherits an unrelated test's panic. let _recovered = LOCK.lock().unwrap_or_else(|err| err.into_inner()); } From 5b066c5d2c6d4fcd7a832cacfa13d79d0b51ec20 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 4 Jul 2026 20:49:46 +0000 Subject: [PATCH 04/15] test: reuse AgentEnvLock in plugin_skill_contract_test Migrate the two call sites from the local install_env_lock() + pinned_profile_storage() pair to crate::common::AgentEnvLock::pin (the canonical bundle this PR introduced), and delete the now-duplicate local helpers and their now-unused imports. Same lock+pin behavior. Co-Authored-By: Claude Fable 5 --- .../agent_suite/plugin_skill_contract_test.rs | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/tests/agent_suite/plugin_skill_contract_test.rs b/tests/agent_suite/plugin_skill_contract_test.rs index 33ae4de2..cf811799 100644 --- a/tests/agent_suite/plugin_skill_contract_test.rs +++ b/tests/agent_suite/plugin_skill_contract_test.rs @@ -5,14 +5,12 @@ use std::path::Path; -use crate::common::{EnvVarGuard, PROCESS_ENV_LOCK}; use crate::plugin_validation_support::{ is_kebab_case_skill_name, load_skill_docs, load_skill_docs_from, relative_files_under, repo_path, SkillDoc, }; use tempfile::TempDir; use tracedecay::agents::{expected_tool_perms, get_integration, InstallContext}; -use tracedecay::config::USER_DATA_DIR_ENV; const CODEX_SKILL_ROOT: &str = "plugin/skills"; const MAX_BUNDLED_SKILL_METADATA_CHARS: usize = 6_000; @@ -45,9 +43,8 @@ fn codex_plugin_skills_match_codex_skill_creator_quick_validate_rules() { #[test] fn generated_codex_plugin_skills_are_byte_copies_of_the_source_bundle() { - let _env_lock = install_env_lock(); let home = TempDir::new().expect("temp home"); - let _data_dir_guard = pinned_profile_storage(home.path()); + let _agent_env = crate::common::AgentEnvLock::pin(home.path()); let codex = get_integration("codex").expect("codex integration"); codex .install(&install_ctx(home.path())) @@ -82,9 +79,8 @@ fn cursor_plugin_skills_match_cursor_skill_contract() { #[test] fn generated_cursor_plugin_skills_are_byte_copies_of_the_source_bundle() { - let _env_lock = install_env_lock(); let home = TempDir::new().expect("temp home"); - let _data_dir_guard = pinned_profile_storage(home.path()); + let _agent_env = crate::common::AgentEnvLock::pin(home.path()); let cursor = get_integration("cursor").expect("cursor integration"); cursor .install(&install_ctx(home.path())) @@ -118,17 +114,6 @@ fn produced_plugin_skills_meet_the_metadata_budget_and_openai_contract() { } } -fn install_env_lock() -> tokio::sync::MutexGuard<'static, ()> { - PROCESS_ENV_LOCK.blocking_lock() -} - -/// Pins TraceDecay profile storage to the temp home so an ambient -/// `TRACEDECAY_DATA_DIR` with active managed skills cannot leak an -/// `agent-managed` overlay into the generated bundle. -fn pinned_profile_storage(home: &Path) -> EnvVarGuard { - EnvVarGuard::set(USER_DATA_DIR_ENV, home.join(".tracedecay")) -} - fn install_ctx(home: &Path) -> InstallContext { InstallContext { home: home.to_path_buf(), From e7f82879b599892d71482ef8f969c0e2ba5bc5a2 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 5 Jul 2026 01:57:55 +0000 Subject: [PATCH 05/15] test: simplify install-test env isolation helpers Extract lock_recovering_poison/lock_global_db_env for poison-tolerant GLOBAL_DB_ENV_LOCK acquires, migrate Hermes update-plugin tests to AgentEnvLock, and drop the one-off hermes_env_guard helper. --- tests/agent_suite/update_plugin_test.rs | 17 +++++------- tests/common/mod.rs | 10 +++++++ tests/hooks_lsp_suite/hooks_test.rs | 36 +++++++------------------ tests/storage_suite/corruption_test.rs | 4 +-- 4 files changed, 28 insertions(+), 39 deletions(-) diff --git a/tests/agent_suite/update_plugin_test.rs b/tests/agent_suite/update_plugin_test.rs index 65f45bca..edfaf52c 100644 --- a/tests/agent_suite/update_plugin_test.rs +++ b/tests/agent_suite/update_plugin_test.rs @@ -14,18 +14,12 @@ use serde_json::json; use tempfile::TempDir; use tracedecay::agents::{get_integration, InstallContext, UpdatePluginOutcome}; -use crate::common::{EnvVarGuard, PROCESS_ENV_LOCK}; +use crate::common::{AgentEnvLock, EnvVarGuard}; use crate::plugin_validation_support::{assert_schema_valid, compile_schema, relative_files_under}; const OLD_BIN: &str = "/old/bin/tracedecay"; const NEW_BIN: &str = "/new/bin/tracedecay"; -fn hermes_env_guard() -> (tokio::sync::MutexGuard<'static, ()>, EnvVarGuard) { - let lock = PROCESS_ENV_LOCK.blocking_lock(); - let guard = EnvVarGuard::unset("HERMES_HOME"); - (lock, guard) -} - fn ctx(home: &Path, tracedecay_bin: &str) -> InstallContext { InstallContext { home: home.to_path_buf(), @@ -160,8 +154,9 @@ fn write_retired_codex_skill(plugin_dir: &Path, name: &str) { #[test] fn hermes_update_plugin_refreshes_all_profiles_without_touching_config() { - let (_env_lock, _hermes_home) = hermes_env_guard(); let home = TempDir::new().unwrap(); + let _agent_env = AgentEnvLock::pin(home.path()); + let _hermes_home = EnvVarGuard::unset("HERMES_HOME"); let project = TempDir::new().unwrap(); let hermes = get_integration("hermes").unwrap(); @@ -227,8 +222,9 @@ fn hermes_update_plugin_refreshes_all_profiles_without_touching_config() { #[test] fn hermes_update_plugin_succeeds_where_a_config_rewrite_would_refuse() { - let (_env_lock, _hermes_home) = hermes_env_guard(); let home = TempDir::new().unwrap(); + let _agent_env = AgentEnvLock::pin(home.path()); + let _hermes_home = EnvVarGuard::unset("HERMES_HOME"); let hermes = get_integration("hermes").unwrap(); hermes.install(&ctx(home.path(), OLD_BIN)).unwrap(); @@ -252,8 +248,9 @@ fn hermes_update_plugin_succeeds_where_a_config_rewrite_would_refuse() { #[test] fn hermes_update_plugin_reports_not_installed_when_nothing_is_detected() { - let (_env_lock, _hermes_home) = hermes_env_guard(); let home = TempDir::new().unwrap(); + let _agent_env = AgentEnvLock::pin(home.path()); + let _hermes_home = EnvVarGuard::unset("HERMES_HOME"); // A Hermes home without a generated plugin must not be installed into. std::fs::create_dir_all(home.path().join(".hermes")).unwrap(); let hermes = get_integration("hermes").unwrap(); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 5918005d..8d3a9de5 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -67,6 +67,16 @@ pub const GLOBAL_DB_ENV: &str = "TRACEDECAY_GLOBAL_DB"; /// a test needs finer-grained control over which env vars it swaps. pub static GLOBAL_DB_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); +/// Acquires `mutex`, recovering the guard even when a prior holder panicked. +pub fn lock_recovering_poison(mutex: &std::sync::Mutex) -> std::sync::MutexGuard<'_, T> { + mutex.lock().unwrap_or_else(|err| err.into_inner()) +} + +/// Serializes tests that pin [`GLOBAL_DB_ENV`], tolerating a poisoned lock. +pub fn lock_global_db_env() -> std::sync::MutexGuard<'static, ()> { + lock_recovering_poison(&GLOBAL_DB_ENV_LOCK) +} + /// Serializes [`IsolatedEnv`] users within one test binary: storage isolation /// swaps process-wide env vars (`HOME`, `TRACEDECAY_DATA_DIR`, ...), so tests /// must not overlap. diff --git a/tests/hooks_lsp_suite/hooks_test.rs b/tests/hooks_lsp_suite/hooks_test.rs index 9da798bc..f7baa5cb 100644 --- a/tests/hooks_lsp_suite/hooks_test.rs +++ b/tests/hooks_lsp_suite/hooks_test.rs @@ -1,4 +1,4 @@ -use crate::common::{EnvVarGuard, GLOBAL_DB_ENV, GLOBAL_DB_ENV_LOCK}; +use crate::common::{lock_global_db_env, lock_recovering_poison, EnvVarGuard, GLOBAL_DB_ENV}; use std::path::Path; use tracedecay::config::USER_DATA_DIR_ENV; use tracedecay::hooks::{ @@ -392,9 +392,7 @@ fn test_cursor_post_tool_use_hints_for_single_file_read() { #[test] fn test_cursor_post_tool_use_dedupes_hints_per_session() { let dir = tempfile::tempdir().unwrap(); - let _env_lock = GLOBAL_DB_ENV_LOCK - .lock() - .unwrap_or_else(|err| err.into_inner()); + let _env_lock = lock_global_db_env(); let project_root = dir.path().canonicalize().unwrap(); let profile_root = project_root.join("profile"); let _env_guards = hook_profile_env(&project_root, &profile_root); @@ -451,9 +449,7 @@ fn test_cursor_post_tool_use_dedupes_hints_per_session() { #[test] fn test_cursor_post_tool_use_records_hint_analytics_for_emitted_duplicate_and_missing_session() { let dir = tempfile::tempdir().unwrap(); - let _env_lock = GLOBAL_DB_ENV_LOCK - .lock() - .unwrap_or_else(|err| err.into_inner()); + let _env_lock = lock_global_db_env(); let project_root = dir.path().canonicalize().unwrap(); let profile_root = project_root.join("profile"); let _env_guards = hook_profile_env(&project_root, &profile_root); @@ -539,9 +535,7 @@ fn test_cursor_post_tool_use_decision_silent_without_index() { #[test] fn test_cursor_post_tool_use_records_uninitialized_suppression() { let dir = tempfile::tempdir().unwrap(); - let _env_lock = GLOBAL_DB_ENV_LOCK - .lock() - .unwrap_or_else(|err| err.into_inner()); + let _env_lock = lock_global_db_env(); let project_root = dir.path().canonicalize().unwrap(); let profile_root = project_root.join("profile"); let _env_guards = hook_profile_env(&project_root, &profile_root); @@ -960,9 +954,7 @@ async fn test_codex_user_prompt_submit_generic_workspace_suppresses_code_hints() // hook context generation resolves profile storage and records analytics. #[allow(clippy::await_holding_lock)] async fn test_codex_user_prompt_submit_records_workspace_status_and_missing_session_hint() { - let _lock = GLOBAL_DB_ENV_LOCK - .lock() - .unwrap_or_else(|err| err.into_inner()); + let _lock = lock_global_db_env(); let project = tempfile::tempdir().unwrap(); let generic = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); @@ -1048,9 +1040,7 @@ fn test_codex_workspace_status_distinguishes_generic_and_project_like_dirs() { #[test] fn test_codex_workspace_status_detects_initialized_trace_decay_project() { - let _lock = GLOBAL_DB_ENV_LOCK - .lock() - .unwrap_or_else(|err| err.into_inner()); + let _lock = lock_global_db_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -1356,9 +1346,7 @@ fn test_codex_subagent_start_injects_context_for_new_no_history_agent() { #[test] fn test_codex_subagent_start_dedupes_context_per_session() { - let _lock = GLOBAL_DB_ENV_LOCK - .lock() - .unwrap_or_else(|err| err.into_inner()); + let _lock = lock_global_db_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -1386,9 +1374,7 @@ fn test_codex_subagent_start_dedupes_context_per_session() { #[test] fn test_codex_subagent_start_no_history_does_not_suppress_later_research_context() { - let _lock = GLOBAL_DB_ENV_LOCK - .lock() - .unwrap_or_else(|err| err.into_inner()); + let _lock = lock_global_db_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -1430,9 +1416,7 @@ fn test_codex_subagent_start_no_history_does_not_suppress_later_research_context #[test] fn test_codex_subagent_start_counts_and_formats_log_line() { - let _lock = GLOBAL_DB_ENV_LOCK - .lock() - .unwrap_or_else(|err| err.into_inner()); + let _lock = lock_global_db_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -1560,5 +1544,5 @@ fn poisoned_env_lock_is_recovered_by_tolerant_acquire() { "poisoned lock must surface Err to a plain lock()/unwrap() caller" ); - let _recovered = LOCK.lock().unwrap_or_else(|err| err.into_inner()); + let _recovered = lock_recovering_poison(&LOCK); } diff --git a/tests/storage_suite/corruption_test.rs b/tests/storage_suite/corruption_test.rs index d429d4cc..0a268c47 100644 --- a/tests/storage_suite/corruption_test.rs +++ b/tests/storage_suite/corruption_test.rs @@ -206,9 +206,7 @@ async fn bulk_load_preserves_platform_synchronous_mode() { // the *durable* platform synchronous mode, which // TRACEDECAY_SQLITE_UNSAFE_FAST=1 (exported for the whole Windows CI test // run) would relax to OFF. - let _env_lock = common::GLOBAL_DB_ENV_LOCK - .lock() - .unwrap_or_else(|err| err.into_inner()); + let _env_lock = common::lock_global_db_env(); let _unsafe_fast_off = common::EnvVarGuard::unset(tracedecay::db::SQLITE_UNSAFE_FAST_ENV); let (db, _dir, _path) = setup_db().await; From 5218e4da922231e9d8661f186a4749a3f3ad06c3 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 5 Jul 2026 02:04:13 +0000 Subject: [PATCH 06/15] test: use imported AgentEnvLock consistently --- tests/agent_suite/update_plugin_test.rs | 42 ++++++++++++------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/agent_suite/update_plugin_test.rs b/tests/agent_suite/update_plugin_test.rs index edfaf52c..2485c848 100644 --- a/tests/agent_suite/update_plugin_test.rs +++ b/tests/agent_suite/update_plugin_test.rs @@ -267,7 +267,7 @@ fn hermes_update_plugin_reports_not_installed_when_nothing_is_detected() { #[test] fn cursor_update_plugin_refreshes_bundle_and_preserves_user_config() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let cursor = get_integration("cursor").unwrap(); // User-owned Cursor config that update-plugin must never write. @@ -334,7 +334,7 @@ fn cursor_update_plugin_refreshes_bundle_and_preserves_user_config() { #[test] fn cursor_update_plugin_reports_not_installed_without_a_bundle() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); std::fs::create_dir_all(home.path().join(".cursor")).unwrap(); let cursor = get_integration("cursor").unwrap(); let outcome = cursor.update_plugin(&ctx(home.path(), NEW_BIN)).unwrap(); @@ -345,7 +345,7 @@ fn cursor_update_plugin_reports_not_installed_without_a_bundle() { #[test] fn claude_update_plugin_refreshes_bundle_and_preserves_user_config() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let claude = get_integration("claude").unwrap(); // User-owned Claude config that update-plugin must never destroy. @@ -452,7 +452,7 @@ fn claude_update_plugin_writes_plugin_permissions_and_refreshes_claude_md() { #[test] fn claude_update_plugin_reports_not_installed_without_a_bundle() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); std::fs::create_dir_all(home.path().join(".claude")).unwrap(); let claude = get_integration("claude").unwrap(); let outcome = claude.update_plugin(&ctx(home.path(), NEW_BIN)).unwrap(); @@ -470,7 +470,7 @@ fn claude_update_plugin_reports_not_installed_without_a_bundle() { #[test] fn codex_update_plugin_refreshes_bundle_without_touching_config() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let project_root = home.path().join("workspace"); let codex = get_integration("codex").unwrap(); codex.install(&ctx(home.path(), OLD_BIN)).unwrap(); @@ -524,7 +524,7 @@ fn codex_update_plugin_refreshes_bundle_without_touching_config() { #[test] fn codex_update_plugin_refreshes_cache_and_keeps_bootstrap_source_listable() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let project_root = home.path().join("workspace"); let stale_plugin_dir = home .path() @@ -570,7 +570,7 @@ fn codex_update_plugin_refreshes_cache_and_keeps_bootstrap_source_listable() { #[test] fn codex_update_plugin_recreates_bootstrap_source_from_cache_only_state() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let project_root = home.path().join("workspace"); let cached_plugin_dir = codex_cached_plugin_dir(home.path()); let bootstrap_dir = codex_bootstrap_dir(home.path()); @@ -605,7 +605,7 @@ fn codex_update_plugin_recreates_bootstrap_source_from_cache_only_state() { #[test] fn codex_update_plugin_sweeps_legacy_config_when_cache_exists() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let project_root = home.path().join("workspace"); let cached_plugin_dir = codex_cached_plugin_dir(home.path()); let legacy_config = write_codex_legacy_config(home.path()); @@ -631,7 +631,7 @@ fn codex_update_plugin_sweeps_legacy_config_when_cache_exists() { #[test] fn codex_update_plugin_refreshes_global_cache_and_repo_local_bundle() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let project = TempDir::new().unwrap(); let cached_plugin_dir = codex_cached_plugin_dir(home.path()); write_codex_plugin_manifest(&cached_plugin_dir, "0.0.0"); @@ -669,7 +669,7 @@ fn codex_update_plugin_refreshes_global_cache_and_repo_local_bundle() { #[test] fn codex_update_plugin_repairs_personal_marketplace_for_bootstrap_bundle() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let project_root = home.path().join("workspace"); let plugin_dir = codex_bootstrap_dir(home.path()); write_codex_plugin_manifest(&plugin_dir, "0.0.0"); @@ -690,7 +690,7 @@ fn codex_update_plugin_repairs_personal_marketplace_for_bootstrap_bundle() { #[test] fn codex_update_plugin_refreshes_repo_local_bundle_from_project_root() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let project = TempDir::new().unwrap(); let codex = get_integration("codex").unwrap(); codex @@ -715,7 +715,7 @@ fn codex_update_plugin_refreshes_repo_local_bundle_from_project_root() { #[test] fn codex_uninstall_removes_repo_local_bundle_from_project_root() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let project = TempDir::new().unwrap(); let codex = get_integration("codex").unwrap(); let install_ctx = ctx_with_project(home.path(), OLD_BIN, project.path()); @@ -734,7 +734,7 @@ fn codex_uninstall_removes_repo_local_bundle_from_project_root() { #[test] fn codex_update_plugin_migrates_legacy_config_only_install_to_plugin() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let project_root = home.path().join("workspace"); let legacy_config = write_codex_legacy_config(home.path()); let codex = get_integration("codex").unwrap(); @@ -760,7 +760,7 @@ fn codex_update_plugin_migrates_legacy_config_only_install_to_plugin() { #[test] fn codex_update_plugin_migrates_legacy_config_even_when_repo_bundle_refreshes() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let project = TempDir::new().unwrap(); let codex = get_integration("codex").unwrap(); // A repo-local bundle exists (so the refresh list is non-empty) alongside @@ -799,7 +799,7 @@ fn codex_update_plugin_migrates_legacy_config_even_when_repo_bundle_refreshes() #[test] fn codex_update_plugin_reports_not_installed_without_bundle_or_legacy_config() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let project_root = home.path().join("workspace"); std::fs::create_dir_all(home.path().join(".codex")).unwrap(); let codex = get_integration("codex").unwrap(); @@ -817,7 +817,7 @@ fn codex_update_plugin_reports_not_installed_without_bundle_or_legacy_config() { #[test] fn kiro_update_plugin_rebakes_managed_agent_and_preserves_configs() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let kiro = get_integration("kiro").unwrap(); kiro.install(&ctx(home.path(), OLD_BIN)).unwrap(); @@ -853,7 +853,7 @@ fn kiro_update_plugin_rebakes_managed_agent_and_preserves_configs() { #[test] fn kiro_update_plugin_leaves_user_managed_agent_files_alone() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let kiro = get_integration("kiro").unwrap(); let agent_file = home.path().join(".kiro/agents/tracedecay.json"); @@ -1230,7 +1230,7 @@ fn assert_codex_rendered_bundle_valid(plugin_dir: &Path, bin: &str, scope: Codex #[test] fn cursor_install_renders_structurally_valid_bundle() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let cursor = get_integration("cursor").unwrap(); cursor.install(&ctx(home.path(), NEW_BIN)).unwrap(); assert_cursor_rendered_bundle_valid( @@ -1242,7 +1242,7 @@ fn cursor_install_renders_structurally_valid_bundle() { #[test] fn cursor_update_plugin_rerenders_structurally_valid_bundle() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let cursor = get_integration("cursor").unwrap(); cursor.install(&ctx(home.path(), OLD_BIN)).unwrap(); @@ -1257,7 +1257,7 @@ fn cursor_update_plugin_rerenders_structurally_valid_bundle() { #[test] fn codex_install_renders_structurally_valid_bundle() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let codex = get_integration("codex").unwrap(); codex.install(&ctx(home.path(), NEW_BIN)).unwrap(); @@ -1277,7 +1277,7 @@ fn codex_install_renders_structurally_valid_bundle() { #[test] fn codex_local_install_renders_project_scoped_mcp() { let home = TempDir::new().unwrap(); - let _agent_env = crate::common::AgentEnvLock::pin(&home); + let _agent_env = AgentEnvLock::pin(&home); let project = TempDir::new().unwrap(); let codex = get_integration("codex").unwrap(); codex From 514a935b865b6d2ff1f25d01a011c42822efc666 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 5 Jul 2026 02:20:58 +0000 Subject: [PATCH 07/15] test: serialize lib tests sharing global handle store state Parallel lib unit tests that store MCP response handles under the process-global TRACEDECAY_DATA_DIR profile tree raced on profile-sharded response-handles/ directories, causing intermittent store failures. --- src/mcp/response_handles.rs | 17 +++++++++++++++++ src/mcp/tools/handlers/session.rs | 3 +++ src/mcp/tools/render.rs | 8 +++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/mcp/response_handles.rs b/src/mcp/response_handles.rs index 2c3f1b80..419289d7 100644 --- a/src/mcp/response_handles.rs +++ b/src/mcp/response_handles.rs @@ -659,3 +659,20 @@ fn clipped_handle_for_log(handle: &str) -> String { clipped } } + +/// Serializes lib unit tests that store response handles under the +/// process-global profile root (`TRACEDECAY_DATA_DIR`). Parallel tests +/// otherwise race on profile-sharded `response-handles/` directories. +#[cfg(test)] +pub(crate) static RESPONSE_HANDLE_STORE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + +/// Acquires [`RESPONSE_HANDLE_STORE_LOCK`], recovering even when poisoned. +#[cfg(test)] +pub(crate) fn lock_response_handle_store() -> std::sync::MutexGuard<'static, ()> { + lock_recovering_poison(&RESPONSE_HANDLE_STORE_LOCK) +} + +#[cfg(test)] +fn lock_recovering_poison(mutex: &std::sync::Mutex) -> std::sync::MutexGuard<'_, T> { + mutex.lock().unwrap_or_else(|err| err.into_inner()) +} diff --git a/src/mcp/tools/handlers/session.rs b/src/mcp/tools/handlers/session.rs index d57dbae1..21e53a3d 100644 --- a/src/mcp/tools/handlers/session.rs +++ b/src/mcp/tools/handlers/session.rs @@ -2610,6 +2610,7 @@ pub(super) async fn handle_lcm_compress( #[allow(clippy::unwrap_used)] mod tests { use super::*; + use crate::mcp::response_handles::lock_response_handle_store; fn sample_message_search_payload() -> Value { json!({ @@ -2720,6 +2721,7 @@ mod tests { #[test] fn lcm_preflight_markdown_truncation_stores_retrieval_handle() { + let _store_guard = lock_response_handle_store(); // Regression: the markdown-default preflight path must thread the // project root so an oversized payload truncates *with* a recoverable // handle rather than an irreversible clip. @@ -2801,6 +2803,7 @@ mod tests { #[test] fn lcm_expand_query_needs_synthesis_floor_is_bounded_valid_json() { + let _store_guard = lock_response_handle_store(); // Regression (S3): a needs_synthesis payload that is still over budget // after Minimal compaction must NOT be emitted unbounded. The floor // must stay within MAX_RESPONSE_CHARS, remain valid JSON, and keep the diff --git a/src/mcp/tools/render.rs b/src/mcp/tools/render.rs index 8afcc3fb..dcd8419a 100644 --- a/src/mcp/tools/render.rs +++ b/src/mcp/tools/render.rs @@ -518,7 +518,9 @@ fn render_object(md: &mut Md, map: &serde_json::Map, depth: u8) { #[allow(clippy::unwrap_used)] mod tests { use super::*; - use crate::mcp::response_handles::{retrieve_response_handle_from_root, ResponseHandleLookup}; + use crate::mcp::response_handles::{ + lock_response_handle_store, retrieve_response_handle_from_root, ResponseHandleLookup, + }; use crate::tracedecay::current_timestamp; use serde_json::json; @@ -577,6 +579,7 @@ mod tests { #[test] fn truncated_json_envelope_includes_handle() { + let _store_guard = lock_response_handle_store(); let dir = tempfile::TempDir::new().unwrap(); let long = format!( "{{\"items\":[{}]}}", @@ -612,6 +615,7 @@ mod tests { #[test] fn truncated_markdown_includes_readable_handle_guidance() { + let _store_guard = lock_response_handle_store(); let dir = tempfile::TempDir::new().unwrap(); let long = format!("# Scan\n\n{}", "- repeated finding\n".repeat(3_000)); @@ -657,6 +661,7 @@ mod tests { #[test] fn truncate_text_with_handle_stores_reversible_envelope() { + let _store_guard = lock_response_handle_store(); let dir = tempfile::TempDir::new().unwrap(); let long = "- indexed file entry\n".repeat(3_000); @@ -678,6 +683,7 @@ mod tests { #[test] fn truncated_json_envelope_reports_store_failure() { + let _store_guard = lock_response_handle_store(); let dir = tempfile::TempDir::new().unwrap(); std::fs::create_dir_all(dir.path().join(".tracedecay")).unwrap(); std::fs::write( From 3d6a75aed7e125129c7f86444e2412b47e02255f Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 5 Jul 2026 02:48:52 +0000 Subject: [PATCH 08/15] test: unify USER_DATA_DIR env locks across lib tests Parallel lib tests used separate mutexes for TRACEDECAY_DATA_DIR pins, letting hook analytics and profile resolution tests race and flake. --- src/config.rs | 14 ++++++++++++++ src/config/tests.rs | 10 ++++------ src/daemon/service.rs | 22 ++++++++++------------ src/hooks/codex.rs | 2 +- src/hooks/cursor.rs | 2 +- src/hooks/mod.rs | 15 ++++++--------- src/mcp/tools/handlers/mod.rs | 15 ++++++--------- src/user_config.rs | 8 +++----- 8 files changed, 45 insertions(+), 43 deletions(-) diff --git a/src/config.rs b/src/config.rs index 79b4670c..5545d48f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -810,6 +810,20 @@ pub fn is_excluded(file_path: &str, config: &TraceDecayConfig) -> bool { false } +/// Serializes lib unit tests that mutate process-wide storage env vars +/// (`TRACEDECAY_DATA_DIR` and related HOME/profile pins). Parallel tests +/// otherwise race on profile resolution and hook analytics paths. +#[cfg(test)] +pub static USER_DATA_DIR_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + +/// Acquires [`USER_DATA_DIR_TEST_LOCK`], recovering even when poisoned. +#[cfg(test)] +pub fn lock_user_data_dir_test_env() -> std::sync::MutexGuard<'static, ()> { + USER_DATA_DIR_TEST_LOCK + .lock() + .unwrap_or_else(|err| err.into_inner()) +} + #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests; diff --git a/src/config/tests.rs b/src/config/tests.rs index 44d79408..ba04bcc9 100644 --- a/src/config/tests.rs +++ b/src/config/tests.rs @@ -1,15 +1,13 @@ use super::{ db_filename, get_project_db_path, get_tracedecay_dir, is_excluded, is_excluded_dir, - is_ignored_by_explicit_global_excludes, is_ignored_by_git, is_included, user_data_dir, - TraceDecayConfig, USER_DATA_DIR_ENV, + is_ignored_by_explicit_global_excludes, is_ignored_by_git, is_included, + lock_user_data_dir_test_env, user_data_dir, TraceDecayConfig, USER_DATA_DIR_ENV, }; use std::ffi::OsString; use std::fs; use std::process::Command; use tempfile::TempDir; -static USER_DATA_DIR_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); - struct EnvRestore { key: &'static str, previous: Option, @@ -58,7 +56,7 @@ fn test_data_dir_uses_tracedecay_when_present() { #[cfg(unix)] #[test] fn user_data_dir_canonicalizes_symlinked_existing_parent() { - let _lock = USER_DATA_DIR_ENV_LOCK.lock().unwrap(); + let _lock = lock_user_data_dir_test_env(); let root = TempDir::new().unwrap(); let real_home = root.path().join("real-home"); let linked_home = root.path().join("linked-home"); @@ -294,7 +292,7 @@ fn partial_sync_table_fills_missing_fields_with_defaults() { #[test] fn sync_config_env_overrides_bool_and_int() { - let _lock = USER_DATA_DIR_ENV_LOCK.lock().unwrap(); + let _lock = lock_user_data_dir_test_env(); let _watch = EnvRestore::set("TRACEDECAY_SYNC_AUTO_WATCH", "false"); let _debounce = EnvRestore::set("TRACEDECAY_SYNC_WATCH_DEBOUNCE_MS", "5000"); // Unparsable ints/bools are ignored (field keeps its base value). diff --git a/src/daemon/service.rs b/src/daemon/service.rs index 1cfba1f3..3a939908 100644 --- a/src/daemon/service.rs +++ b/src/daemon/service.rs @@ -869,7 +869,6 @@ fn launchctl_stderr_is_not_loaded(stderr: &str) -> bool { mod tests { use std::ffi::{OsStr, OsString}; use std::path::PathBuf; - use std::sync::Mutex; #[cfg(target_os = "linux")] use std::os::unix::fs::PermissionsExt; @@ -877,8 +876,7 @@ mod tests { use tempfile::TempDir; use super::{DaemonServiceSpec, LaunchctlFailureMode, LaunchdCommand}; - - static ENV_LOCK: Mutex<()> = Mutex::new(()); + use crate::config::lock_user_data_dir_test_env; struct EnvVarGuard { key: &'static str, @@ -938,7 +936,7 @@ mod tests { #[cfg(target_os = "macos")] #[test] fn service_status_includes_launchd_debug_commands() { - let _env_lock = ENV_LOCK.lock().expect("env lock"); + let _env_lock = lock_user_data_dir_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let _data_dir_guard = EnvVarGuard::set(crate::config::USER_DATA_DIR_ENV, profile.path()); @@ -1022,7 +1020,7 @@ mod tests { #[cfg(unix)] #[test] fn render_launchd_plist_includes_program_arguments_socket_logs_and_label() { - let _env_lock = ENV_LOCK.lock().expect("env lock"); + let _env_lock = lock_user_data_dir_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let home = tempfile::TempDir::new().expect("home temp dir"); let _home_guard = EnvVarGuard::set("HOME", home.path()); @@ -1061,7 +1059,7 @@ mod tests { #[cfg(unix)] #[test] fn render_launchd_plist_escapes_xml_and_parser_unescapes_socket_path() { - let _env_lock = ENV_LOCK.lock().expect("env lock"); + let _env_lock = lock_user_data_dir_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let home = tempfile::TempDir::new().expect("home temp dir"); let _home_guard = EnvVarGuard::set("HOME", home.path()); @@ -1117,7 +1115,7 @@ mod tests { #[cfg(unix)] #[test] fn launchd_plist_env_value_round_trips_data_dir_override() { - let _env_lock = ENV_LOCK.lock().expect("env lock"); + let _env_lock = lock_user_data_dir_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let home = tempfile::TempDir::new().expect("home temp dir"); let _home_guard = EnvVarGuard::set("HOME", home.path()); @@ -1139,7 +1137,7 @@ mod tests { #[cfg(unix)] #[test] fn launchd_plist_env_value_ignores_plist_without_override() { - let _env_lock = ENV_LOCK.lock().expect("env lock"); + let _env_lock = lock_user_data_dir_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let home = tempfile::TempDir::new().expect("home temp dir"); let _home_guard = EnvVarGuard::set("HOME", home.path()); @@ -1227,7 +1225,7 @@ mod tests { #[cfg(target_os = "linux")] #[test] fn refresh_service_rewrites_unit_and_restarts_daemon() { - let _env_lock = ENV_LOCK.lock().expect("env lock"); + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().expect("temp dir"); let config_home = dir.path().join("config"); let fake_bin = dir.path().join("bin"); @@ -1276,7 +1274,7 @@ mod tests { #[cfg(target_os = "linux")] #[test] fn refresh_installed_service_skips_missing_unit() { - let _env_lock = ENV_LOCK.lock().expect("env lock"); + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().expect("temp dir"); let config_home = dir.path().join("config"); let fake_bin = dir.path().join("bin"); @@ -1305,7 +1303,7 @@ mod tests { #[cfg(target_os = "linux")] #[test] fn refresh_installed_service_preserves_existing_socket_path() { - let _env_lock = ENV_LOCK.lock().expect("env lock"); + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().expect("temp dir"); let config_home = dir.path().join("config"); let fake_bin = dir.path().join("bin"); @@ -1365,7 +1363,7 @@ mod tests { #[test] fn default_socket_path_is_profile_scoped_not_project_scoped() { - let _env_lock = ENV_LOCK.lock().expect("env lock"); + let _env_lock = lock_user_data_dir_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let project_a = tempfile::TempDir::new().expect("project a temp dir"); let project_b = tempfile::TempDir::new().expect("project b temp dir"); diff --git a/src/hooks/codex.rs b/src/hooks/codex.rs index 249a812d..0a711d16 100644 --- a/src/hooks/codex.rs +++ b/src/hooks/codex.rs @@ -644,7 +644,7 @@ mod tests { #[test] fn codex_prompt_hints_dedupe_by_session_and_category() { - let _lock = crate::hooks::test_env_lock().lock().unwrap(); + let _lock = crate::hooks::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); diff --git a/src/hooks/cursor.rs b/src/hooks/cursor.rs index e15b48ca..a151b5db 100644 --- a/src/hooks/cursor.rs +++ b/src/hooks/cursor.rs @@ -788,7 +788,7 @@ mod tests { /// so a prompt-shaped trigger steers identically on both agents. #[test] fn cursor_prompt_hint_runs_decide_hint_and_dedupes_per_session() { - let _lock = crate::hooks::test_env_lock().lock().unwrap(); + let _lock = crate::hooks::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs index 35662976..1f510556 100644 --- a/src/hooks/mod.rs +++ b/src/hooks/mod.rs @@ -6,8 +6,6 @@ use std::io::Read; use std::path::{Component, Path, PathBuf}; use std::sync::atomic::{AtomicU64, Ordering}; -#[cfg(test)] -use std::sync::{Mutex, OnceLock}; use std::time::{SystemTime, UNIX_EPOCH}; use serde_json::Value; @@ -157,9 +155,8 @@ fn now_unix_secs() -> i64 { } #[cfg(test)] -pub(crate) fn test_env_lock() -> &'static Mutex<()> { - static ENV_LOCK: OnceLock> = OnceLock::new(); - ENV_LOCK.get_or_init(|| Mutex::new(())) +pub(crate) fn lock_test_env() -> std::sync::MutexGuard<'static, ()> { + crate::config::lock_user_data_dir_test_env() } /// Mints a unique id for one hint candidate so its `hint_candidate` row and its @@ -661,7 +658,7 @@ mod hint_analytics_tests { #[test] fn record_hint_emitted_missing_session_is_single_terminal() { - let _lock = super::test_env_lock().lock().unwrap(); + let _lock = super::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -689,7 +686,7 @@ mod hint_analytics_tests { /// known. #[test] fn every_hint_branch_yields_exactly_one_terminal_with_hint_id() { - let _lock = super::test_env_lock().lock().unwrap(); + let _lock = super::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -784,7 +781,7 @@ mod hint_analytics_tests { /// terminal, and no hint is returned to the caller. #[test] fn budget_exhaustion_records_suppressed_budget_terminal() { - let _lock = super::test_env_lock().lock().unwrap(); + let _lock = super::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -848,7 +845,7 @@ mod hint_analytics_tests { /// stronger re-hint recorded as `hint_escalated`, with the escalation prefix. #[test] fn repeated_usage_records_hint_escalated_terminal() { - let _lock = super::test_env_lock().lock().unwrap(); + let _lock = super::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); diff --git a/src/mcp/tools/handlers/mod.rs b/src/mcp/tools/handlers/mod.rs index e1e5ab17..0f95e069 100644 --- a/src/mcp/tools/handlers/mod.rs +++ b/src/mcp/tools/handlers/mod.rs @@ -528,13 +528,10 @@ mod tests { use serde_json::json; use tempfile::TempDir; - use tokio::sync::Mutex; use super::super::get_tool_definitions; use super::*; - use crate::config::USER_DATA_DIR_ENV; - - static SELECTOR_ENV_LOCK: Mutex<()> = Mutex::const_new(()); + use crate::config::{lock_user_data_dir_test_env, USER_DATA_DIR_ENV}; struct EnvVarGuard { key: &'static str, @@ -726,7 +723,7 @@ mod tests { #[tokio::test] async fn graph_reader_selector_dispatch_targets_registered_project() { - let _env_lock = SELECTOR_ENV_LOCK.lock().await; + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().unwrap(); let _env = SelectorEnv::new(dir.path()); let active_project = dir.path().join("active"); @@ -788,7 +785,7 @@ mod tests { #[tokio::test] async fn graph_reader_selector_dispatch_accepts_unique_project_basename() { - let _env_lock = SELECTOR_ENV_LOCK.lock().await; + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().unwrap(); let _env = SelectorEnv::new(dir.path()); let active_project = dir.path().join("active"); @@ -837,7 +834,7 @@ mod tests { #[tokio::test] async fn graph_reader_selector_rejects_ambiguous_project_basename() { - let _env_lock = SELECTOR_ENV_LOCK.lock().await; + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().unwrap(); let _env = SelectorEnv::new(dir.path()); let active_project = dir.path().join("active"); @@ -881,7 +878,7 @@ mod tests { #[tokio::test] async fn unsupported_selector_tool_rejects_explicit_project_selector() { - let _env_lock = SELECTOR_ENV_LOCK.lock().await; + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().unwrap(); let _env = SelectorEnv::new(dir.path()); let project = dir.path().join("active"); @@ -916,7 +913,7 @@ mod tests { const LARGE_RESPONSE_MARKER_COUNT: usize = 120; const LAST_LARGE_RESPONSE_MARKER: usize = LARGE_RESPONSE_MARKER_COUNT - 1; - let _env_lock = SELECTOR_ENV_LOCK.lock().await; + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().unwrap(); let _env = SelectorEnv::new(dir.path()); let active_project = dir.path().join("active"); diff --git a/src/user_config.rs b/src/user_config.rs index e5161556..2b57026c 100644 --- a/src/user_config.rs +++ b/src/user_config.rs @@ -251,13 +251,11 @@ pub fn parse_duration(s: &str) -> Option { )] mod tests { use super::*; - use crate::config::USER_DATA_DIR_ENV; + use crate::config::{lock_user_data_dir_test_env, USER_DATA_DIR_ENV}; use std::ffi::OsString; use std::time::Duration; use tempfile::TempDir; - static USER_DATA_DIR_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); - struct EnvRestore { key: &'static str, previous: Option, @@ -307,7 +305,7 @@ mod tests { #[test] fn save_preserves_existing_corrupt_config_file() { - let _lock = USER_DATA_DIR_ENV_LOCK.lock().unwrap(); + let _lock = lock_user_data_dir_test_env(); let temp = TempDir::new().unwrap(); let _env = EnvRestore::set(USER_DATA_DIR_ENV, temp.path()); let path = config_path().expect("config path should resolve"); @@ -327,7 +325,7 @@ mod tests { #[test] fn save_preserves_unknown_config_keys() { - let _lock = USER_DATA_DIR_ENV_LOCK.lock().unwrap(); + let _lock = lock_user_data_dir_test_env(); let temp = TempDir::new().unwrap(); let _env = EnvRestore::set(USER_DATA_DIR_ENV, temp.path()); let path = config_path().expect("config path should resolve"); From 562d5076fa618074575c423a39bd6ba752100ff4 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 5 Jul 2026 02:51:35 +0000 Subject: [PATCH 09/15] test: complete TRACEDECAY_DATA_DIR lock unification for lib tests Add lock_test_env alias, Claude install profile pinning, and route remaining config/daemon/handler env overrides through the shared lock with poison recovery. --- src/agents/claude/tests.rs | 42 +++++++++++++++++++++++++++++++++++ src/config.rs | 14 ------------ src/config/tests.rs | 9 ++++---- src/daemon/service.rs | 20 ++++++++--------- src/hooks/codex.rs | 2 +- src/hooks/cursor.rs | 2 +- src/hooks/mod.rs | 13 ++++------- src/mcp/response_handles.rs | 13 ++++++++--- src/mcp/tools/handlers/mod.rs | 13 ++++++----- src/user_config.rs | 7 +++--- 10 files changed, 84 insertions(+), 51 deletions(-) diff --git a/src/agents/claude/tests.rs b/src/agents/claude/tests.rs index 67cf4818..98f509f3 100644 --- a/src/agents/claude/tests.rs +++ b/src/agents/claude/tests.rs @@ -1,6 +1,45 @@ use super::*; +use crate::config::USER_DATA_DIR_ENV; +use crate::mcp::response_handles::lock_test_env; use serde_json::json; +struct InstallProfileEnv { + _lock: std::sync::MutexGuard<'static, ()>, + _profile: ProfileEnvGuard, +} + +struct ProfileEnvGuard { + key: &'static str, + previous: Option, +} + +impl ProfileEnvGuard { + fn set(key: &'static str, value: impl AsRef) -> Self { + let previous = std::env::var_os(key); + std::env::set_var(key, value.as_ref()); + Self { key, previous } + } +} + +impl Drop for ProfileEnvGuard { + fn drop(&mut self) { + if let Some(previous) = self.previous.take() { + std::env::set_var(self.key, previous); + } else { + std::env::remove_var(self.key); + } + } +} + +fn install_profile_env(home: &Path) -> InstallProfileEnv { + let profile = home.join(".tracedecay"); + std::fs::create_dir_all(&profile).expect("install test profile dir"); + InstallProfileEnv { + _lock: lock_test_env(), + _profile: ProfileEnvGuard::set(USER_DATA_DIR_ENV, profile), + } +} + fn plugin_subdir_names(rel: &str) -> Vec { let root = Path::new(env!("CARGO_MANIFEST_DIR")) .join("plugin") @@ -227,6 +266,7 @@ fn deploy_refuses_to_replace_non_tracedecay_dir() { #[test] fn install_is_idempotent() { let home = tempfile::tempdir().unwrap(); + let _profile_env = install_profile_env(home.path()); let ctx = install_ctx(home.path()); ClaudeIntegration.install(&ctx).unwrap(); @@ -280,6 +320,7 @@ fn register_marketplace_preserves_existing() { #[test] fn install_handles_malformed_settings_parents() { let home = tempfile::tempdir().unwrap(); + let _profile_env = install_profile_env(home.path()); let claude_dir = home.path().join(".claude"); std::fs::create_dir_all(&claude_dir).unwrap(); std::fs::write( @@ -476,6 +517,7 @@ fn uninstall_permissions_removes_tracedecay_entries() { #[test] fn uninstall_removes_plugin_and_marketplace() { let home = tempfile::tempdir().unwrap(); + let _profile_env = install_profile_env(home.path()); let ctx = install_ctx(home.path()); ClaudeIntegration.install(&ctx).unwrap(); assert!(plugin_marketplace_manifest_path(home.path()).exists()); diff --git a/src/config.rs b/src/config.rs index 5545d48f..79b4670c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -810,20 +810,6 @@ pub fn is_excluded(file_path: &str, config: &TraceDecayConfig) -> bool { false } -/// Serializes lib unit tests that mutate process-wide storage env vars -/// (`TRACEDECAY_DATA_DIR` and related HOME/profile pins). Parallel tests -/// otherwise race on profile resolution and hook analytics paths. -#[cfg(test)] -pub static USER_DATA_DIR_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); - -/// Acquires [`USER_DATA_DIR_TEST_LOCK`], recovering even when poisoned. -#[cfg(test)] -pub fn lock_user_data_dir_test_env() -> std::sync::MutexGuard<'static, ()> { - USER_DATA_DIR_TEST_LOCK - .lock() - .unwrap_or_else(|err| err.into_inner()) -} - #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests; diff --git a/src/config/tests.rs b/src/config/tests.rs index ba04bcc9..7a68901c 100644 --- a/src/config/tests.rs +++ b/src/config/tests.rs @@ -1,8 +1,9 @@ use super::{ db_filename, get_project_db_path, get_tracedecay_dir, is_excluded, is_excluded_dir, - is_ignored_by_explicit_global_excludes, is_ignored_by_git, is_included, - lock_user_data_dir_test_env, user_data_dir, TraceDecayConfig, USER_DATA_DIR_ENV, + is_ignored_by_explicit_global_excludes, is_ignored_by_git, is_included, user_data_dir, + TraceDecayConfig, USER_DATA_DIR_ENV, }; +use crate::mcp::response_handles::lock_test_env; use std::ffi::OsString; use std::fs; use std::process::Command; @@ -56,7 +57,7 @@ fn test_data_dir_uses_tracedecay_when_present() { #[cfg(unix)] #[test] fn user_data_dir_canonicalizes_symlinked_existing_parent() { - let _lock = lock_user_data_dir_test_env(); + let _lock = lock_test_env(); let root = TempDir::new().unwrap(); let real_home = root.path().join("real-home"); let linked_home = root.path().join("linked-home"); @@ -292,7 +293,7 @@ fn partial_sync_table_fills_missing_fields_with_defaults() { #[test] fn sync_config_env_overrides_bool_and_int() { - let _lock = lock_user_data_dir_test_env(); + let _lock = lock_test_env(); let _watch = EnvRestore::set("TRACEDECAY_SYNC_AUTO_WATCH", "false"); let _debounce = EnvRestore::set("TRACEDECAY_SYNC_WATCH_DEBOUNCE_MS", "5000"); // Unparsable ints/bools are ignored (field keeps its base value). diff --git a/src/daemon/service.rs b/src/daemon/service.rs index 3a939908..d67c6c6e 100644 --- a/src/daemon/service.rs +++ b/src/daemon/service.rs @@ -876,7 +876,7 @@ mod tests { use tempfile::TempDir; use super::{DaemonServiceSpec, LaunchctlFailureMode, LaunchdCommand}; - use crate::config::lock_user_data_dir_test_env; + use crate::mcp::response_handles::lock_test_env; struct EnvVarGuard { key: &'static str, @@ -936,7 +936,7 @@ mod tests { #[cfg(target_os = "macos")] #[test] fn service_status_includes_launchd_debug_commands() { - let _env_lock = lock_user_data_dir_test_env(); + let _env_lock = lock_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let _data_dir_guard = EnvVarGuard::set(crate::config::USER_DATA_DIR_ENV, profile.path()); @@ -1020,7 +1020,7 @@ mod tests { #[cfg(unix)] #[test] fn render_launchd_plist_includes_program_arguments_socket_logs_and_label() { - let _env_lock = lock_user_data_dir_test_env(); + let _env_lock = lock_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let home = tempfile::TempDir::new().expect("home temp dir"); let _home_guard = EnvVarGuard::set("HOME", home.path()); @@ -1059,7 +1059,7 @@ mod tests { #[cfg(unix)] #[test] fn render_launchd_plist_escapes_xml_and_parser_unescapes_socket_path() { - let _env_lock = lock_user_data_dir_test_env(); + let _env_lock = lock_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let home = tempfile::TempDir::new().expect("home temp dir"); let _home_guard = EnvVarGuard::set("HOME", home.path()); @@ -1115,7 +1115,7 @@ mod tests { #[cfg(unix)] #[test] fn launchd_plist_env_value_round_trips_data_dir_override() { - let _env_lock = lock_user_data_dir_test_env(); + let _env_lock = lock_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let home = tempfile::TempDir::new().expect("home temp dir"); let _home_guard = EnvVarGuard::set("HOME", home.path()); @@ -1137,7 +1137,7 @@ mod tests { #[cfg(unix)] #[test] fn launchd_plist_env_value_ignores_plist_without_override() { - let _env_lock = lock_user_data_dir_test_env(); + let _env_lock = lock_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let home = tempfile::TempDir::new().expect("home temp dir"); let _home_guard = EnvVarGuard::set("HOME", home.path()); @@ -1225,7 +1225,7 @@ mod tests { #[cfg(target_os = "linux")] #[test] fn refresh_service_rewrites_unit_and_restarts_daemon() { - let _env_lock = lock_user_data_dir_test_env(); + let _env_lock = lock_test_env(); let dir = TempDir::new().expect("temp dir"); let config_home = dir.path().join("config"); let fake_bin = dir.path().join("bin"); @@ -1274,7 +1274,7 @@ mod tests { #[cfg(target_os = "linux")] #[test] fn refresh_installed_service_skips_missing_unit() { - let _env_lock = lock_user_data_dir_test_env(); + let _env_lock = lock_test_env(); let dir = TempDir::new().expect("temp dir"); let config_home = dir.path().join("config"); let fake_bin = dir.path().join("bin"); @@ -1303,7 +1303,7 @@ mod tests { #[cfg(target_os = "linux")] #[test] fn refresh_installed_service_preserves_existing_socket_path() { - let _env_lock = lock_user_data_dir_test_env(); + let _env_lock = lock_test_env(); let dir = TempDir::new().expect("temp dir"); let config_home = dir.path().join("config"); let fake_bin = dir.path().join("bin"); @@ -1363,7 +1363,7 @@ mod tests { #[test] fn default_socket_path_is_profile_scoped_not_project_scoped() { - let _env_lock = lock_user_data_dir_test_env(); + let _env_lock = lock_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let project_a = tempfile::TempDir::new().expect("project a temp dir"); let project_b = tempfile::TempDir::new().expect("project b temp dir"); diff --git a/src/hooks/codex.rs b/src/hooks/codex.rs index 0a711d16..d7d32edd 100644 --- a/src/hooks/codex.rs +++ b/src/hooks/codex.rs @@ -644,7 +644,7 @@ mod tests { #[test] fn codex_prompt_hints_dedupe_by_session_and_category() { - let _lock = crate::hooks::lock_test_env(); + let _lock = crate::mcp::response_handles::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); diff --git a/src/hooks/cursor.rs b/src/hooks/cursor.rs index a151b5db..1bd52ad4 100644 --- a/src/hooks/cursor.rs +++ b/src/hooks/cursor.rs @@ -788,7 +788,7 @@ mod tests { /// so a prompt-shaped trigger steers identically on both agents. #[test] fn cursor_prompt_hint_runs_decide_hint_and_dedupes_per_session() { - let _lock = crate::hooks::lock_test_env(); + let _lock = crate::mcp::response_handles::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs index 1f510556..9c031249 100644 --- a/src/hooks/mod.rs +++ b/src/hooks/mod.rs @@ -154,11 +154,6 @@ fn now_unix_secs() -> i64 { .map_or(0, |d| d.as_secs() as i64) } -#[cfg(test)] -pub(crate) fn lock_test_env() -> std::sync::MutexGuard<'static, ()> { - crate::config::lock_user_data_dir_test_env() -} - /// Mints a unique id for one hint candidate so its `hint_candidate` row and its /// single terminal row (`hint_emitted` / `hint_escalated` / `suppressed_duplicate` /// / `suppressed_budget` / `missing_session` / `dropped_no_root`) can be correlated @@ -658,7 +653,7 @@ mod hint_analytics_tests { #[test] fn record_hint_emitted_missing_session_is_single_terminal() { - let _lock = super::lock_test_env(); + let _lock = crate::mcp::response_handles::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -686,7 +681,7 @@ mod hint_analytics_tests { /// known. #[test] fn every_hint_branch_yields_exactly_one_terminal_with_hint_id() { - let _lock = super::lock_test_env(); + let _lock = crate::mcp::response_handles::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -781,7 +776,7 @@ mod hint_analytics_tests { /// terminal, and no hint is returned to the caller. #[test] fn budget_exhaustion_records_suppressed_budget_terminal() { - let _lock = super::lock_test_env(); + let _lock = crate::mcp::response_handles::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -845,7 +840,7 @@ mod hint_analytics_tests { /// stronger re-hint recorded as `hint_escalated`, with the escalation prefix. #[test] fn repeated_usage_records_hint_escalated_terminal() { - let _lock = super::lock_test_env(); + let _lock = crate::mcp::response_handles::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); diff --git a/src/mcp/response_handles.rs b/src/mcp/response_handles.rs index 419289d7..2017dab4 100644 --- a/src/mcp/response_handles.rs +++ b/src/mcp/response_handles.rs @@ -660,9 +660,9 @@ fn clipped_handle_for_log(handle: &str) -> String { } } -/// Serializes lib unit tests that store response handles under the -/// process-global profile root (`TRACEDECAY_DATA_DIR`). Parallel tests -/// otherwise race on profile-sharded `response-handles/` directories. +/// Serializes lib unit tests that touch the process-global profile root +/// (`TRACEDECAY_DATA_DIR`) or store response handles under it. Parallel +/// tests otherwise race on env overrides and profile-sharded store paths. #[cfg(test)] pub(crate) static RESPONSE_HANDLE_STORE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); @@ -672,6 +672,13 @@ pub(crate) fn lock_response_handle_store() -> std::sync::MutexGuard<'static, ()> lock_recovering_poison(&RESPONSE_HANDLE_STORE_LOCK) } +/// Alias for [`lock_response_handle_store`] used by hook analytics tests +/// that override `TRACEDECAY_DATA_DIR`. +#[cfg(test)] +pub(crate) fn lock_test_env() -> std::sync::MutexGuard<'static, ()> { + lock_response_handle_store() +} + #[cfg(test)] fn lock_recovering_poison(mutex: &std::sync::Mutex) -> std::sync::MutexGuard<'_, T> { mutex.lock().unwrap_or_else(|err| err.into_inner()) diff --git a/src/mcp/tools/handlers/mod.rs b/src/mcp/tools/handlers/mod.rs index 0f95e069..83bb0251 100644 --- a/src/mcp/tools/handlers/mod.rs +++ b/src/mcp/tools/handlers/mod.rs @@ -531,7 +531,8 @@ mod tests { use super::super::get_tool_definitions; use super::*; - use crate::config::{lock_user_data_dir_test_env, USER_DATA_DIR_ENV}; + use crate::config::USER_DATA_DIR_ENV; + use crate::mcp::response_handles::lock_test_env; struct EnvVarGuard { key: &'static str, @@ -723,7 +724,7 @@ mod tests { #[tokio::test] async fn graph_reader_selector_dispatch_targets_registered_project() { - let _env_lock = lock_user_data_dir_test_env(); + let _env_lock = lock_test_env(); let dir = TempDir::new().unwrap(); let _env = SelectorEnv::new(dir.path()); let active_project = dir.path().join("active"); @@ -785,7 +786,7 @@ mod tests { #[tokio::test] async fn graph_reader_selector_dispatch_accepts_unique_project_basename() { - let _env_lock = lock_user_data_dir_test_env(); + let _env_lock = lock_test_env(); let dir = TempDir::new().unwrap(); let _env = SelectorEnv::new(dir.path()); let active_project = dir.path().join("active"); @@ -834,7 +835,7 @@ mod tests { #[tokio::test] async fn graph_reader_selector_rejects_ambiguous_project_basename() { - let _env_lock = lock_user_data_dir_test_env(); + let _env_lock = lock_test_env(); let dir = TempDir::new().unwrap(); let _env = SelectorEnv::new(dir.path()); let active_project = dir.path().join("active"); @@ -878,7 +879,7 @@ mod tests { #[tokio::test] async fn unsupported_selector_tool_rejects_explicit_project_selector() { - let _env_lock = lock_user_data_dir_test_env(); + let _env_lock = lock_test_env(); let dir = TempDir::new().unwrap(); let _env = SelectorEnv::new(dir.path()); let project = dir.path().join("active"); @@ -913,7 +914,7 @@ mod tests { const LARGE_RESPONSE_MARKER_COUNT: usize = 120; const LAST_LARGE_RESPONSE_MARKER: usize = LARGE_RESPONSE_MARKER_COUNT - 1; - let _env_lock = lock_user_data_dir_test_env(); + let _env_lock = lock_test_env(); let dir = TempDir::new().unwrap(); let _env = SelectorEnv::new(dir.path()); let active_project = dir.path().join("active"); diff --git a/src/user_config.rs b/src/user_config.rs index 2b57026c..7e92ef0b 100644 --- a/src/user_config.rs +++ b/src/user_config.rs @@ -251,7 +251,8 @@ pub fn parse_duration(s: &str) -> Option { )] mod tests { use super::*; - use crate::config::{lock_user_data_dir_test_env, USER_DATA_DIR_ENV}; + use crate::config::USER_DATA_DIR_ENV; + use crate::mcp::response_handles::lock_test_env; use std::ffi::OsString; use std::time::Duration; use tempfile::TempDir; @@ -305,7 +306,7 @@ mod tests { #[test] fn save_preserves_existing_corrupt_config_file() { - let _lock = lock_user_data_dir_test_env(); + let _lock = lock_test_env(); let temp = TempDir::new().unwrap(); let _env = EnvRestore::set(USER_DATA_DIR_ENV, temp.path()); let path = config_path().expect("config path should resolve"); @@ -325,7 +326,7 @@ mod tests { #[test] fn save_preserves_unknown_config_keys() { - let _lock = lock_user_data_dir_test_env(); + let _lock = lock_test_env(); let temp = TempDir::new().unwrap(); let _env = EnvRestore::set(USER_DATA_DIR_ENV, temp.path()); let path = config_path().expect("config path should resolve"); From 937e695e7bec32629c4c0a24b52b41aac9149f90 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 5 Jul 2026 02:52:18 +0000 Subject: [PATCH 10/15] test: share response-handle store lock with profile env lock Handle-store lib tests must serialize with TRACEDECAY_DATA_DIR mutators; otherwise profile resolution races and truncation handles fail to persist. --- src/agents/claude/tests.rs | 42 ----------------------------------- src/config.rs | 14 ++++++++++++ src/config/tests.rs | 9 ++++---- src/daemon/service.rs | 20 ++++++++--------- src/hooks/codex.rs | 2 +- src/hooks/cursor.rs | 2 +- src/hooks/mod.rs | 13 +++++++---- src/mcp/response_handles.rs | 24 ++++---------------- src/mcp/tools/handlers/mod.rs | 13 +++++------ src/user_config.rs | 7 +++--- 10 files changed, 52 insertions(+), 94 deletions(-) diff --git a/src/agents/claude/tests.rs b/src/agents/claude/tests.rs index 98f509f3..67cf4818 100644 --- a/src/agents/claude/tests.rs +++ b/src/agents/claude/tests.rs @@ -1,45 +1,6 @@ use super::*; -use crate::config::USER_DATA_DIR_ENV; -use crate::mcp::response_handles::lock_test_env; use serde_json::json; -struct InstallProfileEnv { - _lock: std::sync::MutexGuard<'static, ()>, - _profile: ProfileEnvGuard, -} - -struct ProfileEnvGuard { - key: &'static str, - previous: Option, -} - -impl ProfileEnvGuard { - fn set(key: &'static str, value: impl AsRef) -> Self { - let previous = std::env::var_os(key); - std::env::set_var(key, value.as_ref()); - Self { key, previous } - } -} - -impl Drop for ProfileEnvGuard { - fn drop(&mut self) { - if let Some(previous) = self.previous.take() { - std::env::set_var(self.key, previous); - } else { - std::env::remove_var(self.key); - } - } -} - -fn install_profile_env(home: &Path) -> InstallProfileEnv { - let profile = home.join(".tracedecay"); - std::fs::create_dir_all(&profile).expect("install test profile dir"); - InstallProfileEnv { - _lock: lock_test_env(), - _profile: ProfileEnvGuard::set(USER_DATA_DIR_ENV, profile), - } -} - fn plugin_subdir_names(rel: &str) -> Vec { let root = Path::new(env!("CARGO_MANIFEST_DIR")) .join("plugin") @@ -266,7 +227,6 @@ fn deploy_refuses_to_replace_non_tracedecay_dir() { #[test] fn install_is_idempotent() { let home = tempfile::tempdir().unwrap(); - let _profile_env = install_profile_env(home.path()); let ctx = install_ctx(home.path()); ClaudeIntegration.install(&ctx).unwrap(); @@ -320,7 +280,6 @@ fn register_marketplace_preserves_existing() { #[test] fn install_handles_malformed_settings_parents() { let home = tempfile::tempdir().unwrap(); - let _profile_env = install_profile_env(home.path()); let claude_dir = home.path().join(".claude"); std::fs::create_dir_all(&claude_dir).unwrap(); std::fs::write( @@ -517,7 +476,6 @@ fn uninstall_permissions_removes_tracedecay_entries() { #[test] fn uninstall_removes_plugin_and_marketplace() { let home = tempfile::tempdir().unwrap(); - let _profile_env = install_profile_env(home.path()); let ctx = install_ctx(home.path()); ClaudeIntegration.install(&ctx).unwrap(); assert!(plugin_marketplace_manifest_path(home.path()).exists()); diff --git a/src/config.rs b/src/config.rs index 79b4670c..5545d48f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -810,6 +810,20 @@ pub fn is_excluded(file_path: &str, config: &TraceDecayConfig) -> bool { false } +/// Serializes lib unit tests that mutate process-wide storage env vars +/// (`TRACEDECAY_DATA_DIR` and related HOME/profile pins). Parallel tests +/// otherwise race on profile resolution and hook analytics paths. +#[cfg(test)] +pub static USER_DATA_DIR_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + +/// Acquires [`USER_DATA_DIR_TEST_LOCK`], recovering even when poisoned. +#[cfg(test)] +pub fn lock_user_data_dir_test_env() -> std::sync::MutexGuard<'static, ()> { + USER_DATA_DIR_TEST_LOCK + .lock() + .unwrap_or_else(|err| err.into_inner()) +} + #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests; diff --git a/src/config/tests.rs b/src/config/tests.rs index 7a68901c..ba04bcc9 100644 --- a/src/config/tests.rs +++ b/src/config/tests.rs @@ -1,9 +1,8 @@ use super::{ db_filename, get_project_db_path, get_tracedecay_dir, is_excluded, is_excluded_dir, - is_ignored_by_explicit_global_excludes, is_ignored_by_git, is_included, user_data_dir, - TraceDecayConfig, USER_DATA_DIR_ENV, + is_ignored_by_explicit_global_excludes, is_ignored_by_git, is_included, + lock_user_data_dir_test_env, user_data_dir, TraceDecayConfig, USER_DATA_DIR_ENV, }; -use crate::mcp::response_handles::lock_test_env; use std::ffi::OsString; use std::fs; use std::process::Command; @@ -57,7 +56,7 @@ fn test_data_dir_uses_tracedecay_when_present() { #[cfg(unix)] #[test] fn user_data_dir_canonicalizes_symlinked_existing_parent() { - let _lock = lock_test_env(); + let _lock = lock_user_data_dir_test_env(); let root = TempDir::new().unwrap(); let real_home = root.path().join("real-home"); let linked_home = root.path().join("linked-home"); @@ -293,7 +292,7 @@ fn partial_sync_table_fills_missing_fields_with_defaults() { #[test] fn sync_config_env_overrides_bool_and_int() { - let _lock = lock_test_env(); + let _lock = lock_user_data_dir_test_env(); let _watch = EnvRestore::set("TRACEDECAY_SYNC_AUTO_WATCH", "false"); let _debounce = EnvRestore::set("TRACEDECAY_SYNC_WATCH_DEBOUNCE_MS", "5000"); // Unparsable ints/bools are ignored (field keeps its base value). diff --git a/src/daemon/service.rs b/src/daemon/service.rs index d67c6c6e..3a939908 100644 --- a/src/daemon/service.rs +++ b/src/daemon/service.rs @@ -876,7 +876,7 @@ mod tests { use tempfile::TempDir; use super::{DaemonServiceSpec, LaunchctlFailureMode, LaunchdCommand}; - use crate::mcp::response_handles::lock_test_env; + use crate::config::lock_user_data_dir_test_env; struct EnvVarGuard { key: &'static str, @@ -936,7 +936,7 @@ mod tests { #[cfg(target_os = "macos")] #[test] fn service_status_includes_launchd_debug_commands() { - let _env_lock = lock_test_env(); + let _env_lock = lock_user_data_dir_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let _data_dir_guard = EnvVarGuard::set(crate::config::USER_DATA_DIR_ENV, profile.path()); @@ -1020,7 +1020,7 @@ mod tests { #[cfg(unix)] #[test] fn render_launchd_plist_includes_program_arguments_socket_logs_and_label() { - let _env_lock = lock_test_env(); + let _env_lock = lock_user_data_dir_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let home = tempfile::TempDir::new().expect("home temp dir"); let _home_guard = EnvVarGuard::set("HOME", home.path()); @@ -1059,7 +1059,7 @@ mod tests { #[cfg(unix)] #[test] fn render_launchd_plist_escapes_xml_and_parser_unescapes_socket_path() { - let _env_lock = lock_test_env(); + let _env_lock = lock_user_data_dir_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let home = tempfile::TempDir::new().expect("home temp dir"); let _home_guard = EnvVarGuard::set("HOME", home.path()); @@ -1115,7 +1115,7 @@ mod tests { #[cfg(unix)] #[test] fn launchd_plist_env_value_round_trips_data_dir_override() { - let _env_lock = lock_test_env(); + let _env_lock = lock_user_data_dir_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let home = tempfile::TempDir::new().expect("home temp dir"); let _home_guard = EnvVarGuard::set("HOME", home.path()); @@ -1137,7 +1137,7 @@ mod tests { #[cfg(unix)] #[test] fn launchd_plist_env_value_ignores_plist_without_override() { - let _env_lock = lock_test_env(); + let _env_lock = lock_user_data_dir_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let home = tempfile::TempDir::new().expect("home temp dir"); let _home_guard = EnvVarGuard::set("HOME", home.path()); @@ -1225,7 +1225,7 @@ mod tests { #[cfg(target_os = "linux")] #[test] fn refresh_service_rewrites_unit_and_restarts_daemon() { - let _env_lock = lock_test_env(); + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().expect("temp dir"); let config_home = dir.path().join("config"); let fake_bin = dir.path().join("bin"); @@ -1274,7 +1274,7 @@ mod tests { #[cfg(target_os = "linux")] #[test] fn refresh_installed_service_skips_missing_unit() { - let _env_lock = lock_test_env(); + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().expect("temp dir"); let config_home = dir.path().join("config"); let fake_bin = dir.path().join("bin"); @@ -1303,7 +1303,7 @@ mod tests { #[cfg(target_os = "linux")] #[test] fn refresh_installed_service_preserves_existing_socket_path() { - let _env_lock = lock_test_env(); + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().expect("temp dir"); let config_home = dir.path().join("config"); let fake_bin = dir.path().join("bin"); @@ -1363,7 +1363,7 @@ mod tests { #[test] fn default_socket_path_is_profile_scoped_not_project_scoped() { - let _env_lock = lock_test_env(); + let _env_lock = lock_user_data_dir_test_env(); let profile = tempfile::TempDir::new().expect("profile temp dir"); let project_a = tempfile::TempDir::new().expect("project a temp dir"); let project_b = tempfile::TempDir::new().expect("project b temp dir"); diff --git a/src/hooks/codex.rs b/src/hooks/codex.rs index d7d32edd..0a711d16 100644 --- a/src/hooks/codex.rs +++ b/src/hooks/codex.rs @@ -644,7 +644,7 @@ mod tests { #[test] fn codex_prompt_hints_dedupe_by_session_and_category() { - let _lock = crate::mcp::response_handles::lock_test_env(); + let _lock = crate::hooks::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); diff --git a/src/hooks/cursor.rs b/src/hooks/cursor.rs index 1bd52ad4..a151b5db 100644 --- a/src/hooks/cursor.rs +++ b/src/hooks/cursor.rs @@ -788,7 +788,7 @@ mod tests { /// so a prompt-shaped trigger steers identically on both agents. #[test] fn cursor_prompt_hint_runs_decide_hint_and_dedupes_per_session() { - let _lock = crate::mcp::response_handles::lock_test_env(); + let _lock = crate::hooks::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); diff --git a/src/hooks/mod.rs b/src/hooks/mod.rs index 9c031249..1f510556 100644 --- a/src/hooks/mod.rs +++ b/src/hooks/mod.rs @@ -154,6 +154,11 @@ fn now_unix_secs() -> i64 { .map_or(0, |d| d.as_secs() as i64) } +#[cfg(test)] +pub(crate) fn lock_test_env() -> std::sync::MutexGuard<'static, ()> { + crate::config::lock_user_data_dir_test_env() +} + /// Mints a unique id for one hint candidate so its `hint_candidate` row and its /// single terminal row (`hint_emitted` / `hint_escalated` / `suppressed_duplicate` /// / `suppressed_budget` / `missing_session` / `dropped_no_root`) can be correlated @@ -653,7 +658,7 @@ mod hint_analytics_tests { #[test] fn record_hint_emitted_missing_session_is_single_terminal() { - let _lock = crate::mcp::response_handles::lock_test_env(); + let _lock = super::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -681,7 +686,7 @@ mod hint_analytics_tests { /// known. #[test] fn every_hint_branch_yields_exactly_one_terminal_with_hint_id() { - let _lock = crate::mcp::response_handles::lock_test_env(); + let _lock = super::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -776,7 +781,7 @@ mod hint_analytics_tests { /// terminal, and no hint is returned to the caller. #[test] fn budget_exhaustion_records_suppressed_budget_terminal() { - let _lock = crate::mcp::response_handles::lock_test_env(); + let _lock = super::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); @@ -840,7 +845,7 @@ mod hint_analytics_tests { /// stronger re-hint recorded as `hint_escalated`, with the escalation prefix. #[test] fn repeated_usage_records_hint_escalated_terminal() { - let _lock = crate::mcp::response_handles::lock_test_env(); + let _lock = super::lock_test_env(); let project = tempfile::tempdir().unwrap(); let profile = tempfile::tempdir().unwrap(); let project_root = project.path().canonicalize().unwrap(); diff --git a/src/mcp/response_handles.rs b/src/mcp/response_handles.rs index 2017dab4..3a32f18f 100644 --- a/src/mcp/response_handles.rs +++ b/src/mcp/response_handles.rs @@ -660,26 +660,10 @@ fn clipped_handle_for_log(handle: &str) -> String { } } -/// Serializes lib unit tests that touch the process-global profile root -/// (`TRACEDECAY_DATA_DIR`) or store response handles under it. Parallel -/// tests otherwise race on env overrides and profile-sharded store paths. -#[cfg(test)] -pub(crate) static RESPONSE_HANDLE_STORE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); - -/// Acquires [`RESPONSE_HANDLE_STORE_LOCK`], recovering even when poisoned. +/// Serializes lib unit tests that store response handles under the +/// process-global profile root (`TRACEDECAY_DATA_DIR`). Uses the shared +/// user-data-dir test lock so env mutation cannot race profile resolution. #[cfg(test)] pub(crate) fn lock_response_handle_store() -> std::sync::MutexGuard<'static, ()> { - lock_recovering_poison(&RESPONSE_HANDLE_STORE_LOCK) -} - -/// Alias for [`lock_response_handle_store`] used by hook analytics tests -/// that override `TRACEDECAY_DATA_DIR`. -#[cfg(test)] -pub(crate) fn lock_test_env() -> std::sync::MutexGuard<'static, ()> { - lock_response_handle_store() -} - -#[cfg(test)] -fn lock_recovering_poison(mutex: &std::sync::Mutex) -> std::sync::MutexGuard<'_, T> { - mutex.lock().unwrap_or_else(|err| err.into_inner()) + crate::config::lock_user_data_dir_test_env() } diff --git a/src/mcp/tools/handlers/mod.rs b/src/mcp/tools/handlers/mod.rs index 83bb0251..0f95e069 100644 --- a/src/mcp/tools/handlers/mod.rs +++ b/src/mcp/tools/handlers/mod.rs @@ -531,8 +531,7 @@ mod tests { use super::super::get_tool_definitions; use super::*; - use crate::config::USER_DATA_DIR_ENV; - use crate::mcp::response_handles::lock_test_env; + use crate::config::{lock_user_data_dir_test_env, USER_DATA_DIR_ENV}; struct EnvVarGuard { key: &'static str, @@ -724,7 +723,7 @@ mod tests { #[tokio::test] async fn graph_reader_selector_dispatch_targets_registered_project() { - let _env_lock = lock_test_env(); + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().unwrap(); let _env = SelectorEnv::new(dir.path()); let active_project = dir.path().join("active"); @@ -786,7 +785,7 @@ mod tests { #[tokio::test] async fn graph_reader_selector_dispatch_accepts_unique_project_basename() { - let _env_lock = lock_test_env(); + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().unwrap(); let _env = SelectorEnv::new(dir.path()); let active_project = dir.path().join("active"); @@ -835,7 +834,7 @@ mod tests { #[tokio::test] async fn graph_reader_selector_rejects_ambiguous_project_basename() { - let _env_lock = lock_test_env(); + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().unwrap(); let _env = SelectorEnv::new(dir.path()); let active_project = dir.path().join("active"); @@ -879,7 +878,7 @@ mod tests { #[tokio::test] async fn unsupported_selector_tool_rejects_explicit_project_selector() { - let _env_lock = lock_test_env(); + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().unwrap(); let _env = SelectorEnv::new(dir.path()); let project = dir.path().join("active"); @@ -914,7 +913,7 @@ mod tests { const LARGE_RESPONSE_MARKER_COUNT: usize = 120; const LAST_LARGE_RESPONSE_MARKER: usize = LARGE_RESPONSE_MARKER_COUNT - 1; - let _env_lock = lock_test_env(); + let _env_lock = lock_user_data_dir_test_env(); let dir = TempDir::new().unwrap(); let _env = SelectorEnv::new(dir.path()); let active_project = dir.path().join("active"); diff --git a/src/user_config.rs b/src/user_config.rs index 7e92ef0b..2b57026c 100644 --- a/src/user_config.rs +++ b/src/user_config.rs @@ -251,8 +251,7 @@ pub fn parse_duration(s: &str) -> Option { )] mod tests { use super::*; - use crate::config::USER_DATA_DIR_ENV; - use crate::mcp::response_handles::lock_test_env; + use crate::config::{lock_user_data_dir_test_env, USER_DATA_DIR_ENV}; use std::ffi::OsString; use std::time::Duration; use tempfile::TempDir; @@ -306,7 +305,7 @@ mod tests { #[test] fn save_preserves_existing_corrupt_config_file() { - let _lock = lock_test_env(); + let _lock = lock_user_data_dir_test_env(); let temp = TempDir::new().unwrap(); let _env = EnvRestore::set(USER_DATA_DIR_ENV, temp.path()); let path = config_path().expect("config path should resolve"); @@ -326,7 +325,7 @@ mod tests { #[test] fn save_preserves_unknown_config_keys() { - let _lock = lock_test_env(); + let _lock = lock_user_data_dir_test_env(); let temp = TempDir::new().unwrap(); let _env = EnvRestore::set(USER_DATA_DIR_ENV, temp.path()); let path = config_path().expect("config path should resolve"); From 3ac12e77d0f6c1af5858f5ebccca6a524e3389a6 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 5 Jul 2026 02:54:23 +0000 Subject: [PATCH 11/15] test: pin isolated profile in freshness lib tests TraceDecay init/index tests shared TRACEDECAY_DATA_DIR with parallel env mutators; pin an isolated profile under the shared test lock. --- src/config.rs | 37 ++++++++++++++++++++++ src/mcp/server/freshness_tests.rs | 10 +++--- src/tracedecay/indexing/freshness_tests.rs | 14 ++++---- 3 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/config.rs b/src/config.rs index 5545d48f..7660bdfa 100644 --- a/src/config.rs +++ b/src/config.rs @@ -824,6 +824,43 @@ pub fn lock_user_data_dir_test_env() -> std::sync::MutexGuard<'static, ()> { .unwrap_or_else(|err| err.into_inner()) } +/// Pins [`USER_DATA_DIR_ENV`] to an isolated temp profile while holding +/// [`USER_DATA_DIR_TEST_LOCK`], so parallel lib tests cannot race profile +/// resolution during `TraceDecay::init` / indexing. +#[cfg(test)] +pub struct PinnedUserDataDir { + _lock: std::sync::MutexGuard<'static, ()>, + _root: tempfile::TempDir, + previous: Option, +} + +#[cfg(test)] +impl PinnedUserDataDir { + pub fn new() -> Self { + let lock = lock_user_data_dir_test_env(); + let root = tempfile::TempDir::new().expect("temp profile dir"); + let profile = root.path().join(TRACEDECAY_DIR); + fs::create_dir_all(&profile).expect("create isolated profile root"); + let previous = std::env::var_os(USER_DATA_DIR_ENV); + std::env::set_var(USER_DATA_DIR_ENV, &profile); + Self { + _lock: lock, + _root: root, + previous, + } + } +} + +#[cfg(test)] +impl Drop for PinnedUserDataDir { + fn drop(&mut self) { + match self.previous.take() { + Some(previous) => std::env::set_var(USER_DATA_DIR_ENV, previous), + None => std::env::remove_var(USER_DATA_DIR_ENV), + } + } +} + #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used)] mod tests; diff --git a/src/mcp/server/freshness_tests.rs b/src/mcp/server/freshness_tests.rs index 6b212eec..e15a6590 100644 --- a/src/mcp/server/freshness_tests.rs +++ b/src/mcp/server/freshness_tests.rs @@ -1,4 +1,5 @@ use super::{format_index_age_phrase, staleness_banner, McpServer, StalenessBannerInputs}; +use crate::config::PinnedUserDataDir; use crate::tracedecay::TraceDecay; use std::sync::atomic::Ordering; use std::time::Duration; @@ -15,7 +16,8 @@ fn git(root: &std::path::Path, args: &[&str]) { assert!(ok, "git {args:?} failed"); } -async fn init_indexed_repo() -> (TraceDecay, TempDir) { +async fn init_indexed_repo() -> (TraceDecay, TempDir, PinnedUserDataDir) { + let pin = PinnedUserDataDir::new(); let dir = TempDir::new().unwrap(); let root = dir.path(); git(root, &["init", "-q", "-b", "main"]); @@ -28,7 +30,7 @@ async fn init_indexed_repo() -> (TraceDecay, TempDir) { git(root, &["commit", "-q", "-m", "initial"]); let cg = TraceDecay::init(root).await.expect("init"); cg.index_all().await.expect("index"); - (cg, dir) + (cg, dir, pin) } // ---- D7 pure-logic banner tests (test c) -------------------------- @@ -113,7 +115,7 @@ fn banner_instructs_manual_sync_when_auto_sync_disabled() { #[tokio::test] async fn startup_catch_up_spawned_once_per_server() { - let (cg, _dir) = init_indexed_repo().await; + let (cg, _dir, _pin) = init_indexed_repo().await; let server = McpServer::new(cg, None).await; // The D1 spawn should have claimed the one-shot flag. assert!( @@ -144,7 +146,7 @@ async fn startup_catch_up_spawned_once_per_server() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn read_refresh_is_non_blocking_and_single_flighted() { - let (cg, dir) = init_indexed_repo().await; + let (cg, dir, _pin) = init_indexed_repo().await; let root = dir.path().to_path_buf(); let mut config = crate::config::load_config(&root).expect("load config"); config.sync.session_start_sync = false; diff --git a/src/tracedecay/indexing/freshness_tests.rs b/src/tracedecay/indexing/freshness_tests.rs index f257b9e6..a153bdef 100644 --- a/src/tracedecay/indexing/freshness_tests.rs +++ b/src/tracedecay/indexing/freshness_tests.rs @@ -1,4 +1,5 @@ use crate::tracedecay::TraceDecay; +use crate::config::PinnedUserDataDir; use std::path::Path; use std::process::Command; use tempfile::TempDir; @@ -28,7 +29,8 @@ fn head_oid(dir: &Path) -> String { String::from_utf8(out.stdout).unwrap().trim().to_string() } -async fn init_repo_with_commit() -> (TraceDecay, TempDir) { +async fn init_repo_with_commit() -> (TraceDecay, TempDir, PinnedUserDataDir) { + let pin = PinnedUserDataDir::new(); let dir = TempDir::new().unwrap(); let root = dir.path(); git(root, &["init", "-q", "-b", "main"]); @@ -39,19 +41,19 @@ async fn init_repo_with_commit() -> (TraceDecay, TempDir) { let cg = TraceDecay::init(root).await.expect("init"); cg.index_all().await.expect("index"); - (cg, dir) + (cg, dir, pin) } #[tokio::test] async fn last_synced_commit_stamped_after_index() { - let (cg, dir) = init_repo_with_commit().await; + let (cg, dir, _pin) = init_repo_with_commit().await; let stamped = cg.last_synced_commit().await; assert_eq!(stamped.as_deref(), Some(head_oid(dir.path()).as_str())); } #[tokio::test] async fn stale_files_since_commit_reports_changed_file() { - let (cg, dir) = init_repo_with_commit().await; + let (cg, dir, _pin) = init_repo_with_commit().await; let root = dir.path(); let base = head_oid(root); @@ -70,7 +72,7 @@ async fn stale_files_since_commit_reports_changed_file() { #[tokio::test] async fn stale_files_since_commit_none_when_base_missing() { - let (cg, _dir) = init_repo_with_commit().await; + let (cg, _dir, _pin) = init_repo_with_commit().await; // A syntactically valid but unreachable commit id. let bogus = "0".repeat(40); assert!(cg.stale_files_since_commit(&bogus, 500).is_none()); @@ -78,7 +80,7 @@ async fn stale_files_since_commit_none_when_base_missing() { #[tokio::test] async fn stale_files_since_commit_none_when_over_escalation_limit() { - let (cg, dir) = init_repo_with_commit().await; + let (cg, dir, _pin) = init_repo_with_commit().await; let root = dir.path(); let base = head_oid(root); From 861aeaef1b86666bcfc1cf8024905f38998fd8b7 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 5 Jul 2026 03:11:57 +0000 Subject: [PATCH 12/15] fix(test): satisfy isolation clippy --- src/config.rs | 15 ++++++++++++--- src/mcp/server.rs | 6 +++--- src/mcp/tools/handlers/mod.rs | 1 + src/tracedecay/indexing/freshness_tests.rs | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/config.rs b/src/config.rs index 7660bdfa..58ef99c8 100644 --- a/src/config.rs +++ b/src/config.rs @@ -821,7 +821,7 @@ pub static USER_DATA_DIR_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new pub fn lock_user_data_dir_test_env() -> std::sync::MutexGuard<'static, ()> { USER_DATA_DIR_TEST_LOCK .lock() - .unwrap_or_else(|err| err.into_inner()) + .unwrap_or_else(std::sync::PoisonError::into_inner) } /// Pins [`USER_DATA_DIR_ENV`] to an isolated temp profile while holding @@ -838,9 +838,11 @@ pub struct PinnedUserDataDir { impl PinnedUserDataDir { pub fn new() -> Self { let lock = lock_user_data_dir_test_env(); - let root = tempfile::TempDir::new().expect("temp profile dir"); + let root = tempfile::TempDir::new() + .unwrap_or_else(|err| panic!("failed to create temp profile dir: {err}")); let profile = root.path().join(TRACEDECAY_DIR); - fs::create_dir_all(&profile).expect("create isolated profile root"); + fs::create_dir_all(&profile) + .unwrap_or_else(|err| panic!("failed to create isolated profile root: {err}")); let previous = std::env::var_os(USER_DATA_DIR_ENV); std::env::set_var(USER_DATA_DIR_ENV, &profile); Self { @@ -851,6 +853,13 @@ impl PinnedUserDataDir { } } +#[cfg(test)] +impl Default for PinnedUserDataDir { + fn default() -> Self { + Self::new() + } +} + #[cfg(test)] impl Drop for PinnedUserDataDir { fn drop(&mut self) { diff --git a/src/mcp/server.rs b/src/mcp/server.rs index bc3e063c..a186d6e3 100644 --- a/src/mcp/server.rs +++ b/src/mcp/server.rs @@ -1372,7 +1372,7 @@ impl McpServer { return; } - self.spawn_read_refresh_task(Arc::clone(cg), self.sync_config.full_sync_escalation_files); + self.spawn_read_refresh_task(cg, self.sync_config.full_sync_escalation_files); } /// Spawns the detached D4 refresh task. The task owns cheap `Arc` clones @@ -1383,7 +1383,7 @@ impl McpServer { /// /// The caller MUST have already set `background_refresh_running` to /// `true`; this task clears it on completion. - fn spawn_read_refresh_task(&self, cg: Arc, escalation: usize) { + fn spawn_read_refresh_task(&self, cg: &Arc, escalation: usize) { let running = Arc::clone(&self.background_refresh_running); let done_at = Arc::clone(&self.last_background_refresh_done_at); let token_map = Arc::clone(&self.file_token_map); @@ -1461,7 +1461,7 @@ impl McpServer { } self.last_background_refresh_at .store(now, Ordering::Release); - self.spawn_read_refresh_task(cg, self.sync_config.full_sync_escalation_files); + self.spawn_read_refresh_task(&cg, self.sync_config.full_sync_escalation_files); } /// Returns a compact one-line notice when automation runs have staged diff --git a/src/mcp/tools/handlers/mod.rs b/src/mcp/tools/handlers/mod.rs index 0f95e069..5b275546 100644 --- a/src/mcp/tools/handlers/mod.rs +++ b/src/mcp/tools/handlers/mod.rs @@ -516,6 +516,7 @@ pub async fn handle_profile_scoped_lcm_tool_call( #[allow( clippy::unwrap_used, clippy::expect_used, + clippy::await_holding_lock, clippy::redundant_closure_for_method_calls, clippy::uninlined_format_args )] diff --git a/src/tracedecay/indexing/freshness_tests.rs b/src/tracedecay/indexing/freshness_tests.rs index a153bdef..e814c5be 100644 --- a/src/tracedecay/indexing/freshness_tests.rs +++ b/src/tracedecay/indexing/freshness_tests.rs @@ -1,5 +1,5 @@ -use crate::tracedecay::TraceDecay; use crate::config::PinnedUserDataDir; +use crate::tracedecay::TraceDecay; use std::path::Path; use std::process::Command; use tempfile::TempDir; From 9a3fea83c83591db8e84f223f3e2ac279ec43f14 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 5 Jul 2026 03:38:32 +0000 Subject: [PATCH 13/15] fix(test): normalize CI path assertions --- tests/agent_suite/agent_test.rs | 29 ++++++++++++++++++++++--- tests/mcp_suite/git_correlation_test.rs | 3 +++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/tests/agent_suite/agent_test.rs b/tests/agent_suite/agent_test.rs index c8e33c5c..9131ec71 100644 --- a/tests/agent_suite/agent_test.rs +++ b/tests/agent_suite/agent_test.rs @@ -281,6 +281,30 @@ fn expected_tracedecay_bin() -> String { .replace('\\', "/") } +fn expected_tracedecay_bin_variants() -> Vec { + let raw = PathBuf::from(env!("CARGO_BIN_EXE_tracedecay")); + let canonical = std::fs::canonicalize(&raw).unwrap_or_else(|_| raw.clone()); + let mut variants = Vec::new(); + for path in [raw, canonical] { + let native = path.to_string_lossy().to_string(); + let slash = native.replace('\\', "/"); + if !variants.contains(&native) { + variants.push(native); + } + if !variants.contains(&slash) { + variants.push(slash); + } + } + variants +} + +fn contains_expected_tracedecay_bin(body: &str) -> bool { + let slash_body = body.replace('\\', "/"); + expected_tracedecay_bin_variants().iter().any(|expected| { + body.contains(expected) || slash_body.contains(&expected.replace('\\', "/")) + }) +} + /// Python snippet that py_compiles the generated plugin sources inside the /// same interpreter that runs a test's check script, instead of the separate /// `python3 -m py_compile` process `assert_python_compiles` spawns. On @@ -1092,7 +1116,7 @@ fn test_hermes_local_install_writes_profile_plugin() { })); let tools_py = std::fs::read_to_string(plugin_dir.join("tools.py")).unwrap(); - assert!(tools_py.contains(&expected_tracedecay_bin())); + assert!(contains_expected_tracedecay_bin(&tools_py)); assert!(tools_py.contains("subprocess.run")); assert!(tools_py.contains("tracedecay tool")); assert!(tools_py.contains("TRACEDECAY_TIMEOUT_SECONDS = 120")); @@ -2966,9 +2990,8 @@ fn assert_local_install_writes_project_paths(agent: &str, paths: &[&str]) { && (*relative == ".agents/plugins/marketplace.json" || relative.ends_with(".codex-plugin/plugin.json")); if !is_instruction_file && !is_codex_metadata { - let expected = expected_tracedecay_bin(); assert!( - body.contains(&expected), + contains_expected_tracedecay_bin(&body), "{agent} local config {} should use the resolved absolute tracedecay executable", path.display() ); diff --git a/tests/mcp_suite/git_correlation_test.rs b/tests/mcp_suite/git_correlation_test.rs index 4c6eeb76..8ce8621a 100644 --- a/tests/mcp_suite/git_correlation_test.rs +++ b/tests/mcp_suite/git_correlation_test.rs @@ -146,6 +146,9 @@ async fn sessions_for_and_scoped_search_end_to_end() { let profile_root = base.join("profile"); std::fs::create_dir_all(&profile_root).unwrap_or_else(|e| panic!("create profile root: {e}")); + let profile_root = profile_root + .canonicalize() + .unwrap_or_else(|e| panic!("canonicalize profile root: {e}")); let cg = TraceDecay::init_with_options( &project_root, TraceDecayOpenOptions { From 3563d84337b1f32b828f980147a1285240a08713 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 5 Jul 2026 03:48:42 +0000 Subject: [PATCH 14/15] fix(test): normalize Windows command paths --- tests/agent_suite/agent_test.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/tests/agent_suite/agent_test.rs b/tests/agent_suite/agent_test.rs index 9131ec71..c6fdc048 100644 --- a/tests/agent_suite/agent_test.rs +++ b/tests/agent_suite/agent_test.rs @@ -305,6 +305,23 @@ fn contains_expected_tracedecay_bin(body: &str) -> bool { }) } +fn comparable_command_path(command: &str) -> String { + command + .strip_prefix("//?/") + .unwrap_or(command) + .replace('\\', "/") +} + +fn assert_command_eq(actual: &serde_json::Value, expected: &str) { + let actual = actual + .as_str() + .unwrap_or_else(|| panic!("command should be a string: {actual}")); + assert_eq!( + comparable_command_path(actual), + comparable_command_path(expected) + ); +} + /// Python snippet that py_compiles the generated plugin sources inside the /// same interpreter that runs a test's check script, instead of the separate /// `python3 -m py_compile` process `assert_python_compiles` spawns. On @@ -432,7 +449,7 @@ fn assert_codex_plugin_bundle( let mcp = read_json(&plugin_dir.join(".mcp.json")); let server = &mcp["mcpServers"]["tracedecay"]; assert_eq!(server["type"], "stdio"); - assert_eq!(server["command"], expected_command); + assert_command_eq(&server["command"], expected_command); assert_eq!(server["args"], expected_args); if expected_global_bundle { assert_eq!(server["env"]["TRACEDECAY_ENABLE_GLOBAL_DB"], "1"); @@ -547,7 +564,7 @@ fn assert_cursor_plugin_bundle(plugin_dir: &Path, expected_command: &str, expect let mcp = read_json(&plugin_dir.join("mcp.json")); let server = &mcp["mcpServers"]["tracedecay"]; assert_eq!(server["type"], "stdio"); - assert_eq!(server["command"], expected_command); + assert_command_eq(&server["command"], expected_command); assert_eq!( server["args"], serde_json::json!(["serve", "--path", "${workspaceFolder}"]) @@ -3721,7 +3738,7 @@ fn assert_command_contains_expected_bin( }) .expect("handler command should exist"); assert!( - command.contains(expected), + comparable_command_path(command).contains(&comparable_command_path(expected)), "Codex hook command must use the resolved absolute tracedecay executable, got {command}" ); } From b807292815ca0d64b4948ae343e4817d9b953129 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sun, 5 Jul 2026 03:59:34 +0000 Subject: [PATCH 15/15] fix(test): normalize Cursor hook paths --- tests/agent_suite/agent_test.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/agent_suite/agent_test.rs b/tests/agent_suite/agent_test.rs index c6fdc048..d54320e0 100644 --- a/tests/agent_suite/agent_test.rs +++ b/tests/agent_suite/agent_test.rs @@ -602,7 +602,8 @@ fn assert_cursor_plugin_bundle(plugin_dir: &Path, expected_command: &str, expect assert!( hook["command"] .as_str() - .is_some_and(|command| command.contains(expected_command)), + .is_some_and(|command| comparable_command_path(command) + .contains(&comparable_command_path(expected_command))), "plugin hook commands should use the installed tracedecay binary" ); assert!(