Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user