返回 CodeWhale
eval_smoke_acceptance.rs
根目录 / crates / tui / tests / eval_smoke_acceptance.rs
1 //! Gherkin acceptance test: eval smoke test.
2 //!
3 //! Verifies that the binary loads and the eval harness reports step-level
4 //! success for a shell command after Layer 4.2 registry cleanup. Follows the
5 //! proven `core_session_command_extraction.rs` pattern.
6 //!
7 //! NOTE: This is an eval smoke test, not a command-surface verification
8 //! (AT-004) test. It confirms the binary starts and runs eval correctly.
9 //! For AT-004 command-surface coverage (help, palette, completion), see the
10 //! focused unit tests in command_palette.rs, widgets/mod.rs, and
11 //! commands/mod.rs.
12
13 use std::path::PathBuf;
14 use std::process::{Command, ExitStatus};
15
16 use cucumber::{World as _, given, then, when, writer::Stats as _};
17 use serde_json::Value;
18 use tempfile::TempDir;
19
20 const FEATURE_NAME: &str = "Eval smoke test (binary load and eval step reporting)";
21 const FEATURE_PATH: &str = concat!(
22 env!("CARGO_MANIFEST_DIR"),
23 "/tests/features/eval_smoke.feature"
24 );
25 const SMOKE_SCENARIO: &str = "Binary loads and reports step-level success via eval";
26
27 #[derive(Debug, Default, cucumber::World)]
28 struct EvalSmokeWorld {
29 _record_dir: Option<TempDir>,
30 report: Option<Value>,
31 exit_status: Option<ExitStatus>,
32 }
33
34 #[given("a clean CodeWhale evaluation workspace")]
35 fn clean_codewhale_evaluation_workspace(world: &mut EvalSmokeWorld) {
36 world._record_dir = Some(TempDir::new().expect("evaluation TempDir"));
37 }
38
39 #[when("the evaluation harness runs a shell command")]
40 fn eval_harness_runs_shell_command(world: &mut EvalSmokeWorld) {
41 let record_dir = world
42 ._record_dir
43 .as_ref()
44 .expect("evaluation workspace should exist");
45
46 let output = Command::new(codewhale_tui_binary())
47 .args([
48 "eval",
49 "--json",
50 "--shell-command",
51 "echo eval-smoke-test",
52 "--record",
53 ])
54 .arg(record_dir.path())
55 .output()
56 .expect("codewhale-tui eval should start");
57
58 // Capture stdout/stderr for diagnostics
59 let stdout = String::from_utf8_lossy(&output.stdout);
60 let stderr = String::from_utf8_lossy(&output.stderr);
61
62 let report: Value = serde_json::from_str(&stdout).unwrap_or_else(|err| {
63 panic!("eval --json should emit valid JSON: {err}\nstdout:\n{stdout}\nstderr:\n{stderr}")
64 });
65
66 world.exit_status = Some(output.status);
67 world.report = Some(report);
68 }
69
70 #[then("the binary exits without crashing")]
71 fn binary_exits_without_crashing(world: &mut EvalSmokeWorld) {
72 let status = world
73 .exit_status
74 .expect("exit status should have been captured");
75
76 // The eval harness exits with code 1 when `metrics.success` is false
77 // (run_eval in main.rs uses `bail!("...")` for non-successful scenarios).
78 // This is expected behavior: the eval runs a multi-step scenario offline
79 // (List, Read, Search, Edit, ApplyPatch, ExecShell) and the overall
80 // metrics.success reflects all steps, not just Bash. The Bash
81 // step itself succeeds — see `json_report_contains_execution_steps`.
82 //
83 // What we verify here:
84 // 1. The process ran to completion (was killed by no signal)
85 // 2. A known exit code was produced (not a crash/hang)
86 // 3. Step-level success is validated by the next Gherkin step.
87 let exit_code = status.code().expect("process should have terminated");
88 assert_no_signal_crash(&status);
89 assert!(
90 exit_code == 0 || exit_code == 1,
91 "codewhale-tui eval exited with unexpected code {exit_code} (expected 0 or 1)"
92 );
93
94 let report = world.report.as_ref().expect("eval report should exist");
95 let steps = report
96 .get("steps")
97 .and_then(|value| value.as_array())
98 .expect("eval report should have a 'steps' array");
99 assert!(
100 !steps.is_empty(),
101 "eval report should have at least one step"
102 );
103 }
104
105 #[then("the JSON report contains execution steps")]
106 fn json_report_contains_execution_steps(world: &mut EvalSmokeWorld) {
107 let report = world.report.as_ref().expect("eval report should exist");
108 let steps = report
109 .get("steps")
110 .and_then(|value| value.as_array())
111 .expect("eval report should have a 'steps' array");
112
113 // Find the Bash step and verify it contains the expected output
114 let exec_step = steps
115 .iter()
116 .find(|step| step.get("kind").and_then(|v| v.as_str()) == Some("Bash"))
117 .expect("eval report should have a Bash step");
118
119 let step_success = exec_step
120 .get("success")
121 .and_then(|v| v.as_bool())
122 .unwrap_or(false);
123 assert!(step_success, "Bash step should succeed, got: {exec_step:?}");
124
125 let output = exec_step
126 .get("output")
127 .and_then(|v| v.as_str())
128 .unwrap_or("");
129 assert!(
130 output.contains("eval-smoke-test"),
131 "Bash output should contain the shell command echo, got: {output}"
132 );
133 }
134
135 #[tokio::test(flavor = "current_thread")]
136 async fn eval_smoke_binary_loads_and_reports_steps() {
137 let writer = EvalSmokeWorld::cucumber()
138 .fail_on_skipped()
139 .with_default_cli()
140 .filter_run(FEATURE_PATH, move |feature, _, scenario| {
141 feature.name == FEATURE_NAME && scenario.name == SMOKE_SCENARIO
142 })
143 .await;
144 assert_eq!(
145 writer.failed_steps(),
146 0,
147 "scenario failed: {SMOKE_SCENARIO}"
148 );
149 assert_eq!(
150 writer.skipped_steps(),
151 0,
152 "scenario skipped steps: {SMOKE_SCENARIO}"
153 );
154 assert_eq!(
155 writer.passed_steps(),
156 4,
157 "scenario did not run: {SMOKE_SCENARIO}"
158 );
159 }
160
161 /// Assert the process was not killed by a signal (Unix-only check).
162 #[cfg(unix)]
163 fn assert_no_signal_crash(status: &ExitStatus) {
164 use std::os::unix::process::ExitStatusExt;
165 assert!(
166 status.signal().is_none(),
167 "codewhale-tui eval was killed by signal {} (crash?)",
168 status.signal().unwrap()
169 );
170 }
171
172 /// No-op on non-Unix platforms where `ExitStatusExt` is unavailable.
173 #[cfg(not(unix))]
174 fn assert_no_signal_crash(_status: &ExitStatus) {}
175
176 fn codewhale_tui_binary() -> PathBuf {
177 if let Some(path) = option_env!("CARGO_BIN_EXE_codewhale-tui") {
178 return PathBuf::from(path);
179 }
180 if let Ok(path) = std::env::var("CARGO_BIN_EXE_codewhale-tui") {
181 return PathBuf::from(path);
182 }
183
184 let mut path = std::env::current_exe().expect("current test executable path");
185 path.pop();
186 if path.ends_with("deps") {
187 path.pop();
188 }
189 path.push(format!("codewhale-tui{}", std::env::consts::EXE_SUFFIX));
190 path
191 }
192
192 lines RUST