chore: Remove blocking read strings

This commit is contained in:
Jayson Reis 2026-02-18 15:52:07 +00:00 committed by Chummy
parent bc0be9a3c1
commit b9af601943
26 changed files with 331 additions and 243 deletions

View file

@ -626,8 +626,8 @@ mod tests {
assert!(!token_set.is_expiring_within(Duration::from_secs(1))); assert!(!token_set.is_expiring_within(Duration::from_secs(1)));
} }
#[test] #[tokio::test]
fn store_roundtrip_with_encryption() { async fn store_roundtrip_with_encryption() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let store = AuthProfilesStore::new(tmp.path(), true); let store = AuthProfilesStore::new(tmp.path(), true);
@ -661,14 +661,14 @@ mod tests {
Some("refresh-123") Some("refresh-123")
); );
let raw = fs::read_to_string(store.path()).unwrap(); let raw = tokio::fs::read_to_string(store.path()).await.unwrap();
assert!(raw.contains("enc2:")); assert!(raw.contains("enc2:"));
assert!(!raw.contains("refresh-123")); assert!(!raw.contains("refresh-123"));
assert!(!raw.contains("access-123")); assert!(!raw.contains("access-123"));
} }
#[test] #[tokio::test]
fn atomic_write_replaces_file() { async fn atomic_write_replaces_file() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let store = AuthProfilesStore::new(tmp.path(), false); let store = AuthProfilesStore::new(tmp.path(), false);
@ -678,7 +678,7 @@ mod tests {
let path = store.path().to_path_buf(); let path = store.path().to_path_buf();
assert!(path.exists()); assert!(path.exists());
let contents = fs::read_to_string(path).unwrap(); let contents = tokio::fs::read_to_string(path).await.unwrap();
assert!(contents.contains("\"schema_version\": 1")); assert!(contents.contains("\"schema_version\": 1"));
} }
} }

View file

