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>
224 lines
6.8 KiB
Rust
224 lines
6.8 KiB
Rust
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use sgclaw::runtime::{
|
|
load_scene_registry_from_root, match_scene_instruction_in_registry, DispatchMode,
|
|
};
|
|
use uuid::Uuid;
|
|
|
|
#[test]
|
|
fn scene_registry_loads_first_slice_dispatch_policies() {
|
|
let root = TempSceneRoot::new();
|
|
write_first_slice_scenes(&root);
|
|
|
|
let registry = load_scene_registry_from_root(root.path());
|
|
|
|
let fault_details = registry
|
|
.iter()
|
|
.find(|entry| entry.id == "fault-details-report")
|
|
.expect("fault-details-report scene should load");
|
|
assert_eq!(fault_details.dispatch_mode, DispatchMode::DirectBrowser);
|
|
assert_eq!(fault_details.expected_domain, "sgcc.example.invalid");
|
|
assert_eq!(fault_details.skill_package, "fault-details-report");
|
|
assert_eq!(fault_details.skill_tool, "collect_fault_details");
|
|
|
|
let repair_dispatch = registry
|
|
.iter()
|
|
.find(|entry| entry.id == "95598-repair-city-dispatch")
|
|
.expect("95598-repair-city-dispatch scene should load");
|
|
assert_eq!(repair_dispatch.dispatch_mode, DispatchMode::AgentBrowser);
|
|
assert_eq!(repair_dispatch.skill_package, "95598-repair-city-dispatch");
|
|
assert_eq!(repair_dispatch.skill_tool, "collect_repair_orders");
|
|
}
|
|
|
|
#[test]
|
|
fn scene_registry_matches_fault_details_natural_language_instruction() {
|
|
let root = TempSceneRoot::new();
|
|
write_first_slice_scenes(&root);
|
|
let registry = load_scene_registry_from_root(root.path());
|
|
|
|
let matched =
|
|
match_scene_instruction_in_registry(®istry, "请帮我导出故障明细").expect("scene should match");
|
|
|
|
assert_eq!(matched.id, "fault-details-report");
|
|
assert_eq!(matched.dispatch_mode, DispatchMode::DirectBrowser);
|
|
}
|
|
|
|
#[test]
|
|
fn scene_registry_matches_city_dispatch_natural_language_instruction() {
|
|
let root = TempSceneRoot::new();
|
|
write_first_slice_scenes(&root);
|
|
let registry = load_scene_registry_from_root(root.path());
|
|
|
|
let matched = match_scene_instruction_in_registry(®istry, "帮我看一下95598抢修市指监测")
|
|
.expect("scene should match");
|
|
|
|
assert_eq!(matched.id, "95598-repair-city-dispatch");
|
|
assert_eq!(matched.dispatch_mode, DispatchMode::AgentBrowser);
|
|
}
|
|
|
|
#[test]
|
|
fn scene_registry_matches_rephrased_instruction_via_alias_terms() {
|
|
let root = TempSceneRoot::new();
|
|
write_first_slice_scenes(&root);
|
|
let registry = load_scene_registry_from_root(root.path());
|
|
|
|
let matched = match_scene_instruction_in_registry(®istry, "想看市指那边的95598抢修队列")
|
|
.expect("scene should match");
|
|
|
|
assert_eq!(matched.id, "95598-repair-city-dispatch");
|
|
}
|
|
|
|
#[test]
|
|
fn scene_registry_returns_none_for_unrelated_instruction() {
|
|
let root = TempSceneRoot::new();
|
|
write_first_slice_scenes(&root);
|
|
let registry = load_scene_registry_from_root(root.path());
|
|
|
|
assert!(match_scene_instruction_in_registry(®istry, "今天上海天气怎么样").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn scene_registry_ignores_missing_or_broken_scene_files() {
|
|
let root = TempSceneRoot::new();
|
|
root.write_scene(
|
|
"fault-details-report",
|
|
r#"{
|
|
"id": "fault-details-report",
|
|
"name": "故障明细",
|
|
"summary": "查询故障明细行并生成结构化报表。",
|
|
"inputs": ["period"],
|
|
"outputs": ["report-artifact"],
|
|
"tags": ["fault", "report"],
|
|
"skill": {
|
|
"package": "fault-details-report",
|
|
"tool": "collect_fault_details",
|
|
"artifact_type": "report-artifact"
|
|
}
|
|
}"#,
|
|
);
|
|
root.write_scene("95598-repair-city-dispatch", "{ broken json");
|
|
|
|
let registry = load_scene_registry_from_root(root.path());
|
|
|
|
assert_eq!(registry.len(), 1);
|
|
assert_eq!(registry[0].id, "fault-details-report");
|
|
assert_eq!(registry[0].dispatch_mode, DispatchMode::DirectBrowser);
|
|
}
|
|
|
|
#[test]
|
|
fn scene_registry_ignores_mismatched_scene_metadata_id() {
|
|
let root = TempSceneRoot::new();
|
|
root.write_scene(
|
|
"fault-details-report",
|
|
r#"{
|
|
"id": "wrong-scene-id",
|
|
"name": "故障明细",
|
|
"summary": "查询故障明细行并生成结构化报表。",
|
|
"inputs": ["period"],
|
|
"outputs": ["report-artifact"],
|
|
"tags": ["fault", "report"],
|
|
"skill": {
|
|
"package": "fault-details-report",
|
|
"tool": "collect_fault_details",
|
|
"artifact_type": "report-artifact"
|
|
}
|
|
}"#,
|
|
);
|
|
root.write_scene(
|
|
"95598-repair-city-dispatch",
|
|
r#"{
|
|
"id": "95598-repair-city-dispatch",
|
|
"name": "95598抢修市指监测",
|
|
"summary": "采集95598抢修市指监测列表。",
|
|
"inputs": ["period"],
|
|
"outputs": ["repair-orders"],
|
|
"tags": ["95598", "repair", "dispatch"],
|
|
"skill": {
|
|
"package": "95598-repair-city-dispatch",
|
|
"tool": "collect_repair_orders",
|
|
"artifact_type": "repair-orders"
|
|
}
|
|
}"#,
|
|
);
|
|
|
|
let registry = load_scene_registry_from_root(root.path());
|
|
|
|
assert_eq!(registry.len(), 1);
|
|
assert_eq!(registry[0].id, "95598-repair-city-dispatch");
|
|
}
|
|
|
|
#[test]
|
|
fn scene_registry_returns_none_for_ambiguous_instruction() {
|
|
let root = TempSceneRoot::new();
|
|
write_first_slice_scenes(&root);
|
|
let registry = load_scene_registry_from_root(root.path());
|
|
|
|
assert!(
|
|
match_scene_instruction_in_registry(®istry, "请同时处理导出故障明细和95598抢修市指监测").is_none()
|
|
);
|
|
}
|
|
|
|
struct TempSceneRoot {
|
|
root: PathBuf,
|
|
}
|
|
|
|
impl TempSceneRoot {
|
|
fn new() -> Self {
|
|
let root = std::env::temp_dir().join(format!("scene-registry-test-{}", Uuid::new_v4()));
|
|
fs::create_dir_all(root.join("scenes")).expect("temp scene root should be created");
|
|
Self { root }
|
|
}
|
|
|
|
fn path(&self) -> &Path {
|
|
&self.root
|
|
}
|
|
|
|
fn write_scene(&self, scene_id: &str, contents: &str) {
|
|
let scene_dir = self.root.join("scenes").join(scene_id);
|
|
fs::create_dir_all(&scene_dir).expect("scene directory should be created");
|
|
fs::write(scene_dir.join("scene.json"), contents).expect("scene file should be written");
|
|
}
|
|
}
|
|
|
|
fn write_first_slice_scenes(root: &TempSceneRoot) {
|
|
root.write_scene(
|
|
"fault-details-report",
|
|
r#"{
|
|
"id": "fault-details-report",
|
|
"name": "故障明细",
|
|
"summary": "查询故障明细行并生成结构化报表。",
|
|
"inputs": ["period"],
|
|
"outputs": ["report-artifact"],
|
|
"tags": ["fault", "report"],
|
|
"skill": {
|
|
"package": "fault-details-report",
|
|
"tool": "collect_fault_details",
|
|
"artifact_type": "report-artifact"
|
|
}
|
|
}"#,
|
|
);
|
|
root.write_scene(
|
|
"95598-repair-city-dispatch",
|
|
r#"{
|
|
"id": "95598-repair-city-dispatch",
|
|
"name": "95598抢修市指监测",
|
|
"summary": "采集95598抢修市指监测列表。",
|
|
"inputs": ["period"],
|
|
"outputs": ["repair-orders"],
|
|
"tags": ["95598", "repair", "dispatch"],
|
|
"skill": {
|
|
"package": "95598-repair-city-dispatch",
|
|
"tool": "collect_repair_orders",
|
|
"artifact_type": "repair-orders"
|
|
}
|
|
}"#,
|
|
);
|
|
}
|
|
|
|
impl Drop for TempSceneRoot {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.root);
|
|
}
|
|
}
|