From 6f8568d16b42c9e05570ffdb6cfa7afbe885dca6 Mon Sep 17 00:00:00 2001 From: AnyBot Dev Date: Mon, 9 Nov 2026 20:57:32 +0800 Subject: [PATCH] A live-model e2e: any OpenAI-compatible brain must drive a shell turn (gated by env) --- src/agent.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/agent.rs b/src/agent.rs index 9f8d780..e77e43c 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -2421,6 +2421,57 @@ mod tests { /// a box: `ANYBOT_DATA_DIR= cargo test --release -- --ignored /// a_brain_without_chrome_browses_on_its_box --nocapture`. #[test] + #[ignore] + fn real_model_drives_a_shell_turn() { + // A real brain, end to end: point it at any OpenAI-compatible endpoint with + // ANYBOT_LIVE_BASE / ANYBOT_LIVE_KEY / ANYBOT_LIVE_MODEL and run + // `cargo test real_model_drives -- --ignored --nocapture`. It asks the model to write + // a file and read it back; the turn must survive real tool calls, not a script. + let Ok(base) = std::env::var("ANYBOT_LIVE_BASE") else { eprintln!("no ANYBOT_LIVE_BASE; skipped"); return }; + let key = std::env::var("ANYBOT_LIVE_KEY").unwrap_or_default(); + let model = std::env::var("ANYBOT_LIVE_MODEL").unwrap_or_else(|_| "husky".into()); + let own = crate::model::BOT_COMPUTER_NAME.to_string(); + let _ = std::fs::remove_dir_all("/tmp/anybot-live"); + let job = Job { + req: ChatRequest { + kind: ProviderKind::Custom, + base_url: base, + api_key: key, + model, + system: "你是 AnyBot。用工具完成使用者的要求,最後用一句中文回答。".into(), + history: vec![Message::user("在 /tmp/anybot-live 建立 hello.txt,內容寫 anybot-was-here,然後用 shell 讀回來,告訴我市面檔裡寫了什麼。")], + tools: tools::specs(), + session: String::new(), + }, + computers: vec![tools::Target { name: own.clone(), place: tools::Place::Local { workdir: "/tmp".into() } }], + default_computer: own.clone(), + roles: tools::Roles { own, person: None, server: true }, + local_execution: false, + direct: Arc::new(AtomicBool::new(true)), + ..test_job() + }; + let (tx, rx) = channel(); + spawn(job, 1, tx, Arc::new(AtomicBool::new(false)), eframe::egui::Context::default()); + let mut tools_seen = vec![]; + let mut answer = String::new(); + loop { + match rx.recv_timeout(Duration::from_secs(180)).expect("loop stalled") { + (_, StreamEvent::ToolCall(m)) => tools_seen.push(m.name.clone()), + (_, StreamEvent::ToolResult(m)) => { + println!("[tool] {}: {}", m.tool_name, m.text.chars().take(120).collect::()); + tools_seen.push(format!("done:{}", m.tool_name)); + } + (_, StreamEvent::Text(t)) => answer.push_str(&t), + (_, StreamEvent::Done) => break, + (_, StreamEvent::Error(e)) => panic!("model error: {e}"), + _ => {} + } + } + println!("tools: {tools_seen:?}\nanswer: {answer}"); + assert!(tools_seen.iter().any(|t| t.contains("shell")), "the model never ran a shell tool: {tools_seen:?}"); + assert!(answer.contains("anybot-was-here") || std::fs::read_to_string("/tmp/anybot-live/hello.txt").unwrap_or_default().contains("anybot-was-here"), "the file never appeared; answer was: {answer}"); + } + #[ignore] fn a_brain_without_chrome_browses_on_its_box() { let own = crate::model::BOT_COMPUTER_NAME;