sgclaw: move runtime policy into config
This commit is contained in:
@@ -4,10 +4,21 @@ use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use sgclaw::compat::config_adapter::{
|
||||
build_zeroclaw_config,
|
||||
build_zeroclaw_config_from_sgclaw_settings,
|
||||
build_zeroclaw_config_from_settings,
|
||||
resolve_skills_dir,
|
||||
zeroclaw_default_skills_dir,
|
||||
zeroclaw_workspace_dir,
|
||||
};
|
||||
use sgclaw::config::DeepSeekSettings;
|
||||
use sgclaw::config::{
|
||||
BrowserBackend,
|
||||
DeepSeekSettings,
|
||||
OfficeBackend,
|
||||
PlannerMode,
|
||||
SgClawSettings,
|
||||
SkillsPromptMode,
|
||||
};
|
||||
use sgclaw::runtime::RuntimeProfile;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn env_lock() -> &'static Mutex<()> {
|
||||
@@ -44,6 +55,7 @@ fn zeroclaw_config_adapter_uses_deterministic_workspace_dir() {
|
||||
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"));
|
||||
@@ -54,6 +66,10 @@ fn zeroclaw_config_adapter_uses_deterministic_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]
|
||||
@@ -78,13 +94,15 @@ fn deepseek_settings_reload_from_browser_config_path_after_file_changes() {
|
||||
assert_eq!(first.api_key, "sk-first");
|
||||
assert_eq!(first.base_url, "https://api.deepseek.com");
|
||||
assert_eq!(first.model, "deepseek-chat");
|
||||
assert_eq!(first.skills_dir, None);
|
||||
|
||||
fs::write(
|
||||
&config_path,
|
||||
r#"{
|
||||
"apiKey": "sk-second",
|
||||
"baseUrl": "https://proxy.example.com/v1",
|
||||
"model": "deepseek-reasoner"
|
||||
"model": "deepseek-reasoner",
|
||||
"skillsDir": "skill_lib"
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -95,4 +113,184 @@ fn deepseek_settings_reload_from_browser_config_path_after_file_changes() {
|
||||
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 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: 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_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 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");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
use serde_json::json;
|
||||
use sgclaw::agent::planner::{plan_instruction, PlannerError};
|
||||
use sgclaw::agent::planner::{build_execution_preview, plan_instruction, PlannerError};
|
||||
use sgclaw::config::PlannerMode;
|
||||
use sgclaw::pipe::Action;
|
||||
|
||||
#[test]
|
||||
fn planner_module_is_explicitly_legacy_dev_only() {
|
||||
assert!(sgclaw::agent::planner::LEGACY_DEV_ONLY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planner_converts_baidu_search_instruction_into_three_steps() {
|
||||
let plan = plan_instruction("打开百度搜索天气").unwrap();
|
||||
@@ -48,6 +54,36 @@ fn planner_supports_zhihu_search_instruction_with_direct_search_url() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planner_supports_open_zhihu_homepage_instruction() {
|
||||
let plan = plan_instruction("打开知乎").unwrap();
|
||||
|
||||
assert_eq!(plan.summary, "已打开知乎首页");
|
||||
assert_eq!(plan.steps.len(), 1);
|
||||
assert_eq!(plan.steps[0].action, Action::Navigate);
|
||||
assert_eq!(
|
||||
plan.steps[0].params,
|
||||
json!({ "url": "https://www.zhihu.com" })
|
||||
);
|
||||
assert_eq!(plan.steps[0].expected_domain, "www.zhihu.com");
|
||||
assert_eq!(plan.steps[0].log_message, "navigate https://www.zhihu.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planner_supports_open_baidu_homepage_instruction() {
|
||||
let plan = plan_instruction("打开百度").unwrap();
|
||||
|
||||
assert_eq!(plan.summary, "已打开百度首页");
|
||||
assert_eq!(plan.steps.len(), 1);
|
||||
assert_eq!(plan.steps[0].action, Action::Navigate);
|
||||
assert_eq!(
|
||||
plan.steps[0].params,
|
||||
json!({ "url": "https://www.baidu.com" })
|
||||
);
|
||||
assert_eq!(plan.steps[0].expected_domain, "www.baidu.com");
|
||||
assert_eq!(plan.steps[0].log_message, "navigate https://www.baidu.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planner_rejects_unrelated_instruction() {
|
||||
let err = plan_instruction("打开谷歌搜索天气").unwrap_err();
|
||||
@@ -57,3 +93,37 @@ fn planner_rejects_unrelated_instruction() {
|
||||
PlannerError::UnsupportedInstruction("打开谷歌搜索天气".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_first_mode_builds_visible_preview_for_zhihu_excel_flow() {
|
||||
let preview = build_execution_preview(
|
||||
PlannerMode::ZeroclawPlanFirst,
|
||||
"读取知乎热榜数据,并导出 excel 文件",
|
||||
Some("https://www.zhihu.com/hot"),
|
||||
Some("知乎热榜"),
|
||||
)
|
||||
.expect("expected plan preview");
|
||||
|
||||
assert_eq!(preview.summary, "先规划再执行知乎热榜 Excel 导出");
|
||||
assert!(preview
|
||||
.steps
|
||||
.iter()
|
||||
.any(|step| step.contains("navigate https://www.zhihu.com/hot")));
|
||||
assert!(preview.steps.iter().any(|step| step.contains("getText main")));
|
||||
assert!(preview
|
||||
.steps
|
||||
.iter()
|
||||
.any(|step| step.contains("call openxml_office")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_planner_mode_skips_runtime_preview() {
|
||||
let preview = build_execution_preview(
|
||||
PlannerMode::LegacyDeterministic,
|
||||
"打开百度搜索天气",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
assert!(preview.is_none());
|
||||
}
|
||||
|
||||
55
tests/runtime_profile_test.rs
Normal file
55
tests/runtime_profile_test.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use sgclaw::runtime::{RuntimeEngine, RuntimeProfile, ToolPolicy};
|
||||
use sgclaw::config::{BrowserBackend, OfficeBackend, PlannerMode, SgClawSettings};
|
||||
|
||||
#[test]
|
||||
fn browser_attached_profile_exposes_browser_surface_without_becoming_browser_only() {
|
||||
let profile = RuntimeProfile::BrowserAttached;
|
||||
let policy = ToolPolicy::for_profile(profile);
|
||||
|
||||
assert!(policy.allowed_tools.contains(&"browser_action".to_string()));
|
||||
assert!(policy
|
||||
.allowed_tools
|
||||
.contains(&"superrpa_browser".to_string()));
|
||||
assert!(policy.may_use_non_browser_tools);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn general_assistant_profile_does_not_require_browser_surface() {
|
||||
let profile = RuntimeProfile::GeneralAssistant;
|
||||
let policy = ToolPolicy::for_profile(profile);
|
||||
|
||||
assert!(!policy.requires_browser_surface);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn browser_attached_export_prompt_requires_openxml_completion() {
|
||||
let engine = RuntimeEngine::new(RuntimeProfile::BrowserAttached);
|
||||
|
||||
let instruction = engine.build_instruction(
|
||||
"读取知乎热榜数据,并导出 excel 文件",
|
||||
Some("https://www.zhihu.com/hot"),
|
||||
Some("知乎热榜"),
|
||||
true,
|
||||
);
|
||||
|
||||
assert!(instruction.contains("must call openxml_office"));
|
||||
assert!(instruction.contains("Do not stop after describing how you will parse"));
|
||||
assert!(instruction.contains("Never fabricate, simulate, or invent substitute hotlist data"));
|
||||
assert!(instruction.contains("Do not repeat the same sentence or section"));
|
||||
assert!(instruction.contains("final answer must include the generated local .xlsx path"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_settings_default_to_plan_first_superrpa_and_openxml_backends() {
|
||||
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.planner_mode, PlannerMode::ZeroclawPlanFirst);
|
||||
assert_eq!(settings.browser_backend, BrowserBackend::SuperRpa);
|
||||
assert_eq!(settings.office_backend, OfficeBackend::OpenXml);
|
||||
}
|
||||
Reference in New Issue
Block a user