use std::sync::Barrier;
use std::time::{Duration, Instant};
use super::super::markdown_render::markdown_block;
use super::*;
#[test]
fn renderer_starts_without_selected_or_default_agent() {
let (_term, handle, _vt) = setup(80, 24);
let renderer = EventRenderer::new(
handle,
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
assert_eq!(
renderer
.current_agent_state()
.lock()
.expect("current agent")
.selected_agent_id()
.cloned(),
None
);
assert!(
renderer
.known_agents()
.lock()
.expect("known agents")
.is_empty()
);
assert!(
renderer
.agent_navigation()
.lock()
.expect("agent navigation")
.active_agents()
.is_empty()
);
}
#[test]
fn agent_response_marker_tracks_streaming_and_completed_states() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = marker_test_renderer(handle.clone());
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-marker",
"s1",
)));
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-marker"),
"marker answer",
None,
tau_proto::PromptOriginator::User,
),
));
sync(&handle);
assert!(vt.screen_contains(80, "◇ marker answer"));
assert!(!vt.screen_contains(80, "◆ marker answer"));
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-marker",
vec![assistant_message_item("marker answer")],
)));
sync(&handle);
assert!(vt.screen_contains(80, "◆ marker answer"));
assert!(!vt.screen_contains(80, "◇ marker answer"));
}
#[test]
fn final_assistant_projection_is_demand_driven_and_preserves_editor_bytes() {
let (_term, handle, _vt) = setup(80, 24);
let mut renderer = marker_test_renderer(handle);
renderer.switch_agent(agent_id("main"));
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-empty-final",
Vec::new(),
)));
let mut extension_final = finished_response(
"sp-extension-final",
vec![assistant_message_item("private side reply")],
);
extension_final.originator = tau_proto::PromptOriginator::Extension {
name: tau_proto::ExtensionName::parse("projection-test").expect("valid extension name"),
query_id: "side-query".to_owned(),
};
renderer.handle(&Event::ProviderResponseFinished(extension_final));
let unused = renderer.final_semantic_projection_counts_for_test();
assert_eq!(unused.message_materializations, 1);
assert_eq!(unused.message_concat_allocations, 0);
assert_eq!(unused.assistant_materializations, 0);
assert_eq!(unused.assistant_concat_allocations, 0);
assert_eq!(unused.editor_publication_clones, 0);
assert_eq!(unused.editor_publication_clone_bytes, 0);
assert_eq!(unused.reasoning_materializations, 0);
assert_eq!(unused.reasoning_concat_allocations, 0);
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-single-final",
vec![assistant_message_item("single λ part")],
)));
let single = renderer.final_semantic_projection_counts_for_test();
assert_eq!(single.message_materializations, 2);
assert_eq!(single.message_concat_allocations, 0);
assert_eq!(single.assistant_materializations, 1);
assert_eq!(single.assistant_concat_allocations, 0);
assert_eq!(single.editor_publication_clones, 1);
assert_eq!(
single.editor_publication_clone_bytes,
"single λ part".len() as u64
);
assert_eq!(
renderer
.editor_context()
.lock()
.expect("editor context")
.last_response
.as_deref(),
Some("single λ part")
);
let multipart = ContextItem::Message(MessageItem {
role: ContextRole::Assistant,
content: vec![
ContentPart::Text {
text: "first 🦀".to_owned(),
},
ContentPart::HarnessInternalText {
text: String::new(),
},
ContentPart::SyntheticCompactionSummary {
text: "\nsecond λ".to_owned(),
},
],
phase: None,
responses_raw_json: None,
});
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-multipart-final",
vec![multipart, assistant_message_item("\nthird")],
)));
let multipart = renderer.final_semantic_projection_counts_for_test();
assert_eq!(multipart.message_materializations, 4);
assert_eq!(multipart.message_concat_allocations, 1);
assert_eq!(multipart.assistant_materializations, 2);
assert_eq!(multipart.assistant_concat_allocations, 1);
assert_eq!(multipart.editor_publication_clones, 2);
assert_eq!(
multipart.editor_publication_clone_bytes,
("single λ part".len() + "first 🦀\nsecond λ\nthird".len()) as u64
);
assert_eq!(
renderer
.editor_context()
.lock()
.expect("editor context")
.last_response
.as_deref(),
Some("first 🦀\nsecond λ\nthird"),
"the optimized aggregate must preserve part variants, order, empties, and UTF-8 bytes"
);
}
#[test]
fn multipart_final_terminal_projection_matches_prejoined_reference() {
let multipart = ContextItem::Message(MessageItem {
role: ContextRole::Assistant,
content: vec![
ContentPart::Text {
text: "styled **bold 🦀**".to_owned(),
},
ContentPart::HarnessInternalText {
text: "\nsecond `λ`".to_owned(),
},
ContentPart::SyntheticCompactionSummary {
text: "\nthird".to_owned(),
},
],
phase: None,
responses_raw_json: None,
});
let prejoined = assistant_message_item("styled **bold 🦀**\nsecond `λ`\nthird");
let render = |prompt_id: &str, item| {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = marker_test_renderer(handle.clone());
renderer.switch_agent(agent_id("main"));
renderer.handle(&Event::ProviderResponseFinished(finished_response(
prompt_id,
vec![item],
)));
sync(&handle);
(renderer, vt)
};
let (multipart_renderer, multipart_vt) = render("sp-multipart-oracle", multipart);
let (reference_renderer, reference_vt) = render("sp-prejoined-oracle", prejoined);
assert_eq!(
multipart_vt.screen_text(80),
reference_vt.screen_text(80),
"multipart and eagerly prejoined finals must render identical terminal bytes and order"
);
for row in 0..24 {
for column in 0..80 {
assert_eq!(
multipart_vt.cell_style(row, column),
reference_vt.cell_style(row, column),
"terminal style mismatch at row {row}, column {column}"
);
}
}
let multipart_editor = multipart_renderer.editor_context();
let multipart_editor = multipart_editor.lock().expect("multipart editor");
let reference_editor = reference_renderer.editor_context();
let reference_editor = reference_editor.lock().expect("reference editor");
assert_eq!(
multipart_editor.last_response,
reference_editor.last_response
);
let multipart_work = multipart_renderer.final_semantic_projection_counts_for_test();
let reference_work = reference_renderer.final_semantic_projection_counts_for_test();
assert_eq!(multipart_work.message_concat_allocations, 1);
assert_eq!(multipart_work.assistant_concat_allocations, 1);
assert_eq!(reference_work.message_concat_allocations, 0);
assert_eq!(reference_work.assistant_concat_allocations, 0);
}
#[test]
fn multipart_reasoning_projection_matches_prejoined_reference_in_both_modes() {
let reasoning = |text: &str| {
ContextItem::ReasoningText(tau_proto::ReasoningTextItem {
kind: tau_proto::ReasoningTextKind::Summary,
text: text.to_owned(),
})
};
for compact_initially in [false, true] {
let render = |prompt_id: &str, items| {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = marker_test_renderer(handle.clone());
renderer.switch_agent(agent_id("main"));
renderer.apply_setting(
"show-thinking",
if compact_initially { "false" } else { "true" },
);
if compact_initially {
renderer.toggle_verbose_mode();
}
renderer.handle(&Event::ProviderResponseFinished(finished_response(
prompt_id, items,
)));
if compact_initially {
renderer.toggle_verbose_mode();
renderer.apply_setting("show-thinking", "true");
}
sync(&handle);
(renderer, vt)
};
let (multipart_renderer, multipart_vt) = render(
"sp-reasoning-multipart-oracle",
vec![reasoning("reason **🦀** "), reasoning("`λ`")],
);
let (reference_renderer, reference_vt) = render(
"sp-reasoning-prejoined-oracle",
vec![reasoning("reason **🦀** `λ`")],
);
assert_eq!(multipart_vt.screen_text(80), reference_vt.screen_text(80));
for row in 0..24 {
for column in 0..80 {
assert_eq!(
multipart_vt.cell_style(row, column),
reference_vt.cell_style(row, column),
"mode={compact_initially}, row={row}, column={column}"
);
}
}
assert_eq!(
multipart_renderer
.final_semantic_projection_counts_for_test()
.reasoning_concat_allocations,
1
);
assert_eq!(
reference_renderer
.final_semantic_projection_counts_for_test()
.reasoning_concat_allocations,
0
);
}
}
#[test]
fn final_reasoning_projection_skips_unretained_text_and_counts_concatenation() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = marker_test_renderer(handle.clone());
renderer.switch_agent(agent_id("main"));
renderer.apply_setting("show-thinking", "false");
let reasoning = |text: &str| {
ContextItem::ReasoningText(tau_proto::ReasoningTextItem {
kind: tau_proto::ReasoningTextKind::Summary,
text: text.to_owned(),
})
};
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-hidden-reasoning",
vec![reasoning("not retained"), assistant_message_item("answer")],
)));
let hidden = renderer.final_semantic_projection_counts_for_test();
assert_eq!(hidden.reasoning_materializations, 0);
assert_eq!(hidden.reasoning_concat_allocations, 0);
renderer.apply_setting("show-thinking", "true");
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-visible-reasoning",
vec![
reasoning("visible 🦀"),
reasoning(""),
assistant_message_item("second answer"),
],
)));
let single = renderer.final_semantic_projection_counts_for_test();
assert_eq!(single.reasoning_materializations, 1);
assert_eq!(single.reasoning_concat_allocations, 0);
sync(&handle);
assert!(vt.screen_contains(80, "visible 🦀"));
renderer.apply_setting("show-thinking", "false");
renderer.toggle_verbose_mode();
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-retained-reasoning",
vec![
reasoning("retained "),
reasoning("λ"),
assistant_message_item("compact answer"),
],
)));
let retained = renderer.final_semantic_projection_counts_for_test();
assert_eq!(retained.reasoning_materializations, 2);
assert_eq!(retained.reasoning_concat_allocations, 1);
renderer.toggle_verbose_mode();
renderer.apply_setting("show-thinking", "true");
sync(&handle);
assert!(
vt.screen_contains(80, "retained λ"),
"compact-hidden reasoning must retain the exact aggregate for later display"
);
}
#[test]
fn visible_provider_final_requests_one_settled_redraw() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = marker_test_renderer(handle.clone());
renderer.apply_setting("show-thinking", "true");
renderer.apply_setting("show-turn-stats", "true");
for index in 0..40 {
handle.print_output(
format!("atomic-history-{index}"),
format!("history {index}"),
);
}
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-atomic-final",
"s1",
)));
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-atomic-final"),
"live answer",
Some("live reasoning".to_owned()),
tau_proto::PromptOriginator::User,
),
));
sync(&handle);
let generation = vt.frame_generation();
let editor_context = renderer.editor_context();
let staging = Arc::new(Barrier::new(2));
let unrelated = handle.clone();
let producer_staging = staging.clone();
let producer = std::thread::spawn(move || {
producer_staging.wait();
unrelated.print_output("unrelated-final-race", "unrelated output");
unrelated.redraw();
});
let hook_staging = staging.clone();
let hook_vt = vt.clone();
let staging_editor = editor_context.clone();
renderer.set_finished_staging_hook(Arc::new(move || {
assert!(
staging_editor
.lock()
.expect("staging editor")
.last_response
.is_none()
);
hook_staging.wait();
hook_vt.wait_for_frame_containing_after(generation, "unrelated output");
}));
let commit_barrier = Arc::new(Barrier::new(2));
let commit_producer_barrier = commit_barrier.clone();
let commit_handle = handle.clone();
let commit_producer = std::thread::spawn(move || {
commit_producer_barrier.wait();
commit_handle.print_output("unrelated-during-commit", "commit output");
commit_handle.redraw_sync();
commit_producer_barrier.wait();
});
let hook_commit_barrier = commit_barrier.clone();
let commit_editor = editor_context.clone();
renderer.set_finished_commit_hook(Arc::new(move || {
assert!(
commit_editor
.lock()
.expect("commit editor")
.last_response
.is_none()
);
hook_commit_barrier.wait();
hook_commit_barrier.wait();
}));
let published_editor = editor_context.clone();
renderer.set_finished_published_hook(Arc::new(move || {
let editor = published_editor.lock().expect("published editor");
let response = editor.last_response.as_deref().expect("published response");
assert!(response.contains("first settled item"));
assert!(response.contains("settled answer"));
}));
let mut finished =
finished_response_with_usage("sp-atomic-final", "main", 120, 40, 12, "settled answer");
finished.output_items = vec![
ContextItem::ReasoningText(tau_proto::ReasoningTextItem {
kind: tau_proto::ReasoningTextKind::Summary,
text: "settled reasoning".to_owned(),
}),
assistant_message_item("first settled item"),
assistant_message_item("settled answer"),
ContextItem::Compaction(
OpaqueProviderItem::from_raw_json(r#"{"type":"compaction"}"#)
.expect("valid compaction item"),
),
ContextItem::ToolCall(ToolCallItem {
call_id: "atomic-placeholder".into(),
name: tau_proto::ToolName::new("read"),
tool_type: tau_proto::ToolType::Function,
arguments: CborValue::Map(Vec::new()),
raw_arguments_json: None,
responses_envelope: None,
}),
ContextItem::ToolCall(ToolCallItem {
call_id: "atomic-placeholder-second".into(),
name: tau_proto::ToolName::new("search"),
tool_type: tau_proto::ToolType::Function,
arguments: CborValue::Map(Vec::new()),
raw_arguments_json: None,
responses_envelope: None,
}),
];
finished.stop_reason = ProviderStopReason::ToolCalls;
renderer.handle(&Event::ProviderResponseFinished(finished));
producer.join().expect("unrelated producer");
commit_producer.join().expect("commit producer");
let settled_generation = vt.wait_for_frame_containing_after(generation, "◆ settled answer");
let frames = vt.frames.0.lock().expect("frames");
for frame in &frames[generation..settled_generation] {
let text = frame.join("\n");
let complete_live = text.contains("◇ live answer")
&& text.contains("live reasoning")
&& !text.contains("◆ settled answer")
&& !text.contains("◆ first settled item")
&& !text.contains("settled reasoning")
&& !text.contains("compact ok")
&& !text.contains(" ↑120 ↓12")
&& !text.contains("%0/2");
let complete_settled = text.contains("◆ settled answer")
&& text.contains("◆ first settled item")
&& text.contains("settled reasoning")
&& text.contains("compact ok")
&& text.contains(" ↑120 ↓12")
&& text.contains("@main")
&& text.contains("%0/2")
&& !text.contains("◇ live answer")
&& !text.contains("live reasoning");
assert!(
complete_live || complete_settled,
"frame mixed live and settled final state: {frame:?}"
);
}
assert!(
frames[settled_generation - 1]
.iter()
.any(|row| row.contains("unrelated output"))
);
assert!(
frames[settled_generation - 1]
.iter()
.any(|row| row.contains("commit output"))
);
drop(frames);
let editor = editor_context.lock().expect("editor");
let response = editor.last_response.as_deref().expect("last response");
assert!(response.contains("first settled item"));
assert!(response.contains("settled answer"));
drop(editor);
assert_eq!(renderer.test_active_tool_count(), 2);
let placeholder_ids = renderer
.tool_placeholder_ids_for_test(&["atomic-placeholder", "atomic-placeholder-second"]);
assert_eq!(placeholder_ids[1], placeholder_ids[0] + 1);
assert!(vt.scrollback_contains(80, 40, "history 0"));
}
#[test]
fn hidden_provider_final_stays_off_screen_without_redraw() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = marker_test_renderer(handle.clone());
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-hidden-final",
"s1",
)));
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-hidden-final"),
"hidden live answer",
None,
tau_proto::PromptOriginator::User,
),
));
renderer.handle(&Event::StartAgentAccepted(tau_proto::StartAgentAccepted {
start_id: tau_proto::StartOperationId(1),
query_id: "q-visible-worker".to_owned(),
agent_id: agent_id("visible-worker"),
}));
renderer.switch_agent(agent_id("visible-worker"));
sync(&handle);
let redraw_requests = renderer.redraw_request_count_for_test();
let mut hidden_final =
finished_response_with_usage("sp-hidden-final", "main", 12, 4, 2, "hidden settled answer");
hidden_final
.output_items
.push(ContextItem::ToolCall(ToolCallItem {
call_id: "hidden-partial-tool".into(),
name: tau_proto::ToolName::new("read"),
tool_type: tau_proto::ToolType::Function,
arguments: CborValue::Map(Vec::new()),
raw_arguments_json: Some("{".to_owned()),
responses_envelope: None,
}));
hidden_final.stop_reason = ProviderStopReason::Length;
renderer.handle(&Event::ProviderResponseFinished(hidden_final));
assert_eq!(renderer.redraw_request_count_for_test(), redraw_requests);
assert!(!vt.screen_contains(80, "hidden settled answer"));
renderer.switch_agent(agent_id("main"));
sync(&handle);
assert!(vt.screen_contains(80, "◆ hidden settled answer"));
assert!(vt.screen_contains(
80,
"Model reached its output-token limit while producing a tool call"
));
assert!(!vt.screen_contains(80, "◇ hidden live answer"));
assert!(vt.screen_contains(80, "✨ @main"));
assert!(!vt.screen_contains(80, "%0/"));
assert_eq!(renderer.test_active_tool_count(), 0);
}
#[test]
fn empty_and_output_length_finals_publish_complete_frames() {
for (prompt_id, finished, final_markers, frame_completion_marker, final_status) in [
(
"atomic-empty",
finished_response("atomic-empty", Vec::new()),
Vec::new(),
"💤 @main",
"💤 @main",
),
(
"atomic-length",
{
let mut finished = finished_response(
"atomic-length",
vec![
assistant_message_item("partial terminal"),
ContextItem::ToolCall(ToolCallItem {
call_id: "partial-atomic-tool".into(),
name: tau_proto::ToolName::new("read"),
tool_type: tau_proto::ToolType::Function,
arguments: CborValue::Map(Vec::new()),
raw_arguments_json: Some("{".to_owned()),
responses_envelope: None,
}),
],
);
finished.stop_reason = ProviderStopReason::Length;
finished
},
vec![
"◆ partial terminal",
"Model reached its output-token limit while producing a tool call",
],
"◆ partial terminal",
"✨ @main",
),
] {
let (_term, handle, vt) = setup(100, 12);
let mut renderer = marker_test_renderer(handle.clone());
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
prompt_id, "s1",
)));
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id(prompt_id),
"old live terminal",
None,
tau_proto::PromptOriginator::User,
),
));
sync(&handle);
let generation = vt.frame_generation();
let commit_handle = handle.clone();
renderer.set_finished_commit_hook(Arc::new(move || {
commit_handle.redraw_sync();
}));
renderer.handle(&Event::ProviderResponseFinished(finished));
let final_generation =
vt.wait_for_frame_containing_after(generation, frame_completion_marker);
let frames = vt.frames.0.lock().expect("frames");
for frame in &frames[generation..final_generation] {
let text = frame.join("\n");
let live = text.contains("◇ old live terminal")
&& final_markers.iter().all(|marker| !text.contains(marker))
&& !text.contains(frame_completion_marker);
let settled = final_markers.iter().all(|marker| text.contains(marker))
&& text.contains(final_status)
&& !text.contains("%0/")
&& !text.contains("◇ old live terminal")
&& !text.contains("old live terminal")
&& !text.contains("(provider returned an empty response)");
assert!(live || settled, "partial terminal frame: {frame:?}");
}
assert_eq!(renderer.test_active_tool_count(), 0);
}
}
#[test]
fn deferred_initial_discovery_final_uses_atomic_publication_cut() {
let (_term, handle, vt) = setup(100, 16);
let mut renderer = marker_test_renderer(handle.clone());
renderer.handle(&Event::HarnessAgentContextInitialized(
tau_proto::HarnessAgentContextInitialized {
discovery_revision: 0,
discovery_refreshes: Vec::new(),
discovery_diagnostics: Vec::new(),
effective_skills: Vec::new(),
session_id: test_session_id("s1"),
agent_id: agent_id("main"),
agent_initialization_id: tau_proto::AgentInitializationId::parse("main-init")
.expect("initialization id"),
listed_skills: Vec::new(),
agents_files: Vec::new(),
},
));
renderer.handle(&agent_message("main", "worker", "deferred overview once"));
renderer.handle(&Event::MessageDelivered(tau_proto::MessageDelivered::new(
tau_proto::MessagePublisherId::parse("deferred-bridge").expect("publisher id"),
tau_proto::MessageAgentTarget::new("main"),
tau_proto::MessageFactId::new("deferred-owned-fact"),
tau_proto::MessageParty {
stable_id: "deferred-sender".to_owned(),
display_name: None,
sender_auth: None,
sender_trust: None,
},
None,
"deferred owned fact once",
)));
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("deferred-final"),
"deferred live",
None,
tau_proto::PromptOriginator::User,
),
));
let finished = finished_response(
"deferred-final",
vec![assistant_message_item("deferred settled")],
);
renderer.handle(&Event::ProviderResponseFinished(finished));
sync(&handle);
let generation = vt.frame_generation();
let staging_handle = handle.clone();
let staging_vt = vt.clone();
renderer.set_finished_staging_hook(Arc::new(move || {
staging_handle.redraw_sync();
assert!(!staging_vt.screen_contains(100, "deferred live"));
assert!(!staging_vt.screen_contains(100, "deferred settled"));
}));
let commit_handle = handle.clone();
renderer.set_finished_commit_hook(Arc::new(move || commit_handle.redraw_sync()));
let deferred_editor = renderer.editor_context();
let published_editor = deferred_editor.clone();
renderer.set_finished_published_hook(Arc::new(move || {
assert_eq!(
published_editor
.lock()
.expect("published editor")
.last_response
.as_deref(),
Some("deferred settled")
);
}));
renderer.switch_agent(agent_id("main"));
let final_generation = vt.wait_for_frame_containing_after(generation, "◆ deferred settled");
let frames = vt.frames.0.lock().expect("frames");
let mut saw_live = false;
for frame in &frames[generation..final_generation] {
let text = frame.join("\n");
let before_replay = (text.contains("@main") || text.contains("deferred overview once"))
&& !text.contains("deferred live")
&& !text.contains("deferred settled");
let live = text.contains("deferred live")
&& text.contains("✨ @main")
&& !text.contains("deferred settled");
let settled = text.contains("◆ deferred settled")
&& text.contains("💤 @main")
&& !text.contains("deferred live");
if live {
saw_live = true;
}
assert!(
(!saw_live && before_replay) || live || settled,
"mixed deferred final frame: {frame:?}"
);
}
drop(frames);
assert_eq!(
deferred_editor
.lock()
.expect("editor")
.last_response
.as_deref(),
Some("deferred settled")
);
let selected_text = vt.screen_text(100).join("\n");
assert_eq!(selected_text.matches("deferred overview once").count(), 1);
assert!(!selected_text.contains("deferred owned fact once"));
renderer.clear_selected_agent();
sync(&handle);
assert_eq!(
vt.screen_text(100)
.join("\n")
.matches("deferred overview once")
.count(),
1
);
assert_eq!(
vt.screen_text(100)
.join("\n")
.matches("deferred owned fact once")
.count(),
1
);
}
#[test]
fn switching_between_displayed_agents_restores_transcripts() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.switch_agent(agent_id("worker-1"));
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "worker one transcript".into(),
agent_id: agent_id("worker-1"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
renderer.switch_agent(agent_id("worker-2"));
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "worker two transcript".into(),
agent_id: agent_id("worker-2"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
sync(&handle);
assert!(vt.screen_contains(80, "worker two transcript"));
assert!(!vt.screen_contains(80, "worker one transcript"));
let full_render_count = handle.full_render_count();
renderer.switch_agent(agent_id("worker-1"));
sync(&handle);
assert!(vt.screen_contains(80, "worker one transcript"));
assert!(!vt.screen_contains(80, "worker two transcript"));
assert!(handle.full_render_count() > full_render_count);
}
#[test]
fn agent_switch_first_frame_has_matching_transcript_and_placeholder() {
let (_term, handle, vt) = setup(100, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
let generation = vt.frame_generation();
handle.with_redraw_suppressed(|| {
renderer.switch_agent(agent_id("worker-1"));
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "worker one transcript".into(),
agent_id: agent_id("worker-1"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
renderer.switch_agent(agent_id("worker-2"));
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "worker two transcript".into(),
agent_id: agent_id("worker-2"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
});
let generation = vt.wait_for_frame_containing_after(generation, "worker two transcript");
renderer.switch_agent_after_display_update_for_test(agent_id("worker-1"), || {
handle.redraw_sync();
});
let frame = vt.wait_for_frame_after(generation);
assert!(
frame
.iter()
.any(|row| row.contains("worker one transcript")),
"{frame:?}"
);
assert!(
frame
.iter()
.any(|row| row.contains("Write a message to worker-1"))
);
assert!(
!frame
.iter()
.any(|row| row.contains("Write a message to worker-2"))
);
assert!(
frame.iter().any(|row| row.contains("@worker-1")),
"the status identity must change in the same redraw-suppressed frame: {frame:?}"
);
}
#[test]
fn randomized_agent_switching_preserves_transcripts_without_snapshot_clones() {
let (_term, handle, vt) = setup(100, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
let agents = ["alpha", "bravo", "charlie"];
let mut latest = [String::new(), String::new(), String::new()];
let mut seed = 0x71d3_9a4bu64;
for step in 0..96 {
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
let index = (seed as usize) % agents.len();
let agent = agents[index];
renderer.switch_agent(agent_id(agent));
let text = format!("switch-reference-{agent}-{step}");
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("switch-reference"),
text: text.clone(),
agent_id: agent_id(agent),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
latest[index] = text;
if step % 8 == 0 {
sync(&handle);
assert!(
vt.screen_contains(100, &latest[index]),
"selected agent {agent} must match the reference transcript"
);
}
}
for (index, agent) in agents.into_iter().enumerate() {
renderer.switch_agent(agent_id(agent));
sync(&handle);
assert!(
vt.screen_contains(100, &latest[index]),
"final transcript for {agent} must match the reference"
);
}
assert_eq!(
handle.output_snapshot_count(),
0,
"selection must not clone terminal output snapshots"
);
assert!(
handle.output_snapshot_take_count() > 0,
"selection must take snapshots by ownership"
);
}
#[test]
#[ignore = "manual selection ownership benchmark"]
fn benchmark_selection_snapshot_ownership_by_transcript_size() {
for block_count in [0, 1, 16, 128] {
let (_term, handle, _vt) = setup(100, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.switch_agent(agent_id("source"));
for block in 0..block_count {
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("selection-benchmark"),
text: format!("selection-benchmark-source-block-{block}"),
agent_id: agent_id("source"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
}
renderer.switch_agent(agent_id("destination"));
let clones_before = handle.output_snapshot_count();
let takes_before = handle.output_snapshot_take_count();
let started = Instant::now();
for _ in 0..100 {
renderer.switch_agent(agent_id("source"));
renderer.switch_agent(agent_id("destination"));
}
std::hint::black_box(&renderer);
eprintln!(
"selection ownership benchmark: submitted_prompts={block_count} iterations=200 snapshot_clones={} snapshot_takes={} elapsed={:?}; no timing threshold",
handle.output_snapshot_count() - clones_before,
handle.output_snapshot_take_count() - takes_before,
started.elapsed(),
);
}
}
#[test]
fn agent_context_initialization_is_visible_only_in_selected_agent_transcript() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
let initialized = |agent: &str, skill: &str, path: &str| {
Event::HarnessAgentContextInitialized(tau_proto::HarnessAgentContextInitialized {
discovery_revision: 0,
discovery_refreshes: Vec::new(),
discovery_diagnostics: Vec::new(),
effective_skills: Vec::new(),
session_id: test_session_id("session-1"),
agent_id: agent_id(agent),
agent_initialization_id: tau_proto::AgentInitializationId::parse(format!(
"{agent}-init"
))
.expect("test identifier must be valid"),
listed_skills: vec![tau_proto::DiscoveryEffectiveSkill {
visibility: Default::default(),
name: skill.into(),
description: format!("{skill} description"),
source: tau_proto::DiscoveryEffectiveSkillSource::BuiltIn,
add_to_prompt: true,
user_invocable: true,
disable_model_invocation: false,
argument_hint: None,
}],
agents_files: vec![tau_proto::DiscoveryAgentsFileSummary {
file_path: path.into(),
lines: 6,
bytes: 550,
}],
})
};
renderer.switch_agent(agent_id("agent-1"));
renderer.handle(&initialized(
"agent-1",
"foreground-skill",
"/one/AGENTS.md",
));
renderer.handle(&initialized(
"agent-2",
"background-skill",
"/two/AGENTS.md",
));
sync(&handle);
assert!(vt.screen_contains(80, "foreground-skill 1L, 28B"));
assert!(vt.screen_contains(80, "/one/AGENTS.md 6L, 550B"));
assert!(!vt.screen_contains(80, "background-skill"));
assert!(!vt.screen_contains(80, "/two/AGENTS.md"));
renderer.switch_agent(agent_id("agent-2"));
sync(&handle);
assert!(vt.screen_contains(80, "background-skill 1L, 28B"));
assert!(vt.screen_contains(80, "/two/AGENTS.md 6L, 550B"));
assert!(!vt.screen_contains(80, "foreground-skill"));
assert!(!vt.screen_contains(80, "/one/AGENTS.md"));
renderer.switch_agent(agent_id("agent-1"));
sync(&handle);
assert!(vt.screen_contains(80, "foreground-skill 1L, 28B"));
assert!(!vt.screen_contains(80, "background-skill"));
}
#[test]
fn agent_cost_projection_tracks_renderer_session_authority() {
let (_term, handle, _vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle,
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
let projection = renderer.agent_estimated_api_costs();
let cost = tau_proto::EstimatedApiCost::from_picodollars(2_140_000_000_000);
let stats = |session: &str, agent: &str| {
Event::AgentStatsUpdated(tau_proto::AgentStatsUpdated {
session_id: test_session_id(session),
agent_id: agent_id(agent),
navigation_mode: tau_proto::AgentNavigationMode::Active,
runtime_state: tau_proto::AgentRuntimeState::Idle,
turn_activity: tau_proto::AgentTurnActivity::Idle,
tools: tau_proto::AgentToolStats::default(),
context: tau_proto::AgentContextStats::default(),
inner_turns_total: None,
estimated_api_cost: cost,
creator_subtree_estimated_api_cost: Default::default(),
work_status: Default::default(),
})
};
renderer.handle(&Event::SessionStarted(SessionStarted {
session_id: test_session_id("s1"),
reason: SessionStartReason::Initial,
}));
renderer.handle(&stats("s1", "agent-a"));
assert_eq!(
projection.snapshot().get(&agent_id("agent-a")),
Some(&crate::estimated_cost::AgentCostSnapshot::new(
cost,
tau_proto::EstimatedApiCost::default(),
))
);
renderer.handle(&stats("s2", "foreign"));
assert!(!projection.snapshot().contains_key(&agent_id("foreign")));
renderer.handle(&Event::SessionStarted(SessionStarted {
session_id: test_session_id("s2"),
reason: SessionStartReason::Initial,
}));
assert!(projection.snapshot().is_empty());
renderer.handle(&stats("s2", "agent-b"));
assert!(projection.snapshot().is_empty());
renderer.handle(&Event::SessionStarted(SessionStarted {
session_id: test_session_id("s3"),
reason: SessionStartReason::Resume,
}));
assert!(projection.snapshot().is_empty());
renderer.handle(&stats("s3", "agent-c"));
assert!(projection.snapshot().is_empty());
}
#[test]
fn clearing_selected_agent_preserves_previous_transcript() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::StartAgentAccepted(tau_proto::StartAgentAccepted {
start_id: tau_proto::StartOperationId(1),
query_id: "q-worker".to_owned(),
agent_id: agent_id("worker-1"),
}));
renderer.switch_agent(agent_id("worker-1"));
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "worker transcript survives".into(),
agent_id: agent_id("worker-1"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
renderer.clear_selected_agent();
renderer.handle(&Event::StartAgentAccepted(tau_proto::StartAgentAccepted {
start_id: tau_proto::StartOperationId(1),
query_id: "q-helper".to_owned(),
agent_id: agent_id("helper-1"),
}));
renderer.switch_agent(agent_id("helper-1"));
renderer.switch_agent(agent_id("worker-1"));
sync(&handle);
assert!(vt.screen_contains(80, "worker transcript survives"));
}
#[test]
fn agent_switch_preserves_separate_transcripts() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::SessionStarted(tau_proto::SessionStarted {
session_id: test_session_id("s1"),
reason: tau_proto::SessionStartReason::Initial,
}));
renderer.handle(&Event::StartAgentAccepted(tau_proto::StartAgentAccepted {
start_id: tau_proto::StartOperationId(1),
query_id: "q-worker".to_owned(),
agent_id: agent_id("worker-1"),
}));
let originator = tau_proto::PromptOriginator::Extension {
name: tau_proto::ExtensionName::parse("core-subagents")
.expect("test identifier must satisfy its grammar"),
query_id: "q-worker".to_owned(),
};
renderer.handle(&Event::AgentPromptCreated(AgentPromptCreated {
agent_id: agent_id("worker-1"),
originator: originator.clone(),
..agent_prompt_created("worker-sp", "s1")
}));
renderer.handle(&Event::ProviderResponseFinished(ProviderResponseFinished {
automatic_compaction_decision: None,
output_length_disposition: tau_proto::OutputLengthDisposition::None,
agent_id: agent_id("worker-1"),
originator,
..finished_response("worker-sp", vec![assistant_message_item("worker answer")])
}));
sync(&handle);
assert!(!vt.screen_contains(80, "worker answer"));
renderer.switch_agent(agent_id("worker-1"));
sync(&handle);
assert!(vt.screen_contains(80, "worker answer"));
assert!(vt.screen_contains(80, "@worker-1"));
renderer.switch_agent(agent_id("main"));
sync(&handle);
assert!(!vt.screen_contains(80, "worker answer"));
}
#[test]
fn agent_messages_render_all_recipients_as_history() {
let (_term, handle, vt) = setup(120, 8);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::AgentStarted(tau_proto::AgentStarted {
creator: Some(tau_proto::AgentCreator::default()),
parent_agent: None,
agent_id: agent_id("manager_11111111"),
role: "manager".to_owned(),
display_name: Some("add-all-agent-overview for @engineer_22222222".to_owned()),
metadata: Vec::new(),
ephemeral: false,
}));
renderer.handle(&agent_message(
"manager_11111111",
"engineer_22222222",
"hello worker",
));
sync(&handle);
assert!(vt.screen_contains(
120,
"Message from @manager_11111111 (add-all-agent-overview for @engineer_22222222) to @engineer_22222222:"
));
assert!(vt.screen_contains(120, "hello worker"));
let lines = vt.screen_text(120);
let row = lines
.iter()
.position(|line| line.contains("Message from @manager_11111111"))
.expect("message header row") as u16;
use unicode_width::UnicodeWidthStr as _;
let sender_col = lines[row as usize][..lines[row as usize]
.find("@manager_11111111")
.expect("sender column")]
.width() as u16;
let recipient_col = lines[row as usize][..lines[row as usize]
.rfind("@engineer_22222222")
.expect("recipient column")]
.width() as u16;
assert!(vt.cell_style(row, sender_col).2);
assert!(vt.cell_style(row, recipient_col).2);
assert!(!vt.cell_style(row, sender_col - 1).2);
let task_context_col = lines[row as usize][..lines[row as usize]
.find("(add-all-agent-overview for @engineer_22222222)")
.expect("task-name context column")]
.width() as u16;
assert!(!vt.cell_style(row, task_context_col).2);
let context_id_col = lines[row as usize][..lines[row as usize]
.find("@engineer_22222222")
.expect("routing-id text inside task-name context")]
.width() as u16;
assert!(!vt.cell_style(row, context_id_col).2);
for idx in 0..20 {
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: format!("scroll filler {idx}"),
agent_id: agent_id("engineer_22222222"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
}
sync(&handle);
assert!(!vt.screen_contains(
120,
"Message from @manager_11111111 (add-all-agent-overview for @engineer_22222222) to @engineer_22222222:"
));
}
#[test]
fn compact_mode_reprojects_agent_activity_without_content_leaks() {
let status = Event::AgentMessageReceived(tau_proto::AgentMessageReceived {
message_id: tau_proto::AgentMessageId::parse("compact-status").expect("test message id"),
sender_id: agent_id("researcher-iDAo"),
sender_session_id: None,
recipient_id: agent_id("manager"),
kind: tau_proto::AgentMessageKind::WatchWorkStatus,
watch_provider_status: None,
watch_work_status: Some(tau_proto::AgentWatchWorkStatusNotification {
session_id: test_session_id("compact-session"),
subscription_id: "compact-subscription".to_owned(),
status_epoch: tau_proto::AgentWorkStatusEpoch::from_raw(1),
phase: tau_proto::AgentWorkStatusPhase::Working,
title: Some("COMPACT_STATUS_LEAK".to_owned()),
initial: false,
}),
watch_long_wait: None,
watch_lifecycle: None,
sender_notice: None,
recipient_notice: None,
message: "COMPATIBILITY_STATUS_BODY_LEAK".to_owned(),
});
let message = Event::AgentMessageReceived(tau_proto::AgentMessageReceived {
message_id: tau_proto::AgentMessageId::parse("compact-message").expect("test message id"),
sender_id: agent_id("researcher-iDAo"),
sender_session_id: None,
recipient_id: agent_id("manager"),
kind: tau_proto::AgentMessageKind::Message,
watch_provider_status: None,
watch_work_status: None,
watch_long_wait: None,
watch_lifecycle: None,
sender_notice: None,
recipient_notice: None,
message: "COMPACT_MESSAGE_LEAK Δ\nCOMPACT_SECOND_LINE 🧪".to_owned(),
});
let mut watch_prompt = message.clone();
let Event::AgentMessageReceived(watch_prompt_message) = &mut watch_prompt else {
unreachable!("cloned message retains its received event variant");
};
watch_prompt_message.message_id =
tau_proto::AgentMessageId::parse("compact-watch-prompt").expect("test message id");
watch_prompt_message.kind = tau_proto::AgentMessageKind::WatchPrompt;
watch_prompt_message.message = "WATCH_PROMPT_BODY_RETAINS".to_owned();
let mut watch_lifecycle = message.clone();
let Event::AgentMessageReceived(watch_lifecycle_message) = &mut watch_lifecycle else {
unreachable!("cloned message retains its received event variant");
};
watch_lifecycle_message.message_id =
tau_proto::AgentMessageId::parse("compact-watch-lifecycle").expect("test message id");
watch_lifecycle_message.kind = tau_proto::AgentMessageKind::WatchLifecycle;
watch_lifecycle_message.watch_lifecycle = Some(tau_proto::AgentWatchLifecycleNotification {
state: tau_proto::AgentWatchLifecycleState::Stopped,
reason: tau_proto::AgentWatchLifecycleReason::UnexpectedUnload,
});
watch_lifecycle_message.message = String::new();
let assert_compact = |vt: &VtWriter| {
let transcript = visible_lines(vt, 120).join("\n");
assert!(
transcript.contains("■ Message from @researcher-iDAo"),
"compact mode must retain the existing message header: {transcript:?}"
);
for canary in [
"Status update from",
"COMPACT_STATUS_LEAK",
"COMPATIBILITY_STATUS_BODY_LEAK",
"COMPACT_MESSAGE_LEAK",
"COMPACT_SECOND_LINE",
] {
assert!(
!vt.scrollback_contains(120, 100, canary),
"compact terminal rows and scrollback leaked {canary:?}"
);
}
assert!(
transcript.contains("Prompt to @researcher-iDAo observed by @manager:"),
"compact mode must preserve watched user-prompt headers: {transcript:?}"
);
assert!(
vt.scrollback_contains(120, 100, "WATCH_PROMPT_BODY_RETAINS"),
"compact mode must preserve watched user-prompt content"
);
assert!(
transcript.lines().any(|line| {
line.contains("■ Message from @researcher-iDAo") && line.trim_end().ends_with(':')
}),
"compact mode must preserve the content-free lifecycle form: {transcript:?}"
);
};
let (_live_term, live_handle, live_vt) = setup(120, 24);
let mut live = EventRenderer::new(
live_handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
live.switch_agent(agent_id("manager"));
live.toggle_verbose_mode();
live.handle(&status);
live.handle(&message);
live.handle(&watch_prompt);
live.handle(&watch_lifecycle);
sync(&live_handle);
assert_compact(&live_vt);
live.toggle_verbose_mode();
sync(&live_handle);
assert!(live_vt.screen_contains(
120,
"▤ Status update from @researcher-iDAo: 🚀 COMPACT_STATUS_LEAK"
));
assert!(live_vt.screen_contains(120, "■ Message from @researcher-iDAo:"));
assert!(live_vt.screen_contains(120, "COMPACT_MESSAGE_LEAK Δ"));
assert!(live_vt.screen_contains(120, "COMPACT_SECOND_LINE 🧪"));
live.toggle_verbose_mode();
live.switch_agent(agent_id("other"));
live.switch_agent(agent_id("manager"));
sync(&live_handle);
assert_compact(&live_vt);
let compact_live = visible_lines(&live_vt, 120);
let (_replay_term, replay_handle, replay_vt) = setup(120, 24);
let mut replay = EventRenderer::new(
replay_handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
replay.switch_agent(agent_id("manager"));
replay.toggle_verbose_mode();
replay.handle_recorded_at(&status, tau_proto::UnixMicros::new(1));
replay.handle_recorded_at(&message, tau_proto::UnixMicros::new(2));
replay.handle_recorded_at(&watch_prompt, tau_proto::UnixMicros::new(3));
replay.handle_recorded_at(&watch_lifecycle, tau_proto::UnixMicros::new(4));
replay.switch_agent(agent_id("other"));
replay.switch_agent(agent_id("manager"));
sync(&replay_handle);
assert_compact(&replay_vt);
assert_eq!(
visible_lines(&replay_vt, 120),
compact_live,
"live and cold replay must project identical compact agent activity"
);
}
#[test]
fn external_agent_messages_render_session_agent_labels() {
let (_term, handle, vt) = setup(120, 8);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&external_agent_message(
"manager_11111111",
"session-2",
"engineer_22222222",
"hello external",
));
renderer.handle(&Event::AgentMessageReceived(
tau_proto::AgentMessageReceived {
message_id: tau_proto::AgentMessageId::parse("msg-inbound-external")
.expect("test identifier must satisfy its grammar"),
sender_id: agent_id("reviewer_33333333"),
sender_session_id: Some(test_session_id("my_project-cafe-abc123")),
recipient_id: agent_id("manager_11111111"),
kind: tau_proto::AgentMessageKind::Message,
watch_provider_status: None,
watch_work_status: None,
watch_long_wait: None,
watch_lifecycle: None,
sender_notice: None,
recipient_notice: None,
message: "hello back".to_owned(),
},
));
sync(&handle);
assert!(vt.screen_contains(
120,
"Message from @manager_11111111 to session-2/@engineer_22222222:"
));
assert!(vt.screen_contains(
120,
"Message from my_project-cafe-abc123/@reviewer_33333333 to @manager_11111111:"
));
let lines = vt.screen_text(120);
let row = lines
.iter()
.position(|line| line.contains("my_project-cafe-abc123/@reviewer_33333333"))
.expect("external message header row") as u16;
let session_suffix = lines[row as usize]
.find("project-cafe")
.expect("session suffix") as u16;
let remote_agent = lines[row as usize]
.find("@reviewer_33333333")
.expect("remote agent id") as u16;
assert!(vt.cell_style(row, session_suffix).2);
assert!(vt.cell_style(row, remote_agent).2);
}
#[test]
fn late_agent_names_reproject_visible_message_blocks() {
let (_term, handle, vt) = setup(100, 8);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.apply_setting("show-messages", "all-full");
let message = agent_message("agent-a", "agent-b", "semantic body");
renderer.handle(&message);
sync(&handle);
assert!(vt.screen_contains(100, "Message from @agent-a to @agent-b:"));
let generation = vt.frame_generation();
renderer.handle(&Event::AgentDisplayNameSet(
tau_proto::AgentDisplayNameSet {
agent_id: agent_id("agent-b"),
display_name: "review result".to_owned(),
},
));
vt.wait_for_frame_containing_after(
generation,
"Message from @agent-a to @agent-b (review result):",
);
assert!(vt.screen_contains(100, "Message from @agent-a to @agent-b (review result):"));
assert!(!vt.screen_contains(100, "Message from @agent-a to @agent-b:"));
assert!(vt.screen_contains(100, "semantic body"));
}
#[test]
fn selected_agent_empty_quota_state_renders_unknown() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
let model = tau_proto::ModelId::from("chatgpt/gpt-5.6-sol");
renderer.handle(&Event::HarnessRoleSelected(HarnessRoleSelected {
model: Some("other/model".into()),
context_window: None,
role: "engineer".into(),
baseline_params: None,
model_params: tau_proto::ModelParams::default(),
}));
renderer.handle(&Event::AgentPromptStarted(tau_proto::AgentPromptStarted {
model: model.clone(),
..agent_prompt_started("quota-empty-sp", "quota-empty")
}));
renderer.handle(&Event::HarnessProviderQuotaChanged(
tau_proto::HarnessProviderQuotaChanged {
provider: model.provider.clone(),
profile_epoch: tau_proto::ProviderQuotaEpoch::parse("epoch-empty")
.expect("quota epoch"),
sequence: tau_proto::ProviderQuotaSequence::new(1),
windows: Vec::new(),
route_bindings: Vec::new(),
},
));
sync(&handle);
assert!(vt.screen_contains(80, "Q?"));
}
#[test]
fn standalone_compaction_terminals_clear_hidden_watched_activity() {
let (_term, handle, vt) = setup(100, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.switch_agent(agent_id("manager"));
renderer.handle(&Event::AgentWatchesUpdated(
tau_proto::AgentWatchesUpdated {
session_id: test_session_id("s1"),
watcher_id: agent_id("manager"),
watched_agent_ids: vec![agent_id("engineer")],
changed_agent_id: Some(agent_id("engineer")),
cause: tau_proto::AgentWatchUpdateCause::AgentStart,
},
));
let mut started = standalone_compaction_started("ct-side-success", "ap-side-success");
started.agent_id = agent_id("engineer");
renderer.handle(&Event::AgentStandaloneCompactionStarted(started));
let mut prompt = standalone_compaction_prompt_started("ap-side-success");
prompt.agent_id = agent_id("engineer");
renderer.handle(&Event::AgentPromptStarted(prompt));
sync(&handle);
assert_eq!(renderer.active_side_agent_count_for_test(), 1);
assert!(vt.screen_contains(100, "❓✨ @engineer"));
assert!(vt.screen_contains(100, "@1"));
renderer.handle(&Event::AgentCompacted(AgentCompacted {
original_input_tokens: None,
compaction_output_tokens: None,
agent_id: agent_id("engineer"),
transaction_id: tau_proto::CompactionTransactionId::parse("ct-side-success")
.expect("known-safe compaction transaction id"),
cut: tau_proto::AgentHead::Root,
suffix_end: tau_proto::AgentHead::Root,
compact_prompt_id: test_agent_prompt_id("ap-side-success"),
model: "test/model".parse().expect("model id"),
operation: tau_proto::PromptOperation::StandaloneCompaction,
replacement_window: Vec::new(),
}));
sync(&handle);
assert_eq!(renderer.active_side_agent_count_for_test(), 0);
assert!(vt.screen_contains(100, "❓💤 @engineer"));
assert!(!vt.screen_contains(100, "@1"));
let mut started = standalone_compaction_started("ct-side-failed", "ap-side-failed");
started.agent_id = agent_id("engineer");
renderer.handle(&Event::AgentStandaloneCompactionStarted(started));
let mut prompt = standalone_compaction_prompt_started("ap-side-failed");
prompt.agent_id = agent_id("engineer");
renderer.handle(&Event::AgentPromptStarted(prompt));
sync(&handle);
assert_eq!(renderer.active_side_agent_count_for_test(), 1);
assert!(vt.screen_contains(100, "❓✨ @engineer"));
assert!(vt.screen_contains(100, "@1"));
renderer.handle(&Event::AgentStandaloneCompactionFailed(
AgentStandaloneCompactionFailed {
agent_id: agent_id("engineer"),
transaction_id: tau_proto::CompactionTransactionId::parse("ct-side-failed")
.expect("known-safe compaction transaction id"),
cut: tau_proto::AgentHead::Root,
reason: tau_proto::StandaloneCompactionFailureReason::ProviderError,
resume_through: None,
context_retreat: None,
output_length_continuation: None,
incomplete_response: None,
},
));
sync(&handle);
assert_eq!(renderer.active_side_agent_count_for_test(), 0);
assert!(vt.screen_contains(100, "❓💤 @engineer"));
assert!(!vt.screen_contains(100, "@1"));
}
#[test]
fn watched_agent_blocks_are_sorted_by_agent_id() {
let (_term, handle, vt) = setup(100, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.switch_agent(agent_id("parent_1"));
renderer.handle(&Event::AgentWatchesUpdated(
tau_proto::AgentWatchesUpdated {
session_id: test_session_id("s1"),
watcher_id: agent_id("parent_1"),
watched_agent_ids: vec![agent_id("engineer_b"), agent_id("engineer_a")],
changed_agent_id: None,
cause: tau_proto::AgentWatchUpdateCause::AgentWatchEnable,
},
));
for watched in ["engineer_b", "engineer_a"] {
renderer.handle(&Event::AgentPromptStarted(tau_proto::AgentPromptStarted {
model_params: Some(tau_proto::ModelParams::default()),
outer_turn_id: None,
session_id: test_session_id("s1"),
agent_id: agent_id(watched),
agent_prompt_id: test_agent_prompt_id(format!("ap-{watched}-0")),
model: "test/model".parse().expect("model id"),
operation: tau_proto::PromptOperation::Inference,
originator: tau_proto::PromptOriginator::Extension {
name: tau_proto::ExtensionName::parse("__harness__")
.expect("test identifier must satisfy its grammar"),
query_id: format!("delegate-{watched}"),
},
ctx_id: None,
}));
}
sync(&handle);
let screen = vt.screen_text(100);
let first = screen
.iter()
.position(|line| line.contains("❓✨ @engineer_a"))
.expect("engineer_a running row");
let second = screen
.iter()
.position(|line| line.contains("❓✨ @engineer_b"))
.expect("engineer_b running row");
assert!(
first < second,
"watched-agent rows should be sorted by agent id: {screen:?}"
);
}
#[test]
fn turn_stats_and_session_stats_keep_token_scopes_separate() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.apply_setting("show-turn-stats", "true");
renderer.switch_agent(agent_id("worker-1"));
let mut first = finished_response_with_usage(
"worker-1-sp-0",
"worker-1",
100,
50,
10,
"first worker response",
);
first.usage.as_mut().expect("usage").stats.total = tau_proto::TokenUsageCounts {
sent_tokens: 100,
cached_tokens: 50,
received_tokens: 10,
..Default::default()
};
renderer.handle(&Event::ProviderResponseFinished(first));
let mut other_agent = finished_response_with_usage(
"worker-2-sp-0",
"worker-2",
700,
600,
80,
"other worker response",
);
other_agent.usage.as_mut().expect("usage").stats.total = tau_proto::TokenUsageCounts {
sent_tokens: 800,
cached_tokens: 650,
received_tokens: 90,
..Default::default()
};
renderer.handle(&Event::ProviderResponseFinished(other_agent));
let mut second = finished_response_with_usage(
"worker-1-sp-1",
"worker-1",
50,
20,
5,
"second worker response",
);
second.usage.as_mut().expect("usage").stats.total = tau_proto::TokenUsageCounts {
sent_tokens: 850,
cached_tokens: 670,
received_tokens: 95,
..Default::default()
};
renderer.handle(&Event::ProviderResponseFinished(second));
sync(&handle);
assert!(vt.screen_contains(80, "Σ↑70/150 ↓15"));
assert!(!vt.screen_contains(80, "Σ↑670/850 ↓95"));
assert_eq!(
renderer.session_token_stats_text(),
"session token totals: ↑670/850 ↓95"
);
renderer.show_session_token_stats();
sync(&handle);
assert!(vt.screen_contains(80, "session token totals: ↑670/850 ↓95"));
}
#[test]
fn turn_stats_retain_only_scalar_projection_with_exact_legacy_rendering() {
fn usage(
sent: u64,
cached: u64,
ceiling: Option<u64>,
received: u64,
model_count: usize,
) -> tau_proto::ProviderTokenUsage {
let mut usage = tau_proto::ProviderTokenUsage {
prompt_sent_tokens: sent,
prompt_cached_tokens: cached,
prompt_cache_read_ceiling_tokens: ceiling,
response_received_tokens: received,
stats: tau_proto::TokenUsageStats {
total: tau_proto::TokenUsageCounts {
sent_tokens: sent,
cached_tokens: cached,
received_tokens: received,
..Default::default()
},
..Default::default()
},
..Default::default()
};
for index in 0..model_count {
usage.stats.by_model.insert(
format!("provider/model-{index}")
.parse()
.expect("valid model id"),
tau_proto::TokenUsageCounts {
requests: index as u64,
sent_tokens: u64::MAX - index as u64,
in_progress_received_tokens: index as u64,
..Default::default()
},
);
}
usage
}
let first_usage = usage(0, 0, None, 0, 1_024);
let second_usage = usage(1_500, 1_000, None, 250, 2_048);
let cumulative = tau_proto::TokenUsageCounts {
sent_tokens: 1_500,
cached_tokens: 1_000,
received_tokens: 250,
..Default::default()
};
let expected_first = render_provider_turn_stats_block_with_cumulative_usage(
&cli_test_theme(),
&first_usage,
&tau_proto::TokenUsageCounts::default(),
None,
None,
Some(Duration::ZERO),
);
let expected_second = render_provider_turn_stats_block_with_cumulative_usage(
&cli_test_theme(),
&second_usage,
&cumulative,
Some(&first_usage),
None,
Some(Duration::ZERO),
);
let (_term, handle, vt) = setup(100, 30);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.switch_agent(agent_id("projection-agent"));
renderer.apply_setting("show-turn-stats", "true");
for (index, (prompt, usage)) in [
("projection-first", first_usage.clone()),
("projection-second", second_usage.clone()),
]
.into_iter()
.enumerate()
{
let mut finished = finished_response(prompt, Vec::new());
finished.agent_id = agent_id("projection-agent");
finished.usage = Some(usage);
renderer.handle_recorded_at(
&Event::ProviderResponseFinished(finished),
tau_proto::UnixMicros::new(1_700_000_000_000_000 + index as u64 * 3_600_000_000),
);
}
let retained = renderer.retained_turn_stats_blocks_for_test();
assert_eq!(
&retained[0].content.spans()[1..],
expected_first.content.spans()
);
assert_eq!(
&retained[1].content.spans()[1..],
expected_second.content.spans()
);
assert!(retained[0].content.spans()[0].text.ends_with(' '));
assert_eq!(retained[0].content.spans()[0].text.len(), 6);
use jiff::tz::TimeZone;
let zone = TimeZone::try_system().unwrap_or(TimeZone::UTC);
for (index, block) in retained.iter().enumerate() {
let instant =
jiff::Timestamp::from_microsecond(1_700_000_000_000_000 + index as i64 * 3_600_000_000)
.expect("recorded instant");
let local = instant.to_zoned(zone.clone());
assert_eq!(
block.content.spans()[0].text,
format!("{:02}:{:02} ", local.hour(), local.minute()),
"turn completion uses the recorded instant in local time"
);
}
let (entries, retained_bytes, entry_needs_drop, projection_bytes, projection_needs_drop) =
renderer.turn_stats_retention_evidence_for_test();
assert_eq!(entries, 2);
assert!(
retained_bytes <= 2 * 144,
"two complete retained entries must stay within 288 inline bytes, got {retained_bytes}"
);
assert!(
!entry_needs_drop,
"complete retained entries must own no heap values"
);
assert!(
projection_bytes <= 128,
"the scalar projection must stay within 128 inline bytes, got {projection_bytes}"
);
assert!(
!projection_needs_drop,
"retained projections must own no heap values"
);
let themed = tau_themes::Theme::parse(r#"{ styles: { "token.stats.input": { fg: "blue" } } }"#)
.expect("turn-stat theme parses");
let themed_expected = render_provider_turn_stats_block_with_cumulative_usage(
&themed,
&second_usage,
&cumulative,
Some(&first_usage),
None,
Some(Duration::ZERO),
);
renderer.apply_theme(themed);
assert_eq!(
&renderer.retained_turn_stats_blocks_for_test()[1]
.content
.spans()[1..],
themed_expected.content.spans()
);
renderer.switch_agent(agent_id("snapshot-other"));
renderer.switch_agent(agent_id("projection-agent"));
assert_eq!(
&renderer.retained_turn_stats_blocks_for_test()[1]
.content
.spans()[1..],
themed_expected.content.spans(),
"agent transcript snapshots must retain the same scalar projection"
);
renderer.apply_setting("show-turn-stats", "false");
sync(&handle);
assert!(!vt.screen_contains(100, "Δ"));
renderer.apply_setting("show-turn-stats", "true");
sync(&handle);
assert!(vt.screen_contains(100, "Δ! 1k/?"));
let (_cold_term, cold_handle, cold_vt) = setup(100, 30);
let mut cold = EventRenderer::new(
cold_handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
cold.switch_agent(agent_id("visible-agent"));
for (index, (prompt, usage)) in [
("projection-first", first_usage),
("projection-second", second_usage),
]
.into_iter()
.enumerate()
{
let mut finished = finished_response(prompt, Vec::new());
finished.agent_id = agent_id("projection-agent");
finished.usage = Some(usage);
cold.handle_recorded_at(
&Event::ProviderResponseFinished(finished),
tau_proto::UnixMicros::new(1_700_000_000_000_000 + index as u64 * 3_600_000_000),
);
}
cold.switch_agent(agent_id("projection-agent"));
cold.apply_setting("show-turn-stats", "true");
sync(&cold_handle);
assert_eq!(
&cold.retained_turn_stats_blocks_for_test()[1]
.content
.spans()[1..],
expected_second.content.spans()
);
assert_eq!(
cold.retained_turn_stats_blocks_for_test()[1]
.content
.spans()[0],
retained[1].content.spans()[0],
"cold replay preserves the original completion time"
);
assert!(cold_vt.screen_contains(100, "Δ! 1k/?"));
}
#[test]
fn cache_turn_stats_calibrate_after_exact_ceiling_observation() {
let backend = || tau_proto::ProviderBackend {
kind: tau_proto::ProviderBackendKind::Responses,
base_url: "https://chatgpt.com/backend-api".to_owned(),
transport: tau_proto::ProviderBackendTransport::Websocket,
stale_chain_fallback: false,
};
let usage = |sent, cached, ceiling, received| tau_proto::ProviderTokenUsage {
prompt_sent_tokens: sent,
prompt_cached_tokens: cached,
prompt_cache_read_ceiling_tokens: ceiling,
response_received_tokens: received,
..Default::default()
};
let (_term, handle, _vt) = setup(100, 30);
let mut renderer = EventRenderer::new(
handle,
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.switch_agent(agent_id("main"));
renderer.apply_setting("show-turn-stats", "true");
let mut first_prompt = agent_prompt_created("sol-before-observation", "s1");
first_prompt.model = "chatgpt-fedi/gpt-5.6-sol".parse().expect("Sol model");
renderer.handle(&Event::AgentPromptCreated(first_prompt));
let mut first = finished_response("sol-before-observation", Vec::new());
first.backend = Some(backend());
first.usage = Some(usage(23_757, 0, None, 166));
renderer.handle(&Event::ProviderResponseFinished(first));
let mut second_prompt = agent_prompt_created("sol-observation", "s1");
second_prompt.model = "chatgpt-fedi/gpt-5.6-sol".parse().expect("Sol model");
renderer.handle(&Event::AgentPromptCreated(second_prompt));
let mut second = finished_response("sol-observation", Vec::new());
second.backend = Some(backend());
second.usage = Some(usage(23_962, 23_552, Some(23_552), 138));
renderer.handle(&Event::ProviderResponseFinished(second));
let mut third_prompt = agent_prompt_created("sol-calibrated", "s1");
third_prompt.model = "chatgpt-fedi/gpt-5.6-sol".parse().expect("Sol model");
renderer.handle(&Event::AgentPromptCreated(third_prompt));
let mut third = finished_response("sol-calibrated", Vec::new());
third.backend = Some(backend());
third.usage = Some(usage(24_100, 23_680, None, 0));
renderer.handle(&Event::ProviderResponseFinished(third));
let mut fourth_prompt = agent_prompt_created("sol-calibrated-use", "s1");
fourth_prompt.model = "chatgpt-fedi/gpt-5.6-sol".parse().expect("Sol model");
renderer.handle(&Event::AgentPromptCreated(fourth_prompt));
let mut fourth = finished_response("sol-calibrated-use", Vec::new());
fourth.backend = Some(backend());
fourth.usage = Some(usage(24_300, 23_808, None, 0));
renderer.handle(&Event::ProviderResponseFinished(fourth));
let mut reconnect_prompt = agent_prompt_created("sol-reconnect", "s1");
reconnect_prompt.model = "chatgpt-fedi/gpt-5.6-sol".parse().expect("Sol model");
renderer.handle(&Event::AgentPromptCreated(reconnect_prompt));
let mut reconnect = finished_response("sol-reconnect", Vec::new());
reconnect.backend = Some(backend());
reconnect.ws_pool_delta = Some(tau_proto::WsPoolDelta {
upgrades: 1,
silent_reconnects: 0,
});
reconnect.usage = Some(usage(24_500, 24_064, None, 100));
renderer.handle(&Event::ProviderResponseFinished(reconnect));
let mut after_reconnect_prompt = agent_prompt_created("sol-after-reconnect", "s1");
after_reconnect_prompt.model = "chatgpt-fedi/gpt-5.6-sol".parse().expect("Sol model");
renderer.handle(&Event::AgentPromptCreated(after_reconnect_prompt));
let mut after_reconnect = finished_response("sol-after-reconnect", Vec::new());
after_reconnect.backend = Some(backend());
after_reconnect.usage = Some(usage(24_700, 24_192, None, 0));
renderer.handle(&Event::ProviderResponseFinished(after_reconnect));
let retained = renderer.retained_turn_stats_blocks_for_test();
let candidate_line = retained[2]
.content
.spans()
.iter()
.map(|span| span.text.as_str())
.collect::<String>();
assert!(candidate_line.contains(" Δ98%? 23.6k/24.1k? ↑0"));
let line = retained[3]
.content
.spans()
.iter()
.map(|span| span.text.as_str())
.collect::<String>();
assert!(line.contains(" Δ100%? 23.8k/23.8k? ↑492"));
let reconnect_line = retained[4]
.content
.spans()
.iter()
.map(|span| span.text.as_str())
.collect::<String>();
assert!(reconnect_line.contains(" Δ99%? 24k/24.3k? ↑200"));
let after_reconnect_line = retained[5]
.content
.spans()
.iter()
.map(|span| span.text.as_str())
.collect::<String>();
assert!(after_reconnect_line.contains(" Δ98%? 24.1k/24.6k? ↑100"));
}
#[test]
fn calibrated_turn_stats_do_not_reach_past_usage_less_terminal() {
for standalone in [false, true] {
let (_term, handle, _vt) = setup(100, 30);
let mut renderer = EventRenderer::new(
handle,
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.switch_agent(agent_id("main"));
renderer.apply_setting("show-turn-stats", "true");
let mut first_prompt = agent_prompt_created("astra-before-gap", "s1");
first_prompt.model = "chatgpt-fedi/gpt-6-astra".parse().expect("Astra model");
renderer.handle(&Event::AgentPromptCreated(first_prompt));
let mut first = finished_response("astra-before-gap", Vec::new());
first.backend = Some(tau_proto::ProviderBackend {
kind: tau_proto::ProviderBackendKind::Responses,
base_url: "https://chatgpt.com/backend-api".to_owned(),
transport: tau_proto::ProviderBackendTransport::Websocket,
stale_chain_fallback: false,
});
first.usage = Some(tau_proto::ProviderTokenUsage {
prompt_sent_tokens: 2_031,
response_received_tokens: 269,
..Default::default()
});
renderer.handle(&Event::ProviderResponseFinished(first));
let mut gap_prompt = agent_prompt_created("cache-gap", "s1");
gap_prompt.model = "other/model".parse().expect("other model");
if standalone {
gap_prompt.operation = tau_proto::PromptOperation::StandaloneCompaction;
}
renderer.handle(&Event::AgentPromptCreated(gap_prompt));
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"cache-gap",
Vec::new(),
)));
let mut final_prompt = agent_prompt_created("astra-after-gap", "s1");
final_prompt.model = "chatgpt-fedi/gpt-6-astra".parse().expect("Astra model");
renderer.handle(&Event::AgentPromptCreated(final_prompt));
let mut final_response = finished_response("astra-after-gap", Vec::new());
final_response.backend = Some(tau_proto::ProviderBackend {
kind: tau_proto::ProviderBackendKind::Responses,
base_url: "https://chatgpt.com/backend-api".to_owned(),
transport: tau_proto::ProviderBackendTransport::Websocket,
stale_chain_fallback: false,
});
final_response.usage = Some(tau_proto::ProviderTokenUsage {
prompt_sent_tokens: 2_300,
prompt_cached_tokens: 1_792,
..Default::default()
});
renderer.handle(&Event::ProviderResponseFinished(final_response));
let retained = renderer.retained_turn_stats_blocks_for_test();
let line = retained[1]
.content
.spans()
.iter()
.map(|span| span.text.as_str())
.collect::<String>();
assert!(
line.contains(" Δ! 1.7k/? ↑2.3k"),
"standalone={standalone}: {line}"
);
}
}
#[test]
fn foreground_ownership_failure_suppresses_top_level_terminal_report() {
let error = CliError::ForegroundOwnershipUnconfirmed {
message: "restore failed".to_owned(),
diagnostic: tau_cli_term::ForegroundRestorationDiagnostic::tcsetpgrp_unconfirmed(libc::EIO),
};
assert!(!error.should_report_to_terminal());
}
#[test]
fn static_mouse_setting_propagates_to_terminal_options() {
let mut settings = path_tau_config_settings::CliSettings::built_in();
settings.mouse = false;
assert_eq!(
terminal_options_from_settings(&settings),
tau_cli_term::TerminalOptions {
cursor_shape: tau_cli_term::CursorShape::Bar,
mouse: false,
}
);
}
#[test]
fn virtual_terminal_markdown_structure_inherits_transcript_colors() {
let theme = tau_themes::Theme::parse(
r##"{
styles: {
"user.prompt": { fg: "#f0f0f0", bg: "#101010" },
"agent.response": { fg: "#00d0d0", bg: "#101010" },
"markdown.heading": { bold: true },
"markdown.list.marker": { bold: true },
}
}"##,
)
.expect("valid VT Markdown theme");
let (_term, handle, vt) = setup(12, 8);
handle.print_output(
"markdown-user",
markdown_block(&theme, tau_themes::names::USER_PROMPT, "# User\n"),
);
handle.print_output(
"markdown-assistant",
markdown_block(
&theme,
tau_themes::names::AGENT_RESPONSE,
"12. assistant text wraps\n",
),
);
sync(&handle);
let rows = vt.screen_text(12);
let user_row = rows
.iter()
.position(|row| row.contains("# User"))
.expect("user heading row") as u16;
let user_offset = rows[user_row as usize]
.find("# User")
.expect("user heading column");
let user_col = rows[user_row as usize][..user_offset].chars().count() as u16;
let assistant_row = rows
.iter()
.position(|row| row.contains("12."))
.expect("assistant list row") as u16;
let assistant_offset = rows[assistant_row as usize]
.find("12.")
.expect("assistant list column");
let assistant_col = rows[assistant_row as usize][..assistant_offset]
.chars()
.count() as u16;
let continuation_row = rows
.iter()
.position(|row| row.contains("t text wraps"))
.expect("wrapped assistant continuation") as u16;
let continuation_offset = rows[continuation_row as usize]
.find("t text wraps")
.expect("wrapped assistant continuation column");
let continuation_col = rows[continuation_row as usize][..continuation_offset]
.chars()
.count() as u16;
let no_color = std::env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty());
assert_eq!(
vt.cell_style(user_row, user_col),
if no_color {
(vt100::Color::Default, vt100::Color::Default, true)
} else {
(
vt100::Color::Rgb(0xf0, 0xf0, 0xf0),
vt100::Color::Rgb(0x10, 0x10, 0x10),
true,
)
},
"rows={rows:?}, user row={user_row}, col={user_col}"
);
assert_eq!(
vt.cell_style(assistant_row, assistant_col),
if no_color {
(vt100::Color::Default, vt100::Color::Default, true)
} else {
(
vt100::Color::Rgb(0x00, 0xd0, 0xd0),
vt100::Color::Rgb(0x10, 0x10, 0x10),
true,
)
}
);
assert_eq!(
vt.cell_style(continuation_row, continuation_col),
if no_color {
(vt100::Color::Default, vt100::Color::Default, false)
} else {
(
vt100::Color::Rgb(0x00, 0xd0, 0xd0),
vt100::Color::Rgb(0x10, 0x10, 0x10),
false,
)
}
);
}
#[test]
fn compaction_lifecycle_notice_uses_info_style() {
let theme = cli_test_theme();
let lifecycle = render_harness_notice(
&theme,
&tau_proto::HarnessNotice::diagnostic(
tau_proto::notice_kind::HARNESS_NOTICE,
"Starting compaction request cr-35-0 for reviewer-sOqj (ct-35)",
tau_proto::NoticeLevel::Info,
),
);
assert_eq!(lifecycle.content.spans()[0].style.fg, Some(Color::Blue));
assert_eq!(
lifecycle
.content
.spans()
.iter()
.map(|span| span.text.as_str())
.collect::<String>(),
"□ Starting compaction request cr-35-0 for reviewer-sOqj (ct-35)"
);
}
#[test]
fn renderer_output_length_diagnostics_match_disposition_and_visible_output() {
let cases = [
(
Vec::new(),
tau_proto::OutputLengthDisposition::None,
"Output-token limit reached. No output was produced; check the request and server generation limits.",
None,
),
(
vec![ContextItem::ReasoningText(tau_proto::ReasoningTextItem {
kind: tau_proto::ReasoningTextKind::Full,
text: "retained private reasoning".to_owned(),
})],
tau_proto::OutputLengthDisposition::ContinuationPlanned {
outer_turn_id: tau_proto::AgentOuterTurnId::for_prompt(&test_agent_prompt_id(
"length-planned",
)),
successor_agent_prompt_id: test_agent_prompt_id("length-successor"),
ordinal: 1,
limit: 1,
},
"Output limit reached; continuing once from retained reasoning.",
None,
),
(
vec![ContextItem::ReasoningText(tau_proto::ReasoningTextItem {
kind: tau_proto::ReasoningTextKind::Full,
text: "terminal private reasoning".to_owned(),
})],
tau_proto::OutputLengthDisposition::None,
"Model reached its output-token limit before completing the turn. No assistant answer or executable tool call was produced.",
None,
),
(
vec![assistant_message_item("visible partial answer")],
tau_proto::OutputLengthDisposition::None,
"Model reached its output-token limit before completing the turn. The displayed response may be incomplete.",
Some("visible partial answer"),
),
(
vec![ContextItem::ToolCall(ToolCallItem {
call_id: "truncated-call".into(),
name: tau_proto::ToolName::new("read"),
tool_type: tau_proto::ToolType::Function,
arguments: CborValue::Map(Vec::new()),
raw_arguments_json: Some("{".to_owned()),
responses_envelope: None,
})],
tau_proto::OutputLengthDisposition::None,
"Model reached its output-token limit while producing a tool call. The incomplete call was not executed.",
None,
),
];
for (index, (output_items, disposition, warning, visible_output)) in
cases.into_iter().enumerate()
{
let (_term, handle, vt) = setup(160, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.apply_setting("show-thinking", "false");
let mut finished = finished_response(&format!("length-{index}"), output_items);
finished.stop_reason = ProviderStopReason::Length;
finished.output_length_disposition = disposition;
renderer.handle(&Event::ProviderResponseFinished(finished));
sync(&handle);
assert!(
eventually_screen_contains(&vt, 160, warning),
"missing output-limit warning in case {index}: {:?}",
vt.screen_text(160)
);
if let Some(output) = visible_output {
assert!(vt.screen_contains(160, output));
}
assert!(
!vt.screen_contains(160, "retained private reasoning")
&& !vt.screen_contains(160, "terminal private reasoning")
);
assert_eq!(renderer.test_active_tool_count(), 0);
}
}
#[test]
fn renderer_warns_on_each_empty_summary_output_limit() {
let (_term, handle, vt) = setup(160, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
for index in 0..2 {
renderer.handle(&Event::AgentStandaloneCompactionFailed(
AgentStandaloneCompactionFailed {
agent_id: agent_id("agent"),
transaction_id: tau_proto::CompactionTransactionId::parse(format!(
"ct-empty-{index}"
))
.expect("id"),
cut: tau_proto::AgentHead::Root,
reason: tau_proto::StandaloneCompactionFailureReason::OutputLengthExceeded,
resume_through: None,
context_retreat: None,
output_length_continuation: None,
incomplete_response: Some(Box::new(tau_proto::StandaloneCompactionIncomplete {
agent_prompt_id: test_agent_prompt_id(format!("ap-empty-{index}")),
output_items: Vec::new(),
usage: None,
provider_response_id: None,
provider_attempt: Default::default(),
backend: tau_proto::ProviderBackend {
kind: tau_proto::ProviderBackendKind::ChatCompletions,
base_url: "http://localhost/v1".to_owned(),
transport: Default::default(),
stale_chain_fallback: false,
},
})),
},
));
}
sync(&handle);
assert_eq!(
vt.screen_text(160)
.iter()
.filter(|line| line.contains("Summary output-token limit reached."))
.count(),
2
);
}
#[test]
fn markdown_table_response_events_preserve_raw_text_and_replay_projection() {
let source = concat!(
"| Scope | NativeReasoningEffort |\n",
"| --- | ---: |\n",
"| Formed 7-guardian federation, connected gateway, configured/advertising FLIP, log paths, working `fman-cli` | **4–7 engineer-days** |\n",
"| Complete FI-requested/funded liquidity and register the gateway in federation consensus | **8–15 days total** |\n",
"| Real `cloud-fman-telemetry` collection | **+3–6 days** |\n",
"| Throwaway demo script | **2–3 days**, but brittle |\n",
);
let finished = finished_response("sp-table", vec![assistant_message_item(source)]);
let raw_finished = finished.clone();
let (_term, handle, vt) = setup(160, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-table", "s1",
)));
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-table"),
source,
None,
tau_proto::PromptOriginator::User,
),
));
sync(&handle);
assert!(vt.screen_contains(160, "◇ | Scope"));
assert!(vt.screen_contains(160, " NativeReasoningEffort |"));
renderer.handle(&Event::ProviderResponseFinished(finished.clone()));
sync(&handle);
assert_eq!(
finished, raw_finished,
"rendering must not mutate event text"
);
assert!(vt.screen_contains(160, "◆ | Scope"));
let editor_context = renderer.editor_context();
assert_eq!(
editor_context
.lock()
.expect("editor context")
.last_response
.as_deref(),
Some(source)
);
let live_projection = renderer.final_semantic_projection_counts_for_test();
assert_eq!(live_projection.assistant_materializations, 1);
assert_eq!(live_projection.assistant_concat_allocations, 0);
let (_cold_term, cold_handle, cold_vt) = setup(160, 40);
let mut cold_renderer = EventRenderer::new(
cold_handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
let cold_generation = cold_vt.frame_generation();
cold_renderer.handle(&Event::ProviderResponseFinished(finished));
let cold_frame = cold_vt.wait_for_frame_containing_after(cold_generation, "◆ | Scope");
let cold_frames = cold_vt.frames.0.lock().expect("frames");
let settled = cold_frames[cold_frame - 1].join("\n");
assert!(settled.contains(" NativeReasoningEffort |"));
assert!(settled.contains("\n> "), "{settled}");
for frame in &cold_frames[cold_generation..cold_frame] {
let text = frame.join("\n");
assert!(
!text.contains("◆ | Scope") || text.contains("\n> "),
"cold replay published content without final status: {frame:?}"
);
}
drop(cold_frames);
assert_eq!(
cold_renderer
.editor_context()
.lock()
.expect("cold editor")
.last_response
.as_deref(),
Some(source)
);
let cold_projection = cold_renderer.final_semantic_projection_counts_for_test();
assert_eq!(
cold_projection, live_projection,
"live and cold-replayed editor finals must perform equivalent projection work"
);
assert!(cold_vt.screen_contains(160, "◆ | Scope"));
assert!(cold_vt.screen_contains(160, " NativeReasoningEffort |"));
}
#[test]
fn live_markdown_blank_line_seal_does_not_full_redraw_scrollback() {
let (_term, handle, vt) = setup(80, 8);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-md", "s1",
)));
for index in 0..24 {
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-md"),
format!("*line {index}*\n"),
None,
tau_proto::PromptOriginator::User,
),
));
}
sync(&handle);
assert!(vt.screen_contains(80, "*line 23*"));
let full_render_count = handle.full_render_count();
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-md"),
"\n",
None,
tau_proto::PromptOriginator::User,
),
));
sync(&handle);
assert_eq!(handle.full_render_count(), full_render_count);
}
#[test]
fn compact_message_fact_wraps_at_narrow_width_with_code_styled_publisher() {
let (_term, handle, vt) = setup(28, 20);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer
.agent_navigation()
.lock()
.expect("agent navigation lock")
.mark_live(agent_id("selected-agent"));
renderer.switch_agent(agent_id("selected-agent"));
renderer.handle(&Event::MessageDelivered(tau_proto::MessageDelivered::new(
tau_proto::MessagePublisherId::parse("fedi-slack")
.expect("canonical publisher id must satisfy the identifier grammar"),
tau_proto::MessageAgentTarget::new("selected-agent"),
tau_proto::MessageFactId::new("slack-message:opaque"),
tau_proto::MessageParty {
stable_id: "slack-sender:opaque".to_owned(),
display_name: Some("Dawid (dpc)".to_owned()),
sender_auth: None,
sender_trust: None,
},
Some(tau_proto::MessageConversation {
stable_id: "D123".to_owned(),
display_name: Some("dpc-dm".to_owned()),
alias: None,
}),
"Can you see this?",
)));
sync(&handle);
let rows = vt.screen_text(28);
assert!(rows.iter().any(|row| row.contains("External `fedi-slack`")));
assert!(rows.iter().any(|row| row.contains("Can you see this?")));
assert!(!rows.iter().any(|row| row.contains("slack-message:opaque")));
assert!(!rows.iter().any(|row| row.contains("slack-sender:opaque")));
assert!(!rows.iter().any(|row| row.contains("D123")));
assert!(!rows.iter().any(|row| row.contains("Text:")));
if !std::env::var_os("NO_COLOR").is_some_and(|value| !value.is_empty()) {
let publisher_row = rows
.iter()
.position(|row| row.contains("`fedi-slack`"))
.expect("publisher row") as u16;
let publisher_text = &rows[publisher_row as usize];
let publisher_byte = publisher_text.find("fedi-slack").expect("publisher column");
let publisher_col = publisher_text[..publisher_byte].chars().count() as u16;
let external_byte = publisher_text.find("External").expect("heading column");
let external_col = publisher_text[..external_byte].chars().count() as u16;
assert_ne!(
vt.cell_style(publisher_row, publisher_col).0,
vt.cell_style(publisher_row, external_col).0,
"publisher should use the inline-code foreground; rows={rows:?}"
);
}
}
#[test]
fn tree_notice_renders_multiline_result_without_reformatting() {
let (_term, handle, vt) = setup(240, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.switch_agent(agent_id("main"));
renderer.toggle_verbose_mode();
renderer.apply_setting("notice-level", "critical");
let expected = crate::test_support::TREE_PREVIEW_PARITY_NOTICE
.lines()
.collect::<Vec<_>>();
renderer.handle(&Event::HarnessNotice(tau_proto::HarnessNotice {
kind: tau_proto::notice_kind::HARNESS_NOTICE.into(),
message: crate::test_support::TREE_PREVIEW_PARITY_NOTICE.into(),
level: tau_proto::NoticeLevel::Info,
purpose: tau_proto::NoticePurpose::Response,
}));
sync(&handle);
let rows = vt.screen_text(240);
let mut previous_row = None;
let mut rendered_tree_rows = Vec::new();
for line in &expected {
let row = rows
.iter()
.position(|row| row.contains(line))
.unwrap_or_else(|| panic!("missing exact tree row {line:?} in {rows:?}"));
assert!(
previous_row.is_none_or(|previous| previous < row),
"tree rows are out of order: {rows:?}"
);
previous_row = Some(row);
rendered_tree_rows.push(
rows[row]
.strip_prefix("□ ")
.unwrap_or(&rows[row])
.trim_end(),
);
}
assert_eq!(rendered_tree_rows, expected);
assert_eq!(
rows.iter()
.filter(|row| row.contains("before first prompt") || row.contains("before prompt"))
.count(),
expected.len()
);
}
#[test]
fn thinking_renders_as_separate_block_above_response() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "hi".into(),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
renderer.handle(&Event::AgentPromptCreated(AgentPromptCreated {
model_params: tau_proto::ModelParams {
thinking_summary: tau_proto::ThinkingSummary::Auto,
..Default::default()
},
..agent_prompt_created("sp-0", "s1")
}));
sync(&handle);
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-0"),
String::new(),
Some("planning the answer".into()),
tau_proto::PromptOriginator::User,
),
));
sync(&handle);
assert!(
vt.screen_contains(80, "planning the answer"),
"thinking block should be live: {:?}",
vt.screen_text(80)
);
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-0"),
"actual answer",
None,
tau_proto::PromptOriginator::User,
),
));
sync(&handle);
assert!(vt.screen_contains(80, "actual answer"));
assert!(vt.screen_contains(80, "planning the answer"));
let live = vt.screen_text(80);
let live_thinking = live
.iter()
.position(|l| l.contains("planning the answer"))
.unwrap_or_else(|| panic!("live thinking missing: {live:?}"));
let live_response = live
.iter()
.position(|l| l.contains("actual answer"))
.unwrap_or_else(|| panic!("live response missing: {live:?}"));
assert!(
live_thinking < live_response,
"live thinking should render above live response (thinking @ {live_thinking}, response @ {live_response}); lines: {live:?}",
);
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-0",
vec![assistant_message_item("actual answer")],
)));
sync(&handle);
let lines = vt.screen_text(80);
let thinking_row = lines
.iter()
.position(|l| l.contains("planning the answer"))
.unwrap_or_else(|| panic!("thinking should remain in history: {lines:?}"));
let response_row = lines
.iter()
.position(|l| l.contains("actual answer"))
.unwrap_or_else(|| panic!("response should remain in history: {lines:?}"));
assert!(
thinking_row < response_row,
"thinking should render above response (thinking @ {thinking_row}, response @ {response_row}); lines: {lines:?}",
);
}
#[test]
fn no_thinking_block_when_summary_absent() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "hi".into(),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-0", "s1",
)));
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-0"),
"hello",
None,
tau_proto::PromptOriginator::User,
),
));
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-0",
vec![assistant_message_item("hello")],
)));
sync(&handle);
assert!(vt.screen_contains(80, "hello"));
}
#[test]
fn streaming_indicator_appends_during_updates() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-0", "s1",
)));
sync(&handle);
assert!(
!vt.screen_contains(80, "…"),
"prompt creation should not render provider-progress ellipsis before provider bytes: {:?}",
vt.screen_text(80)
);
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-0"),
"Hello",
None,
tau_proto::PromptOriginator::User,
),
));
sync(&handle);
assert!(vt.screen_contains(80, "Hello …"));
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-0",
vec![assistant_message_item("Hello")],
)));
sync(&handle);
assert!(vt.screen_contains(80, "Hello"));
assert!(!vt.screen_contains(80, "Hello …"));
}
#[test]
fn empty_provider_response_final_omits_synthetic_response_row() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-empty",
Vec::new(),
)));
sync(&handle);
assert!(!vt.screen_contains(80, "(provider returned an empty response)"));
}
#[test]
fn render_provider_error_from_non_context_field() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
let mut finished = finished_response("sp-error", Vec::new());
finished.stop_reason = ProviderStopReason::Error;
finished.error = Some("LLM error: boom".to_owned());
renderer.handle(&Event::ProviderResponseFinished(finished));
sync(&handle);
assert!(vt.screen_contains(80, "LLM error: boom"));
}
#[test]
fn standalone_compaction_terminal_failures_clear_private_progress() {
let (_term, handle, vt) = setup(100, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.switch_agent(agent_id("main"));
renderer.handle(&Event::AgentStandaloneCompactionStarted(
standalone_compaction_started("ct-failed", "ap-failed"),
));
renderer.handle(&Event::AgentPromptStarted(
standalone_compaction_prompt_started("ap-failed"),
));
let mut failed_activity = provider_response_delta_update(
test_agent_prompt_id("ap-failed"),
"",
None,
tau_proto::PromptOriginator::User,
);
failed_activity.compaction = Some(tau_proto::ProviderResponseCompactionUpdate {
status: tau_proto::ProviderResponseCompactionStatus::Started,
current: Some(3),
total: None,
original_input_tokens: None,
compaction_output_tokens: None,
});
renderer.handle(&Event::ProviderResponseUpdated(failed_activity.clone()));
renderer.handle(&Event::AgentStandaloneCompactionFailed(
AgentStandaloneCompactionFailed {
agent_id: agent_id("main"),
transaction_id: tau_proto::CompactionTransactionId::parse("ct-failed")
.expect("known-safe compaction transaction id"),
cut: tau_proto::AgentHead::Root,
reason: tau_proto::StandaloneCompactionFailureReason::ProviderError,
resume_through: None,
context_retreat: None,
output_length_continuation: None,
incomplete_response: None,
},
));
sync(&handle);
assert!(vt.screen_contains(100, "compact err: failed"));
assert!(!vt.screen_contains(100, "Compacting…"));
assert!(!renderer.agent_has_active_prompt_for_test(&agent_id("main")));
assert!(!renderer.main_agent_turn_active_for_test());
renderer.handle(&Event::ProviderResponseUpdated(failed_activity.clone()));
sync(&handle);
assert!(!vt.screen_contains(100, "3/3"));
assert!(!vt.screen_contains(100, "3/?"));
assert!(!renderer.agent_has_active_prompt_for_test(&agent_id("main")));
assert!(!renderer.main_agent_turn_active_for_test());
renderer.handle(&Event::AgentPromptStarted(
standalone_compaction_prompt_started("ap-terminated"),
));
failed_activity.agent_prompt_id = test_agent_prompt_id("ap-terminated");
renderer.handle(&Event::ProviderResponseUpdated(failed_activity.clone()));
renderer.handle(&Event::AgentPromptTerminated(AgentPromptTerminated {
automatic_compaction_decision: None,
agent_id: agent_id("main"),
agent_prompt_id: test_agent_prompt_id("ap-terminated"),
reason: AgentPromptTerminationReason::Canceled,
originator: tau_proto::PromptOriginator::User,
}));
sync(&handle);
assert!(vt.screen_contains(100, "compact err: stopped"));
assert!(!vt.screen_contains(100, "Compacting…"));
renderer.handle(&Event::ProviderResponseUpdated(failed_activity));
sync(&handle);
assert!(!vt.screen_contains(100, "3/3"));
assert!(!vt.screen_contains(100, "3/?"));
}
#[test]
fn malformed_standalone_lifecycle_does_not_hide_inference_stream() {
let (_term, handle, vt) = setup(100, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.switch_agent(agent_id("main"));
let mut malformed = standalone_compaction_started("ct-malformed", "ap-inference");
malformed.operation = tau_proto::PromptOperation::Inference;
renderer.handle(&Event::AgentStandaloneCompactionStarted(malformed));
renderer.handle(&Event::AgentPromptStarted(agent_prompt_started(
"ap-inference",
"s1",
)));
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("ap-inference"),
"ordinary inference answer",
Some("ordinary inference reasoning".to_owned()),
tau_proto::PromptOriginator::User,
),
));
sync(&handle);
assert!(vt.screen_contains(100, "ordinary inference answer"));
assert!(vt.screen_contains(100, "ordinary inference reasoning"));
}
#[test]
fn render_provider_compaction_update_as_compact_progress() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::ProviderResponseUpdated(ProviderResponseUpdated {
agent_prompt_id: test_agent_prompt_id("sp-compact"),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
deltas: Vec::new(),
compaction: Some(tau_proto::ProviderResponseCompactionUpdate {
current: None,
total: None,
status: tau_proto::ProviderResponseCompactionStatus::Started,
original_input_tokens: Some(226_200),
compaction_output_tokens: None,
}),
status: None,
response_stats: None,
originator: tau_proto::PromptOriginator::User,
}));
sync(&handle);
let progress = format!("compact #226.2k {}", tau_proto::PROGRESS_INDICATOR_TEXT);
assert!(vt.screen_contains(80, &progress));
assert!(!vt.screen_contains(80, "compacting"));
}
#[test]
fn render_provider_compaction_progress_after_long_response_in_visible_viewport() {
let (_term, handle, vt) = setup(80, 8);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
let agent_prompt_id = test_agent_prompt_id("sp-compact-long-response");
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
agent_prompt_id.clone(),
(0..20)
.map(|line| format!("streamed response line {line}"))
.collect::<Vec<_>>()
.join("\n"),
None,
tau_proto::PromptOriginator::User,
),
));
renderer.handle(&Event::ProviderResponseUpdated(ProviderResponseUpdated {
agent_prompt_id,
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
deltas: Vec::new(),
compaction: Some(tau_proto::ProviderResponseCompactionUpdate {
current: None,
total: None,
status: tau_proto::ProviderResponseCompactionStatus::Started,
original_input_tokens: Some(226_200),
compaction_output_tokens: None,
}),
status: None,
response_stats: None,
originator: tau_proto::PromptOriginator::User,
}));
sync(&handle);
let progress = format!("compact #226.2k {}", tau_proto::PROGRESS_INDICATOR_TEXT);
assert!(vt.screen_contains(80, &progress));
}
#[test]
fn render_provider_compaction_item_when_response_finishes() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
let mut finished = finished_response(
"sp-compact",
vec![ContextItem::Compaction(
OpaqueProviderItem::from_raw_json(r#"{"type":"compaction"}"#)
.expect("valid compaction item"),
)],
);
finished.compaction_original_input_tokens = Some(226_200);
finished.compaction_output_tokens = Some(4_500);
renderer.handle(&Event::ProviderResponseFinished(finished));
sync(&handle);
assert!(vt.screen_contains(80, "compact #226.2k → ? ok"));
assert!(!vt.screen_contains(80, "#4.5k"));
assert!(!vt.screen_contains(80, "compacted"));
}
#[test]
fn mixed_live_activity_blocks_keep_category_and_internal_order() {
for tools_arrive_first in [true, false] {
let (_term, handle, vt) = setup(100, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.switch_agent(agent_id("parent_1"));
let render_tools = |renderer: &mut EventRenderer| {
for call_id in ["read_one", "read_two"] {
renderer.handle(&Event::ToolStarted(tau_proto::ToolStarted {
invocation_policy: tau_proto::ToolInvocationPolicy::default(),
call_id: call_id.into(),
tool_name: tau_proto::ToolName::new(call_id),
arguments: CborValue::Null,
agent_id: agent_id("parent_1"),
originator: tau_proto::PromptOriginator::User,
}));
}
};
let render_watches = |renderer: &mut EventRenderer| {
renderer.handle(&Event::AgentWatchesUpdated(
tau_proto::AgentWatchesUpdated {
session_id: test_session_id("s1"),
watcher_id: agent_id("parent_1"),
watched_agent_ids: vec![agent_id("engineer_b"), agent_id("engineer_a")],
changed_agent_id: None,
cause: tau_proto::AgentWatchUpdateCause::AgentWatchEnable,
},
));
};
if tools_arrive_first {
render_tools(&mut renderer);
} else {
render_watches(&mut renderer);
}
for text in ["queued-one", "queued-two"] {
renderer.handle(&Event::AgentPromptQueued(AgentPromptQueued {
text: text.to_owned(),
agent_id: agent_id("parent_1"),
message_class: tau_proto::PromptMessageClass::User,
}));
}
if tools_arrive_first {
render_watches(&mut renderer);
} else {
render_tools(&mut renderer);
}
renderer.handle(&Event::AgentWatchesUpdated(
tau_proto::AgentWatchesUpdated {
session_id: test_session_id("s1"),
watcher_id: agent_id("parent_1"),
watched_agent_ids: vec![agent_id("engineer_a"), agent_id("engineer_b")],
changed_agent_id: Some(agent_id("engineer_a")),
cause: tau_proto::AgentWatchUpdateCause::AgentWatchEnable,
},
));
sync(&handle);
let screen = vt.screen_text(100);
let positions = [
"read_one 0s pending",
"read_two 0s pending",
"queued-one (queued)",
"queued-two (queued)",
"❓💤 @engineer_a",
"❓💤 @engineer_b",
]
.map(|needle| {
screen
.iter()
.position(|line| line.contains(needle))
.unwrap_or_else(|| panic!("missing `{needle}` in {screen:?}"))
});
assert!(
positions.windows(2).all(|pair| pair[0] < pair[1]),
"expected tools, queued prompts, then sorted watched engineers: {screen:?}"
);
}
}
#[test]
fn provider_terminal_finishes_harness_originated_message_progress() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
let originator = tau_proto::PromptOriginator::Extension {
name: tau_proto::ExtensionName::parse("__harness__").expect("extension name"),
query_id: "peer-auto-start".to_owned(),
};
let mut started = match tool_started(
"message-call",
"message",
CborValue::Map(vec![(
CborValue::Text("recipient_id".into()),
CborValue::Text("engineer".into()),
)]),
) {
Event::ToolStarted(started) => started,
_ => unreachable!("tool_started helper always returns ToolStarted"),
};
started.agent_id = agent_id("coordinator");
started.originator = originator.clone();
renderer.handle(&Event::ToolStarted(started));
renderer.handle(&Event::ToolProgress(tau_proto::ToolProgress {
call_id: "message-call".into(),
tool_name: tau_proto::ToolName::new("message"),
message: None,
progress: None,
display: Some(tau_proto::ToolUseState {
args: "engineer".into(),
status: tau_proto::ToolUseStatus::InProgress,
status_text: tau_proto::PROGRESS_INDICATOR_TEXT.into(),
..Default::default()
}),
}));
renderer.handle(&Event::ProviderToolResult(tau_proto::ToolResult {
presentation: Default::default(),
call_id: "message-call".into(),
tool_name: tau_proto::ToolName::new("message"),
tool_type: tau_proto::ToolType::Function,
result: CborValue::Text("Message sent".into()),
provider_content: Vec::new(),
kind: tau_proto::ToolResultKind::Final,
display: None,
originator,
}));
sync(&handle);
assert!(!vt.screen_contains(80, "message engineer"));
}
#[test]
fn streaming_block_does_not_duplicate_on_finish() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "hi".into(),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-0", "s1",
)));
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-0"),
"hello!",
None,
tau_proto::PromptOriginator::User,
),
));
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-0",
vec![assistant_message_item("hello!")],
)));
sync(&handle);
let count = vt
.screen_text(80)
.iter()
.filter(|r| r.contains("hello!"))
.count();
assert_eq!(
count,
1,
"response should appear exactly once, got {count}: {:?}",
vt.screen_text(80)
);
}
#[test]
fn workdir_result_modes_render_consistently() {
use tau_proto::{ToolUseState, ToolUseStatus};
let long_path = format!("/{}", "segment/".repeat(10));
let display = ToolUseState {
mode: "set".into(),
args: long_path,
status: ToolUseStatus::Success,
status_text: "ok".into(),
..Default::default()
};
let rendered = render_tool_use_state("project_a__workdir", &display);
let theme = cli_test_theme();
let block = render_tool_block(&theme, &rendered);
let cells = priority_header_cells(&block, 120);
let plain: String = cells.iter().map(|cell| cell.ch).collect();
let mode_start = plain.find(" set ").expect("structural set mode") + 1;
let mode_style = cells[mode_start].style;
assert!(plain.contains("/segment/"));
assert!(plain.contains('┄'));
assert!(plain.trim_end().ends_with(" ok"));
assert_eq!(
mode_style,
tau_cli_term::resolve::resolve(&theme, tau_themes::names::TOOL_MODE)
);
assert_ne!(
mode_style,
cells[plain.find("/segment").expect("compacted path")].style
);
}
#[test]
fn render_action_output_block_highlights_approval_ids_and_labels() {
let theme = cli_test_theme();
let block = render_action_output_block(
&theme,
"Incoming approval 7\nstatus: pending\n8 account=personal folder=INBOX\n",
);
let spans = block.content.spans();
let id_style = tau_cli_term::resolve::resolve(&theme, tau_themes::names::ACTION_ID);
let label_style = tau_cli_term::resolve::resolve(&theme, tau_themes::names::ACTION_LABEL);
let marker_style =
tau_cli_term::resolve::resolve(&theme, tau_themes::names::PROMPT_MARKER_SUBMITTED);
let heading_id = spans
.iter()
.find(|span| span.text == "7")
.expect("heading approval id span");
let row_id = spans
.iter()
.find(|span| span.text == "8")
.expect("list row approval id span");
let status_label = spans
.iter()
.find(|span| span.text == "status:")
.expect("status label span");
let account_label = spans
.iter()
.find(|span| span.text == "account=")
.expect("key-value label span");
assert_eq!(heading_id.style, id_style);
assert_eq!(row_id.style, id_style);
assert_eq!(status_label.style, label_style);
assert_eq!(account_label.style, label_style);
assert_eq!(spans[0].text, "□ ");
assert_eq!(spans[0].style, marker_style);
}
#[test]
fn render_action_error_block_uses_action_error_styles() {
let theme = cli_test_theme();
let block = render_action_error_block(&theme, "7", "invalid input");
let spans = block.content.spans();
let id_style = tau_cli_term::resolve::resolve(&theme, tau_themes::names::ACTION_ID);
let error_style = tau_cli_term::resolve::resolve(&theme, tau_themes::names::ACTION_ERROR);
let marker_style =
tau_cli_term::resolve::resolve(&theme, tau_themes::names::PROMPT_MARKER_SUBMITTED);
assert_eq!(spans[0].text, "□ ");
assert_eq!(spans[0].style, marker_style);
assert_eq!(spans[1].text, "7");
assert_eq!(spans[1].style, id_style);
assert_eq!(spans[3].text, "invalid input");
assert_eq!(spans[3].style, error_style);
}
#[test]
fn render_turn_stats_block_uses_dedicated_styles() {
let usage = tau_proto::ProviderTokenUsage {
prompt_sent_tokens: 1_000,
prompt_cached_tokens: 900,
prompt_cache_read_ceiling_tokens: Some(1_000),
response_received_tokens: 42,
stats: tau_proto::TokenUsageStats {
total: tau_proto::TokenUsageCounts {
sent_tokens: 2_000,
cached_tokens: 1_000,
received_tokens: 100,
..Default::default()
},
..Default::default()
},
..Default::default()
};
let previous_usage = tau_proto::ProviderTokenUsage {
prompt_sent_tokens: 1_000,
..Default::default()
};
let block =
render_turn_stats_block(&cli_test_theme(), &usage, Some(&previous_usage), None, None);
let spans = block.content.spans();
assert_eq!(spans[0].text, "Δ90%");
assert!(spans[0].style.bold);
assert_eq!(spans[0].style.fg, Some(Color::DarkGrey));
assert_eq!(spans[1].text, " 900/1k");
assert!(!spans[1].style.bold);
assert_eq!(spans[1].style.fg, Some(Color::Red));
let sigma = spans
.iter()
.find(|span| span.text == " Σ")
.expect("sigma span is rendered");
assert!(sigma.style.bold);
assert_eq!(sigma.style.fg, Some(Color::DarkGrey));
}
#[test]
fn render_turn_stats_block_warns_for_exact_99_percent_efficiency() {
let usage = tau_proto::ProviderTokenUsage {
prompt_sent_tokens: 20_100,
prompt_cached_tokens: 19_456,
prompt_cache_read_ceiling_tokens: Some(19_500),
stats: tau_proto::TokenUsageStats {
total: tau_proto::TokenUsageCounts {
sent_tokens: 40_100,
cached_tokens: 19_456,
..Default::default()
},
..Default::default()
},
..Default::default()
};
let previous_usage = tau_proto::ProviderTokenUsage {
prompt_sent_tokens: 19_500,
..Default::default()
};
let block =
render_turn_stats_block(&cli_test_theme(), &usage, Some(&previous_usage), None, None);
let spans = block.content.spans();
assert_eq!(spans[0].text, "Δ99%");
assert_eq!(spans[1].text, " 19.4k/19.5k");
assert_eq!(spans[1].style.fg, Some(Color::DarkYellow));
}
#[test]
fn render_turn_stats_block_warns_cache_hit_above_90_percent() {
let usage = tau_proto::ProviderTokenUsage {
prompt_sent_tokens: 10_100,
prompt_cached_tokens: 9_100,
prompt_cache_read_ceiling_tokens: Some(10_000),
stats: tau_proto::TokenUsageStats {
total: tau_proto::TokenUsageCounts {
sent_tokens: 20_100,
cached_tokens: 9_100,
..Default::default()
},
..Default::default()
},
..Default::default()
};
let previous_usage = tau_proto::ProviderTokenUsage {
prompt_sent_tokens: 10_000,
..Default::default()
};
let block =
render_turn_stats_block(&cli_test_theme(), &usage, Some(&previous_usage), None, None);
let spans = block.content.spans();
assert_eq!(spans[0].text, "Δ91%");
assert_eq!(spans[1].text, " 9.1k/10k");
assert_eq!(spans[1].style.fg, Some(Color::DarkYellow));
}
#[test]
fn render_turn_stats_block_highlights_cache_hit_at_or_below_90_percent() {
let usage = tau_proto::ProviderTokenUsage {
prompt_sent_tokens: 10_100,
prompt_cached_tokens: 9_000,
prompt_cache_read_ceiling_tokens: Some(10_000),
stats: tau_proto::TokenUsageStats {
total: tau_proto::TokenUsageCounts {
sent_tokens: 20_100,
cached_tokens: 9_000,
..Default::default()
},
..Default::default()
},
..Default::default()
};
let previous_usage = tau_proto::ProviderTokenUsage {
prompt_sent_tokens: 10_000,
..Default::default()
};
let block =
render_turn_stats_block(&cli_test_theme(), &usage, Some(&previous_usage), None, None);
let spans = block.content.spans();
assert_eq!(spans[0].text, "Δ90%");
assert_eq!(spans[1].text, " 9k/10k");
assert_eq!(spans[1].style.fg, Some(Color::Red));
}
#[test]
fn streaming_block_handles_each_trailing_case() {
let theme = cli_test_theme();
let cases = [
("", "…"),
("Hello", "Hello …"),
("Hello ", "Hello …"),
("Hello\t", "Hello\t…"),
("line\n", "line\n…"),
("line\n ", "line\n …"),
];
for (input, expected) in cases {
let block = streaming_block(&theme, tau_themes::names::AGENT_RESPONSE, input);
let actual: String = block
.content
.spans()
.iter()
.map(|s| s.text.as_str())
.collect();
assert_eq!(actual, expected, "input was {input:?}");
}
}
#[test]
fn emoji_in_response_renders_correctly() {
let (_term, handle, vt) = setup(40, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "hi".into(),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-0", "s1",
)));
let response = "Hello! 👋\n\nHow can I help you today?";
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-0"),
response,
None,
tau_proto::PromptOriginator::User,
),
));
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-0",
vec![assistant_message_item(response)],
)));
sync(&handle);
let text = vt.screen_text(40);
assert!(
vt.screen_contains(40, "Hello!"),
"emoji line missing, got: {:?}",
text
);
assert!(
text.iter().any(|r| r.starts_with("How can I help")),
"text after emoji should start at column 0, got: {:?}",
text
);
assert!(
vt.screen_contains(40, "> "),
"prompt missing, got: {:?}",
text
);
}
#[test]
fn multiple_emoji_no_column_drift() {
let (_term, handle, vt) = setup(40, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "hi".into(),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-0", "s1",
)));
let response = "🎉🎊🎈end\nnext line here";
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-0",
vec![assistant_message_item(response)],
)));
sync(&handle);
let text = vt.screen_text(40);
assert!(
text.iter().any(|r| r.starts_with("next line here")),
"line after emoji should start at col 0, got: {:?}",
text
);
}
#[test]
fn overflowing_stream_replaced_cleanly_on_finish() {
let (_term, handle, vt) = setup(40, 5);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "overflow please".into(),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-0", "s1",
)));
let partial = "stream 0\nstream 1\nstream 2\nstream 3\nPARTIAL ONLY";
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-0"),
partial,
None,
tau_proto::PromptOriginator::User,
),
));
sync(&handle);
assert!(
vt.screen_contains(40, "PARTIAL ONLY"),
"partial overflowed response should be visible before finish, got: {:?}",
vt.screen_text(40)
);
let final_text = "final 0\nfinal 1\nfinal 2";
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-0",
vec![assistant_message_item(final_text)],
)));
sync(&handle);
let text = vt.screen_text(40);
assert!(
vt.screen_contains(40, "final 1"),
"final response missing, got: {:?}",
text
);
assert!(
vt.screen_contains(40, "final 2"),
"final response tail missing, got: {:?}",
text
);
assert!(
!vt.screen_contains(40, "PARTIAL ONLY"),
"stale partial content should be gone, got: {:?}",
text
);
assert!(
vt.screen_contains(40, "> "),
"prompt should remain visible, got: {:?}",
text
);
}
#[test]
fn renderer_tracks_custom_prompts_from_harness_event() {
let (_term, handle, _vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle,
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
let prompt = tau_proto::HarnessCustomPrompt {
id: "review".to_owned(),
text: "Review this patch carefully".to_owned(),
};
renderer.handle(&Event::HarnessRolesAvailable(HarnessRolesAvailable {
roles: Vec::new(),
groups: Vec::new(),
custom_prompts: vec![prompt.clone()],
}));
let prompts = renderer.custom_prompts().lock().expect("prompts").clone();
assert_eq!(prompts, vec![prompt]);
}
#[test]
fn known_static_commands_are_identified_for_history_rendering() {
assert!(is_known_static_command(":model engineer"));
assert!(is_known_static_command(":set show-tools compact"));
assert!(is_known_static_command(":theme dpc"));
assert!(is_known_static_command(":session-stats"));
assert!(is_known_static_command(":debug-show-ui-event-stats"));
assert!(is_known_static_command(":debug-show-event-stats std-shell"));
assert!(is_known_static_command(":quit"));
assert!(is_known_static_command(":quit-session"));
assert!(is_known_static_command(":agent"));
assert!(is_known_static_command(":agent switch worker-1"));
assert!(is_known_static_command(":agent suspend"));
assert!(is_known_static_command(":agent resume worker-1"));
assert!(is_known_static_command(":agent new"));
assert!(is_known_static_command(":new"));
assert!(is_known_static_command(":name Current worker"));
assert!(is_known_static_command(":suspend"));
assert!(is_known_static_command(":resume"));
assert!(is_known_static_command(":new now"));
assert!(!is_known_static_command(":session new"));
assert!(is_known_static_command(":version"));
assert!(is_known_static_command(":version now"));
assert!(is_known_static_command(":skill jujutsu"));
assert!(is_known_static_command(":skill:jujutsu args"));
assert!(!is_known_static_command("/skillx jujutsu"));
assert!(!is_known_static_command("hello :model engineer"));
}
#[test]
fn submitted_prompt_markdown_styles_match_live_and_replay_without_mutating_raw_text() {
let theme = tau_themes::Theme::parse(
r##"{
styles: {
"user.prompt": { fg: "#f0f0f0", bg: "#101010" },
"markdown.strong": { bold: true },
"markdown.emphasis": { italic: true },
"markdown.code": { fg: "#00d000" },
"markdown.link": { fg: "#d00000", bold: true },
}
}"##,
)
.expect("valid submitted-prompt Markdown theme");
let source = "**strong** _emphasis_ `code` [link](https://example.test/docs)".to_owned();
for replayed in [false, true] {
let (_term, handle, vt) = setup(100, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
theme.clone(),
);
let event = if replayed {
Event::AgentPromptSubmitted(AgentPromptSubmitted {
inference_activation: false,
agent_id: agent_id("main"),
text: source.clone(),
trusted_internal_spans: Vec::new(),
message_class: tau_proto::PromptMessageClass::User,
internal_kind: None,
originator: tau_proto::PromptOriginator::User,
submission_source: tau_proto::PromptSubmissionSource::HumanUi,
display_name: None,
ctx_id: None,
})
} else {
Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: source.clone(),
agent_id: agent_id("main"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
})
};
renderer.handle(&event);
sync(&handle);
assert_eq!(
renderer.last_submitted_user_prompt_text_for_test(),
Some(source.as_str()),
"the {} projection must retain exact raw prompt bytes",
if replayed { "replayed" } else { "live" }
);
assert_eq!(
rendered_cell_attributes(&vt, 100, "**strong**"),
(
expected_rendered_color(vt100::Color::Rgb(0xf0, 0xf0, 0xf0)),
expected_rendered_color(vt100::Color::Rgb(0x10, 0x10, 0x10)),
true,
false,
false,
)
);
assert_eq!(
rendered_cell_attributes(&vt, 100, "_emphasis_"),
(
expected_rendered_color(vt100::Color::Rgb(0xf0, 0xf0, 0xf0)),
expected_rendered_color(vt100::Color::Rgb(0x10, 0x10, 0x10)),
false,
true,
false,
)
);
assert_eq!(
rendered_cell_attributes(&vt, 100, "`code`"),
(
expected_rendered_color(vt100::Color::Rgb(0x00, 0xd0, 0x00)),
expected_rendered_color(vt100::Color::Rgb(0x10, 0x10, 0x10)),
false,
false,
false,
)
);
assert_eq!(
rendered_cell_attributes(&vt, 100, "link"),
(
expected_rendered_color(vt100::Color::Rgb(0xd0, 0x00, 0x00)),
expected_rendered_color(vt100::Color::Rgb(0x10, 0x10, 0x10)),
true,
false,
false,
)
);
assert!(
!vt.screen_contains(100, "https://example.test/docs"),
"the display-only OSC 8 link projection must not replace retained raw text"
);
}
}
#[test]
fn delayed_prompt_started_does_not_duplicate_live_response_block() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-0"),
"hello",
None,
tau_proto::PromptOriginator::User,
),
));
renderer.handle(&Event::AgentPromptStarted(agent_prompt_started(
"sp-0", "s1",
)));
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-0"),
" world",
None,
tau_proto::PromptOriginator::User,
),
));
sync(&handle);
let lines = visible_lines(&vt, 80);
let response_lines = lines
.iter()
.filter(|line| line.contains("hello"))
.collect::<Vec<_>>();
assert_eq!(
response_lines.len(),
1,
"delayed prompt_started must not leave duplicate live response blocks: {lines:?}"
);
assert!(
response_lines[0].contains("hello world"),
"response should keep accumulating in the single live block: {lines:?}"
);
}
#[test]
fn submitted_prompt_projections_render_default_bright_white() {
let vt = render_submitted_prompt_projections(tau_themes::Theme::builtin());
for text in [
"immediate submitted prompt",
"promoted queued prompt",
"steered submitted prompt",
"replayed submitted prompt",
] {
assert_rendered_bright_white(&vt, 100, text);
}
}
#[test]
fn submitted_prompt_projections_render_dpc_bright_white() {
let vt = render_submitted_prompt_projections(tau_themes::Theme::builtin_dpc());
for text in [
"immediate submitted prompt",
"promoted queued prompt",
"steered submitted prompt",
"replayed submitted prompt",
] {
assert_rendered_bright_white(&vt, 100, text);
}
}
#[test]
fn timer_wakeup_prompt_submitted_renders_visible_marker() {
let (_term, handle, vt) = setup(100, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::SessionStarted(SessionStarted {
session_id: test_session_id("s1"),
reason: SessionStartReason::Initial,
}));
renderer.handle(&Event::AgentPromptSubmitted(AgentPromptSubmitted {
inference_activation: false,
agent_id: agent_id("engineer_abc12345"),
text: "Timer `wake` fired: stand up".to_owned(),
trusted_internal_spans: Vec::new(),
message_class: tau_proto::PromptMessageClass::Internal,
internal_kind: None,
originator: tau_proto::PromptOriginator::User,
submission_source: tau_proto::PromptSubmissionSource::HarnessInternal,
display_name: None,
ctx_id: Some("timer:wake:1".to_owned()),
}));
sync(&handle);
assert!(vt.screen_contains(100, "Timer `wake` woke this agent: stand up"));
assert!(!vt.screen_contains(100, "woke this agent: Timer `wake` fired"));
}
#[test]
fn timer_wakeup_prompt_steered_renders_visible_marker() {
let (_term, handle, vt) = setup(100, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::SessionStarted(SessionStarted {
session_id: test_session_id("s1"),
reason: SessionStartReason::Initial,
}));
renderer.handle(&Event::AgentPromptSteered(AgentPromptSteered {
self_compaction_terminal: None,
inference_activation: false,
submission_source: tau_proto::PromptSubmissionSource::HarnessInternal,
agent_id: agent_id("engineer_abc12345"),
text: "Timer `wake` fired: stand up".to_owned(),
trusted_internal_spans: Vec::new(),
message_class: tau_proto::PromptMessageClass::Internal,
internal_kind: None,
ctx_id: Some("timer:wake:2".to_owned()),
}));
sync(&handle);
assert!(vt.screen_contains(100, "Timer `wake` woke this agent: stand up"));
assert!(!vt.screen_contains(100, "woke this agent: Timer `wake` fired"));
}
#[test]
fn prompt_and_terminal_events_do_not_replace_navigation_snapshot() {
let (_term, handle, _vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle,
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::SessionStarted(SessionStarted {
session_id: test_session_id("s1"),
reason: SessionStartReason::Initial,
}));
renderer.handle(&Event::StartAgentAccepted(tau_proto::StartAgentAccepted {
start_id: tau_proto::StartOperationId(1),
query_id: "q-worker".to_owned(),
agent_id: agent_id("worker-1"),
}));
renderer.handle(&Event::AgentStarted(tau_proto::AgentStarted {
creator: Some(tau_proto::AgentCreator::default()),
parent_agent: None,
agent_id: agent_id("worker-1"),
role: "engineer".to_owned(),
display_name: None,
metadata: Vec::new(),
ephemeral: false,
}));
renderer.handle(&Event::AgentStatsUpdated(tau_proto::AgentStatsUpdated {
session_id: test_session_id("s1"),
agent_id: agent_id("worker-1"),
navigation_mode: tau_proto::AgentNavigationMode::ActiveAuto,
runtime_state: tau_proto::AgentRuntimeState::Running,
turn_activity: tau_proto::AgentTurnActivity::Idle,
tools: Default::default(),
context: Default::default(),
inner_turns_total: None,
estimated_api_cost: Default::default(),
creator_subtree_estimated_api_cost: Default::default(),
work_status: Default::default(),
}));
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "follow up".to_owned(),
agent_id: agent_id("worker-1"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
renderer.handle(&Event::AgentPromptSubmitted(AgentPromptSubmitted {
inference_activation: false,
agent_id: agent_id("worker-1"),
text: "follow up".to_owned(),
trusted_internal_spans: Vec::new(),
message_class: tau_proto::PromptMessageClass::User,
internal_kind: None,
originator: tau_proto::PromptOriginator::User,
submission_source: tau_proto::PromptSubmissionSource::HumanUi,
display_name: None,
ctx_id: None,
}));
renderer.handle(&Event::StartAgentResult(tau_proto::StartAgentResult {
query_id: "q-worker".to_owned(),
text: "done".to_owned(),
error: None,
}));
let navigation = renderer.agent_navigation();
let navigation = navigation.lock().expect("agent navigation");
assert_eq!(
navigation.mode(&agent_id("worker-1")),
AgentNavigationState::ActiveAuto
);
assert!(navigation.is_active(&agent_id("worker-1")));
drop(navigation);
renderer.handle(&Event::AgentStatsUpdated(tau_proto::AgentStatsUpdated {
session_id: test_session_id("s1"),
agent_id: agent_id("worker-1"),
navigation_mode: tau_proto::AgentNavigationMode::Active,
runtime_state: tau_proto::AgentRuntimeState::Idle,
turn_activity: tau_proto::AgentTurnActivity::Idle,
tools: Default::default(),
context: Default::default(),
inner_turns_total: None,
estimated_api_cost: Default::default(),
creator_subtree_estimated_api_cost: Default::default(),
work_status: Default::default(),
}));
let navigation = renderer.agent_navigation();
let navigation = navigation.lock().expect("agent navigation");
assert_eq!(
navigation.mode(&agent_id("worker-1")),
AgentNavigationState::Active
);
assert!(navigation.is_active(&agent_id("worker-1")));
}
#[test]
fn deselect_then_first_prompt_for_new_agent_does_not_inherit_prior_transcript() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::SessionStarted(tau_proto::SessionStarted {
session_id: test_session_id("s1"),
reason: tau_proto::SessionStartReason::Initial,
}));
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "agent one prompt".to_owned(),
agent_id: agent_id("agent-one"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
sync(&handle);
assert!(vt.screen_contains(80, "agent one prompt"));
renderer.clear_selected_agent();
sync(&handle);
assert!(!vt.screen_contains(80, "agent one prompt"));
renderer.switch_agent(agent_id("agent-two"));
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "agent two prompt".to_owned(),
agent_id: agent_id("agent-two"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
sync(&handle);
assert!(vt.screen_contains(80, "agent two prompt"));
assert!(!vt.screen_contains(80, "agent one prompt"));
}
#[test]
fn unknown_ordinary_prompt_termination_does_not_render_compaction() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::AgentPromptTerminated(AgentPromptTerminated {
automatic_compaction_decision: None,
agent_prompt_id: test_agent_prompt_id("sp-unknown"),
agent_id: agent_id("main"),
reason: AgentPromptTerminationReason::Stale,
originator: tau_proto::PromptOriginator::User,
}));
sync(&handle);
let text = vt.screen_text(80).join("\n");
assert!(!text.contains("compact"), "{text}");
}
#[test]
fn queued_prompt_renders_after_first_completes() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "first".into(),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-0", "s1",
)));
renderer.handle(&Event::AgentPromptQueued(AgentPromptQueued {
text: "second".into(),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
message_class: tau_proto::PromptMessageClass::User,
}));
sync(&handle);
assert!(
vt.screen_contains(80, "second (queued)"),
"queued indicator should show, got: {:?}",
vt.screen_text(80)
);
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-0",
vec![assistant_message_item("response one")],
)));
sync(&handle);
assert!(vt.screen_contains(80, "response one"));
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-1", "s1",
)));
sync(&handle);
assert!(
!vt.screen_contains(80, "(queued)"),
"queued indicator should be gone after dispatch, got: {:?}",
vt.screen_text(80)
);
assert!(
vt.screen_contains(80, "> second"),
"dispatched prompt should show normally, got: {:?}",
vt.screen_text(80)
);
assert_eq!(
vt.screen_text(80)
.iter()
.filter(|row| row.contains("second"))
.count(),
1,
"queued prompt should be promoted instead of duplicated, got: {:?}",
vt.screen_text(80)
);
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-1"),
"response two",
None,
tau_proto::PromptOriginator::User,
),
));
sync(&handle);
assert!(
vt.screen_contains(80, "response two"),
"second response should stream, got: {:?}",
vt.screen_text(80)
);
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-1",
vec![assistant_message_item("response two complete")],
)));
sync(&handle);
assert!(
vt.screen_contains(80, "response two complete"),
"final second response should show, got: {:?}",
vt.screen_text(80)
);
assert!(
vt.screen_contains(80, "response one"),
"first response should still show, got: {:?}",
vt.screen_text(80)
);
}
#[test]
fn three_queued_prompts_render_sequentially() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
for i in 0..3 {
if i == 0 {
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: format!("msg-{i}"),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-0", "s1",
)));
} else {
renderer.handle(&Event::AgentPromptQueued(AgentPromptQueued {
text: format!("msg-{i}"),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
message_class: tau_proto::PromptMessageClass::User,
}));
}
}
for i in 0..3 {
let spid: tau_proto::AgentPromptId = test_agent_prompt_id(format!("sp-{i}"));
if 0 < i {
renderer.handle(&Event::AgentPromptCreated(AgentPromptCreated {
agent_prompt_id: spid.clone(),
..agent_prompt_created("sp-ignore", "s1")
}));
}
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
spid.clone(),
format!("partial-{i}"),
None,
tau_proto::PromptOriginator::User,
),
));
renderer.handle(&Event::ProviderResponseFinished(finished_response(
spid.as_ref(),
vec![assistant_message_item(format!("response-{i}"))],
)));
sync(&handle);
}
sync(&handle);
for i in 0..3 {
assert!(
vt.screen_contains(80, &format!("response-{i}")),
"response-{i} should be visible, got: {:?}",
vt.screen_text(80)
);
}
assert!(
!vt.screen_contains(80, "…"),
"no '…' should remain, got: {:?}",
vt.screen_text(80)
);
}
#[test]
fn watched_agent_terminal_event_wins_over_delayed_prompt_start() {
let (_term, handle, vt) = setup(100, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.switch_agent(agent_id("parent_1"));
renderer.handle(&Event::AgentWatchesUpdated(
tau_proto::AgentWatchesUpdated {
session_id: test_session_id("s1"),
watcher_id: agent_id("parent_1"),
watched_agent_ids: vec![agent_id("engineer_1")],
changed_agent_id: Some(agent_id("engineer_1")),
cause: tau_proto::AgentWatchUpdateCause::AgentStart,
},
));
renderer.handle(&Event::ProviderResponseFinished(ProviderResponseFinished {
automatic_compaction_decision: None,
output_length_disposition: tau_proto::OutputLengthDisposition::None,
estimated_api_cost_rates: None,
estimated_api_cost_increment: None,
agent_prompt_id: test_agent_prompt_id("ap-engineer_1-0"),
agent_id: agent_id("engineer_1"),
output_items: Vec::new(),
stop_reason: ProviderStopReason::EndTurn,
error: None,
failure_kind: None,
context_limit_telemetry: None,
final_status_disposition: tau_proto::FinalStatusDisposition::Accepted,
recovery_disposition: tau_proto::ContextRecoveryDisposition::None,
originator: tau_proto::PromptOriginator::Extension {
name: tau_proto::ExtensionName::parse("__harness__")
.expect("test identifier must satisfy its grammar"),
query_id: "delegate-1".to_owned(),
},
usage: None,
compaction_original_input_tokens: None,
compaction_output_tokens: None,
backend: None,
provider_attempt: Default::default(),
provider_response_id: None,
ws_pool_delta: None,
}));
renderer.handle(&Event::AgentPromptStarted(tau_proto::AgentPromptStarted {
model_params: Some(tau_proto::ModelParams::default()),
outer_turn_id: None,
session_id: test_session_id("s1"),
agent_id: agent_id("engineer_1"),
agent_prompt_id: test_agent_prompt_id("ap-engineer_1-0"),
model: "test/model".parse().expect("model id"),
operation: tau_proto::PromptOperation::Inference,
originator: tau_proto::PromptOriginator::Extension {
name: tau_proto::ExtensionName::parse("__harness__")
.expect("test identifier must satisfy its grammar"),
query_id: "delegate-1".to_owned(),
},
ctx_id: None,
}));
renderer.handle(&Event::AgentPromptCreated(AgentPromptCreated {
agent_id: agent_id("engineer_1"),
originator: tau_proto::PromptOriginator::Extension {
name: tau_proto::ExtensionName::parse("__harness__")
.expect("test identifier must satisfy its grammar"),
query_id: "delegate-1".to_owned(),
},
..agent_prompt_created("ap-engineer_1-0", "s1")
}));
sync(&handle);
assert!(
vt.screen_contains(100, "❓💤 @engineer_1"),
"delayed start/create must retain, not reactivate, the status row: {:?}",
vt.screen_text(100)
);
}
#[test]
fn three_prompts_during_streaming_all_render_correctly() {
let (_term, handle, vt) = setup(80, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "hi".into(),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-0", "s1",
)));
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-0"),
"Hello",
None,
tau_proto::PromptOriginator::User,
),
));
sync(&handle);
assert!(
vt.screen_contains(80, "Hello"),
"streaming should show, got: {:?}",
vt.screen_text(80)
);
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "hi".into(),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
renderer.handle(&Event::AgentPromptQueued(AgentPromptQueued {
text: "hi".into(),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
message_class: tau_proto::PromptMessageClass::User,
}));
renderer.handle(&Event::UiPromptSubmitted(UiPromptSubmitted {
literal: false,
session_id: test_session_id("s1"),
text: "hi".into(),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
message_class: tau_proto::PromptMessageClass::User,
originator: tau_proto::PromptOriginator::User,
ctx_id: None,
}));
renderer.handle(&Event::AgentPromptQueued(AgentPromptQueued {
text: "hi".into(),
agent_id: tau_proto::AgentId::parse("main").expect("agent id"),
message_class: tau_proto::PromptMessageClass::User,
}));
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-0"),
"!\n\nHow can I help you today?",
None,
tau_proto::PromptOriginator::User,
),
));
sync(&handle);
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-0",
vec![assistant_message_item(
"Hello!\n\nHow can I help you today?",
)],
)));
sync(&handle);
assert!(
vt.screen_contains(80, "How can I help you today?"),
"response 1 should be in history, got: {:?}",
vt.screen_text(80)
);
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-1", "s1",
)));
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-1"),
"Hello again!\n\nHow can I help you?",
None,
tau_proto::PromptOriginator::User,
),
));
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-1",
vec![assistant_message_item(
"Hello again!\n\nHow can I help you?",
)],
)));
sync(&handle);
assert!(
vt.screen_contains(80, "How can I help you?"),
"response 2 should be visible, got: {:?}",
vt.screen_text(80)
);
renderer.handle(&Event::AgentPromptCreated(agent_prompt_created(
"sp-2", "s1",
)));
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("sp-2"),
"Hi there!\n\nWhat can I help you with?",
None,
tau_proto::PromptOriginator::User,
),
));
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"sp-2",
vec![assistant_message_item(
"Hi there!\n\nWhat can I help you with?",
)],
)));
sync(&handle);
assert!(
vt.screen_contains(80, "How can I help you today?"),
"response 1 missing, got: {:?}",
vt.screen_text(80)
);
assert!(
vt.screen_contains(80, "How can I help you?"),
"response 2 missing, got: {:?}",
vt.screen_text(80)
);
assert!(
vt.screen_contains(80, "What can I help you with?"),
"response 3 missing, got: {:?}",
vt.screen_text(80)
);
assert!(
vt.screen_contains(80, "> "),
"prompt should be visible after all responses, got: {:?}",
vt.screen_text(80)
);
assert!(
!vt.screen_contains(80, "…"),
"no '…' should remain, got: {:?}",
vt.screen_text(80)
);
}
#[test]
fn standalone_compaction_stream_is_hidden_from_cli_output() {
let (_term, handle, vt) = setup(100, 24);
let mut renderer = EventRenderer::new(
handle.clone(),
tau_cli_term::CompletionData::new(),
cli_test_theme(),
);
renderer.switch_agent(agent_id("main"));
renderer.handle(&Event::AgentStandaloneCompactionStarted(
standalone_compaction_started("ct-private", "ap-private"),
));
renderer.handle(&Event::ProviderPromptSubmitted(
tau_proto::ProviderPromptSubmitted {
agent_prompt_id: test_agent_prompt_id("ap-private"),
originator: tau_proto::PromptOriginator::User,
},
));
renderer.handle(&Event::ProviderResponseUpdated(
provider_response_delta_update(
test_agent_prompt_id("ap-private"),
"private compactor answer",
Some("private compactor reasoning".to_owned()),
tau_proto::PromptOriginator::User,
),
));
sync(&handle);
assert!(
vt.screen_contains(100, "private compactor answer"),
"the delayed typed start must remove output that earlier generic events rendered"
);
renderer.handle(&Event::AgentPromptStarted(
standalone_compaction_prompt_started("ap-private"),
));
sync(&handle);
assert!(vt.screen_contains(100, "compact Compacting…"));
assert!(!vt.screen_contains(100, "private compactor answer"));
assert!(!vt.screen_contains(100, "private compactor reasoning"));
let editor_context = renderer.editor_context();
let editor_context = editor_context.lock().expect("editor context");
assert!(editor_context.current_response.is_none());
assert!(editor_context.last_response.is_none());
drop(editor_context);
let mut activity = provider_response_delta_update(
test_agent_prompt_id("ap-private"),
"private compactor answer",
Some("private compactor reasoning".to_owned()),
tau_proto::PromptOriginator::User,
);
activity.compaction = Some(tau_proto::ProviderResponseCompactionUpdate {
status: tau_proto::ProviderResponseCompactionStatus::Completed,
current: Some(2),
total: None,
original_input_tokens: None,
compaction_output_tokens: None,
});
renderer.handle(&Event::ProviderResponseUpdated(activity));
sync(&handle);
assert!(vt.screen_contains(100, "Compacting… 2/?"));
assert!(!vt.screen_contains(100, "2/2"));
assert!(!vt.screen_contains(100, "private compactor"));
renderer.handle(&Event::ProviderResponseFinished(finished_response(
"ap-private",
Vec::new(),
)));
sync(&handle);
assert!(renderer.agent_has_active_prompt_for_test(&agent_id("main")));
assert!(renderer.main_agent_turn_active_for_test());
assert!(vt.screen_contains(100, "Compacting… 2/?"));
assert!(!vt.screen_contains(100, "2/2"));
renderer.handle(&Event::AgentCompacted(AgentCompacted {
original_input_tokens: Some(tau_proto::TokenCount::new(226_200)),
compaction_output_tokens: Some(tau_proto::TokenCount::new(4_500)),
agent_id: agent_id("main"),
transaction_id: tau_proto::CompactionTransactionId::parse("ct-private")
.expect("known-safe compaction transaction id"),
cut: tau_proto::AgentHead::Root,
suffix_end: tau_proto::AgentHead::Root,
compact_prompt_id: test_agent_prompt_id("ap-private"),
model: "test/model".parse().expect("model id"),
operation: tau_proto::PromptOperation::StandaloneCompaction,
replacement_window: vec![assistant_message_item("private checkpoint")],
}));
sync(&handle);
assert!(
vt.screen_contains(100, "compact #226.2k → ? ok 2/2"),
"{:?}",
vt.screen_text(100)
);
assert!(!vt.screen_contains(100, "#4.5k"));
assert!(!vt.screen_contains(100, "compact complete"));
assert!(!vt.screen_contains(100, "Compacting…"));
assert!(!vt.screen_contains(100, "private compactor answer"));
assert!(!vt.screen_contains(100, "private compactor reasoning"));
assert!(!vt.screen_contains(100, "private checkpoint"));
assert!(!renderer.agent_has_active_prompt_for_test(&agent_id("main")));
assert!(!renderer.main_agent_turn_active_for_test());
assert!(
!renderer
.agent_in_progress_state()
.load(std::sync::atomic::Ordering::Relaxed)
);
}