f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
316 lines
11 KiB
Rust
316 lines
11 KiB
Rust
//! Integration tests for Plan Mode engine integration (task 3.5).
|
|
//!
|
|
//! Tests are numbered to match the test-plan.md identifiers (TC-3.5-*).
|
|
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::AtomicBool;
|
|
|
|
use async_trait::async_trait;
|
|
use nomi_agent::plan::tools::{EnterPlanModeTool, ExitPlanModeTool};
|
|
use nomi_protocol::events::ToolCategory;
|
|
use nomi_tools::Tool;
|
|
use nomi_tools::registry::ToolRegistry;
|
|
use serde_json::json;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers: mock tools with configurable categories
|
|
// ---------------------------------------------------------------------------
|
|
|
|
struct MockTool {
|
|
tool_name: String,
|
|
tool_category: ToolCategory,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl Tool for MockTool {
|
|
fn name(&self) -> &str {
|
|
&self.tool_name
|
|
}
|
|
|
|
fn description(&self) -> &str {
|
|
"mock tool"
|
|
}
|
|
|
|
fn input_schema(&self) -> serde_json::Value {
|
|
json!({"type": "object", "properties": {}, "required": []})
|
|
}
|
|
|
|
fn is_concurrency_safe(&self, _input: &serde_json::Value) -> bool {
|
|
true
|
|
}
|
|
|
|
async fn execute(&self, _input: serde_json::Value) -> nomi_types::tool::ToolResult {
|
|
nomi_types::tool::ToolResult {
|
|
content: "ok".to_string(),
|
|
is_error: false,
|
|
images: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn category(&self) -> ToolCategory {
|
|
self.tool_category
|
|
}
|
|
}
|
|
|
|
fn mock_tool(name: &str, category: ToolCategory) -> Box<MockTool> {
|
|
Box::new(MockTool {
|
|
tool_name: name.to_string(),
|
|
tool_category: category,
|
|
})
|
|
}
|
|
|
|
/// Build a registry with typical tools + plan mode tools
|
|
fn build_test_registry() -> ToolRegistry {
|
|
let flag = Arc::new(AtomicBool::new(false));
|
|
let mut registry = ToolRegistry::new();
|
|
|
|
// Info tools
|
|
registry.register(mock_tool("Read", ToolCategory::Info));
|
|
registry.register(mock_tool("Grep", ToolCategory::Info));
|
|
registry.register(mock_tool("Glob", ToolCategory::Info));
|
|
registry.register(mock_tool("Skill", ToolCategory::Info));
|
|
|
|
// Edit tools
|
|
registry.register(mock_tool("Write", ToolCategory::Edit));
|
|
registry.register(mock_tool("Edit", ToolCategory::Edit));
|
|
|
|
// Exec tools
|
|
registry.register(mock_tool("Bash", ToolCategory::Exec));
|
|
|
|
// Plan mode tools (both are Info category)
|
|
registry.register(Box::new(EnterPlanModeTool::new(Arc::clone(&flag))));
|
|
registry.register(Box::new(ExitPlanModeTool::new(Arc::clone(&flag))));
|
|
|
|
registry
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TC-3.5-01: Plan mode filters to only Info tools + ExitPlanMode
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn tc_3_5_01_plan_mode_only_info_tools_plus_exit() {
|
|
let registry = build_test_registry();
|
|
|
|
// Plan mode filter: Info category except EnterPlanMode
|
|
let defs = registry.to_tool_defs_filtered(|t| {
|
|
t.category() == ToolCategory::Info && t.name() != "EnterPlanMode"
|
|
});
|
|
|
|
let names: Vec<&str> = defs.iter().map(|d| d.name.as_str()).collect();
|
|
|
|
// Should include Info tools
|
|
assert!(
|
|
names.contains(&"Read"),
|
|
"Read should be available in plan mode"
|
|
);
|
|
assert!(
|
|
names.contains(&"Grep"),
|
|
"Grep should be available in plan mode"
|
|
);
|
|
assert!(
|
|
names.contains(&"Glob"),
|
|
"Glob should be available in plan mode"
|
|
);
|
|
assert!(
|
|
names.contains(&"Skill"),
|
|
"Skill should be available in plan mode"
|
|
);
|
|
assert!(
|
|
names.contains(&"ExitPlanMode"),
|
|
"ExitPlanMode should be available in plan mode"
|
|
);
|
|
|
|
// Should NOT include write/exec/EnterPlanMode
|
|
assert!(
|
|
!names.contains(&"Write"),
|
|
"Write should NOT be in plan mode"
|
|
);
|
|
assert!(!names.contains(&"Edit"), "Edit should NOT be in plan mode");
|
|
assert!(!names.contains(&"Bash"), "Bash should NOT be in plan mode");
|
|
assert!(
|
|
!names.contains(&"EnterPlanMode"),
|
|
"EnterPlanMode should NOT be in plan mode"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TC-3.5-02: Normal mode includes all tools except ExitPlanMode
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn tc_3_5_02_normal_mode_all_tools_except_exit_plan_mode() {
|
|
let registry = build_test_registry();
|
|
|
|
// Normal mode filter: everything except ExitPlanMode
|
|
let defs = registry.to_tool_defs_filtered(|t| t.name() != "ExitPlanMode");
|
|
|
|
let names: Vec<&str> = defs.iter().map(|d| d.name.as_str()).collect();
|
|
|
|
// Should include all standard tools and EnterPlanMode
|
|
assert!(names.contains(&"Read"));
|
|
assert!(names.contains(&"Grep"));
|
|
assert!(names.contains(&"Glob"));
|
|
assert!(names.contains(&"Skill"));
|
|
assert!(names.contains(&"Write"));
|
|
assert!(names.contains(&"Edit"));
|
|
assert!(names.contains(&"Bash"));
|
|
assert!(
|
|
names.contains(&"EnterPlanMode"),
|
|
"EnterPlanMode should be in normal mode"
|
|
);
|
|
|
|
// Should NOT include ExitPlanMode
|
|
assert!(
|
|
!names.contains(&"ExitPlanMode"),
|
|
"ExitPlanMode should NOT be in normal mode"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TC-3.5-07: to_tool_defs_filtered correctly filters mixed categories
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn tc_3_5_07_to_tool_defs_filtered_mixed_categories() {
|
|
let mut registry = ToolRegistry::new();
|
|
let flag = Arc::new(AtomicBool::new(false));
|
|
|
|
registry.register(mock_tool("Read", ToolCategory::Info));
|
|
registry.register(mock_tool("Write", ToolCategory::Edit));
|
|
registry.register(mock_tool("Bash", ToolCategory::Exec));
|
|
registry.register(Box::new(ExitPlanModeTool::new(flag)));
|
|
|
|
let defs = registry.to_tool_defs_filtered(|t| {
|
|
t.category() == ToolCategory::Info || t.name() == "ExitPlanMode"
|
|
});
|
|
|
|
let names: Vec<&str> = defs.iter().map(|d| d.name.as_str()).collect();
|
|
assert!(names.contains(&"Read"), "Info tool should be included");
|
|
assert!(
|
|
names.contains(&"ExitPlanMode"),
|
|
"ExitPlanMode should be included"
|
|
);
|
|
assert!(!names.contains(&"Write"), "Edit tool should be excluded");
|
|
assert!(!names.contains(&"Bash"), "Exec tool should be excluded");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TC-3.5-08: System prompt dynamically includes plan mode instructions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn tc_3_5_08_system_prompt_includes_plan_mode_when_active() {
|
|
use nomi_agent::plan::prompt::plan_mode_instructions;
|
|
|
|
let base_prompt = "You are an AI assistant.";
|
|
let instructions = plan_mode_instructions();
|
|
|
|
// Simulate plan mode active: system prompt should contain plan instructions
|
|
let active_prompt = format!("{}\n\n{}", base_prompt, instructions);
|
|
assert!(
|
|
active_prompt.contains("Plan Mode"),
|
|
"active prompt should contain plan mode instructions"
|
|
);
|
|
assert!(
|
|
active_prompt.contains("MUST NOT"),
|
|
"should contain plan mode restrictions"
|
|
);
|
|
assert!(
|
|
active_prompt.contains(base_prompt),
|
|
"should still contain base prompt"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn tc_3_5_08_system_prompt_excludes_plan_mode_when_inactive() {
|
|
let base_prompt = "You are an AI assistant.";
|
|
|
|
// Normal mode: system prompt is just the base
|
|
assert!(
|
|
!base_prompt.contains("Plan Mode"),
|
|
"inactive prompt should NOT contain plan mode instructions"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// TC-3.5-06: PlanConfig disabled means no plan tools registered
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn tc_3_5_06_disabled_config_no_plan_tools() {
|
|
// Simulate PlanConfig.enabled = false — do not register plan tools
|
|
let mut registry = ToolRegistry::new();
|
|
registry.register(mock_tool("Read", ToolCategory::Info));
|
|
registry.register(mock_tool("Write", ToolCategory::Edit));
|
|
// Intentionally NOT registering EnterPlanMode/ExitPlanMode
|
|
|
|
let defs = registry.to_tool_defs();
|
|
let names: Vec<&str> = defs.iter().map(|d| d.name.as_str()).collect();
|
|
|
|
assert!(
|
|
!names.contains(&"EnterPlanMode"),
|
|
"EnterPlanMode should not be registered when disabled"
|
|
);
|
|
assert!(
|
|
!names.contains(&"ExitPlanMode"),
|
|
"ExitPlanMode should not be registered when disabled"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Additional: plan mode filter with MCP tools
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn plan_mode_filter_excludes_mcp_tools() {
|
|
let mut registry = ToolRegistry::new();
|
|
registry.register(mock_tool("Read", ToolCategory::Info));
|
|
registry.register(mock_tool("mcp_server_tool", ToolCategory::Mcp));
|
|
|
|
let defs = registry.to_tool_defs_filtered(|t| {
|
|
t.category() == ToolCategory::Info && t.name() != "EnterPlanMode"
|
|
});
|
|
|
|
let names: Vec<&str> = defs.iter().map(|d| d.name.as_str()).collect();
|
|
assert!(names.contains(&"Read"));
|
|
assert!(
|
|
!names.contains(&"mcp_server_tool"),
|
|
"MCP tools should be excluded in plan mode"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Additional: full enter-exit cycle verifies tool set transitions
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn tool_set_transitions_through_plan_mode_cycle() {
|
|
let registry = build_test_registry();
|
|
|
|
// Normal mode: all except ExitPlanMode
|
|
let normal_defs = registry.to_tool_defs_filtered(|t| t.name() != "ExitPlanMode");
|
|
let normal_names: Vec<&str> = normal_defs.iter().map(|d| d.name.as_str()).collect();
|
|
assert!(normal_names.contains(&"Write"));
|
|
assert!(normal_names.contains(&"EnterPlanMode"));
|
|
assert!(!normal_names.contains(&"ExitPlanMode"));
|
|
|
|
// Enter plan mode: only Info (minus EnterPlanMode)
|
|
let plan_defs = registry.to_tool_defs_filtered(|t| {
|
|
t.category() == ToolCategory::Info && t.name() != "EnterPlanMode"
|
|
});
|
|
let plan_names: Vec<&str> = plan_defs.iter().map(|d| d.name.as_str()).collect();
|
|
assert!(!plan_names.contains(&"Write"));
|
|
assert!(!plan_names.contains(&"EnterPlanMode"));
|
|
assert!(plan_names.contains(&"ExitPlanMode"));
|
|
assert!(plan_names.contains(&"Read"));
|
|
|
|
// Exit plan mode: back to normal
|
|
let back_to_normal = registry.to_tool_defs_filtered(|t| t.name() != "ExitPlanMode");
|
|
let back_names: Vec<&str> = back_to_normal.iter().map(|d| d.name.as_str()).collect();
|
|
assert_eq!(
|
|
normal_names, back_names,
|
|
"tool set should be identical after exit"
|
|
);
|
|
}
|