use std::path::Path;
use std::process::Command;
use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use holon::{
config::{AppConfig, ControlAuthMode},
host::RuntimeHost,
provider::{AgentProvider, ProviderTurnRequest, ProviderTurnResponse},
system::{WorkspaceAccessMode, WorkspaceProjectionKind},
};
use tempfile::tempdir;
use tokio::time::{sleep, Duration};
fn test_config() -> AppConfig {
let home_dir = tempdir().unwrap().keep();
AppConfig {
default_agent_id: "default".into(),
http_addr: "127.0.0.1:0".into(),
callback_base_url: "http://127.0.0.1:0".into(),
home_dir: home_dir.clone(),
data_dir: home_dir.clone(),
socket_path: home_dir.join("run").join("holon.sock"),
workspace_dir: tempdir().unwrap().keep(),
context_window_messages: 8,
context_window_briefs: 8,
compaction_trigger_messages: 10,
compaction_keep_recent_messages: 4,
prompt_budget_estimated_tokens: 4096,
compaction_trigger_estimated_tokens: 2048,
compaction_keep_recent_estimated_tokens: 768,
recent_episode_candidates: 12,
max_relevant_episodes: 3,
control_token: Some("secret".into()),
control_auth_mode: ControlAuthMode::Auto,
config_file_path: home_dir.join("config.json"),
stored_config: Default::default(),
default_model: holon::config::ModelRef::parse("anthropic/claude-sonnet-4-6").unwrap(),
fallback_models: Vec::new(),
runtime_max_output_tokens: 8192,
default_tool_output_tokens: 8_000,
max_tool_output_tokens: 64_000,
disable_provider_fallback: false,
tui_alternate_screen: holon::config::AltScreenMode::Auto,
validated_model_overrides: std::collections::HashMap::new(),
validated_unknown_model_fallback: None,
providers: holon::config::provider_registry_for_tests(
None,
Some("dummy"),
home_dir.join(".codex"),
),
web_config: holon::web::WebConfig::default(),
}
}
fn git(path: &Path, args: &[&str]) -> Result<String> {
let output = Command::new("git").args(args).current_dir(path).output()?;
if !output.status.success() {
return Err(anyhow::anyhow!(
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&output.stderr)
));
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn init_git_repo(path: &Path) -> Result<String> {
git(path, &["init"])?;
git(path, &["config", "user.email", "holon@example.com"])?;
git(path, &["config", "user.name", "Holon Test"])?;
std::fs::write(path.join("README.md"), "holon\n")?;
git(path, &["add", "README.md"])?;
git(path, &["commit", "-m", "init"])?;
git(path, &["rev-parse", "--abbrev-ref", "HEAD"])
}
struct ParallelWorktreeProvider {
task_id: std::sync::atomic::AtomicUsize,
}
impl ParallelWorktreeProvider {
fn new() -> Self {
Self {
task_id: std::sync::atomic::AtomicUsize::new(0),
}
}
}
#[async_trait]
impl AgentProvider for ParallelWorktreeProvider {
async fn complete_turn(&self, _request: ProviderTurnRequest) -> Result<ProviderTurnResponse> {
let id = self
.task_id
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let delay = 100 + (id * 50); sleep(Duration::from_millis(delay as u64)).await;
Ok(ProviderTurnResponse {
blocks: vec![holon::provider::ModelBlock::Text {
text: format!("Completed worktree task {}", id),
}],
stop_reason: None,
input_tokens: 100,
output_tokens: 50,
cache_usage: None,
request_diagnostics: None,
})
}
}
async fn wait_until_async<F, Fut>(predicate: F) -> Result<()>
where
F: Fn() -> Fut,
Fut: std::future::Future<Output = Result<bool>>,
{
for _ in 0..50 {
if predicate().await? {
return Ok(());
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
Err(anyhow::anyhow!("timed out waiting for condition"))
}
async fn attach_default_workspace(host: &RuntimeHost) -> Result<()> {
let runtime = host.default_runtime().await?;
let workspace = host.ensure_workspace_entry(host.config().workspace_dir.clone())?;
runtime.attach_workspace(&workspace).await?;
runtime
.enter_workspace(
&workspace,
WorkspaceProjectionKind::CanonicalRoot,
WorkspaceAccessMode::SharedRead,
Some(host.config().workspace_dir.clone()),
None,
)
.await?;
Ok(())
}
#[tokio::test]
async fn wt202_summarize_candidate_worktree_results_for_review() -> Result<()> {
let config = test_config();
let workspace = config.workspace_dir.clone();
std::fs::create_dir_all(&workspace)?;
init_git_repo(&workspace)?;
let host = RuntimeHost::new_with_provider(config, Arc::new(ParallelWorktreeProvider::new()))?;
attach_default_workspace(&host).await?;
let runtime = host.default_runtime().await?;
let _task1 = runtime
.schedule_child_agent_task(
"Implement approach A in worktree".into(),
"Implement the first approach".into(),
holon::types::TrustLevel::TrustedOperator,
holon::types::ChildAgentWorkspaceMode::Worktree,
)
.await?;
let _task2 = runtime
.schedule_child_agent_task(
"Implement approach B in worktree".into(),
"Implement the second approach".into(),
holon::types::TrustLevel::TrustedOperator,
holon::types::ChildAgentWorkspaceMode::Worktree,
)
.await?;
let _task3 = runtime
.schedule_child_agent_task(
"Implement approach C in worktree".into(),
"Implement the third approach".into(),
holon::types::TrustLevel::TrustedOperator,
holon::types::ChildAgentWorkspaceMode::Worktree,
)
.await?;
wait_until_async(|| async { Ok(runtime.active_tasks(10).await?.is_empty()) }).await?;
let summary = runtime.summarize_worktree_tasks().await?;
assert!(summary.contains("Worktree Task Summary"));
assert!(summary.contains("Total tasks"));
let tasks = runtime.latest_task_records().await?;
let worktree_tasks: Vec<_> = tasks
.iter()
.filter(|task| task.is_worktree_child_agent_task())
.collect();
for task in &worktree_tasks {
assert!(
summary.contains(&task.id),
"summary should mention task {}",
task.id
);
if let Some(task_summary) = &task.summary {
assert!(
summary.contains(task_summary),
"summary should include task summary: {}",
task_summary
);
}
}
assert!(summary.contains("Status") || summary.contains("status"));
assert!(summary.contains("Worktree path") || summary.contains("worktree_path"));
println!("\n=== Generated Worktree Task Summary ===\n");
println!("{}", summary);
println!("\n=== End Summary ===\n");
Ok(())
}