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> = 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"); }