428 lines
13 KiB
Rust
428 lines
13 KiB
Rust
use std::fs;
|
|
use std::path::Path;
|
|
use std::sync::{Mutex, OnceLock};
|
|
|
|
use sgclaw::compat::config_adapter::{
|
|
build_zeroclaw_config, build_zeroclaw_config_from_settings,
|
|
build_zeroclaw_config_from_sgclaw_settings, resolve_skills_dir, zeroclaw_default_skills_dir,
|
|
zeroclaw_workspace_dir,
|
|
};
|
|
use sgclaw::config::{
|
|
BrowserBackend, DeepSeekSettings, OfficeBackend, PlannerMode, SgClawSettings, SkillsPromptMode,
|
|
};
|
|
use sgclaw::runtime::RuntimeProfile;
|
|
use uuid::Uuid;
|
|
|
|
fn env_lock() -> &'static Mutex<()> {
|
|
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
|
LOCK.get_or_init(|| Mutex::new(()))
|
|
}
|
|
|
|
#[test]
|
|
fn zeroclaw_config_adapter_maps_deepseek_env_to_zeroclaw_config() {
|
|
let _guard = env_lock().lock().unwrap();
|
|
std::env::set_var("DEEPSEEK_API_KEY", "deepseek-test-key");
|
|
std::env::set_var("DEEPSEEK_BASE_URL", "https://api.deepseek.com");
|
|
std::env::set_var("DEEPSEEK_MODEL", "deepseek-chat");
|
|
|
|
let config = build_zeroclaw_config(Path::new("/tmp/sgclaw")).unwrap();
|
|
|
|
assert_eq!(config.default_provider.as_deref(), Some("deepseek"));
|
|
assert_eq!(config.default_model.as_deref(), Some("deepseek-chat"));
|
|
assert_eq!(config.api_key.as_deref(), Some("deepseek-test-key"));
|
|
assert_eq!(config.api_url.as_deref(), Some("https://api.deepseek.com"));
|
|
assert_eq!(
|
|
config.workspace_dir,
|
|
Path::new("/tmp/sgclaw/.sgclaw-zeroclaw-workspace")
|
|
);
|
|
assert_eq!(
|
|
config.config_path,
|
|
Path::new("/tmp/sgclaw/.sgclaw-zeroclaw-workspace/config.toml")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn zeroclaw_config_adapter_uses_deterministic_workspace_dir() {
|
|
let settings = DeepSeekSettings {
|
|
api_key: "key".to_string(),
|
|
base_url: "https://proxy.example.com/v1".to_string(),
|
|
model: "deepseek-reasoner".to_string(),
|
|
skills_dir: None,
|
|
};
|
|
|
|
let workspace_dir = zeroclaw_workspace_dir(Path::new("/var/lib/sgclaw"));
|
|
let config = build_zeroclaw_config_from_settings(Path::new("/var/lib/sgclaw"), &settings);
|
|
|
|
assert_eq!(
|
|
workspace_dir,
|
|
Path::new("/var/lib/sgclaw/.sgclaw-zeroclaw-workspace")
|
|
);
|
|
assert_eq!(config.workspace_dir, workspace_dir);
|
|
assert_eq!(config.default_provider.as_deref(), Some("deepseek"));
|
|
assert_eq!(config.default_model.as_deref(), Some("deepseek-reasoner"));
|
|
assert_eq!(
|
|
config.api_url.as_deref(),
|
|
Some("https://proxy.example.com/v1")
|
|
);
|
|
assert_eq!(
|
|
resolve_skills_dir(Path::new("/var/lib/sgclaw"), &settings),
|
|
zeroclaw_default_skills_dir(Path::new("/var/lib/sgclaw"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn deepseek_settings_reload_from_browser_config_path_after_file_changes() {
|
|
let root = std::env::temp_dir().join(format!("sgclaw-config-{}", Uuid::new_v4()));
|
|
fs::create_dir_all(&root).unwrap();
|
|
let config_path = root.join("sgclaw_config.json");
|
|
|
|
fs::write(
|
|
&config_path,
|
|
r#"{
|
|
"apiKey": "sk-first",
|
|
"baseUrl": "",
|
|
"model": ""
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let first = DeepSeekSettings::load(Some(config_path.as_path()))
|
|
.unwrap()
|
|
.expect("expected config file to produce settings");
|
|
assert_eq!(first.api_key, "sk-first");
|
|
assert_eq!(first.base_url, "https://api.deepseek.com");
|
|
assert_eq!(first.model, "deepseek-chat");
|
|
assert!(first.skills_dir.is_none());
|
|
|
|
fs::write(
|
|
&config_path,
|
|
r#"{
|
|
"apiKey": "sk-second",
|
|
"baseUrl": "https://proxy.example.com/v1",
|
|
"model": "deepseek-reasoner",
|
|
"skillsDir": "skill_lib"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let second = DeepSeekSettings::load(Some(config_path.as_path()))
|
|
.unwrap()
|
|
.expect("expected updated config file to produce settings");
|
|
assert_eq!(second.api_key, "sk-second");
|
|
assert_eq!(second.base_url, "https://proxy.example.com/v1");
|
|
assert_eq!(second.model, "deepseek-reasoner");
|
|
assert_eq!(second.skills_dir, Some(root.join("skill_lib")));
|
|
}
|
|
|
|
#[test]
|
|
fn ws_cleanup_resolves_single_configured_skills_dir() {
|
|
let root = std::env::temp_dir().join(format!("sgclaw-skills-{}", Uuid::new_v4()));
|
|
fs::create_dir_all(root.join("skill_lib/skills")).unwrap();
|
|
let settings = DeepSeekSettings {
|
|
api_key: "key".to_string(),
|
|
base_url: "https://api.deepseek.com".to_string(),
|
|
model: "deepseek-chat".to_string(),
|
|
skills_dir: Some(root.join("skill_lib")),
|
|
};
|
|
|
|
let resolved = resolve_skills_dir(&root, &settings);
|
|
|
|
assert_eq!(resolved, root.join("skill_lib/skills"));
|
|
}
|
|
|
|
#[test]
|
|
fn resolve_skills_dir_preserves_absolute_configured_skills_directory() {
|
|
let root = std::env::temp_dir().join(format!("sgclaw-skills-{}", Uuid::new_v4()));
|
|
let external_skills = root.join("external-skill-lib/skills");
|
|
fs::create_dir_all(&external_skills).unwrap();
|
|
let settings = DeepSeekSettings {
|
|
api_key: "key".to_string(),
|
|
base_url: "https://api.deepseek.com".to_string(),
|
|
model: "deepseek-chat".to_string(),
|
|
skills_dir: Some(external_skills.clone()),
|
|
};
|
|
|
|
let resolved = resolve_skills_dir(&root, &settings);
|
|
|
|
assert_eq!(resolved, external_skills);
|
|
}
|
|
|
|
#[test]
|
|
fn sgclaw_settings_default_to_compact_skills_and_browser_attached_profile() {
|
|
let settings = SgClawSettings::from_legacy_deepseek_fields(
|
|
"sk-test".to_string(),
|
|
"https://api.deepseek.com".to_string(),
|
|
"deepseek-chat".to_string(),
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(settings.runtime_profile, RuntimeProfile::BrowserAttached);
|
|
assert_eq!(settings.skills_prompt_mode, SkillsPromptMode::Compact);
|
|
}
|
|
|
|
#[test]
|
|
fn sgclaw_settings_load_direct_submit_only_config_and_resolve_relative_skills_dir() {
|
|
let root = std::env::temp_dir().join(format!(
|
|
"sgclaw-direct-submit-only-config-{}",
|
|
Uuid::new_v4()
|
|
));
|
|
fs::create_dir_all(&root).unwrap();
|
|
let config_path = root.join("sgclaw_config.json");
|
|
|
|
fs::write(
|
|
&config_path,
|
|
r#"{
|
|
"providers": [],
|
|
"skillsDir": "skill_lib",
|
|
"directSubmitSkill": "fault-details-report.collect_fault_details"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let settings = SgClawSettings::load(Some(config_path.as_path()))
|
|
.unwrap()
|
|
.expect("expected sgclaw settings from config file");
|
|
|
|
assert_eq!(
|
|
settings.direct_submit_skill.as_deref(),
|
|
Some("fault-details-report.collect_fault_details")
|
|
);
|
|
assert_eq!(settings.skills_dir, Some(root.join("skill_lib")));
|
|
}
|
|
|
|
#[test]
|
|
fn sgclaw_settings_reject_invalid_direct_submit_skill_format() {
|
|
let root = std::env::temp_dir().join(format!(
|
|
"sgclaw-invalid-direct-submit-skill-{}",
|
|
Uuid::new_v4()
|
|
));
|
|
fs::create_dir_all(&root).unwrap();
|
|
let config_path = root.join("sgclaw_config.json");
|
|
|
|
fs::write(
|
|
&config_path,
|
|
r#"{
|
|
"providers": [],
|
|
"skillsDir": "skill_lib",
|
|
"directSubmitSkill": "fault-details-report"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let err = SgClawSettings::load(Some(config_path.as_path()))
|
|
.expect_err("expected invalid directSubmitSkill format");
|
|
let message = err.to_string();
|
|
|
|
assert!(message.contains("directSubmitSkill"));
|
|
assert!(message.contains("skill.tool"));
|
|
}
|
|
|
|
#[test]
|
|
fn sgclaw_settings_load_new_runtime_fields_from_browser_config() {
|
|
let root = std::env::temp_dir().join(format!("sgclaw-runtime-config-{}", Uuid::new_v4()));
|
|
fs::create_dir_all(&root).unwrap();
|
|
let config_path = root.join("sgclaw_config.json");
|
|
|
|
fs::write(
|
|
&config_path,
|
|
r#"{
|
|
"apiKey": "sk-runtime",
|
|
"baseUrl": "https://api.deepseek.com",
|
|
"model": "deepseek-chat",
|
|
"skillsDir": "skill_lib",
|
|
"runtimeProfile": "generalAssistant",
|
|
"skillsPromptMode": "full"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let settings = SgClawSettings::load(Some(config_path.as_path()))
|
|
.unwrap()
|
|
.expect("expected sgclaw settings from config file");
|
|
let config = build_zeroclaw_config_from_sgclaw_settings(&root, &settings);
|
|
|
|
assert_eq!(settings.runtime_profile, RuntimeProfile::GeneralAssistant);
|
|
assert_eq!(settings.skills_prompt_mode, SkillsPromptMode::Full);
|
|
assert_eq!(settings.skills_dir, Some(root.join("skill_lib")));
|
|
assert_eq!(config.skills.prompt_injection_mode, SkillsPromptMode::Full);
|
|
}
|
|
|
|
#[test]
|
|
fn ws_cleanup_rejects_array_style_skills_dir_config() {
|
|
let root = std::env::temp_dir().join(format!("sgclaw-config-{}", uuid::Uuid::new_v4()));
|
|
std::fs::create_dir_all(&root).unwrap();
|
|
let config_path = root.join("sgclaw_config.json");
|
|
std::fs::write(
|
|
&config_path,
|
|
r#"{
|
|
"apiKey": "sk-test",
|
|
"baseUrl": "https://api.deepseek.com",
|
|
"model": "deepseek-chat",
|
|
"skillsDir": ["skill_lib", "skill_staging"]
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
assert!(sgclaw::config::SgClawSettings::load(Some(config_path.as_path())).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn sgclaw_settings_load_browser_ws_url_from_browser_config() {
|
|
let root = std::env::temp_dir().join(format!("sgclaw-browser-ws-config-{}", Uuid::new_v4()));
|
|
fs::create_dir_all(&root).unwrap();
|
|
let config_path = root.join("sgclaw_config.json");
|
|
|
|
fs::write(
|
|
&config_path,
|
|
r#"{
|
|
"apiKey": "sk-runtime",
|
|
"baseUrl": "https://api.deepseek.com",
|
|
"model": "deepseek-chat",
|
|
"browserWsUrl": "ws://127.0.0.1:12345"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let settings = SgClawSettings::load(Some(config_path.as_path()))
|
|
.unwrap()
|
|
.expect("expected sgclaw settings from config file");
|
|
|
|
assert_eq!(
|
|
settings.browser_ws_url.as_deref(),
|
|
Some("ws://127.0.0.1:12345")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn sgclaw_settings_load_service_ws_listen_addr_from_browser_config() {
|
|
let root = std::env::temp_dir().join(format!("sgclaw-service-ws-config-{}", Uuid::new_v4()));
|
|
fs::create_dir_all(&root).unwrap();
|
|
let config_path = root.join("sgclaw_config.json");
|
|
|
|
fs::write(
|
|
&config_path,
|
|
r#"{
|
|
"apiKey": "sk-runtime",
|
|
"baseUrl": "https://api.deepseek.com",
|
|
"model": "deepseek-chat",
|
|
"serviceWsListenAddr": "127.0.0.1:42321"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let settings = SgClawSettings::load(Some(config_path.as_path()))
|
|
.unwrap()
|
|
.expect("expected sgclaw settings from config file");
|
|
|
|
assert_eq!(
|
|
settings.service_ws_listen_addr.as_deref(),
|
|
Some("127.0.0.1:42321")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn browser_attached_config_uses_low_temperature_for_deterministic_execution() {
|
|
let settings = SgClawSettings::from_legacy_deepseek_fields(
|
|
"sk-test".to_string(),
|
|
"https://api.deepseek.com".to_string(),
|
|
"deepseek-chat".to_string(),
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let config = build_zeroclaw_config_from_sgclaw_settings(Path::new("/tmp/sgclaw"), &settings);
|
|
|
|
assert_eq!(config.default_temperature, 0.0);
|
|
}
|
|
|
|
#[test]
|
|
fn sgclaw_settings_load_provider_switching_and_backend_policy_from_browser_config() {
|
|
let root = std::env::temp_dir().join(format!("sgclaw-provider-config-{}", Uuid::new_v4()));
|
|
fs::create_dir_all(&root).unwrap();
|
|
let config_path = root.join("sgclaw_config.json");
|
|
|
|
fs::write(
|
|
&config_path,
|
|
r#"{
|
|
"apiKey": "sk-legacy",
|
|
"baseUrl": "https://api.deepseek.com",
|
|
"model": "deepseek-chat",
|
|
"plannerMode": "zeroclawPlanFirst",
|
|
"activeProvider": "glm-prod",
|
|
"browserBackend": "superrpa",
|
|
"officeBackend": "openxml",
|
|
"providers": [
|
|
{
|
|
"id": "deepseek-default",
|
|
"provider": "deepseek",
|
|
"apiKey": "sk-deepseek",
|
|
"baseUrl": "https://api.deepseek.com",
|
|
"model": "deepseek-chat"
|
|
},
|
|
{
|
|
"id": "glm-prod",
|
|
"provider": "glm",
|
|
"apiKey": "sk-glm",
|
|
"baseUrl": "https://open.bigmodel.cn/api/paas/v4",
|
|
"model": "glm-4.5"
|
|
}
|
|
]
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let settings = SgClawSettings::load(Some(config_path.as_path()))
|
|
.unwrap()
|
|
.expect("expected sgclaw settings from config file");
|
|
let config = build_zeroclaw_config_from_sgclaw_settings(&root, &settings);
|
|
|
|
assert_eq!(settings.planner_mode, PlannerMode::ZeroclawPlanFirst);
|
|
assert_eq!(settings.active_provider, "glm-prod");
|
|
assert_eq!(settings.providers.len(), 2);
|
|
assert_eq!(
|
|
settings.provider_base_url,
|
|
"https://open.bigmodel.cn/api/paas/v4"
|
|
);
|
|
assert_eq!(settings.provider_model, "glm-4.5");
|
|
assert_eq!(settings.browser_backend, BrowserBackend::SuperRpa);
|
|
assert_eq!(settings.office_backend, OfficeBackend::OpenXml);
|
|
assert_eq!(config.default_provider.as_deref(), Some("glm"));
|
|
assert_eq!(config.default_model.as_deref(), Some("glm-4.5"));
|
|
assert_eq!(config.api_key.as_deref(), Some("sk-glm"));
|
|
assert_eq!(
|
|
config.api_url.as_deref(),
|
|
Some("https://open.bigmodel.cn/api/paas/v4")
|
|
);
|
|
assert!(!config.browser.enabled);
|
|
assert!(config.model_providers.contains_key("deepseek-default"));
|
|
assert!(config.model_providers.contains_key("glm-prod"));
|
|
}
|
|
|
|
#[test]
|
|
fn sgclaw_settings_enable_non_host_browser_backend_when_requested() {
|
|
let root = std::env::temp_dir().join(format!("sgclaw-browser-backend-{}", Uuid::new_v4()));
|
|
fs::create_dir_all(&root).unwrap();
|
|
let config_path = root.join("sgclaw_config.json");
|
|
|
|
fs::write(
|
|
&config_path,
|
|
r#"{
|
|
"apiKey": "sk-runtime",
|
|
"baseUrl": "https://api.deepseek.com",
|
|
"model": "deepseek-chat",
|
|
"browserBackend": "rustNative"
|
|
}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let settings = SgClawSettings::load(Some(config_path.as_path()))
|
|
.unwrap()
|
|
.expect("expected sgclaw settings from config file");
|
|
let config = build_zeroclaw_config_from_sgclaw_settings(&root, &settings);
|
|
|
|
assert_eq!(settings.browser_backend, BrowserBackend::RustNative);
|
|
assert!(config.browser.enabled);
|
|
assert_eq!(config.browser.backend, "rust_native");
|
|
}
|