use super::background::{begin, finished, next, nothing_like, running_run, runs_out};
use super::parallel::KeyedRecorder;
use super::subagent::{Script, load, text};
use super::*;
use crate::entities::subagent::{RunKind, RunOutcome};
use crate::shared::api::contract::{ApiRole, ToolCallDelta};
const CAFE: &str = r#"{"a":{"name":"Mara","system_message":"barista"},
"b":{"name":"Jonas","system_message":"customer"},
"opening":{"speaker":"b","text":"Wrong drink?"},
"scene":"Cafe.","direction":"Stop at the goodbye.",
"max_messages":4,"moderate_every":2}"#;
fn start(id: &str) -> Script {
super::subagent::call(id, "start_dialogue", CAFE)
}
fn verdict(name: &str, args: &str) -> Script {
Script {
chunks: vec![
ChatChunk::ToolCall(ToolCallDelta {
index: 0,
id: Some("v0".into()),
name: Some(name.into()),
arguments: args.to_string(),
thought_signature: None,
}),
ChatChunk::Finished(FinishReason::ToolCalls),
],
hang: false,
}
}
fn cfg(sessions: u32) -> AppConfig {
let mut cfg = no_auto_cfg();
cfg.tools.subagent_background = true;
cfg.engine.managed.sessions = sessions;
cfg.engine.managed.context_size = 65_536;
cfg
}
fn scene_backend(parent: Vec<Script>) -> Arc<KeyedRecorder> {
KeyedRecorder::new(
vec![
(
"Stop at the goodbye.",
vec![verdict(
"dialogue_stop",
r#"{"reason":"Resolved","summary":"All good"}"#,
)],
),
("barista", vec![text("Sorry! Remaking it now.")]),
("customer", vec![text("Thanks — bye!")]),
("", parent),
],
10,
)
}
#[tokio::test]
async fn a_background_scene_lands_by_id_and_notifies_in_its_own_words() {
let backend = scene_backend(vec![
start("c1"),
text("staged it"),
text("they sorted the order out"),
]);
let (dir, cmd_tx, mut rx, handle, chat_id) = begin(backend.clone(), cfg(2)).await;
let run_id = running_run(&mut rx).await;
next(&mut rx, finished).await;
next(&mut rx, runs_out(0)).await;
next(&mut rx, finished).await;
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
let chat = load(dir.path(), chat_id);
let record = chat.messages[1].tool_calls[0]
.subagent
.as_deref()
.expect("the placeholder became the run");
assert_eq!(record.id, run_id);
assert_eq!(record.kind, RunKind::Dialogue);
assert!(record.background);
assert_eq!(record.outcome, Some(RunOutcome::Completed));
assert_eq!(record.participants.len(), 2);
assert_eq!(record.title, "Mara ↔ Jonas");
let roles: Vec<MessageRole> = record.messages.iter().map(|m| m.role).collect();
assert_eq!(
roles,
vec![
MessageRole::User,
MessageRole::Assistant,
MessageRole::User,
MessageRole::System,
]
);
let address = crate::features::chat_links::uri(run_id);
let started = &chat.messages[2].text;
assert!(started.contains(&address), "{started}");
assert!(!started.contains("Resolved"), "{started}");
let note = chat
.messages
.iter()
.find(|m| m.is_notification())
.expect("a task notification row");
assert_eq!(note.notification, Some(run_id));
assert!(note.text.contains("Mara ↔ Jonas"), "{}", note.text);
assert!(note.text.contains("Resolved"), "{}", note.text);
assert!(note.text.contains("All good"), "{}", note.text);
let rendered = |key: &str| -> Vec<String> {
[crate::shared::i18n::Lang::En, crate::shared::i18n::Lang::Ru]
.into_iter()
.map(|lang| {
crate::shared::i18n::locale(lang).tf(
key,
&[
("name", "Mara ↔ Jonas"),
("address", &address),
("body", &record.messages[3].text.clone()),
],
)
})
.collect()
};
let head = |t: &str| t.split("\n\n").next().unwrap_or_default().to_string();
let mine = head(¬e.text);
assert!(
rendered("tool.start_dialogue.notification")
.iter()
.any(|t| head(t) == mine),
"not the dialogue's wording: {}",
note.text
);
assert!(
!rendered("tool.start_subagent.notification")
.iter()
.any(|t| head(t) == mine),
"the sub-agent's wording leaked into a dialogue's notification: {}",
note.text
);
assert_eq!(
chat.messages.last().unwrap().text,
"they sorted the order out"
);
}
#[tokio::test]
async fn esc_leaves_the_scene_out_and_the_stop_lands_it_cancelled() {
let backend = KeyedRecorder::new(
vec![
("barista", vec![super::subagent::hang("thinking")]),
("", vec![start("c1"), text("staged it"), text("noted")]),
],
10,
);
let (dir, cmd_tx, mut rx, handle, chat_id) = begin(backend.clone(), cfg(2)).await;
let run_id = running_run(&mut rx).await;
next(&mut rx, finished).await;
cmd_tx.send(AppCommand::Cancel).unwrap();
nothing_like(&mut rx, runs_out(0)).await;
cmd_tx
.send(AppCommand::StopSubagentRun { id: run_id })
.unwrap();
next(&mut rx, runs_out(0)).await;
next(&mut rx, finished).await;
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
let chat = load(dir.path(), chat_id);
let run = chat.messages[1].tool_calls[0].subagent.as_deref().unwrap();
assert_eq!(run.kind, RunKind::Dialogue);
assert_eq!(run.outcome, Some(RunOutcome::Cancelled));
assert!(chat.messages.iter().any(|m| m.is_notification()));
}
#[tokio::test]
async fn with_one_session_the_scene_and_the_turns_take_turns() {
let backend = scene_backend(vec![
start("c1"),
text("staged it"),
text("they sorted the order out"),
]);
let (_dir, cmd_tx, mut rx, handle, _chat_id) = begin(backend.clone(), cfg(1)).await;
next(&mut rx, runs_out(1)).await;
next(&mut rx, finished).await;
next(&mut rx, runs_out(0)).await;
next(&mut rx, finished).await;
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
assert_eq!(
backend.max_in_flight(),
1,
"a scene and a turn overlapped on a one-session engine"
);
}
#[tokio::test]
async fn the_shared_cap_refuses_a_scene_past_it() {
let both = Script {
chunks: vec![
ChatChunk::ToolCall(ToolCallDelta {
index: 0,
id: Some("s1".into()),
name: Some("start_subagent".into()),
arguments: r#"{"name":"Critic","system_message":"be harsh","message":"rate X"}"#
.into(),
thought_signature: None,
}),
ChatChunk::ToolCall(ToolCallDelta {
index: 1,
id: Some("d1".into()),
name: Some("start_dialogue".into()),
arguments: CAFE.into(),
thought_signature: None,
}),
ChatChunk::Finished(FinishReason::ToolCalls),
],
hang: false,
};
let backend = KeyedRecorder::new(
vec![
("be harsh", vec![super::subagent::hang("thinking")]),
("", vec![both, text("one of them started")]),
],
10,
);
let mut cfg = cfg(2);
cfg.tools.subagent_background_max = 1;
let (dir, cmd_tx, mut rx, handle, chat_id) = begin(backend.clone(), cfg).await;
next(&mut rx, finished).await;
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
let chat = load(dir.path(), chat_id);
let calls = &chat.messages[1].tool_calls;
assert_eq!(calls.len(), 2);
let refusal = calls[1].result.as_deref().unwrap();
assert!(
refusal.contains("subagent_background_max"),
"the refusal names the setting: {refusal}"
);
assert!(
calls[1].subagent.is_none(),
"a refused scene leaves no record"
);
}
#[test]
fn start_dialogue_is_gated_by_the_setting() {
use crate::features::tools::{ToolGates, default_tool_ids, effective_tool_ids};
let all = default_tool_ids();
let gates = |background: bool| ToolGates {
web: true,
python: true,
fs: true,
mcp: true,
background,
history: false,
workspace: false,
workspace_commands: Default::default(),
sampling_provider: None,
sampling_endpoint: None,
};
let off = effective_tool_ids(&all, &gates(false));
let on = effective_tool_ids(&all, &gates(true));
assert!(!off.iter().any(|t| t == "start_dialogue"));
assert!(on.iter().any(|t| t == "start_dialogue"));
assert!(
off.iter().any(|t| t == "run_dialogue"),
"the twin is not the gate"
);
let added: Vec<&String> = on.iter().filter(|t| !off.contains(t)).collect();
assert_eq!(
added.len(),
2,
"the switch adds exactly the two twins: {added:?}"
);
}
#[tokio::test]
async fn a_woken_turn_with_an_empty_first_round_is_re_asked_muted() {
let empty = Script {
chunks: vec![ChatChunk::Finished(FinishReason::Length)],
hang: false,
};
let backend = scene_backend(vec![
start("c1"),
text("staged it"),
empty,
text("recovered: they sorted it out"),
]);
let (dir, cmd_tx, mut rx, handle, chat_id) = begin(backend.clone(), cfg(2)).await;
next(&mut rx, runs_out(1)).await;
next(&mut rx, finished).await;
next(&mut rx, runs_out(0)).await;
next(&mut rx, finished).await;
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
let chat = load(dir.path(), chat_id);
assert_eq!(
chat.messages.last().unwrap().text,
"recovered: they sorted it out",
"the muted re-ask is what the chat kept"
);
let parent: Vec<_> = backend
.requests()
.into_iter()
.filter(|r| r.messages.iter().any(|m| m.role == ApiRole::User))
.collect();
let last = parent.last().unwrap();
assert_eq!(last.sampling.reasoning_budget, Some(0));
}