diff --git a/src/agent/mod.rs b/src/agent/mod.rs index 8d2186e..296d173 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -1,19 +1,95 @@ pub mod planner; pub mod runtime; +use std::ffi::OsString; use std::path::PathBuf; use crate::config::DeepSeekSettings; use crate::pipe::{AgentMessage, BrowserMessage, BrowserPipeTool, PipeError, Transport}; -pub fn execute_task( +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AgentRuntimeContext { + config_path: Option, + workspace_root: PathBuf, +} + +impl AgentRuntimeContext { + pub fn new(config_path: Option, workspace_root: PathBuf) -> Self { + Self { + config_path, + workspace_root, + } + } + + pub fn from_process_args(args: I) -> Result + where + I: IntoIterator, + S: Into, + { + let mut config_path = None; + let mut args = args.into_iter().map(Into::into); + let _ = args.next(); + + while let Some(arg) = args.next() { + if arg == OsString::from("--config-path") { + let Some(value) = args.next() else { + return Err(PipeError::Protocol( + "missing value for --config-path".to_string(), + )); + }; + config_path = Some(PathBuf::from(value)); + continue; + } + + let arg_string = arg.to_string_lossy(); + if let Some(value) = arg_string.strip_prefix("--config-path=") { + config_path = Some(PathBuf::from(value)); + } + } + + let workspace_root = config_path + .as_ref() + .and_then(|path| path.parent().map(|parent| parent.to_path_buf())) + .unwrap_or_else(default_workspace_root); + + Ok(Self::new(config_path, workspace_root)) + } + + fn load_deepseek_settings(&self) -> Result, PipeError> { + DeepSeekSettings::load(self.config_path.as_deref()) + .map_err(|err| PipeError::Protocol(err.to_string())) + } + + fn deepseek_source_label(&self) -> String { + match &self.config_path { + Some(path) if path.exists() => path.display().to_string(), + _ => "environment".to_string(), + } + } +} + +impl Default for AgentRuntimeContext { + fn default() -> Self { + Self::new(None, default_workspace_root()) + } +} + +fn default_workspace_root() -> PathBuf { + std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) +} + +fn send_mode_log(transport: &T, mode: &str) -> Result<(), PipeError> { + transport.send(&AgentMessage::LogEntry { + level: "mode".to_string(), + message: mode.to_string(), + }) +} + +fn execute_plan( transport: &T, browser_tool: &BrowserPipeTool, - instruction: &str, + plan: &planner::TaskPlan, ) -> Result { - let plan = planner::plan_instruction(instruction) - .map_err(|err| PipeError::Protocol(err.to_string()))?; - for step in &plan.steps { transport.send(&AgentMessage::LogEntry { level: "info".to_string(), @@ -33,42 +109,98 @@ pub fn execute_task( } } - Ok(plan.summary) + Ok(plan.summary.clone()) +} + +pub fn execute_task( + transport: &T, + browser_tool: &BrowserPipeTool, + instruction: &str, +) -> Result { + let plan = planner::plan_instruction(instruction) + .map_err(|err| PipeError::Protocol(err.to_string()))?; + execute_plan(transport, browser_tool, &plan) } pub fn handle_browser_message( transport: &T, browser_tool: &BrowserPipeTool, message: BrowserMessage, +) -> Result<(), PipeError> { + handle_browser_message_with_context( + transport, + browser_tool, + &AgentRuntimeContext::default(), + message, + ) +} + +pub fn handle_browser_message_with_context( + transport: &T, + browser_tool: &BrowserPipeTool, + context: &AgentRuntimeContext, + message: BrowserMessage, ) -> Result<(), PipeError> { match message { BrowserMessage::SubmitTask { instruction } => { - let completion = match DeepSeekSettings::from_env() { - Ok(_) => match crate::compat::runtime::execute_task( - transport, - browser_tool.clone(), - &instruction, - &std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")), - ) { - Ok(summary) => AgentMessage::TaskComplete { - success: true, - summary, - }, + let completion = match context.load_deepseek_settings() { + Ok(Some(settings)) => { + let _ = transport.send(&AgentMessage::LogEntry { + level: "info".to_string(), + message: format!( + "DeepSeek config loaded from {} model={} base_url={}", + context.deepseek_source_label(), + settings.model, + settings.base_url + ), + }); + let _ = send_mode_log(transport, "compat_llm_primary"); + match crate::compat::runtime::execute_task( + transport, + browser_tool.clone(), + &instruction, + &context.workspace_root, + &settings, + ) { + Ok(summary) => AgentMessage::TaskComplete { + success: true, + summary, + }, + Err(err) => AgentMessage::TaskComplete { + success: false, + summary: err.to_string(), + }, + } + } + Ok(None) => match planner::plan_instruction(&instruction) { + Ok(plan) => { + let _ = send_mode_log(transport, "deterministic_planner"); + match execute_plan(transport, browser_tool, &plan) { + Ok(summary) => AgentMessage::TaskComplete { + success: true, + summary, + }, + Err(err) => AgentMessage::TaskComplete { + success: false, + summary: err.to_string(), + }, + } + } Err(err) => AgentMessage::TaskComplete { success: false, - summary: err.to_string(), + summary: PipeError::Protocol(err.to_string()).to_string(), }, }, - Err(_) => match execute_task(transport, browser_tool, &instruction) { - Ok(summary) => AgentMessage::TaskComplete { - success: true, - summary, - }, - Err(err) => AgentMessage::TaskComplete { + Err(err) => { + let _ = transport.send(&AgentMessage::LogEntry { + level: "error".to_string(), + message: format!("failed to load DeepSeek config: {err}"), + }); + AgentMessage::TaskComplete { success: false, summary: err.to_string(), - }, - }, + } + } }; transport.send(&completion) } diff --git a/src/compat/browser_tool_adapter.rs b/src/compat/browser_tool_adapter.rs index 1be4837..179212b 100644 --- a/src/compat/browser_tool_adapter.rs +++ b/src/compat/browser_tool_adapter.rs @@ -80,7 +80,8 @@ impl Tool for ZeroClawBrowserTool { Ok(ToolResult { success: result.success, output, - error: (!result.success).then(|| "browser action returned success=false".to_string()), + error: (!result.success) + .then(|| format_browser_action_error(&result.data)), }) } } @@ -145,6 +146,21 @@ fn failed_tool_result(error: String) -> ToolResult { } } +fn format_browser_action_error(data: &Value) -> String { + if let Some(error) = data.get("error") { + if let Some(message) = error.get("message").and_then(Value::as_str) { + return message.to_string(); + } + return format!("browser action failed: {error}"); + } + + if data.is_null() { + return "browser action returned success=false".to_string(); + } + + format!("browser action failed: {data}") +} + #[derive(Debug, thiserror::Error)] enum BrowserActionAdapterError { #[error("unsupported action: {0}")] diff --git a/src/compat/runtime.rs b/src/compat/runtime.rs index 5015293..d8618b2 100644 --- a/src/compat/runtime.rs +++ b/src/compat/runtime.rs @@ -15,7 +15,8 @@ use zeroclaw::providers::traits::{ }; use crate::compat::browser_tool_adapter::{ZeroClawBrowserTool, BROWSER_ACTION_TOOL_NAME}; -use crate::compat::config_adapter::build_zeroclaw_config; +use crate::compat::config_adapter::build_zeroclaw_config_from_settings; +use crate::config::DeepSeekSettings; use crate::compat::event_bridge::log_entry_for_turn_event; use crate::compat::memory_adapter::build_memory; use crate::pipe::{BrowserPipeTool, PipeError, Transport}; @@ -25,9 +26,9 @@ pub fn execute_task( browser_tool: BrowserPipeTool, instruction: &str, workspace_root: &Path, + settings: &DeepSeekSettings, ) -> Result { - let config = build_zeroclaw_config(workspace_root) - .map_err(|err| PipeError::Protocol(err.to_string()))?; + let config = build_zeroclaw_config_from_settings(workspace_root, settings); let provider = build_provider(&config)?; let runtime = tokio::runtime::Runtime::new() .map_err(|err| PipeError::Protocol(format!("failed to create tokio runtime: {err}")))?; diff --git a/src/config/settings.rs b/src/config/settings.rs index f2b6d1c..219c1f1 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -1,3 +1,6 @@ +use std::path::{Path, PathBuf}; + +use serde::Deserialize; use thiserror::Error; const DEFAULT_DEEPSEEK_BASE_URL: &str = "https://api.deepseek.com"; @@ -12,20 +15,65 @@ pub struct DeepSeekSettings { impl DeepSeekSettings { pub fn from_env() -> Result { - let api_key = std::env::var("DEEPSEEK_API_KEY") - .map_err(|_| ConfigError::MissingEnv("DEEPSEEK_API_KEY"))?; + Self::maybe_from_env()?.ok_or(ConfigError::MissingEnv("DEEPSEEK_API_KEY")) + } + + pub fn load(config_path: Option<&Path>) -> Result, ConfigError> { + if let Some(path) = config_path { + if path.exists() { + return Self::from_config_path(path).map(Some); + } + } + + Self::maybe_from_env() + } + + fn maybe_from_env() -> Result, ConfigError> { + let api_key = match std::env::var("DEEPSEEK_API_KEY") { + Ok(value) => value, + Err(std::env::VarError::NotPresent) => return Ok(None), + Err(std::env::VarError::NotUnicode(_)) => { + return Err(ConfigError::InvalidEnv("DEEPSEEK_API_KEY")) + } + }; let base_url = std::env::var("DEEPSEEK_BASE_URL") .unwrap_or_else(|_| DEFAULT_DEEPSEEK_BASE_URL.to_string()); let model = std::env::var("DEEPSEEK_MODEL").unwrap_or_else(|_| DEFAULT_DEEPSEEK_MODEL.to_string()); - if api_key.trim().is_empty() { + Ok(Some(Self::new(api_key, base_url, model)?)) + } + + fn from_config_path(path: &Path) -> Result { + let raw = std::fs::read_to_string(path) + .map_err(|err| ConfigError::ConfigRead(path.to_path_buf(), err.to_string()))?; + let config: RawDeepSeekSettings = serde_json::from_str(&raw) + .map_err(|err| ConfigError::ConfigParse(path.to_path_buf(), err.to_string()))?; + + Self::new(config.api_key, config.base_url, config.model) + .map_err(|err| err.with_path(path)) + } + + fn new(api_key: String, base_url: String, model: String) -> Result { + let api_key = api_key.trim().to_string(); + let base_url = if base_url.trim().is_empty() { + DEFAULT_DEEPSEEK_BASE_URL.to_string() + } else { + base_url.trim().to_string() + }; + let model = if model.trim().is_empty() { + DEFAULT_DEEPSEEK_MODEL.to_string() + } else { + model.trim().to_string() + }; + + if api_key.is_empty() { return Err(ConfigError::EmptyValue("DEEPSEEK_API_KEY")); } - if base_url.trim().is_empty() { + if base_url.is_empty() { return Err(ConfigError::EmptyValue("DEEPSEEK_BASE_URL")); } - if model.trim().is_empty() { + if model.is_empty() { return Err(ConfigError::EmptyValue("DEEPSEEK_MODEL")); } @@ -37,10 +85,37 @@ impl DeepSeekSettings { } } +#[derive(Debug, Deserialize)] +struct RawDeepSeekSettings { + #[serde(rename = "apiKey", default)] + api_key: String, + #[serde(rename = "baseUrl", default)] + base_url: String, + #[serde(default)] + model: String, +} + #[derive(Debug, Error, Clone, PartialEq, Eq)] pub enum ConfigError { #[error("missing environment variable: {0}")] MissingEnv(&'static str), #[error("environment variable must not be empty: {0}")] EmptyValue(&'static str), + #[error("invalid non-utf8 environment variable: {0}")] + InvalidEnv(&'static str), + #[error("failed to read DeepSeek config file {0}: {1}")] + ConfigRead(PathBuf, String), + #[error("invalid DeepSeek config JSON in {0}: {1}")] + ConfigParse(PathBuf, String), + #[error("DeepSeek config value must not be empty: {0} ({1})")] + ConfigValueEmpty(&'static str, PathBuf), +} + +impl ConfigError { + fn with_path(self, path: &Path) -> Self { + match self { + Self::EmptyValue(field) => Self::ConfigValueEmpty(field, path.to_path_buf()), + other => other, + } + } } diff --git a/src/lib.rs b/src/lib.rs index fe35d98..807fe53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,18 +9,25 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; -use agent::handle_browser_message; +use agent::{handle_browser_message_with_context, AgentRuntimeContext}; use pipe::{perform_handshake, BrowserPipeTool, PipeError, StdioTransport, Transport}; use security::MacPolicy; +fn default_rules_path_from_executable(executable_path: PathBuf) -> PathBuf { + executable_path + .parent() + .map(|dir| dir.join("resources").join("rules.json")) + .unwrap_or_else(|| PathBuf::from("resources").join("rules.json")) +} + fn default_rules_path() -> PathBuf { - std::env::current_dir() - .unwrap_or_else(|_| PathBuf::from(".")) - .join("resources") - .join("rules.json") + std::env::current_exe() + .map(default_rules_path_from_executable) + .unwrap_or_else(|_| PathBuf::from("resources").join("rules.json")) } pub fn run() -> Result<(), PipeError> { + let runtime_context = AgentRuntimeContext::from_process_args(std::env::args_os())?; let transport = Arc::new(StdioTransport::new(std::io::stdin(), std::io::stdout())); let handshake = perform_handshake(transport.as_ref(), Duration::from_secs(5))?; let mac_policy = MacPolicy::load_from_path(default_rules_path())?; @@ -32,7 +39,12 @@ pub fn run() -> Result<(), PipeError> { loop { match transport.recv_timeout(Duration::from_secs(3600)) { Ok(message) => { - handle_browser_message(transport.as_ref(), &browser_tool, message)?; + handle_browser_message_with_context( + transport.as_ref(), + &browser_tool, + &runtime_context, + message, + )?; } Err(PipeError::Timeout) => continue, Err(PipeError::PipeClosed) => return Ok(()), @@ -40,3 +52,21 @@ pub fn run() -> Result<(), PipeError> { } } } + +#[cfg(test)] +mod tests { + use super::default_rules_path_from_executable; + use std::path::PathBuf; + + #[test] + fn default_rules_path_uses_executable_directory_instead_of_cwd() { + let executable_path = PathBuf::from("/tmp/out/KylinRelease/sgclaw"); + + let resolved = default_rules_path_from_executable(executable_path); + + assert_eq!( + resolved, + PathBuf::from("/tmp/out/KylinRelease/resources/rules.json") + ); + } +} diff --git a/tests/compat_config_test.rs b/tests/compat_config_test.rs index 3a7042a..41ac578 100644 --- a/tests/compat_config_test.rs +++ b/tests/compat_config_test.rs @@ -1,3 +1,4 @@ +use std::fs; use std::path::Path; use std::sync::{Mutex, OnceLock}; @@ -7,6 +8,7 @@ use sgclaw::compat::config_adapter::{ zeroclaw_workspace_dir, }; use sgclaw::config::DeepSeekSettings; +use uuid::Uuid; fn env_lock() -> &'static Mutex<()> { static LOCK: OnceLock> = OnceLock::new(); @@ -53,3 +55,44 @@ fn zeroclaw_config_adapter_uses_deterministic_workspace_dir() { assert_eq!(config.default_model.as_deref(), Some("deepseek-reasoner")); assert_eq!(config.api_url.as_deref(), Some("https://proxy.example.com/v1")); } + +#[test] +fn deepseek_settings_reload_from_browser_config_path_after_file_changes() { + let root = std::env::temp_dir().join(format!("sgclaw-config-{}", Uuid::new_v4())); + fs::create_dir_all(&root).unwrap(); + let config_path = root.join("sgclaw_config.json"); + + fs::write( + &config_path, + r#"{ + "apiKey": "sk-first", + "baseUrl": "", + "model": "" +}"#, + ) + .unwrap(); + + let first = DeepSeekSettings::load(Some(config_path.as_path())) + .unwrap() + .expect("expected config file to produce settings"); + assert_eq!(first.api_key, "sk-first"); + assert_eq!(first.base_url, "https://api.deepseek.com"); + assert_eq!(first.model, "deepseek-chat"); + + fs::write( + &config_path, + r#"{ + "apiKey": "sk-second", + "baseUrl": "https://proxy.example.com/v1", + "model": "deepseek-reasoner" +}"#, + ) + .unwrap(); + + let second = DeepSeekSettings::load(Some(config_path.as_path())) + .unwrap() + .expect("expected updated config file to produce settings"); + assert_eq!(second.api_key, "sk-second"); + assert_eq!(second.base_url, "https://proxy.example.com/v1"); + assert_eq!(second.model, "deepseek-reasoner"); +} diff --git a/tests/compat_runtime_test.rs b/tests/compat_runtime_test.rs index 0851434..4a12b7c 100644 --- a/tests/compat_runtime_test.rs +++ b/tests/compat_runtime_test.rs @@ -1,5 +1,6 @@ mod common; +use std::fs; use std::io::{Read, Write}; use std::net::TcpListener; use std::path::PathBuf; @@ -9,8 +10,13 @@ use std::time::Duration; use common::MockTransport; use serde_json::{json, Value}; -use sgclaw::agent::handle_browser_message; +use sgclaw::agent::{ + handle_browser_message, + handle_browser_message_with_context, + AgentRuntimeContext, +}; use sgclaw::compat::runtime::execute_task; +use sgclaw::config::DeepSeekSettings; use sgclaw::pipe::{Action, AgentMessage, BrowserMessage, BrowserPipeTool, Timing}; use sgclaw::security::MacPolicy; use uuid::Uuid; @@ -40,6 +46,21 @@ fn temp_workspace_root() -> PathBuf { root } +fn write_deepseek_config(root: &PathBuf, api_key: &str, base_url: &str, model: &str) -> PathBuf { + let config_path = root.join("sgclaw_config.json"); + fs::write( + &config_path, + serde_json::to_string_pretty(&json!({ + "apiKey": api_key, + "baseUrl": base_url, + "model": model, + })) + .unwrap(), + ) + .unwrap(); + config_path +} + fn start_fake_deepseek_server( responses: Vec, ) -> (String, Arc>>, thread::JoinHandle<()>) { @@ -177,6 +198,7 @@ fn compat_runtime_uses_zeroclaw_provider_path_and_executes_browser_actions() { std::env::set_var("DEEPSEEK_MODEL", "deepseek-chat"); let workspace_root = temp_workspace_root(); + let settings = DeepSeekSettings::from_env().unwrap(); let transport = Arc::new(MockTransport::new(vec![ BrowserMessage::Response { seq: 1, @@ -211,6 +233,7 @@ fn compat_runtime_uses_zeroclaw_provider_path_and_executes_browser_actions() { browser_tool, "打开百度搜索天气", &workspace_root, + &settings, ) .unwrap(); server_handle.join().unwrap(); @@ -255,7 +278,151 @@ fn compat_runtime_uses_zeroclaw_provider_path_and_executes_browser_actions() { } #[test] -fn handle_browser_message_uses_compat_runtime_summary_when_deepseek_env_is_set() { +fn handle_browser_message_prefers_compat_runtime_for_supported_instruction_when_deepseek_is_configured() { + let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); + + let first_response = json!({ + "choices": [{ + "message": { + "content": "", + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": { + "name": "browser_action", + "arguments": serde_json::to_string(&json!({ + "action": "navigate", + "expected_domain": "www.baidu.com", + "url": "https://www.baidu.com" + })).unwrap() + } + }, + { + "id": "call_2", + "type": "function", + "function": { + "name": "browser_action", + "arguments": serde_json::to_string(&json!({ + "action": "type", + "expected_domain": "www.baidu.com", + "selector": "#kw", + "text": "天气", + "clear_first": true + })).unwrap() + } + }, + { + "id": "call_3", + "type": "function", + "function": { + "name": "browser_action", + "arguments": serde_json::to_string(&json!({ + "action": "click", + "expected_domain": "www.baidu.com", + "selector": "#su" + })).unwrap() + } + } + ] + } + }] + }); + let second_response = json!({ + "choices": [{ + "message": { + "content": "已通过 DeepSeek 执行任务: 打开百度搜索天气" + } + }] + }); + let (base_url, requests, server_handle) = + start_fake_deepseek_server(vec![first_response, second_response]); + + std::env::remove_var("DEEPSEEK_API_KEY"); + std::env::remove_var("DEEPSEEK_BASE_URL"); + std::env::remove_var("DEEPSEEK_MODEL"); + + let workspace_root = temp_workspace_root(); + let config_path = write_deepseek_config( + &workspace_root, + "deepseek-test-key", + &base_url, + "deepseek-chat", + ); + let runtime_context = AgentRuntimeContext::new(Some(config_path), workspace_root.clone()); + + let transport = Arc::new(MockTransport::new(vec![ + BrowserMessage::Response { + seq: 1, + success: true, + data: json!({ "navigated": true }), + aom_snapshot: vec![], + timing: Timing { + queue_ms: 1, + exec_ms: 10, + }, + }, + BrowserMessage::Response { + seq: 2, + success: true, + data: json!({ "typed": true }), + aom_snapshot: vec![], + timing: Timing { + queue_ms: 1, + exec_ms: 10, + }, + }, + BrowserMessage::Response { + seq: 3, + success: true, + data: json!({ "clicked": true }), + aom_snapshot: vec![], + timing: Timing { + queue_ms: 1, + exec_ms: 10, + }, + }, + ])); + let browser_tool = BrowserPipeTool::new( + transport.clone(), + test_policy(), + 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(), + }, + ) + .unwrap(); + server_handle.join().unwrap(); + + let sent = transport.sent_messages(); + let request_bodies = requests.lock().unwrap().clone(); + + assert!(sent.iter().any(|message| { + matches!( + message, + AgentMessage::TaskComplete { success, summary } + if *success && summary == "已通过 DeepSeek 执行任务: 打开百度搜索天气" + ) + })); + assert!(sent.iter().any(|message| { + matches!( + message, + AgentMessage::LogEntry { level, message } + if level == "mode" && message == "compat_llm_primary" + ) + })); + assert_eq!(request_bodies.len(), 2); +} + +#[test] +fn handle_browser_message_falls_back_to_compat_runtime_for_unsupported_instruction() { let _guard = env_lock().lock().unwrap_or_else(|err| err.into_inner()); let first_response = json!({ @@ -284,7 +451,8 @@ fn handle_browser_message_uses_compat_runtime_summary_when_deepseek_env_is_set() } }] }); - let (base_url, _, server_handle) = start_fake_deepseek_server(vec![first_response, second_response]); + let (base_url, requests, server_handle) = + start_fake_deepseek_server(vec![first_response, second_response]); std::env::set_var("DEEPSEEK_API_KEY", "deepseek-test-key"); std::env::set_var("DEEPSEEK_BASE_URL", base_url); @@ -315,7 +483,7 @@ fn handle_browser_message_uses_compat_runtime_summary_when_deepseek_env_is_set() transport.as_ref(), &browser_tool, BrowserMessage::SubmitTask { - instruction: "打开百度搜索天气".to_string(), + instruction: "帮我打开百度首页".to_string(), }, ) .unwrap(); @@ -323,6 +491,7 @@ fn handle_browser_message_uses_compat_runtime_summary_when_deepseek_env_is_set() std::env::set_current_dir(original_dir).unwrap(); let sent = transport.sent_messages(); + let request_bodies = requests.lock().unwrap().clone(); assert!(sent.iter().any(|message| { matches!( @@ -331,4 +500,12 @@ fn handle_browser_message_uses_compat_runtime_summary_when_deepseek_env_is_set() if *success && summary == "来自 ZeroClaw runtime" ) })); + assert!(sent.iter().any(|message| { + matches!( + message, + AgentMessage::LogEntry { level, message } + if level == "mode" && message == "compat_llm_primary" + ) + })); + assert_eq!(request_bodies.len(), 2); } diff --git a/tests/runtime_task_flow_test.rs b/tests/runtime_task_flow_test.rs index af81bbc..a174540 100644 --- a/tests/runtime_task_flow_test.rs +++ b/tests/runtime_task_flow_test.rs @@ -74,39 +74,44 @@ fn submit_task_sends_three_commands_and_finishes_with_task_complete() { let sent = transport.sent_messages(); - assert_eq!(sent.len(), 7); + assert_eq!(sent.len(), 8); assert!(matches!( &sent[0], + AgentMessage::LogEntry { level, message } + if level == "mode" && message == "deterministic_planner" + )); + assert!(matches!( + &sent[1], AgentMessage::LogEntry { level, message } if level == "info" && message == "navigate https://www.baidu.com" )); assert!(matches!( - &sent[1], + &sent[2], AgentMessage::Command { seq, action, .. } if *seq == 1 && action == &Action::Navigate )); assert!(matches!( - &sent[2], + &sent[3], AgentMessage::LogEntry { level, message } if level == "info" && message == "type 天气 into #kw" )); assert!(matches!( - &sent[3], + &sent[4], AgentMessage::Command { seq, action, .. } if *seq == 2 && action == &Action::Type )); assert!(matches!( - &sent[4], + &sent[5], AgentMessage::LogEntry { level, message } if level == "info" && message == "click #su" )); assert!(matches!( - &sent[5], + &sent[6], AgentMessage::Command { seq, action, .. } if *seq == 3 && action == &Action::Click )); assert!(matches!( - &sent[6], + &sent[7], AgentMessage::TaskComplete { success, summary } if *success && summary == "已在百度搜索天气" ));