Files
claw/tests/deterministic_submit_test.rs
木炎 a8a470481d fix: align lineloss default periods with page semantics
Default month/week deterministic lineloss requests to the source page's built-in time ranges while preserving explicit-period parsing and existing routing contracts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 21:35:28 +08:00

453 lines
14 KiB
Rust

mod common;
use std::path::PathBuf;
use chrono::{Datelike, Local};
use zeroclaw::skills::load_skills_from_directory;
use sgclaw::compat::deterministic_submit::{
decide_deterministic_submit, 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;
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}")
}
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)
}
#[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"
);
}
assert_eq!(tool.args.len(), required_args.len());
}
#[test]
fn deterministic_submit_requires_exact_suffix() {
assert!(matches!(
decide_deterministic_submit("兰州公司 月累计 2026-03。。。", None, None),
DeterministicSubmitDecision::Execute(_)
));
assert!(matches!(
decide_deterministic_submit("兰州公司 月累计 2026-03", None, None),
DeterministicSubmitDecision::NotDeterministic
));
}
#[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("支持场景"));
}
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("知乎热榜"),
);
match decision {
DeterministicSubmitDecision::Prompt { summary } => {
assert!(summary.contains("台区线损") || summary.contains("页面") || summary.contains("不匹配"));
}
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("知乎热榜")
));
assert!(matches!(
decide_deterministic_submit(
"打开知乎热榜",
Some("https://www.zhihu.com/hot"),
Some("知乎热榜")
),
DeterministicSubmitDecision::NotDeterministic
));
}
#[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
));
}
}
#[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("台区线损报表"),
);
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.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 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");
}
}