use choreo_daemon::tools::Tool;
use choreo_daemon::tools::context::ToolContext;
use choreo_daemon::tools::subsession::{SpawnSubsession, SpawnSubsessionArgs};
use choreo_daemon::{ChildResult, DaemonCommand, SessionCommand};
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::mpsc;
use std::thread;
mod common;
#[ignore]
#[test]
fn spawn_subsession_happy_path() {
let db = Arc::new(common::test_db());
let (daemon_tx, daemon_rx) = mpsc::channel::<DaemonCommand>();
let daemon_handle = thread::spawn(move || {
match daemon_rx.recv().unwrap() {
DaemonCommand::CreateSession {
title,
parent_session_id,
working_dir,
reasoning_effort,
selected_model,
context_config: _,
account_name,
active_tool_groups,
reply,
} => {
assert_eq!(title.as_deref(), Some("test-sub"));
assert_eq!(parent_session_id, Some(1));
assert_eq!(working_dir, None);
assert_eq!(reasoning_effort, None);
assert_eq!(selected_model, None);
assert_eq!(account_name, None);
assert!(active_tool_groups.is_empty());
let (child_tx, child_rx) = mpsc::channel::<SessionCommand>();
let child_id = 42u64;
reply.send(Ok((child_id, child_tx))).unwrap();
match child_rx.recv().unwrap() {
SessionCommand::RunChildInput {
request_id,
user_text,
reply,
} => {
assert_eq!(request_id, 1);
assert_eq!(user_text.as_deref(), Some("work on this task"));
reply
.send(Ok(ChildResult {
output: "task output here".into(),
is_error: false,
}))
.unwrap();
}
_ => panic!("expected RunChildInput"),
}
}
_ => panic!("expected CreateSession"),
}
});
let tool_ctx = ToolContext {
session_id: 1,
db,
daemon_tx,
active_tool_groups: HashSet::new(),
reasoning_effort: None,
selected_model: None,
working_dir: None,
cancelled: Arc::new(AtomicBool::new(false)),
account_name: None,
};
let result = SpawnSubsession.execute(
SpawnSubsessionArgs {
prompt: "work on this task".into(),
title: Some("test-sub".into()),
categories: None,
},
None, None, Some(&tool_ctx),
);
match result {
Ok(output) => {
assert!(
output.contains("sub-session 42 result:"),
"output should mention child session id: {output}",
);
assert!(
output.contains("task output here"),
"output should contain child result: {output}",
);
}
Err(e) => panic!("SpawnSubsession::execute failed: {e}"),
}
daemon_handle.join().unwrap();
}
#[ignore]
#[test]
fn spawn_subsession_daemon_rejects_creation() {
let db = Arc::new(common::test_db());
let (daemon_tx, daemon_rx) = mpsc::channel::<DaemonCommand>();
let daemon_handle = thread::spawn(move || match daemon_rx.recv().unwrap() {
DaemonCommand::CreateSession { reply, .. } => {
reply
.send(Err(std::io::Error::other("daemon is busy")))
.unwrap();
}
_ => panic!("expected CreateSession"),
});
let tool_ctx = ToolContext {
session_id: 1,
db,
daemon_tx,
active_tool_groups: HashSet::new(),
reasoning_effort: None,
selected_model: None,
working_dir: None,
cancelled: Arc::new(AtomicBool::new(false)),
account_name: None,
};
let result = SpawnSubsession.execute(
SpawnSubsessionArgs {
prompt: "irrelevant".into(),
title: None,
categories: None,
},
None,
None,
Some(&tool_ctx),
);
match result {
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("daemon is busy"),
"error should mention daemon rejection: {msg}",
);
}
Ok(output) => panic!("expected error, got success: {output}"),
}
daemon_handle.join().unwrap();
}
#[ignore]
#[test]
fn spawn_subsession_daemon_disconnected() {
let db = Arc::new(common::test_db());
let (daemon_tx, daemon_rx) = mpsc::channel::<DaemonCommand>();
drop(daemon_rx);
let tool_ctx = ToolContext {
session_id: 1,
db,
daemon_tx,
active_tool_groups: HashSet::new(),
reasoning_effort: None,
selected_model: None,
working_dir: None,
cancelled: Arc::new(AtomicBool::new(false)),
account_name: None,
};
let result = SpawnSubsession.execute(
SpawnSubsessionArgs {
prompt: "should not matter".into(),
title: None,
categories: None,
},
None,
None,
Some(&tool_ctx),
);
match result {
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("daemon communication failed"),
"error should mention communication failure: {msg}",
);
}
Ok(output) => panic!("expected error, got success: {output}"),
}
}
#[ignore]
#[test]
fn spawn_subsession_no_context() {
let result = SpawnSubsession.execute(
SpawnSubsessionArgs {
prompt: "irrelevant".into(),
title: None,
categories: None,
},
None,
None,
None,
);
match result {
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("no session context"),
"error should mention missing context: {msg}",
);
}
Ok(output) => panic!("expected error, got success: {output}"),
}
}
#[ignore]
#[test]
fn spawn_subsession_inherits_categories() {
let db = Arc::new(common::test_db());
let (daemon_tx, daemon_rx) = mpsc::channel::<DaemonCommand>();
let daemon_handle = thread::spawn(move || {
match daemon_rx.recv().unwrap() {
DaemonCommand::CreateSession {
active_tool_groups,
reply,
..
} => {
let mut expected: Vec<String> =
["core", "shell"].into_iter().map(String::from).collect();
let mut actual = active_tool_groups.clone();
expected.sort();
actual.sort();
assert_eq!(actual, expected, "should inherit active_tool_groups");
let (child_tx, child_rx) = mpsc::channel::<SessionCommand>();
reply.send(Ok((1u64, child_tx))).unwrap();
match child_rx.recv().unwrap() {
SessionCommand::RunChildInput { reply, .. } => {
reply
.send(Ok(ChildResult {
output: "ok".into(),
is_error: false,
}))
.unwrap();
}
_ => panic!("expected RunChildInput"),
}
}
_ => panic!("expected CreateSession"),
}
});
let tool_ctx = ToolContext {
session_id: 1,
db,
daemon_tx,
active_tool_groups: ["core", "shell"].into_iter().map(String::from).collect(),
reasoning_effort: None,
selected_model: None,
working_dir: None,
cancelled: Arc::new(AtomicBool::new(false)),
account_name: None,
};
let result = SpawnSubsession.execute(
SpawnSubsessionArgs {
prompt: "work".into(),
title: None,
categories: None, },
None,
None,
Some(&tool_ctx),
);
assert!(result.is_ok(), "expected success: {result:?}");
daemon_handle.join().unwrap();
}
#[ignore]
#[test]
fn spawn_subsession_overrides_categories() {
let db = Arc::new(common::test_db());
let (daemon_tx, daemon_rx) = mpsc::channel::<DaemonCommand>();
let daemon_handle = thread::spawn(move || {
match daemon_rx.recv().unwrap() {
DaemonCommand::CreateSession {
active_tool_groups,
reply,
..
} => {
let mut expected: Vec<String> = ["db"].into_iter().map(String::from).collect();
let mut actual = active_tool_groups.clone();
expected.sort();
actual.sort();
assert_eq!(actual, expected, "should use explicit categories");
let (child_tx, child_rx) = mpsc::channel::<SessionCommand>();
reply.send(Ok((1u64, child_tx))).unwrap();
match child_rx.recv().unwrap() {
SessionCommand::RunChildInput { reply, .. } => {
reply
.send(Ok(ChildResult {
output: "ok".into(),
is_error: false,
}))
.unwrap();
}
_ => panic!("expected RunChildInput"),
}
}
_ => panic!("expected CreateSession"),
}
});
let tool_ctx = ToolContext {
session_id: 1,
db,
daemon_tx,
active_tool_groups: ["core", "shell"].into_iter().map(String::from).collect(),
reasoning_effort: None,
selected_model: None,
working_dir: None,
cancelled: Arc::new(AtomicBool::new(false)),
account_name: None,
};
let result = SpawnSubsession.execute(
SpawnSubsessionArgs {
prompt: "work".into(),
title: None,
categories: Some(vec!["db".into()]),
},
None,
None,
Some(&tool_ctx),
);
assert!(result.is_ok(), "expected success: {result:?}");
daemon_handle.join().unwrap();
}
#[ignore]
#[test]
fn spawn_subsession_inherits_selected_model() {
let db = Arc::new(common::test_db());
let (daemon_tx, daemon_rx) = mpsc::channel::<DaemonCommand>();
let daemon_handle = thread::spawn(move || {
match daemon_rx.recv().unwrap() {
DaemonCommand::CreateSession {
selected_model,
reply,
..
} => {
assert_eq!(
selected_model.as_deref(),
Some("gpt-4o"),
"should inherit selected_model from ToolContext",
);
let (child_tx, child_rx) = mpsc::channel::<SessionCommand>();
reply.send(Ok((1u64, child_tx))).unwrap();
match child_rx.recv().unwrap() {
SessionCommand::RunChildInput { reply, .. } => {
reply
.send(Ok(ChildResult {
output: "ok".into(),
is_error: false,
}))
.unwrap();
}
_ => panic!("expected RunChildInput"),
}
}
_ => panic!("expected CreateSession"),
}
});
let tool_ctx = ToolContext {
session_id: 1,
db,
daemon_tx,
active_tool_groups: HashSet::new(),
reasoning_effort: None,
selected_model: Some("gpt-4o".into()),
working_dir: None,
cancelled: Arc::new(AtomicBool::new(false)),
account_name: None,
};
let result = SpawnSubsession.execute(
SpawnSubsessionArgs {
prompt: "work".into(),
title: None,
categories: None,
},
None,
None,
Some(&tool_ctx),
);
assert!(result.is_ok(), "expected success: {result:?}");
daemon_handle.join().unwrap();
}