566 lines
22 KiB
Rust
566 lines
22 KiB
Rust
//! Declarative end-to-end TUI scenario tests.
|
|
//!
|
|
//! These tests exercise the real `xai-grok-pager` binary through a PTY using
|
|
//! YAML scenarios under `tests/scenarios/`. They are ignored by default because
|
|
//! they build/spawn the pager and stream through a mock inference server.
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use xai_grok_pager_pty_harness::{
|
|
ScriptedRunConfig, ScriptedRunStatus, ScriptedScenario, ScriptedScenarioRunner, pager_binary,
|
|
};
|
|
|
|
fn scenario_path(name: &str) -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests")
|
|
.join("scenarios")
|
|
.join(name)
|
|
}
|
|
|
|
async fn run_scenario(name: &str) {
|
|
let scenario = ScriptedScenario::from_file(&scenario_path(name)).expect("load scenario");
|
|
let artifact_dir = std::env::var_os("CARGO_TARGET_TMPDIR")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(std::env::temp_dir)
|
|
.join("scripted-scenarios");
|
|
let runner = ScriptedScenarioRunner::new(ScriptedRunConfig::new(
|
|
pager_binary().expect("resolve pager binary"),
|
|
artifact_dir,
|
|
));
|
|
let report = runner.run(&scenario).await.expect("run scenario");
|
|
|
|
assert_eq!(
|
|
report.status,
|
|
ScriptedRunStatus::Passed,
|
|
"scenario {} did not pass; report: {report:#?}",
|
|
report.scenario,
|
|
);
|
|
assert!(
|
|
report.bugs.is_empty(),
|
|
"scenario {} identified bugs: {:#?}",
|
|
report.scenario,
|
|
report.bugs
|
|
);
|
|
assert!(
|
|
report
|
|
.steps
|
|
.iter()
|
|
.any(|step| step.action == "screenshot" && !step.artifacts.is_empty()),
|
|
"scenario {} did not capture screenshots/artifacts",
|
|
report.scenario
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore]
|
|
async fn scripted_welcome_screen() {
|
|
run_scenario("welcome.yaml").await;
|
|
}
|
|
|
|
/// Resize while slash dropdown is open must not kill the pager.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_slash_resize_storm() {
|
|
run_scenario("slash_resize_storm.yaml").await;
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore]
|
|
async fn scripted_release_notes_scroll() {
|
|
run_scenario("release_notes_scroll.yaml").await;
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore]
|
|
async fn scripted_mock_response() {
|
|
run_scenario("mock_response.yaml").await;
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore]
|
|
async fn scripted_input_modalities() {
|
|
run_scenario("input_modalities.yaml").await;
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore]
|
|
async fn scripted_ansi_execute_output() {
|
|
run_scenario("ansi_execute_output.yaml").await;
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore]
|
|
async fn scripted_copy_selection() {
|
|
run_scenario("copy_selection.yaml").await;
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore]
|
|
async fn scripted_table_cell_selection() {
|
|
run_scenario("table_cell_selection.yaml").await;
|
|
}
|
|
|
|
/// Type-to-find pickers carry vim-mode. With vim-mode ON the command
|
|
/// palette opens in INPUT (a letter filters immediately); Esc clears the query
|
|
/// then a second Esc drops to NAV (letters no longer filter); `i` re-enters
|
|
/// INPUT. The footer's `i search` hint is absent on open (input) and present in
|
|
/// nav.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_vim_modal_command_palette() {
|
|
run_scenario("vim_modal_command_palette.yaml").await;
|
|
}
|
|
|
|
/// Tool header selection copies only the operand (path/command), not the
|
|
/// `Read ` / `Run ` / `$ ` label — regression guard for Selectable::Spans on
|
|
/// tool-call headers.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_tool_header_path_selection() {
|
|
run_scenario("tool_header_path_selection.yaml").await;
|
|
}
|
|
|
|
/// Typed `[` must appear in the prompt without a follow-up keystroke.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_bracket_prompt_input() {
|
|
run_scenario("bracket_prompt_input.yaml").await;
|
|
}
|
|
|
|
/// Double-clicking a `[Pasted: N lines]` chip expands it into plain
|
|
/// editable prompt text (user feedback: clicking the chip means "edit").
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_paste_chip_double_click() {
|
|
run_scenario("paste_chip_double_click.yaml").await;
|
|
}
|
|
|
|
/// Pasting identical content twice expands the chip instead of stacking a
|
|
/// duplicate one ("paste again" means "show the text").
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_paste_chip_repaste() {
|
|
run_scenario("paste_chip_repaste.yaml").await;
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore]
|
|
async fn scripted_image_inputs() {
|
|
run_scenario("image_inputs.yaml").await;
|
|
}
|
|
|
|
/// Path-free image-chip preview and dismissal on a non-graphics PTY.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_image_chip_preview_path_free() {
|
|
run_scenario("image_chip_preview_path_free.yaml").await;
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_image_normalize_corrupt_dropped() {
|
|
run_scenario("image_normalize_corrupt_dropped.yaml").await;
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_image_normalize_persist_atomic() {
|
|
run_scenario("image_normalize_persist_atomic.yaml").await;
|
|
}
|
|
|
|
/// Mermaid fence (the engine is always compiled in) → inline Unicode-art diagram
|
|
/// + clickable affordance row ([Open Image] [Copy Image Path] [Copy Source]) and
|
|
/// NO inline image. The diagram renders as text art and the run never panics.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_mermaid_affordances() {
|
|
run_scenario("mermaid-affordances.yaml").await;
|
|
}
|
|
|
|
/// User report: file path with a space (`Demo App.app`) must render
|
|
/// fully and the ptyctl/PTY harness must carry OSC 8 for the *full* path
|
|
/// (`Demo%20App.app`), not a truncated link ending at `Demo`.
|
|
/// Screen assertions live in `path_space_hyperlink.yaml`; this wrapper also
|
|
/// checks `raw_output.bin` so CI has byte-level proof without only relying on
|
|
/// the ignored `pty_e2e` test.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_path_space_hyperlink() {
|
|
const FILE_URL_MARKER: &str = "Demo%20App.app";
|
|
|
|
let scenario = ScriptedScenario::from_file(&scenario_path("path_space_hyperlink.yaml"))
|
|
.expect("load scenario");
|
|
let artifact_dir = std::env::var_os("CARGO_TARGET_TMPDIR")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(std::env::temp_dir)
|
|
.join("scripted-scenarios");
|
|
let runner = ScriptedScenarioRunner::new(ScriptedRunConfig::new(
|
|
pager_binary().expect("resolve pager binary"),
|
|
artifact_dir,
|
|
));
|
|
let report = runner.run(&scenario).await.expect("run scenario");
|
|
|
|
let raw_path = report.artifact_dir.join("raw_output.bin");
|
|
let raw_bytes = std::fs::read(&raw_path).unwrap_or_else(|err| {
|
|
panic!(
|
|
"raw_output.bin missing at {} (report status: {:?}): {err}",
|
|
raw_path.display(),
|
|
report.status
|
|
);
|
|
});
|
|
let raw = String::from_utf8_lossy(&raw_bytes);
|
|
|
|
eprintln!(
|
|
"\n=== PTY path-space hyperlink (scripted_path_space_hyperlink) ===\n\
|
|
scenario status: {:?}\n\
|
|
raw PTY bytes: {}\n\
|
|
OSC 8 present: {}\n\
|
|
full URL marker: {}\n\
|
|
===============================================================\n",
|
|
report.status,
|
|
raw_bytes.len(),
|
|
raw.contains("\x1b]8;"),
|
|
raw.contains(FILE_URL_MARKER),
|
|
);
|
|
|
|
assert_eq!(
|
|
report.status,
|
|
ScriptedRunStatus::Passed,
|
|
"scenario failed: {:#?}",
|
|
report.bugs
|
|
);
|
|
assert!(
|
|
raw.contains("\x1b]8;"),
|
|
"expected OSC 8 hyperlink sequences in ptyctl PTY stream (path should be clickable)"
|
|
);
|
|
assert!(
|
|
raw.contains(FILE_URL_MARKER),
|
|
"OSC 8 file:// URL must include `{FILE_URL_MARKER}` so click/underline covers \
|
|
`Demo App.app`, not just `Demo` (pre-fix truncated at the space).\n\
|
|
truncated-only would omit this marker."
|
|
);
|
|
let has_truncated_only =
|
|
raw.contains("mac-arm64/Demo\x07") || raw.contains("mac-arm64/Demo\x1b\\");
|
|
assert!(
|
|
!has_truncated_only || raw.contains(FILE_URL_MARKER),
|
|
"must not emit a truncated OSC 8 ending at `Demo` without the space suffix"
|
|
);
|
|
}
|
|
|
|
/// Count Kitty APC action tokens in raw PTY bytes.
|
|
fn count_kitty_actions(raw: &[u8]) -> (usize, usize, usize) {
|
|
let text = String::from_utf8_lossy(raw);
|
|
let mut transmit_display = 0usize;
|
|
let mut transmit_only = 0usize;
|
|
let mut place_only = 0usize;
|
|
for part in text.split("\x1b_G") {
|
|
if part.is_empty() {
|
|
continue;
|
|
}
|
|
let params = part.split(';').next().unwrap_or("");
|
|
if params.contains("a=T") {
|
|
transmit_display += 1;
|
|
} else if params.contains("a=t") {
|
|
transmit_only += 1;
|
|
}
|
|
if params.contains("a=p") {
|
|
place_only += 1;
|
|
}
|
|
}
|
|
(transmit_display, transmit_only, place_only)
|
|
}
|
|
|
|
/// PTY harness: paste one PNG, hammer redraws, prove uploads stay bounded.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_inline_image_memory() {
|
|
let scenario = ScriptedScenario::from_file(&scenario_path("inline_image_memory.yaml"))
|
|
.expect("load scenario");
|
|
let artifact_dir = std::env::var_os("CARGO_TARGET_TMPDIR")
|
|
.map(PathBuf::from)
|
|
.unwrap_or_else(std::env::temp_dir)
|
|
.join("scripted-scenarios");
|
|
let runner = ScriptedScenarioRunner::new(ScriptedRunConfig::new(
|
|
pager_binary().expect("resolve pager binary"),
|
|
artifact_dir.clone(),
|
|
));
|
|
let report = runner.run(&scenario).await.expect("run scenario");
|
|
|
|
// Harness writes raw bytes under the per-run artifact dir (see scripted.rs).
|
|
let raw_path = report.artifact_dir.join("raw_output.bin");
|
|
let raw = std::fs::read(&raw_path).unwrap_or_else(|err| {
|
|
panic!(
|
|
"raw_output.bin missing at {} (report status: {:?}): {err}",
|
|
raw_path.display(),
|
|
report.status
|
|
);
|
|
});
|
|
|
|
let (at, a_t, a_p) = count_kitty_actions(&raw);
|
|
eprintln!(
|
|
"\n=== PTY inline image memory (scripted_inline_image_memory) ===\n\
|
|
scenario status: {:?}\n\
|
|
raw PTY bytes: {}\n\
|
|
Kitty a=T (combined upload+display): {at}\n\
|
|
Kitty a=t (transmit only): {a_t}\n\
|
|
Kitty a=p (place only): {a_p}\n\
|
|
===============================================================\n",
|
|
report.status,
|
|
raw.len()
|
|
);
|
|
|
|
assert!(
|
|
at + a_t + a_p > 0,
|
|
"expected Kitty graphics in raw PTY output"
|
|
);
|
|
|
|
let uploads = at + a_t;
|
|
assert!(
|
|
uploads < 30,
|
|
"too many image upload APCs (a=T + a=t = {uploads}); place={a_p}"
|
|
);
|
|
if a_p > 0 {
|
|
assert!(
|
|
a_p >= uploads,
|
|
"expected place-only (a=p={a_p}) >= uploads (a=T+a=t={uploads})"
|
|
);
|
|
}
|
|
|
|
assert_eq!(
|
|
report.status,
|
|
ScriptedRunStatus::Passed,
|
|
"scenario failed: {:#?}",
|
|
report.bugs
|
|
);
|
|
}
|
|
|
|
/// enterprise deploy report: with `GROK_GOAL=1`, `/goal` must
|
|
/// show in the slash menu on the welcome screen *before* the first user turn.
|
|
/// The scenario types `/goal` pre-session and asserts the dropdown carries the
|
|
/// builtin's description (which only renders when the command is advertised),
|
|
/// then captures screenshot artifacts.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_goal_slash_presession() {
|
|
run_scenario("goal_slash_presession.yaml").await;
|
|
}
|
|
|
|
/// Counterpart to `scripted_goal_slash_presession`: with the goal flag
|
|
/// explicitly off (`GROK_GOAL=0`; goal mode defaults on), `/goal` must stay
|
|
/// hidden pre-session (gate fail-closed) while an
|
|
/// AlwaysOn builtin (`/compact`) still shows — proving it's the gate, not a
|
|
/// broken dropdown.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_goal_slash_presession_disabled() {
|
|
run_scenario("goal_slash_presession_disabled.yaml").await;
|
|
}
|
|
|
|
/// Full folder-trust session: with `GROK_FOLDER_TRUST=1` and a git repo that
|
|
/// ships a repo-local `.mcp.json` (declared via the scenario `workspace`), the
|
|
/// trust question renders before any session, accepting it (`y`) lets the
|
|
/// session proceed, and a submitted prompt streams the mock response. The
|
|
/// declarative counterpart to the programmatic `folder_trust_*` PTY tests.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_folder_trust_prompt() {
|
|
run_scenario("folder_trust_prompt.yaml").await;
|
|
}
|
|
|
|
/// Dashboard `/model` list: mouse-clicking a model must accept the completion
|
|
/// into the dispatch prompt and must not attach the session row under the
|
|
/// dropdown (click-through regression).
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_dashboard_model_list_click() {
|
|
run_scenario("dashboard_model_list_click.yaml").await;
|
|
}
|
|
|
|
/// Shortcuts cheatsheet end-to-end: open the modal, inline-expand a hint (→) and
|
|
/// collapse it (←), open the man-style detail screen (Enter) showing long_help,
|
|
/// return to browse (Esc), and close with the global Ctrl+X chord.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_shortcuts_help_detail() {
|
|
run_scenario("shortcuts_help_detail.yaml").await;
|
|
}
|
|
|
|
/// Undo tip happy path: a substantial Ctrl+U wipe shows the "… to undo"
|
|
/// banner (which then clears on its own TTL — no early retire on undo).
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_undo_tip_clear_shows() {
|
|
run_scenario("undo_tip_clear_shows.yaml").await;
|
|
}
|
|
|
|
/// Undo tip Ctrl+C variant: clearing a substantial draft with Ctrl+C shows
|
|
/// the banner too.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_undo_tip_ctrl_c_clear_shows() {
|
|
run_scenario("undo_tip_ctrl_c_clear_shows.yaml").await;
|
|
}
|
|
|
|
/// Undo tip no-show edge: wiping a short (<20 char) draft shows nothing.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_undo_tip_short_draft_no_show() {
|
|
run_scenario("undo_tip_short_draft_no_show.yaml").await;
|
|
}
|
|
|
|
/// Undo tip no-show edge: on a short terminal the renderability gate refuses
|
|
/// the banner even after a substantial wipe.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_undo_tip_short_terminal_no_show() {
|
|
run_scenario("undo_tip_short_terminal_no_show.yaml").await;
|
|
}
|
|
|
|
/// Undo tip no-show edge: accepting an @-file completion is not a wipe and
|
|
/// must not hint.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_undo_tip_completion_accept_no_show() {
|
|
run_scenario("undo_tip_completion_accept_no_show.yaml").await;
|
|
}
|
|
|
|
/// Plan-nudge happy path: with contextual hints enabled, typing a planning
|
|
/// keyword shows the "Planning? Check out plan mode via shift+tab" banner.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_plan_nudge_shows() {
|
|
run_scenario("plan_nudge_shows.yaml").await;
|
|
}
|
|
|
|
/// Plan-nudge opt-out edge: contextual hints ship default-OFF, so this scenario
|
|
/// also pins `GROK_CONTEXTUAL_HINTS=0` (belt-and-suspenders); the same planning
|
|
/// keyword shows nothing.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_plan_nudge_opt_out_no_show() {
|
|
run_scenario("plan_nudge_opt_out_no_show.yaml").await;
|
|
}
|
|
|
|
/// Follow-up: a mid-message `/test-skill` token stays teal in the
|
|
/// scrollback echo after submit (screenshots capture composer + echo styling).
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_mid_text_skill_token_echo() {
|
|
run_scenario("mid_text_skill_token_echo.yaml").await;
|
|
}
|
|
|
|
/// Auto-compact: shrinking to 14 rows drops the sticky prev-question header
|
|
/// (compact chrome engages); growing back to 32 rows restores it.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_auto_compact_resize() {
|
|
run_scenario("auto_compact_resize.yaml").await;
|
|
}
|
|
|
|
/// Small-screen tip happy path: a 24-row terminal (in the 21..=28 band) shows
|
|
/// the one-shot "Tight on space? Try /compact-mode" banner, which expires on
|
|
/// its TTL and never re-shows after resizes.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_small_screen_tip_band() {
|
|
run_scenario("small_screen_tip_band.yaml").await;
|
|
}
|
|
|
|
/// Small-screen tip no-show edge: a 40-row terminal is above the band.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_small_screen_tip_no_show_tall() {
|
|
run_scenario("small_screen_tip_no_show_tall.yaml").await;
|
|
}
|
|
|
|
/// Small-screen tip no-show edge: at 14 rows auto-compact engages instead and
|
|
/// the banner row cannot render.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_small_screen_tip_no_show_tiny() {
|
|
run_scenario("small_screen_tip_no_show_tiny.yaml").await;
|
|
}
|
|
|
|
/// Inline edit-and-resubmit happy path: Enter on a selected previous prompt
|
|
/// opens the in-place editor; Enter with changed text raises the inline
|
|
/// rewind popup ("Resubmit from here", no files-only row); `b` + `y`
|
|
/// rewinds the conversation and resubmits the edited text as a fresh turn
|
|
/// (distinct per-turn mock sentinels prove the tail was truncated), with no
|
|
/// "Reverted conversation" note.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_inline_edit_resubmit() {
|
|
run_scenario("inline_edit_resubmit.yaml").await;
|
|
}
|
|
|
|
/// Enter on an unchanged inline edit just closes the editor: no rewind
|
|
/// popup, no resubmit, transcript untouched.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_inline_edit_unchanged_exit() {
|
|
run_scenario("inline_edit_unchanged_exit.yaml").await;
|
|
}
|
|
|
|
/// Esc from the inline resubmit popup returns to the still-open editor with
|
|
/// the edit intact; a second Esc discards the edit and restores the original
|
|
/// prompt text in place.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
#[ignore = "scripted scenario; run with cargo test -- --ignored"]
|
|
async fn scripted_inline_edit_dismiss_returns_to_editor() {
|
|
run_scenario("inline_edit_dismiss_returns_to_editor.yaml").await;
|
|
}
|
|
|
|
/// Sanity-check: every scenario YAML in the list below must parse. Runs at
|
|
/// `cargo test` time without `--ignored` so a malformed YAML breaks CI
|
|
/// immediately rather than only when the scripted runner is opted in.
|
|
#[test]
|
|
fn scenarios_parse() {
|
|
for name in [
|
|
"image_normalize_corrupt_dropped.yaml",
|
|
"image_normalize_persist_atomic.yaml",
|
|
"mermaid-affordances.yaml",
|
|
"path_space_hyperlink.yaml",
|
|
"goal_slash_presession.yaml",
|
|
"goal_slash_presession_disabled.yaml",
|
|
"folder_trust_prompt.yaml",
|
|
"dashboard_model_list_click.yaml",
|
|
"paste_chip_double_click.yaml",
|
|
"paste_chip_repaste.yaml",
|
|
"slash_resize_storm.yaml",
|
|
"shortcuts_help_detail.yaml",
|
|
"undo_tip_clear_shows.yaml",
|
|
"undo_tip_ctrl_c_clear_shows.yaml",
|
|
"undo_tip_short_draft_no_show.yaml",
|
|
"undo_tip_short_terminal_no_show.yaml",
|
|
"undo_tip_completion_accept_no_show.yaml",
|
|
"plan_nudge_shows.yaml",
|
|
"plan_nudge_opt_out_no_show.yaml",
|
|
"mid_text_skill_token_echo.yaml",
|
|
"auto_compact_resize.yaml",
|
|
"small_screen_tip_band.yaml",
|
|
"small_screen_tip_no_show_tall.yaml",
|
|
"small_screen_tip_no_show_tiny.yaml",
|
|
"inline_edit_resubmit.yaml",
|
|
"inline_edit_unchanged_exit.yaml",
|
|
"inline_edit_dismiss_returns_to_editor.yaml",
|
|
] {
|
|
let path = scenario_path(name);
|
|
let scenario =
|
|
ScriptedScenario::from_file(&path).unwrap_or_else(|err| panic!("parse {name}: {err}"));
|
|
assert!(!scenario.steps.is_empty(), "{name} has no steps");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn ansi_execute_output_scenario_parses() {
|
|
let path = scenario_path("ansi_execute_output.yaml");
|
|
let scenario = ScriptedScenario::from_file(&path).unwrap_or_else(|err| panic!("parse: {err}"));
|
|
assert!(!scenario.steps.is_empty(), "scenario has no steps");
|
|
}
|
|
|
|
#[test]
|
|
fn vim_modal_command_palette_scenario_parses() {
|
|
let path = scenario_path("vim_modal_command_palette.yaml");
|
|
let scenario = ScriptedScenario::from_file(&path).unwrap_or_else(|err| panic!("parse: {err}"));
|
|
assert!(!scenario.steps.is_empty(), "scenario has no steps");
|
|
}
|