返回 CodeWhale
acceptance.rs
根目录 / crates / tui / src / commands / groups / core / acceptance.rs
1 //! Gherkin acceptance coverage for visible core command surfaces.
2
3 use cucumber::{World as _, given, then, when, writer::Stats as _};
4 use tempfile::TempDir;
5
6 use crate::commands::{self, CommandResult};
7 use crate::config::{ApiProvider, Config};
8 use crate::test_support::{EnvVarGuard, lock_test_env};
9 use crate::tui::app::{App, TuiOptions};
10 use crate::tui::history::HistoryCell;
11
12 const FEATURE_NAME: &str = "Core command visible surfaces";
13 const FEATURE_PATH: &str = concat!(
14 env!("CARGO_MANIFEST_DIR"),
15 "/tests/features/core_command_surfaces.feature"
16 );
17 const INFORMATIONAL_SCENARIO: &str =
18 "Core informational commands write visible transcript messages";
19 const STATE_SCENARIO: &str = "Core state commands report visible changes";
20 const CLEAR_SCENARIO: &str = "Clear replaces prior transcript with visible confirmation";
21 const PERSISTENT_WORK_SCENARIO: &str = "Persistent work commands report visible dispatch requests";
22
23 #[derive(Default, cucumber::World)]
24 struct CoreCommandWorld {
25 tmpdir: Option<TempDir>,
26 app: Option<Box<App>>,
27 home_path: Option<std::path::PathBuf>,
28 last_message: Option<String>,
29 last_result_is_error: Option<bool>,
30 }
31
32 impl std::fmt::Debug for CoreCommandWorld {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 f.debug_struct("CoreCommandWorld")
35 .field("has_tmpdir", &self.tmpdir.is_some())
36 .field("has_app", &self.app.is_some())
37 .field("home_path", &self.home_path)
38 .field("last_message", &self.last_message)
39 .field("last_result_is_error", &self.last_result_is_error)
40 .finish()
41 }
42 }
43
44 #[given("a CodeWhale core command workspace")]
45 fn core_command_workspace(world: &mut CoreCommandWorld) {
46 let tmpdir = TempDir::new().expect("core command TempDir");
47 let mut app = create_test_app_with_tmpdir(&tmpdir);
48 app.ui_locale = crate::localization::Locale::En;
49 app.api_provider = ApiProvider::Deepseek;
50 app.model = "deepseek-v4-pro".to_string();
51 app.auto_model = false;
52 app.model_ids_passthrough = false;
53
54 world.home_path = Some(tmpdir.path().join("home"));
55 world.app = Some(Box::new(app));
56 world.tmpdir = Some(tmpdir);
57 }
58
59 #[given("a CodeWhale core command workspace with one visible user message")]
60 fn core_command_workspace_with_one_visible_user_message(world: &mut CoreCommandWorld) {
61 core_command_workspace(world);
62 let app = world.app.as_deref_mut().expect("app should exist");
63 app.add_message(HistoryCell::User {
64 content: "Remember the whale migration".to_string(),
65 });
66 }
67
68 #[when(regex = r#"^the user runs the core command "([^"]+)"$"#)]
69 fn user_runs_core_command(world: &mut CoreCommandWorld, command: String) {
70 let result = execute_isolated(world, &command);
71 record_visible_result(world, result);
72 }
73
74 #[then(regex = r#"^the message window should include "([^"]+)"$"#)]
75 fn message_window_should_include(world: &mut CoreCommandWorld, expected: String) {
76 let visible = visible_message_window(world);
77
78 assert!(
79 visible.contains(&expected),
80 "message window should include {expected:?}\nvisible transcript:\n{visible}"
81 );
82 }
83
84 #[then(regex = r#"^the message window should not include "([^"]+)"$"#)]
85 fn message_window_should_not_include(world: &mut CoreCommandWorld, forbidden: String) {
86 let visible = visible_message_window(world);
87
88 assert!(
89 !visible.contains(&forbidden),
90 "message window should not include {forbidden:?}\nvisible transcript:\n{visible}"
91 );
92 }
93
94 #[tokio::test(flavor = "current_thread")]
95 async fn core_informational_commands_write_visible_transcript_messages() {
96 run_scenario(INFORMATIONAL_SCENARIO, 11).await;
97 }
98
99 #[tokio::test(flavor = "current_thread")]
100 async fn core_state_commands_report_visible_changes() {
101 run_scenario(STATE_SCENARIO, 8).await;
102 }
103
104 #[tokio::test(flavor = "current_thread")]
105 async fn clear_replaces_prior_transcript_with_visible_confirmation() {
106 run_scenario(CLEAR_SCENARIO, 4).await;
107 }
108
109 #[tokio::test(flavor = "current_thread")]
110 async fn persistent_work_commands_report_visible_dispatch_requests() {
111 run_scenario(PERSISTENT_WORK_SCENARIO, 8).await;
112 }
113
114 async fn run_scenario(name: &'static str, expected_steps: usize) {
115 let writer = CoreCommandWorld::cucumber()
116 .fail_on_skipped()
117 .with_default_cli()
118 .filter_run(FEATURE_PATH, move |feature, _, scenario| {
119 feature.name == FEATURE_NAME && scenario.name == name
120 })
121 .await;
122 assert_eq!(writer.failed_steps(), 0, "scenario failed: {name}");
123 assert_eq!(writer.skipped_steps(), 0, "scenario skipped steps: {name}");
124 assert_eq!(
125 writer.passed_steps(),
126 expected_steps,
127 "scenario did not run: {name}"
128 );
129 }
130
131 fn create_test_app_with_tmpdir(tmpdir: &TempDir) -> App {
132 let options = TuiOptions {
133 skills_dir: tmpdir.path().join("skills"),
134 memory_path: tmpdir.path().join("memory.md"),
135 notes_path: tmpdir.path().join("notes.txt"),
136 mcp_config_path: tmpdir.path().join("mcp.json"),
137 ..crate::test_support::test_tui_options(tmpdir.path())
138 };
139 App::new(options, &Config::default())
140 }
141
142 fn execute_isolated(world: &mut CoreCommandWorld, command: &str) -> CommandResult {
143 let home = world
144 .home_path
145 .as_ref()
146 .expect("test home should exist")
147 .clone();
148 std::fs::create_dir_all(&home).expect("create isolated test home");
149
150 let _lock = lock_test_env();
151 let _home = EnvVarGuard::set("HOME", &home);
152 let _codewhale_home = EnvVarGuard::set("CODEWHALE_HOME", home.join(".codewhale"));
153
154 let app = world.app.as_deref_mut().expect("app should exist");
155 commands::user_registry::reload(Some(&app.workspace));
156 commands::execute(command, app)
157 }
158
159 fn record_visible_result(world: &mut CoreCommandWorld, result: CommandResult) {
160 world.last_result_is_error = Some(result.is_error);
161 world.last_message = result.message.clone();
162
163 if let Some(message) = result.message {
164 let app = world.app.as_deref_mut().expect("app should exist");
165 app.add_message(HistoryCell::System { content: message });
166 }
167 }
168
169 fn visible_message_window(world: &CoreCommandWorld) -> String {
170 let app = world.app.as_deref().expect("app should exist");
171 app.history
172 .iter()
173 .filter_map(|cell| match cell {
174 HistoryCell::User { content }
175 | HistoryCell::Assistant { content, .. }
176 | HistoryCell::System { content }
177 | HistoryCell::Thinking { content, .. } => Some(content.as_str()),
178 HistoryCell::Error { message, .. } => Some(message.as_str()),
179 HistoryCell::ArchivedContext { summary, .. } => Some(summary.as_str()),
180 HistoryCell::Tool(_) | HistoryCell::SubAgent(_) => None,
181 })
182 .collect::<Vec<_>>()
183 .join("\n")
184 }
185
185 lines RUST