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