Files
claw/tests/compat_config_test.rs
木炎 96c3bf1dee feat: route staged scene skills through runtime
Add registry-driven scene routing and multi-root skill loading so fault-details and 95598 scene skills can be triggered from natural language while still running through the browser-backed runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 16:17:17 +08:00

381 lines
12 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_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<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: 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");
}