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>
189 lines
6.7 KiB
Rust
189 lines
6.7 KiB
Rust
use std::fs;
|
||
use std::path::PathBuf;
|
||
|
||
use sgclaw::compat::config_adapter::{
|
||
build_zeroclaw_config_from_sgclaw_settings, resolve_skills_dir_from_sgclaw_settings,
|
||
};
|
||
use sgclaw::config::{BrowserBackend, OfficeBackend, PlannerMode, SgClawSettings};
|
||
use sgclaw::runtime::{RuntimeEngine, RuntimeProfile, ToolPolicy};
|
||
use uuid::Uuid;
|
||
|
||
fn temp_skill_root() -> PathBuf {
|
||
let root = std::env::temp_dir().join(format!(
|
||
"sgclaw-runtime-profile-skills-{}",
|
||
Uuid::new_v4()
|
||
));
|
||
fs::create_dir_all(root.join("skills")).unwrap();
|
||
root
|
||
}
|
||
|
||
fn write_browser_script_skill(skill_root: &std::path::Path, skill_name: &str) {
|
||
let skill_dir = skill_root.join("skills").join(skill_name);
|
||
fs::create_dir_all(&skill_dir).unwrap();
|
||
fs::write(
|
||
skill_dir.join("SKILL.toml"),
|
||
format!(
|
||
r#"
|
||
[skill]
|
||
name = "{skill_name}"
|
||
description = "Browser-only test skill."
|
||
version = "0.1.0"
|
||
|
||
[[tools]]
|
||
name = "run"
|
||
description = "Run browser-only script."
|
||
kind = "browser_script"
|
||
command = "scripts/run.js"
|
||
"#
|
||
),
|
||
)
|
||
.unwrap();
|
||
fs::create_dir_all(skill_dir.join("scripts")).unwrap();
|
||
fs::write(skill_dir.join("scripts/run.js"), "return { ok: true };\n").unwrap();
|
||
}
|
||
|
||
#[test]
|
||
fn loaded_skills_excludes_browser_script_tools_when_browser_surface_is_unavailable() {
|
||
let workspace_root = std::env::temp_dir().join(format!(
|
||
"sgclaw-runtime-profile-workspace-{}",
|
||
Uuid::new_v4()
|
||
));
|
||
fs::create_dir_all(&workspace_root).unwrap();
|
||
let skill_root = temp_skill_root();
|
||
write_browser_script_skill(&skill_root, "fault-details-report");
|
||
|
||
let mut settings = SgClawSettings::from_legacy_deepseek_fields(
|
||
"sk-test".to_string(),
|
||
"https://api.deepseek.com".to_string(),
|
||
"deepseek-chat".to_string(),
|
||
vec![skill_root.clone()],
|
||
)
|
||
.unwrap();
|
||
settings.runtime_profile = RuntimeProfile::GeneralAssistant;
|
||
let config = build_zeroclaw_config_from_sgclaw_settings(&workspace_root, &settings);
|
||
let skills_dir = resolve_skills_dir_from_sgclaw_settings(&workspace_root, &settings);
|
||
let engine = RuntimeEngine::new(RuntimeProfile::GeneralAssistant);
|
||
|
||
let loaded_skills = engine.loaded_skills(&config, &skills_dir);
|
||
|
||
assert!(loaded_skills.is_empty());
|
||
}
|
||
|
||
#[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 browser_attached_publish_prompt_requires_explicit_confirmation_before_clicking_publish() {
|
||
let engine = RuntimeEngine::new(RuntimeProfile::BrowserAttached);
|
||
|
||
let instruction = engine.build_instruction(
|
||
"请直接发表这篇知乎文章,标题是测试标题,正文是第一段内容",
|
||
Some("https://www.zhihu.com/creator"),
|
||
Some("知乎创作中心"),
|
||
true,
|
||
);
|
||
|
||
assert!(instruction.contains("publish a Zhihu article"));
|
||
assert!(instruction.contains("must not click publish without explicit human confirmation"));
|
||
assert!(instruction.contains("ask for confirmation concisely"));
|
||
assert!(instruction.contains("stop after the confirmation request"));
|
||
}
|
||
|
||
#[test]
|
||
fn browser_attached_95598_scene_prompt_requires_scene_tool_before_generic_browser_probing() {
|
||
let engine = RuntimeEngine::new(RuntimeProfile::BrowserAttached);
|
||
|
||
let instruction = engine.build_instruction(
|
||
"请处理95598-repair-city-dispatch场景,查看抢修市指派单并汇总当前队列",
|
||
Some("https://95598.example.invalid/dispatch"),
|
||
Some("95598抢修市指监测"),
|
||
true,
|
||
);
|
||
|
||
assert!(instruction.contains("95598-repair-city-dispatch.collect_repair_orders"));
|
||
assert!(instruction.contains("browser workflow, not a text-only task"));
|
||
assert!(instruction.contains("generic browser probing only after"));
|
||
}
|
||
|
||
#[test]
|
||
fn browser_attached_unrelated_task_does_not_receive_95598_scene_contract() {
|
||
let engine = RuntimeEngine::new(RuntimeProfile::BrowserAttached);
|
||
|
||
let instruction = engine.build_instruction(
|
||
"帮我总结今天的会议纪要",
|
||
None,
|
||
None,
|
||
true,
|
||
);
|
||
|
||
assert!(!instruction.contains("95598-repair-city-dispatch.collect_repair_orders"));
|
||
assert!(!instruction.contains("browser workflow, not a text-only task"));
|
||
assert!(!instruction.contains("generic browser probing only after"));
|
||
}
|
||
|
||
#[test]
|
||
fn general_assistant_95598_scene_prompt_does_not_receive_browser_scene_contract() {
|
||
let engine = RuntimeEngine::new(RuntimeProfile::GeneralAssistant);
|
||
|
||
let instruction = engine.build_instruction(
|
||
"请处理95598-repair-city-dispatch场景,查看抢修市指派单并汇总当前队列",
|
||
Some("https://95598.example.invalid/dispatch"),
|
||
Some("95598抢修市指监测"),
|
||
false,
|
||
);
|
||
|
||
assert!(!instruction.contains("95598-repair-city-dispatch.collect_repair_orders"));
|
||
assert!(!instruction.contains("browser workflow, not a text-only task"));
|
||
assert!(!instruction.contains("generic browser probing only after"));
|
||
}
|
||
|
||
#[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(),
|
||
Vec::new(),
|
||
)
|
||
.unwrap();
|
||
|
||
assert_eq!(settings.planner_mode, PlannerMode::ZeroclawPlanFirst);
|
||
assert_eq!(settings.browser_backend, BrowserBackend::SuperRpa);
|
||
assert_eq!(settings.office_backend, OfficeBackend::OpenXml);
|
||
}
|