merge: bring main lineloss defaults into ws

Bring the main-branch lineloss default-period fix into feature/claw-ws while keeping the ws submit/backend path intact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
木炎
2026-04-12 21:37:03 +08:00
4 changed files with 430 additions and 18 deletions

View File

@@ -285,6 +285,16 @@ fn submit_fault_details_message() -> BrowserMessage {
}
}
fn submit_zhihu_hotlist_export_message() -> BrowserMessage {
BrowserMessage::SubmitTask {
instruction: "打开知乎热榜获取前10条数据并导出 Excel".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: String::new(),
page_title: String::new(),
}
}
fn direct_submit_mode_logs(sent: &[AgentMessage]) -> Vec<String> {
sent.iter()
.filter_map(|message| match message {
@@ -680,6 +690,47 @@ fn submit_task_treats_error_report_artifact_as_failure() {
assert!(completion.1.contains("detail_normalization_failed"));
}
#[test]
fn submit_task_routes_zhihu_hotlist_export_before_direct_submit() {
std::env::remove_var("DEEPSEEK_API_KEY");
std::env::remove_var("DEEPSEEK_BASE_URL");
std::env::remove_var("DEEPSEEK_MODEL");
let skill_root = build_direct_runtime_skill_root();
let runtime_context = direct_submit_runtime_context(&skill_root);
let transport = Arc::new(MockTransport::new(vec![]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
policy_for_domains(&["www.zhihu.com"]),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
submit_zhihu_hotlist_export_message(),
)
.unwrap();
let sent = transport.sent_messages();
let mode_logs = direct_submit_mode_logs(&sent);
let completion = direct_submit_completion(&sent).expect("task completion");
assert_eq!(mode_logs, vec!["zeroclaw_process_message_primary".to_string()]);
assert!(
!completion.0,
"expected zhihu export without page context to fail before browser actions: {sent:?}"
);
assert!(
!completion
.1
.contains("direct submit skill requires page_url so expected_domain can be derived"),
"unexpected direct submit fallback: {sent:?}"
);
}
#[test]
fn direct_skill_mode_logs_direct_skill_primary() {
std::env::remove_var("DEEPSEEK_API_KEY");

View File

@@ -8,6 +8,7 @@ use std::sync::{Arc, Mutex, OnceLock};
use std::thread;
use std::time::Duration;
use chrono::{Datelike, Local};
use common::MockTransport;
use serde_json::{json, Value};
use sgclaw::agent::{
@@ -278,6 +279,24 @@ fn extract_generated_artifact_path(summary: &str, extension: &str) -> PathBuf {
.expect("expected artifact path in task summary")
}
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 compat_runtime_uses_zeroclaw_provider_path_and_executes_browser_actions() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
@@ -4042,6 +4061,210 @@ fn deterministic_lineloss_runtime_passes_canonical_args_to_browser_script_tool()
}));
}
#[test]
fn deterministic_lineloss_runtime_defaults_missing_month_period_to_page_semantics() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let expected_month = expected_default_month();
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(staged_lineloss_skills_root().to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
1,
json!({
"text": {
"type": "report-artifact",
"report_name": "tq-lineloss-report",
"status": "ok",
"org": {
"label": "国网兰州供电公司",
"code": "62401"
},
"period": {
"mode": "month",
"mode_code": "1",
"value": expected_month,
"payload": {
"fdate": expected_default_month()
}
},
"rows": [
{ "ORG_NAME": "国网兰州供电公司", "LINE_LOSS_RATE": "3.21" }
],
"counts": {
"rows": 1
},
"reasons": []
}
}),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
policy_for_domains(&["20.76.57.61"]),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "兰州公司 月累计。。。".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "http://20.76.57.61:8080/#/lineloss".to_string(),
page_title: "台区线损报表".to_string(),
},
)
.unwrap();
let sent = transport.sent_messages();
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success
&& summary.contains("tq-lineloss-report")
&& summary.contains("国网兰州供电公司")
&& summary.contains(&expected_default_month())
&& summary.contains("rows=1")
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command {
action,
params,
security,
..
}
if action == &Action::Eval
&& security.expected_domain == "20.76.57.61"
&& params["script"].as_str().is_some_and(|script|
script.contains("\"period_mode\":\"month\"")
&& script.contains("\"period_mode_code\":\"1\"")
&& script.contains(&format!("\"period_value\":\"{}\"", expected_default_month()))
&& script.contains("\"period_payload\":\"{")
&& script.contains(&format!("\\\"fdate\\\":\\\"{}\\\"", expected_default_month()))
)
)
}), "sent messages were: {sent:?}");
}
#[test]
fn deterministic_lineloss_runtime_defaults_missing_week_period_to_page_semantics() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());
let (expected_value, expected_start, expected_end) = expected_default_week_range();
let workspace_root = temp_workspace_root();
let config_path = write_deepseek_config_with_skills_dir(
&workspace_root,
"deepseek-test-key",
"http://127.0.0.1:9",
"deepseek-chat",
Some(staged_lineloss_skills_root().to_str().unwrap()),
);
let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone());
let transport = Arc::new(MockTransport::new(vec![success_browser_response(
1,
json!({
"text": {
"type": "report-artifact",
"report_name": "tq-lineloss-report",
"status": "ok",
"org": {
"label": "国网兰州供电公司",
"code": "62401"
},
"period": {
"mode": "week",
"mode_code": "2",
"value": expected_value,
"payload": {
"tjzq": "week",
"level": "00",
"weekSfdate": expected_start,
"weekEfdate": expected_end
}
},
"rows": [
{ "ORG_NAME3": "国网兰州供电公司", "LINELOSS_RATE": "3.21" }
],
"counts": {
"rows": 1
},
"reasons": []
}
}),
)]));
let browser_tool = BrowserPipeTool::new(
transport.clone(),
policy_for_domains(&["20.76.57.61"]),
vec![1, 2, 3, 4, 5, 6, 7, 8],
)
.with_response_timeout(Duration::from_secs(1));
handle_browser_message_with_context(
transport.as_ref(),
&browser_tool,
&runtime_context,
BrowserMessage::SubmitTask {
instruction: "兰州公司 周累计。。。".to_string(),
conversation_id: String::new(),
messages: vec![],
page_url: "http://20.76.57.61:8080/#/lineloss".to_string(),
page_title: "台区线损报表".to_string(),
},
)
.unwrap();
let sent = transport.sent_messages();
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::TaskComplete { success, summary }
if *success
&& summary.contains("tq-lineloss-report")
&& summary.contains("国网兰州供电公司")
&& summary.contains(&expected_value)
&& summary.contains("rows=1")
)
}));
assert!(sent.iter().any(|message| {
matches!(
message,
AgentMessage::Command {
action,
params,
security,
..
}
if action == &Action::Eval
&& security.expected_domain == "20.76.57.61"
&& params["script"].as_str().is_some_and(|script|
script.contains("\"period_mode\":\"week\"")
&& script.contains("\"period_mode_code\":\"2\"")
&& script.contains(&format!("\"period_value\":\"{}\"", expected_value))
&& script.contains("\"period_payload\":\"{")
&& script.contains(&format!("\\\"weekSfdate\\\":\\\"{}\\\"", expected_start))
&& script.contains(&format!("\\\"weekEfdate\\\":\\\"{}\\\"", expected_end))
)
)
}), "sent messages were: {sent:?}");
}
#[test]
fn deterministic_lineloss_missing_company_prompt_skips_browser_execution() {
let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner());

View File

@@ -2,6 +2,7 @@ mod common;
use std::path::PathBuf;
use chrono::{Datelike, Local};
use zeroclaw::skills::load_skills_from_directory;
use sgclaw::compat::deterministic_submit::{
@@ -14,6 +15,24 @@ use sgclaw::compat::tq_lineloss::{
};
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");
@@ -225,6 +244,43 @@ fn lineloss_period_resolver_parses_week_text() {
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周")
@@ -279,14 +335,35 @@ fn deterministic_lineloss_execution_plan_contains_canonical_args() {
}
#[test]
fn deterministic_lineloss_missing_period_does_not_reach_execution_plan() {
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::Prompt { summary } => {
assert!(summary.contains("周期") || summary.contains("时间") || summary.contains("2026-03"));
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-period prompt before execution, got {other:?}"),
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:?}"),
}
}