Files
freedak f7a720204a Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
2026-07-04 19:20:46 +08:00

322 lines
10 KiB
Rust

mod common;
use std::sync::Arc;
use serde_json::json;
use nomi_agent::engine::AgentEngine;
use nomi_agent::output::OutputSink;
use nomi_agent::output::terminal::TerminalSink;
use nomi_protocol::writer::ProtocolWriter;
use nomi_protocol::{ToolApprovalManager, ToolApprovalResult};
use nomi_tools::registry::ToolRegistry;
use nomi_types::llm::LlmEvent;
use nomi_types::message::{StopReason, TokenUsage};
use common::{ExecMockTool, MockLlmProvider, test_config};
fn silent_output() -> Arc<dyn OutputSink> {
Arc::new(TerminalSink::new(true))
}
fn token_usage(input: u64, output: u64) -> TokenUsage {
TokenUsage {
input_tokens: input,
output_tokens: output,
cache_creation_tokens: 0,
cache_read_tokens: 0,
}
}
// ---------------------------------------------------------------------------
// test: tool approval approve flow
//
// LLM requests exec_tool → engine pauses at approval_manager.request_approval
// → background task resolves with Approved → tool executes → LLM continues
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_tool_approval_approve_flow() {
let turn1 = vec![
LlmEvent::ToolUse {
id: "call-1".to_string(),
name: "exec_tool".to_string(),
input: json!({}),
extra: None,
},
LlmEvent::Done {
stop_reason: StopReason::ToolUse,
usage: token_usage(80, 30),
},
];
let turn2 = vec![
LlmEvent::TextDelta("Done".to_string()),
LlmEvent::Done {
stop_reason: StopReason::EndTurn,
usage: token_usage(100, 50),
},
];
let provider = Arc::new(MockLlmProvider::with_turns(vec![turn1, turn2]));
let mut config = test_config();
config.tools.auto_approve = false;
let mut registry = ToolRegistry::new();
registry.register(Box::new(ExecMockTool::new("exec_tool", "tool output")));
let output = silent_output();
let approval_manager = Arc::new(ToolApprovalManager::new());
let writer = Arc::new(ProtocolWriter::new());
let mut engine =
AgentEngine::new_with_provider(provider, config, registry, output, std::env::temp_dir());
engine.set_approval_manager(approval_manager.clone());
engine.set_protocol_writer(writer);
// Spawn a task that approves the tool call after a short delay
let am = approval_manager.clone();
tokio::spawn(async move {
// Wait until the approval request appears
loop {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
let has_pending = {
// Check if there's a pending request by trying to resolve a known id
// We know the call_id is "call-1" from the mock
true
};
if has_pending {
am.resolve("call-1", ToolApprovalResult::Approved);
break;
}
}
});
let result = engine
.run("Use the tool", "msg-1")
.await
.expect("should succeed");
assert_eq!(result.text, "Done");
assert_eq!(result.turns, 2);
}
// ---------------------------------------------------------------------------
// test: tool approval deny flow
//
// LLM requests exec_tool → engine pauses → background resolves with Denied
// → tool_cancelled → denial fed back to LLM → LLM responds with text
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_tool_approval_deny_flow() {
let turn1 = vec![
LlmEvent::ToolUse {
id: "call-2".to_string(),
name: "exec_tool".to_string(),
input: json!({}),
extra: None,
},
LlmEvent::Done {
stop_reason: StopReason::ToolUse,
usage: token_usage(80, 30),
},
];
let turn2 = vec![
LlmEvent::TextDelta("Cannot run tool".to_string()),
LlmEvent::Done {
stop_reason: StopReason::EndTurn,
usage: token_usage(100, 50),
},
];
let provider = Arc::new(MockLlmProvider::with_turns(vec![turn1, turn2]));
let mut config = test_config();
config.tools.auto_approve = false;
let mut registry = ToolRegistry::new();
registry.register(Box::new(ExecMockTool::new("exec_tool", "tool output")));
let output = silent_output();
let approval_manager = Arc::new(ToolApprovalManager::new());
let writer = Arc::new(ProtocolWriter::new());
let mut engine =
AgentEngine::new_with_provider(provider, config, registry, output, std::env::temp_dir());
engine.set_approval_manager(approval_manager.clone());
engine.set_protocol_writer(writer);
let am = approval_manager.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
am.resolve(
"call-2",
ToolApprovalResult::Denied {
reason: "policy violation".into(),
},
);
});
let result = engine
.run("Use the tool", "msg-2")
.await
.expect("should succeed");
assert_eq!(result.text, "Cannot run tool");
assert_eq!(result.turns, 2);
}
// ---------------------------------------------------------------------------
// test: auto_approve bypasses approval wait
//
// With auto_approve=true, exec category tools should execute immediately
// without waiting for approval.
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_auto_approve_bypasses_approval() {
let turn1 = vec![
LlmEvent::ToolUse {
id: "call-3".to_string(),
name: "exec_tool".to_string(),
input: json!({}),
extra: None,
},
LlmEvent::Done {
stop_reason: StopReason::ToolUse,
usage: token_usage(80, 30),
},
];
let turn2 = vec![
LlmEvent::TextDelta("Auto done".to_string()),
LlmEvent::Done {
stop_reason: StopReason::EndTurn,
usage: token_usage(100, 50),
},
];
let provider = Arc::new(MockLlmProvider::with_turns(vec![turn1, turn2]));
let mut config = test_config();
config.tools.auto_approve = true;
let mut registry = ToolRegistry::new();
registry.register(Box::new(ExecMockTool::new("exec_tool", "tool output")));
let output = silent_output();
let approval_manager = Arc::new(ToolApprovalManager::new());
let writer = Arc::new(ProtocolWriter::new());
let mut engine =
AgentEngine::new_with_provider(provider, config, registry, output, std::env::temp_dir());
engine.set_approval_manager(approval_manager.clone());
engine.set_protocol_writer(writer);
// No background task to approve — should not hang
let result = engine
.run("Use the tool", "msg-3")
.await
.expect("should succeed");
assert_eq!(result.text, "Auto done");
assert_eq!(result.turns, 2);
}
// ---------------------------------------------------------------------------
// test: session auto-approve (scope=always) bypasses future approvals
//
// After add_auto_approve("exec"), exec tools skip the approval wait.
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_session_auto_approve_category() {
let turn1 = vec![
LlmEvent::ToolUse {
id: "call-4".to_string(),
name: "exec_tool".to_string(),
input: json!({}),
extra: None,
},
LlmEvent::Done {
stop_reason: StopReason::ToolUse,
usage: token_usage(80, 30),
},
];
let turn2 = vec![
LlmEvent::TextDelta("Session auto".to_string()),
LlmEvent::Done {
stop_reason: StopReason::EndTurn,
usage: token_usage(100, 50),
},
];
let provider = Arc::new(MockLlmProvider::with_turns(vec![turn1, turn2]));
let mut config = test_config();
config.tools.auto_approve = false;
let mut registry = ToolRegistry::new();
registry.register(Box::new(ExecMockTool::new("exec_tool", "tool output")));
let output = silent_output();
let approval_manager = Arc::new(ToolApprovalManager::new());
// Pre-approve the "exec" category
approval_manager.add_auto_approve("exec");
let writer = Arc::new(ProtocolWriter::new());
let mut engine =
AgentEngine::new_with_provider(provider, config, registry, output, std::env::temp_dir());
engine.set_approval_manager(approval_manager.clone());
engine.set_protocol_writer(writer);
// No background task to approve — should not hang
let result = engine
.run("Use the tool", "msg-4")
.await
.expect("should succeed");
assert_eq!(result.text, "Session auto");
assert_eq!(result.turns, 2);
}
// ---------------------------------------------------------------------------
// test: client disconnect (channel drop) causes UserAborted
//
// If the approval channel sender is dropped before resolve, the engine
// should return an abort error.
// ---------------------------------------------------------------------------
#[tokio::test]
async fn test_client_disconnect_aborts() {
let turn1 = vec![
LlmEvent::ToolUse {
id: "call-5".to_string(),
name: "exec_tool".to_string(),
input: json!({}),
extra: None,
},
LlmEvent::Done {
stop_reason: StopReason::ToolUse,
usage: token_usage(80, 30),
},
];
let provider = Arc::new(MockLlmProvider::with_turns(vec![turn1]));
let mut config = test_config();
config.tools.auto_approve = false;
let mut registry = ToolRegistry::new();
registry.register(Box::new(ExecMockTool::new("exec_tool", "tool output")));
let output = silent_output();
let approval_manager = Arc::new(ToolApprovalManager::new());
let writer = Arc::new(ProtocolWriter::new());
let mut engine =
AgentEngine::new_with_provider(provider, config, registry, output, std::env::temp_dir());
engine.set_approval_manager(approval_manager.clone());
engine.set_protocol_writer(writer);
// Simulate client disconnect: drop the pending sender without resolving
let am = approval_manager.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
am.drop_pending("call-5");
});
let err = engine.run("Use the tool", "msg-5").await.unwrap_err();
assert!(
format!("{:?}", err).contains("UserAborted"),
"expected UserAborted, got: {:?}",
err
);
}