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