@ -3385,8 +3385,8 @@ tool_dispatcher = "xml"
assert_eq!(parsed.agent.tool_dispatcher, "xml"); assert_eq!(parsed.agent.tool_dispatcher, "xml");
} }
#[test] #[tokio::test]
fn config_save_and_load_tmpdir() { async fn config_save_and_load_tmpdir() {
let dir = std::env::temp_dir().join("zeroclaw_test_config"); let dir = std::env::temp_dir().join("zeroclaw_test_config");
let _ = fs::remove_dir_all(&dir); let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap(); fs::create_dir_all(&dir).unwrap();
@ -3431,7 +3431,7 @@ tool_dispatcher = "xml"
config.save().unwrap(); config.save().unwrap();
assert!(config_path.exists()); assert!(config_path.exists());
let contents = fs::read_to_string(&config_path).unwrap(); let contents = tokio::fs::read_to_string(&config_path).await.unwrap();
let loaded: Config = toml::from_str(&contents).unwrap(); let loaded: Config = toml::from_str(&contents).unwrap();
assert!(loaded assert!(loaded
.api_key .api_key
@ -3446,8 +3446,8 @@ tool_dispatcher = "xml"
let _ = fs::remove_dir_all(&dir); let _ = fs::remove_dir_all(&dir);
} }
#[test] #[tokio::test]
fn config_save_encrypts_nested_credentials() { async fn config_save_encrypts_nested_credentials() {
let dir = std::env::temp_dir().join(format!( let dir = std::env::temp_dir().join(format!(
"zeroclaw_test_nested_credentials_{}", "zeroclaw_test_nested_credentials_{}",
uuid::Uuid::new_v4() uuid::Uuid::new_v4()
@ -3477,7 +3477,9 @@ tool_dispatcher = "xml"
config.save().unwrap(); config.save().unwrap();
let contents = fs::read_to_string(config.config_path.clone()).unwrap(); let contents = tokio::fs::read_to_string(config.config_path.clone())
.await
.unwrap();
let stored: Config = toml::from_str(&contents).unwrap(); let stored: Config = toml::from_str(&contents).unwrap();
let store = crate::security::SecretStore::new(&dir, true); let store = crate::security::SecretStore::new(&dir, true);
@ -3527,8 +3529,8 @@ tool_dispatcher = "xml"
let _ = fs::remove_dir_all(&dir); let _ = fs::remove_dir_all(&dir);
} }
#[test] #[tokio::test]
fn config_save_atomic_cleanup() { async fn config_save_atomic_cleanup() {
let dir = let dir =
std::env::temp_dir().join(format!("zeroclaw_test_config_{}", uuid::Uuid::new_v4())); std::env::temp_dir().join(format!("zeroclaw_test_config_{}", uuid::Uuid::new_v4()));
fs::create_dir_all(&dir).unwrap(); fs::create_dir_all(&dir).unwrap();
@ -3545,7 +3547,7 @@ tool_dispatcher = "xml"
config.default_model = Some("model-b".into()); config.default_model = Some("model-b".into());
config.save().unwrap(); config.save().unwrap();
let contents = fs::read_to_string(&config_path).unwrap(); let contents = tokio::fs::read_to_string(&config_path).await.unwrap();
assert!(contents.contains("model-b")); assert!(contents.contains("model-b"));
let names: Vec<String> = fs::read_dir(&dir) let names: Vec<String> = fs::read_dir(&dir)

View file

@ -475,13 +475,13 @@ mod tests {
use chrono::{Duration as ChronoDuration, Utc}; use chrono::{Duration as ChronoDuration, Utc};
use tempfile::TempDir; use tempfile::TempDir;
fn test_config(tmp: &TempDir) -> Config { async fn test_config(tmp: &TempDir) -> Config {
let config = Config { let config = Config {
workspace_dir: tmp.path().join("workspace"), workspace_dir: tmp.path().join("workspace"),
config_path: tmp.path().join("config.toml"), config_path: tmp.path().join("config.toml"),
..Config::default() ..Config::default()
}; };
std::fs::create_dir_all(&config.workspace_dir).unwrap(); tokio::fs::create_dir_all(&config.workspace_dir).await.unwrap();
config config
} }
@ -513,7 +513,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn run_job_command_success() { async fn run_job_command_success() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let config = test_config(&tmp); let config = test_config(&tmp).await;
let job = test_job("echo scheduler-ok"); let job = test_job("echo scheduler-ok");
let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir); let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir);
@ -526,7 +526,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn run_job_command_failure() { async fn run_job_command_failure() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let config = test_config(&tmp); let config = test_config(&tmp).await;
let job = test_job("ls definitely_missing_file_for_scheduler_test"); let job = test_job("ls definitely_missing_file_for_scheduler_test");
let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir); let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir);
@ -539,7 +539,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn run_job_command_times_out() { async fn run_job_command_times_out() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let mut config = test_config(&tmp); let mut config = test_config(&tmp).await;
config.autonomy.allowed_commands = vec!["sleep".into()]; config.autonomy.allowed_commands = vec!["sleep".into()];
let job = test_job("sleep 1"); let job = test_job("sleep 1");
let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir); let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir);
@ -553,7 +553,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn run_job_command_blocks_disallowed_command() { async fn run_job_command_blocks_disallowed_command() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let mut config = test_config(&tmp); let mut config = test_config(&tmp).await;
config.autonomy.allowed_commands = vec!["echo".into()]; config.autonomy.allowed_commands = vec!["echo".into()];
let job = test_job("curl https://evil.example"); let job = test_job("curl https://evil.example");
let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir); let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir);
@ -567,7 +567,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn run_job_command_blocks_forbidden_path_argument() { async fn run_job_command_blocks_forbidden_path_argument() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let mut config = test_config(&tmp); let mut config = test_config(&tmp).await;
config.autonomy.allowed_commands = vec!["cat".into()]; config.autonomy.allowed_commands = vec!["cat".into()];
let job = test_job("cat /etc/passwd"); let job = test_job("cat /etc/passwd");
let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir); let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir);
@ -582,7 +582,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn run_job_command_blocks_readonly_mode() { async fn run_job_command_blocks_readonly_mode() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let mut config = test_config(&tmp); let mut config = test_config(&tmp).await;
config.autonomy.level = crate::security::AutonomyLevel::ReadOnly; config.autonomy.level = crate::security::AutonomyLevel::ReadOnly;
let job = test_job("echo should-not-run"); let job = test_job("echo should-not-run");
let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir); let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir);
@ -596,7 +596,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn run_job_command_blocks_rate_limited() { async fn run_job_command_blocks_rate_limited() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let mut config = test_config(&tmp); let mut config = test_config(&tmp).await;
config.autonomy.max_actions_per_hour = 0; config.autonomy.max_actions_per_hour = 0;
let job = test_job("echo should-not-run"); let job = test_job("echo should-not-run");
let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir); let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir);
@ -610,16 +610,17 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn execute_job_with_retry_recovers_after_first_failure() { async fn execute_job_with_retry_recovers_after_first_failure() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let mut config = test_config(&tmp); let mut config = test_config(&tmp).await;
config.reliability.scheduler_retries = 1; config.reliability.scheduler_retries = 1;
config.reliability.provider_backoff_ms = 1; config.reliability.provider_backoff_ms = 1;
config.autonomy.allowed_commands = vec!["sh".into()]; config.autonomy.allowed_commands = vec!["sh".into()];
let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir); let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir);
std::fs::write( tokio::fs::write(
config.workspace_dir.join("retry-once.sh"), config.workspace_dir.join("retry-once.sh"),
"#!/bin/sh\nif [ -f retry-ok.flag ]; then\n echo recovered\n exit 0\nfi\ntouch retry-ok.flag\nexit 1\n", "#!/bin/sh\nif [ -f retry-ok.flag ]; then\n echo recovered\n exit 0\nfi\ntouch retry-ok.flag\nexit 1\n",
) )
.await
.unwrap(); .unwrap();
let job = test_job("sh ./retry-once.sh"); let job = test_job("sh ./retry-once.sh");
@ -631,7 +632,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn execute_job_with_retry_exhausts_attempts() { async fn execute_job_with_retry_exhausts_attempts() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let mut config = test_config(&tmp); let mut config = test_config(&tmp).await;
config.reliability.scheduler_retries = 1; config.reliability.scheduler_retries = 1;
config.reliability.provider_backoff_ms = 1; config.reliability.provider_backoff_ms = 1;
let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir); let security = SecurityPolicy::from_config(&config.autonomy, &config.workspace_dir);
@ -646,7 +647,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn run_agent_job_returns_error_without_provider_key() { async fn run_agent_job_returns_error_without_provider_key() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let config = test_config(&tmp); let config = test_config(&tmp).await;
let mut job = test_job(""); let mut job = test_job("");
job.job_type = JobType::Agent; job.job_type = JobType::Agent;
job.prompt = Some("Say hello".into()); job.prompt = Some("Say hello".into());
@ -662,7 +663,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn persist_job_result_records_run_and_reschedules_shell_job() { async fn persist_job_result_records_run_and_reschedules_shell_job() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let config = test_config(&tmp); let config = test_config(&tmp).await;
let job = cron::add_job(&config, "*/5 * * * *", "echo ok").unwrap(); let job = cron::add_job(&config, "*/5 * * * *", "echo ok").unwrap();
let started = Utc::now(); let started = Utc::now();
let finished = started + ChronoDuration::milliseconds(10); let finished = started + ChronoDuration::milliseconds(10);
@ -679,7 +680,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn persist_job_result_success_deletes_one_shot() { async fn persist_job_result_success_deletes_one_shot() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let config = test_config(&tmp); let config = test_config(&tmp).await;
let at = Utc::now() + ChronoDuration::minutes(10); let at = Utc::now() + ChronoDuration::minutes(10);
let job = cron::add_agent_job( let job = cron::add_agent_job(
&config, &config,
@ -704,7 +705,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn persist_job_result_failure_disables_one_shot() { async fn persist_job_result_failure_disables_one_shot() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let config = test_config(&tmp); let config = test_config(&tmp).await;
let at = Utc::now() + ChronoDuration::minutes(10); let at = Utc::now() + ChronoDuration::minutes(10);
let job = cron::add_agent_job( let job = cron::add_agent_job(
&config, &config,
@ -730,7 +731,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn deliver_if_configured_handles_none_and_invalid_channel() { async fn deliver_if_configured_handles_none_and_invalid_channel() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let config = test_config(&tmp); let config = test_config(&tmp).await;
let mut job = test_job("echo ok"); let mut job = test_job("echo ok");
assert!(deliver_if_configured(&config, &job, "x").await.is_ok()); assert!(deliver_if_configured(&config, &job, "x").await.is_ok());

View file

@ -1381,8 +1381,8 @@ mod tests {
assert_eq!(normalize_max_keys(1, 10_000), 1); assert_eq!(normalize_max_keys(1, 10_000), 1);
} }
#[test] #[tokio::test]
fn persist_pairing_tokens_writes_config_tokens() { async fn persist_pairing_tokens_writes_config_tokens() {
let temp = tempfile::tempdir().unwrap(); let temp = tempfile::tempdir().unwrap();
let config_path = temp.path().join("config.toml"); let config_path = temp.path().join("config.toml");
let workspace_path = temp.path().join("workspace"); let workspace_path = temp.path().join("workspace");
@ -1400,7 +1400,7 @@ mod tests {
let shared_config = Arc::new(Mutex::new(config)); let shared_config = Arc::new(Mutex::new(config));
persist_pairing_tokens(&shared_config, &guard).unwrap(); persist_pairing_tokens(&shared_config, &guard).unwrap();
let saved = std::fs::read_to_string(config_path).unwrap(); let saved = tokio::fs::read_to_string(config_path).await.unwrap();
let parsed: Config = toml::from_str(&saved).unwrap(); let parsed: Config = toml::from_str(&saved).unwrap();
assert_eq!(parsed.gateway.paired_tokens.len(), 1); assert_eq!(parsed.gateway.paired_tokens.len(), 1);
let persisted = &parsed.gateway.paired_tokens[0]; let persisted = &parsed.gateway.paired_tokens[0];

View file

@ -608,7 +608,7 @@ exit 1
.iter() .iter()
.any(|e| e.content.contains("Rust should stay local-first"))); .any(|e| e.content.contains("Rust should stay local-first")));
let context_calls = fs::read_to_string(&marker).unwrap_or_default(); let context_calls = tokio::fs::read_to_string(&marker).await.unwrap_or_default();
assert!( assert!(
context_calls.trim().is_empty(), context_calls.trim().is_empty(),
"Expected local-hit short-circuit; got calls: {context_calls}" "Expected local-hit short-circuit; got calls: {context_calls}"
@ -669,7 +669,7 @@ exit 1
assert!(first.is_empty()); assert!(first.is_empty());
assert!(second.is_empty()); assert!(second.is_empty());
let calls = fs::read_to_string(&marker).unwrap_or_default(); let calls = tokio::fs::read_to_string(&marker).await.unwrap_or_default();
assert_eq!(calls.lines().count(), 1); assert_eq!(calls.lines().count(), 1);
} }
} }

View file

@ -229,7 +229,6 @@ impl Memory for MarkdownMemory {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::fs as sync_fs;
use tempfile::TempDir; use tempfile::TempDir;
fn temp_workspace() -> (TempDir, MarkdownMemory) { fn temp_workspace() -> (TempDir, MarkdownMemory) {
@ -256,7 +255,7 @@ mod tests {
mem.store("pref", "User likes Rust", MemoryCategory::Core, None) mem.store("pref", "User likes Rust", MemoryCategory::Core, None)
.await .await
.unwrap(); .unwrap();
let content = sync_fs::read_to_string(mem.core_path()).unwrap(); let content = fs::read_to_string(mem.core_path()).await.unwrap();
assert!(content.contains("User likes Rust")); assert!(content.contains("User likes Rust"));
} }
@ -267,7 +266,7 @@ mod tests {
.await .await
.unwrap(); .unwrap();
let path = mem.daily_path(); let path = mem.daily_path();
let content = sync_fs::read_to_string(path).unwrap(); let content = fs::read_to_string(path).await.unwrap();
assert!(content.contains("Finished tests")); assert!(content.contains("Finished tests"));
} }

View file

@ -4537,8 +4537,8 @@ mod tests {
// ── scaffold_workspace: personalization ───────────────────── // ── scaffold_workspace: personalization ─────────────────────
#[test] #[tokio::test]
fn scaffold_bakes_user_name_into_files() { async fn scaffold_bakes_user_name_into_files() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let ctx = ProjectContext { let ctx = ProjectContext {
user_name: "Alice".into(), user_name: "Alice".into(),
@ -4546,21 +4546,25 @@ mod tests {
}; };
scaffold_workspace(tmp.path(), &ctx).unwrap(); scaffold_workspace(tmp.path(), &ctx).unwrap();
let user_md = fs::read_to_string(tmp.path().join("USER.md")).unwrap(); let user_md = tokio::fs::read_to_string(tmp.path().join("USER.md"))
.await
.unwrap();
assert!( assert!(
user_md.contains("**Name:** Alice"), user_md.contains("**Name:** Alice"),
"USER.md should contain user name" "USER.md should contain user name"
); );
let bootstrap = fs::read_to_string(tmp.path().join("BOOTSTRAP.md")).unwrap(); let bootstrap = tokio::fs::read_to_string(tmp.path().join("BOOTSTRAP.md"))
.await
.unwrap();
assert!( assert!(
bootstrap.contains("**Alice**"), bootstrap.contains("**Alice**"),
"BOOTSTRAP.md should contain user name" "BOOTSTRAP.md should contain user name"
); );
} }
#[test] #[tokio::test]
fn scaffold_bakes_timezone_into_files() { async fn scaffold_bakes_timezone_into_files() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let ctx = ProjectContext { let ctx = ProjectContext {
timezone: "US/Pacific".into(), timezone: "US/Pacific".into(),
@ -4568,21 +4572,25 @@ mod tests {
}; };
scaffold_workspace(tmp.path(), &ctx).unwrap(); scaffold_workspace(tmp.path(), &ctx).unwrap();
let user_md = fs::read_to_string(tmp.path().join("USER.md")).unwrap(); let user_md = tokio::fs::read_to_string(tmp.path().join("USER.md"))
.await
.unwrap();
assert!( assert!(
user_md.contains("**Timezone:** US/Pacific"), user_md.contains("**Timezone:** US/Pacific"),
"USER.md should contain timezone" "USER.md should contain timezone"
); );
let bootstrap = fs::read_to_string(tmp.path().join("BOOTSTRAP.md")).unwrap(); let bootstrap = tokio::fs::read_to_string(tmp.path().join("BOOTSTRAP.md"))
.await
.unwrap();
assert!( assert!(
bootstrap.contains("US/Pacific"), bootstrap.contains("US/Pacific"),
"BOOTSTRAP.md should contain timezone" "BOOTSTRAP.md should contain timezone"
); );
} }
#[test] #[tokio::test]
fn scaffold_bakes_agent_name_into_files() { async fn scaffold_bakes_agent_name_into_files() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let ctx = ProjectContext { let ctx = ProjectContext {
agent_name: "Crabby".into(), agent_name: "Crabby".into(),
@ -4590,39 +4598,49 @@ mod tests {
}; };
scaffold_workspace(tmp.path(), &ctx).unwrap(); scaffold_workspace(tmp.path(), &ctx).unwrap();
let identity = fs::read_to_string(tmp.path().join("IDENTITY.md")).unwrap(); let identity = tokio::fs::read_to_string(tmp.path().join("IDENTITY.md"))
.await
.unwrap();
assert!( assert!(
identity.contains("**Name:** Crabby"), identity.contains("**Name:** Crabby"),
"IDENTITY.md should contain agent name" "IDENTITY.md should contain agent name"
); );
let soul = fs::read_to_string(tmp.path().join("SOUL.md")).unwrap(); let soul = tokio::fs::read_to_string(tmp.path().join("SOUL.md"))
.await
.unwrap();
assert!( assert!(
soul.contains("You are **Crabby**"), soul.contains("You are **Crabby**"),
"SOUL.md should contain agent name" "SOUL.md should contain agent name"
); );
let agents = fs::read_to_string(tmp.path().join("AGENTS.md")).unwrap(); let agents = tokio::fs::read_to_string(tmp.path().join("AGENTS.md"))
.await
.unwrap();
assert!( assert!(
agents.contains("Crabby Personal Assistant"), agents.contains("Crabby Personal Assistant"),
"AGENTS.md should contain agent name" "AGENTS.md should contain agent name"
); );
let heartbeat = fs::read_to_string(tmp.path().join("HEARTBEAT.md")).unwrap(); let heartbeat = tokio::fs::read_to_string(tmp.path().join("HEARTBEAT.md"))
.await
.unwrap();
assert!( assert!(
heartbeat.contains("Crabby"), heartbeat.contains("Crabby"),
"HEARTBEAT.md should contain agent name" "HEARTBEAT.md should contain agent name"
); );
let bootstrap = fs::read_to_string(tmp.path().join("BOOTSTRAP.md")).unwrap(); let bootstrap = tokio::fs::read_to_string(tmp.path().join("BOOTSTRAP.md"))
.await
.unwrap();
assert!( assert!(
bootstrap.contains("Introduce yourself as Crabby"), bootstrap.contains("Introduce yourself as Crabby"),
"BOOTSTRAP.md should contain agent name" "BOOTSTRAP.md should contain agent name"
); );
} }
#[test] #[tokio::test]
fn scaffold_bakes_communication_style() { async fn scaffold_bakes_communication_style() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let ctx = ProjectContext { let ctx = ProjectContext {
communication_style: "Be technical and detailed.".into(), communication_style: "Be technical and detailed.".into(),
@ -4630,19 +4648,25 @@ mod tests {
}; };
scaffold_workspace(tmp.path(), &ctx).unwrap(); scaffold_workspace(tmp.path(), &ctx).unwrap();
let soul = fs::read_to_string(tmp.path().join("SOUL.md")).unwrap(); let soul = tokio::fs::read_to_string(tmp.path().join("SOUL.md"))
.await
.unwrap();
assert!( assert!(
soul.contains("Be technical and detailed."), soul.contains("Be technical and detailed."),
"SOUL.md should contain communication style" "SOUL.md should contain communication style"
); );
let user_md = fs::read_to_string(tmp.path().join("USER.md")).unwrap(); let user_md = tokio::fs::read_to_string(tmp.path().join("USER.md"))
.await
.unwrap();
assert!( assert!(
user_md.contains("Be technical and detailed."), user_md.contains("Be technical and detailed."),
"USER.md should contain communication style" "USER.md should contain communication style"
); );
let bootstrap = fs::read_to_string(tmp.path().join("BOOTSTRAP.md")).unwrap(); let bootstrap = tokio::fs::read_to_string(tmp.path().join("BOOTSTRAP.md"))
.await
.unwrap();
assert!( assert!(
bootstrap.contains("Be technical and detailed."), bootstrap.contains("Be technical and detailed."),
"BOOTSTRAP.md should contain communication style" "BOOTSTRAP.md should contain communication style"
@ -4651,19 +4675,23 @@ mod tests {
// ── scaffold_workspace: defaults when context is empty ────── // ── scaffold_workspace: defaults when context is empty ──────
#[test] #[tokio::test]
fn scaffold_uses_defaults_for_empty_context() { async fn scaffold_uses_defaults_for_empty_context() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let ctx = ProjectContext::default(); // all empty let ctx = ProjectContext::default(); // all empty
scaffold_workspace(tmp.path(), &ctx).unwrap(); scaffold_workspace(tmp.path(), &ctx).unwrap();
let identity = fs::read_to_string(tmp.path().join("IDENTITY.md")).unwrap(); let identity = tokio::fs::read_to_string(tmp.path().join("IDENTITY.md"))
.await
.unwrap();
assert!( assert!(
identity.contains("**Name:** ZeroClaw"), identity.contains("**Name:** ZeroClaw"),
"should default agent name to ZeroClaw" "should default agent name to ZeroClaw"
); );
let user_md = fs::read_to_string(tmp.path().join("USER.md")).unwrap(); let user_md = tokio::fs::read_to_string(tmp.path().join("USER.md"))
.await
.unwrap();
assert!( assert!(
user_md.contains("**Name:** User"), user_md.contains("**Name:** User"),
"should default user name to User" "should default user name to User"
@ -4673,7 +4701,9 @@ mod tests {
"should default timezone to UTC" "should default timezone to UTC"
); );
let soul = fs::read_to_string(tmp.path().join("SOUL.md")).unwrap(); let soul = tokio::fs::read_to_string(tmp.path().join("SOUL.md"))
.await
.unwrap();
assert!( assert!(
soul.contains("Be warm, natural, and clear."), soul.contains("Be warm, natural, and clear."),
"should default communication style" "should default communication style"
@ -4682,8 +4712,8 @@ mod tests {
// ── scaffold_workspace: skip existing files ───────────────── // ── scaffold_workspace: skip existing files ─────────────────
#[test] #[tokio::test]
fn scaffold_does_not_overwrite_existing_files() { async fn scaffold_does_not_overwrite_existing_files() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let ctx = ProjectContext { let ctx = ProjectContext {
user_name: "Bob".into(), user_name: "Bob".into(),
@ -4697,7 +4727,7 @@ mod tests {
scaffold_workspace(tmp.path(), &ctx).unwrap(); scaffold_workspace(tmp.path(), &ctx).unwrap();
// SOUL.md should be untouched // SOUL.md should be untouched
let soul = fs::read_to_string(&soul_path).unwrap(); let soul = tokio::fs::read_to_string(&soul_path).await.unwrap();
assert!( assert!(
soul.contains("Do not overwrite me"), soul.contains("Do not overwrite me"),
"existing files should not be overwritten" "existing files should not be overwritten"
@ -4708,14 +4738,16 @@ mod tests {
); );
// But USER.md should be created fresh // But USER.md should be created fresh
let user_md = fs::read_to_string(tmp.path().join("USER.md")).unwrap(); let user_md = tokio::fs::read_to_string(tmp.path().join("USER.md"))
.await
.unwrap();
assert!(user_md.contains("**Name:** Bob")); assert!(user_md.contains("**Name:** Bob"));
} }
// ── scaffold_workspace: idempotent ────────────────────────── // ── scaffold_workspace: idempotent ──────────────────────────
#[test] #[tokio::test]
fn scaffold_is_idempotent() { async fn scaffold_is_idempotent() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let ctx = ProjectContext { let ctx = ProjectContext {
user_name: "Eve".into(), user_name: "Eve".into(),
@ -4724,19 +4756,23 @@ mod tests {
}; };
scaffold_workspace(tmp.path(), &ctx).unwrap(); scaffold_workspace(tmp.path(), &ctx).unwrap();
let soul_v1 = fs::read_to_string(tmp.path().join("SOUL.md")).unwrap(); let soul_v1 = tokio::fs::read_to_string(tmp.path().join("SOUL.md"))
.await
.unwrap();
// Run again — should not change anything // Run again — should not change anything
scaffold_workspace(tmp.path(), &ctx).unwrap(); scaffold_workspace(tmp.path(), &ctx).unwrap();
let soul_v2 = fs::read_to_string(tmp.path().join("SOUL.md")).unwrap(); let soul_v2 = tokio::fs::read_to_string(tmp.path().join("SOUL.md"))
.await
.unwrap();
assert_eq!(soul_v1, soul_v2, "scaffold should be idempotent"); assert_eq!(soul_v1, soul_v2, "scaffold should be idempotent");
} }
// ── scaffold_workspace: all files are non-empty ───────────── // ── scaffold_workspace: all files are non-empty ─────────────
#[test] #[tokio::test]
fn scaffold_files_are_non_empty() { async fn scaffold_files_are_non_empty() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let ctx = ProjectContext::default(); let ctx = ProjectContext::default();
scaffold_workspace(tmp.path(), &ctx).unwrap(); scaffold_workspace(tmp.path(), &ctx).unwrap();
@ -4751,20 +4787,22 @@ mod tests {
"BOOTSTRAP.md", "BOOTSTRAP.md",
"MEMORY.md", "MEMORY.md",
] { ] {
let content = fs::read_to_string(tmp.path().join(f)).unwrap(); let content = tokio::fs::read_to_string(tmp.path().join(f)).await.unwrap();
assert!(!content.trim().is_empty(), "{f} should not be empty"); assert!(!content.trim().is_empty(), "{f} should not be empty");
} }
} }
// ── scaffold_workspace: AGENTS.md references on-demand memory // ── scaffold_workspace: AGENTS.md references on-demand memory
#[test] #[tokio::test]
fn agents_md_references_on_demand_memory() { async fn agents_md_references_on_demand_memory() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let ctx = ProjectContext::default(); let ctx = ProjectContext::default();
scaffold_workspace(tmp.path(), &ctx).unwrap(); scaffold_workspace(tmp.path(), &ctx).unwrap();
let agents = fs::read_to_string(tmp.path().join("AGENTS.md")).unwrap(); let agents = tokio::fs::read_to_string(tmp.path().join("AGENTS.md"))
.await
.unwrap();
assert!( assert!(
agents.contains("memory_recall"), agents.contains("memory_recall"),
"AGENTS.md should reference memory_recall for on-demand access" "AGENTS.md should reference memory_recall for on-demand access"
@ -4777,13 +4815,15 @@ mod tests {
// ── scaffold_workspace: MEMORY.md warns about token cost ──── // ── scaffold_workspace: MEMORY.md warns about token cost ────
#[test] #[tokio::test]
fn memory_md_warns_about_token_cost() { async fn memory_md_warns_about_token_cost() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let ctx = ProjectContext::default(); let ctx = ProjectContext::default();
scaffold_workspace(tmp.path(), &ctx).unwrap(); scaffold_workspace(tmp.path(), &ctx).unwrap();
let memory = fs::read_to_string(tmp.path().join("MEMORY.md")).unwrap(); let memory = tokio::fs::read_to_string(tmp.path().join("MEMORY.md"))
.await
.unwrap();
assert!( assert!(
memory.contains("costs tokens"), memory.contains("costs tokens"),
"MEMORY.md should warn about token cost" "MEMORY.md should warn about token cost"
@ -4796,13 +4836,15 @@ mod tests {
// ── scaffold_workspace: TOOLS.md lists memory_forget ──────── // ── scaffold_workspace: TOOLS.md lists memory_forget ────────
#[test] #[tokio::test]
fn tools_md_lists_all_builtin_tools() { async fn tools_md_lists_all_builtin_tools() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let ctx = ProjectContext::default(); let ctx = ProjectContext::default();
scaffold_workspace(tmp.path(), &ctx).unwrap(); scaffold_workspace(tmp.path(), &ctx).unwrap();
let tools = fs::read_to_string(tmp.path().join("TOOLS.md")).unwrap(); let tools = tokio::fs::read_to_string(tmp.path().join("TOOLS.md"))
.await
.unwrap();
for tool in &[ for tool in &[
"shell", "shell",
"file_read", "file_read",
@ -4826,13 +4868,15 @@ mod tests {
); );
} }
#[test] #[tokio::test]
fn soul_md_includes_emoji_awareness_guidance() { async fn soul_md_includes_emoji_awareness_guidance() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let ctx = ProjectContext::default(); let ctx = ProjectContext::default();
scaffold_workspace(tmp.path(), &ctx).unwrap(); scaffold_workspace(tmp.path(), &ctx).unwrap();
let soul = fs::read_to_string(tmp.path().join("SOUL.md")).unwrap(); let soul = tokio::fs::read_to_string(tmp.path().join("SOUL.md"))
.await
.unwrap();
assert!( assert!(
soul.contains("Use emojis naturally (0-2 max"), soul.contains("Use emojis naturally (0-2 max"),
"SOUL.md should include emoji usage guidance" "SOUL.md should include emoji usage guidance"
@ -4845,8 +4889,8 @@ mod tests {
// ── scaffold_workspace: special characters in names ───────── // ── scaffold_workspace: special characters in names ─────────
#[test] #[tokio::test]
fn scaffold_handles_special_characters_in_names() { async fn scaffold_handles_special_characters_in_names() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let ctx = ProjectContext { let ctx = ProjectContext {
user_name: "José María".into(), user_name: "José María".into(),
@ -4856,17 +4900,21 @@ mod tests {
}; };
scaffold_workspace(tmp.path(), &ctx).unwrap(); scaffold_workspace(tmp.path(), &ctx).unwrap();
let user_md = fs::read_to_string(tmp.path().join("USER.md")).unwrap(); let user_md = tokio::fs::read_to_string(tmp.path().join("USER.md"))
.await
.unwrap();
assert!(user_md.contains("José María")); assert!(user_md.contains("José María"));
let soul = fs::read_to_string(tmp.path().join("SOUL.md")).unwrap(); let soul = tokio::fs::read_to_string(tmp.path().join("SOUL.md"))
.await
.unwrap();
assert!(soul.contains("ZeroClaw-v2")); assert!(soul.contains("ZeroClaw-v2"));
} }
// ── scaffold_workspace: full personalization round-trip ───── // ── scaffold_workspace: full personalization round-trip ─────
#[test] #[tokio::test]
fn scaffold_full_personalization() { async fn scaffold_full_personalization() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let ctx = ProjectContext { let ctx = ProjectContext {
user_name: "Argenis".into(), user_name: "Argenis".into(),
@ -4879,27 +4927,39 @@ mod tests {
scaffold_workspace(tmp.path(), &ctx).unwrap(); scaffold_workspace(tmp.path(), &ctx).unwrap();
// Verify every file got personalized // Verify every file got personalized
let identity = fs::read_to_string(tmp.path().join("IDENTITY.md")).unwrap(); let identity = tokio::fs::read_to_string(tmp.path().join("IDENTITY.md"))
.await
.unwrap();
assert!(identity.contains("**Name:** Claw")); assert!(identity.contains("**Name:** Claw"));
let soul = fs::read_to_string(tmp.path().join("SOUL.md")).unwrap(); let soul = tokio::fs::read_to_string(tmp.path().join("SOUL.md"))
.await
.unwrap();
assert!(soul.contains("You are **Claw**")); assert!(soul.contains("You are **Claw**"));
assert!(soul.contains("Be friendly, human, and conversational")); assert!(soul.contains("Be friendly, human, and conversational"));
let user_md = fs::read_to_string(tmp.path().join("USER.md")).unwrap(); let user_md = tokio::fs::read_to_string(tmp.path().join("USER.md"))
.await
.unwrap();
assert!(user_md.contains("**Name:** Argenis")); assert!(user_md.contains("**Name:** Argenis"));
assert!(user_md.contains("**Timezone:** US/Eastern")); assert!(user_md.contains("**Timezone:** US/Eastern"));
assert!(user_md.contains("Be friendly, human, and conversational")); assert!(user_md.contains("Be friendly, human, and conversational"));
let agents = fs::read_to_string(tmp.path().join("AGENTS.md")).unwrap(); let agents = tokio::fs::read_to_string(tmp.path().join("AGENTS.md"))
.await
.unwrap();
assert!(agents.contains("Claw Personal Assistant")); assert!(agents.contains("Claw Personal Assistant"));
let bootstrap = fs::read_to_string(tmp.path().join("BOOTSTRAP.md")).unwrap(); let bootstrap = tokio::fs::read_to_string(tmp.path().join("BOOTSTRAP.md"))
.await
.unwrap();
assert!(bootstrap.contains("**Argenis**")); assert!(bootstrap.contains("**Argenis**"));
assert!(bootstrap.contains("US/Eastern")); assert!(bootstrap.contains("US/Eastern"));
assert!(bootstrap.contains("Introduce yourself as Claw")); assert!(bootstrap.contains("Introduce yourself as Claw"));
let heartbeat = fs::read_to_string(tmp.path().join("HEARTBEAT.md")).unwrap(); let heartbeat = tokio::fs::read_to_string(tmp.path().join("HEARTBEAT.md"))
.await
.unwrap();
assert!(heartbeat.contains("Claw")); assert!(heartbeat.contains("Claw"));
} }

View file

@ -75,7 +75,7 @@ impl Tool for ArduinoUploadTool {
let sketch_dir = temp_dir.join(sketch_name); let sketch_dir = temp_dir.join(sketch_name);
let ino_path = sketch_dir.join(format!("{}.ino", sketch_name)); let ino_path = sketch_dir.join(format!("{}.ino", sketch_name));
if let Err(e) = std::fs::create_dir_all(&sketch_dir) { if let Err(e) = tokio::fs::create_dir_all(&sketch_dir).await {
return Ok(ToolResult { return Ok(ToolResult {
success: false, success: false,
output: format!("Failed to create sketch dir: {}", e), output: format!("Failed to create sketch dir: {}", e),
@ -83,8 +83,8 @@ impl Tool for ArduinoUploadTool {
}); });
} }
if let Err(e) = std::fs::write(&ino_path, code) { if let Err(e) = tokio::fs::write(&ino_path, code).await {
let _ = std::fs::remove_dir_all(&temp_dir); let _ = tokio::fs::remove_dir_all(&temp_dir).await;
return Ok(ToolResult { return Ok(ToolResult {
success: false, success: false,
output: format!("Failed to write sketch: {}", e), output: format!("Failed to write sketch: {}", e),
@ -103,7 +103,7 @@ impl Tool for ArduinoUploadTool {
let compile_output = match compile { let compile_output = match compile {
Ok(o) => o, Ok(o) => o,
Err(e) => { Err(e) => {
let _ = std::fs::remove_dir_all(&temp_dir); let _ = tokio::fs::remove_dir_all(&temp_dir).await;
return Ok(ToolResult { return Ok(ToolResult {
success: false, success: false,
output: format!("arduino-cli compile failed: {}", e), output: format!("arduino-cli compile failed: {}", e),
@ -114,7 +114,7 @@ impl Tool for ArduinoUploadTool {
if !compile_output.status.success() { if !compile_output.status.success() {
let stderr = String::from_utf8_lossy(&compile_output.stderr); let stderr = String::from_utf8_lossy(&compile_output.stderr);
let _ = std::fs::remove_dir_all(&temp_dir); let _ = tokio::fs::remove_dir_all(&temp_dir).await;
return Ok(ToolResult { return Ok(ToolResult {
success: false, success: false,
output: format!("Compile failed:\n{}", stderr), output: format!("Compile failed:\n{}", stderr),
@ -130,7 +130,7 @@ impl Tool for ArduinoUploadTool {
let upload_output = match upload { let upload_output = match upload {
Ok(o) => o, Ok(o) => o,
Err(e) => { Err(e) => {
let _ = std::fs::remove_dir_all(&temp_dir); let _ = tokio::fs::remove_dir_all(&temp_dir).await;
return Ok(ToolResult { return Ok(ToolResult {
success: false, success: false,
output: format!("arduino-cli upload failed: {}", e), output: format!("arduino-cli upload failed: {}", e),
@ -139,7 +139,7 @@ impl Tool for ArduinoUploadTool {
} }
}; };
let _ = std::fs::remove_dir_all(&temp_dir); let _ = tokio::fs::remove_dir_all(&temp_dir).await;
if !upload_output.status.success() { if !upload_output.status.success() {
let stderr = String::from_utf8_lossy(&upload_output.stderr); let stderr = String::from_utf8_lossy(&upload_output.stderr);

View file

@ -1054,7 +1054,10 @@ impl Provider for OpenAiCompatibleProvider {
let url = self.chat_completions_url(); let url = self.chat_completions_url();
let response = self let response = self
.apply_auth_header(self.http_client().post(&url).json(&native_request), credential) .apply_auth_header(
self.http_client().post(&url).json(&native_request),
credential,
)
.send() .send()
.await?; .await?;

View file

@ -45,14 +45,12 @@ fn is_non_retryable(err: &anyhow::Error) -> bool {
return true; return true;
} }
let model_catalog_mismatch = msg_lower.contains("model") msg_lower.contains("model")
&& (msg_lower.contains("not found") && (msg_lower.contains("not found")
|| msg_lower.contains("unknown") || msg_lower.contains("unknown")
|| msg_lower.contains("unsupported") || msg_lower.contains("unsupported")
|| msg_lower.contains("does not exist") || msg_lower.contains("does not exist")
|| msg_lower.contains("invalid")); || msg_lower.contains("invalid"))
model_catalog_mismatch
} }
/// Check if an error is a rate-limit (429) error. /// Check if an error is a rate-limit (429) error.

View file

@ -335,8 +335,8 @@ mod tests {
// ── §8.1 Log rotation tests ───────────────────────────── // ── §8.1 Log rotation tests ─────────────────────────────
#[test] #[tokio::test]
fn audit_logger_writes_event_when_enabled() -> Result<()> { async fn audit_logger_writes_event_when_enabled() -> Result<()> {
let tmp = TempDir::new()?; let tmp = TempDir::new()?;
let config = AuditConfig { let config = AuditConfig {
enabled: true, enabled: true,
@ -353,7 +353,7 @@ mod tests {
let log_path = tmp.path().join("audit.log"); let log_path = tmp.path().join("audit.log");
assert!(log_path.exists(), "audit log file must be created"); assert!(log_path.exists(), "audit log file must be created");
let content = std::fs::read_to_string(&log_path)?; let content = tokio::fs::read_to_string(&log_path).await?;
assert!(!content.is_empty(), "audit log must not be empty"); assert!(!content.is_empty(), "audit log must not be empty");
let parsed: AuditEvent = serde_json::from_str(content.trim())?; let parsed: AuditEvent = serde_json::from_str(content.trim())?;
@ -361,8 +361,8 @@ mod tests {
Ok(()) Ok(())
} }
#[test] #[tokio::test]
fn audit_log_command_event_writes_structured_entry() -> Result<()> { async fn audit_log_command_event_writes_structured_entry() -> Result<()> {
let tmp = TempDir::new()?; let tmp = TempDir::new()?;
let config = AuditConfig { let config = AuditConfig {
enabled: true, enabled: true,
@ -382,7 +382,7 @@ mod tests {
})?; })?;
let log_path = tmp.path().join("audit.log"); let log_path = tmp.path().join("audit.log");
let content = std::fs::read_to_string(&log_path)?; let content = tokio::fs::read_to_string(&log_path).await?;
let parsed: AuditEvent = serde_json::from_str(content.trim())?; let parsed: AuditEvent = serde_json::from_str(content.trim())?;
let action = parsed.action.unwrap(); let action = parsed.action.unwrap();

View file

@ -334,8 +334,8 @@ mod tests {
assert!(!SecretStore::is_encrypted("")); assert!(!SecretStore::is_encrypted(""));
} }
#[test] #[tokio::test]
fn key_file_created_on_first_encrypt() { async fn key_file_created_on_first_encrypt() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let store = SecretStore::new(tmp.path(), true); let store = SecretStore::new(tmp.path(), true);
assert!(!store.key_path.exists()); assert!(!store.key_path.exists());
@ -343,7 +343,7 @@ mod tests {
store.encrypt("test").unwrap(); store.encrypt("test").unwrap();
assert!(store.key_path.exists(), "Key file should be created"); assert!(store.key_path.exists(), "Key file should be created");
let key_hex = fs::read_to_string(&store.key_path).unwrap(); let key_hex = tokio::fs::read_to_string(&store.key_path).await.unwrap();
assert_eq!( assert_eq!(
key_hex.len(), key_hex.len(),
KEY_LEN * 2, KEY_LEN * 2,

View file

@ -191,8 +191,8 @@ mod tests {
} }
} }
#[test] #[tokio::test]
fn integrate_creates_files() { async fn integrate_creates_files() {
let tmp = std::env::temp_dir().join("zeroclaw-test-integrate"); let tmp = std::env::temp_dir().join("zeroclaw-test-integrate");
let _ = fs::remove_dir_all(&tmp); let _ = fs::remove_dir_all(&tmp);
@ -203,11 +203,15 @@ mod tests {
assert!(path.join("SKILL.toml").exists()); assert!(path.join("SKILL.toml").exists());
assert!(path.join("SKILL.md").exists()); assert!(path.join("SKILL.md").exists());
let toml = fs::read_to_string(path.join("SKILL.toml")).unwrap(); let toml = tokio::fs::read_to_string(path.join("SKILL.toml"))
.await
.unwrap();
assert!(toml.contains("name = \"test-skill\"")); assert!(toml.contains("name = \"test-skill\""));
assert!(toml.contains("stars = 42")); assert!(toml.contains("stars = 42"));
let md = fs::read_to_string(path.join("SKILL.md")).unwrap(); let md = tokio::fs::read_to_string(path.join("SKILL.md"))
.await
.unwrap();
assert!(md.contains("# test-skill")); assert!(md.contains("# test-skill"));
assert!(md.contains("A test skill for unit tests")); assert!(md.contains("A test skill for unit tests"));

View file

@ -4,21 +4,21 @@ mod tests {
use std::path::Path; use std::path::Path;
use tempfile::TempDir; use tempfile::TempDir;
#[test] #[tokio::test]
fn test_skills_symlink_unix_edge_cases() { async fn test_skills_symlink_unix_edge_cases() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let workspace_dir = tmp.path().join("workspace"); let workspace_dir = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace_dir).unwrap(); tokio::fs::create_dir_all(&workspace_dir).await.unwrap();
let skills_path = skills_dir(&workspace_dir); let skills_path = skills_dir(&workspace_dir);
std::fs::create_dir_all(&skills_path).unwrap(); tokio::fs::create_dir_all(&skills_path).await.unwrap();
// Test case 1: Valid symlink creation on Unix // Test case 1: Valid symlink creation on Unix
#[cfg(unix)] #[cfg(unix)]
{ {
let source_dir = tmp.path().join("source_skill"); let source_dir = tmp.path().join("source_skill");
std::fs::create_dir_all(&source_dir).unwrap(); tokio::fs::create_dir_all(&source_dir).await.unwrap();
std::fs::write(source_dir.join("SKILL.md"), "# Test Skill\nContent").unwrap(); tokio::fs::write(source_dir.join("SKILL.md"), "# Test Skill\nContent").await.unwrap();
let dest_link = skills_path.join("linked_skill"); let dest_link = skills_path.join("linked_skill");
@ -31,7 +31,7 @@ mod tests {
assert!(dest_link.is_symlink()); assert!(dest_link.is_symlink());
// Verify we can read through symlink // Verify we can read through symlink
let content = std::fs::read_to_string(dest_link.join("SKILL.md")); let content = tokio::fs::read_to_string(dest_link.join("SKILL.md")).await;
assert!(content.is_ok()); assert!(content.is_ok());
assert!(content.unwrap().contains("Test Skill")); assert!(content.unwrap().contains("Test Skill"));
@ -45,7 +45,7 @@ mod tests {
); );
// But reading through it should fail // But reading through it should fail
let content = std::fs::read_to_string(broken_link.join("SKILL.md")); let content = tokio::fs::read_to_string(broken_link.join("SKILL.md")).await;
assert!(content.is_err()); assert!(content.is_err());
} }
@ -53,7 +53,7 @@ mod tests {
#[cfg(windows)] #[cfg(windows)]
{ {
let source_dir = tmp.path().join("source_skill"); let source_dir = tmp.path().join("source_skill");
std::fs::create_dir_all(&source_dir).unwrap(); tokio::fs::create_dir_all(&source_dir).await.unwrap();
let dest_link = skills_path.join("linked_skill"); let dest_link = skills_path.join("linked_skill");
@ -64,7 +64,7 @@ mod tests {
assert!(!dest_link.exists()); assert!(!dest_link.exists());
} else { } else {
// Clean up if it succeeded // Clean up if it succeeded
let _ = std::fs::remove_dir(&dest_link); let _ = tokio::fs::remove_dir(&dest_link).await;
} }
} }
@ -80,21 +80,21 @@ mod tests {
assert!(!empty_skills_path.exists()); assert!(!empty_skills_path.exists());
} }
#[test] #[tokio::test]
fn test_skills_symlink_permissions_and_safety() { async fn test_skills_symlink_permissions_and_safety() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let workspace_dir = tmp.path().join("workspace"); let workspace_dir = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace_dir).unwrap(); tokio::fs::create_dir_all(&workspace_dir).await.unwrap();
let skills_path = skills_dir(&workspace_dir); let skills_path = skills_dir(&workspace_dir);
std::fs::create_dir_all(&skills_path).unwrap(); tokio::fs::create_dir_all(&skills_path).await.unwrap();
#[cfg(unix)] #[cfg(unix)]
{ {
// Test case: Symlink outside workspace should be allowed (user responsibility) // Test case: Symlink outside workspace should be allowed (user responsibility)
let outside_dir = tmp.path().join("outside_skill"); let outside_dir = tmp.path().join("outside_skill");
std::fs::create_dir_all(&outside_dir).unwrap(); tokio::fs::create_dir_all(&outside_dir).await.unwrap();
std::fs::write(outside_dir.join("SKILL.md"), "# Outside Skill\nContent").unwrap(); tokio::fs::write(outside_dir.join("SKILL.md"), "# Outside Skill\nContent").await.unwrap();
let dest_link = skills_path.join("outside_skill"); let dest_link = skills_path.join("outside_skill");
let result = std::os::unix::fs::symlink(&outside_dir, &dest_link); let result = std::os::unix::fs::symlink(&outside_dir, &dest_link);
@ -104,7 +104,7 @@ mod tests {
); );
// Should still be readable // Should still be readable
let content = std::fs::read_to_string(dest_link.join("SKILL.md")); let content = tokio::fs::read_to_string(dest_link.join("SKILL.md")).await;
assert!(content.is_ok()); assert!(content.is_ok());
assert!(content.unwrap().contains("Outside Skill")); assert!(content.unwrap().contains("Outside Skill"));
} }

View file

@ -1225,8 +1225,9 @@ mod native_backend {
}); });
if let Some(path_str) = path { if let Some(path_str) = path {
std::fs::write(&path_str, &png) tokio::fs::write(&path_str, &png).await.with_context(|| {
.with_context(|| format!("Failed to write screenshot to {path_str}"))?; format!("Failed to write screenshot to {path_str}")
})?;
payload["path"] = Value::String(path_str); payload["path"] = Value::String(path_str);
} else { } else {
payload["png_base64"] = payload["png_base64"] =

View file

@ -217,13 +217,13 @@ mod tests {
use crate::security::AutonomyLevel; use crate::security::AutonomyLevel;
use tempfile::TempDir; use tempfile::TempDir;
fn test_config(tmp: &TempDir) -> Arc<Config> { async fn test_config(tmp: &TempDir) -> Arc<Config> {
let config = Config { let config = Config {
workspace_dir: tmp.path().join("workspace"), workspace_dir: tmp.path().join("workspace"),
config_path: tmp.path().join("config.toml"), config_path: tmp.path().join("config.toml"),
..Config::default() ..Config::default()
}; };
std::fs::create_dir_all(&config.workspace_dir).unwrap(); tokio::fs::create_dir_all(&config.workspace_dir).await.unwrap();
Arc::new(config) Arc::new(config)
} }
@ -237,7 +237,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn adds_shell_job() { async fn adds_shell_job() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let cfg = test_config(&tmp); let cfg = test_config(&tmp).await;
let tool = CronAddTool::new(cfg.clone(), test_security(&cfg)); let tool = CronAddTool::new(cfg.clone(), test_security(&cfg));
let result = tool let result = tool
.execute(json!({ .execute(json!({
@ -262,7 +262,7 @@ mod tests {
}; };
config.autonomy.allowed_commands = vec!["echo".into()]; config.autonomy.allowed_commands = vec!["echo".into()];
config.autonomy.level = AutonomyLevel::Supervised; config.autonomy.level = AutonomyLevel::Supervised;
std::fs::create_dir_all(&config.workspace_dir).unwrap(); tokio::fs::create_dir_all(&config.workspace_dir).await.unwrap();
let cfg = Arc::new(config); let cfg = Arc::new(config);
let tool = CronAddTool::new(cfg.clone(), test_security(&cfg)); let tool = CronAddTool::new(cfg.clone(), test_security(&cfg));
@ -285,7 +285,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn rejects_invalid_schedule() { async fn rejects_invalid_schedule() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let cfg = test_config(&tmp); let cfg = test_config(&tmp).await;
let tool = CronAddTool::new(cfg.clone(), test_security(&cfg)); let tool = CronAddTool::new(cfg.clone(), test_security(&cfg));
let result = tool let result = tool
@ -307,7 +307,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn agent_job_requires_prompt() { async fn agent_job_requires_prompt() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let cfg = test_config(&tmp); let cfg = test_config(&tmp).await;
let tool = CronAddTool::new(cfg.clone(), test_security(&cfg)); let tool = CronAddTool::new(cfg.clone(), test_security(&cfg));
let result = tool let result = tool

View file

@ -63,20 +63,20 @@ mod tests {
use crate::config::Config; use crate::config::Config;
use tempfile::TempDir; use tempfile::TempDir;
fn test_config(tmp: &TempDir) -> Arc<Config> { async fn test_config(tmp: &TempDir) -> Arc<Config> {
let config = Config { let config = Config {
workspace_dir: tmp.path().join("workspace"), workspace_dir: tmp.path().join("workspace"),
config_path: tmp.path().join("config.toml"), config_path: tmp.path().join("config.toml"),
..Config::default() ..Config::default()
}; };
std::fs::create_dir_all(&config.workspace_dir).unwrap(); tokio::fs::create_dir_all(&config.workspace_dir).await.unwrap();
Arc::new(config) Arc::new(config)
} }
#[tokio::test] #[tokio::test]
async fn returns_empty_list_when_no_jobs() { async fn returns_empty_list_when_no_jobs() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let cfg = test_config(&tmp); let cfg = test_config(&tmp).await;
let tool = CronListTool::new(cfg); let tool = CronListTool::new(cfg);
let result = tool.execute(json!({})).await.unwrap(); let result = tool.execute(json!({})).await.unwrap();
@ -87,7 +87,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn errors_when_cron_disabled() { async fn errors_when_cron_disabled() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let mut cfg = (*test_config(&tmp)).clone(); let mut cfg = (*test_config(&tmp).await).clone();
cfg.cron.enabled = false; cfg.cron.enabled = false;
let tool = CronListTool::new(Arc::new(cfg)); let tool = CronListTool::new(Arc::new(cfg));

View file

@ -76,20 +76,20 @@ mod tests {
use crate::config::Config; use crate::config::Config;
use tempfile::TempDir; use tempfile::TempDir;
fn test_config(tmp: &TempDir) -> Arc<Config> { async fn test_config(tmp: &TempDir) -> Arc<Config> {
let config = Config { let config = Config {
workspace_dir: tmp.path().join("workspace"), workspace_dir: tmp.path().join("workspace"),
config_path: tmp.path().join("config.toml"), config_path: tmp.path().join("config.toml"),
..Config::default() ..Config::default()
}; };
std::fs::create_dir_all(&config.workspace_dir).unwrap(); tokio::fs::create_dir_all(&config.workspace_dir).await.unwrap();
Arc::new(config) Arc::new(config)
} }
#[tokio::test] #[tokio::test]
async fn removes_existing_job() { async fn removes_existing_job() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let cfg = test_config(&tmp); let cfg = test_config(&tmp).await;
let job = cron::add_job(&cfg, "*/5 * * * *", "echo ok").unwrap(); let job = cron::add_job(&cfg, "*/5 * * * *", "echo ok").unwrap();
let tool = CronRemoveTool::new(cfg.clone()); let tool = CronRemoveTool::new(cfg.clone());
@ -101,7 +101,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn errors_when_job_id_missing() { async fn errors_when_job_id_missing() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let cfg = test_config(&tmp); let cfg = test_config(&tmp).await;
let tool = CronRemoveTool::new(cfg); let tool = CronRemoveTool::new(cfg);
let result = tool.execute(json!({})).await.unwrap(); let result = tool.execute(json!({})).await.unwrap();

View file

@ -107,20 +107,20 @@ mod tests {
use crate::config::Config; use crate::config::Config;
use tempfile::TempDir; use tempfile::TempDir;
fn test_config(tmp: &TempDir) -> Arc<Config> { async fn test_config(tmp: &TempDir) -> Arc<Config> {
let config = Config { let config = Config {
workspace_dir: tmp.path().join("workspace"), workspace_dir: tmp.path().join("workspace"),
config_path: tmp.path().join("config.toml"), config_path: tmp.path().join("config.toml"),
..Config::default() ..Config::default()
}; };
std::fs::create_dir_all(&config.workspace_dir).unwrap(); tokio::fs::create_dir_all(&config.workspace_dir).await.unwrap();
Arc::new(config) Arc::new(config)
} }
#[tokio::test] #[tokio::test]
async fn force_runs_job_and_records_history() { async fn force_runs_job_and_records_history() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let cfg = test_config(&tmp); let cfg = test_config(&tmp).await;
let job = cron::add_job(&cfg, "*/5 * * * *", "echo run-now").unwrap(); let job = cron::add_job(&cfg, "*/5 * * * *", "echo run-now").unwrap();
let tool = CronRunTool::new(cfg.clone()); let tool = CronRunTool::new(cfg.clone());
@ -134,7 +134,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn errors_for_missing_job() { async fn errors_for_missing_job() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let cfg = test_config(&tmp); let cfg = test_config(&tmp).await;
let tool = CronRunTool::new(cfg); let tool = CronRunTool::new(cfg);
let result = tool let result = tool

View file

@ -121,20 +121,20 @@ mod tests {
use chrono::{Duration as ChronoDuration, Utc}; use chrono::{Duration as ChronoDuration, Utc};
use tempfile::TempDir; use tempfile::TempDir;
fn test_config(tmp: &TempDir) -> Arc<Config> { async fn test_config(tmp: &TempDir) -> Arc<Config> {
let config = Config { let config = Config {
workspace_dir: tmp.path().join("workspace"), workspace_dir: tmp.path().join("workspace"),
config_path: tmp.path().join("config.toml"), config_path: tmp.path().join("config.toml"),
..Config::default() ..Config::default()
}; };
std::fs::create_dir_all(&config.workspace_dir).unwrap(); tokio::fs::create_dir_all(&config.workspace_dir).await.unwrap();
Arc::new(config) Arc::new(config)
} }
#[tokio::test] #[tokio::test]
async fn lists_runs_with_truncation() { async fn lists_runs_with_truncation() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let cfg = test_config(&tmp); let cfg = test_config(&tmp).await;
let job = cron::add_job(&cfg, "*/5 * * * *", "echo ok").unwrap(); let job = cron::add_job(&cfg, "*/5 * * * *", "echo ok").unwrap();
let long_output = "x".repeat(1000); let long_output = "x".repeat(1000);
@ -163,7 +163,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn errors_when_job_id_missing() { async fn errors_when_job_id_missing() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let cfg = test_config(&tmp); let cfg = test_config(&tmp).await;
let tool = CronRunsTool::new(cfg); let tool = CronRunsTool::new(cfg);
let result = tool.execute(json!({})).await.unwrap(); let result = tool.execute(json!({})).await.unwrap();
assert!(!result.success); assert!(!result.success);

View file

@ -111,13 +111,13 @@ mod tests {
use crate::config::Config; use crate::config::Config;
use tempfile::TempDir; use tempfile::TempDir;
fn test_config(tmp: &TempDir) -> Arc<Config> { async fn test_config(tmp: &TempDir) -> Arc<Config> {
let config = Config { let config = Config {
workspace_dir: tmp.path().join("workspace"), workspace_dir: tmp.path().join("workspace"),
config_path: tmp.path().join("config.toml"), config_path: tmp.path().join("config.toml"),
..Config::default() ..Config::default()
}; };
std::fs::create_dir_all(&config.workspace_dir).unwrap(); tokio::fs::create_dir_all(&config.workspace_dir).await.unwrap();
Arc::new(config) Arc::new(config)
} }
@ -131,7 +131,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn updates_enabled_flag() { async fn updates_enabled_flag() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let cfg = test_config(&tmp); let cfg = test_config(&tmp).await;
let job = cron::add_job(&cfg, "*/5 * * * *", "echo ok").unwrap(); let job = cron::add_job(&cfg, "*/5 * * * *", "echo ok").unwrap();
let tool = CronUpdateTool::new(cfg.clone(), test_security(&cfg)); let tool = CronUpdateTool::new(cfg.clone(), test_security(&cfg));
@ -156,7 +156,7 @@ mod tests {
..Config::default() ..Config::default()
}; };
config.autonomy.allowed_commands = vec!["echo".into()]; config.autonomy.allowed_commands = vec!["echo".into()];
std::fs::create_dir_all(&config.workspace_dir).unwrap(); tokio::fs::create_dir_all(&config.workspace_dir).await.unwrap();
let cfg = Arc::new(config); let cfg = Arc::new(config);
let job = cron::add_job(&cfg, "*/5 * * * *", "echo ok").unwrap(); let job = cron::add_job(&cfg, "*/5 * * * *", "echo ok").unwrap();
let tool = CronUpdateTool::new(cfg.clone(), test_security(&cfg)); let tool = CronUpdateTool::new(cfg.clone(), test_security(&cfg));

View file

@ -428,7 +428,7 @@ mod tests {
async fn execute_real_file() { async fn execute_real_file() {
// Create a minimal valid PNG // Create a minimal valid PNG
let dir = std::env::temp_dir().join("zeroclaw_image_info_test"); let dir = std::env::temp_dir().join("zeroclaw_image_info_test");
let _ = std::fs::create_dir_all(&dir); let _ = tokio::fs::create_dir_all(&dir).await;
let png_path = dir.join("test.png"); let png_path = dir.join("test.png");
// Minimal 1x1 red PNG (67 bytes) // Minimal 1x1 red PNG (67 bytes)
@ -448,7 +448,7 @@ mod tests {
0x49, 0x45, 0x4E, 0x44, // IEND 0x49, 0x45, 0x4E, 0x44, // IEND
0xAE, 0x42, 0x60, 0x82, // CRC 0xAE, 0x42, 0x60, 0x82, // CRC
]; ];
std::fs::write(&png_path, &png_bytes).unwrap(); tokio::fs::write(&png_path, &png_bytes).await.unwrap();
let tool = ImageInfoTool::new(test_security()); let tool = ImageInfoTool::new(test_security());
let result = tool let result = tool
@ -461,13 +461,13 @@ mod tests {
assert!(!result.output.contains("data:")); assert!(!result.output.contains("data:"));
// Clean up // Clean up
let _ = std::fs::remove_dir_all(&dir); let _ = tokio::fs::remove_dir_all(&dir).await;
} }
#[tokio::test] #[tokio::test]
async fn execute_with_base64() { async fn execute_with_base64() {
let dir = std::env::temp_dir().join("zeroclaw_image_info_b64"); let dir = std::env::temp_dir().join("zeroclaw_image_info_b64");
let _ = std::fs::create_dir_all(&dir); let _ = tokio::fs::create_dir_all(&dir).await;
let png_path = dir.join("test_b64.png"); let png_path = dir.join("test_b64.png");
// Minimal 1x1 PNG // Minimal 1x1 PNG
@ -478,7 +478,7 @@ mod tests {
0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xE2, 0x21, 0xBC, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xE2, 0x21, 0xBC,
0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
]; ];
std::fs::write(&png_path, &png_bytes).unwrap(); tokio::fs::write(&png_path, &png_bytes).await.unwrap();
let tool = ImageInfoTool::new(test_security()); let tool = ImageInfoTool::new(test_security());
let result = tool let result = tool
@ -488,6 +488,6 @@ mod tests {
assert!(result.success); assert!(result.success);
assert!(result.output.contains("data:image/png;base64,")); assert!(result.output.contains("data:image/png;base64,"));
let _ = std::fs::remove_dir_all(&dir); let _ = tokio::fs::remove_dir_all(&dir).await;
} }
} }

View file

@ -41,9 +41,10 @@ impl PushoverTool {
) )
} }
fn get_credentials(&self) -> anyhow::Result<(String, String)> { async fn get_credentials(&self) -> anyhow::Result<(String, String)> {
let env_path = self.workspace_dir.join(".env"); let env_path = self.workspace_dir.join(".env");
let content = std::fs::read_to_string(&env_path) let content = tokio::fs::read_to_string(&env_path)
.await
.map_err(|e| anyhow::anyhow!("Failed to read {}: {}", env_path.display(), e))?; .map_err(|e| anyhow::anyhow!("Failed to read {}: {}", env_path.display(), e))?;
let mut token = None; let mut token = None;
@ -153,7 +154,7 @@ impl Tool for PushoverTool {
let sound = args.get("sound").and_then(|v| v.as_str()).map(String::from); let sound = args.get("sound").and_then(|v| v.as_str()).map(String::from);
let (token, user_key) = self.get_credentials()?; let (token, user_key) = self.get_credentials().await?;
let mut form = reqwest::multipart::Form::new() let mut form = reqwest::multipart::Form::new()
.text("token", token) .text("token", token)
@ -269,8 +270,8 @@ mod tests {
assert!(required.contains(&serde_json::Value::String("message".to_string()))); assert!(required.contains(&serde_json::Value::String("message".to_string())));
} }
#[test] #[tokio::test]
fn credentials_parsed_from_env_file() { async fn credentials_parsed_from_env_file() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let env_path = tmp.path().join(".env"); let env_path = tmp.path().join(".env");
fs::write( fs::write(
@ -283,7 +284,7 @@ mod tests {
test_security(AutonomyLevel::Full, 100), test_security(AutonomyLevel::Full, 100),
tmp.path().to_path_buf(), tmp.path().to_path_buf(),
); );
let result = tool.get_credentials(); let result = tool.get_credentials().await;
assert!(result.is_ok()); assert!(result.is_ok());
let (token, user_key) = result.unwrap(); let (token, user_key) = result.unwrap();
@ -291,20 +292,20 @@ mod tests {
assert_eq!(user_key, "userkey456"); assert_eq!(user_key, "userkey456");
} }
#[test] #[tokio::test]
fn credentials_fail_without_env_file() { async fn credentials_fail_without_env_file() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let tool = PushoverTool::new( let tool = PushoverTool::new(
test_security(AutonomyLevel::Full, 100), test_security(AutonomyLevel::Full, 100),
tmp.path().to_path_buf(), tmp.path().to_path_buf(),
); );
let result = tool.get_credentials(); let result = tool.get_credentials().await;
assert!(result.is_err()); assert!(result.is_err());
} }
#[test] #[tokio::test]
fn credentials_fail_without_token() { async fn credentials_fail_without_token() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let env_path = tmp.path().join(".env"); let env_path = tmp.path().join(".env");
fs::write(&env_path, "PUSHOVER_USER_KEY=userkey456\n").unwrap(); fs::write(&env_path, "PUSHOVER_USER_KEY=userkey456\n").unwrap();
@ -313,13 +314,13 @@ mod tests {
test_security(AutonomyLevel::Full, 100), test_security(AutonomyLevel::Full, 100),
tmp.path().to_path_buf(), tmp.path().to_path_buf(),
); );
let result = tool.get_credentials(); let result = tool.get_credentials().await;
assert!(result.is_err()); assert!(result.is_err());
} }
#[test] #[tokio::test]
fn credentials_fail_without_user_key() { async fn credentials_fail_without_user_key() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let env_path = tmp.path().join(".env"); let env_path = tmp.path().join(".env");
fs::write(&env_path, "PUSHOVER_TOKEN=testtoken123\n").unwrap(); fs::write(&env_path, "PUSHOVER_TOKEN=testtoken123\n").unwrap();
@ -328,13 +329,13 @@ mod tests {
test_security(AutonomyLevel::Full, 100), test_security(AutonomyLevel::Full, 100),
tmp.path().to_path_buf(), tmp.path().to_path_buf(),
); );
let result = tool.get_credentials(); let result = tool.get_credentials().await;
assert!(result.is_err()); assert!(result.is_err());
} }
#[test] #[tokio::test]
fn credentials_ignore_comments() { async fn credentials_ignore_comments() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let env_path = tmp.path().join(".env"); let env_path = tmp.path().join(".env");
fs::write(&env_path, "# This is a comment\nPUSHOVER_TOKEN=realtoken\n# Another comment\nPUSHOVER_USER_KEY=realuser\n").unwrap(); fs::write(&env_path, "# This is a comment\nPUSHOVER_TOKEN=realtoken\n# Another comment\nPUSHOVER_USER_KEY=realuser\n").unwrap();
@ -343,7 +344,7 @@ mod tests {
test_security(AutonomyLevel::Full, 100), test_security(AutonomyLevel::Full, 100),
tmp.path().to_path_buf(), tmp.path().to_path_buf(),
); );
let result = tool.get_credentials(); let result = tool.get_credentials().await;
assert!(result.is_ok()); assert!(result.is_ok());
let (token, user_key) = result.unwrap(); let (token, user_key) = result.unwrap();
@ -371,8 +372,8 @@ mod tests {
assert!(schema["properties"].get("sound").is_some()); assert!(schema["properties"].get("sound").is_some());
} }
#[test] #[tokio::test]
fn credentials_support_export_and_quoted_values() { async fn credentials_support_export_and_quoted_values() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let env_path = tmp.path().join(".env"); let env_path = tmp.path().join(".env");
fs::write( fs::write(
@ -385,7 +386,7 @@ mod tests {
test_security(AutonomyLevel::Full, 100), test_security(AutonomyLevel::Full, 100),
tmp.path().to_path_buf(), tmp.path().to_path_buf(),
); );
let result = tool.get_credentials(); let result = tool.get_credentials().await;
assert!(result.is_ok()); assert!(result.is_ok());
let (token, user_key) = result.unwrap(); let (token, user_key) = result.unwrap();

View file

@ -368,14 +368,14 @@ mod tests {
use crate::security::AutonomyLevel; use crate::security::AutonomyLevel;
use tempfile::TempDir; use tempfile::TempDir;
fn test_setup() -> (TempDir, Config, Arc<SecurityPolicy>) { async fn test_setup() -> (TempDir, Config, Arc<SecurityPolicy>) {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
let config = Config { let config = Config {
workspace_dir: tmp.path().join("workspace"), workspace_dir: tmp.path().join("workspace"),
config_path: tmp.path().join("config.toml"), config_path: tmp.path().join("config.toml"),
..Config::default() ..Config::default()
}; };
std::fs::create_dir_all(&config.workspace_dir).unwrap(); tokio::fs::create_dir_all(&config.workspace_dir).await.unwrap();
let security = Arc::new(SecurityPolicy::from_config( let security = Arc::new(SecurityPolicy::from_config(
&config.autonomy, &config.autonomy,
&config.workspace_dir, &config.workspace_dir,
@ -383,9 +383,9 @@ mod tests {
(tmp, config, security) (tmp, config, security)
} }
#[test] #[tokio::test]
fn tool_name_and_schema() { async fn tool_name_and_schema() {
let (_tmp, config, security) = test_setup(); let (_tmp, config, security) = test_setup().await;
let tool = ScheduleTool::new(security, config); let tool = ScheduleTool::new(security, config);
assert_eq!(tool.name(), "schedule"); assert_eq!(tool.name(), "schedule");
let schema = tool.parameters_schema(); let schema = tool.parameters_schema();
@ -394,7 +394,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn list_empty() { async fn list_empty() {
let (_tmp, config, security) = test_setup(); let (_tmp, config, security) = test_setup().await;
let tool = ScheduleTool::new(security, config); let tool = ScheduleTool::new(security, config);
let result = tool.execute(json!({"action": "list"})).await.unwrap(); let result = tool.execute(json!({"action": "list"})).await.unwrap();
@ -404,7 +404,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn create_get_and_cancel_roundtrip() { async fn create_get_and_cancel_roundtrip() {
let (_tmp, config, security) = test_setup(); let (_tmp, config, security) = test_setup().await;
let tool = ScheduleTool::new(security, config); let tool = ScheduleTool::new(security, config);
let create = tool let create = tool
@ -440,7 +440,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn once_and_pause_resume_aliases_work() { async fn once_and_pause_resume_aliases_work() {
let (_tmp, config, security) = test_setup(); let (_tmp, config, security) = test_setup().await;
let tool = ScheduleTool::new(security, config); let tool = ScheduleTool::new(security, config);
let once = tool let once = tool
@ -489,7 +489,7 @@ mod tests {
}, },
..Config::default() ..Config::default()
}; };
std::fs::create_dir_all(&config.workspace_dir).unwrap(); tokio::fs::create_dir_all(&config.workspace_dir).await.unwrap();
let security = Arc::new(SecurityPolicy::from_config( let security = Arc::new(SecurityPolicy::from_config(
&config.autonomy, &config.autonomy,
&config.workspace_dir, &config.workspace_dir,
@ -514,7 +514,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn unknown_action_returns_failure() { async fn unknown_action_returns_failure() {
let (_tmp, config, security) = test_setup(); let (_tmp, config, security) = test_setup().await;
let tool = ScheduleTool::new(security, config); let tool = ScheduleTool::new(security, config);
let result = tool.execute(json!({"action": "explode"})).await.unwrap(); let result = tool.execute(json!({"action": "explode"})).await.unwrap();

View file

@ -363,7 +363,7 @@ mod tests {
.unwrap(); .unwrap();
assert!(allowed.success); assert!(allowed.success);
let _ = std::fs::remove_file(std::env::temp_dir().join("zeroclaw_shell_approval_test")); let _ = tokio::fs::remove_file(std::env::temp_dir().join("zeroclaw_shell_approval_test")).await;
} }
// ── §5.2 Shell timeout enforcement tests ───────────────── // ── §5.2 Shell timeout enforcement tests ─────────────────

View file

@ -6,7 +6,6 @@
//! 3. All build-essential paths are NOT excluded //! 3. All build-essential paths are NOT excluded
//! 4. Pattern syntax is valid //! 4. Pattern syntax is valid
use std::fs;
use std::path::Path; use std::path::Path;
/// Paths that MUST be excluded from Docker build context (security/performance) /// Paths that MUST be excluded from Docker build context (security/performance)
@ -96,8 +95,8 @@ fn is_excluded(patterns: &[String], path: &str) -> bool {
excluded excluded
} }
#[test] #[tokio::test]
fn dockerignore_file_exists() { async fn dockerignore_file_exists() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore"); let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
assert!( assert!(
path.exists(), path.exists(),
@ -105,10 +104,12 @@ fn dockerignore_file_exists() {
); );
} }
#[test] #[tokio::test]
fn dockerignore_excludes_security_critical_paths() { async fn dockerignore_excludes_security_critical_paths() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore"); let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
let content = fs::read_to_string(&path).expect("Failed to read .dockerignore"); let content = tokio::fs::read_to_string(&path)
.await
.expect("Failed to read .dockerignore");
let patterns = parse_dockerignore(&content); let patterns = parse_dockerignore(&content);
for must_exclude in MUST_EXCLUDE { for must_exclude in MUST_EXCLUDE {
@ -129,10 +130,12 @@ fn dockerignore_excludes_security_critical_paths() {
} }
} }
#[test] #[tokio::test]
fn dockerignore_does_not_exclude_build_essentials() { async fn dockerignore_does_not_exclude_build_essentials() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore"); let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
let content = fs::read_to_string(&path).expect("Failed to read .dockerignore"); let content = tokio::fs::read_to_string(&path)
.await
.expect("Failed to read .dockerignore");
let patterns = parse_dockerignore(&content); let patterns = parse_dockerignore(&content);
for must_include in MUST_INCLUDE { for must_include in MUST_INCLUDE {
@ -144,10 +147,12 @@ fn dockerignore_does_not_exclude_build_essentials() {
} }
} }
#[test] #[tokio::test]
fn dockerignore_excludes_git_directory() { async fn dockerignore_excludes_git_directory() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore"); let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
let content = fs::read_to_string(&path).expect("Failed to read .dockerignore"); let content = tokio::fs::read_to_string(&path)
.await
.expect("Failed to read .dockerignore");
let patterns = parse_dockerignore(&content); let patterns = parse_dockerignore(&content);
// .git directory and its contents must be excluded // .git directory and its contents must be excluded
@ -162,10 +167,12 @@ fn dockerignore_excludes_git_directory() {
); );
} }
#[test] #[tokio::test]
fn dockerignore_excludes_target_directory() { async fn dockerignore_excludes_target_directory() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore"); let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
let content = fs::read_to_string(&path).expect("Failed to read .dockerignore"); let content = tokio::fs::read_to_string(&path)
.await
.expect("Failed to read .dockerignore");
let patterns = parse_dockerignore(&content); let patterns = parse_dockerignore(&content);
assert!(is_excluded(&patterns, "target"), "target must be excluded"); assert!(is_excluded(&patterns, "target"), "target must be excluded");
@ -179,10 +186,12 @@ fn dockerignore_excludes_target_directory() {
); );
} }
#[test] #[tokio::test]
fn dockerignore_excludes_database_files() { async fn dockerignore_excludes_database_files() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore"); let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
let content = fs::read_to_string(&path).expect("Failed to read .dockerignore"); let content = tokio::fs::read_to_string(&path)
.await
.expect("Failed to read .dockerignore");
let patterns = parse_dockerignore(&content); let patterns = parse_dockerignore(&content);
assert!( assert!(
@ -199,10 +208,12 @@ fn dockerignore_excludes_database_files() {
); );
} }
#[test] #[tokio::test]
fn dockerignore_excludes_markdown_files() { async fn dockerignore_excludes_markdown_files() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore"); let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
let content = fs::read_to_string(&path).expect("Failed to read .dockerignore"); let content = tokio::fs::read_to_string(&path)
.await
.expect("Failed to read .dockerignore");
let patterns = parse_dockerignore(&content); let patterns = parse_dockerignore(&content);
assert!( assert!(
@ -219,10 +230,12 @@ fn dockerignore_excludes_markdown_files() {
); );
} }
#[test] #[tokio::test]
fn dockerignore_excludes_image_files() { async fn dockerignore_excludes_image_files() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore"); let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
let content = fs::read_to_string(&path).expect("Failed to read .dockerignore"); let content = tokio::fs::read_to_string(&path)
.await
.expect("Failed to read .dockerignore");
let patterns = parse_dockerignore(&content); let patterns = parse_dockerignore(&content);
assert!( assert!(
@ -235,10 +248,12 @@ fn dockerignore_excludes_image_files() {
); );
} }
#[test] #[tokio::test]
fn dockerignore_excludes_env_files() { async fn dockerignore_excludes_env_files() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore"); let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
let content = fs::read_to_string(&path).expect("Failed to read .dockerignore"); let content = tokio::fs::read_to_string(&path)
.await
.expect("Failed to read .dockerignore");
let patterns = parse_dockerignore(&content); let patterns = parse_dockerignore(&content);
assert!( assert!(
@ -247,10 +262,12 @@ fn dockerignore_excludes_env_files() {
); );
} }
#[test] #[tokio::test]
fn dockerignore_excludes_ci_configs() { async fn dockerignore_excludes_ci_configs() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore"); let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
let content = fs::read_to_string(&path).expect("Failed to read .dockerignore"); let content = tokio::fs::read_to_string(&path)
.await
.expect("Failed to read .dockerignore");
let patterns = parse_dockerignore(&content); let patterns = parse_dockerignore(&content);
assert!( assert!(
@ -263,10 +280,12 @@ fn dockerignore_excludes_ci_configs() {
); );
} }
#[test] #[tokio::test]
fn dockerignore_has_valid_syntax() { async fn dockerignore_has_valid_syntax() {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore"); let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(".dockerignore");
let content = fs::read_to_string(&path).expect("Failed to read .dockerignore"); let content = tokio::fs::read_to_string(&path)
.await
.expect("Failed to read .dockerignore");
for (line_num, line) in content.lines().enumerate() { for (line_num, line) in content.lines().enumerate() {
let trimmed = line.trim(); let trimmed = line.trim();
@ -294,8 +313,8 @@ fn dockerignore_has_valid_syntax() {
} }
} }
#[test] #[tokio::test]
fn dockerignore_pattern_matching_edge_cases() { async fn dockerignore_pattern_matching_edge_cases() {
// Test the pattern matching logic itself // Test the pattern matching logic itself
let patterns = vec![ let patterns = vec![
".git".to_string(), ".git".to_string(),