use super::{Transcript, apply_event};
use saya_agent::AgentEvent;
fn write_effect() -> Option<saya_agent::ToolEffect> {
Some(saya_agent::ToolEffect {
database_data: false,
external_side_effect: false,
requires_approval: false,
local_state: saya_agent::LocalStateEffect::WriteWorkspace,
})
}
fn two_write_calls() -> Vec<AgentEvent> {
vec![
AgentEvent::tool_requested(
"workspace_write",
serde_json::json!({"path": "notes.md", "content": "hi"}),
write_effect(),
),
AgentEvent::ToolCompleted {
name: "workspace_write".into(),
summary: "notes.md written".into(),
},
AgentEvent::tool_requested(
"workspace_write",
serde_json::json!({"path": "other.md", "content": "hi"}),
write_effect(),
),
AgentEvent::ToolCompleted {
name: "workspace_write".into(),
summary: "other.md written".into(),
},
]
}
#[test]
fn open_group_shows_per_call_lines_live_until_the_boundary() {
let mut transcript = Transcript::new();
for event in two_write_calls() {
apply_event(&mut transcript, event, false);
}
let texts: Vec<&str> = transcript
.blocks()
.iter()
.map(|b| b.text.as_str())
.collect();
assert_eq!(
texts,
vec![
"→ workspace_write: notes.md",
"✓ workspace_write: notes.md written",
"→ workspace_write: other.md",
"✓ workspace_write: other.md written",
],
"before the boundary the stream shows today's lines live"
);
apply_event(&mut transcript, AgentEvent::assistant_text("done"), false);
let texts: Vec<&str> = transcript
.blocks()
.iter()
.map(|b| b.text.as_str())
.collect();
assert_eq!(
texts,
vec![
"▸ 2 tool calls · ok — workspace_write notes.md, other.md",
"",
"done",
],
"the boundary collapses the run and the text follows"
);
}
#[test]
fn approvals_are_untouched() {
let mut transcript = Transcript::new();
apply_event(
&mut transcript,
AgentEvent::tool_requested(
"workspace_write",
serde_json::json!({"path": "a.md", "content": "hi"}),
write_effect(),
),
false,
);
apply_event(
&mut transcript,
AgentEvent::ToolCompleted {
name: "workspace_write".into(),
summary: "a.md written".into(),
},
false,
);
apply_event(
&mut transcript,
AgentEvent::ToolDenied {
name: "run_command".into(),
reason: "denied".into(),
},
false,
);
let texts: Vec<&str> = transcript
.blocks()
.iter()
.map(|b| b.text.as_str())
.collect();
assert_eq!(
texts,
vec![
"→ workspace_write: a.md",
"✓ workspace_write: a.md written",
"✗ run_command denied: denied",
],
"one-member groups stay verbatim and the denial is its own line"
);
assert!(
transcript.blocks().iter().all(|b| !b.is_collapsible()),
"the denial splits the run: no collapsible group may span it"
);
}
fn last_block_text(transcript: &Transcript) -> Option<&str> {
transcript.blocks().last().map(|b| b.text.as_str())
}
#[test]
fn tui_request_block_names_the_same_file_as_the_piped_line() {
use saya_agent::{LocalStateEffect, ToolEffect};
let mut transcript = Transcript::new();
apply_event(
&mut transcript,
AgentEvent::tool_requested(
"workspace_write",
serde_json::json!({"path": "notes.md", "content": "hi"}),
Some(ToolEffect {
database_data: false,
external_side_effect: false,
requires_approval: false,
local_state: LocalStateEffect::WriteWorkspace,
}),
),
false,
);
let block = last_block_text(&transcript).expect("a block was pushed");
assert!(
block.contains("notes.md"),
"the TUI request block must name the file: {block:?}"
);
}