Plan Execution System: - Add PlanStep, AccumulatedPlan types for multi-turn tool call accumulation - Implement AccumulatedPlanStatus for tracking plan lifecycle - Support selective approval of proposed tool calls before execution External Tools Integration: - Add ExternalToolDefinition and ExternalToolTransport to plugins crate - Extend ToolContext with external_tools registry - Add external_tool_to_llm_tool conversion for LLM compatibility JSON-RPC Communication: - Add jsonrpc crate for JSON-RPC 2.0 protocol support - Enable stdio-based communication with external tool servers UI & Engine Updates: - Add plan_panel.rs component for displaying accumulated plans - Wire plan mode into engine loop - Add plan mode integration tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
201 lines
6.1 KiB
Rust
201 lines
6.1 KiB
Rust
//! Integration tests for Plan Mode multi-turn accumulation
|
|
|
|
use agent_core::{
|
|
AccumulatedPlan, AccumulatedPlanStatus, PlanApproval, PlanStep,
|
|
};
|
|
use serde_json::json;
|
|
|
|
/// Test that PlanStep can be created and has correct initial state
|
|
#[test]
|
|
fn test_plan_step_creation() {
|
|
let step = PlanStep::new(
|
|
"call_123".to_string(),
|
|
1,
|
|
"read".to_string(),
|
|
json!({"path": "/src/main.rs"}),
|
|
);
|
|
|
|
assert_eq!(step.id, "call_123");
|
|
assert_eq!(step.turn, 1);
|
|
assert_eq!(step.tool, "read");
|
|
assert!(step.is_pending());
|
|
assert!(!step.is_approved());
|
|
assert!(!step.is_rejected());
|
|
}
|
|
|
|
/// Test multi-turn accumulation of steps
|
|
#[test]
|
|
fn test_multi_turn_accumulation() {
|
|
let mut plan = AccumulatedPlan::new();
|
|
assert_eq!(plan.status, AccumulatedPlanStatus::Accumulating);
|
|
assert_eq!(plan.current_turn, 0);
|
|
|
|
// Turn 1: Agent proposes reading a file
|
|
plan.next_turn();
|
|
plan.add_step_with_rationale(
|
|
"call_1".to_string(),
|
|
"read".to_string(),
|
|
json!({"path": "Cargo.toml"}),
|
|
"I need to read the project configuration".to_string(),
|
|
);
|
|
|
|
assert_eq!(plan.steps.len(), 1);
|
|
assert_eq!(plan.current_turn, 1);
|
|
|
|
// Turn 2: Agent proposes editing the file
|
|
plan.next_turn();
|
|
plan.add_step_with_rationale(
|
|
"call_2".to_string(),
|
|
"edit".to_string(),
|
|
json!({"path": "Cargo.toml", "old": "version = \"0.1.0\"", "new": "version = \"0.2.0\""}),
|
|
"Bump the version".to_string(),
|
|
);
|
|
|
|
// Turn 2: Agent also proposes running tests
|
|
plan.add_step_with_rationale(
|
|
"call_3".to_string(),
|
|
"bash".to_string(),
|
|
json!({"command": "cargo test"}),
|
|
"Verify the change doesn't break anything".to_string(),
|
|
);
|
|
|
|
assert_eq!(plan.steps.len(), 3);
|
|
assert_eq!(plan.current_turn, 2);
|
|
|
|
// All steps should be pending
|
|
let (pending, approved, rejected) = plan.counts();
|
|
assert_eq!((pending, approved, rejected), (3, 0, 0));
|
|
}
|
|
|
|
/// Test plan finalization and status transition
|
|
#[test]
|
|
fn test_plan_finalization() {
|
|
let mut plan = AccumulatedPlan::new();
|
|
plan.next_turn();
|
|
plan.add_step_for_current_turn("call_1".to_string(), "read".to_string(), json!({}));
|
|
|
|
assert_eq!(plan.status, AccumulatedPlanStatus::Accumulating);
|
|
|
|
plan.finalize();
|
|
assert_eq!(plan.status, AccumulatedPlanStatus::Reviewing);
|
|
}
|
|
|
|
/// Test selective approval workflow
|
|
#[test]
|
|
fn test_selective_approval() {
|
|
let mut plan = AccumulatedPlan::new();
|
|
|
|
// Add three steps
|
|
plan.add_step_for_current_turn("step_read".to_string(), "read".to_string(), json!({}));
|
|
plan.add_step_for_current_turn("step_write".to_string(), "write".to_string(), json!({}));
|
|
plan.add_step_for_current_turn("step_bash".to_string(), "bash".to_string(), json!({}));
|
|
|
|
plan.finalize();
|
|
|
|
// User approves read and write, rejects bash
|
|
let approval = PlanApproval {
|
|
approved_ids: vec!["step_read".to_string(), "step_write".to_string()],
|
|
rejected_ids: vec!["step_bash".to_string()],
|
|
};
|
|
|
|
approval.apply_to(&mut plan);
|
|
|
|
// Verify approval state
|
|
assert!(plan.steps[0].is_approved());
|
|
assert!(plan.steps[1].is_approved());
|
|
assert!(plan.steps[2].is_rejected());
|
|
|
|
assert_eq!(plan.approved_steps().len(), 2);
|
|
assert_eq!(plan.rejected_steps().len(), 1);
|
|
assert!(plan.all_decided());
|
|
}
|
|
|
|
/// Test execution workflow
|
|
#[test]
|
|
fn test_execution_workflow() {
|
|
let mut plan = AccumulatedPlan::new();
|
|
plan.add_step_for_current_turn("s1".to_string(), "read".to_string(), json!({}));
|
|
plan.add_step_for_current_turn("s2".to_string(), "write".to_string(), json!({}));
|
|
|
|
// Finalize
|
|
plan.finalize();
|
|
assert_eq!(plan.status, AccumulatedPlanStatus::Reviewing);
|
|
|
|
// Approve all
|
|
plan.approve_all();
|
|
|
|
// Start execution
|
|
plan.start_execution();
|
|
assert_eq!(plan.status, AccumulatedPlanStatus::Executing);
|
|
|
|
// Complete execution
|
|
plan.complete();
|
|
assert_eq!(plan.status, AccumulatedPlanStatus::Completed);
|
|
}
|
|
|
|
/// Test plan cancellation
|
|
#[test]
|
|
fn test_plan_cancellation() {
|
|
let mut plan = AccumulatedPlan::new();
|
|
plan.add_step_for_current_turn("s1".to_string(), "read".to_string(), json!({}));
|
|
plan.finalize();
|
|
|
|
plan.cancel();
|
|
assert_eq!(plan.status, AccumulatedPlanStatus::Cancelled);
|
|
}
|
|
|
|
/// Test approve_all only affects pending steps
|
|
#[test]
|
|
fn test_approve_all_respects_existing_decisions() {
|
|
let mut plan = AccumulatedPlan::new();
|
|
plan.add_step_for_current_turn("s1".to_string(), "read".to_string(), json!({}));
|
|
plan.add_step_for_current_turn("s2".to_string(), "write".to_string(), json!({}));
|
|
plan.add_step_for_current_turn("s3".to_string(), "bash".to_string(), json!({}));
|
|
|
|
// Reject s3 before approve_all
|
|
plan.reject_step("s3");
|
|
|
|
// Approve all (should only affect pending)
|
|
plan.approve_all();
|
|
|
|
assert!(plan.steps[0].is_approved()); // was pending, now approved
|
|
assert!(plan.steps[1].is_approved()); // was pending, now approved
|
|
assert!(plan.steps[2].is_rejected()); // was rejected, stays rejected
|
|
}
|
|
|
|
/// Test plan with name
|
|
#[test]
|
|
fn test_named_plan() {
|
|
let plan = AccumulatedPlan::with_name("Fix bug #123".to_string());
|
|
assert_eq!(plan.name, Some("Fix bug #123".to_string()));
|
|
}
|
|
|
|
/// Test step rationale tracking
|
|
#[test]
|
|
fn test_step_rationale() {
|
|
let step = PlanStep::new("id".to_string(), 1, "read".to_string(), json!({}))
|
|
.with_rationale("I need to understand the code structure".to_string());
|
|
|
|
assert_eq!(
|
|
step.rationale,
|
|
Some("I need to understand the code structure".to_string())
|
|
);
|
|
}
|
|
|
|
/// Test filtering steps by approval status
|
|
#[test]
|
|
fn test_step_filtering() {
|
|
let mut plan = AccumulatedPlan::new();
|
|
plan.add_step_for_current_turn("a".to_string(), "read".to_string(), json!({}));
|
|
plan.add_step_for_current_turn("b".to_string(), "write".to_string(), json!({}));
|
|
plan.add_step_for_current_turn("c".to_string(), "bash".to_string(), json!({}));
|
|
|
|
plan.approve_step("a");
|
|
plan.reject_step("b");
|
|
// c remains pending
|
|
|
|
assert_eq!(plan.approved_steps().len(), 1);
|
|
assert_eq!(plan.rejected_steps().len(), 1);
|
|
assert_eq!(plan.pending_steps().len(), 1);
|
|
}
|