mod common; use std::path::PathBuf; 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; #[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_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_does_not_reach_execution_plan() { let decision = decide_deterministic_submit("兰州公司 月累计。。。", None, None); match decision { DeterministicSubmitDecision::Prompt { summary } => { assert!(summary.contains("周期") || summary.contains("时间") || summary.contains("2026-03")); } other => panic!("expected missing-period prompt before 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"); } }