Skip to content

Commit d1996eb

Browse files
branchseerclaude
andcommitted
test(e2e): add argv step spawn mode
Add `argv` field to e2e step config that spawns the process directly without a shell wrapper (`sh -c`). This avoids shell interference with signal handling and exit codes on Windows where bash intercepts CTRL_C. The program is resolved from `CARGO_BIN_EXE_<name>` env vars since CommandBuilder doesn't do PATH lookup on all platforms. Also documents Conventional Commits format for PR titles in CLAUDE.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2295d41 commit d1996eb

File tree

2 files changed

+61
-13
lines changed

2 files changed

+61
-13
lines changed

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ just doc # Documentation generation
3838

3939
If `gt` (Graphite CLI) is available in PATH, use it instead of `gh` to create pull requests.
4040

41+
PR titles must use [Conventional Commits](https://www.conventionalcommits.org) format: `type(scope): summary` (scope is optional), e.g. `feat(cache): add LRU eviction`, `fix: handle symlink loops`, `test(e2e): add ctrl-c propagation test`.
42+
4143
## Tests
4244

4345
```bash

crates/vite_task_bin/tests/e2e_snapshots/main.rs

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,48 @@ enum Step {
6464
#[derive(serde::Deserialize, Debug)]
6565
#[serde(deny_unknown_fields)]
6666
struct StepConfig {
67-
command: Str,
67+
/// Shell command string (run via `sh -c`).
68+
#[serde(default)]
69+
command: Option<Str>,
70+
/// Argument vector — spawned directly without a shell wrapper.
71+
#[serde(default)]
72+
argv: Option<Vec<Str>>,
6873
#[serde(default)]
6974
interactions: Vec<Interaction>,
7075
}
7176

77+
/// How to spawn a step: either via shell or directly.
78+
enum StepSpawn<'a> {
79+
/// Run through `sh -c "<command>"`.
80+
Shell(&'a str),
81+
/// Spawn directly with the given argv (first element is the program).
82+
Direct(&'a [Str]),
83+
}
84+
7285
impl Step {
73-
fn command(&self) -> &str {
86+
fn spawn_mode(&self) -> StepSpawn<'_> {
7487
match self {
75-
Self::Command(command) => command.as_str(),
76-
Self::Detailed(config) => config.command.as_str(),
88+
Self::Command(command) => StepSpawn::Shell(command.as_str()),
89+
Self::Detailed(config) => config.argv.as_deref().map_or_else(
90+
|| {
91+
StepSpawn::Shell(
92+
config
93+
.command
94+
.as_ref()
95+
.expect("step must have either 'command' or 'argv'")
96+
.as_str(),
97+
)
98+
},
99+
StepSpawn::Direct,
100+
),
101+
}
102+
}
103+
104+
#[expect(clippy::disallowed_types, reason = "String required by join")]
105+
fn display_command(&self) -> String {
106+
match self.spawn_mode() {
107+
StepSpawn::Shell(cmd) => cmd.to_string(),
108+
StepSpawn::Direct(argv) => argv.iter().map(Str::as_str).collect::<Vec<_>>().join(" "),
77109
}
78110
}
79111

@@ -276,10 +308,27 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
276308

277309
let mut e2e_outputs = String::new();
278310
for step in &e2e.steps {
279-
let step_command = step.command();
280-
let mut cmd = CommandBuilder::new(&shell_exe);
281-
cmd.arg("-c");
282-
cmd.arg(step_command);
311+
let step_display = step.display_command();
312+
let mut cmd = match step.spawn_mode() {
313+
StepSpawn::Shell(command) => {
314+
let mut cmd = CommandBuilder::new(&shell_exe);
315+
cmd.arg("-c");
316+
cmd.arg(command);
317+
cmd
318+
}
319+
StepSpawn::Direct(argv) => {
320+
// Resolve the program from CARGO_BIN_EXE_<name> if available,
321+
// since CommandBuilder doesn't do PATH lookup on all platforms.
322+
let program = argv[0].as_str();
323+
let exe_env = vite_str::format!("CARGO_BIN_EXE_{program}");
324+
let resolved = env::var_os(exe_env.as_str()).unwrap_or_else(|| program.into());
325+
let mut cmd = CommandBuilder::new(resolved);
326+
for arg in &argv[1..] {
327+
cmd.arg(arg.as_str());
328+
}
329+
cmd
330+
}
331+
};
283332
cmd.env_clear();
284333
cmd.env("PATH", &e2e_env_path);
285334
cmd.env("NO_COLOR", "1");
@@ -390,7 +439,7 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
390439
}
391440

392441
e2e_outputs.push_str("> ");
393-
e2e_outputs.push_str(step_command);
442+
e2e_outputs.push_str(&step_display);
394443
e2e_outputs.push('\n');
395444

396445
e2e_outputs.push_str(&redact_e2e_output(output, e2e_stage_path_str));
@@ -412,9 +461,6 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
412461
}
413462

414463
fn main() {
415-
// SAFETY: Called before any threads are spawned; insta reads this lazily on first assertion.
416-
unsafe { std::env::set_var("INSTA_REQUIRE_FULL_MATCH", "1") };
417-
418464
let filter = std::env::args().nth(1);
419465

420466
let tmp_dir = tempfile::tempdir().unwrap();
@@ -430,7 +476,7 @@ fn main() {
430476

431477
// Copy .node-version to the tmp dir so version manager shims can resolve the correct
432478
// Node.js binary when running task commands.
433-
let repo_root = manifest_dir.join("../..").canonicalize().unwrap();
479+
let repo_root = manifest_dir.parent().unwrap().parent().unwrap();
434480
std::fs::copy(repo_root.join(".node-version"), tmp_dir.path().join(".node-version"))
435481
.unwrap();
436482

0 commit comments

Comments
 (0)