Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,48 @@
use crate::integration_testing::agent_mode::util::get_base_server_url;
use anyhow::Result;
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize)]
pub struct LLMGenerateRequest {
pub prompt: String,
pub user_messages: Vec<String>,
/// These are model IDs internal to warp-server.
/// See warp-server/logic/ai/llm/llm.go
pub model_id: String,
}
#[derive(Debug, Deserialize)]
pub struct ExactTokenUsage {
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_reads: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_writes: Option<i32>,
pub total_input: i32,
pub output: i32,
}
#[derive(Debug, Deserialize)]
pub struct LLMGenerateResponse {
pub content: String,
pub token_usage: ExactTokenUsage,
}
pub fn generate_llm_response(
client: &Client,
request: LLMGenerateRequest,
) -> Result<LLMGenerateResponse> {
let url = format!("{}/agent-mode-evals/llm_generate", get_base_server_url());
let response = client.post(&url).json(&request).send()?;
if !response.status().is_success() {
return Err(anyhow::anyhow!(
"Failed to generate LLM response: {}",
response.status()
));
}
let response = response.json::<LLMGenerateResponse>()?;
Ok(response)
}
@@ -0,0 +1,271 @@
pub mod llm_generate;
use anyhow::Result;
use llm_generate::LLMGenerateRequest;
use reqwest::blocking::Client;
use serde::Deserialize;
use warp_multi_agent_api::{
apply_file_diffs_result::success::UpdatedFileContent, message, Message,
};
use crate::ai::agent::conversation::AIConversation;
#[derive(Clone, Debug)]
pub struct LLMJudgeConfig {
pub prompt: String,
pub model: &'static str,
}
/// The LLM judge's response content will be directly unmarshalled into this struct.
/// Each judge's prompt must instruct the LLM to output its response in this format.
#[derive(Debug, Deserialize)]
pub struct LLMJudgeResult {
pub pass: bool,
pub critique: String,
}
pub struct LLMJudge {
config: LLMJudgeConfig,
client: Client,
}
impl LLMJudge {
pub fn new(config: LLMJudgeConfig) -> Self {
Self {
config,
client: Client::new(),
}
}
/// Format the conversation history to pass to the LLM judge.
/// TODO: support optional different formatting for different judges
fn format_conversation_history_for_llm_judge(conversation: &AIConversation) -> Result<String> {
let messages = conversation.all_linearized_messages();
// Filter out tool call result contents from the task's messages.
let filtered_messages = filter_tool_call_results_from_messages(messages.into_iter());
// Use debug format for now since we can't serialize protos to JSON.
let task_json = format!("{filtered_messages:?}");
Ok(task_json)
}
pub fn judge(&self, conversation: &AIConversation) -> Result<LLMJudgeResult> {
let formatted_conversation_history =
Self::format_conversation_history_for_llm_judge(conversation)?;
let request = LLMGenerateRequest {
prompt: self.config.prompt.to_owned(),
user_messages: vec![formatted_conversation_history],
model_id: self.config.model.to_owned(),
};
let response = llm_generate::generate_llm_response(&self.client, request)?;
// Parse the response content as JSON
let result: LLMJudgeResult = serde_json::from_str(&response.content)?;
Ok(result)
}
}
/// Filter out tool call result contents while preserving structure and success/error status
pub fn filter_tool_call_result(result: &message::ToolCallResult) -> message::ToolCallResult {
use message::tool_call_result::Result as ToolResult;
use warp_multi_agent_api::*;
let filtered_result = match &result.result {
Some(ToolResult::RunShellCommand(cmd_result)) =>
{
#[allow(deprecated)]
Some(ToolResult::RunShellCommand(RunShellCommandResult {
command: cmd_result.command.clone(),
output: Default::default(),
exit_code: Default::default(),
result: Some(
warp_multi_agent_api::run_shell_command_result::Result::CommandFinished(
warp_multi_agent_api::ShellCommandFinished {
command_id: "command_id".to_string(),
output: "[OUTPUT OMITTED]".to_string(),
exit_code: cmd_result.exit_code,
},
),
),
}))
}
Some(ToolResult::ReadFiles(read_result)) => {
use read_files_result::Result as ReadResult;
let filtered_read_result = match &read_result.result {
Some(ReadResult::TextFilesSuccess(success)) => {
// Keep file paths and line ranges but remove content
let filtered_files = success
.files
.iter()
.map(|file| FileContent {
file_path: file.file_path.clone(),
line_range: file.line_range,
content: "[CONTENT OMITTED]".to_string(),
})
.collect();
Some(ReadResult::TextFilesSuccess(
read_files_result::TextFilesSuccess {
files: filtered_files,
},
))
}
Some(ReadResult::AnyFilesSuccess(success)) => {
// Keep file paths and line ranges but remove content
let filtered_files = success
.files
.iter()
.map(|file| match &file.content {
Some(any_file_content::Content::TextContent(text_content)) => {
AnyFileContent {
content: Some(any_file_content::Content::TextContent(
warp_multi_agent_api::FileContent {
file_path: text_content.file_path.clone(),
content: "[CONTENT OMITTED]".to_string(),
line_range: text_content.line_range,
},
)),
}
}
Some(any_file_content::Content::BinaryContent(binary_content)) => {
AnyFileContent {
content: Some(any_file_content::Content::BinaryContent(
warp_multi_agent_api::BinaryFileContent {
file_path: binary_content.file_path.clone(),
data: vec![],
},
)),
}
}
None => unreachable!(
"AnyFileContent should always contain TextContent or BinaryContent"
),
})
.collect();
Some(ReadResult::AnyFilesSuccess(
read_files_result::AnyFilesSuccess {
files: filtered_files,
},
))
}
Some(ReadResult::Error(err)) => Some(ReadResult::Error(err.clone())),
None => None,
};
Some(ToolResult::ReadFiles(ReadFilesResult {
result: filtered_read_result,
}))
}
Some(ToolResult::SearchCodebase(search_result)) => {
use search_codebase_result::Result as SearchResult;
let filtered_search_result = match &search_result.result {
Some(SearchResult::Success(success)) => {
// Keep file paths and line ranges but remove file contents
let filtered_files = success
.files
.iter()
.map(|file| FileContent {
file_path: file.file_path.clone(),
line_range: file.line_range,
content: "[CONTENT OMITTED]".to_string(),
})
.collect();
Some(SearchResult::Success(search_codebase_result::Success {
files: filtered_files,
}))
}
Some(SearchResult::Error(err)) => Some(SearchResult::Error(err.clone())),
None => None,
};
Some(ToolResult::SearchCodebase(SearchCodebaseResult {
result: filtered_search_result,
}))
}
Some(ToolResult::ApplyFileDiffs(diff_result)) => {
use apply_file_diffs_result::Result as DiffResult;
let filtered_diff_result = match &diff_result.result {
Some(DiffResult::Success(success)) => {
// Keep file paths and line ranges but remove file contents
let filtered_files = success
.updated_files_v2
.iter()
.map(|file| UpdatedFileContent {
file: file.file.as_ref().map(|file_content| FileContent {
file_path: file_content.file_path.clone(),
line_range: file_content.line_range,
content: "[CONTENT OMITTED]".to_string(),
}),
was_edited_by_user: file.was_edited_by_user,
})
.collect();
Some(DiffResult::Success(apply_file_diffs_result::Success {
updated_files_v2: filtered_files,
..Default::default()
}))
}
Some(DiffResult::Error(err)) => Some(DiffResult::Error(err.clone())),
None => None,
};
Some(ToolResult::ApplyFileDiffs(ApplyFileDiffsResult {
result: filtered_diff_result,
}))
}
Some(ToolResult::Grep(grep_result)) => {
use grep_result::Result as GrepResult;
let filtered_grep_result = match &grep_result.result {
Some(GrepResult::Success(success)) => {
// Keep only file paths, remove all matched content and line numbers
let filtered_files = success
.matched_files
.iter()
.map(|file| {
grep_result::success::GrepFileMatch {
file_path: file.file_path.clone(),
// These can be very long so they're filtered, but we can't replace with a placeholder.
// The prompt should address this.
matched_lines: vec![],
}
})
.collect();
Some(GrepResult::Success(grep_result::Success {
matched_files: filtered_files,
}))
}
Some(GrepResult::Error(err)) => Some(GrepResult::Error(err.clone())),
None => None,
};
Some(ToolResult::Grep(warp_multi_agent_api::GrepResult {
result: filtered_grep_result,
}))
}
other => other.clone(),
};
message::ToolCallResult {
tool_call_id: result.tool_call_id.clone(),
context: None, // Remove context to reduce noise
result: filtered_result,
}
}
/// Filter out tool call result contents while preserving structure
pub fn filter_tool_call_results_from_messages<'a>(
messages: impl Iterator<Item = &'a Message>,
) -> Vec<Message> {
messages
.map(|msg| {
let mut filtered_msg = msg.clone();
if let Some(message::Message::ToolCallResult(ref tool_result)) = &msg.message {
filtered_msg.message = Some(message::Message::ToolCallResult(
filter_tool_call_result(tool_result),
));
}
filtered_msg
})
.collect()
}
@@ -0,0 +1,306 @@
mod assertions;
pub mod llm_judge;
mod step;
mod user_defaults;
mod util;
use std::collections::HashSet;
use std::fs::File;
use std::io::Write;
use crate::ai::agent::{AIAgentOutputStatus, FinishedAIAgentOutput};
pub use crate::ai::blocklist::agent_view::AgentViewState;
use crate::BlocklistAIHistoryModel;
use crate::{ai::agent::AIAgentActionType, integration_testing::view_getters::terminal_view};
pub use assertions::*;
pub use step::*;
pub use user_defaults::*;
pub use util::*;
use warpui::integration::PersistedDataMap;
pub use warpui::integration::RUNTIME_TAG_FAILURE_REASON;
use warpui::{App, SingletonEntity as _, WindowId};
pub const TOTAL_REQUEST_COST_PREFIX: &str = "Total request cost: ";
pub const TOTAL_EXCHANGES_PREFIX: &str = "Total number of exchanges: ";
pub const TOTAL_TOKEN_USAGE_PREFIX: &str = "Total token usage: ";
pub const RUNTIME_TAG_TOTAL_REQUEST_COST: &str = "total_request_cost";
pub const RUNTIME_TAG_TOTAL_EXCHANGES: &str = "total_exchanges";
pub const RUNTIME_TAG_TOKEN_USAGE_PREFIX: &str = "token_usage.";
const CODE_DIFF_OUTPUT_FILE_ENV_VAR: &str = "CODE_DIFF_OUTPUT_FILE";
pub fn output_code_diff_with_base_commit(
base_commit: &str,
working_dir: &str,
test_files_str: &str,
) {
use command::blocking::Command;
let Some(mut output_file) = open_debug_file_from_env(CODE_DIFF_OUTPUT_FILE_ENV_VAR) else {
log::error!("Could not open debug file from env");
return;
};
// Clear the test files from the diff, because we are not interested in seeing those.
log::debug!(
"[GIT OPERATION] mod.rs output_code_diff_with_base_commit git checkout {base_commit} -- {test_files_str}"
);
let _ = Command::new("git")
.args(["checkout", base_commit, "--", test_files_str])
.current_dir(working_dir)
.output();
log::debug!(
"[GIT OPERATION] mod.rs output_code_diff_with_base_commit git --no-pager diff {base_commit}"
);
let git_diff_output = Command::new("git")
.args(["--no-pager", "diff", base_commit])
.current_dir(working_dir)
.output();
write_git_diff_output_to_file(
git_diff_output,
&mut output_file,
&std::env::var(CODE_DIFF_OUTPUT_FILE_ENV_VAR)
.expect("Could not find diff output file env var"),
)
}
pub fn output_code_diff_debug_info(app: &mut App, window_id: WindowId) {
let terminal_view = terminal_view(app, window_id, 0, 0);
let Some(current_dir) = terminal_view.read(app, |terminal_view, _| terminal_view.pwd()) else {
log::error!("Could not get current directory");
return;
};
BlocklistAIHistoryModel::handle(app).update(app, |history_model, _| {
let Some(conversation) = history_model.active_conversation(terminal_view.id()) else {
return;
};
let mut edited_files = HashSet::new();
for exchange in conversation.all_exchanges().into_iter() {
let AIAgentOutputStatus::Finished { finished_output } = &exchange.output_status else {
continue;
};
if let FinishedAIAgentOutput::Success { output } = finished_output {
let agent_output = output.get();
for action in agent_output.actions() {
if let AIAgentActionType::RequestFileEdits { file_edits, .. } = &action.action {
for edit in file_edits {
if let Some(file) = edit.file() {
edited_files.insert(file.to_owned());
}
}
}
}
}
}
let mut output_file = open_debug_file_from_env(CODE_DIFF_OUTPUT_FILE_ENV_VAR);
if let Some(output_file) = &mut output_file {
use command::blocking::Command;
use std::io::Write;
if edited_files.is_empty() {
writeln!(output_file, "No files were edited for this test")
.expect("Failed to write to code diff file");
} else {
for file_name in edited_files {
log::debug!(
"[GIT OPERATION] mod.rs output_code_diff_debug_info git diff -- {file_name}"
);
let output = Command::new("git")
.args(["diff", "--", &file_name])
.current_dir(&current_dir)
.output();
write_git_diff_output_to_file(output, output_file, &file_name);
}
}
}
});
}
fn write_git_diff_output_to_file(
diff_output: std::io::Result<std::process::Output>,
file: &mut File,
file_name: &str,
) {
match diff_output {
Ok(output) if output.status.success() => {
let diff = String::from_utf8_lossy(&output.stdout);
writeln!(file, "Diff for file: {file_name}\n{diff}\n")
.expect("Failed to write diff to code diff file");
}
Ok(output) => {
let err = String::from_utf8_lossy(&output.stderr);
writeln!(
file,
"Failed to get diff for file: {file_name}\nGit error:\n{err}\n"
)
.expect("Failed to write error to code diff file");
}
Err(e) => {
writeln!(
file,
"Failed to run git diff for file: {file_name}\nError: {e}\n"
)
.expect("Failed to write command error to code diff file");
}
}
}
pub fn output_conversation_debug_info(
app: &mut App,
window_id: WindowId,
persisted_data: &mut PersistedDataMap,
) {
let terminal_view = terminal_view(app, window_id, 0, 0);
BlocklistAIHistoryModel::handle(app).update(app, |history_model, _| {
let Some(conversation) = history_model.active_conversation(terminal_view.id()) else {
return;
};
let mut output_file = open_debug_file_from_env("DEBUG_OUTPUT_FILE");
// Create a function to handle output
let mut write_to_debug_file = |text: &str| {
if let Some(file) = &mut output_file {
use std::io::Write;
writeln!(file, "{text}").expect("Failed to write to debug output file");
} else {
println!("{text}");
}
};
// Add timestamp to the header
let current_time = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ");
write_to_debug_file(&format!(
"========Conversation Debug Info (Generated: {current_time})========"
));
let debug_link = conversation
.server_conversation_token()
.map(|token| {
// The debug link within the container will be using host.docker.internal, but we're opening
// from outside the container.
// The server is configured to always write debug data to GCS instead of locally when run for evals, so we replace
// with staging.warp.dev instead of localhost:8080.
token
.debug_link()
.replace("host.docker.internal:8080", "staging.warp.dev")
})
.unwrap_or("unavailable".to_owned());
write_to_debug_file(&format!("Conversation Debug Link: {debug_link}"));
let total_request_cost = conversation.total_request_cost();
let total_exchanges = conversation.all_exchanges().len();
let token_usage = conversation.total_token_usage();
// Populate runtime tags with conversation data
persisted_data.insert(
RUNTIME_TAG_TOTAL_REQUEST_COST.to_string(),
total_request_cost.to_string(),
);
persisted_data.insert(
RUNTIME_TAG_TOTAL_EXCHANGES.to_string(),
total_exchanges.to_string(),
);
// Add token usage as separate runtime tags
for usage in token_usage.iter() {
persisted_data.insert(
format!(
"{}{}.total_input",
RUNTIME_TAG_TOKEN_USAGE_PREFIX, usage.model_id
),
usage.total_input.to_string(),
);
persisted_data.insert(
format!(
"{}{}.output",
RUNTIME_TAG_TOKEN_USAGE_PREFIX, usage.model_id
),
usage.output.to_string(),
);
persisted_data.insert(
format!(
"{}{}.input_cache_read",
RUNTIME_TAG_TOKEN_USAGE_PREFIX, usage.model_id
),
usage.input_cache_read.to_string(),
);
persisted_data.insert(
format!(
"{}{}.input_cache_write",
RUNTIME_TAG_TOKEN_USAGE_PREFIX, usage.model_id
),
usage.input_cache_write.to_string(),
);
persisted_data.insert(
format!(
"{}{}.cost_in_cents",
RUNTIME_TAG_TOKEN_USAGE_PREFIX, usage.model_id
),
usage.cost_in_cents.to_string(),
);
}
// Write to debug file for backward compatibility
write_to_debug_file(&format!(
"{TOTAL_REQUEST_COST_PREFIX}{total_request_cost}"
));
write_to_debug_file(&format!("{TOTAL_EXCHANGES_PREFIX}{total_exchanges}"));
write_to_debug_file(&format!(
"{TOTAL_TOKEN_USAGE_PREFIX}{}",
token_usage
.iter()
.map(|usage| format!(
"model_id={},total_input={},output={},input_cache_read={},input_cache_write={},cost_in_cents={}",
usage.model_id,
usage.total_input,
usage.output,
usage.input_cache_read,
usage.input_cache_write,
usage.cost_in_cents
))
.collect::<Vec<_>>()
.join("|")
));
write_to_debug_file("\nConversation Exchanges:\n");
for (i, exchange) in conversation.all_exchanges().into_iter().enumerate() {
// Add timestamp for each exchange
let exchange_time = chrono::DateTime::<chrono::Utc>::from(exchange.start_time)
.format("%Y-%m-%dT%H:%M:%S%.3fZ");
write_to_debug_file(&format!(
"\n--- Exchange {} (Started: {}) ---",
i + 1,
exchange_time
));
for input in &exchange.input {
write_to_debug_file(&format!("\nInput:\n\n{input}\n"));
}
write_to_debug_file(&format!("Output:\n{}", &exchange.output_status))
}
// Add completion timestamp
let completion_time = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ");
write_to_debug_file(&format!(
"\n========Debug Info Complete ({completion_time})=========="
));
})
}
// Get debug output file path from environment
pub fn open_debug_file_from_env(env_var: &str) -> Option<File> {
let file_path = std::env::var(env_var).ok();
if let Some(file_path) = &file_path {
// Clear the file if it exists
let _ = std::fs::remove_file(file_path);
Some(
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(file_path)
.expect("Failed to open debug output file"),
)
} else {
None
}
}
@@ -0,0 +1,251 @@
use std::{fs::read, io::Cursor, path::Path, time::Duration};
use prost::Message;
use warpui::{async_assert, integration::TestStep, SingletonEntity};
use crate::ai::execution_profiles::profiles::AIExecutionProfilesModel;
use crate::ai::execution_profiles::ActionPermission;
use crate::ai::llms::{LLMId, LLMPreferences};
use crate::integration_testing::agent_mode::ConversationTarget;
use crate::integration_testing::{
agent_mode::{assert_latest_task_succeeds_or_blocked, assert_task_is_blocked},
step::{new_step_with_default_assertions, new_step_with_default_assertions_for_pane},
terminal::assert_input_is_focused,
view_getters::terminal_view,
};
pub const AGENT_MODE_RUNNING_STEP_GROUP_NAME: &str = "Agent mode running";
use super::hydrate_ai_conversation_assertion;
/// Assumes that the terminal input is currently not in AI input mode.
pub fn enter_agent_view() -> TestStep {
new_step_with_default_assertions("Enter Agent View")
.with_keystrokes(&["ctrl-shift-enter"])
.add_named_assertion(
"Assert that we are in Agent View and AI input mode",
move |app, window_id| {
let terminal_view = terminal_view(app, window_id, 0, 0);
terminal_view.read(app, |terminal_view, app| {
let is_ai_input_mode = terminal_view
.input()
.read(app, |input, app| input.input_type(app).is_ai());
let agent_view_state = {
let model = terminal_view.model.lock();
model.block_list().agent_view_state().clone()
};
async_assert!(
is_ai_input_mode && agent_view_state.is_active(),
"Expected fullscreen Agent View + AI input mode, got agent_view_state={agent_view_state:?}, is_ai_input_mode={is_ai_input_mode}"
)
})
},
)
}
/// Assumes that the terminal input is currently in AI input mode.
pub fn exit_agent_view() -> TestStep {
new_step_with_default_assertions("Exit Agent View")
.with_keystrokes(&["escape"])
.add_named_assertion(
"Assert that we exited Agent View and are not in AI input mode",
move |app, window_id| {
let terminal_view = terminal_view(app, window_id, 0, 0);
terminal_view.read(app, |terminal_view, app| {
let is_ai_input_mode = terminal_view
.input()
.read(app, |input, app| input.input_type(app).is_ai());
let agent_view_state = {
let model = terminal_view.model.lock();
model.block_list().agent_view_state().clone()
};
async_assert!(
!is_ai_input_mode && !agent_view_state.is_active(),
"Expected inactive Agent View + non-AI input mode, got agent_view_state={agent_view_state:?}, is_ai_input_mode={is_ai_input_mode}"
)
})
},
)
}
/// Hydrates a conversation from a protobuf file.
/// File should be generated into the `input_data` directory.
/// See the agent_mode_eval README for more details.
pub fn hydrate_ai_conversation(file_name: &str) -> TestStep {
let file_bytes = get_input_data(file_name);
let Ok(request) = warp_multi_agent_api::Request::decode(file_bytes) else {
panic!("Failed to decode request from protobuf");
};
let tasks = request
.task_context
.map(|ctx| ctx.tasks)
.unwrap_or_default();
new_step_with_default_assertions("Hydrate AI conversation").add_named_assertion(
"Assert that conversation was hydrated successfully",
hydrate_ai_conversation_assertion(tasks),
)
}
/// Attach the latest block in the blocklist (command + output) to the AI query.
pub fn attach_recent_block_as_context() -> TestStep {
TestStep::new("Attach last block as context").add_named_assertion(
"Attach last block as context",
|app, window_id| {
let terminal_view = terminal_view(app, window_id, 0, 0);
terminal_view.update(app, |view, ctx| {
let last_index = {
let model = view.model.lock();
model.block_list().last_non_hidden_block_by_index()
};
if let Some(idx) = last_index {
view.integration_test_change_block_selection_to_single(idx, ctx);
}
});
terminal_view.read(app, |view, ctx| {
let count = view
.ai_context_model()
.as_ref(ctx)
.pending_context_block_ids()
.len();
async_assert!(
count == 1,
"Expected exactly 1 attached context block, got {count}"
)
})
},
)
}
// This will fail immediately on any error responses.
pub fn submit_ai_query_and_wait_until_done(query: &str, timeout: Duration) -> TestStep {
submit_ai_query(query, timeout)
.add_named_assertion(
"Assert the agent task is complete",
assert_latest_task_succeeds_or_blocked(ConversationTarget::Active, None),
)
.add_named_assertion(
"Assert that that input has been returned to the user",
assert_input_is_focused(),
)
}
/// Submits an AI query and waits until the task is blocked (waiting for user approval).
/// This is useful for tests where auto-execution is disabled and you want to verify
/// the command that would be executed without actually running it.
pub fn submit_ai_query_and_wait_until_blocked(query: &str, timeout: Duration) -> TestStep {
submit_ai_query(query, timeout).add_named_assertion(
"Assert the agent task is blocked",
assert_task_is_blocked(ConversationTarget::Active),
)
}
// Runs an AI query without waiting for anything.
// This is useful if you expect a specific sequence of responses (e.g. expect a certain command to be requested immediately),
// since it lets you make assertions on responses as they become ready and fail early instead of waiting for the agent to finish all its turns.
pub fn submit_ai_query(query: &str, timeout: Duration) -> TestStep {
new_step_with_default_assertions_for_pane(&format!("Enter AI query: {query}"), 0, 0)
.set_timeout(timeout)
.set_step_group_name(AGENT_MODE_RUNNING_STEP_GROUP_NAME)
.with_typed_characters(&[query])
.with_keystrokes(&["enter"])
.add_named_assertion(
"Print conversation ID to stdout",
print_conversation_id_assertion(),
)
}
/// Returns an assertion that prints the conversation ID to stdout once available.
/// This assertion will poll until the conversation token is received from the server.
fn print_conversation_id_assertion(
) -> impl FnMut(&mut warpui::App, warpui::WindowId) -> warpui::integration::AssertionOutcome {
|app, window_id| {
use crate::BlocklistAIHistoryModel;
use warpui::integration::AssertionOutcome;
let terminal_view = terminal_view(app, window_id, 0, 0);
BlocklistAIHistoryModel::handle(app).read(app, |history_model, _| {
if let Some(conversation) = history_model.active_conversation(terminal_view.id()) {
if let Some(token) = conversation.server_conversation_token() {
// The debug link within the container will be using host.docker.internal, but we're opening
// from outside the container.
let debug_link = token
.debug_link()
.replace("host.docker.internal", "localhost");
println!("Conversation ID (debug link): {debug_link}");
return AssertionOutcome::Success;
}
}
// If we don't have a conversation token yet, keep polling
AssertionOutcome::failure("Waiting for conversation token to be available".to_string())
})
}
}
/// Sets the preferred agent mode LLM. This is the base model for agent and inline AI conversations.
pub fn set_preferred_agent_mode_llm(llm_id: &str) -> TestStep {
let llm_id = LLMId::from(llm_id);
TestStep::new(&format!("Set preferred agent mode LLM to {llm_id}")).add_named_assertion(
"Update preferred agent mode LLM",
move |app, window_id| {
let llm_id = llm_id.clone();
let terminal_view_id = terminal_view(app, window_id, 0, 0).id();
LLMPreferences::handle(app).update(app, |llm_preferences, ctx| {
// Validate that the LLM ID is actually available. We only do this
// for the base model, since the coding and planning models are
// currently unused in the product.
assert!(
llm_preferences.is_available_agent_mode_llm(&llm_id),
"LLM ID '{llm_id}' is not a valid agent mode LLM",
);
llm_preferences.update_preferred_agent_mode_llm(&llm_id, terminal_view_id, ctx);
});
async_assert!(true, "Successfully updated preferred agent mode LLM")
},
)
}
/// Sets the preferred coding LLM. Note that the server currently ignores this.
pub fn set_preferred_coding_llm(llm_id: &str) -> TestStep {
let llm_id = LLMId::from(llm_id);
TestStep::new(&format!("Set preferred coding LLM to {llm_id}")).add_named_assertion(
"Update preferred coding LLM",
move |app, window_id| {
let llm_id = llm_id.clone();
let terminal_view_id = terminal_view(app, window_id, 0, 0).id();
LLMPreferences::handle(app).update(app, |llm_preferences, ctx| {
llm_preferences.update_preferred_coding_llm(&llm_id, Some(terminal_view_id), ctx);
});
async_assert!(true, "Successfully updated preferred coding LLM")
},
)
}
fn get_input_data(file_name: &str) -> Cursor<Vec<u8>> {
let input_data_dir = std::env::var("INPUT_DATA_DIR").expect(
"INPUT_DATA_DIR is not set. This is needed to hydrate conversations from eval tests.",
);
let path = Path::new(&input_data_dir).join(file_name);
Cursor::new(read(&path).expect("Failed to read binary input data"))
}
/// Sets the execution profile to not auto-execute commands.
/// This changes the `execute_commands` permission from `AlwaysAllow` to `AlwaysAsk`,
/// which means commands will be proposed but not automatically executed.
pub fn set_execution_profile_no_auto_execute() -> TestStep {
TestStep::new("Set execution profile to not auto-execute commands").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_execute_commands(
default_profile_id,
&ActionPermission::AlwaysAsk,
ctx,
);
});
async_assert!(true, "Successfully updated execution profile")
},
)
}
@@ -0,0 +1,46 @@
use std::collections::HashMap;
// User default keys
const IS_ACTIVE_AI_ENABLED: &str = "IsActiveAIEnabled";
const INTELLIGENT_AUTOSUGGESTIONS_ENABLED: &str = "IntelligentAutosuggestionsEnabled";
const NATURAL_LANGUAGE_AUTOSUGGESTIONS_ENABLED: &str = "NaturalLanguageAutosuggestionsEnabled";
const AGENT_MODE_QUERY_SUGGESTIONS_ENABLED: &str = "AgentModeQuerySuggestionsEnabled";
const CODE_SUGGESTIONS_ENABLED: &str = "CodeSuggestionsEnabled";
pub fn user_defaults_map_with_active_ai(enabled: bool) -> HashMap<String, String> {
HashMap::from_iter([
(
INTELLIGENT_AUTOSUGGESTIONS_ENABLED.to_owned(),
enabled.to_string(),
),
(
AGENT_MODE_QUERY_SUGGESTIONS_ENABLED.to_owned(),
enabled.to_string(),
),
(CODE_SUGGESTIONS_ENABLED.to_owned(), enabled.to_string()),
(
NATURAL_LANGUAGE_AUTOSUGGESTIONS_ENABLED.to_owned(),
enabled.to_string(),
),
(IS_ACTIVE_AI_ENABLED.to_owned(), enabled.to_string()),
])
}
/// User defaults for predictable AI input behavior needed in evals.
///
/// This allows tests to more reliably enter and exit AI input mode.
///
/// * UDI is enabled
/// * Natural language detection is disabled
pub fn user_defaults_map_for_ai_input() -> HashMap<String, String> {
HashMap::from_iter([
(
"AIAutoDetectionEnabled".to_owned(),
serde_json::to_string(&false).unwrap(),
),
(
"InputBoxTypeSetting".to_owned(),
serde_json::to_string("Universal").unwrap(),
),
])
}
@@ -0,0 +1,28 @@
/// Check that a CLI command contains a sequence of tokens in order.
/// Useful to ignore flags that may be present between tokens.
pub fn command_contains_sequence(cmd: &str, sequence: &[&str]) -> bool {
let mut required_index = 0;
for token in cmd.split_whitespace() {
if token == sequence[required_index] {
required_index += 1;
if required_index == sequence.len() {
return true;
}
}
}
false
}
pub fn is_running_in_docker() -> bool {
std::path::Path::new("/.dockerenv").exists()
}
pub fn get_base_server_url() -> String {
if is_running_in_docker() {
"http://host.docker.internal:8080".to_string()
} else {
"http://localhost:8080".to_string()
}
}