fix(tests): isolate agent configuration paths from user environment

Ignore inherited client configuration paths in test builds and enforce temporary-home boundaries for history fixtures.

Validation: 435 Rust tests pass, including a child-process regression with inherited client environment overrides.
This commit is contained in:
lzt404 committed 2026-09-11 16:25:14 +08:00
1 parent 36d3181475
commit 0a019ad1d7
4 files changed
+89 -25

No files matched your search

+21 -25
View File
@@ -3,6 +3,18 @@ use super::*;
const AGENT_VERSION_PROBE_TIMEOUT: Duration = Duration::from_secs(5);
const AGENT_VERSION_PROBE_POLL_INTERVAL: Duration = Duration::from_millis(20);
#[cfg(not(test))]
fn agent_configuration_environment(name: &str) -> Option<PathBuf> {
env::var_os(name)
.map(PathBuf::from)
.filter(|path| !path.as_os_str().is_empty())
}
#[cfg(test)]
fn agent_configuration_environment(_: &str) -> Option<PathBuf> {
None
}
pub(crate) fn agent_config_paths(client: AgentClient, home: &Path) -> Vec<PathBuf> {
match client {
AgentClient::ClaudeCode => {
@@ -89,23 +101,17 @@ pub(crate) fn opencode_config_path_from_environment(
}
pub(crate) fn kimi_code_home(home: &Path) -> PathBuf {
env::var_os("KIMI_CODE_HOME")
.map(PathBuf::from)
.filter(|path| !path.as_os_str().is_empty())
agent_configuration_environment("KIMI_CODE_HOME")
.unwrap_or_else(|| home.join(".kimi-code"))
}
pub(crate) fn grok_build_home(home: &Path) -> PathBuf {
env::var_os("GROK_HOME")
.map(PathBuf::from)
.filter(|path| !path.as_os_str().is_empty())
agent_configuration_environment("GROK_HOME")
.unwrap_or_else(|| home.join(".grok"))
}
pub(crate) fn deepseek_harness_home(home: &Path) -> PathBuf {
env::var_os("DSH_HOME")
.map(PathBuf::from)
.filter(|path| !path.as_os_str().is_empty())
agent_configuration_environment("DSH_HOME")
.unwrap_or_else(|| home.join(".dsh"))
}
@@ -131,9 +137,7 @@ pub(crate) fn read_package_json_version(path: &Path) -> Option<String> {
}
pub(crate) fn pi_agent_directory(home: &Path) -> PathBuf {
env::var_os("PI_CODING_AGENT_DIR")
.map(PathBuf::from)
.filter(|path| !path.as_os_str().is_empty())
agent_configuration_environment("PI_CODING_AGENT_DIR")
.unwrap_or_else(|| home.join(".pi/agent"))
}
@@ -683,9 +687,7 @@ pub(crate) fn remove_pi_package(executable: &Path, home: &Path) -> Result<(), St
}
pub(crate) fn codex_configuration_directory(home: &Path) -> PathBuf {
env::var_os("CODEX_HOME")
.map(PathBuf::from)
.filter(|path| !path.as_os_str().is_empty())
agent_configuration_environment("CODEX_HOME")
.unwrap_or_else(|| home.join(".codex"))
}
@@ -786,15 +788,13 @@ pub(crate) fn claude_desktop_config_paths(_home: &Path) -> Vec<PathBuf> {
};
#[cfg(target_os = "windows")]
{
let local = env::var_os("LOCALAPPDATA")
.map(PathBuf::from)
let local = agent_configuration_environment("LOCALAPPDATA")
.unwrap_or_else(|| _home.join("AppData/Local"));
claude_desktop_config_paths_from_local_app_data(&local)
}
#[cfg(target_os = "linux")]
let (normal, threep) = {
let config_home = env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
let config_home = agent_configuration_environment("XDG_CONFIG_HOME")
.filter(|path| path.is_absolute())
.unwrap_or_else(|| _home.join(".config"));
(config_home.join("Claude"), config_home.join("Claude-3p"))
@@ -865,16 +865,12 @@ pub(crate) fn find_windows_claude_data_directory(
}
pub(crate) fn hermes_agent_config_path(home: &Path) -> PathBuf {
if let Some(directory) = env::var_os("HERMES_HOME")
.map(PathBuf::from)
.filter(|path| !path.as_os_str().is_empty())
{
if let Some(directory) = agent_configuration_environment("HERMES_HOME") {
return directory.join("config.yaml");
}
#[cfg(target_os = "windows")]
{
env::var_os("LOCALAPPDATA")
.map(PathBuf::from)
agent_configuration_environment("LOCALAPPDATA")
.unwrap_or_else(|| home.join("AppData/Local"))
.join("hermes/config.yaml")
}
+7
View File
@@ -65,6 +65,13 @@ pub(crate) struct HistoryPreview {
}
pub(crate) fn history_paths(client: &str, home: &Path) -> Result<Vec<PathBuf>, String> {
let paths = history_paths_inner(client, home)?;
#[cfg(test)]
assert!(paths.iter().all(|path| path.starts_with(home)));
Ok(paths)
}
fn history_paths_inner(client: &str, home: &Path) -> Result<Vec<PathBuf>, String> {
if client == PI_AGENT_ID {
return Ok(vec![
pi_provider_config_path(home),
+1
View File
@@ -1,6 +1,7 @@
use super::*;
mod agent_configuration;
mod agent_paths;
mod agent_state;
mod agent_transactions;
mod alias_edit;
+60
View File
@@ -0,0 +1,60 @@
use super::support::*;
use super::*;
#[test]
fn configuration_paths_ignore_inherited_environment() {
let home = agent_test_home("isolated-paths");
for client in [
"claude-code",
"claude-desktop",
"codex",
"opencode",
"openclaw",
"hermes",
"deepseek-harness",
"zcode",
"kimi-code",
"grok-build",
"pi",
] {
let paths = history_paths(client, &home).unwrap();
assert!(!paths.is_empty(), "{client}");
assert!(
paths.iter().all(|path| path.starts_with(&home)),
"{client}: {paths:?}"
);
}
if env::var_os("CPA_PATH_ISOLATION_CHILD").is_none() {
let outside = home.join("inherited-environment");
let mut command = Command::new(env::current_exe().unwrap());
command.args([
"--exact",
"tests::agent_paths::configuration_paths_ignore_inherited_environment",
"--nocapture",
]);
command.env("CPA_PATH_ISOLATION_CHILD", "1");
for variable in [
"LOCALAPPDATA",
"XDG_CONFIG_HOME",
"CODEX_HOME",
"HERMES_HOME",
"OPENCODE_CONFIG",
"KIMI_CODE_HOME",
"GROK_HOME",
"DSH_HOME",
"PI_CODING_AGENT_DIR",
] {
command.env(variable, &outside);
}
configure_background_command(&mut command);
let output = command.output().unwrap();
assert!(
output.status.success(),
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(!outside.exists());
}
fs::remove_dir_all(home).unwrap();
}