test(tui): cover pet anchor layout

This commit is contained in:
Felipe Coury
2026-05-04 17:26:20 -03:00
parent 83f030a802
commit 81899bcc9b
15 changed files with 105 additions and 123 deletions

View File

@@ -6,4 +6,4 @@ expression: terminal.backend()
" /pet "
" "
" "
" /pets choose or disable the terminal pet "
" /pets choose or hide the terminal pet "

View File

@@ -1,7 +1,7 @@
---
source: tui/src/chatwidget/tests.rs
expression: terminal.backend()
source: tui/src/chatwidget/tests/status_and_layout.rs
expression: normalized_backend_snapshot(terminal.backend())
---
" "
" "
" Running "
" Thinking"
" "

View File

@@ -39,6 +39,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
• Investigating rendering code (0s • esc to interrupt)
Summarize recent commits
Summarize recent commits Running
Thinking
tab to queue message 100% context left

View File

@@ -23,6 +23,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
↳ Hello, world! 14
↳ Hello, world! 15
Ask Codex to do anything
Ask Codex to do anything Running
Thinking
gpt-5.5 default · /tmp/project

View File

@@ -17,6 +17,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
• Messages to be submitted at end of turn
↳ Steer submitted while /compact was running.
Ask Codex to do anything
Ask Codex to do anything Running
Thinking
gpt-5.5 default · /tmp/project

View File

@@ -7,6 +7,6 @@ expression: normalize_snapshot_paths(rendered)
• rm -rf '/tmp/guardian target 2'
Ask Codex to do anything
Ask Codex to do anything Running
Thinking
gpt-5.5 default · /tmp/project

View File

@@ -6,6 +6,6 @@ expression: normalized_backend_snapshot(terminal.backend())
"• Working (0s • esc to interrupt) "
" "
" "
" Ask Codex to do anything "
" "
" Ask Codex to do anything Running "
" Thinking"
" gpt-5.5 default · /tmp/project "

View File

@@ -17,6 +17,6 @@ expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
• Messages to be submitted at end of turn
↳ Steer submitted while /review was running.
Ask Codex to do anything
Ask Codex to do anything Running
Thinking
gpt-5.5 default · /tmp/project

View File

@@ -6,6 +6,6 @@ expression: normalized_backend_snapshot(terminal.backend())
"• Working (0s • esc to interrupt) "
" "
" "
" Ask Codex to do anything "
" "
" Ask Codex to do anything Running "
" Thinking"
" gpt-5.5 default Side starting... "

View File

@@ -6,6 +6,6 @@ expression: normalized_backend_snapshot(terminal.backend())
"• Analyzing (0s • esc to interrupt) "
" "
" "
" Ask Codex to do anything "
" "
" Ask Codex to do anything Running "
" Thinking"
" gpt-5.5 default · /tmp/project "

View File

@@ -6,6 +6,6 @@ expression: normalized_backend_snapshot(terminal.backend())
"• Working (0s • esc to interrupt) · 1 background terminal running · /ps to view…"
" "
" "
" Ask Codex to do anything "
" "
" Ask Codex to do anything Running "
" Thinking"
" gpt-5.5 default · /tmp/project "

View File

@@ -6,6 +6,6 @@ expression: normalize_snapshot_paths(rendered)
└ cargo test -p codex-core -- --exact…
Ask Codex to do anything
Ask Codex to do anything Running
Thinking
gpt-5.5 default · /tmp/project

View File

