Files
galaxy/app/src/ai/acp/response_translator_tests.rs
T

352 lines
12 KiB
Rust

use galaxy_acp::{
AcpEvent, ContentBlock, SessionId, StopReason, TextContent, ToolCallId, ToolCallStatus,
};
use warp_multi_agent_api::{client_action, message, response_event};
use super::AcpResponseTranslator;
#[test]
fn initializes_the_existing_chat_exchange_and_persists_user_text() {
let mut translator = AcpResponseTranslator::new(
"task".to_owned(),
true,
Some("hello".to_owned()),
"acp:codex".to_owned(),
);
let events = translator
.translate(AcpEvent::SessionStarted {
session_id: SessionId::from("session"),
can_load: true,
can_steer: true,
})
.expect("translate");
assert!(matches!(
events[0].r#type,
Some(response_event::Type::Init(_))
));
assert!(matches!(
events[1].r#type,
Some(response_event::Type::ClientActions(_))
));
assert!(matches!(
events[2].r#type,
Some(response_event::Type::ClientActions(_))
));
}
#[test]
fn streams_agent_text_as_add_then_append() {
let mut translator =
AcpResponseTranslator::new("task".to_owned(), false, None, "acp:codex".to_owned());
let first = translator
.translate(AcpEvent::AgentText {
text: "one".to_owned(),
})
.expect("first");
let second = translator
.translate(AcpEvent::AgentText {
text: " two".to_owned(),
})
.expect("second");
let Some(response_event::Type::ClientActions(first_actions)) = &first[1].r#type else {
panic!("expected first client action");
};
assert!(matches!(
first_actions.actions[0].action,
Some(client_action::Action::AddMessagesToTask(_))
));
let Some(response_event::Type::ClientActions(second_actions)) = &second[0].r#type else {
panic!("expected append client action");
};
assert!(matches!(
second_actions.actions[0].action,
Some(client_action::Action::AppendToMessageContent(_))
));
}
#[test]
fn renders_acp_tool_progress_as_text_not_an_executable_galaxy_action() {
let mut translator =
AcpResponseTranslator::new("task".to_owned(), false, None, "acp:codex".to_owned());
let events = translator
.translate(AcpEvent::ToolCall {
id: ToolCallId::from("tool-1"),
title: "Read file".to_owned(),
status: ToolCallStatus::InProgress,
output: None,
})
.expect("tool");
let Some(response_event::Type::ClientActions(actions)) = &events[1].r#type else {
panic!("expected client action");
};
let Some(client_action::Action::AddMessagesToTask(add)) = &actions.actions[0].action else {
panic!("expected display-only message");
};
assert!(matches!(
add.messages[0].message,
Some(message::Message::AgentOutput(_))
));
}
#[test]
fn maps_usage_and_successful_completion() {
let mut translator =
AcpResponseTranslator::new("task".to_owned(), false, None, "acp:codex".to_owned());
translator
.translate(AcpEvent::Usage {
used: 25,
size: 100,
cost: None,
})
.expect("usage");
let events = translator
.translate(AcpEvent::Finished {
stop_reason: StopReason::EndTurn,
})
.expect("finished");
let Some(finished) = events.iter().find_map(|event| {
let Some(response_event::Type::Finished(finished)) = &event.r#type else {
return None;
};
Some(finished)
}) else {
panic!("expected finished");
};
assert_eq!(finished.token_usage[0].total_input, 0);
assert_eq!(
finished
.conversation_usage_metadata
.as_ref()
.expect("metadata")
.context_window_usage,
0.25
);
}
#[test]
fn renders_bounded_tool_output_in_the_agent_transcript() {
let mut translator =
AcpResponseTranslator::new("task".to_owned(), false, None, "acp:codex".to_owned());
let events = translator
.translate(AcpEvent::ToolCall {
id: ToolCallId::from("tool-1"),
title: "Run tests".to_owned(),
status: ToolCallStatus::Completed,
output: Some("test one ... ok\ntest two ... ok".to_owned()),
})
.expect("tool");
let Some(response_event::Type::ClientActions(status_actions)) = &events[1].r#type else {
panic!("expected status action");
};
let Some(client_action::Action::AddMessagesToTask(add_status)) =
&status_actions.actions[0].action
else {
panic!("expected status message");
};
let Some(message::Message::AgentOutput(status)) = &add_status.messages[0].message else {
panic!("expected agent output");
};
assert!(status.text.contains("Run tests"));
let Some(response_event::Type::ClientActions(output_actions)) = &events[2].r#type else {
panic!("expected output action");
};
assert!(matches!(
output_actions.actions[0].action,
Some(client_action::Action::AppendToMessageContent(_))
));
}
#[test]
fn successful_turn_without_agent_output_is_still_visible() {
let mut translator =
AcpResponseTranslator::new("task".to_owned(), false, None, "acp:codex".to_owned());
let events = translator
.translate(AcpEvent::Finished {
stop_reason: StopReason::EndTurn,
})
.expect("finished");
assert!(events.iter().any(|event| {
let Some(response_event::Type::ClientActions(actions)) = &event.r#type else {
return false;
};
let Some(client_action::Action::AddMessagesToTask(add)) = &actions.actions[0].action else {
return false;
};
let Some(message::Message::AgentOutput(output)) = &add.messages[0].message else {
return false;
};
output.text.contains("completed without a text response")
}));
}
#[test]
fn suppresses_unsolicited_user_content_so_initial_hidden_context_cannot_leak() {
let mut translator =
AcpResponseTranslator::new("task".to_owned(), false, None, "acp:codex".to_owned());
let events = translator
.translate(AcpEvent::UserContent {
content: ContentBlock::Text(TextContent::new(
"hidden initial prompt and system context",
)),
})
.expect("translate");
assert!(events.is_empty());
}
#[test]
fn live_steering_adds_a_user_bubble_and_starts_a_new_assistant_bubble() {
let mut translator =
AcpResponseTranslator::new("task".to_owned(), false, None, "acp:codex".to_owned());
translator
.translate(AcpEvent::AgentText {
text: "original response".to_owned(),
})
.expect("initial output");
let steered = translator
.translate_steered_user_content(ContentBlock::Text(TextContent::new("stop at 75s")))
.expect("steering");
let Some(response_event::Type::ClientActions(user_actions)) = &steered[0].r#type else {
panic!("expected user client action");
};
let Some(client_action::Action::AddMessagesToTask(add_user)) = &user_actions.actions[0].action
else {
panic!("expected user message");
};
assert!(matches!(
add_user.messages[0].message,
Some(message::Message::UserQuery(_))
));
let resumed = translator
.translate(AcpEvent::AgentText {
text: "steered response".to_owned(),
})
.expect("resumed output");
let Some(response_event::Type::ClientActions(agent_actions)) = &resumed[0].r#type else {
panic!("expected agent client action");
};
assert!(matches!(
agent_actions.actions[0].action,
Some(client_action::Action::AddMessagesToTask(_))
));
}
#[test]
fn steering_failure_surfaces_an_indeterminate_delivery_warning() {
let mut translator =
AcpResponseTranslator::new("task".to_owned(), false, None, "acp:codex".to_owned());
translator
.translate(AcpEvent::SessionStarted {
session_id: SessionId::from("session"),
can_load: true,
can_steer: true,
})
.expect("initialize");
let events = translator.steering_failed("turn is no longer active");
assert_eq!(events.len(), 1);
let Some(response_event::Type::ClientActions(error_actions)) = &events[0].r#type else {
panic!("expected visible error action");
};
let Some(client_action::Action::AddMessagesToTask(add_error)) =
&error_actions.actions[0].action
else {
panic!("expected visible error message");
};
let Some(message::Message::AgentOutput(output)) = &add_error.messages[0].message else {
panic!("expected agent output");
};
assert!(output.text.contains("couldn't confirm"));
assert!(output.text.contains("before retrying"));
}
#[test]
fn implicit_steering_turn_warning_does_not_recommend_a_blind_retry() {
let mut translator =
AcpResponseTranslator::new("task".to_owned(), false, None, "acp:codex".to_owned());
let events = translator.steering_started_new_turn();
let text =
events
.iter()
.filter_map(|event| match &event.r#type {
Some(response_event::Type::ClientActions(actions)) => actions
.actions
.iter()
.find_map(|action| match &action.action {
Some(client_action::Action::AddMessagesToTask(add)) => add
.messages
.iter()
.find_map(|message| match &message.message {
Some(message::Message::AgentOutput(output)) => {
Some(output.text.as_str())
}
_ => None,
}),
_ => None,
}),
_ => None,
})
.collect::<String>();
assert!(text.contains("started"));
assert!(text.contains("terminated"));
assert!(text.contains("immediately"));
assert!(text.contains("may have begun acting"));
}
#[test]
fn startup_error_keeps_the_user_request_and_finishes_visibly() {
let mut translator = AcpResponseTranslator::new(
"task".to_owned(),
false,
Some("help me".to_owned()),
"acp:codex".to_owned(),
);
let events = translator.startup_error("adapter missing");
assert_eq!(events.len(), 4);
assert!(matches!(
events[0].r#type,
Some(response_event::Type::Init(_))
));
let Some(response_event::Type::ClientActions(user_actions)) = &events[1].r#type else {
panic!("expected visible user request");
};
let Some(client_action::Action::AddMessagesToTask(user_messages)) =
&user_actions.actions[0].action
else {
panic!("expected user message");
};
assert!(matches!(
user_messages.messages[0].message,
Some(message::Message::UserQuery(_))
));
let Some(response_event::Type::ClientActions(error_actions)) = &events[2].r#type else {
panic!("expected visible startup error");
};
let Some(client_action::Action::AddMessagesToTask(error_messages)) =
&error_actions.actions[0].action
else {
panic!("expected error message");
};
let Some(message::Message::AgentOutput(output)) = &error_messages.messages[0].message else {
panic!("expected agent output");
};
assert!(output.text.contains("adapter missing"));
assert!(matches!(
events[3].r#type,
Some(response_event::Type::Finished(_))
));
}