use super::super::common::TestFixture;
use crate::agents::AgentRole;
use crate::executor::MockProcessExecutor;
use crate::reducer::boundary::MainEffectHandler;
use crate::reducer::event::{AgentEvent, PipelineEvent};
use crate::reducer::state::{AgentChainState, PipelineState};
use crate::workspace::MemoryWorkspace;
use std::sync::Arc;
#[test]
fn test_invoke_planning_agent_uses_unique_logfile_path_with_attempt() {
let workspace =
MemoryWorkspace::new_test().with_file(".agent/tmp/planning_prompt.txt", "planning prompt");
let mut fixture = TestFixture::with_workspace(workspace);
fixture.executor = Arc::new(
MockProcessExecutor::new()
.with_agent_result("claude", Ok(crate::executor::AgentCommandResult::success())),
);
let mut ctx = fixture.ctx();
ctx.developer_agent = "claude";
ctx.reviewer_agent = "codex";
let mut handler = MainEffectHandler::new(PipelineState::initial(1, 1));
handler.state.agent_chain = AgentChainState::initial().with_agents(
vec!["claude".to_string()],
vec![vec!["model-a".to_string()]],
AgentRole::Developer,
);
let result = handler
.invoke_planning_agent(&mut ctx, 0)
.expect("invoke_planning_agent should succeed");
assert!(matches!(
result.event,
PipelineEvent::Agent(AgentEvent::InvocationStarted { .. })
));
assert!(result.additional_events.iter().any(|e| {
matches!(
e,
PipelineEvent::Agent(AgentEvent::InvocationSucceeded { .. })
)
}));
let calls = fixture.executor.agent_calls();
assert_eq!(calls.len(), 1);
assert!(
calls[0].logfile.contains("/agents/planning_1.log"),
"logfile should use per-run format with phase_index naming: {}",
calls[0].logfile
);
}
#[test]
fn test_invoke_agent_prefers_same_agent_retry_prompt_over_rate_limit_continuation_prompt() {
let mut fixture = TestFixture::new();
fixture.executor = Arc::new(
MockProcessExecutor::new()
.with_agent_result("claude", Ok(crate::executor::AgentCommandResult::success())),
);
let mut ctx = fixture.ctx();
ctx.developer_agent = "claude";
ctx.reviewer_agent = "codex";
let mut handler = MainEffectHandler::new(PipelineState::initial(1, 1));
handler.state.agent_chain = AgentChainState::initial().with_agents(
vec!["claude".to_string()],
vec![vec![]],
AgentRole::Developer,
);
let saved_prompt = "CONTINUATION PROMPT (stale)".to_string();
handler.state.agent_chain.rate_limit_continuation_prompt =
Some(crate::reducer::state::RateLimitContinuationPrompt {
drain: crate::agents::AgentDrain::Development,
role: AgentRole::Developer,
prompt: saved_prompt,
});
handler.state.continuation.same_agent_retry_count = 1;
handler.state.continuation.same_agent_retry_reason =
Some(crate::reducer::state::SameAgentRetryReason::Timeout);
let retry_preamble = crate::reducer::boundary::retry_guidance::same_agent_retry_preamble(
&handler.state.continuation,
);
let retry_prompt = format!(
"{retry_preamble}\n\
ORIGINAL PROMPT BODY\n\
RETRY PROMPT MARKER"
);
let _ = handler
.invoke_agent(
&mut ctx,
crate::agents::AgentDrain::Development,
AgentRole::Developer,
"claude",
None,
retry_prompt,
)
.expect("invoke_agent should succeed");
let calls = fixture.executor.agent_calls();
assert_eq!(calls.len(), 1);
assert!(
calls[0].prompt.contains("RETRY PROMPT MARKER"),
"retry prompt marker should be present in effective prompt"
);
assert!(
calls[0].prompt.starts_with("## Retry Note"),
"effective prompt should preserve same-agent retry preamble"
);
assert!(
!calls[0].prompt.contains("CONTINUATION PROMPT (stale)"),
"effective prompt should not be overwritten by stale continuation prompt"
);
}
#[test]
fn test_invoke_agent_prefers_xsd_retry_prompt_over_rate_limit_continuation_prompt() {
let mut fixture = TestFixture::new();
fixture.executor = Arc::new(
MockProcessExecutor::new()
.with_agent_result("claude", Ok(crate::executor::AgentCommandResult::success())),
);
let mut ctx = fixture.ctx();
ctx.developer_agent = "claude";
ctx.reviewer_agent = "codex";
let mut handler = MainEffectHandler::new(PipelineState::initial(1, 1));
handler.state.agent_chain = AgentChainState::initial().with_agents(
vec!["claude".to_string()],
vec![vec![]],
AgentRole::Developer,
);
handler.state.agent_chain.rate_limit_continuation_prompt =
Some(crate::reducer::state::RateLimitContinuationPrompt {
drain: crate::agents::AgentDrain::Development,
role: AgentRole::Developer,
prompt: "CONTINUATION PROMPT (stale)".to_string(),
});
handler.state.continuation.xsd_retry_session_reuse_pending = true;
let xsd_retry_prompt = "XSD RETRY PROMPT MARKER".to_string();
let _ = handler
.invoke_agent(
&mut ctx,
crate::agents::AgentDrain::Development,
AgentRole::Developer,
"claude",
None,
xsd_retry_prompt.clone(),
)
.expect("invoke_agent should succeed");
let calls = fixture.executor.agent_calls();
assert_eq!(calls.len(), 1);
assert_eq!(
calls[0].prompt, xsd_retry_prompt,
"effective prompt should use the XSD retry prompt, not stale continuation prompt"
);
}
#[test]
fn test_invoke_analysis_agent_does_not_use_rate_limit_continuation_prompt() {
let workspace =
MemoryWorkspace::new_test().with_file(".agent/PLAN.md", "# Plan\n\n- Do the thing\n");
let mut fixture = TestFixture::with_workspace(workspace);
fixture.executor = Arc::new(
MockProcessExecutor::new()
.with_agent_result("claude", Ok(crate::executor::AgentCommandResult::success())),
);
let mut ctx = fixture.ctx();
ctx.developer_agent = "claude";
ctx.reviewer_agent = "codex";
let mut handler = MainEffectHandler::new(PipelineState::initial(1, 0));
handler.state.phase = crate::reducer::event::PipelinePhase::Development;
handler.state.iteration = 0;
handler.state.agent_chain = AgentChainState::initial().with_agents(
vec!["claude".to_string()],
vec![vec![]],
AgentRole::Developer,
);
let saved_prompt = "CONTINUATION PROMPT (stale)".to_string();
handler.state.agent_chain.rate_limit_continuation_prompt =
Some(crate::reducer::state::RateLimitContinuationPrompt {
drain: crate::agents::AgentDrain::Development,
role: AgentRole::Developer,
prompt: saved_prompt.clone(),
});
let _ = handler
.invoke_analysis_agent(&mut ctx, 0)
.expect("invoke_analysis_agent should succeed");
let calls = fixture.executor.agent_calls();
assert_eq!(calls.len(), 1);
assert!(
calls[0]
.prompt
.contains("independent, objective code verification agent"),
"analysis invocation should use analysis prompt, not a stale continuation prompt"
);
assert_ne!(
calls[0].prompt, saved_prompt,
"analysis invocation must not be overridden by a role-mismatched continuation prompt"
);
}
#[test]
fn test_xsd_retry_reuses_session_id_even_after_prompt_prepared_clears_pending() {
use crate::reducer::state_reduction::reduce;
let workspace =
MemoryWorkspace::new_test().with_file(".agent/tmp/planning_prompt.txt", "planning prompt");
let mut fixture = TestFixture::with_workspace(workspace);
fixture.executor = Arc::new(
MockProcessExecutor::new()
.with_agent_result("claude", Ok(crate::executor::AgentCommandResult::success())),
);
let mut ctx = fixture.ctx();
ctx.developer_agent = "claude";
ctx.reviewer_agent = "codex";
let session_id = "session-123".to_string();
let mut state = PipelineState::initial(1, 0);
state.agent_chain = AgentChainState::initial()
.with_agents(
vec!["claude".to_string()],
vec![vec![]],
AgentRole::Developer,
)
.with_session_id(Some(session_id.clone()));
state.continuation.xsd_retry_pending = true;
state.continuation.xsd_retry_session_reuse_pending = true;
state = reduce(state, PipelineEvent::planning_prompt_prepared(0));
assert!(
!state.continuation.xsd_retry_pending,
"prompt preparation clears xsd_retry_pending before invocation"
);
let mut handler = MainEffectHandler::new(state);
let _ = handler
.invoke_planning_agent(&mut ctx, 0)
.expect("invoke_planning_agent should succeed");
let calls = fixture.executor.agent_calls();
assert_eq!(calls.len(), 1);
assert!(
calls[0].args.iter().any(|a| a == "--resume"),
"agent command should include session continuation flag for XSD retry"
);
assert!(
calls[0].args.iter().any(|a| a == &session_id),
"agent command should include session id value for XSD retry"
);
}
#[test]
fn test_invoke_planning_agent_logfile_attempt_is_collision_free_and_does_not_depend_on_counter_magnitude(
) {
let workspace =
MemoryWorkspace::new_test().with_file(".agent/tmp/planning_prompt.txt", "planning prompt");
let mut fixture = TestFixture::with_workspace(workspace);
fixture.executor = Arc::new(
MockProcessExecutor::new()
.with_agent_result("claude", Ok(crate::executor::AgentCommandResult::success())),
);
let mut ctx = fixture.ctx();
ctx.developer_agent = "claude";
ctx.reviewer_agent = "codex";
let mut handler = MainEffectHandler::new(PipelineState::initial(1, 1));
handler.state.agent_chain = AgentChainState::initial().with_agents(
vec!["claude".to_string()],
vec![vec!["model-a".to_string()]],
AgentRole::Developer,
);
handler.state.agent_chain.retry_cycle = u32::MAX;
handler.state.continuation.continuation_attempt = 1;
handler.state.continuation.xsd_retry_count = 1;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
handler.invoke_planning_agent(&mut ctx, 0)
}));
assert!(
result.is_ok(),
"invoke_planning_agent should not panic when attempt counter would overflow"
);
let effect_result = result
.unwrap()
.expect("invoke_planning_agent should succeed");
assert!(matches!(
effect_result.event,
PipelineEvent::Agent(AgentEvent::InvocationStarted { .. })
));
assert!(effect_result.additional_events.iter().any(|e| {
matches!(
e,
PipelineEvent::Agent(AgentEvent::InvocationSucceeded { .. })
)
}));
let calls = fixture.executor.agent_calls();
assert_eq!(calls.len(), 1);
assert!(
calls[0].logfile.contains("/agents/planning_1.log"),
"logfile should use per-run format with phase_index naming: {}",
calls[0].logfile
);
}
#[test]
fn test_invoke_planning_agent_logfile_attempt_does_not_collide_across_distinct_attempt_context() {
let workspace =
MemoryWorkspace::new_test().with_file(".agent/tmp/planning_prompt.txt", "planning prompt");
let mut fixture = TestFixture::with_workspace(workspace);
fixture.executor = Arc::new(
MockProcessExecutor::new()
.with_agent_result("claude", Ok(crate::executor::AgentCommandResult::success())),
);
let mut ctx = fixture.ctx();
ctx.developer_agent = "claude";
ctx.reviewer_agent = "codex";
let mut handler = MainEffectHandler::new(PipelineState::initial(1, 1));
handler.state.agent_chain = AgentChainState::initial().with_agents(
vec!["claude".to_string()],
vec![vec!["model-a".to_string()]],
AgentRole::Developer,
);
handler.state.agent_chain.retry_cycle = 0;
handler.state.continuation.continuation_attempt = 100;
handler.state.continuation.xsd_retry_count = 0;
let _ = handler
.invoke_planning_agent(&mut ctx, 0)
.expect("first invoke_planning_agent should succeed");
handler.state.agent_chain.retry_cycle = 1;
handler.state.continuation.continuation_attempt = 0;
handler.state.continuation.xsd_retry_count = 0;
let _ = handler
.invoke_planning_agent(&mut ctx, 0)
.expect("second invoke_planning_agent should succeed");
let calls = fixture.executor.agent_calls();
assert_eq!(calls.len(), 2);
assert_ne!(
calls[0].logfile, calls[1].logfile,
"logfile path must not collide across distinct attempt contexts"
);
}