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

276 lines
9.0 KiB
Rust

//! Integration tests for Plan Mode tools (task 3.3).
//!
//! Tests are numbered to match the test-plan.md identifiers (TC-3.3-*).
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use serde_json::json;
use nomi_agent::plan::state::PlanState;
use nomi_agent::plan::tools::{EnterPlanModeTool, ExitPlanModeTool};
use nomi_protocol::events::ToolCategory;
use nomi_tools::Tool;
use nomi_types::skill_types::PlanModeTransition;
// ---------------------------------------------------------------------------
// TC-3.3-01 PlanState initial state
// ---------------------------------------------------------------------------
#[test]
fn tc_3_3_01_plan_state_default_is_inactive() {
let state = PlanState::default();
assert!(!state.is_active);
}
#[test]
fn tc_3_3_01_plan_state_default_allow_list_empty() {
let state = PlanState::default();
assert!(state.pre_plan_allow_list.is_empty());
}
// ---------------------------------------------------------------------------
// TC-3.3-02 EnterPlanMode normal execution
// ---------------------------------------------------------------------------
#[tokio::test]
async fn tc_3_3_02_enter_plan_mode_succeeds_when_not_active() {
let flag = Arc::new(AtomicBool::new(false));
let tool = EnterPlanModeTool::new(flag);
let result = tool.execute(json!({})).await;
assert!(!result.is_error, "should succeed when not in plan mode");
assert!(
result.content.contains("plan mode"),
"confirmation message should mention plan mode"
);
}
// ---------------------------------------------------------------------------
// TC-3.3-03 EnterPlanMode duplicate entry rejected
// ---------------------------------------------------------------------------
#[tokio::test]
async fn tc_3_3_03_enter_plan_mode_rejects_when_already_active() {
let flag = Arc::new(AtomicBool::new(true));
let tool = EnterPlanModeTool::new(flag);
let result = tool.execute(json!({})).await;
assert!(result.is_error, "should fail when already in plan mode");
assert!(
result.content.contains("Already in plan mode"),
"error message should indicate already in plan mode"
);
}
// ---------------------------------------------------------------------------
// TC-3.3-04 ExitPlanMode normal execution
// ---------------------------------------------------------------------------
#[tokio::test]
async fn tc_3_3_04_exit_plan_mode_succeeds_when_active() {
let flag = Arc::new(AtomicBool::new(true));
let tool = ExitPlanModeTool::new(flag);
let result = tool.execute(json!({})).await;
assert!(!result.is_error, "should succeed when in plan mode");
assert!(
result.content.contains("Exited plan mode"),
"confirmation message should mention exiting"
);
}
// ---------------------------------------------------------------------------
// TC-3.3-05 ExitPlanMode when not in plan mode
// ---------------------------------------------------------------------------
#[tokio::test]
async fn tc_3_3_05_exit_plan_mode_rejects_when_not_active() {
let flag = Arc::new(AtomicBool::new(false));
let tool = ExitPlanModeTool::new(flag);
let result = tool.execute(json!({})).await;
assert!(result.is_error, "should fail when not in plan mode");
assert!(
result.content.contains("Not in plan mode"),
"error message should indicate not in plan mode"
);
}
// ---------------------------------------------------------------------------
// TC-3.3-06 EnterPlanMode context_modifier returns Enter
// ---------------------------------------------------------------------------
#[test]
fn tc_3_3_06_enter_context_modifier_returns_enter_transition() {
let flag = Arc::new(AtomicBool::new(false));
let tool = EnterPlanModeTool::new(flag);
let modifier = tool.context_modifier_for(&json!({}));
assert!(modifier.is_some(), "should return a context modifier");
let cm = modifier.unwrap();
assert_eq!(
cm.plan_mode_transition,
Some(PlanModeTransition::Enter),
"transition should be Enter"
);
}
// ---------------------------------------------------------------------------
// TC-3.3-07 ExitPlanMode context_modifier returns Exit
// ---------------------------------------------------------------------------
#[test]
fn tc_3_3_07_exit_context_modifier_returns_exit_transition() {
let flag = Arc::new(AtomicBool::new(true));
let tool = ExitPlanModeTool::new(flag);
let modifier = tool.context_modifier_for(&json!({}));
assert!(modifier.is_some(), "should return a context modifier");
let cm = modifier.unwrap();
assert!(
matches!(
cm.plan_mode_transition,
Some(PlanModeTransition::Exit { .. })
),
"transition should be Exit variant"
);
}
// ---------------------------------------------------------------------------
// TC-3.3-08 EnterPlanMode tool metadata
// ---------------------------------------------------------------------------
#[test]
fn tc_3_3_08_enter_tool_name() {
let flag = Arc::new(AtomicBool::new(false));
let tool = EnterPlanModeTool::new(flag);
assert_eq!(tool.name(), "EnterPlanMode");
}
#[test]
fn tc_3_3_08_enter_tool_category_is_info() {
let flag = Arc::new(AtomicBool::new(false));
let tool = EnterPlanModeTool::new(flag);
assert!(matches!(tool.category(), ToolCategory::Info));
}
#[test]
fn tc_3_3_08_enter_tool_is_concurrency_safe() {
let flag = Arc::new(AtomicBool::new(false));
let tool = EnterPlanModeTool::new(flag);
assert!(tool.is_concurrency_safe(&json!({})));
}
#[test]
fn tc_3_3_08_enter_tool_schema_no_required_params() {
let flag = Arc::new(AtomicBool::new(false));
let tool = EnterPlanModeTool::new(flag);
let schema = tool.input_schema();
let required = schema["required"]
.as_array()
.expect("required should be an array");
assert!(required.is_empty(), "no required parameters expected");
}
// ---------------------------------------------------------------------------
// TC-3.3-09 ExitPlanMode tool metadata
// ---------------------------------------------------------------------------
#[test]
fn tc_3_3_09_exit_tool_name() {
let flag = Arc::new(AtomicBool::new(false));
let tool = ExitPlanModeTool::new(flag);
assert_eq!(tool.name(), "ExitPlanMode");
}
#[test]
fn tc_3_3_09_exit_tool_category_is_info() {
let flag = Arc::new(AtomicBool::new(false));
let tool = ExitPlanModeTool::new(flag);
assert!(matches!(tool.category(), ToolCategory::Info));
}
#[test]
fn tc_3_3_09_exit_tool_is_concurrency_safe() {
let flag = Arc::new(AtomicBool::new(false));
let tool = ExitPlanModeTool::new(flag);
assert!(tool.is_concurrency_safe(&json!({})));
}
#[test]
fn tc_3_3_09_exit_tool_schema_no_required_params() {
let flag = Arc::new(AtomicBool::new(false));
let tool = ExitPlanModeTool::new(flag);
let schema = tool.input_schema();
let required = schema["required"]
.as_array()
.expect("required should be an array");
assert!(required.is_empty(), "no required parameters expected");
}
// ---------------------------------------------------------------------------
// Additional: full enter-exit cycle with shared flag
// ---------------------------------------------------------------------------
#[tokio::test]
async fn enter_exit_cycle_with_shared_flag() {
let flag = Arc::new(AtomicBool::new(false));
let enter = EnterPlanModeTool::new(Arc::clone(&flag));
let exit = ExitPlanModeTool::new(Arc::clone(&flag));
// Phase 1: enter should succeed
let r = enter.execute(json!({})).await;
assert!(!r.is_error);
// Simulate engine applying the transition
flag.store(true, Ordering::Release);
// Phase 2: double-enter should fail
let r = enter.execute(json!({})).await;
assert!(r.is_error);
// Phase 3: exit should succeed
let r = exit.execute(json!({})).await;
assert!(!r.is_error);
// Simulate engine applying the transition
flag.store(false, Ordering::Release);
// Phase 4: double-exit should fail
let r = exit.execute(json!({})).await;
assert!(r.is_error);
// Phase 5: re-enter should succeed
let r = enter.execute(json!({})).await;
assert!(!r.is_error);
}
// ---------------------------------------------------------------------------
// Additional: context_modifier fields are default except plan_mode_transition
// ---------------------------------------------------------------------------
#[test]
fn enter_context_modifier_other_fields_are_default() {
let tool = EnterPlanModeTool::new(Arc::new(AtomicBool::new(false)));
let cm = tool.context_modifier_for(&json!({})).unwrap();
assert!(cm.model.is_none());
assert!(cm.effort.is_none());
assert!(cm.allowed_tools.is_empty());
}
#[test]
fn exit_context_modifier_other_fields_are_default() {
let tool = ExitPlanModeTool::new(Arc::new(AtomicBool::new(false)));
let cm = tool.context_modifier_for(&json!({})).unwrap();
assert!(cm.model.is_none());
assert!(cm.effort.is_none());
assert!(cm.allowed_tools.is_empty());
}