From 2d60d3b2e1208a98e13b6192dcf7b82ddf1cb279 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 5 Jul 2026 16:29:33 -0400 Subject: [PATCH 1/3] ci: add full Rust PR validation Signed-off-by: Timothy Wayne Gregg --- .github/workflows/rust-ci.yml | 64 +++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/rust-ci.yml diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml new file mode 100644 index 00000000..4143024e --- /dev/null +++ b/.github/workflows/rust-ci.yml @@ -0,0 +1,64 @@ +name: Rust CI + +on: + pull_request: + paths: + - 'src-rust/**' + - '.github/workflows/rust-ci.yml' + workflow_dispatch: + +concurrency: + group: rust-ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + +jobs: + rust: + name: Format, lint, and test + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v5 + with: + persist-credentials: false + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libasound2-dev pkg-config + + - name: Cache cargo registry and build output + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + src-rust/target + key: rust-ci-cargo-${{ runner.os }}-${{ hashFiles('src-rust/Cargo.lock') }} + restore-keys: rust-ci-cargo-${{ runner.os }}- + + - name: Check formatting + working-directory: src-rust + run: cargo fmt --all -- --check + + - name: Check workspace + working-directory: src-rust + run: cargo check --workspace --locked + + - name: Run Clippy + working-directory: src-rust + run: cargo clippy --workspace --all-targets --locked -- -D warnings + + - name: Run tests + working-directory: src-rust + run: cargo test --workspace --locked --quiet From c05b5298f9b14504b522b4eabd28ed77376b4275 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 5 Jul 2026 16:52:14 -0400 Subject: [PATCH 2/3] test: stabilize Windows Rust CI validation Signed-off-by: Timothy Wayne Gregg --- src-rust/crates/commands/src/lib.rs | 7 ++++ .../crates/commands/src/named_commands.rs | 4 +- .../crates/core/src/anthropic_cli_import.rs | 13 ++++++- src-rust/crates/core/src/claudemd.rs | 39 ++++++++++++++++++- src-rust/crates/core/src/coven_daemon.rs | 3 ++ src-rust/crates/core/src/import_config.rs | 27 ++++++++++++- src-rust/crates/core/src/lib.rs | 21 ++++++++++ src-rust/crates/core/src/roster_reset.rs | 20 +++++++++- src-rust/crates/tui/src/app.rs | 14 +++---- src-rust/crates/tui/src/image_paste.rs | 9 +++-- 10 files changed, 138 insertions(+), 19 deletions(-) diff --git a/src-rust/crates/commands/src/lib.rs b/src-rust/crates/commands/src/lib.rs index cb24161a..e0f3b6ff 100644 --- a/src-rust/crates/commands/src/lib.rs +++ b/src-rust/crates/commands/src/lib.rs @@ -10363,6 +10363,7 @@ pub(crate) mod test_env { pub(crate) struct CommandEnvGuard { old_home: Option, + old_userprofile: Option, old_coven_home: Option, old_user: Option, old_username: Option, @@ -10376,6 +10377,7 @@ pub(crate) mod test_env { let lock = ENV_LOCK.lock().unwrap_or_else(|err| err.into_inner()); let guard = Self { old_home: std::env::var("HOME").ok(), + old_userprofile: std::env::var("USERPROFILE").ok(), old_coven_home: std::env::var("COVEN_HOME").ok(), old_user: std::env::var("USER").ok(), old_username: std::env::var("USERNAME").ok(), @@ -10384,6 +10386,7 @@ pub(crate) mod test_env { _lock: lock, }; std::env::set_var("HOME", home); + std::env::set_var("USERPROFILE", home); std::env::set_var("COVEN_HOME", coven_home); match user { Some(value) => std::env::set_var("USER", value), @@ -10419,6 +10422,10 @@ pub(crate) mod test_env { Some(value) => std::env::set_var("COVEN_HOME", value), None => std::env::remove_var("COVEN_HOME"), } + match &self.old_userprofile { + Some(value) => std::env::set_var("USERPROFILE", value), + None => std::env::remove_var("USERPROFILE"), + } match &self.old_user { Some(value) => std::env::set_var("USER", value), None => std::env::remove_var("USER"), diff --git a/src-rust/crates/commands/src/named_commands.rs b/src-rust/crates/commands/src/named_commands.rs index c16c28d4..0bdbe921 100644 --- a/src-rust/crates/commands/src/named_commands.rs +++ b/src-rust/crates/commands/src/named_commands.rs @@ -1012,12 +1012,12 @@ mod tests { let home = temp.path().join("home"); let coven_home = temp.path().join("coven"); let project = temp.path().join("project"); - let global_agents = home.join(".coven-code").join("agents"); let project_agents = project.join(".coven-code").join("agents"); - std::fs::create_dir_all(&global_agents).expect("global agents dir"); std::fs::create_dir_all(&project_agents).expect("project agents dir"); std::fs::create_dir_all(&coven_home).expect("coven home"); let _guard = CommandEnvGuard::set(&home, &coven_home, None); + let global_agents = claurst_core::Settings::config_dir().join("agents"); + std::fs::create_dir_all(&global_agents).expect("global agents dir"); let global_agent = global_agents.join("global.md"); let project_agent = project_agents.join("project.md"); diff --git a/src-rust/crates/core/src/anthropic_cli_import.rs b/src-rust/crates/core/src/anthropic_cli_import.rs index 9eaddd93..36428bd7 100644 --- a/src-rust/crates/core/src/anthropic_cli_import.rs +++ b/src-rust/crates/core/src/anthropic_cli_import.rs @@ -47,7 +47,18 @@ pub struct DiscoveredCredential { // --------------------------------------------------------------------------- fn claude_code_credentials_path() -> Option { - Some(dirs::home_dir()?.join(".claude").join(".credentials.json")) + Some(cli_home_dir()?.join(".claude").join(".credentials.json")) +} + +fn cli_home_dir() -> Option { + #[cfg(test)] + if let Ok(home) = std::env::var("COVEN_CODE_TEST_HOME") { + if !home.is_empty() { + return Some(PathBuf::from(home)); + } + } + + dirs::home_dir() } fn ant_credentials_dir() -> Option { diff --git a/src-rust/crates/core/src/claudemd.rs b/src-rust/crates/core/src/claudemd.rs index 9459b3cc..0602310f 100644 --- a/src-rust/crates/core/src/claudemd.rs +++ b/src-rust/crates/core/src/claudemd.rs @@ -83,6 +83,17 @@ impl MemoryLoadOptions { } } +fn memory_home_dir() -> Option { + #[cfg(test)] + if let Ok(path) = std::env::var("COVEN_CODE_TEST_HOME") { + if !path.is_empty() { + return Some(PathBuf::from(path)); + } + } + + dirs::home_dir() +} + // --------------------------------------------------------------------------- // Cache // --------------------------------------------------------------------------- @@ -172,7 +183,7 @@ pub fn expand_includes( let path_str = path_str.trim(); // Resolve relative to base_dir; expand ~ to home dir. let include_path = if path_str.starts_with('~') { - dirs::home_dir().unwrap_or_default().join(&path_str[2..]) + memory_home_dir().unwrap_or_default().join(&path_str[2..]) } else if Path::new(path_str).is_absolute() { PathBuf::from(path_str) } else { @@ -284,7 +295,7 @@ pub fn load_all_memory_files_with_options( let mut files = Vec::new(); // 1. Managed: ~/.coven-code/rules/*.md - if let Some(home) = dirs::home_dir() { + if let Some(home) = memory_home_dir() { if options.allow_managed_rules { let rules_dir = home.join(".coven-code/rules"); if let Ok(entries) = std::fs::read_dir(&rules_dir) { @@ -412,8 +423,12 @@ mod tests { let _lock = crate::coven_shared::COVEN_HOME_ENV_LOCK .lock() .unwrap_or_else(|err| err.into_inner()); + let original_test_home = std::env::var("COVEN_CODE_TEST_HOME").ok(); let original_home = std::env::var("HOME").ok(); + let original_userprofile = std::env::var("USERPROFILE").ok(); + std::env::set_var("COVEN_CODE_TEST_HOME", home.path()); std::env::set_var("HOME", home.path()); + std::env::set_var("USERPROFILE", home.path()); let files = load_all_memory_files_with_options(project.path(), &MemoryLoadOptions::hosted_review()); @@ -422,6 +437,14 @@ mod tests { Some(value) => std::env::set_var("HOME", value), None => std::env::remove_var("HOME"), } + match original_test_home { + Some(value) => std::env::set_var("COVEN_CODE_TEST_HOME", value), + None => std::env::remove_var("COVEN_CODE_TEST_HOME"), + } + match original_userprofile { + Some(value) => std::env::set_var("USERPROFILE", value), + None => std::env::remove_var("USERPROFILE"), + } assert!(files.iter().all(|file| file.scope != MemoryScope::User)); assert!(files.iter().any(|file| { @@ -442,8 +465,12 @@ mod tests { let _lock = crate::coven_shared::COVEN_HOME_ENV_LOCK .lock() .unwrap_or_else(|err| err.into_inner()); + let original_test_home = std::env::var("COVEN_CODE_TEST_HOME").ok(); let original_home = std::env::var("HOME").ok(); + let original_userprofile = std::env::var("USERPROFILE").ok(); + std::env::set_var("COVEN_CODE_TEST_HOME", home.path()); std::env::set_var("HOME", home.path()); + std::env::set_var("USERPROFILE", home.path()); let files = load_all_memory_files_with_options(project.path(), &MemoryLoadOptions::local()); @@ -451,6 +478,14 @@ mod tests { Some(value) => std::env::set_var("HOME", value), None => std::env::remove_var("HOME"), } + match original_test_home { + Some(value) => std::env::set_var("COVEN_CODE_TEST_HOME", value), + None => std::env::remove_var("COVEN_CODE_TEST_HOME"), + } + match original_userprofile { + Some(value) => std::env::set_var("USERPROFILE", value), + None => std::env::remove_var("USERPROFILE"), + } assert!(files .iter() diff --git a/src-rust/crates/core/src/coven_daemon.rs b/src-rust/crates/core/src/coven_daemon.rs index da03183e..28231f10 100644 --- a/src-rust/crates/core/src/coven_daemon.rs +++ b/src-rust/crates/core/src/coven_daemon.rs @@ -771,6 +771,7 @@ fn url_quote(input: &str) -> String { mod tests { use super::*; use crate::coven_shared::COVEN_HOME_ENV_LOCK; + #[cfg(unix)] use std::fs; /// Guard that temporarily sets `COVEN_HOME` and restores it on drop. @@ -805,6 +806,7 @@ mod tests { assert!(DaemonClient::new().is_none()); } + #[cfg(unix)] #[test] fn new_returns_some_when_sock_present() { let _lock = COVEN_HOME_ENV_LOCK @@ -873,6 +875,7 @@ mod tests { assert_eq!(s1.active_sessions, 0); } + #[cfg(unix)] #[test] fn familiar_statuses_returns_offline_when_connect_fails() { let _lock = COVEN_HOME_ENV_LOCK diff --git a/src-rust/crates/core/src/import_config.rs b/src-rust/crates/core/src/import_config.rs index 6f58ffff..a95a4080 100644 --- a/src-rust/crates/core/src/import_config.rs +++ b/src-rust/crates/core/src/import_config.rs @@ -30,7 +30,7 @@ pub struct ImportPaths { impl ImportPaths { pub fn detect() -> Self { - let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); + let home = import_home_dir(); let claude_dir = home.join(".claude"); let claurst_dir = Settings::config_dir(); Self { @@ -42,6 +42,17 @@ impl ImportPaths { } } +fn import_home_dir() -> PathBuf { + #[cfg(test)] + if let Ok(home) = std::env::var("COVEN_CODE_TEST_HOME") { + if !home.is_empty() { + return PathBuf::from(home); + } + } + + dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")) +} + #[derive(Debug, Clone)] pub struct FilePlan { pub source_path: PathBuf, @@ -654,7 +665,11 @@ mod tests { .unwrap(); let old_home = std::env::var("HOME").ok(); + let old_test_home = std::env::var("COVEN_CODE_TEST_HOME").ok(); + let old_userprofile = std::env::var("USERPROFILE").ok(); std::env::set_var("HOME", home); + std::env::set_var("COVEN_CODE_TEST_HOME", home); + std::env::set_var("USERPROFILE", home); let preview = build_import_preview(ImportSelection::Both).unwrap(); assert!(preview.claude_md.is_some()); @@ -682,6 +697,16 @@ mod tests { } else { std::env::remove_var("HOME"); } + if let Some(old) = old_test_home { + std::env::set_var("COVEN_CODE_TEST_HOME", old); + } else { + std::env::remove_var("COVEN_CODE_TEST_HOME"); + } + if let Some(old) = old_userprofile { + std::env::set_var("USERPROFILE", old); + } else { + std::env::remove_var("USERPROFILE"); + } } #[test] diff --git a/src-rust/crates/core/src/lib.rs b/src-rust/crates/core/src/lib.rs index c12650c0..030b27bf 100644 --- a/src-rust/crates/core/src/lib.rs +++ b/src-rust/crates/core/src/lib.rs @@ -1459,6 +1459,13 @@ pub mod config { impl Settings { /// The per-user configuration directory (`~/.coven-code`). pub fn config_dir() -> PathBuf { + #[cfg(test)] + if let Ok(home) = std::env::var("COVEN_CODE_TEST_HOME") { + if !home.is_empty() { + return PathBuf::from(home).join(".coven-code"); + } + } + dirs::home_dir() .unwrap_or_else(|| PathBuf::from(".")) .join(".coven-code") @@ -4940,6 +4947,8 @@ mod tests { fn test_imported_anthropic_cli_token_resolves_without_coven_oauth_client() { struct EnvRestore { home: Option, + test_home: Option, + userprofile: Option, api_key: Option, client_id: Option, } @@ -4950,6 +4959,14 @@ mod tests { Some(value) => std::env::set_var("HOME", value), None => std::env::remove_var("HOME"), } + match self.test_home.take() { + Some(value) => std::env::set_var("COVEN_CODE_TEST_HOME", value), + None => std::env::remove_var("COVEN_CODE_TEST_HOME"), + } + match self.userprofile.take() { + Some(value) => std::env::set_var("USERPROFILE", value), + None => std::env::remove_var("USERPROFILE"), + } match self.api_key.take() { Some(value) => std::env::set_var("ANTHROPIC_API_KEY", value), None => std::env::remove_var("ANTHROPIC_API_KEY"), @@ -4974,10 +4991,14 @@ mod tests { let temp_home = tempfile::tempdir().expect("temp home"); let _restore = EnvRestore { home: std::env::var("HOME").ok(), + test_home: std::env::var("COVEN_CODE_TEST_HOME").ok(), + userprofile: std::env::var("USERPROFILE").ok(), api_key: std::env::var("ANTHROPIC_API_KEY").ok(), client_id: std::env::var(crate::oauth::CLIENT_ID_ENV).ok(), }; std::env::set_var("HOME", temp_home.path()); + std::env::set_var("COVEN_CODE_TEST_HOME", temp_home.path()); + std::env::set_var("USERPROFILE", temp_home.path()); std::env::remove_var("ANTHROPIC_API_KEY"); std::env::remove_var(crate::oauth::CLIENT_ID_ENV); diff --git a/src-rust/crates/core/src/roster_reset.rs b/src-rust/crates/core/src/roster_reset.rs index bfe22f53..2324b2eb 100644 --- a/src-rust/crates/core/src/roster_reset.rs +++ b/src-rust/crates/core/src/roster_reset.rs @@ -139,17 +139,25 @@ mod tests { struct HomeGuard { old_home: Option, + old_test_home: Option, + old_userprofile: Option, old_coven_home: Option, } impl HomeGuard { fn set(home: &Path, coven_home: &Path) -> Self { let old_home = std::env::var("HOME").ok(); + let old_test_home = std::env::var("COVEN_CODE_TEST_HOME").ok(); + let old_userprofile = std::env::var("USERPROFILE").ok(); let old_coven_home = std::env::var("COVEN_HOME").ok(); std::env::set_var("HOME", home); + std::env::set_var("COVEN_CODE_TEST_HOME", home); + std::env::set_var("USERPROFILE", home); std::env::set_var("COVEN_HOME", coven_home); Self { old_home, + old_test_home, + old_userprofile, old_coven_home, } } @@ -161,6 +169,14 @@ mod tests { Some(value) => std::env::set_var("HOME", value), None => std::env::remove_var("HOME"), } + match &self.old_test_home { + Some(value) => std::env::set_var("COVEN_CODE_TEST_HOME", value), + None => std::env::remove_var("COVEN_CODE_TEST_HOME"), + } + match &self.old_userprofile { + Some(value) => std::env::set_var("USERPROFILE", value), + None => std::env::remove_var("USERPROFILE"), + } match &self.old_coven_home { Some(value) => std::env::set_var("COVEN_HOME", value), None => std::env::remove_var("COVEN_HOME"), @@ -204,12 +220,12 @@ mod tests { let home = temp.path().join("home"); let coven_home = temp.path().join("coven"); let project = temp.path().join("project"); - let global_agents = home.join(".coven-code").join("agents"); let project_agents = project.join(".coven-code").join("agents"); - std::fs::create_dir_all(&global_agents).expect("global agents dir"); std::fs::create_dir_all(&project_agents).expect("project agents dir"); std::fs::create_dir_all(&coven_home).expect("coven home"); let _guard = HomeGuard::set(&home, &coven_home); + let global_agents = Settings::config_dir().join("agents"); + std::fs::create_dir_all(&global_agents).expect("global agents dir"); std::fs::write(global_agents.join("global.md"), "global").expect("global agent"); std::fs::write(global_agents.join("README.txt"), "keep").expect("global keep"); diff --git a/src-rust/crates/tui/src/app.rs b/src-rust/crates/tui/src/app.rs index c53f661c..f4e5e60e 100644 --- a/src-rust/crates/tui/src/app.rs +++ b/src-rust/crates/tui/src/app.rs @@ -7420,22 +7420,22 @@ role = "Research" #[test] fn windows_system_root_prefers_absolute_system_root() { - let root = windows_system_root_from_env( - Some(std::ffi::OsString::from("/windows")), - Some(std::ffi::OsString::from("/windir")), - ); + let system_root = std::env::temp_dir().join("windows-root").into_os_string(); + let windir = std::env::temp_dir().join("windir").into_os_string(); + let root = windows_system_root_from_env(Some(system_root.clone()), Some(windir)); - assert_eq!(root, std::ffi::OsString::from("/windows")); + assert_eq!(root, system_root); } #[test] fn windows_system_root_falls_back_to_absolute_windir() { + let windir = std::env::temp_dir().join("windir").into_os_string(); let root = windows_system_root_from_env( Some(std::ffi::OsString::from("relative-system-root")), - Some(std::ffi::OsString::from("/windir")), + Some(windir.clone()), ); - assert_eq!(root, std::ffi::OsString::from("/windir")); + assert_eq!(root, windir); } #[test] diff --git a/src-rust/crates/tui/src/image_paste.rs b/src-rust/crates/tui/src/image_paste.rs index 043436b2..d35e1a01 100644 --- a/src-rust/crates/tui/src/image_paste.rs +++ b/src-rust/crates/tui/src/image_paste.rs @@ -10,7 +10,9 @@ // Linux : xclip / wl-paste // Windows: PowerShell Get-Clipboard -use std::path::{Path, PathBuf}; +#[cfg(not(target_os = "windows"))] +use std::path::Path; +use std::path::PathBuf; use std::process::Command; #[cfg(not(target_os = "windows"))] @@ -425,14 +427,13 @@ fn write_text_windows_w(text: &str) -> bool { use std::io::Write; use std::process::Stdio; // PowerShell Set-Clipboard reads from stdin via pipe - let script = - format!("[Console]::InputEncoding = [System.Text.Encoding]::UTF8; $input | Set-Clipboard"); + let script = "[Console]::InputEncoding = [System.Text.Encoding]::UTF8; $input | Set-Clipboard"; let powershell = match trusted_windows_powershell() { Some(path) => path, None => return false, }; let mut child = match Command::new(powershell) - .args(["-NoProfile", "-Command", &script]) + .args(["-NoProfile", "-Command", script]) .stdin(Stdio::piped()) .spawn() { From 66bf26b822119b0f2eda8cc0ea9143d12143a7e4 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Sun, 5 Jul 2026 17:00:02 -0400 Subject: [PATCH 3/3] fix(tui): satisfy Linux clippy in image paste Signed-off-by: Timothy Wayne Gregg --- src-rust/crates/tui/src/image_paste.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src-rust/crates/tui/src/image_paste.rs b/src-rust/crates/tui/src/image_paste.rs index d35e1a01..200a60e2 100644 --- a/src-rust/crates/tui/src/image_paste.rs +++ b/src-rust/crates/tui/src/image_paste.rs @@ -306,10 +306,11 @@ fn try_save_linux_image(path: &PathBuf) -> bool { .args(["-selection", "clipboard", "-t", "image/png", "-o"]) .output() { - if out.status.success() && !out.stdout.is_empty() { - if std::fs::write(path, &out.stdout).is_ok() { - return true; - } + if out.status.success() + && !out.stdout.is_empty() + && std::fs::write(path, &out.stdout).is_ok() + { + return true; } } } @@ -320,10 +321,11 @@ fn try_save_linux_image(path: &PathBuf) -> bool { .args(["--type", "image/png"]) .output() { - if out.status.success() && !out.stdout.is_empty() { - if std::fs::write(path, &out.stdout).is_ok() { - return true; - } + if out.status.success() + && !out.stdout.is_empty() + && std::fs::write(path, &out.stdout).is_ok() + { + return true; } } }