feat: add generated scene skill platform hardening

This commit is contained in:
木炎
2026-04-21 23:19:06 +08:00
parent 118fc77935
commit 956f0c2b68
439 changed files with 61974 additions and 3645 deletions

View File

@@ -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,
),
"缺少统计周期",
);
}