use std::env;
use std::path::PathBuf;
use clawft_platform::config_loader::load_config_raw;
use serde_json::Value;
use tokio::sync::Mutex;
static CWD_LOCK: Mutex<()> = Mutex::const_new(());
struct CwdGuard {
original: PathBuf,
}
impl CwdGuard {
fn switch_to(target: &std::path::Path) -> Self {
let original = env::current_dir().expect("read cwd");
env::set_current_dir(target).expect("switch cwd");
Self { original }
}
}
impl Drop for CwdGuard {
fn drop(&mut self) {
let _ = env::set_current_dir(&self.original);
}
}
struct EmptyEnv;
impl clawft_platform::env::Environment for EmptyEnv {
fn get_var(&self, _name: &str) -> Option<String> {
None
}
fn set_var(&self, _name: &str, _value: &str) {}
fn remove_var(&self, _name: &str) {}
}
#[tokio::test]
async fn workspace_overlay_applied_against_real_fs() {
let _guard = CWD_LOCK.lock().await;
let tmp = tempfile::TempDir::new().expect("tempdir");
let dot_clawft = tmp.path().join(".clawft");
std::fs::create_dir_all(&dot_clawft).expect("mkdir .clawft");
std::fs::write(
dot_clawft.join("config.json"),
r#"{
"routing": {
"permissions": {
"channels": { "agent.chat": { "level": 2 } }
}
}
}"#,
)
.expect("write config");
let _cwd = CwdGuard::switch_to(tmp.path());
let fs = clawft_platform::fs::NativeFileSystem;
let env = EmptyEnv;
let raw = load_config_raw(&fs, &env).await.expect("load_config_raw");
let level = raw
.pointer("/routing/permissions/channels/agent.chat/level")
.and_then(Value::as_u64);
assert_eq!(
level,
Some(2),
"workspace overlay must surface agent.chat.level=2; merged: {raw}"
);
}
#[tokio::test]
async fn workspace_overlay_skipped_when_absent_against_real_fs() {
let _guard = CWD_LOCK.lock().await;
let tmp = tempfile::TempDir::new().expect("tempdir");
let _cwd = CwdGuard::switch_to(tmp.path());
let fs = clawft_platform::fs::NativeFileSystem;
let env = EmptyEnv;
let raw = load_config_raw(&fs, &env).await.expect("load_config_raw");
assert!(
raw.pointer("/routing/permissions/channels/agent.chat")
.is_none(),
"no overlay should mean no agent.chat key; merged: {raw}"
);
}
#[tokio::test]
async fn workspace_overlay_invalid_json_is_ignored_against_real_fs() {
let _guard = CWD_LOCK.lock().await;
let tmp = tempfile::TempDir::new().expect("tempdir");
let dot_clawft = tmp.path().join(".clawft");
std::fs::create_dir_all(&dot_clawft).expect("mkdir .clawft");
std::fs::write(dot_clawft.join("config.json"), "{ this is not json")
.expect("write malformed config");
let _cwd = CwdGuard::switch_to(tmp.path());
let fs = clawft_platform::fs::NativeFileSystem;
let env = EmptyEnv;
let raw = load_config_raw(&fs, &env).await.expect("load_config_raw");
assert!(
raw.is_object(),
"loader returns ok despite parse failure; merged: {raw}"
);
assert!(
raw.pointer("/routing/permissions/channels/agent.chat")
.is_none(),
"malformed overlay must not synthesize keys; merged: {raw}"
);
}
#[tokio::test]
async fn workspace_overlay_keys_normalize_to_snake_case_against_real_fs() {
let _guard = CWD_LOCK.lock().await;
let tmp = tempfile::TempDir::new().expect("tempdir");
let dot_clawft = tmp.path().join(".clawft");
std::fs::create_dir_all(&dot_clawft).expect("mkdir .clawft");
std::fs::write(
dot_clawft.join("config.json"),
r#"{ "agentDefaults": { "maxTokens": 8192 } }"#,
)
.expect("write config");
let _cwd = CwdGuard::switch_to(tmp.path());
let fs = clawft_platform::fs::NativeFileSystem;
let env = EmptyEnv;
let raw = load_config_raw(&fs, &env).await.expect("load_config_raw");
assert_eq!(
raw.pointer("/agent_defaults/max_tokens")
.and_then(Value::as_u64),
Some(8192),
"snake_case normalization missing; merged: {raw}"
);
}