#![allow(dead_code, unused_imports)]
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use chrono::Utc;
use holon::{
config::{AppConfig, ControlAuthMode},
host::RuntimeHost,
ingress::{WakeDisposition, WakeHint},
policy::validate_message_kind_for_origin,
provider::{
AgentProvider, ConversationMessage, ModelBlock, ProviderTurnRequest, ProviderTurnResponse,
StubProvider,
},
system::{WorkspaceAccessMode, WorkspaceProjectionKind},
tool::{ToolCall, ToolError, ToolRegistry, ToolResult},
types::{
AgentKind, AgentProfilePreset, AgentStatus, BriefKind, CallbackDeliveryMode,
ChildAgentPhase, ClosureOutcome, CommandTaskSpec, ControlAction, ExternalTriggerStatus,
FailureArtifactCategory, MessageBody, MessageEnvelope, MessageKind, MessageOrigin,
OperatorNotificationBoundary, OperatorTransportBinding, OperatorTransportBindingStatus,
OperatorTransportCapabilities, OperatorTransportDeliveryAuth,
OperatorTransportDeliveryAuthKind, Priority, TaskStatus, TodoItem, TodoItemState,
TokenUsage, TranscriptEntry, TranscriptEntryKind, TrustLevel, WaitingIntentStatus,
WaitingReason, WorkItemState,
},
};
use serde_json::json;
use tokio::sync::Mutex;
use tokio::time::{sleep, Duration};
use crate::support::runtime_helpers::{
aggressive_compaction_config, git, init_git_repo, operator_transport_binding,
parse_tool_result_payload, parse_tool_result_value, test_config, wait_for_worktree_presence,
wait_until, wait_until_async, wait_until_async_for,
};
use crate::support::runtime_providers::{
DelayedTextProvider, DelegatedBoundaryProvider, FileEditingProvider, LongShellProvider,
NotifyThenAgentGetProvider, RecordingPromptProvider, RuntimeFailureProvider, ShellProvider,
SleepOnlyCompletionAfterTextProvider, TerminalResultBriefProvider, ToolErrorProvider,
ToolUsingProvider, TruncatedShellReinjectionProvider, UseWorkspaceProvider,
VerboseRuntimeFailureProvider, WakeHintProvider, WorktreeCapturingProvider,
WorktreeLifecycleProvider,
};
use crate::support::{attach_default_workspace, TestConfigBuilder};
pub async fn task_output_returns_worktree_subagent_result_text() -> Result<()> {
let config = test_config();
let workspace = config.workspace_dir.clone();
std::fs::create_dir_all(&workspace)?;
init_git_repo(&workspace)?;
let provider = Arc::new(WorktreeCapturingProvider::new("worktree subagent result"));
let host = RuntimeHost::new_with_provider(config, provider)?;
attach_default_workspace(&host).await?;
let runtime = host.default_runtime().await?;
let registry = ToolRegistry::new(runtime.workspace_root());
let task = runtime
.schedule_child_agent_task(
"delegate worktree task".into(),
"return a worktree result".into(),
TrustLevel::TrustedOperator,
holon::types::ChildAgentWorkspaceMode::Worktree,
)
.await?;
let (result, _) = registry
.execute(
&runtime,
"default",
&TrustLevel::TrustedOperator,
&ToolCall {
id: "tool-task-output-worktree".into(),
name: "TaskOutput".into(),
input: json!({ "task_id": task.id, "timeout_ms": 10_000 }),
},
)
.await?;
let value: serde_json::Value = parse_tool_result_payload(&result)?;
assert_eq!(value["retrieval_status"], "success");
assert_eq!(value["task"]["kind"], "child_agent_task");
assert!(value["task"]["output_preview"]
.as_str()
.expect("worktree task output should be text")
.contains("worktree subagent result"));
Ok(())
}
pub async fn enter_worktree_tool_switches_workspace_and_restores_on_reload() -> Result<()> {
let config = test_config();
let workspace = config.workspace_dir.clone();
std::fs::create_dir_all(&workspace)?;
let original_branch = init_git_repo(&workspace)?;
let branch_name = "feature-enter-worktree";
let host = RuntimeHost::new_with_provider(
config.clone(),
Arc::new(UseWorkspaceProvider::new(workspace.clone(), branch_name)),
)?;
attach_default_workspace(&host).await?;
let runtime = host.default_runtime().await?;
runtime
.enqueue(MessageEnvelope::new(
"default",
MessageKind::OperatorPrompt,
MessageOrigin::Operator { actor_id: None },
TrustLevel::TrustedOperator,
Priority::Normal,
MessageBody::Text {
text: "enter a managed worktree".into(),
},
))
.await?;
for _ in 0..30 {
if runtime.agent_state().await?.worktree_session.is_some() {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}
let session = runtime.agent_state().await?;
let worktree = session
.worktree_session
.clone()
.expect("missing worktree state");
assert_eq!(worktree.original_cwd, workspace);
assert_eq!(worktree.original_branch, original_branch);
assert_eq!(worktree.worktree_branch, branch_name);
assert!(worktree.worktree_path.exists());
assert_eq!(runtime.workspace_root(), worktree.worktree_path);
assert_eq!(
git(
&runtime.workspace_root(),
&["rev-parse", "--abbrev-ref", "HEAD"]
)?,
branch_name
);
let events = runtime.recent_events(20).await?;
assert!(events.iter().any(|event| event.kind == "workspace_entered"));
let restarted_host =
RuntimeHost::new_with_provider(config, Arc::new(StubProvider::new("unused")))?;
let restarted_runtime = restarted_host.default_runtime().await?;
assert_eq!(restarted_runtime.workspace_root(), worktree.worktree_path);
Ok(())
}
pub async fn use_workspace_path_adopts_attached_parent_for_existing_git_worktree() -> Result<()> {
let config = test_config();
let workspace = config.workspace_dir.clone();
std::fs::create_dir_all(&workspace)?;
init_git_repo(&workspace)?;
let external_worktree = workspace
.parent()
.expect("workspace should have parent")
.join(format!("manual-worktree-{}", uuid::Uuid::new_v4().simple()));
git(
&workspace,
&[
"worktree",
"add",
external_worktree.to_str().expect("utf8 worktree path"),
"-b",
"manual-worktree",
],
)?;
let host = RuntimeHost::new_with_provider(config, Arc::new(StubProvider::new("unused")))?;
attach_default_workspace(&host).await?;
let runtime = host.default_runtime().await?;
let registry = ToolRegistry::new(runtime.workspace_root());
let original_workspace_id = runtime
.agent_state()
.await?
.active_workspace_entry
.as_ref()
.map(|entry| entry.workspace_id.clone())
.expect("default workspace should be active");
let (result, _) = registry
.execute(
&runtime,
"default",
&TrustLevel::TrustedOperator,
&ToolCall {
id: "tool-use-existing-worktree".into(),
name: "UseWorkspace".into(),
input: json!({ "path": external_worktree }),
},
)
.await?;
let value = parse_tool_result_payload(&result)?;
assert_eq!(value["workspace_id"], original_workspace_id);
assert_eq!(
PathBuf::from(value["workspace_anchor"].as_str().unwrap()),
workspace
);
assert_eq!(
PathBuf::from(value["execution_root"].as_str().unwrap()),
external_worktree
);
assert_eq!(value["projection_kind"], "git_worktree_root");
let summary = result.summary_text().unwrap_or_default();
assert!(summary.contains("detected an existing git worktree"));
assert!(summary.contains("\"mode\":\"isolated\""));
let state = runtime.agent_state().await?;
let active = state
.active_workspace_entry
.expect("external worktree should be active");
assert_eq!(active.workspace_id, original_workspace_id);
assert_eq!(active.workspace_anchor, workspace);
assert_eq!(active.execution_root, external_worktree);
assert_eq!(
active.projection_kind,
WorkspaceProjectionKind::GitWorktreeRoot
);
assert!(state.worktree_session.is_none());
assert_eq!(
active
.projection_metadata
.as_ref()
.and_then(|metadata| metadata["ownership"].as_str()),
Some("external")
);
Ok(())
}
pub async fn enter_workspace_conflict_preserves_existing_occupancy() -> Result<()> {
let config = test_config();
let workspace = config.workspace_dir.clone();
std::fs::create_dir_all(&workspace)?;
let host = RuntimeHost::new_with_provider(config, Arc::new(StubProvider::new("unused")))?;
let workspace_entry = host.ensure_workspace_entry(workspace.clone())?;
let default_runtime = host.default_runtime().await?;
default_runtime.attach_workspace(&workspace_entry).await?;
default_runtime
.enter_workspace(
&workspace_entry,
WorkspaceProjectionKind::CanonicalRoot,
WorkspaceAccessMode::SharedRead,
Some(workspace.clone()),
None,
)
.await?;
host.create_named_agent("alpha", None).await?;
let alpha = host.get_or_create_agent("alpha").await?;
alpha.attach_workspace(&workspace_entry).await?;
alpha
.enter_workspace(
&workspace_entry,
WorkspaceProjectionKind::CanonicalRoot,
WorkspaceAccessMode::ExclusiveWrite,
Some(workspace.clone()),
None,
)
.await?;
let error = default_runtime
.enter_workspace(
&workspace_entry,
WorkspaceProjectionKind::CanonicalRoot,
WorkspaceAccessMode::ExclusiveWrite,
Some(workspace.clone()),
None,
)
.await
.expect_err("exclusive_write conflict should fail");
assert!(error
.to_string()
.contains("already has an exclusive_write holder"));
let state = default_runtime.agent_state().await?;
let active_entry = state
.active_workspace_entry
.expect("shared_read entry should still be active");
assert_eq!(active_entry.access_mode, WorkspaceAccessMode::SharedRead);
let summary = default_runtime.agent_summary().await?;
let occupancy = summary
.active_workspace_occupancy
.expect("shared_read occupancy should still be held");
assert_eq!(occupancy.access_mode, WorkspaceAccessMode::SharedRead);
Ok(())
}
pub async fn detach_workspace_persists_empty_binding_across_restart() -> Result<()> {
let config = test_config();
let workspace = config.workspace_dir.clone();
let agent_home_id = holon::types::agent_home_workspace_id(config.default_agent_id.as_str());
std::fs::create_dir_all(&workspace)?;
init_git_repo(&workspace)?;
let host =
RuntimeHost::new_with_provider(config.clone(), Arc::new(StubProvider::new("unused")))?;
attach_default_workspace(&host).await?;
let runtime = host.default_runtime().await?;
let workspace_id = runtime
.agent_state()
.await?
.active_workspace_entry
.as_ref()
.map(|e| e.workspace_id.clone())
.expect("default workspace should be active");
runtime.exit_workspace().await?;
runtime.detach_workspace(&workspace_id).await?;
let state = runtime.agent_state().await?;
assert_eq!(
state
.active_workspace_entry
.as_ref()
.map(|e| e.workspace_id.as_str()),
Some(agent_home_id.as_str())
);
assert!(state.active_workspace_entry.is_some());
assert_eq!(state.attached_workspaces, vec![agent_home_id.clone()]);
let restarted_host =
RuntimeHost::new_with_provider(config, Arc::new(StubProvider::new("unused")))?;
let restarted_runtime = restarted_host.default_runtime().await?;
let restarted_state = restarted_runtime.agent_state().await?;
assert_eq!(
restarted_state
.active_workspace_entry
.as_ref()
.map(|e| e.workspace_id.clone())
.as_deref(),
Some(agent_home_id.as_str())
);
assert!(restarted_state.active_workspace_entry.is_some());
Ok(())
}
pub async fn enter_worktree_projection_honors_requested_cwd() -> 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(StubProvider::new("unused")))?;
attach_default_workspace(&host).await?;
let runtime = host.default_runtime().await?;
let workspace_entry = host.ensure_workspace_entry(workspace.clone())?;
runtime
.enter_workspace(
&workspace_entry,
WorkspaceProjectionKind::GitWorktreeRoot,
WorkspaceAccessMode::ExclusiveWrite,
Some(Path::new("nested/src").to_path_buf()),
Some("feature-worktree-cwd".into()),
)
.await?;
let state = runtime.agent_state().await?;
let worktree = state
.worktree_session
.clone()
.expect("missing worktree session");
let active_entry = state
.active_workspace_entry
.expect("missing active workspace entry");
assert_eq!(active_entry.cwd, worktree.worktree_path.join("nested/src"));
runtime.exit_workspace().await?;
assert!(worktree.worktree_path.exists());
Ok(())
}
pub async fn exit_worktree_keep_restores_workspace_and_persists_state() -> Result<()> {
let config = test_config();
let workspace = config.workspace_dir.clone();
let agent_home_id = holon::types::agent_home_workspace_id(config.default_agent_id.as_str());
std::fs::create_dir_all(&workspace)?;
init_git_repo(&workspace)?;
let branch_name = "feature-exit-keep";
let host = RuntimeHost::new_with_provider(
config.clone(),
Arc::new(WorktreeLifecycleProvider::new(
workspace.clone(),
branch_name,
format!("\"workspace_id\":\"{agent_home_id}\""),
)),
)?;
attach_default_workspace(&host).await?;
let runtime = host.default_runtime().await?;
runtime
.enqueue(MessageEnvelope::new(
"default",
MessageKind::OperatorPrompt,
MessageOrigin::Operator { actor_id: None },
TrustLevel::TrustedOperator,
Priority::Normal,
MessageBody::Text {
text: "enter a managed worktree".into(),
},
))
.await?;
wait_for_worktree_presence(&runtime, true).await?;
let worktree = runtime
.agent_state()
.await?
.worktree_session
.clone()
.expect("missing worktree state");
runtime
.enqueue(MessageEnvelope::new(
"default",
MessageKind::OperatorPrompt,
MessageOrigin::Operator { actor_id: None },
TrustLevel::TrustedOperator,
Priority::Normal,
MessageBody::Text {
text: "exit the worktree but keep it".into(),
},
))
.await?;
wait_for_worktree_presence(&runtime, false).await?;
assert_eq!(
runtime
.agent_state()
.await?
.active_workspace_entry
.as_ref()
.map(|e| e.workspace_id.clone())
.as_deref(),
Some(agent_home_id.as_str())
);
assert!(worktree.worktree_path.exists());
let restarted_host =
RuntimeHost::new_with_provider(config, Arc::new(StubProvider::new("unused")))?;
let restarted_runtime = restarted_host.default_runtime().await?;
let restarted_state = restarted_runtime.agent_state().await?;
assert_eq!(
restarted_state
.active_workspace_entry
.as_ref()
.map(|e| e.workspace_id.clone())
.as_deref(),
Some(agent_home_id.as_str())
);
assert!(restarted_state.worktree_session.is_none());
Ok(())
}
pub async fn exit_worktree_does_not_remove_clean_worktree() -> Result<()> {
let config = test_config();
let workspace = config.workspace_dir.clone();
let agent_home_id = holon::types::agent_home_workspace_id(config.default_agent_id.as_str());
std::fs::create_dir_all(&workspace)?;
init_git_repo(&workspace)?;
let branch_name = "feature-exit-remove";
let host = RuntimeHost::new_with_provider(
config.clone(),
Arc::new(WorktreeLifecycleProvider::new(
workspace.clone(),
branch_name,
format!("\"workspace_id\":\"{agent_home_id}\""),
)),
)?;
attach_default_workspace(&host).await?;
let runtime = host.default_runtime().await?;
runtime
.enqueue(MessageEnvelope::new(
"default",
MessageKind::OperatorPrompt,
MessageOrigin::Operator { actor_id: None },
TrustLevel::TrustedOperator,
Priority::Normal,
MessageBody::Text {
text: "enter a managed worktree".into(),
},
))
.await?;
wait_for_worktree_presence(&runtime, true).await?;
let worktree = runtime
.agent_state()
.await?
.worktree_session
.clone()
.expect("missing worktree state");
runtime
.enqueue(MessageEnvelope::new(
"default",
MessageKind::OperatorPrompt,
MessageOrigin::Operator { actor_id: None },
TrustLevel::TrustedOperator,
Priority::Normal,
MessageBody::Text {
text: "exit the worktree".into(),
},
))
.await?;
wait_for_worktree_presence(&runtime, false).await?;
assert_eq!(
runtime
.agent_state()
.await?
.active_workspace_entry
.as_ref()
.map(|e| e.workspace_id.clone())
.as_deref(),
Some(agent_home_id.as_str())
);
assert!(worktree.worktree_path.exists());
let restarted_host =
RuntimeHost::new_with_provider(config, Arc::new(StubProvider::new("unused")))?;
let restarted_runtime = restarted_host.default_runtime().await?;
let restarted_state = restarted_runtime.agent_state().await?;
assert_eq!(
restarted_state
.active_workspace_entry
.as_ref()
.map(|e| e.workspace_id.clone())
.as_deref(),
Some(agent_home_id.as_str())
);
assert!(restarted_state.worktree_session.is_none());
Ok(())
}
pub async fn exit_worktree_does_not_remove_dirty_worktree() -> Result<()> {
let config = test_config();
let workspace = config.workspace_dir.clone();
let agent_home_id = holon::types::agent_home_workspace_id(config.default_agent_id.as_str());
std::fs::create_dir_all(&workspace)?;
init_git_repo(&workspace)?;
let branch_name = "feature-exit-refuse";
let host = RuntimeHost::new_with_provider(
config.clone(),
Arc::new(WorktreeLifecycleProvider::new(
workspace.clone(),
branch_name,
format!("\"workspace_id\":\"{agent_home_id}\""),
)),
)?;
attach_default_workspace(&host).await?;
let runtime = host.default_runtime().await?;
runtime
.enqueue(MessageEnvelope::new(
"default",
MessageKind::OperatorPrompt,
MessageOrigin::Operator { actor_id: None },
TrustLevel::TrustedOperator,
Priority::Normal,
MessageBody::Text {
text: "enter a managed worktree".into(),
},
))
.await?;
wait_for_worktree_presence(&runtime, true).await?;
let worktree = runtime
.agent_state()
.await?
.worktree_session
.clone()
.expect("missing worktree state");
std::fs::write(worktree.worktree_path.join("README.md"), "changed\n")?;
runtime
.enqueue(MessageEnvelope::new(
"default",
MessageKind::OperatorPrompt,
MessageOrigin::Operator { actor_id: None },
TrustLevel::TrustedOperator,
Priority::Normal,
MessageBody::Text {
text: "exit the dirty worktree".into(),
},
))
.await?;
wait_for_worktree_presence(&runtime, false).await?;
let session = runtime.agent_state().await?;
assert!(session.worktree_session.is_none());
assert_eq!(
session
.active_workspace_entry
.as_ref()
.map(|e| e.workspace_id.clone())
.as_deref(),
Some(agent_home_id.as_str())
);
assert!(session.active_workspace_entry.is_some());
assert!(worktree.worktree_path.exists());
let events = runtime.recent_events(20).await?;
assert!(events.iter().any(|event| event.kind == "workspace_used"));
let restarted_host =
RuntimeHost::new_with_provider(config, Arc::new(StubProvider::new("unused")))?;
let restarted_runtime = restarted_host.default_runtime().await?;
let restarted_state = restarted_runtime.agent_state().await?;
assert_eq!(
restarted_state
.active_workspace_entry
.as_ref()
.map(|e| e.workspace_id.clone())
.as_deref(),
Some(agent_home_id.as_str())
);
assert!(restarted_state.worktree_session.is_none());
Ok(())
}
pub async fn worktree_subagent_task_creates_dedicated_per_task_worktree() -> Result<()> {
let config = test_config();
let workspace = config.workspace_dir.clone();
std::fs::create_dir_all(&workspace)?;
init_git_repo(&workspace)?;
let provider = Arc::new(WorktreeCapturingProvider::new("worktree subagent result"));
let host = RuntimeHost::new_with_provider(config.clone(), provider.clone())?;
attach_default_workspace(&host).await?;
let runtime = host.default_runtime().await?;
let task = runtime
.schedule_child_agent_task(
"delegate work in worktree".into(),
"return a worktree-isolated result".into(),
TrustLevel::TrustedOperator,
holon::types::ChildAgentWorkspaceMode::Worktree,
)
.await?;
wait_until(|| {
let briefs = runtime.storage().read_recent_briefs(20)?;
Ok(briefs
.iter()
.any(|brief| brief.text.contains("worktree subagent result")))
})
.await?;
let expected_worktree = workspace
.parent()
.unwrap_or(workspace.as_path())
.join(format!(
".holon-worktrees-{}/task-{}",
workspace
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("repo"),
task.id
));
let events = runtime.recent_events(50).await?;
assert!(
events
.iter()
.any(|event| event.kind == "worktree_created_for_task"
&& event.data["task_id"] == task.id
&& event.data["worktree_path"].as_str()
== Some(expected_worktree.to_str().unwrap())),
"worktree should have been created during task execution"
);
assert_eq!(runtime.workspace_root(), workspace);
let transcript = runtime.storage().read_all_transcript()?;
assert!(transcript.iter().any(|entry| {
entry.kind == holon::types::TranscriptEntryKind::SubagentPrompt
&& entry.data["task_id"] == task.id
&& entry.data["workspace_root"]
.as_str()
.map(|path| path.contains(&task.id))
.unwrap_or(false)
}));
let prompts = provider.prompts().await;
let expected_worktree_text = expected_worktree.to_string_lossy().to_string();
assert!(
prompts
.iter()
.any(|prompt| prompt.contains(expected_worktree_text.as_str())),
"subagent prompt should be rooted in the dedicated worktree"
);
assert!(
!expected_worktree.exists(),
"worktree should be auto-removed when no changes were made (WT-104)"
);
assert!(
events
.iter()
.any(|event| event.kind == "worktree_auto_cleaned_up"
&& event.data["task_id"] == task.id
&& event.data["reason"].as_str() == Some("terminal_task_result")),
"worktree should have been auto-cleaned up after task completion (WT-104)"
);
Ok(())
}
pub async fn subagent_task_returns_result_to_parent_session() -> Result<()> {
let host = RuntimeHost::new_with_provider(
test_config(),
Arc::new(StubProvider::new("subagent result payload")),
)?;
let runtime = host.default_runtime().await?;
let task = runtime
.schedule_child_agent_task(
"delegate work".into(),
"return a concise subagent result".into(),
TrustLevel::TrustedOperator,
holon::types::ChildAgentWorkspaceMode::Inherit,
)
.await?;
wait_until(|| {
let briefs = runtime.storage().read_recent_briefs(20)?;
Ok(briefs
.iter()
.any(|brief| brief.text.contains("subagent result payload")))
})
.await?;
let tasks = runtime.recent_tasks(10).await?;
assert!(tasks
.iter()
.any(|record| record.id == task.id && record.kind.as_str() == "child_agent_task"));
let briefs = runtime.recent_briefs(10).await?;
assert!(briefs
.iter()
.any(|brief| brief.text.contains("subagent result payload")));
Ok(())
}
pub async fn worktree_child_agent_task_records_workspace_mode() -> 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(StubProvider::new("worktree subagent result")),
)?;
attach_default_workspace(&host).await?;
let runtime = host.default_runtime().await?;
let task = runtime
.schedule_child_agent_task(
"delegate work in worktree".into(),
"return a worktree-isolated result".into(),
TrustLevel::TrustedOperator,
holon::types::ChildAgentWorkspaceMode::Worktree,
)
.await?;
wait_until(|| {
let briefs = runtime.storage().read_recent_briefs(20)?;
Ok(briefs
.iter()
.any(|brief| brief.text.contains("worktree subagent result")))
})
.await?;
let tasks = runtime.recent_tasks(10).await?;
let record = tasks
.iter()
.find(|record| record.id == task.id)
.expect("worktree child task record");
assert_eq!(record.kind.as_str(), "child_agent_task");
assert_eq!(
record
.detail
.as_ref()
.and_then(|detail| detail.get("workspace_mode"))
.and_then(|value| value.as_str()),
Some("worktree")
);
let messages = runtime.storage().read_recent_messages(20)?;
let running_status = messages
.iter()
.find(|message| {
matches!(message.kind, MessageKind::TaskStatus)
&& message
.metadata
.as_ref()
.and_then(|metadata| metadata.get("task_id"))
.and_then(|value| value.as_str())
== Some(task.id.as_str())
})
.expect("worktree child task should emit a running task status message");
let running_detail = running_status
.metadata
.as_ref()
.and_then(|metadata| metadata.get("task_detail"))
.expect("running task status should carry task detail");
assert_eq!(
running_detail
.get("workspace_mode")
.and_then(|value| value.as_str()),
Some("worktree")
);
assert!(
running_detail.get("worktree").is_some(),
"running task status should preserve task-owned worktree metadata"
);
let briefs = runtime.recent_briefs(10).await?;
assert!(briefs
.iter()
.any(|brief| brief.text.contains("worktree subagent result")));
Ok(())
}
pub async fn worktree_subagent_task_returns_metadata_to_parent_session() -> 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(StubProvider::new("worktree metadata result")),
)?;
attach_default_workspace(&host).await?;
let runtime = host.default_runtime().await?;
let task = runtime
.schedule_child_agent_task(
"delegate work in worktree".into(),
"return worktree metadata".into(),
TrustLevel::TrustedOperator,
holon::types::ChildAgentWorkspaceMode::Worktree,
)
.await?;
wait_until(|| {
let briefs = runtime.storage().read_recent_briefs(20)?;
Ok(briefs.iter().any(|brief| {
brief.text.contains("Worktree path:")
&& brief.text.contains("Worktree branch:")
&& brief.text.contains("Changed files:")
}))
})
.await?;
let messages = runtime.storage().read_recent_messages(20)?;
let task_result = messages
.iter()
.find(|message| matches!(message.kind, MessageKind::TaskResult))
.expect("missing task result message");
let worktree = task_result
.metadata
.as_ref()
.and_then(|metadata| metadata.get("worktree"))
.expect("missing worktree metadata");
let expected_branch = format!("task-{}", task.id);
assert_eq!(
worktree["worktree_branch"].as_str(),
Some(expected_branch.as_str())
);
assert!(worktree["worktree_path"]
.as_str()
.map(|path| path.contains(&task.id))
.unwrap_or(false));
assert_eq!(
worktree["changed_files"]
.as_array()
.expect("changed_files should be an array")
.len(),
0
);
let briefs = runtime.recent_briefs(10).await?;
assert!(briefs.iter().any(|brief| {
brief.text.contains("Task")
&& brief.text.contains("Worktree path:")
&& brief.text.contains("Worktree branch:")
&& brief.text.contains("Changed files: none")
}));
Ok(())
}
pub fn policy_blocks_mismatched_origin() {
let mismatch = validate_message_kind_for_origin(
&MessageKind::WebhookEvent,
&MessageOrigin::Operator { actor_id: None },
);
assert!(!mismatch.allowed);
}
pub async fn worktree_subagent_task_auto_removes_worktree_when_no_changes_wt104() -> 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(StubProvider::new("no changes made")))?;
attach_default_workspace(&host).await?;
let runtime = host.default_runtime().await?;
let task = runtime
.schedule_child_agent_task(
"do nothing in worktree".into(),
"just return a result without making changes".into(),
TrustLevel::TrustedOperator,
holon::types::ChildAgentWorkspaceMode::Worktree,
)
.await?;
wait_until(|| {
let tasks = runtime.storage().latest_task_records()?;
Ok(tasks.iter().any(|record| {
record.id == task.id && record.status == holon::types::TaskStatus::Completed
}))
})
.await?;
let expected_worktree = workspace
.parent()
.unwrap_or(workspace.as_path())
.join(format!(
".holon-worktrees-{}/task-{}",
workspace
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("repo"),
task.id
));
assert!(
!expected_worktree.exists(),
"worktree should be auto-removed when no changes were made"
);
let events = runtime.recent_events(50).await?;
assert!(
events
.iter()
.any(|event| event.kind == "worktree_auto_cleaned_up"),
"should have logged worktree_auto_cleaned_up event"
);
let cleanup_event = events
.iter()
.find(|event| event.kind == "worktree_auto_cleaned_up")
.expect("cleanup event should exist");
assert_eq!(cleanup_event.data["task_id"], task.id);
assert_eq!(
cleanup_event.data["reason"].as_str(),
Some("terminal_task_result")
);
let messages = runtime.storage().read_recent_messages(20)?;
let task_result = messages
.iter()
.find(|message| {
matches!(message.kind, MessageKind::TaskResult)
&& message
.metadata
.as_ref()
.and_then(|m| m.get("task_id"))
.and_then(|id| id.as_str())
== Some(&task.id[..])
})
.expect("missing task result message");
let worktree_metadata = task_result
.metadata
.as_ref()
.and_then(|m| m.get("worktree"))
.expect("should still have worktree metadata even after cleanup");
assert_eq!(
worktree_metadata["changed_files"]
.as_array()
.expect("changed_files should be an array")
.len(),
0
);
assert_eq!(worktree_metadata["auto_cleaned_up"].as_bool(), Some(true));
let task_result_text = match &task_result.body {
MessageBody::Text { text } => text,
other => panic!("expected text task result, got {other:?}"),
};
assert!(
task_result_text.contains("Worktree cleanup: auto-removed clean task-owned artifact."),
"task result should report cleanup state: {task_result_text}"
);
Ok(())
}
pub async fn worktree_subagent_task_retains_worktree_when_changes_detected_wt105() -> 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(DelayedTextProvider))?;
attach_default_workspace(&host).await?;
let runtime = host.default_runtime().await?;
let task = runtime
.schedule_child_agent_task(
"make changes in worktree".into(),
"modify files in the worktree".into(),
TrustLevel::TrustedOperator,
holon::types::ChildAgentWorkspaceMode::Worktree,
)
.await?;
let expected_worktree = workspace
.parent()
.unwrap_or(workspace.as_path())
.join(format!(
".holon-worktrees-{}/task-{}",
workspace
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("repo"),
task.id
));
wait_until(|| {
let events = runtime.storage().read_recent_events(50)?;
Ok(events.iter().any(|event| {
event.kind == "worktree_created_for_task"
&& event.data["task_id"] == task.id
&& event.data["worktree_path"].as_str() == Some(expected_worktree.to_str().unwrap())
}))
})
.await?;
std::fs::write(
expected_worktree.join("changed_file.txt"),
"This file was changed in the worktree",
)?;
wait_until(|| {
let tasks = runtime.storage().latest_task_records()?;
Ok(tasks.iter().any(|record| {
record.id == task.id && record.status == holon::types::TaskStatus::Completed
}))
})
.await?;
assert!(
expected_worktree.exists(),
"worktree should be retained when changes were made (WT-105)"
);
let events = runtime.recent_events(50).await?;
let retained_events: Vec<_> = events
.iter()
.filter(|event| event.kind == "worktree_retained_for_review")
.collect();
assert!(
!retained_events.is_empty(),
"should have logged worktree_retained_for_review event when changes were detected"
);
let retained_event = retained_events
.iter()
.find(|event| event.data["task_id"] == task.id)
.expect("retained event should exist for this task");
assert_eq!(retained_event.data["task_id"], task.id);
assert_eq!(
retained_event.data["reason"].as_str(),
Some("changes detected in worktree")
);
let changed_files = retained_event.data["changed_files"]
.as_array()
.expect("changed_files should be an array in retained event");
assert!(
!changed_files.is_empty(),
"retained event should list changed files"
);
let messages = runtime.storage().read_recent_messages(20)?;
let task_result = messages
.iter()
.find(|message| {
matches!(message.kind, MessageKind::TaskResult)
&& message
.metadata
.as_ref()
.and_then(|m| m.get("task_id"))
.and_then(|id| id.as_str())
== Some(&task.id[..])
})
.expect("missing task result message");
let worktree_metadata = task_result
.metadata
.as_ref()
.and_then(|m| m.get("worktree"))
.expect("should have worktree metadata");
assert_eq!(
worktree_metadata["retained_for_review"].as_bool(),
Some(true)
);
let changed_files_in_metadata = worktree_metadata["changed_files"]
.as_array()
.expect("changed_files should be in metadata");
assert!(
!changed_files_in_metadata.is_empty(),
"metadata should list changed files"
);
let task_result_text = match &task_result.body {
MessageBody::Text { text } => text,
other => panic!("expected text task result, got {other:?}"),
};
assert!(
task_result_text.contains("Worktree retained for review"),
"task result should indicate worktree is retained: {task_result_text}"
);
assert!(
task_result_text.contains("changes detected"),
"task result should mention changes were detected: {task_result_text}"
);
let changed_file_path = expected_worktree.join("changed_file.txt");
assert!(
changed_file_path.exists(),
"changed file should exist in retained worktree"
);
let content = std::fs::read_to_string(&changed_file_path)?;
assert!(
content.contains("This file was changed in the worktree"),
"changed file should have the expected content"
);
Ok(())
}