use std::fs; use std::path::{Path, PathBuf}; use sgclaw::compat::scene_platform::registry::{load_scene_registry, SceneRegistryError}; use uuid::Uuid; fn temp_root(prefix: &str) -> PathBuf { let root = std::env::temp_dir().join(format!("{prefix}-{}", Uuid::new_v4())); fs::create_dir_all(&root).unwrap(); root } fn write_skill( root: &Path, skill_name: &str, skill_toml: &str, scene_toml: Option<&str>, ) -> PathBuf { let skill_root = root.join(skill_name); fs::create_dir_all(&skill_root).unwrap(); fs::write(skill_root.join("SKILL.toml"), skill_toml).unwrap(); if let Some(scene_toml) = scene_toml { fs::write(skill_root.join("scene.toml"), scene_toml).unwrap(); } skill_root } fn browser_script_skill_toml(skill_name: &str, tool_name: &str, tool_kind: &str) -> String { format!( r#"[skill] name = "{skill_name}" description = "test skill" version = "0.1.0" [[tools]] name = "{tool_name}" description = "test tool" kind = "{tool_kind}" command = "scripts/{tool_name}.js" "# ) } fn scene_toml( scene_id: &str, skill_name: &str, tool_name: &str, schema_version: &str, kind: &str, ) -> String { format!( r#"[scene] id = "{scene_id}" skill = "{skill_name}" tool = "{tool_name}" kind = "{kind}" version = "0.1.0" category = "report_collection" [manifest] schema_version = "{schema_version}" [bootstrap] expected_domain = "20.76.57.61" target_url = "http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor" requires_target_page = true [deterministic] suffix = "。。。" include_keywords = ["线损"] exclude_keywords = ["知乎"] [[params]] name = "org" resolver = "dictionary_entity" required = true prompt_missing = "缺少供电单位" prompt_ambiguous = "供电单位存在歧义" [params.resolver_config] dictionary_ref = "references/org-dictionary.json" output_label_field = "org_label" output_code_field = "org_code" [artifact] type = "report-artifact" success_status = ["ok", "partial", "empty"] failure_status = ["blocked", "error"] "# ) } #[test] fn registry_loads_scene_manifest_from_skill_root() { let skills_root = temp_root("sgclaw-scene-registry"); write_skill( &skills_root, "tq-lineloss-report", &browser_script_skill_toml("tq-lineloss-report", "collect_lineloss", "browser_script"), Some(&scene_toml( "tq-lineloss-report", "tq-lineloss-report", "collect_lineloss", "1", "browser_script", )), ); let registry = load_scene_registry(&skills_root).unwrap(); assert_eq!(registry.len(), 1); assert_eq!(registry[0].manifest.scene.id, "tq-lineloss-report"); assert_eq!( registry[0].skill_root, skills_root.join("tq-lineloss-report") ); assert!(registry[0].skill_root.join("scene.toml").exists()); assert!(!registry[0] .skill_root .to_string_lossy() .contains("skill_staging/scenes")); } #[test] fn registry_rejects_duplicate_scene_ids_with_both_paths_in_error() { let skills_root = temp_root("sgclaw-scene-registry-dup"); let first = write_skill( &skills_root, "skill-a", &browser_script_skill_toml("skill-a", "collect_a", "browser_script"), Some(&scene_toml( "duplicate-scene", "skill-a", "collect_a", "1", "browser_script", )), ); let second = write_skill( &skills_root, "skill-b", &browser_script_skill_toml("skill-b", "collect_b", "browser_script"), Some(&scene_toml( "duplicate-scene", "skill-b", "collect_b", "1", "browser_script", )), ); let err = load_scene_registry(&skills_root).expect_err("duplicate scene ids should fail"); let message = err.to_string(); assert!(matches!(err, SceneRegistryError::DuplicateSceneId { .. })); assert!(message.contains(&first.join("scene.toml").display().to_string())); assert!(message.contains(&second.join("scene.toml").display().to_string())); } #[test] fn registry_rejects_unknown_manifest_schema_version() { let skills_root = temp_root("sgclaw-scene-registry-schema"); write_skill( &skills_root, "tq-lineloss-report", &browser_script_skill_toml("tq-lineloss-report", "collect_lineloss", "browser_script"), Some(&scene_toml( "tq-lineloss-report", "tq-lineloss-report", "collect_lineloss", "999", "browser_script", )), ); let err = load_scene_registry(&skills_root).expect_err("unknown schema version should fail"); let message = err.to_string(); assert!(matches!( err, SceneRegistryError::UnsupportedSchemaVersion { .. } )); assert!(message.contains("999")); } #[test] fn registry_rejects_non_browser_script_scene_tool_in_v1() { let skills_root = temp_root("sgclaw-scene-registry-kind"); write_skill( &skills_root, "shell-scene", &browser_script_skill_toml("shell-scene", "collect_shell", "shell"), Some(&scene_toml( "shell-scene", "shell-scene", "collect_shell", "1", "shell", )), ); let err = load_scene_registry(&skills_root).expect_err("non browser_script scenes should fail in v1"); let message = err.to_string(); assert!(matches!( err, SceneRegistryError::UnsupportedSceneKind { .. } )); assert!(message.contains("browser_script")); } #[test] fn committed_lineloss_sample_package_is_registration_ready() { let skills_root = Path::new("examples/generated_scene_platform/skills"); let registry = load_scene_registry(skills_root).unwrap(); let entry = registry .iter() .find(|entry| entry.manifest.scene.id == "tq-lineloss-report") .expect("committed line-loss sample package should be registered"); assert_eq!(entry.manifest.scene.skill, "tq-lineloss-report"); assert_eq!(entry.manifest.scene.tool, "collect_lineloss"); assert_eq!(entry.manifest.scene.kind, "browser_script"); assert_eq!(entry.manifest.scene.category, "report_collection"); assert_eq!(entry.manifest.bootstrap.expected_domain, "20.76.57.61"); assert_eq!( entry.manifest.bootstrap.target_url, "http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor" ); assert!(entry .manifest .deterministic .include_keywords .iter() .any(|keyword| keyword == "统计分析")); assert!(entry .skill_root .join("references") .join("org-dictionary.json") .exists()); assert!(entry .skill_root .join("scripts") .join("collect_lineloss.js") .exists()); }