mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Prune TUI snapshots and consolidate behavior tests (#50808)
## What changed - Remove redundant snapshot fixtures and selected rendering test cases across the TUI. - Replace several full-output snapshots with direct assertions on messages, popup state, status indicators, and rendered content. - Consolidate slash-command aliases, command-prefix selection, category titles, and hook events into shared test loops. - Use shared snapshots for the setup import prompt and paginated transcript overlay across platforms. GitOrigin-RevId: 461e30ebaf70ba930d91a135d2fc598ee4f04f27
This commit is contained in:
@@ -251,11 +251,12 @@ async fn archiving_selects_the_next_displayed_task() -> Result<()> {
|
||||
app_server.shutdown().await?;
|
||||
}
|
||||
}
|
||||
let snapshot = match action {
|
||||
AgentsOverviewAction::Archive => "archiving_selects_the_next_displayed_task",
|
||||
AgentsOverviewAction::Delete => "deleting_selects_the_next_displayed_task",
|
||||
};
|
||||
insta::assert_snapshot!(snapshot, selections.join("\n"));
|
||||
if matches!(action, AgentsOverviewAction::Archive) {
|
||||
insta::assert_snapshot!(
|
||||
"archiving_selects_the_next_displayed_task",
|
||||
selections.join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
---
|
||||
source: tui/src/app/agents_overview_actions_tests.rs
|
||||
assertion_line: 308
|
||||
expression: "selections.join(\"\\n\")"
|
||||
---
|
||||
› ○ Task 3
|
||||
› ○ Other 2
|
||||
› ○ Task 1
|
||||
› ○ Task 4
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
---
|
||||
source: tui/src/app/tests.rs
|
||||
expression: "rendered.lines.iter().map(rendered_line_text).collect::<Vec<_>>().join(\"\\n\")"
|
||||
---
|
||||
latest user question
|
||||
|
||||
• Final answer:
|
||||
|
||||
Pattern Outcome
|
||||
━━━━━━━━━━━━ ━━━━━━━━━━━
|
||||
Table tail Preserved
|
||||
@@ -7144,15 +7144,15 @@ async fn required_stream_reflow_during_capped_initial_replay_survives_transcript
|
||||
|
||||
let rendered = app.render_transcript_lines_for_reflow(/*width*/ 80);
|
||||
assert_eq!(rendered.lines.len(), 7);
|
||||
assert_snapshot!(
|
||||
"required_stream_reflow_during_capped_initial_replay_survives_transcript_overlay",
|
||||
rendered
|
||||
.lines
|
||||
.iter()
|
||||
.map(rendered_line_text)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
let rendered = rendered
|
||||
.lines
|
||||
.iter()
|
||||
.map(rendered_line_text)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(rendered.contains("Table tail"));
|
||||
assert!(rendered.contains("Preserved"));
|
||||
assert!(!rendered.contains("stale streamed table tail"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -118,9 +118,9 @@ async fn mcp_login_retry_suppresses_old_completion_and_late_browser_open() {
|
||||
for event in snapshot.events {
|
||||
app.handle_thread_event_replay(event);
|
||||
}
|
||||
assert_snapshot!(
|
||||
"mcp_login_retry_completion",
|
||||
next_history_message(&mut events)
|
||||
assert_eq!(
|
||||
next_history_message(&mut events),
|
||||
"• Signed in to enterprise."
|
||||
);
|
||||
assert!(events.try_recv().is_err());
|
||||
// A cancellation delivered after the replacement response is stale as well.
|
||||
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: tui/src/app/tests/mcp_login_tests.rs
|
||||
expression: next_history_message(&mut events)
|
||||
---
|
||||
• Signed in to enterprise.
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
---
|
||||
source: tui/src/app/tests/user_verification_routes_tests.rs
|
||||
expression: "render_bottom_popup(&app.chat_widget, 80)"
|
||||
---
|
||||
Approve deployment?
|
||||
|
||||
Thread: Verifier [worker]
|
||||
|
||||
Server: deployments
|
||||
|
||||
Verify the production deployment.
|
||||
|
||||
|
||||
› 1. Verify and approve (y)
|
||||
2. Cancel this request (c)
|
||||
|
||||
Press enter to confirm or esc to cancel
|
||||
@@ -137,9 +137,9 @@ async fn side_toggle_surfaces_pending_verification_from_an_inactive_thread() ->
|
||||
app.toggle_side_conversation(&mut tui, &mut app_server)
|
||||
.await?;
|
||||
assert_eq!(app.active_thread_id, Some(primary_thread_id));
|
||||
insta::assert_snapshot!(
|
||||
"side_toggle_surfaces_pending_user_verification",
|
||||
render_bottom_popup(&app.chat_widget, /*width*/ 80)
|
||||
assert!(app.chat_widget.has_active_view());
|
||||
assert!(
|
||||
render_bottom_popup(&app.chat_widget, /*width*/ 80).contains("Approve deployment?")
|
||||
);
|
||||
app_server.shutdown().await?;
|
||||
Ok(())
|
||||
|
||||
@@ -5266,18 +5266,6 @@ mod tests {
|
||||
},
|
||||
);
|
||||
|
||||
snapshot_composer_state(
|
||||
"footer_mode_ctrl_c_interrupt",
|
||||
/*enhanced_keys_supported*/ true,
|
||||
|composer| {
|
||||
composer.set_task_running(/*running*/ true);
|
||||
composer.show_quit_shortcut_hint(
|
||||
key_hint::ctrl(KeyCode::Char('c')),
|
||||
/*has_focus*/ true,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
snapshot_composer_state(
|
||||
"footer_mode_ctrl_c_then_esc_hint",
|
||||
/*enhanced_keys_supported*/ true,
|
||||
@@ -9275,20 +9263,6 @@ mod tests {
|
||||
},
|
||||
);
|
||||
|
||||
snapshot_composer_state(
|
||||
"remote_image_rows_selected",
|
||||
/*enhanced_keys_supported*/ false,
|
||||
|composer| {
|
||||
composer.set_remote_image_urls(vec![
|
||||
"https://example.com/one.png".to_string(),
|
||||
"https://example.com/two.png".to_string(),
|
||||
]);
|
||||
composer.set_text_content("describe these".to_string(), Vec::new(), Vec::new());
|
||||
composer.draft.textarea.set_cursor(/*pos*/ 0);
|
||||
let _ = composer.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE));
|
||||
},
|
||||
);
|
||||
|
||||
snapshot_composer_state(
|
||||
"remote_image_rows_after_delete_first",
|
||||
/*enhanced_keys_supported*/ false,
|
||||
@@ -9308,7 +9282,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_model_first_for_mo_ui() {
|
||||
fn slash_popup_model_clipping_snapshot() {
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::TestBackend;
|
||||
|
||||
@@ -9326,293 +9300,51 @@ mod tests {
|
||||
// Type "/mo" humanlike so paste-burst doesn’t interfere.
|
||||
type_chars_humanlike(&mut composer, &['/', 'm', 'o']);
|
||||
|
||||
for (height, snapshot, cursor) in [
|
||||
(3, "slash_popup_mo_clipped", (5, 1)),
|
||||
(5, "slash_popup_mo", (5, 2)),
|
||||
let mut terminal =
|
||||
Terminal::new(TestBackend::new(/*width*/ 60, /*height*/ 3)).expect("create terminal");
|
||||
terminal
|
||||
.draw(|f| {
|
||||
composer.render(f.area(), f.buffer_mut());
|
||||
assert_eq!(composer.cursor_pos(f.area()), Some((5, 1)));
|
||||
})
|
||||
.expect("draw composer");
|
||||
|
||||
// The picker sits above the draft; clipping must keep the draft and cursor visible.
|
||||
insta::assert_snapshot!("slash_popup_mo_clipped", terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_selects_first_matching_command() {
|
||||
use super::super::command_popup::CommandItem;
|
||||
for (input, expected) in [
|
||||
("/mo", "model"),
|
||||
("/res", "resume"),
|
||||
("/ar", "archive"),
|
||||
("/pet", "pets"),
|
||||
("/bt", "btw"),
|
||||
("/si", "side"),
|
||||
] {
|
||||
let mut terminal =
|
||||
Terminal::new(TestBackend::new(/*width*/ 60, height)).expect("create terminal");
|
||||
terminal
|
||||
.draw(|f| {
|
||||
composer.render(f.area(), f.buffer_mut());
|
||||
assert_eq!(composer.cursor_pos(f.area()), Some(cursor));
|
||||
})
|
||||
.expect("draw composer");
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
type_chars_humanlike(&mut composer, &input.chars().collect::<Vec<_>>());
|
||||
|
||||
// The picker sits above the draft; clipping must keep the draft and cursor visible.
|
||||
insta::assert_snapshot!(snapshot, terminal.backend());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_model_first_for_mo_logic() {
|
||||
use super::super::command_popup::CommandItem;
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
type_chars_humanlike(&mut composer, &['/', 'm', 'o']);
|
||||
|
||||
match &composer.popups.active {
|
||||
ActivePopup::Command(popup) => match popup.selected_item() {
|
||||
Some(CommandItem::Builtin(cmd)) => {
|
||||
assert_eq!(cmd.command(), "model")
|
||||
}
|
||||
Some(CommandItem::ServiceTier(command)) => {
|
||||
panic!("expected model command, got service tier {command:?}")
|
||||
}
|
||||
None => panic!("no selected command for '/mo'"),
|
||||
},
|
||||
_ => panic!("slash popup not active after typing '/mo'"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_resume_for_res_ui() {
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::TestBackend;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
|
||||
// Type "/res" humanlike so paste-burst doesn’t interfere.
|
||||
type_chars_humanlike(&mut composer, &['/', 'r', 'e', 's']);
|
||||
|
||||
let mut terminal = Terminal::new(TestBackend::new(60, 6)).expect("terminal");
|
||||
terminal
|
||||
.draw(|f| composer.render(f.area(), f.buffer_mut()))
|
||||
.expect("draw composer");
|
||||
|
||||
// Snapshot should show /resume as the first entry for /res.
|
||||
insta::assert_snapshot!("slash_popup_res", terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_archive_for_ar_ui() {
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::TestBackend;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
|
||||
type_chars_humanlike(&mut composer, &['/', 'a', 'r']);
|
||||
|
||||
let mut terminal = Terminal::new(TestBackend::new(60, 5)).expect("terminal");
|
||||
terminal
|
||||
.draw(|f| composer.render(f.area(), f.buffer_mut()))
|
||||
.expect("draw composer");
|
||||
|
||||
insta::assert_snapshot!("slash_popup_ar", terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_resume_for_res_logic() {
|
||||
use super::super::command_popup::CommandItem;
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
type_chars_humanlike(&mut composer, &['/', 'r', 'e', 's']);
|
||||
|
||||
match &composer.popups.active {
|
||||
ActivePopup::Command(popup) => match popup.selected_item() {
|
||||
Some(CommandItem::Builtin(cmd)) => {
|
||||
assert_eq!(cmd.command(), "resume")
|
||||
}
|
||||
Some(CommandItem::ServiceTier(command)) => {
|
||||
panic!("expected resume command, got service tier {command:?}")
|
||||
}
|
||||
None => panic!("no selected command for '/res'"),
|
||||
},
|
||||
_ => panic!("slash popup not active after typing '/res'"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_pets_for_pet_ui() {
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::TestBackend;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
|
||||
type_chars_humanlike(&mut composer, &['/', 'p', 'e', 't']);
|
||||
|
||||
let mut terminal = Terminal::new(TestBackend::new(60, 5)).expect("terminal");
|
||||
terminal
|
||||
.draw(|f| composer.render(f.area(), f.buffer_mut()))
|
||||
.expect("draw composer");
|
||||
|
||||
insta::assert_snapshot!("slash_popup_pet", terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_pets_for_pet_logic() {
|
||||
use super::super::command_popup::CommandItem;
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
type_chars_humanlike(&mut composer, &['/', 'p', 'e', 't']);
|
||||
|
||||
match &composer.popups.active {
|
||||
ActivePopup::Command(popup) => match popup.selected_item() {
|
||||
Some(CommandItem::Builtin(cmd)) => {
|
||||
assert_eq!(cmd.command(), "pets")
|
||||
}
|
||||
Some(CommandItem::ServiceTier(command)) => {
|
||||
panic!("expected pets command, got service tier {command:?}")
|
||||
}
|
||||
None => panic!("no selected command for '/pet'"),
|
||||
},
|
||||
_ => panic!("slash popup not active after typing '/pet'"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_btw_for_bt_ui() {
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::TestBackend;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
|
||||
type_chars_humanlike(&mut composer, &['/', 'b', 't']);
|
||||
|
||||
let mut terminal = Terminal::new(TestBackend::new(60, 5)).expect("terminal");
|
||||
terminal
|
||||
.draw(|f| composer.render(f.area(), f.buffer_mut()))
|
||||
.expect("draw composer");
|
||||
|
||||
insta::assert_snapshot!("slash_popup_bt", terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_btw_for_bt_logic() {
|
||||
use super::super::command_popup::CommandItem;
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
type_chars_humanlike(&mut composer, &['/', 'b', 't']);
|
||||
|
||||
match &composer.popups.active {
|
||||
ActivePopup::Command(popup) => match popup.selected_item() {
|
||||
Some(CommandItem::Builtin(cmd)) => {
|
||||
assert_eq!(cmd.command(), "btw")
|
||||
}
|
||||
Some(CommandItem::ServiceTier(command)) => {
|
||||
panic!("expected btw command, got service tier {command:?}")
|
||||
}
|
||||
None => panic!("no selected command for '/bt'"),
|
||||
},
|
||||
_ => panic!("slash popup not active after typing '/bt'"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_side_for_si_ui() {
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::TestBackend;
|
||||
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
|
||||
type_chars_humanlike(&mut composer, &['/', 's', 'i']);
|
||||
|
||||
let mut terminal = Terminal::new(TestBackend::new(60, 5)).expect("terminal");
|
||||
terminal
|
||||
.draw(|f| composer.render(f.area(), f.buffer_mut()))
|
||||
.expect("draw composer");
|
||||
|
||||
insta::assert_snapshot!("slash_popup_si", terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slash_popup_side_for_si_logic() {
|
||||
use super::super::command_popup::CommandItem;
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let sender = AppEventSender::new(tx);
|
||||
let mut composer = ChatComposer::new(
|
||||
/*has_input_focus*/ true,
|
||||
sender,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
"Ask Codex to do anything".to_string(),
|
||||
/*disable_paste_burst*/ false,
|
||||
);
|
||||
type_chars_humanlike(&mut composer, &['/', 's', 'i']);
|
||||
|
||||
match &composer.popups.active {
|
||||
ActivePopup::Command(popup) => match popup.selected_item() {
|
||||
Some(CommandItem::Builtin(cmd)) => {
|
||||
assert_eq!(cmd.command(), "side")
|
||||
}
|
||||
Some(CommandItem::ServiceTier(command)) => {
|
||||
panic!("expected side command, got service tier {command:?}")
|
||||
}
|
||||
None => panic!("no selected command for '/si'"),
|
||||
},
|
||||
_ => panic!("slash popup not active after typing '/si'"),
|
||||
match &composer.popups.active {
|
||||
ActivePopup::Command(popup) => match popup.selected_item() {
|
||||
Some(CommandItem::Builtin(cmd)) => assert_eq!(cmd.command(), expected),
|
||||
Some(CommandItem::ServiceTier(command)) => {
|
||||
panic!("expected {expected} command, got service tier {command:?}")
|
||||
}
|
||||
None => panic!("no selected command for '{input}'"),
|
||||
},
|
||||
_ => panic!("slash popup not active after typing '{input}'"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12063,31 +11795,28 @@ mod tests {
|
||||
assert!(composer.handle_paste_burst_flush(now + PasteBurst::recommended_flush_delay()));
|
||||
assert_eq!(composer.draft.textarea.text(), " ");
|
||||
assert!(!composer.is_in_paste_burst());
|
||||
|
||||
let snapshot_name = if modifiers == KeyModifiers::NONE {
|
||||
"plain_space_is_rendered"
|
||||
} else {
|
||||
"shift_space_is_rendered"
|
||||
};
|
||||
snapshot_composer_state(
|
||||
snapshot_name,
|
||||
/*enhanced_keys_supported*/ false,
|
||||
|composer| {
|
||||
let now = Instant::now();
|
||||
for input in [
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE),
|
||||
KeyEvent::new(KeyCode::Char(' '), modifiers),
|
||||
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE),
|
||||
] {
|
||||
composer.handle_input_basic_with_time(input, now);
|
||||
}
|
||||
|
||||
assert!(composer.handle_paste_burst_flush(
|
||||
now + PasteBurst::recommended_active_flush_delay()
|
||||
));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
snapshot_composer_state(
|
||||
"plain_space_is_rendered",
|
||||
/*enhanced_keys_supported*/ false,
|
||||
|composer| {
|
||||
let now = Instant::now();
|
||||
for input in [
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE),
|
||||
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
|
||||
KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE),
|
||||
] {
|
||||
composer.handle_input_basic_with_time(input, now);
|
||||
}
|
||||
|
||||
assert!(
|
||||
composer.handle_paste_burst_flush(
|
||||
now + PasteBurst::recommended_active_flush_delay()
|
||||
)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -570,26 +570,6 @@ mod tests {
|
||||
insta::assert_snapshot!("command_popup_voice", format!("{buf:?}"));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn default_command_popup_items_snapshot() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default(), Vec::new());
|
||||
popup.on_composer_text_change("/".to_string());
|
||||
|
||||
let commands = popup
|
||||
.filtered_items()
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let command = item.command();
|
||||
let description = item.description();
|
||||
format!("/{command} - {description}")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
insta::assert_snapshot!("command_popup_default_items", commands);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefix_filter_limits_matches_for_ac() {
|
||||
let mut popup = CommandPopup::new(CommandPopupFlags::default(), Vec::new());
|
||||
|
||||
@@ -298,13 +298,8 @@ fn voice_discovery_requires_the_client_runtime() {
|
||||
.unwrap();
|
||||
assert!(view.pre_draw_tick(Instant::now()));
|
||||
assert_eq!(view.features.len(), usize::from(supported));
|
||||
snapshot_view(
|
||||
if supported {
|
||||
"voice_runtime_available"
|
||||
} else {
|
||||
"voice_runtime_unavailable"
|
||||
},
|
||||
&view,
|
||||
);
|
||||
if supported {
|
||||
snapshot_view("voice_runtime_available", &view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,17 +48,16 @@ fn make_view(category: FeedbackCategory) -> FeedbackNoteView {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feedback_view_bad_result() {
|
||||
let view = make_view(FeedbackCategory::BadResult);
|
||||
let rendered = render(&view, /*width*/ 60);
|
||||
insta::assert_snapshot!("feedback_view_bad_result", rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feedback_view_good_result() {
|
||||
let view = make_view(FeedbackCategory::GoodResult);
|
||||
let rendered = render(&view, /*width*/ 60);
|
||||
insta::assert_snapshot!("feedback_view_good_result", rendered);
|
||||
fn feedback_category_titles() {
|
||||
for (category, title) in [
|
||||
(FeedbackCategory::BadResult, "▌ Tell us more (bad result)"),
|
||||
(FeedbackCategory::GoodResult, "▌ Tell us more (good result)"),
|
||||
(FeedbackCategory::Bug, "▌ Tell us more (bug)"),
|
||||
(FeedbackCategory::Other, "▌ Tell us more (other)"),
|
||||
] {
|
||||
let rendered = render(&make_view(category), /*width*/ 60);
|
||||
assert_eq!(rendered.lines().next(), Some(title));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -68,13 +67,6 @@ fn feedback_view_bug() {
|
||||
insta::assert_snapshot!("feedback_view_bug", rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feedback_view_other() {
|
||||
let view = make_view(FeedbackCategory::Other);
|
||||
let rendered = render(&view, /*width*/ 60);
|
||||
insta::assert_snapshot!("feedback_view_other", rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feedback_view_safety_check() {
|
||||
let view = make_view(FeedbackCategory::SafetyCheck);
|
||||
@@ -82,22 +74,6 @@ fn feedback_view_safety_check() {
|
||||
insta::assert_snapshot!("feedback_view_safety_check", rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feedback_view_with_connectivity_diagnostics() {
|
||||
let (tx_raw, _rx) = tokio::sync::mpsc::unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let view = FeedbackNoteView::new(
|
||||
FeedbackCategory::Bug,
|
||||
/*turn_id*/ None,
|
||||
tx,
|
||||
/*include_logs*/ false,
|
||||
FeedbackAudience::External,
|
||||
);
|
||||
let rendered = render(&view, /*width*/ 60);
|
||||
|
||||
insta::assert_snapshot!("feedback_view_with_connectivity_diagnostics", rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn feedback_disclosure_matches_audience_at_normal_and_narrow_widths() {
|
||||
let mut rendered = Vec::new();
|
||||
|
||||
@@ -1375,23 +1375,6 @@ mod tests {
|
||||
},
|
||||
);
|
||||
|
||||
snapshot_footer(
|
||||
"footer_ctrl_c_quit_running",
|
||||
FooterProps {
|
||||
mode: FooterMode::QuitShortcutReminder,
|
||||
esc_backtrack_hint: false,
|
||||
is_task_running: true,
|
||||
queue_submissions: false,
|
||||
collaboration_modes_enabled: false,
|
||||
is_wsl: false,
|
||||
quit_shortcut_key: key_hint::ctrl(KeyCode::Char('c')),
|
||||
status_line_value: None,
|
||||
status_line_enabled: false,
|
||||
key_hints: FooterKeyHints::default_bindings(),
|
||||
active_agent_label: None,
|
||||
},
|
||||
);
|
||||
|
||||
snapshot_footer(
|
||||
"footer_esc_hint_idle",
|
||||
FooterProps {
|
||||
|
||||
@@ -222,13 +222,6 @@ mod tests {
|
||||
assert_eq!(queue.desired_height(/*width*/ 40), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn desired_height_one_message() {
|
||||
let mut queue = PendingInputPreview::new();
|
||||
queue.queued_messages.push("Hello, world!".to_string());
|
||||
assert_eq!(queue.desired_height(/*width*/ 40), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_one_message() {
|
||||
let mut queue = PendingInputPreview::new();
|
||||
@@ -240,21 +233,6 @@ mod tests {
|
||||
assert_snapshot!("render_one_message", format!("{buf:?}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_one_message_with_shift_left_binding() {
|
||||
let mut queue = PendingInputPreview::new();
|
||||
queue.queued_messages.push("Hello, world!".to_string());
|
||||
queue.set_edit_binding(Some(key_hint::shift(KeyCode::Left).into()));
|
||||
let width = 40;
|
||||
let height = queue.desired_height(width);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, width, height));
|
||||
queue.render(Rect::new(0, 0, width, height), &mut buf);
|
||||
assert_snapshot!(
|
||||
"render_one_message_with_shift_left_binding",
|
||||
format!("{buf:?}")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_two_messages() {
|
||||
let mut queue = PendingInputPreview::new();
|
||||
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" ⌃c again to quit "
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
" [Image #1] "
|
||||
" [Image #2] "
|
||||
" "
|
||||
"› describe these "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left "
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
"› a b "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
" 100% context left "
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"› /archive archive this session "
|
||||
" "
|
||||
"› /ar "
|
||||
" "
|
||||
" 100% context left "
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"› /btw start a side conversation in an ephemeral fork "
|
||||
" "
|
||||
"› /bt "
|
||||
" "
|
||||
" 100% context left "
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"› /model choose what model and reasoning effort to use "
|
||||
" "
|
||||
"› /mo "
|
||||
" "
|
||||
" 100% context left "
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"› /pets choose or hide the terminal pet "
|
||||
" "
|
||||
"› /pet "
|
||||
" "
|
||||
" 100% context left "
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
assertion_line: 9511
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"› /resume resume a saved chat "
|
||||
" "
|
||||
"› /res "
|
||||
" "
|
||||
" "
|
||||
" 100% context left "
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/chat_composer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
"› /side start a side conversation in an ephemeral fork "
|
||||
" "
|
||||
"› /si "
|
||||
" "
|
||||
" 100% context left "
|
||||
-53
@@ -1,53 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/command_popup.rs
|
||||
expression: commands
|
||||
---
|
||||
/model - choose what model and reasoning effort to use
|
||||
/ide - include current selection, open files, and other context from your IDE
|
||||
/permissions - choose what Codex is allowed to do
|
||||
/keymap - remap TUI shortcuts
|
||||
/vim - toggle Vim mode for the composer
|
||||
/experimental - toggle experimental features
|
||||
/approve - approve one retry of a recent auto-review denial
|
||||
/memories - configure memory use and generation
|
||||
/skills - use skills to improve how Codex performs specific tasks
|
||||
/import - import setup, this project, and recent chats from Claude Code
|
||||
/hooks - view and manage lifecycle hooks
|
||||
/review - review my current changes and find issues
|
||||
/rename - rename the current thread
|
||||
/new - start a new chat during a conversation
|
||||
/archive - archive this session
|
||||
/delete - permanently delete this session
|
||||
/resume - resume a saved chat
|
||||
/fork - fork the current chat
|
||||
/app - continue this session in the Desktop app
|
||||
/init - create an AGENTS.md file with instructions for Codex
|
||||
/compact - summarize conversation to prevent hitting the context limit
|
||||
/recap - summarize the current conversation now
|
||||
/agents - open the agent command center
|
||||
/side - start a side conversation in an ephemeral fork
|
||||
/copy - copy the last response or part of it
|
||||
/export - export the conversation as markdown
|
||||
/raw - toggle raw scrollback mode for copy-friendly terminal selection
|
||||
/tui - choose the TUI mode for the next launch
|
||||
/diff - show git diff (including untracked files)
|
||||
/mention - mention a file
|
||||
/status - show current session configuration and token usage
|
||||
/daemon - Manage the local background server
|
||||
/warnings - view retained warnings and diagnostic details
|
||||
/cd - change the current working directory
|
||||
/pwd - show the current working directory
|
||||
/title - configure which items appear in the terminal title
|
||||
/statusline - configure which items appear in the status line
|
||||
/theme - choose a syntax highlighting theme
|
||||
/pets - choose or hide the terminal pet
|
||||
/mcp - list MCP tools; use /mcp verbose or /mcp login <name>
|
||||
/logout - log out of Codex
|
||||
/exit - exit Codex
|
||||
/feedback - send logs to maintainers
|
||||
/rollout - print the rollout file path
|
||||
/ps - list background terminals
|
||||
/stop - stop all background terminals
|
||||
/clear - clear the terminal and start a new chat
|
||||
/test-approval - test approval request
|
||||
/subagents - switch between this session's subagents
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/experimental_features_view_tests.rs
|
||||
expression: buffer_text(&buffer)
|
||||
---
|
||||
|
||||
Experimental features
|
||||
Checked features are configured on. Some experimental features take effect
|
||||
only in new tasks or after restarting the Codex server.
|
||||
No server experiments available.
|
||||
|
||||
|
||||
No experimental features available for now
|
||||
|
||||
|
||||
space toggle · enter save · esc save/close
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/feedback_note_view_tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
▌ Tell us more (bad result)
|
||||
▌
|
||||
▌ Your feedback can be used to improve ChatGPT. Learn more.
|
||||
▌
|
||||
▌ (optional) Write a short description to help us further
|
||||
|
||||
Press enter to submit or esc to cancel
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/feedback_note_view_tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
▌ Tell us more (good result)
|
||||
▌
|
||||
▌ Your feedback can be used to improve ChatGPT. Learn more.
|
||||
▌
|
||||
▌ (optional) Write a short description to help us further
|
||||
|
||||
Press enter to submit or esc to cancel
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/feedback_note_view_tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
▌ Tell us more (other)
|
||||
▌
|
||||
▌ Your feedback can be used to improve ChatGPT. Learn more.
|
||||
▌
|
||||
▌ (optional) Write a short description to help us further
|
||||
|
||||
Press enter to submit or esc to cancel
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/feedback_note_view_tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
▌ Tell us more (bug)
|
||||
▌
|
||||
▌ Your feedback can be used to improve ChatGPT. Learn more.
|
||||
▌
|
||||
▌ (optional) Write a short description to help us further
|
||||
|
||||
Press enter to submit or esc to cancel
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/footer.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" ⌃c again to quit "
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/message_queue.rs
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 40, height: 5 },
|
||||
content: [
|
||||
" ↳ This is ",
|
||||
" a message ",
|
||||
" with many ",
|
||||
" … ",
|
||||
" alt + ↑ edit ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 11, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 13, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 13, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 5, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 16, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/message_queue.rs
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 40, height: 2 },
|
||||
content: [
|
||||
" ↳ Hello, world! ",
|
||||
" alt + ↑ edit ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 17, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 16, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/message_queue.rs
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 40, height: 3 },
|
||||
content: [
|
||||
" ↳ Hello, world! ",
|
||||
" ↳ This is another message ",
|
||||
" alt + ↑ edit ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 17, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 27, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 16, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/message_queue.rs
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 40, height: 4 },
|
||||
content: [
|
||||
" ↳ This is a longer message that should",
|
||||
" be wrapped ",
|
||||
" ↳ This is another message ",
|
||||
" alt + ↑ edit ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 14, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 27, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 16, y: 3, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/pending_input_preview.rs
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 40, height: 3 },
|
||||
content: [
|
||||
"• Queued follow-up inputs ",
|
||||
" ↳ Hello, world! ",
|
||||
" ⇧← edit last queued message ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 2, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 4, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: DIM | ITALIC,
|
||||
x: 17, y: 1, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 6, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 31, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
]
|
||||
}
|
||||
@@ -153,7 +153,6 @@ async fn delegation_started_before_peer_connection_keeps_its_voice_origin() {
|
||||
Ok(AppCommand::RealtimeConversationSpeech { text, .. }) if text.as_str() == "Spoken answer"
|
||||
));
|
||||
assert!(ops.try_recv().is_err());
|
||||
let mut rendered_history = Vec::new();
|
||||
commit_realtime_history_events(&mut chat, &mut events);
|
||||
while let Ok(event) = events.try_recv() {
|
||||
if let AppEvent::InsertHistoryCell(cell) = event
|
||||
@@ -166,13 +165,8 @@ async fn delegation_started_before_peer_connection_keeps_its_voice_origin() {
|
||||
.collect::<String>();
|
||||
assert!(!rendered.contains("Spoken answer"));
|
||||
assert!(!rendered.contains("<realtime_delegation>"));
|
||||
rendered_history.push(rendered);
|
||||
}
|
||||
}
|
||||
insta::assert_snapshot!(
|
||||
"voice_delegation_during_connection",
|
||||
rendered_history.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/realtime_tests/handoffs.rs
|
||||
assertion_line: 514
|
||||
expression: "rendered_history.join(\"\\n\")"
|
||||
---
|
||||
• Voice conversation started. Use /voice mute to mute or /voice to stop.
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/realtime_tests/transcripts.rs
|
||||
assertion_line: 446
|
||||
expression: rendered
|
||||
---
|
||||
|
||||
› Correction
|
||||
|
||||
• First answer
|
||||
@@ -324,7 +324,6 @@ async fn intentional_stop_preserves_both_interleaved_partials_once() {
|
||||
.join("\n");
|
||||
assert_eq!(rendered.matches("Correction").count(), 1);
|
||||
assert_eq!(rendered.matches("First answer").count(), 1);
|
||||
insta::assert_snapshot!("interleaved_partials_on_stop", rendered);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/bedrock_catalog_tests.rs
|
||||
expression: "render_bottom_popup(&chat, 100)"
|
||||
---
|
||||
Advanced Reasoning
|
||||
⚠ Consumes usage limits faster
|
||||
|
||||
|
||||
› 1. Max For difficult problems when quality matters more than speed · higher usage
|
||||
2. Ultra For demanding work using multiple agents · highest usage
|
||||
|
||||
enter default · s session · esc back
|
||||
-153
@@ -1,153 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: "lines[start_idx..].join(\"\\n\")"
|
||||
---
|
||||
• I need to check the codex-rs repository to explain why the project's binaries
|
||||
are large. The user is likely seeking specifics about the setup: are Rust
|
||||
builds static, what features are enabled, and is debug information included?
|
||||
It could be due to static linking, included OpenSSL, or how panic handling
|
||||
is set up. I should look into the Cargo.toml file to confirm features and
|
||||
profiles without needing to edit any code. Let's get started on this!
|
||||
|
||||
─ Worked for 0s ────────────────────────────────────────────────────────────────
|
||||
|
||||
• I’m going to scan the workspace and Cargo manifests to see build profiles and
|
||||
dependencies that impact binary size. Then I’ll summarize the main causes.
|
||||
|
||||
• Explored
|
||||
└ List ls -la
|
||||
Read Cargo.toml
|
||||
|
||||
• I'm reviewing the workspace's release profile, which has settings like
|
||||
lto=fat, strip=symbols, and codegen-units=1 to reduce binary size. However, it
|
||||
seems that "strip = symbols" only removes symbols, not debug information. In
|
||||
Rust 1.73+, both "symbols" and "debuginfo" stripping options are available. I
|
||||
should check whether using these results in a smaller binary. I’ll also skim
|
||||
through crates for heavy dependencies in Cargo.toml, including cli, core,
|
||||
exec, linux-sandbox, tui, login, ollama, and mcp.
|
||||
|
||||
• Ran for d in ansi-escape apply-patch arg0 cli common core exec execpolicy
|
||||
│ file-search linux-sandbox login mcp-client mcp-server ollama
|
||||
│ tui; do echo "--- $d/Cargo.toml"; sed -n '1,200p' $d/Cargo.toml; echo;
|
||||
│ … +1 line
|
||||
└ --- ansi-escape/Cargo.toml
|
||||
[package]
|
||||
… +243 lines
|
||||
] }
|
||||
tracing = { version
|
||||
|
||||
• Explored
|
||||
└ Read Cargo.toml
|
||||
|
||||
• I’m evaluating how binary size is influenced by various factors. There are
|
||||
many binaries, such as codex and codex-exec, which include dependencies by
|
||||
default, pulling in features like Tokio and hyper. For instance, reqwest
|
||||
includes default features like TLS through rustls. If default features remain
|
||||
enabled, it inflates the size with many additional components. I notice
|
||||
that for musl targets, using vendored builds of OpenSSL could significantly
|
||||
increase binary size. This is quite the intricate web of dependencies!
|
||||
|
||||
Analyzing musl and binary size
|
||||
|
||||
I’m considering the implications of building for musl with static linking.
|
||||
In this case, OpenSSL might come into play through some crates, but reqwest
|
||||
defaults to using rustls, although openssl-sys vendored could increase
|
||||
binary size. It’s interesting to think about whether they’re targeting musl,
|
||||
especially since it seems they might often be building for Mac and Linux with
|
||||
dynamic linking instead.
|
||||
|
||||
I also realize there’s a difference between debug and release builds — release
|
||||
should be smaller due to optimizations, but if they’re defaulting to debug
|
||||
builds, that could lead to much larger binaries.
|
||||
|
||||
Assessing crate impacts on size
|
||||
|
||||
I need to be precise when analyzing binary sizes from various crates. Some
|
||||
crates, like ratatui-image, include heavy features such as libjpeg decoders,
|
||||
which can really bulk things up. Then there are the tree-sitter crates that
|
||||
contain large static grammar tables, adding significant size. The starlark
|
||||
crate and tracing-subscriber also contribute to size increases.
|
||||
|
||||
With static linking per binary being standard in Rust, it leads to multiple
|
||||
copies of the same dependencies unless cdylibs are used. That makes the
|
||||
compile times longer, but using LTO helps reduce bloat somewhat through
|
||||
deduplication.
|
||||
|
||||
Evaluating binary size factors
|
||||
|
||||
Having multiple binaries indeed increases the overall size significantly.
|
||||
I'm considering options like upgrading to "opt-level = 'z'" or using
|
||||
"strip=debuginfo" for reducing size. It's interesting that while the profile
|
||||
only sets symbols for stripping, not debuginfo, this could inflate sizes if
|
||||
retained in release builds.
|
||||
|
||||
The distinction between "symbols" and "debuginfo" is critical; "symbols"
|
||||
keeps debug information which may not align with the goal of minimizing
|
||||
size. I’d like to verify with the Rust documentation, but my network access
|
||||
is restricted.
|
||||
|
||||
Explaining large binary sizes
|
||||
|
||||
I’m analyzing why binaries in this project are so large. The primary reasons
|
||||
include monolithic static linking, where each binary includes the full
|
||||
dependency graph without shared libraries. Heavy dependencies such as reqwest,
|
||||
hyper, and TLS components also contribute significantly to size, along with
|
||||
image decoders and other libraries.
|
||||
|
||||
Using MUSL targets means vendored OpenSSL can drastically increase the
|
||||
size due to static linking. Furthermore, if the release profile keeps debug
|
||||
information due to using "strip = symbols," that can lead to larger binaries
|
||||
as well. It’s a mix of several factors, including panic strategies and
|
||||
platform-specific linking behavior.
|
||||
|
||||
Identifying binary size contributors
|
||||
|
||||
I’m considering why binaries are still large even without static linking. The
|
||||
tracing-subscriber crate potentially adds weight because it includes regex,
|
||||
though it can sometimes use a lighter version. References from files like
|
||||
"core/Cargo.toml" show dependencies like reqwest and tokio, while the tui uses
|
||||
the image crate.
|
||||
|
||||
To present this clearly, I’ll format the final answer with two main sections:
|
||||
"Main Causes" and "Build-Mode Notes." I can also include brief suggestions for
|
||||
reducing size, but I want to stay focused on answering the user's question.
|
||||
|
||||
─ Worked for 0s ────────────────────────────────────────────────────────────────
|
||||
|
||||
• Here’s what’s driving size in this workspace’s binaries.
|
||||
|
||||
Main Causes
|
||||
|
||||
- Static linking style: Each bin (codex, codex-tui, codex-exec, codex-mcp-
|
||||
server, etc.) statically links its full dependency graph, so common code isn’t
|
||||
shared at runtime across executables.
|
||||
- Heavy deps (HTTP/TLS): reqwest brings in Hyper, HTTP/2, compressors,
|
||||
and a TLS stack (rustls by default; OpenSSL on musl). In core, login, tui,
|
||||
and ollama you enable reqwest with json/stream, which still pulls a large
|
||||
transitive set.
|
||||
- Image/terminal stack: codex-tui includes image (with jpeg), ratatui,
|
||||
crossterm, and ratatui-image, which together contribute significant code size
|
||||
(decoders, pixel ops, terminal backends).
|
||||
- Parsers/VMs: tree-sitter + tree-sitter-bash (in core and apply-patch) and
|
||||
starlark (in execpolicy) include sizeable parser/VM tables and runtimes.
|
||||
- Tokio runtime: Broad tokio features (rt-multi-thread, macros, process,
|
||||
signal) across many crates inflate code size even if only subsets are used
|
||||
per bin.
|
||||
- Panic + backtraces: Default panic = unwind and backtrace support keep
|
||||
unwinding tables and symbols that add weight.
|
||||
- Per-target OpenSSL (musl): For *-unknown-linux-musl, core enables openssl-
|
||||
sys with vendored, compiling OpenSSL into the binary—this adds multiple
|
||||
megabytes per executable.
|
||||
|
||||
Build-Mode Notes
|
||||
|
||||
- Release settings: You use lto = "fat" and codegen-units = 1 (good for size),
|
||||
but strip = "symbols" keeps debuginfo. Debuginfo is often the largest single
|
||||
contributor; if you build in release with that setting, binaries can still
|
||||
be large.
|
||||
- Debug builds: cargo build (dev profile) includes full debuginfo, no LTO, and
|
||||
assertions—outputs are much larger than cargo build --release.
|
||||
|
||||
If you want, I can outline targeted trims (e.g., strip = "debuginfo", opt-
|
||||
level = "z", panic abort, tighter tokio/reqwest features) and estimate impact
|
||||
per binary.
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
" "
|
||||
" "
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/status_and_layout.rs
|
||||
expression: normalized_backend_snapshot(terminal.backend())
|
||||
---
|
||||
" "
|
||||
" "
|
||||
" "
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/exec_flow.rs
|
||||
assertion_line: 217
|
||||
expression: lines_to_single_string(&aborted_long)
|
||||
---
|
||||
✗ You canceled the request to run echo
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/exec_flow.rs
|
||||
assertion_line: 183
|
||||
expression: lines_to_single_string(&aborted_multi)
|
||||
---
|
||||
✗ You canceled the request to run echo line1 ...
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/exec_flow.rs
|
||||
assertion_line: 47
|
||||
expression: lines_to_single_string(&decision)
|
||||
---
|
||||
✔ You approved codex to run echo hello world this time
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/exec_flow.rs
|
||||
expression: "format!(\"{buf:?}\")"
|
||||
---
|
||||
Buffer {
|
||||
area: Rect { x: 0, y: 0, width: 80, height: 14 },
|
||||
content: [
|
||||
" ",
|
||||
" ",
|
||||
" Would you like to run the following command? ",
|
||||
" ",
|
||||
" Reason: this is a test reason such as one that would be produced by the ",
|
||||
" model ",
|
||||
" ",
|
||||
" $ echo hello world ",
|
||||
" ",
|
||||
" ",
|
||||
"› 1. Yes, proceed (y) ",
|
||||
" 2. No, and tell Codex what to do differently (esc) ",
|
||||
" ",
|
||||
" Press enter to confirm or esc to cancel ",
|
||||
],
|
||||
styles: [
|
||||
x: 0, y: 0, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 46, y: 2, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 10, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC,
|
||||
x: 73, y: 4, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: ITALIC,
|
||||
x: 7, y: 5, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 4, y: 7, fg: Rgb(137, 180, 250), bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 8, y: 7, fg: Rgb(205, 214, 244), bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 20, y: 7, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 0, y: 10, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD | REVERSED,
|
||||
x: 0, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 48, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 51, y: 11, fg: Reset, bg: Reset, underline: Reset, modifier: NONE,
|
||||
x: 2, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 8, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 13, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
x: 28, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: BOLD,
|
||||
x: 31, y: 13, fg: Reset, bg: Reset, underline: Reset, modifier: DIM,
|
||||
]
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: active_blob(&chat)
|
||||
---
|
||||
• Explored
|
||||
└ List ls -la
|
||||
Read foo.txt
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/exec_flow.rs
|
||||
expression: combined
|
||||
---
|
||||
• Viewed image example.png
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/goal_menu.rs
|
||||
expression: rendered_goal_summary(&mut rx)
|
||||
---
|
||||
Goal
|
||||
Status: stalled
|
||||
Objective: Keep improving the bare goal command until it feels calm and useful.
|
||||
Time used: 1m
|
||||
Tokens used: 12.5K
|
||||
|
||||
Commands: /goal edit, /goal resume, /goal clear
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/goal_menu.rs
|
||||
expression: rendered_goal_summary(&mut rx)
|
||||
---
|
||||
Goal
|
||||
Status: usage limited
|
||||
Objective: Keep improving the bare goal command until it feels calm and useful.
|
||||
Time used: 1m
|
||||
Tokens used: 12.5K
|
||||
|
||||
Commands: /goal edit, /goal resume, /goal clear
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/exec_flow.rs
|
||||
expression: normalized_backend_snapshot(terminal.backend())
|
||||
---
|
||||
" "
|
||||
"• Working (0s • esc to interrupt) "
|
||||
" "
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: snapshot
|
||||
---
|
||||
cells=1
|
||||
■ Conversation interrupted - tell the model what to do differently. Something went wrong? Hit `/feedback` to report the issue.
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/helpers.rs
|
||||
expression: combined
|
||||
---
|
||||
↳ Hook · Heads up from the hook
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/exec_flow.rs
|
||||
expression: combined
|
||||
---
|
||||
• Viewed image example.png
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/compaction_tests.rs
|
||||
expression: "normalize_compaction_snapshot(render_bottom_popup(&chat, 80))"
|
||||
---
|
||||
• Compacting context (<elapsed> • esc to interrupt)
|
||||
└ Making room to continue.
|
||||
|
||||
|
||||
› Ask Codex to do anything
|
||||
|
||||
GPT-5.6-Sol default · /tmp/project
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/permission_shortcuts_tests.rs
|
||||
assertion_line: 156
|
||||
expression: "render_bottom_popup(&chat, 80)"
|
||||
---
|
||||
Update Model Permissions
|
||||
|
||||
|
||||
› Loading permission profiles…
|
||||
|
||||
enter select · esc back
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/plan_mode.rs
|
||||
expression: "render_bottom_popup(&chat, 36)"
|
||||
---
|
||||
› make a plan
|
||||
|
||||
gpt-5 default · /tmp/project
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/plan_mode.rs
|
||||
expression: popup
|
||||
---
|
||||
Implement this plan?
|
||||
|
||||
|
||||
1. Yes, implement this plan Switch to Default and start coding
|
||||
› 2. Yes, clear context and implement Fresh thread with this plan
|
||||
3. No, stay in Plan mode Continue planning with the model
|
||||
|
||||
enter select · esc back
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/helpers.rs
|
||||
expression: combined
|
||||
---
|
||||
↳ Hook · Heads up from the hook
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/helpers.rs
|
||||
expression: combined
|
||||
---
|
||||
↳ Hook · Heads up from the hook
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/exec_flow.rs
|
||||
expression: normalized_backend_snapshot(terminal.backend())
|
||||
---
|
||||
" "
|
||||
"• Working (0s • esc to interrupt) "
|
||||
" "
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" GPT-5.6-Sol default · /tmp/project "
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: header
|
||||
---
|
||||
› Ask Codex to do anything
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/helpers.rs
|
||||
expression: combined
|
||||
---
|
||||
↳ Hook · Heads up from the hook
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests.rs
|
||||
expression: terminal.backend().vt100().screen().contents()
|
||||
---
|
||||
✨ New version available! Would you like to update?
|
||||
|
||||
Full release notes: https://github.com/openai/codex/releases/latest
|
||||
|
||||
|
||||
› 1. Yes, update now
|
||||
2. No, not now
|
||||
3. Don't remind me
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
@@ -63,10 +63,12 @@ async fn bedrock_astra_model_and_reasoning_pickers() {
|
||||
_ => None,
|
||||
});
|
||||
chat.open_advanced_reasoning_popup(advanced.expect("advanced reasoning popup"));
|
||||
assert_chatwidget_snapshot!(
|
||||
format!("bedrock_{name}_astra_advanced_reasoning"),
|
||||
render_bottom_popup(&chat, /*width*/ 100)
|
||||
);
|
||||
if name == "mantle" {
|
||||
assert_chatwidget_snapshot!(
|
||||
"bedrock_mantle_astra_advanced_reasoning",
|
||||
render_bottom_popup(&chat, /*width*/ 100)
|
||||
);
|
||||
}
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Char('2')));
|
||||
let selected =
|
||||
std::iter::from_fn(|| events.try_recv().ok()).find_map(|event| match event {
|
||||
|
||||
@@ -92,9 +92,10 @@ async fn compaction_status_survives_follow_up_and_preserves_turn_time() {
|
||||
async fn manual_compaction_shows_status_before_backend_events() {
|
||||
let (mut chat, _rx, _ops) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.dispatch_command(SlashCommand::Compact);
|
||||
assert_chatwidget_snapshot!(
|
||||
"manual_compaction_pending",
|
||||
normalize_compaction_snapshot(render_bottom_popup(&chat, /*width*/ 80))
|
||||
assert!(chat.bottom_pane.status_indicator_visible());
|
||||
assert_eq!(
|
||||
chat.bottom_pane.status_widget().unwrap().header(),
|
||||
"Compacting context"
|
||||
);
|
||||
assert!(chat.handle_turn_start_rejection("Could not start compaction".to_string()));
|
||||
assert!(!chat.bottom_pane.status_indicator_visible());
|
||||
|
||||
@@ -420,259 +420,6 @@ declined
|
||||
");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exec_approval_emits_proposed_command_and_decision_history() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
// Trigger an exec approval request with a short, single-line command
|
||||
let ev = ExecApprovalRequestEvent {
|
||||
kind: Default::default(),
|
||||
call_id: "call-short".into(),
|
||||
approval_id: Some("call-short".into()),
|
||||
turn_id: "turn-short".into(),
|
||||
environment_id: None,
|
||||
command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],
|
||||
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
|
||||
reason: Some(
|
||||
"this is a test reason such as one that would be produced by the model".into(),
|
||||
),
|
||||
network_approval_context: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
proposed_network_policy_amendments: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
};
|
||||
handle_exec_approval_request(&mut chat, "sub-short", ev);
|
||||
|
||||
let proposed_cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
proposed_cells.is_empty(),
|
||||
"expected approval request to render via modal without emitting history cells"
|
||||
);
|
||||
|
||||
// The approval modal should display the command snippet for user confirmation.
|
||||
let area = Rect::new(0, 0, 80, chat.desired_height(/*width*/ 80));
|
||||
let mut buf = ratatui::buffer::Buffer::empty(area);
|
||||
chat.render(area, &mut buf);
|
||||
assert_chatwidget_snapshot!("exec_approval_modal_exec", format!("{buf:?}"));
|
||||
|
||||
// Approve via keyboard and verify a concise decision history line is added
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
let decision = drain_insert_history(&mut rx)
|
||||
.pop()
|
||||
.expect("expected decision cell in history");
|
||||
assert_chatwidget_snapshot!(
|
||||
"exec_approval_history_decision_approved_short",
|
||||
lines_to_single_string(&decision)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_server_exec_approval_request_splits_shell_wrapped_command() {
|
||||
let script = r#"python3 -c 'print("Hello, world!")'"#;
|
||||
let request = exec_approval_request_from_params(
|
||||
AppServerCommandExecutionRequestApprovalParams {
|
||||
kind: Default::default(),
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "item-1".to_string(),
|
||||
started_at_ms: 0,
|
||||
approval_id: Some("approval-1".to_string()),
|
||||
environment_id: None,
|
||||
reason: None,
|
||||
network_approval_context: None,
|
||||
command: Some(
|
||||
shlex::try_join(["/bin/zsh", "-lc", script])
|
||||
.expect("round-trippable shell wrapper"),
|
||||
),
|
||||
cwd: Some(test_path_buf("/tmp").abs().into()),
|
||||
command_actions: None,
|
||||
additional_permissions: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
proposed_network_policy_amendments: None,
|
||||
available_decisions: None,
|
||||
},
|
||||
&test_path_buf("/tmp").abs(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
request.command,
|
||||
vec![
|
||||
"/bin/zsh".to_string(),
|
||||
"-lc".to_string(),
|
||||
script.to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exec_approval_uses_approval_id_when_present() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
handle_exec_approval_request(
|
||||
&mut chat,
|
||||
"sub-short",
|
||||
ExecApprovalRequestEvent {
|
||||
kind: Default::default(),
|
||||
call_id: "call-parent".into(),
|
||||
approval_id: Some("approval-subcommand".into()),
|
||||
turn_id: "turn-short".into(),
|
||||
environment_id: None,
|
||||
command: vec!["bash".into(), "-lc".into(), "echo hello world".into()],
|
||||
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
|
||||
reason: Some(
|
||||
"this is a test reason such as one that would be produced by the model".into(),
|
||||
),
|
||||
network_approval_context: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
proposed_network_policy_amendments: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
},
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::NONE));
|
||||
|
||||
let mut found = false;
|
||||
while let Ok(app_ev) = rx.try_recv() {
|
||||
if let AppEvent::SubmitThreadOp {
|
||||
op: Op::ExecApproval { id, decision, .. },
|
||||
..
|
||||
} = app_ev
|
||||
{
|
||||
assert_eq!(id, "approval-subcommand");
|
||||
assert_matches!(
|
||||
decision,
|
||||
codex_app_server_protocol::CommandExecutionApprovalDecision::Accept
|
||||
);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(found, "expected ExecApproval op to be sent");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exec_approval_decision_truncates_multiline_and_long_commands() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
// Multiline command: modal should show full command, history records decision only
|
||||
let ev_multi = ExecApprovalRequestEvent {
|
||||
kind: Default::default(),
|
||||
call_id: "call-multi".into(),
|
||||
approval_id: Some("call-multi".into()),
|
||||
turn_id: "turn-multi".into(),
|
||||
environment_id: None,
|
||||
command: vec!["bash".into(), "-lc".into(), "echo line1\necho line2".into()],
|
||||
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
|
||||
reason: Some(
|
||||
"this is a test reason such as one that would be produced by the model".into(),
|
||||
),
|
||||
network_approval_context: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
proposed_network_policy_amendments: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
};
|
||||
handle_exec_approval_request(&mut chat, "sub-multi", ev_multi);
|
||||
let proposed_multi = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
proposed_multi.is_empty(),
|
||||
"expected multiline approval request to render via modal without emitting history cells"
|
||||
);
|
||||
|
||||
let area = Rect::new(0, 0, 80, chat.desired_height(/*width*/ 80));
|
||||
let mut buf = ratatui::buffer::Buffer::empty(area);
|
||||
chat.render(area, &mut buf);
|
||||
let mut saw_first_line = false;
|
||||
for y in 0..area.height {
|
||||
let mut row = String::new();
|
||||
for x in 0..area.width {
|
||||
row.push(buf[(x, y)].symbol().chars().next().unwrap_or(' '));
|
||||
}
|
||||
if row.contains("echo line1") {
|
||||
saw_first_line = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
saw_first_line,
|
||||
"expected modal to show first line of multiline snippet"
|
||||
);
|
||||
|
||||
// Deny via keyboard; decision snippet should be single-line and elided with " ..."
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE));
|
||||
let aborted_multi = drain_insert_history(&mut rx)
|
||||
.pop()
|
||||
.expect("expected aborted decision cell (multiline)");
|
||||
assert_chatwidget_snapshot!(
|
||||
"exec_approval_history_decision_aborted_multiline",
|
||||
lines_to_single_string(&aborted_multi)
|
||||
);
|
||||
|
||||
// Very long single-line command: decision snippet should be truncated <= 80 chars with trailing ...
|
||||
let long = format!("echo {}", "a".repeat(200));
|
||||
let ev_long = ExecApprovalRequestEvent {
|
||||
kind: Default::default(),
|
||||
call_id: "call-long".into(),
|
||||
approval_id: Some("call-long".into()),
|
||||
turn_id: "turn-long".into(),
|
||||
environment_id: None,
|
||||
command: vec!["bash".into(), "-lc".into(), long],
|
||||
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
|
||||
reason: None,
|
||||
network_approval_context: None,
|
||||
proposed_execpolicy_amendment: None,
|
||||
proposed_network_policy_amendments: None,
|
||||
additional_permissions: None,
|
||||
available_decisions: None,
|
||||
};
|
||||
handle_exec_approval_request(&mut chat, "sub-long", ev_long);
|
||||
let proposed_long = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
proposed_long.is_empty(),
|
||||
"expected long approval request to avoid emitting history cells before decision"
|
||||
);
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE));
|
||||
let aborted_long = drain_insert_history(&mut rx)
|
||||
.pop()
|
||||
.expect("expected aborted decision cell (long)");
|
||||
assert_chatwidget_snapshot!(
|
||||
"exec_approval_history_decision_aborted_long",
|
||||
lines_to_single_string(&aborted_long)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preamble_keeps_working_status_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.thread_id = Some(ThreadId::new());
|
||||
|
||||
// Regression sequence: a preamble line is committed to history before any exec/tool event.
|
||||
// After commentary completes, the status row should be restored before subsequent work.
|
||||
chat.on_task_started();
|
||||
chat.on_agent_message_delta("Preamble line\n".to_string());
|
||||
chat.on_commit_tick();
|
||||
drain_insert_history(&mut rx);
|
||||
complete_assistant_message(
|
||||
&mut chat,
|
||||
"msg-commentary-snapshot",
|
||||
"Preamble line\n",
|
||||
Some(MessagePhase::Commentary),
|
||||
);
|
||||
|
||||
let height = chat.desired_height(/*width*/ 80);
|
||||
let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(80, height))
|
||||
.expect("create terminal");
|
||||
terminal
|
||||
.draw(|f| chat.render(f.area(), f.buffer_mut()))
|
||||
.expect("draw preamble + status widget");
|
||||
assert_chatwidget_snapshot!(
|
||||
"preamble_keeps_working_status",
|
||||
normalized_backend_snapshot(terminal.backend())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unified_exec_begin_restores_status_indicator_after_preamble() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
@@ -1319,7 +1066,7 @@ async fn view_image_tool_call_adds_history_cell() {
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected a single history cell");
|
||||
let combined = lines_to_single_string(&cells[0]);
|
||||
assert_chatwidget_snapshot!("local_image_attachment_history_snapshot", combined);
|
||||
assert_eq!(combined, "• Viewed image example.png\n");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1334,7 +1081,7 @@ async fn view_image_tool_call_preserves_foreign_path() {
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected a single history cell");
|
||||
let combined = lines_to_single_string(&cells[0]);
|
||||
assert_chatwidget_snapshot!("foreign_image_attachment_history_snapshot", combined);
|
||||
assert_eq!(combined, "• Viewed image example.png\n");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1347,18 +1094,6 @@ async fn image_generation_begin_restores_working_status_after_single_line_preamb
|
||||
|
||||
assert!(chat.bottom_pane.is_task_running());
|
||||
assert!(chat.bottom_pane.status_indicator_visible());
|
||||
|
||||
let width: u16 = 80;
|
||||
let height = chat.desired_height(width);
|
||||
let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(width, height))
|
||||
.expect("create terminal");
|
||||
terminal
|
||||
.draw(|frame| chat.render(frame.area(), frame.buffer_mut()))
|
||||
.expect("draw image generation status");
|
||||
assert_chatwidget_snapshot!(
|
||||
"image_generation_begin_restores_working_status",
|
||||
normalized_backend_snapshot(terminal.backend())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1422,7 +1157,8 @@ async fn exec_history_extends_previous_when_consecutive() {
|
||||
"",
|
||||
/*exit_code*/ 0,
|
||||
);
|
||||
assert_chatwidget_snapshot!("exploring_step4_finish_cat_foo", active_blob(&chat));
|
||||
let explored_foo = active_blob(&chat);
|
||||
assert_chatwidget_snapshot!("exploring_step4_finish_cat_foo", &explored_foo);
|
||||
|
||||
// 5) Start & complete "sed -n 100,200p foo.txt" (treated as Read of foo.txt)
|
||||
let begin_sed_range = begin_exec(&mut chat, "call-sed-range", "sed -n 100,200p foo.txt");
|
||||
@@ -1433,7 +1169,7 @@ async fn exec_history_extends_previous_when_consecutive() {
|
||||
"",
|
||||
/*exit_code*/ 0,
|
||||
);
|
||||
assert_chatwidget_snapshot!("exploring_step5_finish_sed_range", active_blob(&chat));
|
||||
assert_eq!(active_blob(&chat), explored_foo);
|
||||
|
||||
// 6) Start & complete "cat bar.txt"
|
||||
let begin_cat_bar = begin_exec(&mut chat, "call-cat-bar", "cat bar.txt");
|
||||
|
||||
@@ -28,34 +28,6 @@ async fn goal_menu_paused_snapshot() {
|
||||
assert_chatwidget_snapshot!("goal_menu_paused", rendered_goal_summary(&mut rx));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn goal_menu_blocked_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let thread_id = ThreadId::new();
|
||||
|
||||
chat.show_goal_summary(test_goal(
|
||||
thread_id,
|
||||
AppThreadGoalStatus::Blocked,
|
||||
/*token_budget*/ None,
|
||||
));
|
||||
|
||||
assert_chatwidget_snapshot!("goal_menu_blocked", rendered_goal_summary(&mut rx));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn goal_menu_usage_limited_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let thread_id = ThreadId::new();
|
||||
|
||||
chat.show_goal_summary(test_goal(
|
||||
thread_id,
|
||||
AppThreadGoalStatus::UsageLimited,
|
||||
/*token_budget*/ None,
|
||||
));
|
||||
|
||||
assert_chatwidget_snapshot!("goal_menu_usage_limited", rendered_goal_summary(&mut rx));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn goal_menu_budget_limited_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::*;
|
||||
use codex_app_server_protocol::ImageGenerationItem;
|
||||
use codex_app_server_protocol::PluginAvailability;
|
||||
use codex_utils_absolute_path::test_support::PathExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
pub(super) async fn test_config() -> (tempfile::TempDir, Config) {
|
||||
// Start from the built-in defaults so tests do not inherit host/system config.
|
||||
@@ -1751,11 +1752,10 @@ pub(super) fn hook_run(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn assert_hook_events_snapshot(
|
||||
pub(super) async fn assert_hook_events(
|
||||
event_name: codex_app_server_protocol::HookEventName,
|
||||
run_id: &str,
|
||||
status_message: &str,
|
||||
snapshot_name: &str,
|
||||
) {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.on_task_started();
|
||||
@@ -1816,7 +1816,7 @@ pub(super) async fn assert_hook_events_snapshot(
|
||||
.iter()
|
||||
.map(|lines| lines_to_single_string(lines))
|
||||
.collect::<String>();
|
||||
assert_chatwidget_snapshot!(snapshot_name, combined);
|
||||
assert_eq!(combined, "↳ Hook · Heads up from the hook\n");
|
||||
}
|
||||
|
||||
/// Normalize timestamps only in structurally identified completion footer cells.
|
||||
|
||||
@@ -156,10 +156,7 @@ async fn permission_shortcuts_load_once_and_reuse_the_picker_catalog() {
|
||||
chat.chat_keymap.next_permission_mode = vec![crate::key_hint::plain(KeyCode::F(8))];
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::F(8)));
|
||||
let request_id = assert_matches!(events.try_recv(), Ok(AppEvent::FetchPermissionProfiles { request_id, .. }) => request_id);
|
||||
assert_chatwidget_snapshot!(
|
||||
"permission_shortcut_loading",
|
||||
render_bottom_popup(&chat, /*width*/ 80)
|
||||
);
|
||||
assert!(chat.bottom_pane.has_active_view());
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Esc));
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::F(8)));
|
||||
assert!(events.try_recv().is_err(), "reuse pending fetch");
|
||||
|
||||
@@ -79,19 +79,6 @@ async fn plan_draft_footer_snapshot() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_draft_footer_narrow_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
chat.bottom_pane
|
||||
.set_composer_text("make a plan".to_string(), Vec::new(), Vec::new());
|
||||
chat.pre_draw_tick();
|
||||
|
||||
assert_chatwidget_snapshot!(
|
||||
"plan_draft_footer_narrow",
|
||||
normalize_snapshot_paths(render_bottom_popup(&chat, /*width*/ 36))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_implementation_popup_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
@@ -115,17 +102,6 @@ async fn plan_implementation_popup_context_usage_snapshot() {
|
||||
assert_chatwidget_snapshot!("plan_implementation_popup_context_usage", popup);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_implementation_popup_no_selected_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
chat.on_plan_item_completed("- Step 1\n- Step 2\n".to_string());
|
||||
chat.open_plan_implementation_prompt();
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert_chatwidget_snapshot!("plan_implementation_popup_no_selected", popup);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plan_implementation_popup_yes_emits_submit_message_event() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
|
||||
@@ -3,30 +3,6 @@ use pretty_assertions::assert_eq;
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::TestBackend;
|
||||
|
||||
#[tokio::test]
|
||||
async fn forked_thread_history_line_without_name_shows_id_once_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
let forked_from_id =
|
||||
ThreadId::from_string("019c2d47-4935-7423-a190-05691f566092").expect("forked id");
|
||||
chat.emit_forked_thread_event(forked_from_id, /*fork_parent_title*/ None);
|
||||
|
||||
let history_cell = tokio::time::timeout(std::time::Duration::from_secs(2), async {
|
||||
loop {
|
||||
match rx.recv().await {
|
||||
Some(AppEvent::InsertHistoryCell(cell)) => break cell,
|
||||
Some(_) => continue,
|
||||
None => panic!("app event channel closed before forked thread history was emitted"),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("timed out waiting for forked thread history");
|
||||
let combined = lines_to_single_string(&history_cell.display_lines(/*width*/ 80));
|
||||
|
||||
assert_chatwidget_snapshot!("forked_thread_history_line_without_name", combined);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn suppressed_interrupted_turn_notice_skips_history_warning() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
@@ -147,57 +123,34 @@ async fn slash_side_is_rejected_for_side_threads() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_side_is_rejected_during_review_mode() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.review.is_review_mode = true;
|
||||
async fn side_aliases_are_rejected_during_review_mode() {
|
||||
for (command, name) in [(SlashCommand::Side, "side"), (SlashCommand::Btw, "btw")] {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.review.is_review_mode = true;
|
||||
|
||||
chat.dispatch_command(SlashCommand::Side);
|
||||
chat.dispatch_command(command);
|
||||
|
||||
let event = rx
|
||||
.try_recv()
|
||||
.expect("expected review-mode side conversation error");
|
||||
match event {
|
||||
AppEvent::InsertHistoryCell(cell) => {
|
||||
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
|
||||
assert!(
|
||||
rendered.contains("'/side' is unavailable while code review is running."),
|
||||
"expected review-mode side conversation error, got {rendered:?}"
|
||||
);
|
||||
let event = rx
|
||||
.try_recv()
|
||||
.expect("expected review-mode side conversation error");
|
||||
match event {
|
||||
AppEvent::InsertHistoryCell(cell) => {
|
||||
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
|
||||
assert!(
|
||||
rendered.contains(&format!(
|
||||
"'/{name}' is unavailable while code review is running."
|
||||
)),
|
||||
"expected review-mode side conversation error, got {rendered:?}"
|
||||
);
|
||||
}
|
||||
other => panic!("expected InsertHistoryCell error, got {other:?}"),
|
||||
}
|
||||
other => panic!("expected InsertHistoryCell error, got {other:?}"),
|
||||
assert!(rx.try_recv().is_err(), "expected no follow-up events");
|
||||
assert!(
|
||||
op_rx.try_recv().is_err(),
|
||||
"expected no side conversation op"
|
||||
);
|
||||
}
|
||||
assert!(rx.try_recv().is_err(), "expected no follow-up events");
|
||||
assert!(
|
||||
op_rx.try_recv().is_err(),
|
||||
"expected no side conversation op"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_btw_is_rejected_during_review_mode() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.review.is_review_mode = true;
|
||||
|
||||
chat.dispatch_command(SlashCommand::Btw);
|
||||
|
||||
let event = rx
|
||||
.try_recv()
|
||||
.expect("expected review-mode btw conversation error");
|
||||
match event {
|
||||
AppEvent::InsertHistoryCell(cell) => {
|
||||
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
|
||||
assert!(
|
||||
rendered.contains("'/btw' is unavailable while code review is running."),
|
||||
"expected review-mode btw conversation error, got {rendered:?}"
|
||||
);
|
||||
}
|
||||
other => panic!("expected InsertHistoryCell error, got {other:?}"),
|
||||
}
|
||||
assert!(rx.try_recv().is_err(), "expected no follow-up events");
|
||||
assert!(
|
||||
op_rx.try_recv().is_err(),
|
||||
"expected no side conversation op"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -248,145 +201,83 @@ async fn submit_user_message_as_plain_user_turn_does_not_run_shell_commands() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_side_without_args_starts_empty_side_conversation() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let parent_thread_id = ThreadId::new();
|
||||
chat.thread_id = Some(parent_thread_id);
|
||||
chat.on_task_started();
|
||||
chat.bottom_pane
|
||||
.set_composer_text("/side".to_string(), Vec::new(), Vec::new());
|
||||
async fn side_aliases_without_args_start_empty_side_conversations() {
|
||||
for input in ["/side", "/btw"] {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let parent_thread_id = ThreadId::new();
|
||||
chat.thread_id = Some(parent_thread_id);
|
||||
chat.on_task_started();
|
||||
chat.bottom_pane
|
||||
.set_composer_text(input.to_string(), Vec::new(), Vec::new());
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
|
||||
|
||||
assert_matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::StartSide {
|
||||
parent_thread_id: emitted_parent_thread_id,
|
||||
user_message: None,
|
||||
}) if emitted_parent_thread_id == parent_thread_id
|
||||
);
|
||||
assert!(
|
||||
op_rx.try_recv().is_err(),
|
||||
"bare /side should not submit an op on the parent thread"
|
||||
);
|
||||
assert!(chat.input_queue.queued_user_messages.is_empty());
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
|
||||
assert_matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::StartSide {
|
||||
parent_thread_id: emitted_parent_thread_id,
|
||||
user_message: None,
|
||||
}) if emitted_parent_thread_id == parent_thread_id
|
||||
);
|
||||
assert!(
|
||||
op_rx.try_recv().is_err(),
|
||||
"bare {input} should not submit an op on the parent thread"
|
||||
);
|
||||
assert!(chat.input_queue.queued_user_messages.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_btw_without_args_starts_empty_side_conversation() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let parent_thread_id = ThreadId::new();
|
||||
chat.thread_id = Some(parent_thread_id);
|
||||
chat.on_task_started();
|
||||
chat.bottom_pane
|
||||
.set_composer_text("/btw".to_string(), Vec::new(), Vec::new());
|
||||
async fn side_aliases_request_forked_questions_while_task_running() {
|
||||
for input in ["/side explore the codebase", "/btw explore the codebase"] {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let parent_thread_id = ThreadId::new();
|
||||
chat.thread_id = Some(parent_thread_id);
|
||||
chat.local_settings.tui.status_line = Some(vec!["model-with-reasoning".to_string()]);
|
||||
chat.refresh_status_line();
|
||||
chat.on_task_started();
|
||||
chat.show_welcome_banner = false;
|
||||
chat.bottom_pane
|
||||
.set_composer_text(input.to_string(), Vec::new(), Vec::new());
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
|
||||
assert_matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::StartSide {
|
||||
parent_thread_id: emitted_parent_thread_id,
|
||||
user_message: Some(user_message),
|
||||
}) if emitted_parent_thread_id == parent_thread_id
|
||||
&& user_message
|
||||
== UserMessage {
|
||||
text: "explore the codebase".to_string(),
|
||||
local_images: Vec::new(),
|
||||
remote_image_urls: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
mention_bindings: Vec::new(),
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
op_rx.try_recv().is_err(),
|
||||
"expected no op on the parent thread"
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::StartSide {
|
||||
parent_thread_id: emitted_parent_thread_id,
|
||||
user_message: None,
|
||||
}) if emitted_parent_thread_id == parent_thread_id
|
||||
);
|
||||
assert!(
|
||||
op_rx.try_recv().is_err(),
|
||||
"bare /btw should not submit an op on the parent thread"
|
||||
);
|
||||
assert!(chat.input_queue.queued_user_messages.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_side_requests_forked_side_question_while_task_running() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let parent_thread_id = ThreadId::new();
|
||||
chat.thread_id = Some(parent_thread_id);
|
||||
chat.local_settings.tui.status_line = Some(vec!["model-with-reasoning".to_string()]);
|
||||
chat.refresh_status_line();
|
||||
chat.on_task_started();
|
||||
chat.show_welcome_banner = false;
|
||||
chat.bottom_pane.set_composer_text(
|
||||
"/side explore the codebase".to_string(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
|
||||
|
||||
assert_matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::StartSide {
|
||||
parent_thread_id: emitted_parent_thread_id,
|
||||
user_message: Some(user_message),
|
||||
}) if emitted_parent_thread_id == parent_thread_id
|
||||
&& user_message
|
||||
== UserMessage {
|
||||
text: "explore the codebase".to_string(),
|
||||
local_images: Vec::new(),
|
||||
remote_image_urls: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
mention_bindings: Vec::new(),
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
op_rx.try_recv().is_err(),
|
||||
"expected no op on the parent thread"
|
||||
);
|
||||
|
||||
let width = 80;
|
||||
let height = chat.desired_height(width);
|
||||
let mut terminal = Terminal::new(TestBackend::new(width, height)).expect("create terminal");
|
||||
terminal
|
||||
.draw(|f| chat.render(f.area(), f.buffer_mut()))
|
||||
.expect("draw side conversation footer");
|
||||
assert_chatwidget_snapshot!(
|
||||
"slash_side_requests_forked_side_question_while_task_running",
|
||||
normalized_backend_snapshot(terminal.backend())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_btw_requests_forked_side_question_while_task_running() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let parent_thread_id = ThreadId::new();
|
||||
chat.thread_id = Some(parent_thread_id);
|
||||
chat.on_task_started();
|
||||
chat.bottom_pane.set_composer_text(
|
||||
"/btw explore the codebase".to_string(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
|
||||
|
||||
assert_matches!(
|
||||
rx.try_recv(),
|
||||
Ok(AppEvent::StartSide {
|
||||
parent_thread_id: emitted_parent_thread_id,
|
||||
user_message: Some(user_message),
|
||||
}) if emitted_parent_thread_id == parent_thread_id
|
||||
&& user_message
|
||||
== UserMessage {
|
||||
text: "explore the codebase".to_string(),
|
||||
local_images: Vec::new(),
|
||||
remote_image_urls: Vec::new(),
|
||||
text_elements: Vec::new(),
|
||||
mention_bindings: Vec::new(),
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
op_rx.try_recv().is_err(),
|
||||
"expected no op on the parent thread"
|
||||
);
|
||||
if input.starts_with("/side") {
|
||||
let width = 80;
|
||||
let height = chat.desired_height(width);
|
||||
let mut terminal =
|
||||
Terminal::new(TestBackend::new(width, height)).expect("create terminal");
|
||||
terminal
|
||||
.draw(|f| chat.render(f.area(), f.buffer_mut()))
|
||||
.expect("draw side conversation footer");
|
||||
assert_chatwidget_snapshot!(
|
||||
"slash_side_requests_forked_side_question_while_task_running",
|
||||
normalized_backend_snapshot(terminal.backend())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1530,12 +1530,12 @@ async fn no_op_stub_slash_command_is_available_from_local_recall() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_quit_requests_exit() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.dispatch_command(SlashCommand::Quit);
|
||||
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::Exit(ExitMode::ShutdownFirst)));
|
||||
async fn slash_exit_aliases_request_exit() {
|
||||
for command in [SlashCommand::Quit, SlashCommand::Exit] {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.dispatch_command(command);
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::Exit(ExitMode::ShutdownFirst)));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -2631,15 +2631,6 @@ async fn queued_menu_slash_keeps_agent_turn_complete_notification() {
|
||||
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_exit_requests_exit() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.dispatch_command(SlashCommand::Exit);
|
||||
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::Exit(ExitMode::ShutdownFirst)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_stop_submits_background_terminal_cleanup() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
@@ -2771,23 +2762,25 @@ async fn slash_archive_cancellation_keeps_task_running() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_memory_drop_reports_stubbed_feature() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
async fn slash_memory_maintenance_reports_stubbed_feature() {
|
||||
for command in [SlashCommand::MemoryDrop, SlashCommand::MemoryUpdate] {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.dispatch_command(SlashCommand::MemoryDrop);
|
||||
chat.dispatch_command(command);
|
||||
|
||||
let event = rx.try_recv().expect("expected unsupported-feature error");
|
||||
match event {
|
||||
AppEvent::InsertHistoryCell(cell) => {
|
||||
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
|
||||
assert!(rendered.contains("Memory maintenance: Not available in TUI yet."));
|
||||
let event = rx.try_recv().expect("expected unsupported-feature error");
|
||||
match event {
|
||||
AppEvent::InsertHistoryCell(cell) => {
|
||||
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
|
||||
assert!(rendered.contains("Memory maintenance: Not available in TUI yet."));
|
||||
}
|
||||
other => panic!("expected InsertHistoryCell error, got {other:?}"),
|
||||
}
|
||||
other => panic!("expected InsertHistoryCell error, got {other:?}"),
|
||||
assert!(
|
||||
op_rx.try_recv().is_err(),
|
||||
"expected no memory op to be sent"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
op_rx.try_recv().is_err(),
|
||||
"expected no memory op to be sent"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -2878,26 +2871,6 @@ async fn slash_memories_opens_memory_menu() {
|
||||
assert!(op_rx.try_recv().is_err(), "expected no core op to be sent");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_memory_update_reports_stubbed_feature() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.dispatch_command(SlashCommand::MemoryUpdate);
|
||||
|
||||
let event = rx.try_recv().expect("expected unsupported-feature error");
|
||||
match event {
|
||||
AppEvent::InsertHistoryCell(cell) => {
|
||||
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
|
||||
assert!(rendered.contains("Memory maintenance: Not available in TUI yet."));
|
||||
}
|
||||
other => panic!("expected InsertHistoryCell error, got {other:?}"),
|
||||
}
|
||||
assert!(
|
||||
op_rx.try_recv().is_err(),
|
||||
"expected no memory op to be sent"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_resume_opens_picker_while_mcp_startup_is_running() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
@@ -3031,34 +3004,19 @@ async fn slash_pets_with_arg_selects_named_pet() {
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn slash_pets_disable_disables_pets_even_on_unsupported_terminal() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
force_tmux_pet_image_unsupported(&mut chat);
|
||||
async fn slash_pet_disable_aliases_work_on_unsupported_terminal() {
|
||||
for input in ["/pets disable", "/pet hide"] {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
force_tmux_pet_image_unsupported(&mut chat);
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("/pets disable".to_string(), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
|
||||
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::PetDisabled));
|
||||
assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));
|
||||
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn slash_pet_hide_disables_pets_even_on_unsupported_terminal() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
force_tmux_pet_image_unsupported(&mut chat);
|
||||
|
||||
chat.bottom_pane
|
||||
.set_composer_text("/pet hide".to_string(), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
|
||||
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::PetDisabled));
|
||||
assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));
|
||||
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
|
||||
chat.bottom_pane
|
||||
.set_composer_text(input.to_string(), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::FollowTranscript));
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::PetDisabled));
|
||||
assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));
|
||||
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -3736,10 +3694,6 @@ async fn transcript_copy_feedback_stays_in_the_footer_without_history_or_interru
|
||||
"transcript_copy_unconfirmed",
|
||||
render_bottom_popup(&chat, /*width*/ 80)
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
"transcript_copy_unconfirmed_narrow",
|
||||
render_bottom_popup(&chat, /*width*/ 40)
|
||||
);
|
||||
chat.open_warnings(&[Arc::new(history_cell::new_warning_event(
|
||||
"selected café".into(),
|
||||
))]);
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/slash_commands.rs
|
||||
expression: "render_bottom_popup(&chat, 40)"
|
||||
---
|
||||
› Ask Codex to do anything
|
||||
|
||||
Copy unconfirmed; /export saves chat
|
||||
@@ -2468,23 +2468,29 @@ async fn ui_snapshots_small_heights_idle() {
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshot test: ChatWidget at very small heights (task running)
|
||||
// Validates how status + composer are presented within tight space.
|
||||
// Running state remains hidden when the terminal is too short to present it.
|
||||
#[tokio::test]
|
||||
async fn ui_snapshots_small_heights_task_running() {
|
||||
async fn ui_small_heights_hide_running_state() {
|
||||
use ratatui::Terminal;
|
||||
use ratatui::backend::TestBackend;
|
||||
let (idle_chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
// Activate status line
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
handle_agent_reasoning_delta(&mut chat, "**Thinking**");
|
||||
for h in [1u16, 2, 3] {
|
||||
let name = format!("chat_small_running_h{h}");
|
||||
let mut idle_terminal =
|
||||
Terminal::new(TestBackend::new(40, h)).expect("create idle terminal");
|
||||
idle_terminal
|
||||
.draw(|f| idle_chat.render(f.area(), f.buffer_mut()))
|
||||
.expect("draw idle chat");
|
||||
let mut terminal = Terminal::new(TestBackend::new(40, h)).expect("create terminal");
|
||||
terminal
|
||||
.draw(|f| chat.render(f.area(), f.buffer_mut()))
|
||||
.expect("draw chat running");
|
||||
assert_chatwidget_snapshot!(name, normalized_backend_snapshot(terminal.backend()));
|
||||
assert_eq!(
|
||||
normalized_backend_snapshot(terminal.backend()),
|
||||
normalized_backend_snapshot(idle_terminal.backend()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5008,36 +5014,31 @@ async fn user_prompt_submit_app_server_hook_notifications_render_snapshot() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn interrupt_hook_events_render_snapshot() {
|
||||
assert_hook_events_snapshot(
|
||||
codex_app_server_protocol::HookEventName::Interrupt,
|
||||
"interrupt:0:/tmp/hooks.json",
|
||||
"cleaning up the interrupted turn",
|
||||
"interrupt_hook_events_render_snapshot",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pre_tool_use_hook_events_render_snapshot() {
|
||||
assert_hook_events_snapshot(
|
||||
codex_app_server_protocol::HookEventName::PreToolUse,
|
||||
"pre-tool-use:0:/tmp/hooks.json",
|
||||
"warming the shell",
|
||||
"pre_tool_use_hook_events_render_snapshot",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn post_tool_use_hook_events_render_snapshot() {
|
||||
assert_hook_events_snapshot(
|
||||
codex_app_server_protocol::HookEventName::PostToolUse,
|
||||
"post-tool-use:0:/tmp/hooks.json",
|
||||
"warming the shell",
|
||||
"post_tool_use_hook_events_render_snapshot",
|
||||
)
|
||||
.await;
|
||||
async fn hook_events_render_consistently() {
|
||||
for (event_name, run_id, status_message) in [
|
||||
(
|
||||
codex_app_server_protocol::HookEventName::Interrupt,
|
||||
"interrupt:0:/tmp/hooks.json",
|
||||
"cleaning up the interrupted turn",
|
||||
),
|
||||
(
|
||||
codex_app_server_protocol::HookEventName::PreToolUse,
|
||||
"pre-tool-use:0:/tmp/hooks.json",
|
||||
"warming the shell",
|
||||
),
|
||||
(
|
||||
codex_app_server_protocol::HookEventName::PostToolUse,
|
||||
"post-tool-use:0:/tmp/hooks.json",
|
||||
"warming the shell",
|
||||
),
|
||||
(
|
||||
codex_app_server_protocol::HookEventName::SessionStart,
|
||||
"session-start:0:/tmp/hooks.json",
|
||||
"warming the shell",
|
||||
),
|
||||
] {
|
||||
assert_hook_events(event_name, run_id, status_message).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -5588,17 +5589,6 @@ async fn stopped_hook_hides_model_context_and_preserves_stop_reason_snapshot() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_start_hook_events_render_snapshot() {
|
||||
assert_hook_events_snapshot(
|
||||
codex_app_server_protocol::HookEventName::SessionStart,
|
||||
"session-start:0:/tmp/hooks.json",
|
||||
"warming the shell",
|
||||
"session_start_hook_events_render_snapshot",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn hook_started_run(
|
||||
id: &str,
|
||||
event_name: codex_app_server_protocol::HookEventName,
|
||||
|
||||
@@ -868,9 +868,6 @@ mod tests {
|
||||
);
|
||||
|
||||
let rendered = render_screen(&screen, /*width*/ 80, /*height*/ 24);
|
||||
#[cfg(windows)]
|
||||
assert_snapshot!("external_agent_config_migration_prompt_windows", rendered);
|
||||
#[cfg(not(windows))]
|
||||
assert_snapshot!("external_agent_config_migration_prompt", rendered);
|
||||
}
|
||||
|
||||
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
---
|
||||
source: tui/src/external_agent_config_migration/mod.rs
|
||||
expression: rendered
|
||||
---
|
||||
|
||||
> Import setup
|
||||
Bring over supported setup from another coding agent.
|
||||
Codex may add files to your current project folder.
|
||||
Your existing setup will not be changed.
|
||||
[x] Tools & setup
|
||||
Settings, instructions, integrations, agents, commands, and skills
|
||||
Importing: Settings 1, Memory 1
|
||||
[x] Current project
|
||||
Add Codex files alongside your existing project files
|
||||
Importing: Plugins 6, Instructions 1
|
||||
[x] Chat sessions (1)
|
||||
Last 30 days of chats
|
||||
Importing: Chat sessions 1
|
||||
|
||||
Selected 5 of 5 items.
|
||||
› 1. Import selected
|
||||
2. Customize selection
|
||||
3. Cancel
|
||||
Use ↑/↓ to move, enter to select, c to customize
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
---
|
||||
source: tui/src/history_cell/tests.rs
|
||||
expression: rendered
|
||||
---
|
||||
╭─────────────────────────────────────────────────╮
|
||||
│ ✨ Update available! 0.0.0 -> 9.9.9 │
|
||||
│ Run vp install -g @openai/codex to update. │
|
||||
│ │
|
||||
│ See full release notes: │
|
||||
│ https://github.com/openai/codex/releases/latest │
|
||||
╰─────────────────────────────────────────────────╯
|
||||
@@ -1397,17 +1397,6 @@ fn pnpm_update_available_history_cell_snapshot() {
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vite_plus_update_available_history_cell_snapshot() {
|
||||
let cell = UpdateAvailableHistoryCell::new(
|
||||
"9.9.9".to_string(),
|
||||
Some(UpdateAction::VitePlusGlobalLatest),
|
||||
);
|
||||
let rendered = render_lines(&cell.display_lines(/*width*/ 110)).join("\n");
|
||||
|
||||
insta::assert_snapshot!(rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_search_history_cell_without_detail_snapshot() {
|
||||
let cell = new_web_search_call("call-1".to_string(), String::new(), WebSearchAction::Other);
|
||||
|
||||
@@ -1184,16 +1184,6 @@ mod tests {
|
||||
#[test]
|
||||
fn picker_question_actions_snapshot() {
|
||||
let runtime = RuntimeKeymap::defaults();
|
||||
let params = build_keymap_picker_params_for_selected_action(
|
||||
&runtime,
|
||||
&TuiKeymap::default(),
|
||||
"chat",
|
||||
"skip_question",
|
||||
);
|
||||
assert_snapshot!(
|
||||
"keymap_question_actions",
|
||||
render_picker(params, /*width*/ 120)
|
||||
);
|
||||
let descriptions = ["edit_queued_message", "prompt_stack_back", "skip_question"]
|
||||
.map(|action| {
|
||||
render_picker(
|
||||
|
||||
@@ -1556,23 +1556,6 @@ fn code_block_multiple_lines_inside_unordered_list() {
|
||||
assert_eq!(lines, vec!["• Item", "", " first", " second"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn code_block_inside_unordered_list_item_multiple_lines() {
|
||||
let md = "- Item\n\n ```\n first\n second\n ```\n";
|
||||
let text = render_markdown_text(md);
|
||||
let lines: Vec<String> = text
|
||||
.lines
|
||||
.iter()
|
||||
.map(|l| {
|
||||
l.spans
|
||||
.iter()
|
||||
.map(|s| s.content.clone())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(lines, vec!["• Item", "", " first", " second"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_item_after_code_block_keeps_blank_separator() {
|
||||
let md = "1. First:\n\n ```rust\n fn first() {}\n ```\n\n2. Second:\n";
|
||||
|
||||
@@ -483,60 +483,6 @@ mod tests {
|
||||
assert_snapshot!("model_migration_prompt_gpt5_family", terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_snapshot_gpt5_codex() {
|
||||
let backend = VT100Backend::new(/*width*/ 60, /*height*/ 22);
|
||||
let mut terminal = Terminal::with_options(backend).expect("terminal");
|
||||
terminal.set_viewport_area(Rect::new(0, 0, 60, 22));
|
||||
|
||||
let screen = ModelMigrationScreen::new(
|
||||
FrameRequester::test_dummy(),
|
||||
migration_copy_for_models(
|
||||
"gpt-5-codex",
|
||||
"gpt-5.1-codex-max",
|
||||
Some("https://www.codex.com/models/gpt-5.1-codex-max".to_string()),
|
||||
/*migration_copy*/ None,
|
||||
/*migration_markdown*/ None,
|
||||
"gpt-5.1-codex-max".to_string(),
|
||||
Some("Codex-optimized flagship for deep and fast reasoning.".to_string()),
|
||||
/*can_opt_out*/ false,
|
||||
),
|
||||
);
|
||||
{
|
||||
let mut frame = terminal.get_frame();
|
||||
frame.render_widget_ref(&screen, frame.area());
|
||||
}
|
||||
terminal.flush().expect("flush");
|
||||
assert_snapshot!("model_migration_prompt_gpt5_codex", terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_snapshot_gpt5_codex_mini() {
|
||||
let backend = VT100Backend::new(/*width*/ 60, /*height*/ 22);
|
||||
let mut terminal = Terminal::with_options(backend).expect("terminal");
|
||||
terminal.set_viewport_area(Rect::new(0, 0, 60, 22));
|
||||
|
||||
let screen = ModelMigrationScreen::new(
|
||||
FrameRequester::test_dummy(),
|
||||
migration_copy_for_models(
|
||||
"gpt-5-codex-mini",
|
||||
"gpt-5.1-codex-mini",
|
||||
Some("https://www.codex.com/models/gpt-5.1-codex-mini".to_string()),
|
||||
/*migration_copy*/ None,
|
||||
/*migration_markdown*/ None,
|
||||
"gpt-5.1-codex-mini".to_string(),
|
||||
Some("Optimized for codex. Cheaper, faster, but less capable.".to_string()),
|
||||
/*can_opt_out*/ false,
|
||||
),
|
||||
);
|
||||
{
|
||||
let mut frame = terminal.get_frame();
|
||||
frame.render_widget_ref(&screen, frame.area());
|
||||
}
|
||||
terminal.flush().expect("flush");
|
||||
assert_snapshot!("model_migration_prompt_gpt5_codex_mini", terminal.backend());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_key_accepts_prompt() {
|
||||
let mut screen = ModelMigrationScreen::new(
|
||||
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
---
|
||||
source: tui/src/pager_overlay/transcript_tests.rs
|
||||
assertion_line: 417
|
||||
expression: snapshots
|
||||
---
|
||||
--- loading ---
|
||||
/ T R A N S C R I P T / / / / / / / / / / / / / / / / / / / / / / / / /
|
||||
recent transcript
|
||||
|
||||
|
||||
|
||||
|
||||
↑ Loading earlier messages…
|
||||
|
||||
|
||||
|
||||
--- partial ---
|
||||
/ T R A N S C R I P T / / / / / / / / / / / / / / / / / / / / / / / / /
|
||||
recent transcript
|
||||
|
||||
|
||||
|
||||
|
||||
Earlier messages available. ⌃space select
|
||||
↑/↓ to scroll · pgup/pgdn to page · home/end to jump
|
||||
q close · f3 find · esc browse prompts
|
||||
|
||||
--- failed ---
|
||||
/ T R A N S C R I P T / / / / / / / / / / / / / / / / / / / / / / / / /
|
||||
recent transcript
|
||||
|
||||
|
||||
|
||||
|
||||
Retry history: ⌥</⌃home.
|
||||
|
||||
|
||||
|
||||
--- complete ---
|
||||
/ T R A N S C R I P T / / / / / / / / / / / / / / / / / / / / / / / / /
|
||||
recent transcript
|
||||
|
||||
|
||||
|
||||
|
||||
⌃space select
|
||||
↑/↓ to scroll · pgup/pgdn to page · home/end to jump
|
||||
q close · f3 find · esc browse prompts
|
||||
@@ -454,12 +454,7 @@ fn transcript_overlay_snapshots_paginated_history_states() {
|
||||
snapshots.push_str(&format!("--- {name} ---\n{}", buffer_to_text(&buf, area)));
|
||||
}
|
||||
|
||||
let snapshot_name = if cfg!(target_os = "macos") {
|
||||
"transcript_overlay_paginated_history_states"
|
||||
} else {
|
||||
"transcript_overlay_paginated_history_states_non_macos"
|
||||
};
|
||||
assert_snapshot!(snapshot_name, snapshots);
|
||||
assert_snapshot!("transcript_overlay_paginated_history_states", snapshots);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5668,36 +5668,6 @@ session_picker_view = "dense"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dense_session_snapshot_includes_cwd_in_all_filter() {
|
||||
assert_snapshot!(
|
||||
"resume_picker_dense_all",
|
||||
render_dense_row_snapshot(
|
||||
/*show_all*/ true, /*filter_cwd*/ None, /*width*/ 120,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dense_session_snapshot_auto_hides_cwd_when_narrow() {
|
||||
assert_snapshot!(
|
||||
"resume_picker_dense_all_auto_hidden_cwd",
|
||||
render_dense_row_snapshot(
|
||||
/*show_all*/ true, /*filter_cwd*/ None, /*width*/ 100,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dense_session_snapshot_forces_cwd_when_narrow() {
|
||||
assert_snapshot!(
|
||||
"resume_picker_dense_all_forced_cwd",
|
||||
render_dense_row_snapshot(
|
||||
/*show_all*/ true, /*filter_cwd*/ None, /*width*/ 48,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dense_session_snapshot_drops_metadata_when_narrow() {
|
||||
assert_snapshot!(
|
||||
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
---
|
||||
source: tui/src/keymap_setup.rs
|
||||
expression: "render_picker(params, 120)"
|
||||
---
|
||||
|
||||
Keymap
|
||||
All configurable shortcuts.
|
||||
151 actions, 0 customized, 5 unbound.
|
||||
|
||||
All Common Customized (0) Unbound (5) App Composer Editor Vim Navigation Agents Approval ›
|
||||
|
||||
Type to search shortcuts
|
||||
|
||||
Global - Open Agents unbound
|
||||
Global Open Transcript ctrl-t
|
||||
Global Find Transcript f3
|
||||
Global Focus Activity f4
|
||||
Global Open External Editor ctrl-g
|
||||
Global Copy ctrl-o
|
||||
Global Clear Terminal ctrl-l
|
||||
Global - Toggle Vim Mode unbound
|
||||
Global Toggle Raw Output alt-r
|
||||
Global Toggle Side Conversation ctrl-/
|
||||
Chat Interrupt Turn esc
|
||||
Chat Decrease Reasoning Effort alt-,, shift-down
|
||||
Chat Increase Reasoning Effort alt-., shift-up
|
||||
Chat - Previous Permission Mode unbound
|
||||
Chat - Next Permission Mode unbound
|
||||
Chat Edit Queued Message shift-left, alt-up
|
||||
Chat Prompt Stack Back shift-right, alt-down
|
||||
› Chat Skip Question ctrl-]
|
||||
Chat Toggle Voice f8
|
||||
Chat Toggle Voice Mute ctrl-x
|
||||
Composer Submit enter
|
||||
Composer Queue tab
|
||||
Composer Toggle Shortcuts ?, shift-?
|
||||
Composer History Search Previous ctrl-r
|
||||
↓
|
||||
left/right group · enter edit shortcut · * custom · - unbound · esc close
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
---
|
||||
source: tui/src/model_migration.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
|
||||
Codex just got an upgrade. Introducing
|
||||
gpt-5.1-codex-max.
|
||||
|
||||
We recommend switching from gpt-5-codex to
|
||||
gpt-5.1-codex-max.
|
||||
|
||||
Codex-optimized flagship for deep and fast reasoning.
|
||||
Learn more about gpt-5.1-codex-max at
|
||||
https://www.codex.com/models/gpt-5.1-codex-max
|
||||
|
||||
enter/esc continue · ⌃c quit
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
---
|
||||
source: tui/src/model_migration.rs
|
||||
expression: terminal.backend()
|
||||
---
|
||||
|
||||
Codex just got an upgrade. Introducing
|
||||
gpt-5.1-codex-mini.
|
||||
|
||||
We recommend switching from gpt-5-codex-mini to
|
||||
gpt-5.1-codex-mini.
|
||||
|
||||
Optimized for codex. Cheaper, faster, but less capable.
|
||||
Learn more about gpt-5.1-codex-mini at
|
||||
https://www.codex.com/models/gpt-5.1-codex-mini
|
||||
|
||||
enter/esc continue · ⌃c quit
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: "render_dense_row_snapshot(true, None, 120,)"
|
||||
---
|
||||
› 15m ago Propose session picker redesign with enough title text to exercise truncation
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: "render_dense_row_snapshot(true, None, 100,)"
|
||||
---
|
||||
› 15m ago Propose session picker redesign with enough title text to exercise truncation
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: tui/src/resume_picker.rs
|
||||
expression: "render_dense_row_snapshot(true, None, 48,)"
|
||||
---
|
||||
› 15m ago Propose session picker redesig...
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
---
|
||||
source: tui/src/transcript_view_tests.rs
|
||||
expression: text(&selected)
|
||||
---
|
||||
row 22
|
||||
row 23
|
||||
row 24
|
||||
row 25
|
||||
row 26
|
||||
row 27
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
---
|
||||
source: tui/src/status/tests.rs
|
||||
expression: sanitized
|
||||
---
|
||||
/status
|
||||
|
||||
>_ OpenAI Codex (v0.0.0)
|
||||
|
||||
Visit https://chatgpt.com/settings/usage for up-to-date
|
||||
information on rate limits and credits
|
||||
|
||||
Model: gpt-5.1-codex-max (reasoning none, summaries auto)
|
||||
Directory: [[workspace]]
|
||||
Permissions: Custom (workspace with network access, Ask for approval)
|
||||
Agents.md: <none>
|
||||
|
||||
Token usage: 750 total (500 input + 250 output)
|
||||
Context window: 100% left (750 used / 272K)
|
||||
Limits: not available for this account
|
||||
@@ -2101,7 +2101,7 @@ async fn status_snapshot_treats_refreshing_empty_limits_as_unavailable() {
|
||||
}
|
||||
}
|
||||
let sanitized = sanitize_directory(rendered_lines).join("\n");
|
||||
assert_snapshot!(sanitized);
|
||||
assert!(sanitized.contains("Limits: not available for this account"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -648,10 +648,9 @@ fn selecting_within_an_edge_row_does_not_start_selection_autoscroll() {
|
||||
);
|
||||
assert_eq!(view.selected_text(&cells).as_deref(), Some("row"));
|
||||
}
|
||||
insta::assert_snapshot!(
|
||||
format!("horizontal_selection_at_edge_{edge}"),
|
||||
text(&selected)
|
||||
);
|
||||
if edge == 0 {
|
||||
insta::assert_snapshot!("horizontal_selection_at_edge_0", text(&selected));
|
||||
}
|
||||
let drag = MouseEvent {
|
||||
kind: MouseEventKind::Drag(MouseButton::Left),
|
||||
column: 3,
|
||||
|
||||
Reference in New Issue
Block a user