Files
galaxy/app/src/terminal/history_tests.rs
T
rkw6086 7c106eecd5 feat: expand Galaxy agent and remote tooling
Add Wormhole remote helpers, provider and agent improvements, filesystem diagnostics, model metadata support, and schema-aware settings IntelliSense.
2026-08-23 13:55:47 -05:00

1022 lines
36 KiB
Rust

use std::path::PathBuf;
use std::pin::pin;
use std::sync::Arc;
use chrono::Local;
use futures::future::join_all;
use futures::Future;
use futures_lite::StreamExt;
use galaxy_core::command::ExitCode;
use galaxyui::{App, ModelHandle};
use itertools::Itertools;
use super::{HistoryEntry, HistoryEvent, PersistedCommand, ShellHost};
use crate::ai::agent::conversation::AIConversationId;
use crate::terminal::model::block::{
AgentInteractionMetadata, SerializedAIMetadata, SerializedBlock,
};
use crate::terminal::model::bootstrap::BootstrapStage;
use crate::terminal::model::session::command_executor::testing::TestCommandExecutor;
use crate::terminal::model::session::{BootstrapSessionType, Session, SessionId, SessionInfo};
use crate::terminal::model::test_utils::TestBlockBuilder;
use crate::terminal::shell::ShellType;
use crate::terminal::History;
use crate::test_util::{Stub, VirtualFS};
impl History {
/// Returns a Future that completes when `History` is initialized for all sessions with IDs in
/// the given `session_ids` vector.
pub async fn initialized_sessions(
history_handle: &mut ModelHandle<History>,
app: &mut App,
session_ids: Vec<SessionId>,
) {
let mut history_initialization_receivers = vec![];
for session_id in session_ids {
let is_session_initialized = history_handle.read(app, |history, _| {
history.is_session_initialized(&session_id)
});
if !is_session_initialized {
let (tx, rx) = async_channel::unbounded();
let history_handle_clone = history_handle.clone();
app.update(|ctx| {
ctx.subscribe_to_model(&history_handle_clone, move |_, event, _| {
let HistoryEvent::Initialized(event_id) = event;
if session_id == *event_id {
let _ = tx.try_send(());
}
});
});
history_initialization_receivers.push(rx);
}
}
join_all(
history_initialization_receivers
.into_iter()
.map(|rx| async move { rx.recv().await }),
)
.await;
}
}
impl HistoryEntry {
fn with_session_id<S: Into<String>>(session_id: SessionId, command: S) -> Self {
Self {
session_id: Some(session_id),
exit_code: Some(ExitCode::from(0)),
command: command.into(),
workflow_id: None,
workflow_command: None,
pwd: None,
start_ts: None,
completed_ts: None,
git_head: None,
shell_host: None,
is_agent_executed: false,
is_for_restored_block: false,
}
}
pub fn with_pwd_and_exit_code<S: Into<String>>(
command: S,
pwd: S,
exit_code: impl Into<ExitCode>,
) -> Self {
Self {
session_id: None,
exit_code: Some(exit_code.into()),
command: command.into(),
workflow_id: None,
workflow_command: None,
pwd: Some(pwd.into()),
start_ts: None,
completed_ts: None,
git_head: None,
shell_host: None,
is_agent_executed: false,
is_for_restored_block: false,
}
}
}
#[test]
fn history_entry_for_restored_block_preserves_agent_execution() {
let mut block = TestBlockBuilder::new()
.with_bootstrap_stage(BootstrapStage::RestoreBlocks)
.build();
block.set_agent_interaction_mode_for_requested_command(
String::from("action-id").into(),
None,
AIConversationId::new(),
);
let entry = HistoryEntry::for_restored_block("ls".to_string(), &block);
assert!(entry.is_agent_executed);
}
#[test]
fn history_entry_for_restored_block_does_not_treat_all_agent_interactions_as_agent_execution() {
let mut block = TestBlockBuilder::new()
.with_bootstrap_stage(BootstrapStage::RestoreBlocks)
.build();
block.set_agent_interaction_mode(AgentInteractionMetadata::new(
None,
AIConversationId::new(),
None,
None,
false,
false,
));
let entry = HistoryEntry::for_restored_block("ls".to_string(), &block);
assert!(!entry.is_agent_executed);
}
#[test]
fn history_entry_for_completed_block_preserves_agent_execution() {
let ai_metadata = serde_json::to_string(&SerializedAIMetadata::from(
AgentInteractionMetadata::new_hidden(
String::from("action-id").into(),
AIConversationId::new(),
),
))
.unwrap();
let block = SerializedBlock {
ai_metadata: Some(ai_metadata),
..SerializedBlock::new_for_test("ls".as_bytes().to_vec(), vec![])
};
let entry = HistoryEntry::for_completed_block("ls".to_string(), &block);
assert!(entry.is_agent_executed);
}
#[test]
fn history_entry_for_completed_block_does_not_treat_all_agent_interactions_as_agent_execution() {
let ai_metadata = serde_json::to_string(&SerializedAIMetadata::from(
AgentInteractionMetadata::new(None, AIConversationId::new(), None, None, false, false),
))
.unwrap();
let block = SerializedBlock {
ai_metadata: Some(ai_metadata),
..SerializedBlock::new_for_test("ls".as_bytes().to_vec(), vec![])
};
let entry = HistoryEntry::for_completed_block("ls".to_string(), &block);
assert!(!entry.is_agent_executed);
}
/// Initializes history for testing
/// initialization is complete.
///
/// `history_commands_fn` is an async function that should emulate the async operation of reading
/// command from a session's history file.
///
/// `session_commands_to_append` are commands that are appended to the in-memory session history
/// after commands have been read from the history file.
async fn initialize_history_for_testing<F>(
history_handle: &mut ModelHandle<History>,
session: Arc<Session>,
history_commands_fn: F,
session_commands_to_append: Vec<String>,
app: &mut App,
) where
F: 'static + Future<Output = Vec<String>> + Send,
{
let session_id = session.id();
history_handle.update(app, move |history, ctx| {
history.init_session_with(session, history_commands_fn, ctx);
});
History::initialized_sessions(history_handle, app, vec![session_id]).await;
history_handle.update(app, move |history, _| {
history.append_commands(
session_id,
session_commands_to_append
.into_iter()
.map(|command| HistoryEntry::with_session_id(session_id, command))
.collect_vec(),
);
});
}
#[test]
fn test_append_commands() {
VirtualFS::test("history_append_command", |dirs, mut sandbox| {
App::test((), |mut app| async move {
sandbox.with_files(vec![Stub::FileWithContentToBeTrimmed(
".bash_history",
r#"
ls
pwd
warp --listen --ports=8080,8081
"#,
)]);
let mut history_handle = app.add_model(|_| History::default());
let file = Some(dirs.tests().join(".bash_history").display().to_string());
let session = Arc::new(Session::new(
SessionInfo::new_for_test()
.with_histfile(file)
.with_shell_type(ShellType::Bash),
Arc::new(TestCommandExecutor::default()),
));
let session_clone = session.clone();
initialize_history_for_testing(
&mut history_handle,
session.clone(),
async move { session_clone.read_history(false).await },
vec![
"pwd".to_owned(),
"ls".to_owned(),
"pwd".to_owned(),
"git status".to_owned(),
],
&mut app,
)
.await;
history_handle.read(&app, |history, _ctx| {
assert_eq!(
history.commands(session.id()).unwrap_or_default(),
vec![
&HistoryEntry::command_only("warp --listen --ports=8080,8081"),
&HistoryEntry::with_session_id(session.id(), "ls"),
&HistoryEntry::with_session_id(session.id(), "pwd"),
&HistoryEntry::with_session_id(session.id(), "git status"),
]
);
});
});
});
}
#[cfg_attr(windows, ignore = "TODO(CORE-3626)")]
#[test]
fn test_append_multiple_sessions() {
VirtualFS::test("append_multiple_sessions", |dirs, mut sandbox| {
App::test((), |mut app| async move {
sandbox.with_files(vec![Stub::FileWithContentToBeTrimmed(
".bash_history",
r#"
cd warp
cargo run --bin dev
"#,
)]);
let mut history_handle = app.add_model(|_| History::default());
let file = Some(dirs.tests().join(".bash_history").display().to_string());
let session = Arc::new(Session::new(
SessionInfo::new_for_test()
.with_histfile(file)
.with_shell_type(ShellType::Bash),
Arc::new(TestCommandExecutor::default()),
));
let session_clone = session.clone();
initialize_history_for_testing(
&mut history_handle,
session.clone(),
async move { session_clone.read_history(false).await },
vec![
"ls target/".to_owned(),
"cargo clean".to_owned(),
"ls target/".to_owned(),
],
&mut app,
)
.await;
history_handle.read(&app, |history, _ctx| {
assert_eq!(
history.commands(session.id()).unwrap_or_default(),
vec![
&HistoryEntry::command_only("cd warp"),
&HistoryEntry::command_only("cargo run --bin dev"),
&HistoryEntry::with_session_id(session.id(), "cargo clean"),
&HistoryEntry::with_session_id(session.id(), "ls target/"),
]
);
});
let second_session = Arc::new(Session::new(
SessionInfo::new_for_test().with_id(1),
Arc::new(TestCommandExecutor::default()),
));
initialize_history_for_testing(
&mut history_handle,
second_session.clone(),
async move { Vec::new() },
vec!["ls target/".to_owned()],
&mut app,
)
.await;
history_handle.read(&app, |history, _ctx| {
assert_eq!(
history.commands(second_session.id()).unwrap_or_default(),
vec![
&HistoryEntry::command_only("cd warp"),
&HistoryEntry::command_only("cargo run --bin dev"),
&HistoryEntry::with_session_id(session.id(), "cargo clean"),
&HistoryEntry::with_session_id(second_session.id(), "ls target/"),
]
);
});
});
});
}
#[test]
fn test_len() {
VirtualFS::test("history_len", |dirs, mut sandbox| {
App::test((), |mut app| async move {
sandbox.with_files(vec![Stub::FileWithContentToBeTrimmed(
".bash_history",
r#"
cd warp
cargo run --bin dev
touch
"#,
)]);
let mut history_handle = app.add_model(|_| History::default());
let file = Some(dirs.tests().join(".bash_history").display().to_string());
let session = Arc::new(Session::new(
SessionInfo::new_for_test()
.with_histfile(file)
.with_shell_type(ShellType::Bash),
Arc::new(TestCommandExecutor::default()),
));
let session_clone = session.clone();
initialize_history_for_testing(
&mut history_handle,
session.clone(),
async move { session_clone.read_history(false).await },
vec!["ls".to_owned(), "echo 'hello'".to_owned()],
&mut app,
)
.await;
history_handle.read(&app, |history, _ctx| {
assert_eq!(history.len(session.id()), 5);
});
history_handle.update(&mut app, |history, _ctx| {
history.append_commands(
session.id(),
vec![HistoryEntry::with_session_id(
session.id(),
"cargo run --bin dev".to_string(),
)],
);
});
history_handle.read(&app, |history, _ctx| {
assert_eq!(history.len(session.id()), 5);
assert_eq!(
history.commands(session.id()).unwrap_or_default(),
vec![
&HistoryEntry::command_only("cd warp"),
&HistoryEntry::command_only("touch"),
&HistoryEntry::with_session_id(session.id(), "ls"),
&HistoryEntry::with_session_id(session.id(), "echo 'hello'"),
&HistoryEntry::with_session_id(session.id(), "cargo run --bin dev")
]
);
});
});
});
}
#[test]
fn test_multiple_shells() {
VirtualFS::test("multiple_shells", |dirs, mut sandbox| {
App::test((), |mut app| async move {
sandbox.with_files(vec![
Stub::FileWithContentToBeTrimmed(
".bash_history",
r#"
bash-cmd1
bash-cmd2
"#,
),
Stub::FileWithContentToBeTrimmed(
".zsh_history",
r#"
zsh-cmd1
zsh-cmd2
"#,
),
]);
let mut history_handle = app.add_model(|_| History::default());
let root = dirs.tests();
let (bash_file, zsh_file) = (
Some(root.join(".bash_history").display().to_string()),
Some(root.join(".zsh_history").display().to_string()),
);
let (bash_session, zsh_session) = (
Arc::new(Session::new(
SessionInfo::new_for_test()
.with_id(0)
.with_histfile(bash_file)
.with_shell_type(ShellType::Bash),
Arc::new(TestCommandExecutor::default()),
)),
Arc::new(Session::new(
SessionInfo::new_for_test()
.with_id(1)
.with_histfile(zsh_file)
.with_shell_type(ShellType::Zsh),
Arc::new(TestCommandExecutor::default()),
)),
);
let bash_session_clone = bash_session.clone();
initialize_history_for_testing(
&mut history_handle,
bash_session.clone(),
async move { bash_session_clone.read_history(false).await },
vec!["bash-cmd3".to_owned()],
&mut app,
)
.await;
let zsh_session_clone = zsh_session.clone();
initialize_history_for_testing(
&mut history_handle,
zsh_session.clone(),
async move { zsh_session_clone.read_history(false).await },
vec!["zsh-cmd3".to_owned()],
&mut app,
)
.await;
history_handle.read(&app, |history, _ctx| {
assert_eq!(history.len(bash_session.id()), 3);
assert_eq!(history.len(zsh_session.id()), 3);
});
history_handle.update(&mut app, |history, _ctx| {
history.append_commands(
bash_session.id(),
vec![HistoryEntry::with_session_id(
bash_session.id(),
"bash-cmd4".to_string(),
)],
);
});
history_handle.read(&app, |history, _ctx| {
assert_eq!(history.len(bash_session.id()), 4);
assert_eq!(history.len(zsh_session.id()), 3);
});
history_handle.update(&mut app, |history, _ctx| {
history.append_commands(
zsh_session.id(),
vec![HistoryEntry::with_session_id(
zsh_session.id(),
"zsh-cmd4".to_string(),
)],
);
});
history_handle.read(&app, |history, _ctx| {
assert_eq!(history.len(bash_session.id()), 4);
assert_eq!(history.len(zsh_session.id()), 4);
});
});
});
}
#[test]
fn test_multiple_machines() {
App::test((), |mut app| async move {
let session_a = Arc::new(Session::new(
SessionInfo::new_for_test()
.with_id(0)
.with_shell_type(ShellType::Zsh)
.with_session_type(BootstrapSessionType::WormholedRemote)
.with_hostname("prod".to_string())
.with_user("user".to_string())
.with_ssh_socket_path(PathBuf::from("~/.ssh/12345"))
.with_home_dir("/users/warpuser".to_owned()),
Arc::new(TestCommandExecutor::default()),
));
let session_b = Arc::new(Session::new(
SessionInfo::new_for_test()
.with_id(1)
.with_shell_type(ShellType::Zsh)
.with_session_type(BootstrapSessionType::WormholedRemote)
.with_hostname("dev".to_string())
.with_user("user2".to_string())
.with_ssh_socket_path(PathBuf::from("~/.ssh/12345"))
.with_home_dir("/users/warpuser".to_owned()),
Arc::new(TestCommandExecutor::default()),
));
let mut history_handle = app.add_model(|_| History::default());
initialize_history_for_testing(
&mut history_handle,
session_a.clone(),
async { vec!["p1".to_string(), "p2".to_string()] },
vec!["p3".to_string()],
&mut app,
)
.await;
initialize_history_for_testing(
&mut history_handle,
session_b.clone(),
async { vec!["d1".to_string(), "d2".to_string()] },
vec!["d3".to_string()],
&mut app,
)
.await;
history_handle.read(&app, |history, _ctx| {
assert_eq!(history.len(session_a.id()), 3);
assert_eq!(history.len(session_b.id()), 3);
});
history_handle.update(&mut app, |history, _ctx| {
history.append_commands(
session_a.id(),
vec![HistoryEntry::with_session_id(
session_a.id(),
"p4".to_string(),
)],
);
});
history_handle.read(&app, |history, _ctx| {
assert_eq!(history.len(session_a.id()), 4);
assert_eq!(history.len(session_b.id()), 3);
});
history_handle.update(&mut app, |history, _ctx| {
history.append_commands(
session_b.id(),
vec![HistoryEntry::with_session_id(
session_b.id(),
"d4".to_string(),
)],
);
});
history_handle.read(&app, |history, _ctx| {
assert_eq!(history.len(session_a.id()), 4);
assert_eq!(history.len(session_b.id()), 4);
});
});
}
#[test]
fn test_sessions_same_shell_same_machine() {
App::test((), |mut app| async move {
let session_a = Arc::new(Session::new(
SessionInfo::new_for_test()
.with_id(0)
.with_shell_type(ShellType::Zsh),
Arc::new(TestCommandExecutor::default()),
));
let session_b = Arc::new(Session::new(
SessionInfo::new_for_test()
.with_id(1)
.with_shell_type(ShellType::Zsh),
Arc::new(TestCommandExecutor::default()),
));
let mut history_handle = app.add_model(|_| History::default());
initialize_history_for_testing(
&mut history_handle,
session_a.clone(),
async { vec!["z1".to_string(), "z2".to_string()] },
vec!["z3".to_string()],
&mut app,
)
.await;
initialize_history_for_testing(
&mut history_handle,
session_b.clone(),
async { vec![] },
vec!["z4".to_string()],
&mut app,
)
.await;
history_handle.read(&app, |history, _ctx| {
assert_eq!(history.len(session_a.id()), 3);
assert_eq!(history.len(session_b.id()), 4);
});
history_handle.update(&mut app, |history, _ctx| {
history.append_commands(
session_a.id(),
vec![HistoryEntry::with_session_id(session_a.id(), "z4")],
);
});
history_handle.read(&app, |history, _ctx| {
assert_eq!(history.len(session_a.id()), 4);
assert_eq!(history.len(session_b.id()), 4);
});
history_handle.update(&mut app, |history, _ctx| {
history.append_commands(
session_b.id(),
vec![HistoryEntry::with_session_id(session_b.id(), "z5")],
);
});
history_handle.read(&app, |history, _ctx| {
assert_eq!(history.len(session_a.id()), 4);
assert_eq!(history.len(session_b.id()), 5);
});
history_handle.update(&mut app, |history, _ctx| {
history.append_commands(
session_b.id(),
vec![HistoryEntry::with_session_id(session_b.id(), "z5")],
);
});
history_handle.read(&app, |history, _ctx| {
assert_eq!(history.len(session_a.id()), 4);
assert_eq!(history.len(session_b.id()), 5);
});
});
}
#[test]
fn test_sessions_no_dupes_new_session() {
App::test((), |mut app| async move {
let session_a = Arc::new(Session::new(
SessionInfo::new_for_test()
.with_id(0)
.with_shell_type(ShellType::Zsh),
Arc::new(TestCommandExecutor::default()),
));
let session_b = Arc::new(Session::new(
SessionInfo::new_for_test()
.with_id(1)
.with_shell_type(ShellType::Zsh),
Arc::new(TestCommandExecutor::default()),
));
let session_c = Arc::new(Session::new(
SessionInfo::new_for_test()
.with_id(2)
.with_shell_type(ShellType::Zsh),
Arc::new(TestCommandExecutor::default()),
));
let mut history_handle = app.add_model(|_| History::default());
initialize_history_for_testing(
&mut history_handle,
session_a.clone(),
async { vec!["z1".to_string(), "z2".to_string()] },
vec!["z3".to_string()],
&mut app,
)
.await;
initialize_history_for_testing(
&mut history_handle,
session_b.clone(),
async { vec![] },
vec!["z2".to_string()],
&mut app,
)
.await;
history_handle.read(&app, |history, _ctx| {
assert_eq!(
history.commands(session_a.id()).unwrap_or_default(),
vec![
&HistoryEntry::command_only("z1"),
&HistoryEntry::command_only("z2"),
&HistoryEntry::with_session_id(session_a.id(), "z3"),
]
);
assert_eq!(
history.commands(session_b.id()).unwrap_or_default(),
vec![
&HistoryEntry::command_only("z1"),
&HistoryEntry::with_session_id(session_a.id(), "z3"),
&HistoryEntry::with_session_id(session_b.id(), "z2"),
]
);
});
initialize_history_for_testing(
&mut history_handle,
session_c.clone(),
async { vec![] },
vec!["z4".to_string()],
&mut app,
)
.await;
history_handle.read(&app, |history, _ctx| {
assert_eq!(
history.commands(session_c.id()).unwrap_or_default(),
vec![
&HistoryEntry::command_only("z1"),
&HistoryEntry::with_session_id(session_a.id(), "z3"),
&HistoryEntry::with_session_id(session_b.id(), "z2"),
&HistoryEntry::with_session_id(session_c.id(), "z4")
]
);
});
});
}
#[cfg_attr(windows, ignore = "TODO(CORE-3626)")]
#[test]
fn append_command_with_rich_history_data() {
App::test((), |mut app| async move {
let session = Arc::new(Session::new(
SessionInfo::new_for_test().with_id(0),
Arc::new(TestCommandExecutor::default()),
));
let start_ts_1 = Local::now();
let end_ts_1 = Local::now();
let start_ts_2 = Local::now();
let end_ts_2 = Local::now();
let start_ts_3 = Local::now();
let end_ts_3 = Local::now();
let start_ts_4 = Local::now();
let end_ts_4 = Local::now();
let shell_host = ShellHost {
shell_type: ShellType::Bash,
user: String::from("local:user"),
hostname: String::from("local:host"),
};
let persisted_commands = vec![
PersistedCommand {
id: 0,
command: String::from("ls"),
exit_code: Some(ExitCode::from(0)),
start_ts: Some(start_ts_1),
completed_ts: Some(end_ts_1),
pwd: Some(String::from("/")),
shell_host: Some(shell_host.clone()),
session_id: None,
git_branch: None,
workflow_id: None,
workflow_command: None,
is_agent_executed: false,
},
PersistedCommand {
id: 0,
command: String::from("date"),
exit_code: Some(ExitCode::from(0)),
start_ts: Some(start_ts_2),
completed_ts: Some(end_ts_2),
pwd: Some(String::from("/usr/bin")),
shell_host: Some(shell_host.clone()),
session_id: None,
git_branch: Some(String::from("foobar")),
workflow_id: None,
workflow_command: None,
is_agent_executed: false,
},
];
let mut history_handle = app.add_model(|_| History::new(persisted_commands));
initialize_history_for_testing(
&mut history_handle,
session.clone(),
async {
vec![
"cd ~/Desktop".to_string(),
"ls".to_string(),
"date".to_string(),
]
},
Vec::new(),
&mut app,
)
.await;
history_handle.update(&mut app, |history, _ctx| {
history.append_commands(
session.id(),
vec![
HistoryEntry {
session_id: Some(session.id()),
command: String::from("touch foobar"),
pwd: Some(String::from("/Users/andy/")),
start_ts: Some(start_ts_3),
completed_ts: Some(end_ts_3),
workflow_id: None,
workflow_command: None,
exit_code: Some(ExitCode::from(0)),
git_head: None,
shell_host: None,
is_agent_executed: false,
is_for_restored_block: false,
},
HistoryEntry {
session_id: Some(session.id()),
command: String::from("ls"),
pwd: Some(String::from("/Users/andy/")),
start_ts: Some(start_ts_4),
completed_ts: Some(end_ts_4),
workflow_id: None,
workflow_command: None,
exit_code: Some(ExitCode::from(0)),
git_head: None,
shell_host: None,
is_agent_executed: false,
is_for_restored_block: false,
},
],
);
});
history_handle.read(&app, |history, _ctx| {
assert_eq!(
history.commands(session.id()).unwrap_or_default(),
vec![
&HistoryEntry::command_only("cd ~/Desktop"),
&HistoryEntry {
session_id: None,
command: String::from("date"),
pwd: Some(String::from("/usr/bin")),
start_ts: Some(start_ts_2),
completed_ts: Some(end_ts_2),
workflow_id: None,
workflow_command: None,
exit_code: Some(ExitCode::from(0)),
git_head: Some(String::from("foobar")),
shell_host: Some(shell_host.clone()),
is_agent_executed: false,
is_for_restored_block: false,
},
&HistoryEntry {
session_id: Some(session.id()),
command: String::from("touch foobar"),
pwd: Some(String::from("/Users/andy/")),
start_ts: Some(start_ts_3),
completed_ts: Some(end_ts_3),
workflow_id: None,
workflow_command: None,
exit_code: Some(ExitCode::from(0)),
git_head: None,
shell_host: None,
is_agent_executed: false,
is_for_restored_block: false,
},
&HistoryEntry {
session_id: Some(session.id()),
command: String::from("ls"),
pwd: Some(String::from("/Users/andy/")),
start_ts: Some(start_ts_4),
completed_ts: Some(end_ts_4),
workflow_id: None,
workflow_command: None,
exit_code: Some(ExitCode::from(0)),
git_head: None,
shell_host: None,
is_agent_executed: false,
is_for_restored_block: false,
},
]
);
});
});
}
#[test]
fn append_restored_command_doesnt_overwrite_rich_history() {
App::test((), |mut app| async move {
let session = Arc::new(Session::new(
SessionInfo::new_for_test().with_id(0),
Arc::new(TestCommandExecutor::default()),
));
let start_ts = Local::now();
let end_ts = Local::now();
let shell_host = ShellHost {
shell_type: ShellType::Bash,
user: String::from("local:user"),
hostname: String::from("local:host"),
};
let persisted_commands = vec![PersistedCommand {
id: 0,
command: String::from("ls"),
exit_code: Some(ExitCode::from(0)),
start_ts: Some(start_ts),
completed_ts: Some(end_ts),
pwd: Some(String::from("/tmp")),
shell_host: Some(shell_host),
session_id: None,
git_branch: None,
workflow_id: None,
workflow_command: None,
is_agent_executed: false,
}];
let mut history_handle = app.add_model(|_| History::new(persisted_commands));
initialize_history_for_testing(
&mut history_handle,
session.clone(),
async { vec!["cd ~/Desktop".to_string(), "ls".to_string()] },
Vec::new(),
&mut app,
)
.await;
history_handle.update(&mut app, |history, _ctx| {
history.append_restored_commands(
session.id(),
vec![HistoryEntry {
session_id: Some(session.id()),
command: "ls".to_string(),
pwd: Some("/tmp".to_string()),
start_ts: Some(start_ts),
completed_ts: Some(end_ts),
workflow_id: None,
workflow_command: None,
exit_code: Some(ExitCode::from(0)),
git_head: None,
shell_host: None,
is_agent_executed: false,
is_for_restored_block: true,
}],
);
});
history_handle.read(&app, |history, _ctx| {
assert_eq!(
history.commands(session.id()).unwrap_or_default(),
vec![
&HistoryEntry::command_only("cd ~/Desktop"),
&HistoryEntry {
session_id: Some(session.id()),
command: String::from("ls"),
pwd: Some(String::from("/tmp")),
start_ts: Some(start_ts),
completed_ts: Some(end_ts),
workflow_id: None,
workflow_command: None,
exit_code: Some(ExitCode::from(0)),
git_head: None,
shell_host: None,
is_agent_executed: false,
is_for_restored_block: true,
},
]
)
});
});
}
#[test]
fn is_appendable_vs_is_queryable() {
App::test((), |mut app| async move {
let mut history_handle = app.add_model(|_| History::new(vec![]));
let (tx, rx) = async_channel::bounded(1);
let session = Arc::new(Session::new(
SessionInfo::new_for_test().with_id(0),
Arc::new(TestCommandExecutor::default()),
));
let session_id = session.id();
history_handle.update(&mut app, move |history, ctx| {
history.init_session_with(
session,
async move {
pin!(rx).next().await;
vec![]
},
ctx,
);
});
// When the session has just been registered with the history model,
// its history will be appendable but not queryable.
history_handle.read(&app, |history, _| {
assert!(history.is_appendable(&session_id));
assert!(!history.is_queryable(&session_id));
});
// Simulate the asynchronous reading of the histfile.
tx.send(()).await.unwrap();
History::initialized_sessions(&mut history_handle, &mut app, vec![session_id]).await;
// Once we've read the histfile, the model should be appendable and queryable.
history_handle.read(&app, |history, _| {
assert!(history.is_appendable(&session_id));
assert!(history.is_queryable(&session_id));
});
});
}