use std::os::unix::net as path_std_os_unix_net;
use std::time as path_std_time;
use tau_proto::{
Event, HarnessInputMessage, HarnessOutputMessage, NodeId, PromptOriginator, UiRoleUpdateAction,
UiTreeNavigationTarget,
};
use super::{
command_response_stdout_text, event_for_test_line, message_for_line, read_command_response,
run_send as run_send_typed,
};
use crate::test_support::TREE_PREVIEW_PARITY_NOTICE;
use crate::ui_prompt::DEFAULT_AGENT_ROLE;
const SESSION_ID: &str = "test-session";
fn event(text: &str) -> Option<Event> {
event_for_test_line(SESSION_ID, text)
}
fn message(text: &str) -> Option<HarnessInputMessage> {
message_for_line(SESSION_ID, text)
}
fn run_send(session_id: &str, line: &str) -> Result<(), crate::CliError> {
run_send_typed(
&session_id.parse().expect("test session id must be valid"),
line,
)
}
fn prompt_text(text: &str) -> String {
match event(text).expect("prompt event") {
Event::UiCreateAgent(req) => {
assert_eq!(req.session_id, SESSION_ID);
assert_eq!(req.role, DEFAULT_AGENT_ROLE);
assert_eq!(req.model_override, None);
assert_eq!(req.originator, PromptOriginator::User);
let ctx_id = req.ctx_id.as_deref().expect("prompt correlation id");
assert!(!ctx_id.is_empty());
assert_ne!(ctx_id, req.request_id);
req.initial_prompt.expect("initial prompt")
}
other => panic!("expected UiCreateAgent, got {other:?}"),
}
}
#[test]
fn quit_session_and_detach_are_no_ops() {
assert_eq!(event(":quit"), None);
assert_eq!(event(":quit-session"), None);
assert_eq!(event(":detach"), None);
}
#[test]
fn agent_picker_commands_are_exact_headless_no_ops() {
for command in [":pick-agent", ":pick-agent-all"] {
run_send("definitely-not-running", command).expect("exact picker command must be a no-op");
let malformed = format!("{command} unexpected");
let error = run_send("definitely-not-running", &malformed)
.expect_err("picker arguments must fail before daemon lookup");
assert!(error.to_string().contains("unknown or unsupported command"));
}
}
#[test]
fn session_stats_is_an_exact_headless_no_op() {
run_send("definitely-not-running", ":session-stats")
.expect("exact session stats command must be a no-op");
let error = run_send("definitely-not-running", ":session-stats unexpected")
.expect_err("session stats arguments must fail before daemon lookup");
assert!(error.to_string().contains("unknown or unsupported command"));
}
#[test]
fn cancel_requests_prompt_cancellation() {
match event(":cancel").expect("cancel event") {
Event::UiCancelPrompt(cancel) => {
assert_eq!(cancel.session_id, SESSION_ID);
assert_eq!(cancel.agent_prompt_id, None);
}
other => panic!("expected UiCancelPrompt, got {other:?}"),
}
}
#[test]
fn retry_requests_exact_delayed_prompt_release() {
match event(":retry").expect("retry event") {
Event::UiRetryPrompt(retry) => {
assert_eq!(retry.session_id, SESSION_ID);
assert_eq!(retry.target_agent_id, None);
assert_eq!(retry.agent_prompt_id, None);
}
other => panic!("expected UiRetryPrompt, got {other:?}"),
}
assert!(
!matches!(event(":retry later"), Some(Event::UiRetryPrompt(_))),
"arguments must not invoke retry"
);
assert!(matches!(event(":retry "), Some(Event::UiRetryPrompt(_))));
for malformed in [":retry\tlater", ":retry\nlater"] {
assert_eq!(
event(malformed),
None,
"malformed retry syntax must neither retry nor become a prompt"
);
}
}
#[test]
fn retry_extension_builds_dedicated_requests() {
for (line, expected) in [
(":retry-extension", None),
(
":retry-extension tool-a",
Some(tau_proto::ExtensionName::parse("tool-a").expect("valid extension name")),
),
] {
let HarnessInputMessage::UiRetryExtensionRequest(request) =
message(line).expect("retry extension message")
else {
panic!("expected dedicated retry extension request");
};
assert_eq!(request.extension_name, expected);
}
for malformed in [":retry-extension bad/name", ":retry-extension tool-a extra"] {
let error = run_send("definitely-not-running", malformed)
.expect_err("malformed request must fail before daemon lookup");
assert!(error.to_string().contains("usage: :retry-extension"));
}
}
#[test]
fn tree_commands_request_or_navigate_tree() {
match message(":tree").expect("tree message") {
HarnessInputMessage::UiTreeRequest(req) => assert_eq!(req.session_id, SESSION_ID),
other => panic!("expected UiTreeRequest message, got {other:?}"),
}
match event(":tree 42").expect("navigate event") {
Event::UiNavigateTree(req) => {
assert_eq!(req.session_id, SESSION_ID);
assert_eq!(req.target, UiTreeNavigationTarget::PromptAnchor(42));
}
other => panic!("expected UiNavigateTree, got {other:?}"),
}
match event(":tree 0").expect("root navigate event") {
Event::UiNavigateTree(req) => {
assert_eq!(req.session_id, SESSION_ID);
assert_eq!(req.target, UiTreeNavigationTarget::Root);
}
other => panic!("expected UiNavigateTree, got {other:?}"),
}
match event(":tree node 42").expect("raw node navigate event") {
Event::UiNavigateTree(req) => {
assert_eq!(req.session_id, SESSION_ID);
assert_eq!(req.target, UiTreeNavigationTarget::Node(NodeId::new(42)));
}
other => panic!("expected UiNavigateTree, got {other:?}"),
}
assert_eq!(event(":tree nope"), None);
}
#[test]
fn headless_tree_result_reads_one_multiline_notice() {
let (reader_stream, mut harness_stream) =
path_std_os_unix_net::UnixStream::pair().expect("reader stream pair");
reader_stream
.set_read_timeout(Some(path_std_time::Duration::from_secs(2)))
.expect("read timeout");
let (writer_stream, _discard_stream) =
path_std_os_unix_net::UnixStream::pair().expect("writer stream pair");
let (mut reader, _writer) =
crate::ui_client::connect_ui_streams(reader_stream, writer_stream, "tree-test", None)
.expect("connect UI streams");
let mut harness_writer = tau_proto::HarnessOutputWriter::new(&mut harness_stream);
harness_writer
.write_message(&HarnessOutputMessage::deliver_live(
tau_proto::UnixMicros::now(),
Event::HarnessNotice(tau_proto::HarnessNotice::diagnostic(
tau_proto::notice_kind::HARNESS_NOTICE,
"concurrent diagnostic",
tau_proto::NoticeLevel::Info,
)),
))
.expect("write concurrent diagnostic");
harness_writer
.write_message(&HarnessOutputMessage::deliver_live(
tau_proto::UnixMicros::now(),
Event::HarnessNotice(tau_proto::HarnessNotice {
kind: tau_proto::notice_kind::HARNESS_NOTICE.to_owned(),
message: TREE_PREVIEW_PARITY_NOTICE.to_owned(),
level: tau_proto::NoticeLevel::Info,
purpose: tau_proto::NoticePurpose::Response,
}),
))
.expect("write tree result");
harness_writer.flush().expect("flush tree result");
let result = read_command_response(&mut reader).expect("read tree result");
assert_eq!(result, TREE_PREVIEW_PARITY_NOTICE);
let stdout = command_response_stdout_text(&result);
assert_eq!(
stdout.as_bytes(),
format!("{TREE_PREVIEW_PARITY_NOTICE}\n").as_bytes()
);
assert!(
stdout
.chars()
.all(|character| character == '\n' || !character.is_control())
);
}
#[test]
fn compact_requests_compaction() {
match event(":compact").expect("compact event") {
Event::UiCompactRequest(req) => assert_eq!(req.session_id, SESSION_ID),
other => panic!("expected UiCompactRequest, got {other:?}"),
}
}
#[test]
fn local_configuration_commands_are_ignored() {
let command = ":fast";
assert_eq!(event(command), None);
assert!(
run_send("definitely-not-running", command).is_ok(),
"{command} must be a valid headless no-op"
);
}
#[test]
fn role_and_model_selection_commands_are_distinct() {
assert_eq!(event(":role"), None);
match event(":role reviewer").expect("role select") {
Event::UiRoleSelect(select) => assert_eq!(select.role, "reviewer"),
other => panic!("expected UiRoleSelect, got {other:?}"),
}
match event(":model test/model").expect("agent model select") {
Event::UiAgentModelSelect(select) => {
assert_eq!(select.session_id, SESSION_ID);
assert_eq!(select.target_agent_id, None);
assert_eq!(select.model.to_string(), "test/model");
}
other => panic!("expected UiAgentModelSelect, got {other:?}"),
}
assert_eq!(event(":model reviewer"), None);
assert_eq!(event(":model "), None);
match event(":effort 0.75").expect("agent effort select") {
Event::UiAgentEffortSelect(select) => {
assert_eq!(select.session_id, SESSION_ID);
assert_eq!(select.target_agent_id, None);
assert_eq!(select.effort, Some("0.75".parse().expect("effort")));
}
other => panic!("expected UiAgentEffortSelect, got {other:?}"),
}
assert_eq!(
event(":effort reset"),
Some(crate::ui_events::agent_effort_select(
&SESSION_ID.parse().expect("session id"),
None,
None,
))
);
assert_eq!(event(":effort 1.1"), None);
}
#[test]
fn role_delete_command_updates_roles() {
match event(":role scratch delete").expect("role update") {
Event::UiRoleUpdate(update) => {
assert_eq!(update.role, "scratch");
assert_eq!(update.action, UiRoleUpdateAction::Delete);
}
other => panic!("expected UiRoleUpdate, got {other:?}"),
}
}
#[test]
fn shell_commands_record_context_mode() {
match event("!! echo hi").expect("ui-only shell command") {
Event::UiShellCommand(command) => {
assert_eq!(command.session_id, SESSION_ID);
assert!(command.command_id.as_str().starts_with("ui-sh-"));
assert_eq!(command.command, "echo hi");
assert!(!command.include_in_context);
}
other => panic!("expected UiShellCommand, got {other:?}"),
}
match event("! echo hi").expect("context shell command") {
Event::UiShellCommand(command) => {
assert_eq!(command.session_id, SESSION_ID);
assert!(command.command_id.as_str().starts_with("ui-sh-"));
assert_eq!(command.command, "echo hi");
assert!(command.include_in_context);
}
other => panic!("expected UiShellCommand, got {other:?}"),
}
assert_eq!(event("!!"), None);
assert_eq!(event("!"), None);
}
#[test]
fn normal_text_submits_user_prompt() {
assert_eq!(prompt_text("explain this diff"), "explain this diff");
}
#[test]
fn old_slash_commands_submit_as_prompt_text() {
for line in ["/quit", "/tree", "/model test/model", "/skill demo"] {
assert_eq!(prompt_text(line), line);
}
}
#[test]
fn literal_colon_escape_submits_canonical_prompt_text() {
assert_eq!(prompt_text("::literal"), ":literal");
assert_eq!(prompt_text(" ::literal"), ":literal");
let Event::UiCreateAgent(request) = event("::skill demo").expect("literal prompt event") else {
panic!("expected create-agent prompt");
};
assert_eq!(request.initial_prompt.as_deref(), Some(":skill demo"));
assert!(request.literal);
}
#[test]
fn literal_gmail_oauth_finish_prompt_is_redacted() {
let Event::UiCreateAgent(request) = event(
" ::email auth google finish work \
http://127.0.0.1:54321/?code=PLACEHOLDER_CODE&state=PLACEHOLDER_STATE",
)
.expect("literal prompt event") else {
panic!("expected create-agent prompt");
};
let prompt = request.initial_prompt.as_deref().expect("initial prompt");
assert_eq!(prompt, ":email auth google finish <redacted>");
assert!(request.literal);
assert!(!prompt.contains("PLACEHOLDER_CODE"));
assert!(!prompt.contains("PLACEHOLDER_STATE"));
assert_eq!(
prompt_text(
"::email auth google start work \
http://127.0.0.1:54321/?code=PLACEHOLDER_CODE&state=PLACEHOLDER_STATE"
),
":email auth google start work \
http://127.0.0.1:54321/?code=PLACEHOLDER_CODE&state=PLACEHOLDER_STATE"
);
}
#[test]
fn unknown_colon_commands_are_not_prompt_events() {
assert_eq!(event(":unknown argument"), None);
assert_eq!(event(":tree invalid"), None);
let error = run_send("definitely-not-running", ":unknown argument")
.expect_err("unknown command must fail before daemon lookup");
assert!(error.to_string().contains("unknown or unsupported command"));
}
#[test]
fn malformed_headless_noop_commands_fail_locally() {
for line in [
":quit junk",
":new one two",
":ephemeral garbage",
":theme one two",
":set only-name",
":session nope",
":debug-show-event-stats",
":agent bogus",
] {
let error = run_send("definitely-not-running", line)
.expect_err("malformed command must fail before daemon lookup");
assert!(
error.to_string().contains("unknown or unsupported command"),
"{line}: {error}"
);
}
}