352 lines
12 KiB
Rust
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(_))
|
|
));
|
|
}
|