Skip to content

Commit cae622a

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: - Use staged temp dir for insta snapshot paths so .snap.new writes don't fail on read-only filesystems (e.g. WebDAV via cargo-xtest) - Remove INSTA_REQUIRE_FULL_MATCH (false-positives on remote execution where snapshot source metadata differs) - Document 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 cae622a

File tree

2 files changed

+65
-15
lines changed

2 files changed

+65
-15
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: 63 additions & 15 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

@@ -187,9 +219,11 @@ fn run_case(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, filter: Optio
187219
{
188220
println!("{fixture_name}");
189221
}
190-
// Configure insta to write snapshots to fixture directory
222+
// Use the staged copy's snapshot dir so insta can write .snap.new files.
223+
// The original fixture_path may be on a read-only filesystem (e.g. WebDAV).
224+
let stage_snapshot_path = tmpdir.as_path().join(fixture_name).join("snapshots");
191225
let mut settings = insta::Settings::clone_current();
192-
settings.set_snapshot_path(fixture_path.join("snapshots"));
226+
settings.set_snapshot_path(stage_snapshot_path);
193227
settings.set_prepend_module_to_snapshot(false);
194228
settings.remove_snapshot_suffix();
195229

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

277311
let mut e2e_outputs = String::new();
278312
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);
313+
let step_display = step.display_command();
314+
let mut cmd = match step.spawn_mode() {
315+
StepSpawn::Shell(command) => {
316+
let mut cmd = CommandBuilder::new(&shell_exe);
317+
cmd.arg("-c");
318+
cmd.arg(command);
319+
cmd
320+
}
321+
StepSpawn::Direct(argv) => {
322+
// Resolve the program from CARGO_BIN_EXE_<name> if available,
323+
// since CommandBuilder doesn't do PATH lookup on all platforms.
324+
let program = argv[0].as_str();
325+
let exe_env = vite_str::format!("CARGO_BIN_EXE_{program}");
326+
let resolved = env::var_os(exe_env.as_str()).unwrap_or_else(|| program.into());
327+
let mut cmd = CommandBuilder::new(resolved);
328+
for arg in &argv[1..] {
329+
cmd.arg(arg.as_str());
330+
}
331+
cmd
332+
}
333+
};
283334
cmd.env_clear();
284335
cmd.env("PATH", &e2e_env_path);
285336
cmd.env("NO_COLOR", "1");
@@ -390,7 +441,7 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
390441
}
391442

392443
e2e_outputs.push_str("> ");
393-
e2e_outputs.push_str(step_command);
444+
e2e_outputs.push_str(&step_display);
394445
e2e_outputs.push('\n');
395446

396447
e2e_outputs.push_str(&redact_e2e_output(output, e2e_stage_path_str));
@@ -412,9 +463,6 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture
412463
}
413464

414465
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-
418466
let filter = std::env::args().nth(1);
419467

420468
let tmp_dir = tempfile::tempdir().unwrap();
@@ -430,7 +478,7 @@ fn main() {
430478

431479
// Copy .node-version to the tmp dir so version manager shims can resolve the correct
432480
// Node.js binary when running task commands.
433-
let repo_root = manifest_dir.join("../..").canonicalize().unwrap();
481+
let repo_root = manifest_dir.parent().unwrap().parent().unwrap();
434482
std::fs::copy(repo_root.join(".node-version"), tmp_dir.path().join(".node-version"))
435483
.unwrap();
436484

0 commit comments

Comments
 (0)