@@ -260,6 +260,7 @@ pub(super) async fn make_chatwidget_manual(
current_status: StatusIndicatorState::working(),
active_hook_cell: None,
ambient_pet,
pet_image_support_override: None,
retry_status_header: None,
pending_status_indicator_restore: false,
suppress_queue_autosend: false,

View File

@@ -2,47 +2,16 @@ use super::*;
use pretty_assertions::assert_eq;
use serial_test::serial;
struct EnvVarGuard {
key: &'static str,
previous: Option<std::ffi::OsString>,
fn force_pet_image_support(chat: &mut ChatWidget) {
chat.set_pet_image_support_for_tests(crate::pets::PetImageSupport::Supported(
crate::pets::ImageProtocol::Kitty,
));
}
impl EnvVarGuard {
fn remove(key: &'static str) -> Self {
let previous = std::env::var_os(key);
unsafe { std::env::remove_var(key) };
Self { key, previous }
}
fn set(key: &'static str, value: &'static str) -> Self {
let previous = std::env::var_os(key);
unsafe { std::env::set_var(key, value) };
Self { key, previous }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match self.previous.take() {
Some(value) => unsafe { std::env::set_var(self.key, value) },
None => unsafe { std::env::remove_var(self.key) },
}
}
}
fn supported_pet_image_env() -> [EnvVarGuard; 6] {
[
EnvVarGuard::remove("TMUX"),
EnvVarGuard::remove("TMUX_PANE"),
EnvVarGuard::remove("ZELLIJ"),
EnvVarGuard::remove("ZELLIJ_SESSION_NAME"),
EnvVarGuard::remove("ZELLIJ_VERSION"),
EnvVarGuard::set("KITTY_WINDOW_ID", "test-window"),
]
}
fn tmux_pet_image_env() -> EnvVarGuard {
EnvVarGuard::set("TMUX", "session")
fn force_tmux_pet_image_unsupported(chat: &mut ChatWidget) {
chat.set_pet_image_support_for_tests(crate::pets::PetImageSupport::Unsupported(
crate::pets::PetImageUnsupportedReason::Tmux,
));
}
fn complete_turn_with_message(chat: &mut ChatWidget, turn_id: &str, message: Option<&str>) {
@@ -1793,8 +1762,8 @@ async fn slash_resume_with_arg_requests_named_session() {
#[tokio::test]
#[serial]
async fn slash_pets_opens_picker() {
let _env_guard = supported_pet_image_env();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
force_pet_image_support(&mut chat);
chat.dispatch_command(SlashCommand::Pets);
@@ -1808,8 +1777,8 @@ async fn slash_pets_opens_picker() {
#[tokio::test]
#[serial]
async fn slash_pets_with_arg_selects_named_pet() {
let _env_guard = supported_pet_image_env();
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
force_pet_image_support(&mut chat);
chat.bottom_pane
.set_composer_text("/pets chefito".to_string(), Vec::new(), Vec::new());
@@ -1825,8 +1794,8 @@ async fn slash_pets_with_arg_selects_named_pet() {
#[tokio::test]
#[serial]
async fn slash_pets_disable_disables_pets_even_on_unsupported_terminal() {
let _env_guard = tmux_pet_image_env();
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());
@@ -1840,8 +1809,8 @@ async fn slash_pets_disable_disables_pets_even_on_unsupported_terminal() {
#[tokio::test]
#[serial]
async fn slash_pet_hide_disables_pets_even_on_unsupported_terminal() {
let _env_guard = tmux_pet_image_env();
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());
@@ -1855,8 +1824,8 @@ async fn slash_pet_hide_disables_pets_even_on_unsupported_terminal() {
#[tokio::test]
#[serial]
async fn slash_pets_on_unsupported_terminal_warns_without_picker() {
let _env_guard = tmux_pet_image_env();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
force_tmux_pet_image_unsupported(&mut chat);
chat.dispatch_command(SlashCommand::Pets);
@@ -1874,8 +1843,8 @@ async fn slash_pets_on_unsupported_terminal_warns_without_picker() {
#[tokio::test]
#[serial]
async fn slash_pets_with_arg_on_unsupported_terminal_warns_without_selection() {
let _env_guard = tmux_pet_image_env();
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 chefito".to_string(), Vec::new(), Vec::new());

View File

@@ -3,43 +3,10 @@ use crate::bottom_pane::goal_status_indicator_line;
use pretty_assertions::assert_eq;
use serial_test::serial;
struct EnvVarGuard {
key: &'static str,
previous: Option<std::ffi::OsString>,
}
impl EnvVarGuard {
fn remove(key: &'static str) -> Self {
let previous = std::env::var_os(key);
unsafe { std::env::remove_var(key) };
Self { key, previous }
}
fn set(key: &'static str, value: &'static str) -> Self {
let previous = std::env::var_os(key);
unsafe { std::env::set_var(key, value) };
Self { key, previous }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
match self.previous.take() {
Some(value) => unsafe { std::env::set_var(self.key, value) },
None => unsafe { std::env::remove_var(self.key) },
}
}
}
fn supported_pet_image_env() -> [EnvVarGuard; 6] {
[
EnvVarGuard::remove("TMUX"),
EnvVarGuard::remove("TMUX_PANE"),
EnvVarGuard::remove("ZELLIJ"),
EnvVarGuard::remove("ZELLIJ_SESSION_NAME"),
EnvVarGuard::remove("ZELLIJ_VERSION"),
EnvVarGuard::set("KITTY_WINDOW_ID", "test-window"),
]
fn force_pet_image_support(chat: &mut ChatWidget) {
chat.set_pet_image_support_for_tests(crate::pets::PetImageSupport::Supported(
crate::pets::ImageProtocol::Kitty,
));
}
/// Receiving a token usage update without usage clears the context indicator.
@@ -1239,11 +1206,11 @@ async fn ui_snapshots_small_heights_task_running() {
#[tokio::test]
#[serial]
async fn ambient_pet_defaults_to_codex_and_stays_above_the_footer() {
async fn ambient_pet_defaults_to_codex_and_anchors_to_composer_bottom() {
use ratatui::layout::Rect;
let _env_guard = supported_pet_image_env();
let (chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
force_pet_image_support(&mut chat);
assert_eq!(
chat.ambient_pet
.as_ref()
@@ -1255,18 +1222,57 @@ async fn ambient_pet_defaults_to_codex_and_stays_above_the_footer() {
/*x*/ 0, /*y*/ 0, /*width*/ 60, /*height*/ 20,
);
let draw = chat
.ambient_pet_draw(area)
.ambient_pet_draw(area, area.bottom())
.expect("ambient pet draw request");
assert_eq!(draw.x, 51);
assert_eq!(draw.y, 10);
assert_eq!(draw.y, 14);
assert_eq!(draw.columns, 9);
assert_eq!(draw.rows, 5);
assert_eq!(
draw.y.saturating_add(draw.rows),
area.bottom().saturating_sub(/*rhs*/ 1)
);
handle_turn_started(&mut chat, "turn-1");
handle_agent_reasoning_delta(&mut chat, "**Thinking**");
let draw_with_status = chat
.ambient_pet_draw(area, area.bottom())
.expect("ambient pet draw request with status");
assert_eq!(draw_with_status.y, draw.y);
assert_eq!(
draw_with_status.y.saturating_add(draw_with_status.rows),
area.bottom().saturating_sub(/*rhs*/ 1)
);
}
#[tokio::test]
#[serial]
async fn ambient_pet_screen_bottom_anchor_uses_terminal_bottom() {
use codex_config::types::TuiPetAnchor;
use ratatui::layout::Rect;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
force_pet_image_support(&mut chat);
let terminal_area = Rect::new(
/*x*/ 0, /*y*/ 0, /*width*/ 80, /*height*/ 24,
);
let composer_bottom_y = 20;
let default_draw = chat
.ambient_pet_draw(terminal_area, composer_bottom_y)
.expect("composer-anchored pet draw request");
assert_eq!(default_draw.y, 14);
chat.config.tui_pet_anchor = TuiPetAnchor::ScreenBottom;
let screen_bottom_draw = chat
.ambient_pet_draw(terminal_area, composer_bottom_y)
.expect("screen-bottom anchored pet draw request");
assert_eq!(screen_bottom_draw.y, 18);
}
#[tokio::test]
#[serial]
async fn ambient_pet_can_be_disabled() {
let _env_guard = supported_pet_image_env();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.set_tui_pet(Some(crate::pets::DISABLED_PET_ID.to_string()));
@@ -1279,24 +1285,30 @@ async fn ambient_pet_can_be_disabled() {
async fn ambient_pet_draw_uses_terminal_screen_area_not_short_inline_viewport() {
use ratatui::layout::Rect;
let _env_guard = supported_pet_image_env();
let (chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
force_pet_image_support(&mut chat);
assert!(
chat.ambient_pet_draw(Rect::new(
/*x*/ 0, /*y*/ 21, /*width*/ 80, /*height*/ 3,
))
chat.ambient_pet_draw(
Rect::new(
/*x*/ 0, /*y*/ 21, /*width*/ 80, /*height*/ 3,
),
/*composer_bottom_y*/ 24
)
.is_none(),
"a normal short inline viewport cannot fit the ambient pet"
);
let draw = chat
.ambient_pet_draw(Rect::new(
/*x*/ 0, /*y*/ 0, /*width*/ 80, /*height*/ 24,
))
.ambient_pet_draw(
Rect::new(
/*x*/ 0, /*y*/ 0, /*width*/ 80, /*height*/ 24,
),
/*composer_bottom_y*/ 24,
)
.expect("full terminal screen has room for the ambient pet");
assert_eq!(draw.x, 71);
assert_eq!(draw.y, 14);
assert_eq!(draw.y, 18);
}
#[tokio::test]
@@ -1305,8 +1317,8 @@ async fn ambient_pet_uses_the_app_notification_labels() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let _env_guard = supported_pet_image_env();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
force_pet_image_support(&mut chat);
for (kind, label) in [
(crate::pets::PetNotificationKind::Running, "Running"),
(crate::pets::PetNotificationKind::Waiting, "Needs input"),