feat: add generated scene skill platform hardening
This commit is contained in:
@@ -1,126 +1,501 @@
|
||||
mod common;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::fs;
|
||||
use std::panic::AssertUnwindSafe;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use chrono::{Datelike, Local};
|
||||
use zeroclaw::skills::load_skills_from_directory;
|
||||
|
||||
use sgclaw::compat::deterministic_submit::{
|
||||
decide_deterministic_submit, DeterministicSubmitDecision,
|
||||
decide_deterministic_submit, decide_deterministic_submit_with_skills_dir,
|
||||
DeterministicSubmitDecision,
|
||||
};
|
||||
use sgclaw::compat::tq_lineloss::{
|
||||
contracts::{PeriodMode, ResolvedOrg, ResolvedPeriod},
|
||||
org_resolver::resolve_org,
|
||||
period_resolver::resolve_period,
|
||||
};
|
||||
use sgclaw::runtime::is_zhihu_hotlist_task;
|
||||
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 current_dir_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
}
|
||||
|
||||
fn with_temp_workspace<T>(prefix: &str, test: impl FnOnce(&Path) -> T) -> T {
|
||||
let _guard = current_dir_lock().lock().unwrap();
|
||||
let workspace_root = temp_root(prefix);
|
||||
let original_dir = std::env::current_dir().unwrap();
|
||||
std::env::set_current_dir(&workspace_root).unwrap();
|
||||
|
||||
let result = std::panic::catch_unwind(AssertUnwindSafe(|| test(&workspace_root)));
|
||||
|
||||
std::env::set_current_dir(original_dir).unwrap();
|
||||
match result {
|
||||
Ok(value) => value,
|
||||
Err(payload) => std::panic::resume_unwind(payload),
|
||||
}
|
||||
}
|
||||
|
||||
fn default_skills_root(workspace_root: &Path) -> PathBuf {
|
||||
workspace_root
|
||||
.join(".sgclaw-zeroclaw-workspace")
|
||||
.join("skills")
|
||||
}
|
||||
|
||||
fn browser_script_skill_toml(skill_name: &str, tool_name: &str) -> String {
|
||||
format!(
|
||||
r#"[skill]
|
||||
name = "{skill_name}"
|
||||
description = "test skill"
|
||||
version = "0.1.0"
|
||||
|
||||
[[tools]]
|
||||
name = "{tool_name}"
|
||||
description = "test tool"
|
||||
kind = "browser_script"
|
||||
command = "scripts/{tool_name}.js"
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
fn toml_array(values: &[&str]) -> String {
|
||||
if values.is_empty() {
|
||||
return "[]".to_string();
|
||||
}
|
||||
|
||||
let joined = values
|
||||
.iter()
|
||||
.map(|value| format!("\"{value}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!("[{joined}]")
|
||||
}
|
||||
|
||||
fn scene_toml(
|
||||
scene_id: &str,
|
||||
skill_name: &str,
|
||||
tool_name: &str,
|
||||
expected_domain: &str,
|
||||
target_url: &str,
|
||||
include_keywords: &[&str],
|
||||
exclude_keywords: &[&str],
|
||||
page_title_keywords: &[&str],
|
||||
) -> String {
|
||||
format!(
|
||||
r#"[scene]
|
||||
id = "{scene_id}"
|
||||
skill = "{skill_name}"
|
||||
tool = "{tool_name}"
|
||||
kind = "browser_script"
|
||||
version = "0.1.0"
|
||||
category = "report_collection"
|
||||
|
||||
[manifest]
|
||||
schema_version = "1"
|
||||
|
||||
[bootstrap]
|
||||
expected_domain = "{expected_domain}"
|
||||
target_url = "{target_url}"
|
||||
page_title_keywords = {page_title_keywords}
|
||||
requires_target_page = true
|
||||
|
||||
[deterministic]
|
||||
suffix = "。。。"
|
||||
include_keywords = {include_keywords}
|
||||
exclude_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"
|
||||
|
||||
[[params]]
|
||||
name = "period"
|
||||
resolver = "month_week_period"
|
||||
required = true
|
||||
prompt_missing = "已命中台区线损报表技能,但缺少统计周期。"
|
||||
prompt_ambiguous = "已命中台区线损报表技能,但统计周期存在歧义,请补充更明确表达。"
|
||||
|
||||
[artifact]
|
||||
type = "report-artifact"
|
||||
success_status = ["ok", "partial", "empty"]
|
||||
failure_status = ["blocked", "error"]
|
||||
"#,
|
||||
include_keywords = toml_array(include_keywords),
|
||||
exclude_keywords = toml_array(exclude_keywords),
|
||||
page_title_keywords = toml_array(page_title_keywords),
|
||||
)
|
||||
}
|
||||
|
||||
fn org_dictionary_json() -> &'static str {
|
||||
r#"[
|
||||
{
|
||||
"label": "国网兰州供电公司",
|
||||
"code": "62401",
|
||||
"aliases": ["国网兰州供电公司", "兰州供电公司", "兰州公司"]
|
||||
},
|
||||
{
|
||||
"label": "城关供电分公司",
|
||||
"code": "6240108",
|
||||
"aliases": ["城关供电分公司", "城关分公司"]
|
||||
},
|
||||
{
|
||||
"label": "国网天水供电公司",
|
||||
"code": "62403",
|
||||
"aliases": ["国网天水供电公司", "天水供电公司", "天水公司"]
|
||||
}
|
||||
]"#
|
||||
}
|
||||
|
||||
fn write_scene_skill(
|
||||
skills_root: &Path,
|
||||
scene_id: &str,
|
||||
skill_name: &str,
|
||||
tool_name: &str,
|
||||
expected_domain: &str,
|
||||
target_url: &str,
|
||||
include_keywords: &[&str],
|
||||
exclude_keywords: &[&str],
|
||||
page_title_keywords: &[&str],
|
||||
) {
|
||||
let skill_root = skills_root.join(skill_name);
|
||||
fs::create_dir_all(skill_root.join("references")).unwrap();
|
||||
fs::write(
|
||||
skill_root.join("SKILL.toml"),
|
||||
browser_script_skill_toml(skill_name, tool_name),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
skill_root.join("scene.toml"),
|
||||
scene_toml(
|
||||
scene_id,
|
||||
skill_name,
|
||||
tool_name,
|
||||
expected_domain,
|
||||
target_url,
|
||||
include_keywords,
|
||||
exclude_keywords,
|
||||
page_title_keywords,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
skill_root.join("references").join("org-dictionary.json"),
|
||||
org_dictionary_json(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn assert_prompt_contains(decision: DeterministicSubmitDecision, needle: &str) {
|
||||
match decision {
|
||||
DeterministicSubmitDecision::Prompt { summary } => {
|
||||
assert!(summary.contains(needle), "unexpected prompt: {summary}");
|
||||
}
|
||||
other => panic!("expected prompt containing {needle}, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_submit_matches_final_bundle_lineloss_alias() {
|
||||
let skills_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("dist")
|
||||
.join("sgclaw_102_pseudoprod_validation_bundle_2026-04-20")
|
||||
.join("skills");
|
||||
|
||||
let decision = decide_deterministic_submit_with_skills_dir(
|
||||
"\u{53f0}\u{533a}\u{7ebf}\u{635f}\u{3002}\u{3002}\u{3002}",
|
||||
None,
|
||||
None,
|
||||
&skills_root,
|
||||
);
|
||||
|
||||
match decision {
|
||||
DeterministicSubmitDecision::Prompt { summary } => {
|
||||
assert!(
|
||||
!summary.contains(
|
||||
"\u{786e}\u{5b9a}\u{6027}\u{63d0}\u{4ea4}\u{5f53}\u{524d}\u{53ea}\u{652f}\u{6301}\u{5df2}\u{6ce8}\u{518c}"
|
||||
),
|
||||
"expected line-loss alias to reach registered scene resolver, got unsupported prompt: {summary}"
|
||||
);
|
||||
}
|
||||
DeterministicSubmitDecision::Execute(plan) => {
|
||||
assert_eq!(plan.tool_name, "sweep-030-scene.collect_sweep_030_scene");
|
||||
}
|
||||
DeterministicSubmitDecision::NotDeterministic => {
|
||||
panic!("expected deterministic line-loss alias to be recognized")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_submit_defaults_final_bundle_lineloss_month_to_page_semantics() {
|
||||
let skills_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("dist")
|
||||
.join("sgclaw_102_pseudoprod_validation_bundle_2026-04-20")
|
||||
.join("skills");
|
||||
|
||||
let decision = decide_deterministic_submit_with_skills_dir(
|
||||
"\u{5170}\u{5dde}\u{516c}\u{53f8} \u{7ebf}\u{635f}\u{5927}\u{6570}\u{636e} \u{6708}\u{7d2f}\u{8ba1}\u{7ebf}\u{635f}\u{7edf}\u{8ba1}\u{5206}\u{6790}\u{3002}\u{3002}\u{3002}",
|
||||
None,
|
||||
None,
|
||||
&skills_root,
|
||||
);
|
||||
|
||||
fn expected_default_month() -> String {
|
||||
let today = Local::now().date_naive();
|
||||
let (year, month) = if today.month() == 1 {
|
||||
(today.year() - 1, 12)
|
||||
} else {
|
||||
(today.year(), today.month() - 1)
|
||||
};
|
||||
format!("{year}-{month:02}")
|
||||
}
|
||||
let expected_month = format!("{year}-{month:02}");
|
||||
|
||||
fn expected_default_week_range() -> (String, String, String) {
|
||||
let today = Local::now().date_naive();
|
||||
let month_start = today.with_day(1).expect("current month should have day 1");
|
||||
let start = month_start.format("%Y-%m-%d").to_string();
|
||||
let end = today.format("%Y-%m-%d").to_string();
|
||||
(format!("{start}至{end}"), start, end)
|
||||
match decision {
|
||||
DeterministicSubmitDecision::Execute(plan) => {
|
||||
assert_eq!(plan.tool_name, "sweep-030-scene.collect_sweep_030_scene");
|
||||
assert_eq!(plan.period_mode, "month");
|
||||
assert_eq!(plan.period_mode_code, "1");
|
||||
assert_eq!(plan.period_value, expected_month);
|
||||
assert!(plan.period_payload.contains("fdate"));
|
||||
}
|
||||
other => panic!("expected execute plan with default month semantics, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_submit_discovers_tq_lineloss_skill_contract() {
|
||||
let skills_root = PathBuf::from("D:/data/ideaSpace/rust/sgClaw/claw/claw/skills/skill_staging/skills");
|
||||
let skills = load_skills_from_directory(&skills_root, true);
|
||||
|
||||
let skill = skills
|
||||
.iter()
|
||||
.find(|skill| skill.name == "tq-lineloss-report")
|
||||
.expect("tq-lineloss-report should be discoverable from staged skills root");
|
||||
|
||||
let tool = skill
|
||||
.tools
|
||||
.iter()
|
||||
.find(|tool| tool.name == "collect_lineloss")
|
||||
.expect("collect_lineloss tool should be discoverable");
|
||||
|
||||
assert_eq!(tool.kind, "browser_script");
|
||||
assert_eq!(tool.command, "scripts/collect_lineloss.js");
|
||||
|
||||
let required_args = [
|
||||
"expected_domain",
|
||||
"org_label",
|
||||
"org_code",
|
||||
"period_mode",
|
||||
"period_mode_code",
|
||||
"period_value",
|
||||
"period_payload",
|
||||
];
|
||||
|
||||
for arg in required_args {
|
||||
assert!(
|
||||
tool.args.contains_key(arg),
|
||||
"expected required arg {arg} in tq-lineloss-report.collect_lineloss"
|
||||
fn deterministic_submit_uses_registry_backed_scene_plan() {
|
||||
with_temp_workspace("sgclaw-deterministic-scene", |workspace_root| {
|
||||
let skills_root = default_skills_root(workspace_root);
|
||||
write_scene_skill(
|
||||
&skills_root,
|
||||
"tq-lineloss-report",
|
||||
"tq-lineloss-report",
|
||||
"collect_lineloss",
|
||||
"20.76.57.61",
|
||||
"http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor",
|
||||
&["线损", "统计分析"],
|
||||
&["知乎"],
|
||||
&["线损报表"],
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(tool.args.len(), required_args.len());
|
||||
let decision = decide_deterministic_submit(
|
||||
"兰州公司 台区线损大数据 月累计线损率统计分析 2026-03。。。",
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
match decision {
|
||||
DeterministicSubmitDecision::Execute(plan) => {
|
||||
assert_eq!(plan.tool_name, "tq-lineloss-report.collect_lineloss");
|
||||
assert_eq!(plan.expected_domain, "20.76.57.61");
|
||||
assert_eq!(
|
||||
plan.target_url,
|
||||
"http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor"
|
||||
);
|
||||
assert_eq!(plan.org_label, "国网兰州供电公司");
|
||||
assert_eq!(plan.org_code, "62401");
|
||||
assert_eq!(plan.period_mode, "month");
|
||||
assert_eq!(plan.period_mode_code, "1");
|
||||
assert_eq!(plan.period_value, "2026-03");
|
||||
assert!(plan.period_payload.contains("fdate"));
|
||||
}
|
||||
other => panic!("expected execute plan, got {other:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_submit_requires_exact_suffix() {
|
||||
assert!(matches!(
|
||||
decide_deterministic_submit("兰州公司 月累计 2026-03。。。", None, None),
|
||||
DeterministicSubmitDecision::Execute(_)
|
||||
));
|
||||
with_temp_workspace("sgclaw-deterministic-suffix", |workspace_root| {
|
||||
let skills_root = default_skills_root(workspace_root);
|
||||
write_scene_skill(
|
||||
&skills_root,
|
||||
"tq-lineloss-report",
|
||||
"tq-lineloss-report",
|
||||
"collect_lineloss",
|
||||
"20.76.57.61",
|
||||
"http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor",
|
||||
&["线损", "统计分析"],
|
||||
&["知乎"],
|
||||
&["线损报表"],
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
decide_deterministic_submit("兰州公司 月累计 2026-03", None, None),
|
||||
DeterministicSubmitDecision::NotDeterministic
|
||||
));
|
||||
}
|
||||
assert!(matches!(
|
||||
decide_deterministic_submit(
|
||||
"兰州公司 台区线损大数据 月累计线损率统计分析 2026-03。。。",
|
||||
None,
|
||||
None
|
||||
),
|
||||
DeterministicSubmitDecision::Execute(_)
|
||||
));
|
||||
|
||||
#[test]
|
||||
fn deterministic_submit_nonmatch_returns_supported_scene_message() {
|
||||
let decision = decide_deterministic_submit("帮我打开百度。。。", None, None);
|
||||
|
||||
match decision {
|
||||
DeterministicSubmitDecision::Prompt { summary } => {
|
||||
assert!(summary.contains("台区线损") || summary.contains("支持场景"));
|
||||
for instruction in [
|
||||
"兰州公司 台区线损大数据 月累计线损率统计分析 2026-03",
|
||||
"兰州公司 台区线损大数据 月累计线损率统计分析 2026-03...",
|
||||
"兰州公司 台区线损大数据 月累计线损率统计分析 2026-03。。。。",
|
||||
"兰州公司 台区线损大数据 月累计线损率统计分析 2026-03。。。 ",
|
||||
] {
|
||||
assert!(matches!(
|
||||
decide_deterministic_submit(instruction, None, None),
|
||||
DeterministicSubmitDecision::NotDeterministic
|
||||
));
|
||||
}
|
||||
other => panic!("expected deterministic prompt for unsupported scene, got {other:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_submit_rejects_page_context_mismatch() {
|
||||
let decision = decide_deterministic_submit(
|
||||
"兰州公司 月累计 2026-03。。。",
|
||||
Some("https://www.zhihu.com/hot"),
|
||||
Some("知乎热榜"),
|
||||
);
|
||||
fn deterministic_submit_fails_closed_on_scene_ambiguity() {
|
||||
with_temp_workspace("sgclaw-deterministic-ambiguity", |workspace_root| {
|
||||
let skills_root = default_skills_root(workspace_root);
|
||||
write_scene_skill(
|
||||
&skills_root,
|
||||
"tq-lineloss-report",
|
||||
"tq-lineloss-report",
|
||||
"collect_lineloss",
|
||||
"20.76.57.61",
|
||||
"http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor",
|
||||
&["统计分析"],
|
||||
&[],
|
||||
&["线损报表"],
|
||||
);
|
||||
write_scene_skill(
|
||||
&skills_root,
|
||||
"other-report",
|
||||
"other-report",
|
||||
"collect_other",
|
||||
"20.76.57.61",
|
||||
"http://20.76.57.61:18080/other/report",
|
||||
&["统计分析"],
|
||||
&[],
|
||||
&["其他报表"],
|
||||
);
|
||||
|
||||
match decision {
|
||||
DeterministicSubmitDecision::Prompt { summary } => {
|
||||
assert!(summary.contains("台区线损") || summary.contains("页面") || summary.contains("不匹配"));
|
||||
let decision = decide_deterministic_submit("兰州公司 统计分析。。。", None, None);
|
||||
|
||||
match decision {
|
||||
DeterministicSubmitDecision::Prompt { summary } => {
|
||||
assert!(
|
||||
summary.contains("多个确定性场景"),
|
||||
"unexpected prompt: {summary}"
|
||||
);
|
||||
assert!(
|
||||
summary.contains("tq-lineloss-report"),
|
||||
"unexpected prompt: {summary}"
|
||||
);
|
||||
assert!(
|
||||
summary.contains("other-report"),
|
||||
"unexpected prompt: {summary}"
|
||||
);
|
||||
}
|
||||
other => panic!("expected ambiguity prompt, got {other:?}"),
|
||||
}
|
||||
other => panic!("expected deterministic mismatch prompt, got {other:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhihu_hotlist_request_without_suffix_keeps_existing_route() {
|
||||
assert!(is_zhihu_hotlist_task(
|
||||
"打开知乎热榜",
|
||||
Some("https://www.zhihu.com/hot"),
|
||||
Some("知乎热榜")
|
||||
));
|
||||
fn deterministic_submit_prompts_for_missing_period_instead_of_defaulting() {
|
||||
with_temp_workspace("sgclaw-deterministic-period", |workspace_root| {
|
||||
let skills_root = default_skills_root(workspace_root);
|
||||
write_scene_skill(
|
||||
&skills_root,
|
||||
"tq-lineloss-report",
|
||||
"tq-lineloss-report",
|
||||
"collect_lineloss",
|
||||
"20.76.57.61",
|
||||
"http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor",
|
||||
&["线损", "统计分析"],
|
||||
&["知乎"],
|
||||
&["线损报表"],
|
||||
);
|
||||
|
||||
assert_prompt_contains(
|
||||
decide_deterministic_submit("兰州公司 月累计 统计分析。。。", None, None),
|
||||
"缺少统计周期",
|
||||
);
|
||||
assert_prompt_contains(
|
||||
decide_deterministic_submit("兰州公司 周累计 统计分析。。。", None, None),
|
||||
"缺少统计周期",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_submit_uses_page_context_to_break_ties_before_keyword_only_match() {
|
||||
with_temp_workspace("sgclaw-deterministic-page-context", |workspace_root| {
|
||||
let skills_root = default_skills_root(workspace_root);
|
||||
write_scene_skill(
|
||||
&skills_root,
|
||||
"tq-lineloss-report",
|
||||
"tq-lineloss-report",
|
||||
"collect_lineloss",
|
||||
"20.76.57.61",
|
||||
"http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor",
|
||||
&["统计分析"],
|
||||
&[],
|
||||
&["线损报表"],
|
||||
);
|
||||
write_scene_skill(
|
||||
&skills_root,
|
||||
"fault-report",
|
||||
"fault-report",
|
||||
"collect_fault",
|
||||
"20.76.57.61",
|
||||
"http://20.76.57.61:18080/fault/report",
|
||||
&["统计分析"],
|
||||
&[],
|
||||
&["95598工单"],
|
||||
);
|
||||
|
||||
let decision = decide_deterministic_submit(
|
||||
"兰州公司 月累计 统计分析 2026-03。。。",
|
||||
Some("http://20.76.57.61:18080/#/lineloss"),
|
||||
Some("台区线损报表"),
|
||||
);
|
||||
|
||||
match decision {
|
||||
DeterministicSubmitDecision::Execute(plan) => {
|
||||
assert_eq!(plan.tool_name, "tq-lineloss-report.collect_lineloss");
|
||||
assert_eq!(
|
||||
plan.target_url,
|
||||
"http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor"
|
||||
);
|
||||
}
|
||||
other => panic!("expected page context to select lineloss scene, got {other:?}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_submit_unsupported_suffix_request_returns_supported_scene_message() {
|
||||
with_temp_workspace("sgclaw-deterministic-unsupported", |workspace_root| {
|
||||
let skills_root = default_skills_root(workspace_root);
|
||||
write_scene_skill(
|
||||
&skills_root,
|
||||
"tq-lineloss-report",
|
||||
"tq-lineloss-report",
|
||||
"collect_lineloss",
|
||||
"20.76.57.61",
|
||||
"http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor",
|
||||
&["线损", "统计分析"],
|
||||
&["知乎"],
|
||||
&["线损报表"],
|
||||
);
|
||||
|
||||
assert_prompt_contains(
|
||||
decide_deterministic_submit("打开知乎热榜。。。", None, None),
|
||||
"已注册的报表采集场景",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zhihu_without_suffix_remains_not_deterministic() {
|
||||
assert!(matches!(
|
||||
decide_deterministic_submit(
|
||||
"打开知乎热榜",
|
||||
@@ -132,321 +507,51 @@ fn zhihu_hotlist_request_without_suffix_keeps_existing_route() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_submit_rejects_non_exact_suffix_variants() {
|
||||
for instruction in [
|
||||
"兰州公司 月累计 2026-03...",
|
||||
"兰州公司 月累计 2026-03。。。。",
|
||||
"兰州公司。。。月累计 2026-03",
|
||||
"兰州公司 月累计 2026-03。。。 ",
|
||||
] {
|
||||
assert!(matches!(
|
||||
decide_deterministic_submit(instruction, None, None),
|
||||
DeterministicSubmitDecision::NotDeterministic
|
||||
));
|
||||
}
|
||||
}
|
||||
fn committed_lineloss_sample_package_drives_deterministic_submit() {
|
||||
let _guard = current_dir_lock().lock().unwrap();
|
||||
let skills_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("examples")
|
||||
.join("generated_scene_platform")
|
||||
.join("skills");
|
||||
|
||||
#[test]
|
||||
fn lineloss_org_resolver_matches_city_alias() {
|
||||
assert_eq!(
|
||||
resolve_org("兰州公司").unwrap(),
|
||||
ResolvedOrg {
|
||||
label: "国网兰州供电公司".to_string(),
|
||||
code: "62401".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
resolve_org("天水公司").unwrap(),
|
||||
ResolvedOrg {
|
||||
label: "国网天水供电公司".to_string(),
|
||||
code: "62403".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lineloss_org_resolver_matches_county_alias() {
|
||||
assert_eq!(
|
||||
resolve_org("榆中县公司").unwrap(),
|
||||
ResolvedOrg {
|
||||
label: "国网榆中县供电公司".to_string(),
|
||||
code: "6240121".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
resolve_org("城关供电分公司").unwrap(),
|
||||
ResolvedOrg {
|
||||
label: "城关供电分公司".to_string(),
|
||||
code: "6240108".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lineloss_org_resolver_prompts_on_ambiguity() {
|
||||
let summary = resolve_org("城关")
|
||||
.expect_err("ambiguous alias should prompt instead of guessing");
|
||||
|
||||
assert!(summary.contains("供电单位存在歧义") || summary.contains("更完整名称"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_submit_lineloss_missing_company_prompts() {
|
||||
let decision = decide_deterministic_submit("月累计 2026-03。。。", None, None);
|
||||
|
||||
match decision {
|
||||
DeterministicSubmitDecision::Prompt { summary } => {
|
||||
assert!(summary.contains("缺少供电单位") || summary.contains("兰州公司"));
|
||||
}
|
||||
other => panic!("expected missing-company prompt, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lineloss_period_resolver_parses_month_text() {
|
||||
assert_eq!(
|
||||
resolve_period("月累计 2026-03").unwrap(),
|
||||
ResolvedPeriod {
|
||||
mode: PeriodMode::Month,
|
||||
mode_code: "1".to_string(),
|
||||
value: "2026-03".to_string(),
|
||||
payload: serde_json::json!({
|
||||
"fdate": "2026-03",
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
resolve_period("月累计 2026年3月").unwrap(),
|
||||
ResolvedPeriod {
|
||||
mode: PeriodMode::Month,
|
||||
mode_code: "1".to_string(),
|
||||
value: "2026-03".to_string(),
|
||||
payload: serde_json::json!({
|
||||
"fdate": "2026-03",
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lineloss_period_resolver_parses_week_text() {
|
||||
let resolved = resolve_period("周累计 2026年第12周").unwrap();
|
||||
|
||||
assert_eq!(resolved.mode, PeriodMode::Week);
|
||||
assert_eq!(resolved.mode_code, "2");
|
||||
assert_eq!(resolved.value, "2026-W12");
|
||||
assert_eq!(resolved.payload["tjzq"], "week");
|
||||
assert_eq!(resolved.payload["level"], "00");
|
||||
assert_eq!(resolved.payload["weekSfdate"], "2026-03-16");
|
||||
assert_eq!(resolved.payload["weekEfdate"], "2026-03-22");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lineloss_period_resolver_defaults_month_period_from_page_semantics() {
|
||||
let expected_month = expected_default_month();
|
||||
|
||||
assert_eq!(
|
||||
resolve_period("兰州公司 月累计").unwrap(),
|
||||
ResolvedPeriod {
|
||||
mode: PeriodMode::Month,
|
||||
mode_code: "1".to_string(),
|
||||
value: expected_month.clone(),
|
||||
payload: serde_json::json!({
|
||||
"fdate": expected_month,
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lineloss_period_resolver_defaults_week_period_from_page_semantics() {
|
||||
let (expected_value, expected_start, expected_end) = expected_default_week_range();
|
||||
|
||||
assert_eq!(
|
||||
resolve_period("兰州公司 周累计").unwrap(),
|
||||
ResolvedPeriod {
|
||||
mode: PeriodMode::Week,
|
||||
mode_code: "2".to_string(),
|
||||
value: expected_value,
|
||||
payload: serde_json::json!({
|
||||
"tjzq": "week",
|
||||
"level": "00",
|
||||
"weekSfdate": expected_start,
|
||||
"weekEfdate": expected_end,
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lineloss_period_resolver_prompts_for_missing_year_on_week() {
|
||||
let summary = resolve_period("周累计 第12周")
|
||||
.expect_err("bare week should prompt for year instead of guessing");
|
||||
|
||||
assert!(summary.contains("年份") || summary.contains("第12周"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lineloss_period_resolver_rejects_contradictory_mode() {
|
||||
let summary = resolve_period("月累计 周累计 2026-03")
|
||||
.expect_err("contradictory month/week intent should not execute");
|
||||
|
||||
assert!(summary.contains("月/周") || summary.contains("冲突") || summary.contains("歧义"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lineloss_period_resolver_prompts_for_missing_mode() {
|
||||
let summary = resolve_period("兰州公司 2026-03")
|
||||
.expect_err("missing mode should prompt instead of guessing");
|
||||
|
||||
assert!(summary.contains("月/周类型") || summary.contains("月累计") || summary.contains("周累计"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lineloss_period_resolver_prompts_for_missing_period() {
|
||||
let summary = resolve_period("兰州公司 月累计")
|
||||
.expect_err("missing period should prompt instead of guessing");
|
||||
|
||||
assert!(summary.contains("周期") || summary.contains("时间") || summary.contains("2026-03"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_lineloss_execution_plan_contains_canonical_args() {
|
||||
let decision = decide_deterministic_submit(
|
||||
"兰州公司 月累计 2026-03。。。",
|
||||
Some("http://20.76.57.61:8080/#/lineloss"),
|
||||
Some("台区线损报表"),
|
||||
let decision = decide_deterministic_submit_with_skills_dir(
|
||||
"兰州公司 台区线损大数据 月累计线损率统计分析 2026-03。。。",
|
||||
None,
|
||||
None,
|
||||
&skills_dir,
|
||||
);
|
||||
|
||||
match decision {
|
||||
DeterministicSubmitDecision::Execute(plan) => {
|
||||
let debug = format!("{plan:?}");
|
||||
assert!(debug.contains("国网兰州供电公司"), "missing canonical org label: {debug}");
|
||||
assert!(debug.contains("62401"), "missing canonical org code: {debug}");
|
||||
assert!(debug.contains("2026-03"), "missing canonical period value: {debug}");
|
||||
assert!(debug.contains("month") || debug.contains("Month"), "missing canonical month mode: {debug}");
|
||||
assert!(debug.contains("fdate"), "missing canonical month payload: {debug}");
|
||||
}
|
||||
other => panic!("expected deterministic execute plan, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_lineloss_missing_period_uses_default_month_execution_plan() {
|
||||
let expected_month = expected_default_month();
|
||||
let decision = decide_deterministic_submit("兰州公司 月累计。。。", None, None);
|
||||
|
||||
match decision {
|
||||
DeterministicSubmitDecision::Execute(plan) => {
|
||||
assert_eq!(plan.tool_name, "tq-lineloss-report.collect_lineloss");
|
||||
assert_eq!(plan.expected_domain, "20.76.57.61");
|
||||
assert_eq!(
|
||||
plan.target_url,
|
||||
"http://20.76.57.61:18080/gsllys/tqLinelossStatis/tqQualifyRateMonitor"
|
||||
);
|
||||
assert_eq!(plan.org_label, "国网兰州供电公司");
|
||||
assert_eq!(plan.org_code, "62401");
|
||||
assert_eq!(plan.period_mode, "month");
|
||||
assert_eq!(plan.period_mode_code, "1");
|
||||
assert_eq!(plan.period_value, expected_month);
|
||||
assert_eq!(plan.period_value, "2026-03");
|
||||
assert!(plan.period_payload.contains("fdate"));
|
||||
assert_eq!(
|
||||
plan.postprocess
|
||||
.as_ref()
|
||||
.map(|postprocess| postprocess.exporter.as_str()),
|
||||
Some("xlsx_report")
|
||||
);
|
||||
}
|
||||
other => panic!("expected missing month period to default into execution, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_lineloss_missing_period_uses_default_week_execution_plan() {
|
||||
let (expected_value, expected_start, expected_end) = expected_default_week_range();
|
||||
let decision = decide_deterministic_submit("兰州公司 周累计。。。", None, None);
|
||||
|
||||
match decision {
|
||||
DeterministicSubmitDecision::Execute(plan) => {
|
||||
assert_eq!(plan.period_mode, "week");
|
||||
assert_eq!(plan.period_mode_code, "2");
|
||||
assert_eq!(plan.period_value, expected_value);
|
||||
assert!(plan.period_payload.contains(&expected_start));
|
||||
assert!(plan.period_payload.contains(&expected_end));
|
||||
}
|
||||
other => panic!("expected missing week period to default into execution, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_lineloss_partial_artifact_summary_contract_is_locked() {
|
||||
let artifact = serde_json::json!({
|
||||
"type": "report-artifact",
|
||||
"report_name": "tq-lineloss-report",
|
||||
"status": "partial",
|
||||
"org": {
|
||||
"label": "国网兰州供电公司",
|
||||
"code": "62401"
|
||||
},
|
||||
"period": {
|
||||
"mode": "month",
|
||||
"mode_code": "1",
|
||||
"value": "2026-03",
|
||||
"payload": {
|
||||
"fdate": "2026-03"
|
||||
}
|
||||
},
|
||||
"columns": ["ORG_NAME", "LINE_LOSS_RATE"],
|
||||
"rows": [
|
||||
{ "ORG_NAME": "国网兰州供电公司", "LINE_LOSS_RATE": "3.21" }
|
||||
],
|
||||
"counts": {
|
||||
"rows": 1
|
||||
},
|
||||
"export": {
|
||||
"attempted": true,
|
||||
"status": "failed",
|
||||
"message": "report_log_failed"
|
||||
},
|
||||
"reasons": ["report_log_failed"]
|
||||
});
|
||||
|
||||
assert_eq!(artifact["type"], "report-artifact");
|
||||
assert_eq!(artifact["report_name"], "tq-lineloss-report");
|
||||
assert_eq!(artifact["status"], "partial");
|
||||
assert_eq!(artifact["org"]["label"], "国网兰州供电公司");
|
||||
assert_eq!(artifact["period"]["value"], "2026-03");
|
||||
assert_eq!(artifact["counts"]["rows"], 1);
|
||||
assert_eq!(artifact["reasons"][0], "report_log_failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deterministic_lineloss_blocked_and_error_artifact_statuses_are_failure_contracts() {
|
||||
for status in ["blocked", "error"] {
|
||||
let artifact = serde_json::json!({
|
||||
"type": "report-artifact",
|
||||
"report_name": "tq-lineloss-report",
|
||||
"status": status,
|
||||
"org": {
|
||||
"label": "国网兰州供电公司",
|
||||
"code": "62401"
|
||||
},
|
||||
"period": {
|
||||
"mode": "week",
|
||||
"mode_code": "2",
|
||||
"value": "2026-W12",
|
||||
"payload": {
|
||||
"tjzq": "week",
|
||||
"level": "00",
|
||||
"weekSfdate": "2026-03-16",
|
||||
"weekEfdate": "2026-03-22"
|
||||
}
|
||||
},
|
||||
"columns": [],
|
||||
"rows": [],
|
||||
"counts": {
|
||||
"rows": 0
|
||||
},
|
||||
"export": {
|
||||
"attempted": false,
|
||||
"status": "skipped",
|
||||
"message": null
|
||||
},
|
||||
"reasons": ["selected_range_unavailable"]
|
||||
});
|
||||
|
||||
assert_eq!(artifact["status"], status);
|
||||
assert_eq!(artifact["type"], "report-artifact");
|
||||
assert_eq!(artifact["period"]["mode"], "week");
|
||||
assert_eq!(artifact["reasons"][0], "selected_range_unavailable");
|
||||
other => panic!("expected committed sample package execute plan, got {other:?}"),
|
||||
}
|
||||
|
||||
assert_prompt_contains(
|
||||
decide_deterministic_submit_with_skills_dir(
|
||||
"兰州公司 台区线损大数据 月累计线损率统计分析。。。",
|
||||
None,
|
||||
None,
|
||||
&skills_dir,
|
||||
),
|
||||
"缺少统计周期",
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user