mirror of
https://github.com/openai/codex.git
synced 2026-06-01 19:02:59 +00:00
Simplify TUI startup test coverage (#22573)
## Why The TUI startup test surface had drifted into expensive, brittle coverage: - `tui/tests/suite/no_panic_on_startup.rs` was already ignored as flaky while still spawning a PTY to exercise malformed exec-policy rules. - `tui/tests/suite/model_availability_nux.rs` used a seeded session, cursor-query spoofing, and repeated interrupts to verify a narrow resume-path invariant. - `app/tests.rs` had started accumulating unrelated startup and summary coverage in one flat module even after the surrounding app code was split into feature modules. This keeps those behaviors covered while making the tests cheaper to understand and less likely to rot. It also preserves the malformed-rules regression from #8803 without requiring a terminal orchestration test. ## What changed - Replaced the malformed `rules` startup PTY case with a direct exec-policy loader regression: [`rules_path_file_returns_read_dir_error`](21b6b5622f/codex-rs/core/src/exec_policy_tests.rs (L264-L284)) - Made the existing fresh-session-only startup tooltip behavior explicit with [`should_prepare_startup_tooltip_override`](21b6b5622f/codex-rs/tui/src/app/thread_routing.rs (L1272-L1279)), then added focused coverage for the resume/fork gate and the persisted NUX counter. - Split startup and session-summary coverage out of `tui/src/app/tests.rs` into dedicated modules so the test layout better mirrors the current app architecture. - Converted one single-message goal validation snapshot into semantic assertions where layout was not the behavior under test. - Removed the two PTY-heavy suite files that the narrower tests now supersede. ## Verification - `cargo test -p codex-core rules_path_file_returns_read_dir_error` - `cargo test -p codex-tui startup_` - `cargo test -p codex-tui session_summary_` - `cargo test -p codex-tui goal_slash_command_rejects_oversized_objective`
This commit is contained in:
@@ -755,12 +755,15 @@ impl App {
|
||||
&initial_prompt,
|
||||
&initial_images,
|
||||
);
|
||||
let startup_tooltip_override =
|
||||
if Self::should_prepare_startup_tooltip_override(&session_selection) {
|
||||
prepare_startup_tooltip_override(&mut config, &available_models, is_first_run).await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (mut chat_widget, initial_started_thread) = match session_selection {
|
||||
SessionSelection::StartFresh | SessionSelection::Exit => {
|
||||
let started = app_server.start_thread(&config).await?;
|
||||
let startup_tooltip_override =
|
||||
prepare_startup_tooltip_override(&mut config, &available_models, is_first_run)
|
||||
.await;
|
||||
let init = crate::chatwidget::ChatWidgetInit {
|
||||
config: config.clone(),
|
||||
environment_manager: environment_manager.clone(),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! App-level orchestration tests for the TUI.
|
||||
|
||||
mod model_catalog;
|
||||
mod session_summary;
|
||||
mod startup;
|
||||
|
||||
use super::*;
|
||||
use crate::app_backtrack::BacktrackSelection;
|
||||
@@ -127,183 +129,6 @@ async fn handle_mcp_inventory_result_clears_committed_loading_cell() {
|
||||
assert_eq!(app.transcript_cells.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_waiting_gate_is_only_for_fresh_or_exit_session_selection() {
|
||||
assert_eq!(
|
||||
App::should_wait_for_initial_session(&SessionSelection::StartFresh),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
App::should_wait_for_initial_session(&SessionSelection::Exit),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
App::should_wait_for_initial_session(&SessionSelection::Resume(
|
||||
crate::resume_picker::SessionTarget {
|
||||
path: Some(PathBuf::from("/tmp/restore")),
|
||||
thread_id: ThreadId::new(),
|
||||
}
|
||||
)),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
App::should_wait_for_initial_session(&SessionSelection::Fork(
|
||||
crate::resume_picker::SessionTarget {
|
||||
path: Some(PathBuf::from("/tmp/fork")),
|
||||
thread_id: ThreadId::new(),
|
||||
}
|
||||
)),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_paused_goal_prompt_gate_is_only_for_quiet_resume() {
|
||||
let resume = SessionSelection::Resume(crate::resume_picker::SessionTarget {
|
||||
path: Some(PathBuf::from("/tmp/restore")),
|
||||
thread_id: ThreadId::new(),
|
||||
});
|
||||
let fork = SessionSelection::Fork(crate::resume_picker::SessionTarget {
|
||||
path: Some(PathBuf::from("/tmp/fork")),
|
||||
thread_id: ThreadId::new(),
|
||||
});
|
||||
let no_images: Vec<PathBuf> = Vec::new();
|
||||
let initial_images = vec![PathBuf::from("/tmp/image.png")];
|
||||
|
||||
assert!(App::should_prompt_for_paused_goal_after_startup_resume(
|
||||
&resume, &None, &no_images
|
||||
));
|
||||
assert!(!App::should_prompt_for_paused_goal_after_startup_resume(
|
||||
&resume,
|
||||
&Some("continue from here".to_string()),
|
||||
&no_images
|
||||
));
|
||||
assert!(!App::should_prompt_for_paused_goal_after_startup_resume(
|
||||
&resume,
|
||||
&None,
|
||||
&initial_images
|
||||
));
|
||||
assert!(!App::should_prompt_for_paused_goal_after_startup_resume(
|
||||
&SessionSelection::StartFresh,
|
||||
&None,
|
||||
&no_images
|
||||
));
|
||||
assert!(!App::should_prompt_for_paused_goal_after_startup_resume(
|
||||
&fork, &None, &no_images
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_waiting_gate_holds_active_thread_events_until_primary_thread_configured() {
|
||||
let mut wait_for_initial_session =
|
||||
App::should_wait_for_initial_session(&SessionSelection::StartFresh);
|
||||
assert_eq!(wait_for_initial_session, true);
|
||||
assert_eq!(
|
||||
App::should_handle_active_thread_events(
|
||||
wait_for_initial_session,
|
||||
/*has_active_thread_receiver*/ true
|
||||
),
|
||||
false
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
App::should_stop_waiting_for_initial_session(
|
||||
wait_for_initial_session,
|
||||
/*primary_thread_id*/ None
|
||||
),
|
||||
false
|
||||
);
|
||||
if App::should_stop_waiting_for_initial_session(wait_for_initial_session, Some(ThreadId::new()))
|
||||
{
|
||||
wait_for_initial_session = false;
|
||||
}
|
||||
assert_eq!(wait_for_initial_session, false);
|
||||
|
||||
assert_eq!(
|
||||
App::should_handle_active_thread_events(
|
||||
wait_for_initial_session,
|
||||
/*has_active_thread_receiver*/ true
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_waiting_gate_not_applied_for_resume_or_fork_session_selection() {
|
||||
let wait_for_resume = App::should_wait_for_initial_session(&SessionSelection::Resume(
|
||||
crate::resume_picker::SessionTarget {
|
||||
path: Some(PathBuf::from("/tmp/restore")),
|
||||
thread_id: ThreadId::new(),
|
||||
},
|
||||
));
|
||||
assert_eq!(
|
||||
App::should_handle_active_thread_events(
|
||||
wait_for_resume,
|
||||
/*has_active_thread_receiver*/ true
|
||||
),
|
||||
true
|
||||
);
|
||||
let wait_for_fork = App::should_wait_for_initial_session(&SessionSelection::Fork(
|
||||
crate::resume_picker::SessionTarget {
|
||||
path: Some(PathBuf::from("/tmp/fork")),
|
||||
thread_id: ThreadId::new(),
|
||||
},
|
||||
));
|
||||
assert_eq!(
|
||||
App::should_handle_active_thread_events(
|
||||
wait_for_fork,
|
||||
/*has_active_thread_receiver*/ true
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ignore_same_thread_resume_reports_noop_for_current_thread() {
|
||||
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
|
||||
let thread_id = ThreadId::new();
|
||||
let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));
|
||||
app.chat_widget.handle_thread_session(session.clone());
|
||||
app.thread_event_channels.insert(
|
||||
thread_id,
|
||||
ThreadEventChannel::new_with_session(THREAD_EVENT_CHANNEL_CAPACITY, session, Vec::new()),
|
||||
);
|
||||
app.activate_thread_channel(thread_id).await;
|
||||
while app_event_rx.try_recv().is_ok() {}
|
||||
|
||||
let ignored = app.ignore_same_thread_resume(&crate::resume_picker::SessionTarget {
|
||||
path: Some(test_path_buf("/tmp/project")),
|
||||
thread_id,
|
||||
});
|
||||
|
||||
assert!(ignored);
|
||||
let cell = match app_event_rx.try_recv() {
|
||||
Ok(AppEvent::InsertHistoryCell(cell)) => cell,
|
||||
other => panic!("expected info message after same-thread resume, saw {other:?}"),
|
||||
};
|
||||
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
|
||||
assert!(rendered.contains(&format!(
|
||||
"Already viewing {}.",
|
||||
test_path_display("/tmp/project")
|
||||
)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ignore_same_thread_resume_allows_reattaching_displayed_inactive_thread() {
|
||||
let mut app = make_test_app().await;
|
||||
let thread_id = ThreadId::new();
|
||||
let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));
|
||||
app.chat_widget.handle_thread_session(session);
|
||||
|
||||
let ignored = app.ignore_same_thread_resume(&crate::resume_picker::SessionTarget {
|
||||
path: Some(test_path_buf("/tmp/project")),
|
||||
thread_id,
|
||||
});
|
||||
|
||||
assert!(!ignored);
|
||||
assert!(app.transcript_cells.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bypass_hook_trust_startup_warning_snapshot() {
|
||||
let rendered = lines_to_single_string(
|
||||
@@ -5417,90 +5242,3 @@ async fn backtrack_esc_does_not_steal_empty_vim_insert_escape() {
|
||||
assert!(!app.chat_widget.should_handle_vim_insert_escape(esc));
|
||||
assert!(app.should_handle_backtrack_esc(esc));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_summary_skips_when_no_usage_or_resume_hint() {
|
||||
assert!(
|
||||
session_summary(
|
||||
TokenUsage::default(),
|
||||
/*thread_id*/ None,
|
||||
/*thread_name*/ None,
|
||||
/*rollout_path*/ None,
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_summary_skips_resume_hint_until_rollout_exists() {
|
||||
let usage = TokenUsage::default();
|
||||
let conversation = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap();
|
||||
let temp_dir = tempdir().expect("temp dir");
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
|
||||
assert!(
|
||||
session_summary(
|
||||
usage,
|
||||
Some(conversation),
|
||||
/*thread_name*/ None,
|
||||
Some(&rollout_path),
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_summary_includes_resume_hint_for_persisted_rollout() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 10,
|
||||
output_tokens: 2,
|
||||
total_tokens: 12,
|
||||
..Default::default()
|
||||
};
|
||||
let conversation = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap();
|
||||
let temp_dir = tempdir().expect("temp dir");
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
std::fs::write(&rollout_path, "{}\n").expect("write rollout");
|
||||
|
||||
let summary = session_summary(
|
||||
usage,
|
||||
Some(conversation),
|
||||
/*thread_name*/ None,
|
||||
Some(&rollout_path),
|
||||
)
|
||||
.expect("summary");
|
||||
assert_eq!(
|
||||
summary.usage_line,
|
||||
Some("Token usage: total=12 input=10 output=2".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
summary.resume_command,
|
||||
Some("codex resume 123e4567-e89b-12d3-a456-426614174000".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_summary_uses_id_even_when_thread_has_name() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 10,
|
||||
output_tokens: 2,
|
||||
total_tokens: 12,
|
||||
..Default::default()
|
||||
};
|
||||
let conversation = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap();
|
||||
let temp_dir = tempdir().expect("temp dir");
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
std::fs::write(&rollout_path, "{}\n").expect("write rollout");
|
||||
|
||||
let summary = session_summary(
|
||||
usage,
|
||||
Some(conversation),
|
||||
Some("my-session".to_string()),
|
||||
Some(&rollout_path),
|
||||
)
|
||||
.expect("summary");
|
||||
assert_eq!(
|
||||
summary.resume_command,
|
||||
Some("codex resume 123e4567-e89b-12d3-a456-426614174000".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -174,6 +174,46 @@ fn select_model_availability_nux_returns_none_when_all_models_are_exhausted() {
|
||||
assert_eq!(selected, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prepare_startup_tooltip_override_persists_model_availability_nux_count() {
|
||||
let codex_home = tempdir().expect("temp codex home");
|
||||
let mut config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.build()
|
||||
.await
|
||||
.expect("config");
|
||||
let mut presets = all_model_presets();
|
||||
presets.iter_mut().for_each(|preset| {
|
||||
preset.availability_nux = None;
|
||||
});
|
||||
let target = presets
|
||||
.iter_mut()
|
||||
.find(|preset| preset.model == "gpt-5.4")
|
||||
.expect("target preset present");
|
||||
target.availability_nux = Some(ModelAvailabilityNux {
|
||||
message: "gpt-5.4 is available".to_string(),
|
||||
});
|
||||
|
||||
let tooltip =
|
||||
prepare_startup_tooltip_override(&mut config, &presets, /*is_first_run*/ false).await;
|
||||
|
||||
assert_eq!(tooltip.as_deref(), Some("gpt-5.4 is available"));
|
||||
assert_eq!(
|
||||
config.model_availability_nux.shown_count,
|
||||
HashMap::from([("gpt-5.4".to_string(), 1)])
|
||||
);
|
||||
|
||||
let reloaded = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.build()
|
||||
.await
|
||||
.expect("reloaded config");
|
||||
assert_eq!(
|
||||
reloaded.model_availability_nux.shown_count,
|
||||
HashMap::from([("gpt-5.4".to_string(), 1)])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn accepted_model_migration_persists_target_default_reasoning_effort() {
|
||||
let codex_home = tempdir().expect("temp codex home");
|
||||
|
||||
89
codex-rs/tui/src/app/tests/session_summary.rs
Normal file
89
codex-rs/tui/src/app/tests/session_summary.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_summary_skips_when_no_usage_or_resume_hint() {
|
||||
assert!(
|
||||
session_summary(
|
||||
TokenUsage::default(),
|
||||
/*thread_id*/ None,
|
||||
/*thread_name*/ None,
|
||||
/*rollout_path*/ None,
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_summary_skips_resume_hint_until_rollout_exists() {
|
||||
let usage = TokenUsage::default();
|
||||
let conversation = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap();
|
||||
let temp_dir = tempdir().expect("temp dir");
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
|
||||
assert!(
|
||||
session_summary(
|
||||
usage,
|
||||
Some(conversation),
|
||||
/*thread_name*/ None,
|
||||
Some(&rollout_path),
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_summary_includes_resume_hint_for_persisted_rollout() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 10,
|
||||
output_tokens: 2,
|
||||
total_tokens: 12,
|
||||
..Default::default()
|
||||
};
|
||||
let conversation = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap();
|
||||
let temp_dir = tempdir().expect("temp dir");
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
std::fs::write(&rollout_path, "{}\n").expect("write rollout");
|
||||
|
||||
let summary = session_summary(
|
||||
usage,
|
||||
Some(conversation),
|
||||
/*thread_name*/ None,
|
||||
Some(&rollout_path),
|
||||
)
|
||||
.expect("summary");
|
||||
assert_eq!(
|
||||
summary.usage_line,
|
||||
Some("Token usage: total=12 input=10 output=2".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
summary.resume_command,
|
||||
Some("codex resume 123e4567-e89b-12d3-a456-426614174000".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_summary_uses_id_even_when_thread_has_name() {
|
||||
let usage = TokenUsage {
|
||||
input_tokens: 10,
|
||||
output_tokens: 2,
|
||||
total_tokens: 12,
|
||||
..Default::default()
|
||||
};
|
||||
let conversation = ThreadId::from_string("123e4567-e89b-12d3-a456-426614174000").unwrap();
|
||||
let temp_dir = tempdir().expect("temp dir");
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
std::fs::write(&rollout_path, "{}\n").expect("write rollout");
|
||||
|
||||
let summary = session_summary(
|
||||
usage,
|
||||
Some(conversation),
|
||||
Some("my-session".to_string()),
|
||||
Some(&rollout_path),
|
||||
)
|
||||
.expect("summary");
|
||||
assert_eq!(
|
||||
summary.resume_command,
|
||||
Some("codex resume 123e4567-e89b-12d3-a456-426614174000".to_string())
|
||||
);
|
||||
}
|
||||
201
codex-rs/tui/src/app/tests/startup.rs
Normal file
201
codex-rs/tui/src/app/tests/startup.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn startup_waiting_gate_is_only_for_fresh_or_exit_session_selection() {
|
||||
assert_eq!(
|
||||
App::should_wait_for_initial_session(&SessionSelection::StartFresh),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
App::should_wait_for_initial_session(&SessionSelection::Exit),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
App::should_wait_for_initial_session(&SessionSelection::Resume(
|
||||
crate::resume_picker::SessionTarget {
|
||||
path: Some(PathBuf::from("/tmp/restore")),
|
||||
thread_id: ThreadId::new(),
|
||||
}
|
||||
)),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
App::should_wait_for_initial_session(&SessionSelection::Fork(
|
||||
crate::resume_picker::SessionTarget {
|
||||
path: Some(PathBuf::from("/tmp/fork")),
|
||||
thread_id: ThreadId::new(),
|
||||
}
|
||||
)),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_tooltip_override_is_only_prepared_for_fresh_or_exit_session_selection() {
|
||||
assert!(App::should_prepare_startup_tooltip_override(
|
||||
&SessionSelection::StartFresh
|
||||
));
|
||||
assert!(App::should_prepare_startup_tooltip_override(
|
||||
&SessionSelection::Exit
|
||||
));
|
||||
assert!(!App::should_prepare_startup_tooltip_override(
|
||||
&SessionSelection::Resume(crate::resume_picker::SessionTarget {
|
||||
path: Some(PathBuf::from("/tmp/restore")),
|
||||
thread_id: ThreadId::new(),
|
||||
})
|
||||
));
|
||||
assert!(!App::should_prepare_startup_tooltip_override(
|
||||
&SessionSelection::Fork(crate::resume_picker::SessionTarget {
|
||||
path: Some(PathBuf::from("/tmp/fork")),
|
||||
thread_id: ThreadId::new(),
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_paused_goal_prompt_gate_is_only_for_quiet_resume() {
|
||||
let resume = SessionSelection::Resume(crate::resume_picker::SessionTarget {
|
||||
path: Some(PathBuf::from("/tmp/restore")),
|
||||
thread_id: ThreadId::new(),
|
||||
});
|
||||
let fork = SessionSelection::Fork(crate::resume_picker::SessionTarget {
|
||||
path: Some(PathBuf::from("/tmp/fork")),
|
||||
thread_id: ThreadId::new(),
|
||||
});
|
||||
let no_images: Vec<PathBuf> = Vec::new();
|
||||
let initial_images = vec![PathBuf::from("/tmp/image.png")];
|
||||
|
||||
assert!(App::should_prompt_for_paused_goal_after_startup_resume(
|
||||
&resume, &None, &no_images
|
||||
));
|
||||
assert!(!App::should_prompt_for_paused_goal_after_startup_resume(
|
||||
&resume,
|
||||
&Some("continue from here".to_string()),
|
||||
&no_images
|
||||
));
|
||||
assert!(!App::should_prompt_for_paused_goal_after_startup_resume(
|
||||
&resume,
|
||||
&None,
|
||||
&initial_images
|
||||
));
|
||||
assert!(!App::should_prompt_for_paused_goal_after_startup_resume(
|
||||
&SessionSelection::StartFresh,
|
||||
&None,
|
||||
&no_images
|
||||
));
|
||||
assert!(!App::should_prompt_for_paused_goal_after_startup_resume(
|
||||
&fork, &None, &no_images
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_waiting_gate_holds_active_thread_events_until_primary_thread_configured() {
|
||||
let mut wait_for_initial_session =
|
||||
App::should_wait_for_initial_session(&SessionSelection::StartFresh);
|
||||
assert_eq!(wait_for_initial_session, true);
|
||||
assert_eq!(
|
||||
App::should_handle_active_thread_events(
|
||||
wait_for_initial_session,
|
||||
/*has_active_thread_receiver*/ true
|
||||
),
|
||||
false
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
App::should_stop_waiting_for_initial_session(
|
||||
wait_for_initial_session,
|
||||
/*primary_thread_id*/ None
|
||||
),
|
||||
false
|
||||
);
|
||||
if App::should_stop_waiting_for_initial_session(wait_for_initial_session, Some(ThreadId::new()))
|
||||
{
|
||||
wait_for_initial_session = false;
|
||||
}
|
||||
assert_eq!(wait_for_initial_session, false);
|
||||
|
||||
assert_eq!(
|
||||
App::should_handle_active_thread_events(
|
||||
wait_for_initial_session,
|
||||
/*has_active_thread_receiver*/ true
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_waiting_gate_not_applied_for_resume_or_fork_session_selection() {
|
||||
let wait_for_resume = App::should_wait_for_initial_session(&SessionSelection::Resume(
|
||||
crate::resume_picker::SessionTarget {
|
||||
path: Some(PathBuf::from("/tmp/restore")),
|
||||
thread_id: ThreadId::new(),
|
||||
},
|
||||
));
|
||||
assert_eq!(
|
||||
App::should_handle_active_thread_events(
|
||||
wait_for_resume,
|
||||
/*has_active_thread_receiver*/ true
|
||||
),
|
||||
true
|
||||
);
|
||||
let wait_for_fork = App::should_wait_for_initial_session(&SessionSelection::Fork(
|
||||
crate::resume_picker::SessionTarget {
|
||||
path: Some(PathBuf::from("/tmp/fork")),
|
||||
thread_id: ThreadId::new(),
|
||||
},
|
||||
));
|
||||
assert_eq!(
|
||||
App::should_handle_active_thread_events(
|
||||
wait_for_fork,
|
||||
/*has_active_thread_receiver*/ true
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ignore_same_thread_resume_reports_noop_for_current_thread() {
|
||||
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
|
||||
let thread_id = ThreadId::new();
|
||||
let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));
|
||||
app.chat_widget.handle_thread_session(session.clone());
|
||||
app.thread_event_channels.insert(
|
||||
thread_id,
|
||||
ThreadEventChannel::new_with_session(THREAD_EVENT_CHANNEL_CAPACITY, session, Vec::new()),
|
||||
);
|
||||
app.activate_thread_channel(thread_id).await;
|
||||
while app_event_rx.try_recv().is_ok() {}
|
||||
|
||||
let ignored = app.ignore_same_thread_resume(&crate::resume_picker::SessionTarget {
|
||||
path: Some(test_path_buf("/tmp/project")),
|
||||
thread_id,
|
||||
});
|
||||
|
||||
assert!(ignored);
|
||||
let cell = match app_event_rx.try_recv() {
|
||||
Ok(AppEvent::InsertHistoryCell(cell)) => cell,
|
||||
other => panic!("expected info message after same-thread resume, saw {other:?}"),
|
||||
};
|
||||
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
|
||||
assert!(rendered.contains(&format!(
|
||||
"Already viewing {}.",
|
||||
test_path_display("/tmp/project")
|
||||
)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ignore_same_thread_resume_allows_reattaching_displayed_inactive_thread() {
|
||||
let mut app = make_test_app().await;
|
||||
let thread_id = ThreadId::new();
|
||||
let session = test_thread_session(thread_id, test_path_buf("/tmp/project"));
|
||||
app.chat_widget.handle_thread_session(session);
|
||||
|
||||
let ignored = app.ignore_same_thread_resume(&crate::resume_picker::SessionTarget {
|
||||
path: Some(test_path_buf("/tmp/project")),
|
||||
thread_id,
|
||||
});
|
||||
|
||||
assert!(!ignored);
|
||||
assert!(app.transcript_cells.is_empty());
|
||||
}
|
||||
@@ -1269,6 +1269,15 @@ impl App {
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn should_prepare_startup_tooltip_override(
|
||||
session_selection: &SessionSelection,
|
||||
) -> bool {
|
||||
matches!(
|
||||
session_selection,
|
||||
SessionSelection::StartFresh | SessionSelection::Exit
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn should_prompt_for_paused_goal_after_startup_resume(
|
||||
session_selection: &SessionSelection,
|
||||
initial_prompt: &Option<String>,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/goal_validation.rs
|
||||
expression: rendered
|
||||
---
|
||||
■ Goal objective is too long: 4,001 characters. Limit: 4,000 characters. Put longer instructions in a file and refer to that file in the goal, for example: /goal follow the instructions in docs/goal.md.
|
||||
@@ -120,7 +120,12 @@ async fn goal_slash_command_rejects_oversized_objective() {
|
||||
"oversized goal should not emit a SetThreadGoalObjective event: {events:?}"
|
||||
);
|
||||
let rendered = rendered_insert_history(&events);
|
||||
assert_chatwidget_snapshot!("goal_slash_command_oversized_objective_error", rendered);
|
||||
assert!(rendered.contains("Goal objective is too long"));
|
||||
assert!(rendered.contains("Put longer instructions in a file"));
|
||||
assert!(
|
||||
!rendered.contains("Message exceeds the maximum length"),
|
||||
"expected goal-specific length error, got {rendered:?}"
|
||||
);
|
||||
assert_no_submit_op(&mut op_rx);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user