返回 CodeWhale
root_resume_dispatch.rs
根目录 / crates / cli / tests / root_resume_dispatch.rs
1 //! `codewhale --resume <id>`, `--resume=<id>`, `-r <id>` and `--session-id <id>`
2 //! are root dispatcher flags (the operations runbook advertises the first)
3 //! that forward to the in-process TUI's `--resume`. They used to be swallowed
4 //! by the trailing prompt positional and forwarded as `--prompt "--resume
5 //! <id>"`, which the TUI's own parser rejected (`error: unexpected argument
6 //! '--resume <id>' found`, exit 2). A non-TTY launch must now get past
7 //! argument parsing and fail only on the interactive-terminal contract, and
8 //! must not create session state under a sealed HOME on the way out.
9 //! Fresh-session and mouse flags exercise the same boundary: an unrecognized
10 //! `--fresh` previously swallowed the remaining flags as one prompt value.
11
12 #![cfg(unix)]
13
14 use std::path::{Path, PathBuf};
15 use std::process::{Command, Stdio};
16
17 use tempfile::TempDir;
18
19 fn codewhale_binary() -> PathBuf {
20 if let Some(path) = option_env!("CARGO_BIN_EXE_codewhale") {
21 return PathBuf::from(path);
22 }
23 if let Ok(path) = std::env::var("CARGO_BIN_EXE_codewhale") {
24 return PathBuf::from(path);
25 }
26 let mut path = std::env::current_exe().expect("current test executable path");
27 path.pop();
28 if path.ends_with("deps") {
29 path.pop();
30 }
31 path.push(format!("codewhale{}", std::env::consts::EXE_SUFFIX));
32 path
33 }
34
35 fn isolated_command(home: &Path, codewhale_home: &Path, workspace: &Path) -> Command {
36 let mut command = Command::new(codewhale_binary());
37 command
38 .env_clear()
39 .env("HOME", home)
40 .env("USERPROFILE", home)
41 .env("CODEWHALE_HOME", codewhale_home)
42 .env("CODEWHALE_SECRET_BACKEND", "file")
43 .env("CODEWHALE_NO_UPDATE_CHECK", "1")
44 .env("CODEWHALE_DISABLE_MODELS_DEV_FETCH", "1")
45 .env("CODEWHALE_TELEMETRY", "0")
46 .env("NO_COLOR", "1")
47 .current_dir(workspace)
48 .stdin(Stdio::null())
49 .stdout(Stdio::piped())
50 .stderr(Stdio::piped());
51 command
52 }
53
54 #[test]
55 fn root_session_flags_reach_the_tui_launch_contract_without_touching_state() {
56 for argv in [
57 &["--resume", "800596e6"][..],
58 &["--resume=800596e6"][..],
59 &["-r", "800596e6"][..],
60 &["--session-id", "800596e6"][..],
61 &["--continue"][..],
62 &["--no-project-config", "--fresh", "--mouse-capture"][..],
63 &["--no-project-config", "--mouse-capture", "--fresh"][..],
64 ] {
65 let fixture = TempDir::new().expect("fixture root");
66 let home = fixture.path().join("sealed-home");
67 let codewhale_home = fixture.path().join("sealed-codewhale-home");
68 let workspace = fixture.path().join("workspace with spaces");
69 std::fs::create_dir_all(&home).expect("home");
70 std::fs::create_dir_all(&workspace).expect("workspace");
71
72 let output = isolated_command(&home, &codewhale_home, &workspace)
73 .arg("--workspace")
74 .arg(&workspace)
75 .args(argv)
76 .output()
77 .expect("run codewhale");
78 let stderr = String::from_utf8_lossy(&output.stderr);
79 let stdout = String::from_utf8_lossy(&output.stdout);
80
81 assert!(
82 !output.status.success(),
83 "{argv:?} must not start an interactive TUI without a TTY:\n{stdout}\n{stderr}"
84 );
85 assert_ne!(
86 output.status.code(),
87 Some(2),
88 "{argv:?} must parse at the root dispatcher (clap usage error):\n{stderr}"
89 );
90 assert!(
91 !stderr.contains("unexpected argument") && !stderr.contains("--prompt <PROMPT>"),
92 "{argv:?} must not be forwarded as a prompt:\n{stderr}"
93 );
94 assert!(
95 stderr.contains("requires an interactive terminal"),
96 "{argv:?} must fail on the TTY contract, not earlier:\n{stdout}\n{stderr}"
97 );
98 assert!(
99 !stderr.contains("Recovered interrupted session"),
100 "{argv:?} must not consume a checkpoint when the TUI cannot start:\n{stderr}"
101 );
102 let sessions_dir = codewhale_home.join("sessions");
103 let persisted: Vec<_> = std::fs::read_dir(&sessions_dir)
104 .into_iter()
105 .flatten()
106 .flatten()
107 .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "json"))
108 .collect();
109 assert!(
110 persisted.is_empty(),
111 "{argv:?} must not write session files under a sealed home: {persisted:?}"
112 );
113 }
114 }
115
115 lines RUST