e2e testing

This commit is contained in:
2026-08-10 07:22:56 -05:00
parent fa43f723a5
commit 88ad290c7e
29 changed files with 1695 additions and 99 deletions
@@ -92,6 +92,99 @@ pub fn assert_latest_exchange_text(
})
}
/// Asserts that the active conversation created exactly one hidden leaf child
/// and that the child completed with the expected identity and output.
pub fn assert_single_hidden_child_agent_succeeds(
expected_agent_name: &'static str,
expected_output: &'static str,
) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = terminal_view(app, window_id, 0, 0);
BlocklistAIHistoryModel::handle(app).update(app, |history_model, _| {
let Some(parent) = history_model.active_conversation(terminal_view.id()) else {
return AssertionOutcome::failure("No active parent conversation".to_owned());
};
let parent_id = parent.id();
let children = history_model.child_conversations_of(parent_id);
let child = match children.as_slice() {
[] => {
return AssertionOutcome::failure(
"Waiting for the hidden child conversation".to_owned(),
);
}
[child] => *child,
_ => {
return AssertionOutcome::immediate_failure(format!(
"Expected exactly one child conversation, found {}",
children.len()
));
}
};
match child.status() {
ConversationStatus::Success => {}
ConversationStatus::InProgress
| ConversationStatus::TransientError
| ConversationStatus::WaitingForEvents => {
return AssertionOutcome::failure(format!(
"Waiting for child agent to succeed; current status: {:?}",
child.status()
));
}
ConversationStatus::Blocked { .. }
| ConversationStatus::Error
| ConversationStatus::Cancelled => {
return AssertionOutcome::immediate_failure(format!(
"Child agent finished unsuccessfully: {:?}",
child.status()
));
}
}
if child.agent_name() != Some(expected_agent_name) {
return AssertionOutcome::immediate_failure(format!(
"Expected child name {expected_agent_name:?}, found {:?}",
child.agent_name()
));
}
if child.parent_conversation_id() != Some(parent_id) {
return AssertionOutcome::immediate_failure(format!(
"Child {:?} was not linked to active parent {parent_id:?}",
child.id()
));
}
if !child.is_child_agent_conversation() || !child.should_exclude_from_navigation() {
return AssertionOutcome::immediate_failure(
"Child conversation was not hidden from normal navigation".to_owned(),
);
}
let grandchildren = history_model.child_conversations_of(child.id());
if !grandchildren.is_empty() {
return AssertionOutcome::immediate_failure(format!(
"Leaf child unexpectedly created {} grandchildren",
grandchildren.len()
));
}
let output = child
.all_exchanges()
.into_iter()
.filter_map(|exchange| exchange.output_status.output())
.map(|output| output.get().format_for_copy(None))
.filter(|text| !text.is_empty())
.collect::<Vec<_>>()
.join("\n\n");
if !output.contains(expected_output) {
return AssertionOutcome::immediate_failure(format!(
"Child output did not contain {expected_output:?}: {output}"
));
}
AssertionOutcome::Success
})
})
}
// Make an assertion on the action requested in the exchange at exchange_index.
/// This is private because `AIAgentActionType` is not public outside the warp app crate
/// for use within agent mode evals, so they can't write the `ActionAssertion` directly.
@@ -180,22 +273,41 @@ pub fn assert_any_exchange_text(
Box::new(move |app, window_id| {
let terminal_view = terminal_view(app, window_id, 0, 0);
BlocklistAIHistoryModel::handle(app).update(app, |history_model, _| {
let exchange_count = get_exchange_count(terminal_view.id(), history_model);
(0..exchange_count)
.map(|exchange_index| {
exchange_succeeds_with_expected_output(
Some(Box::new(assertion.clone())),
None,
ConversationTarget::Active,
terminal_view.id(),
exchange_index,
history_model,
)
})
.find(|outcome| matches!(outcome, AssertionOutcome::Success))
.unwrap_or(AssertionOutcome::failure(
"No exchanges match assertion".to_owned(),
))
let Some(conversation) = history_model.active_conversation(terminal_view.id()) else {
return AssertionOutcome::failure("No active conversation".to_owned());
};
let mut output_texts = Vec::with_capacity(conversation.exchange_count());
for exchange in conversation.all_exchanges() {
let AIAgentOutputStatus::Finished { finished_output } = &exchange.output_status
else {
return AssertionOutcome::failure(format!(
"Exchange {:?} is not finished",
exchange.id
));
};
match finished_output {
FinishedAIAgentOutput::Success { output } => {
let text = output.get().format_for_copy(None);
if assertion(&text) {
return AssertionOutcome::Success;
}
output_texts.push(text);
}
FinishedAIAgentOutput::Error { error, .. } => {
return AssertionOutcome::immediate_failure(format!(
"Exchange failed with error: {error:?}"
));
}
FinishedAIAgentOutput::Cancelled { .. } => {
return AssertionOutcome::immediate_failure(
"Exchange was cancelled".to_owned(),
);
}
}
}
AssertionOutcome::failure(format!(
"No exchanges match assertion. Exchange outputs: {output_texts:?}"
))
})
})
}
+15 -1
View File
@@ -9,7 +9,7 @@ use galaxyui::{async_assert, SingletonEntity};
use prost::Message;
use crate::ai::execution_profiles::profiles::AIExecutionProfilesModel;
use crate::ai::execution_profiles::ActionPermission;
use crate::ai::execution_profiles::{ActionPermission, RunAgentsPermission};
use crate::ai::llms::{LLMId, LLMPreferences};
use crate::ai::mcp::{
JsonTemplate, TemplatableMCPServer, TemplatableMCPServerInstallation,
@@ -284,6 +284,20 @@ pub fn set_execution_profile_auto_execute() -> TestStep {
)
}
/// Sets the execution profile to auto-run child agents.
pub fn set_execution_profile_auto_run_agents() -> TestStep {
TestStep::new("Set execution profile to auto-run child agents").add_named_assertion(
"Update execution profile",
|app, _window_id| {
AIExecutionProfilesModel::handle(app).update(app, |profiles, ctx| {
let default_profile_id = *profiles.default_profile(ctx).id();
profiles.set_run_agents(default_profile_id, RunAgentsPermission::AlwaysAllow, ctx);
});
async_assert!(true, "Successfully updated execution profile")
},
)
}
/// Sets the execution profile to auto-apply code diffs.
pub fn set_execution_profile_auto_apply_code_diffs() -> TestStep {
TestStep::new("Set execution profile to auto-apply code diffs").add_named_assertion(
@@ -807,8 +807,12 @@ pub fn assert_active_session_local_path(expected_path: &'static str) -> Assertio
}
pub fn assert_input_is_focused() -> AssertionCallback {
Box::new(|app, window_id| {
let terminal_view = single_terminal_view_for_tab(app, window_id, 0);
assert_input_is_focused_for_pane(0, 0)
}
pub fn assert_input_is_focused_for_pane(tab_index: usize, pane_index: usize) -> AssertionCallback {
Box::new(move |app, window_id| {
let terminal_view = terminal_view(app, window_id, tab_index, pane_index);
terminal_view.read(app, |view, ctx| {
let is_input_focused = view.input().as_ref(ctx).editor().as_ref(ctx).is_focused();
async_assert!(is_input_focused)