Files
zclaw_openfang/crates/zclaw-kernel/tests/hand_chain.rs
iven 591af5802c
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
test(kernel,growth): Phase 1 缝测试安全网 — 3条核心链路 19 测试全部通过
对话链路: 4 缝测试 (Tauri→Kernel / Kernel→LLM / LLM→UI / 流式生命周期)
Hands链路: 3 缝测试 (工具路由 / 执行回调 / 通用工具)
记忆链路: 3 缝测试 (FTS5存储 / 模式检索 / 去重)
冒烟测试: 3 Rust + 8 TypeScript 全量 PASS
- Kernel::boot_with_driver() 测试辅助方法
- 全量 cargo test 0 回归

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 21:36:46 +08:00

225 lines
7.9 KiB
Rust

//! Hands chain seam tests
//!
//! Verifies the integration seams in the Hand execution pipeline:
//! 1. Tool routing: LLM tool_call → HandRegistry correct dispatch
//! 2. Execution callback: Hand complete → LoopEvent emitted
//! 3. Non-hand tool routing
use std::sync::Arc;
use zclaw_kernel::{Kernel, KernelConfig};
use zclaw_runtime::test_util::MockLlmDriver;
use zclaw_runtime::stream::StreamChunk;
use zclaw_runtime::{LoopEvent, LlmDriver};
use zclaw_types::AgentConfig;
// ---------------------------------------------------------------------------
// Seam 1: Tool routing — LLM tool_call triggers HandTool dispatch
// ---------------------------------------------------------------------------
#[tokio::test]
async fn seam_hand_tool_routing() {
// First stream: tool_use for hand_quiz
let mock = MockLlmDriver::new()
.with_stream_chunks(vec![
StreamChunk::TextDelta { delta: "Let me generate a quiz.".to_string() },
StreamChunk::ToolUseStart { id: "call_quiz_1".to_string(), name: "hand_quiz".to_string() },
StreamChunk::ToolUseEnd {
id: "call_quiz_1".to_string(),
input: serde_json::json!({ "topic": "math", "count": 3 }),
},
StreamChunk::Complete {
input_tokens: 10,
output_tokens: 20,
stop_reason: "tool_use".to_string(),
},
])
// Second stream: final text after tool executes
.with_stream_chunks(vec![
StreamChunk::TextDelta { delta: "Here is your quiz!".to_string() },
StreamChunk::Complete {
input_tokens: 10,
output_tokens: 5,
stop_reason: "end_turn".to_string(),
},
]);
let config = KernelConfig::default();
let kernel = Kernel::boot_with_driver(config, Arc::new(mock) as Arc<dyn LlmDriver>)
.await
.expect("kernel boot");
let agent_config = AgentConfig::new("test-agent")
.with_system_prompt("You are a test assistant.");
let id = agent_config.id;
kernel.spawn_agent(agent_config).await.expect("spawn agent");
let mut rx = kernel
.send_message_stream(&id, "Generate a math quiz".to_string())
.await
.expect("stream");
let mut tool_starts = Vec::new();
let mut tool_ends = Vec::new();
let mut got_complete = false;
while let Some(event) = rx.recv().await {
match &event {
LoopEvent::ToolStart { name, input } => {
tool_starts.push((name.clone(), input.clone()));
}
LoopEvent::ToolEnd { name, output } => {
tool_ends.push((name.clone(), output.clone()));
}
LoopEvent::Complete(_) => {
got_complete = true;
break;
}
LoopEvent::Error(msg) => {
panic!("unexpected error: {}", msg);
}
_ => {}
}
}
assert!(got_complete, "stream should complete");
assert!(
tool_starts.iter().any(|(n, _)| n == "hand_quiz"),
"should see hand_quiz tool_start, got: {:?}",
tool_starts
);
}
// ---------------------------------------------------------------------------
// Seam 2: Execution callback — Hand completes and produces tool_end
// ---------------------------------------------------------------------------
#[tokio::test]
async fn seam_hand_execution_callback() {
let mock = MockLlmDriver::new()
.with_stream_chunks(vec![
StreamChunk::ToolUseStart { id: "call_quiz_1".to_string(), name: "hand_quiz".to_string() },
StreamChunk::ToolUseEnd {
id: "call_quiz_1".to_string(),
input: serde_json::json!({ "topic": "math" }),
},
StreamChunk::Complete {
input_tokens: 10,
output_tokens: 5,
stop_reason: "tool_use".to_string(),
},
])
.with_stream_chunks(vec![
StreamChunk::TextDelta { delta: "Done!".to_string() },
StreamChunk::Complete {
input_tokens: 5,
output_tokens: 1,
stop_reason: "end_turn".to_string(),
},
]);
let config = KernelConfig::default();
let kernel = Kernel::boot_with_driver(config, Arc::new(mock) as Arc<dyn LlmDriver>)
.await
.expect("kernel boot");
let agent_config = AgentConfig::new("test-agent");
let id = agent_config.id;
kernel.spawn_agent(agent_config).await.expect("spawn agent");
let mut rx = kernel
.send_message_stream(&id, "Quiz me".to_string())
.await
.expect("stream");
let mut got_tool_end = false;
let mut got_complete = false;
while let Some(event) = rx.recv().await {
match &event {
LoopEvent::ToolEnd { name, output } => {
got_tool_end = true;
assert!(name.starts_with("hand_"), "tool_end should be hand tool, got: {}", name);
// Quiz hand returns structured JSON output
assert!(output.is_object() || output.is_string(), "output should be JSON, got: {}", output);
}
LoopEvent::Complete(_) => {
got_complete = true;
break;
}
LoopEvent::Error(msg) => {
panic!("unexpected error: {}", msg);
}
_ => {}
}
}
assert!(got_tool_end, "should receive tool_end after hand execution");
assert!(got_complete, "should complete after tool_end");
}
// ---------------------------------------------------------------------------
// Seam 3: Non-hand tool call (generic tool) routes correctly
// ---------------------------------------------------------------------------
#[tokio::test]
async fn seam_generic_tool_routing() {
// Mock with a generic tool call (web_search)
let mock = MockLlmDriver::new()
.with_stream_chunks(vec![
StreamChunk::ToolUseStart { id: "call_ws_1".to_string(), name: "web_search".to_string() },
StreamChunk::ToolUseEnd {
id: "call_ws_1".to_string(),
input: serde_json::json!({ "query": "test query" }),
},
StreamChunk::Complete {
input_tokens: 10,
output_tokens: 5,
stop_reason: "tool_use".to_string(),
},
])
.with_stream_chunks(vec![
StreamChunk::TextDelta { delta: "Search results found.".to_string() },
StreamChunk::Complete {
input_tokens: 5,
output_tokens: 3,
stop_reason: "end_turn".to_string(),
},
]);
let config = KernelConfig::default();
let kernel = Kernel::boot_with_driver(config, Arc::new(mock) as Arc<dyn LlmDriver>)
.await
.expect("kernel boot");
let agent_config = AgentConfig::new("test-agent");
let id = agent_config.id;
kernel.spawn_agent(agent_config).await.expect("spawn agent");
let mut rx = kernel
.send_message_stream(&id, "Search for test".to_string())
.await
.expect("stream");
let mut tool_names = Vec::new();
let mut got_complete = false;
while let Some(event) = rx.recv().await {
match &event {
LoopEvent::ToolStart { name, .. } => tool_names.push(name.clone()),
LoopEvent::ToolEnd { name, .. } => tool_names.push(format!("end:{}", name)),
LoopEvent::Complete(_) => {
got_complete = true;
break;
}
LoopEvent::Error(msg) => {
panic!("unexpected error: {}", msg);
}
_ => {}
}
}
assert!(got_complete, "stream should complete");
assert!(
tool_names.iter().any(|n| n.contains("web_search")),
"should see web_search tool events, got: {:?}",
tool_names
);
}