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_scene_skills_dir_path, 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: Vec::new(), }; 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), vec![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_empty()); 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, vec![root.join("skill_lib")]); } #[test] fn resolve_skills_dir_prefers_nested_skills_subdirectory_for_configured_repo_root() { 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: vec![root.join("skill_lib")], }; let resolved = resolve_skills_dir(&root, &settings); assert_eq!(resolved, vec![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: vec![external_skills.clone()], }; let resolved = resolve_skills_dir(&root, &settings); assert_eq!(resolved, vec![external_skills]); } #[test] fn resolve_skills_dir_uses_skills_child_for_external_staged_root() { let root = std::env::temp_dir().join(format!("sgclaw-skills-{}", Uuid::new_v4())); let staged_root = root.join("external/skill_staging"); fs::create_dir_all(staged_root.join("skills")).unwrap(); fs::create_dir_all(staged_root.join("scenes")).unwrap(); let settings = DeepSeekSettings { api_key: "key".to_string(), base_url: "https://api.deepseek.com".to_string(), model: "deepseek-chat".to_string(), skills_dir: vec![staged_root.clone()], }; let resolved = resolve_skills_dir(&root, &settings); assert_eq!(resolved, vec![staged_root.join("skills")]); } #[test] fn resolve_scene_skills_dir_path_prefers_staged_skills_child_under_project_root() { let root = std::env::temp_dir().join(format!("sgclaw-scene-skills-{}", Uuid::new_v4())); let top_level_skills = root.join("project/skills"); fs::create_dir_all(top_level_skills.join("skill_staging/skills")).unwrap(); let resolved = resolve_scene_skills_dir_path(top_level_skills.clone()); assert_eq!(resolved, top_level_skills.join("skill_staging/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(), Vec::new(), ) .unwrap(); assert_eq!(settings.runtime_profile, RuntimeProfile::BrowserAttached); assert_eq!(settings.skills_prompt_mode, SkillsPromptMode::Compact); } #[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, vec![root.join("skill_lib")]); assert_eq!(config.skills.prompt_injection_mode, SkillsPromptMode::Full); } #[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(), Vec::new(), ) .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"); }