Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions src-rust/crates/commands/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10363,6 +10363,7 @@ pub(crate) mod test_env {

pub(crate) struct CommandEnvGuard {
old_home: Option<String>,
old_userprofile: Option<String>,
old_coven_home: Option<String>,
old_user: Option<String>,
old_username: Option<String>,
Expand All @@ -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(),
Expand All @@ -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),
Expand Down Expand Up @@ -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"),
Expand Down
4 changes: 2 additions & 2 deletions src-rust/crates/commands/src/named_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
13 changes: 12 additions & 1 deletion src-rust/crates/core/src/anthropic_cli_import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,18 @@ pub struct DiscoveredCredential {
// ---------------------------------------------------------------------------

fn claude_code_credentials_path() -> Option<PathBuf> {
Some(dirs::home_dir()?.join(".claude").join(".credentials.json"))
Some(cli_home_dir()?.join(".claude").join(".credentials.json"))
}

fn cli_home_dir() -> Option<PathBuf> {
#[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<PathBuf> {
Expand Down
39 changes: 37 additions & 2 deletions src-rust/crates/core/src/claudemd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,17 @@ impl MemoryLoadOptions {
}
}

fn memory_home_dir() -> Option<PathBuf> {
#[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
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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());
Expand All @@ -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| {
Expand All @@ -442,15 +465,27 @@ 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());

match original_home {
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()
Expand Down
3 changes: 3 additions & 0 deletions src-rust/crates/core/src/coven_daemon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
27 changes: 26 additions & 1 deletion src-rust/crates/core/src/import_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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]
Expand Down
21 changes: 21 additions & 0 deletions src-rust/crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -4940,6 +4947,8 @@ mod tests {
fn test_imported_anthropic_cli_token_resolves_without_coven_oauth_client() {
struct EnvRestore {
home: Option<String>,
test_home: Option<String>,
userprofile: Option<String>,
api_key: Option<String>,
client_id: Option<String>,
}
Expand All @@ -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"),
Expand All @@ -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);

Expand Down
Loading