From 0a87b59792c50d8957766a3f981d996d3c4cd376 Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 18 Mar 2026 23:16:28 +0800 Subject: [PATCH 1/2] fix: make e2e tests resilient to version manager shims Version manager shims (e.g. mise) read `package.json` in the working directory to resolve the Node.js version. When fspy tracks task execution, this read gets captured as an inferred input, causing spurious cache misses when `package.json` is subsequently modified. Two fixes: - Move the `cache-miss-command-change` test's task definition from `package.json` scripts to `vite-task.json` tasks, so modifying the command no longer touches a file that shims read. - Copy `.node-version` into the e2e temp directory so shims can resolve the correct Node.js version even without a global install. Also hoists `test_bin_path` computation to `main()` to avoid redundant per-case `CARGO_MANIFEST_DIR` lookups. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cache-miss-command-change/package.json | 6 +-- .../cache-miss-command-change/snapshots.toml | 4 +- .../snapshots/cache miss command change.snap | 4 +- .../cache-miss-command-change/vite-task.json | 6 ++- .../vite_task_bin/tests/e2e_snapshots/main.rs | 53 +++++++++++-------- 5 files changed, 42 insertions(+), 31 deletions(-) diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/package.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/package.json index 73a11332..0967ef42 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/package.json +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/package.json @@ -1,5 +1 @@ -{ - "scripts": { - "task": "print foo && print bar" - } -} +{} diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/snapshots.toml index 36c09e7b..d6470879 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/snapshots.toml +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/snapshots.toml @@ -4,8 +4,8 @@ name = "cache miss command change" steps = [ "vt run task # cache miss", - "json-edit package.json '_.scripts.task = \"print baz && print bar\"' # change first subtask", + "json-edit vite-task.json '_.tasks.task.command = \"print baz && print bar\"' # change first subtask", "vt run task # first: cache miss, second: cache hit", - "json-edit package.json '_.scripts.task = \"print bar\"' # remove first subtask", + "json-edit vite-task.json '_.tasks.task.command = \"print bar\"' # remove first subtask", "vt run task # cache hit", ] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/snapshots/cache miss command change.snap b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/snapshots/cache miss command change.snap index b4b953c8..8f9c0e75 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/snapshots/cache miss command change.snap +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/snapshots/cache miss command change.snap @@ -11,7 +11,7 @@ bar --- vt run: 0/2 cache hit (0%). (Run `vt run --last-details` for full details) -> json-edit package.json '_.scripts.task = "print baz && print bar"' # change first subtask +> json-edit vite-task.json '_.tasks.task.command = "print baz && print bar"' # change first subtask > vt run task # first: cache miss, second: cache hit $ print baz ○ cache miss: args changed, executing @@ -22,7 +22,7 @@ bar --- vt run: 1/2 cache hit (50%), saved. (Run `vt run --last-details` for full details) -> json-edit package.json '_.scripts.task = "print bar"' # remove first subtask +> json-edit vite-task.json '_.tasks.task.command = "print bar"' # remove first subtask > vt run task # cache hit $ print bar ◉ cache hit, replaying diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/vite-task.json index d548edfa..11b87a3f 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/vite-task.json +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/cache-miss-command-change/vite-task.json @@ -1,3 +1,7 @@ { - "cache": true + "tasks": { + "task": { + "command": "print foo && print bar" + } + } } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/main.rs b/crates/vite_task_bin/tests/e2e_snapshots/main.rs index e17d6acd..aed47b50 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/main.rs +++ b/crates/vite_task_bin/tests/e2e_snapshots/main.rs @@ -207,7 +207,12 @@ struct SnapshotsFile { } #[expect(clippy::disallowed_types, reason = "Path required by insta::glob! callback signature")] -fn run_case(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, filter: Option<&str>) { +fn run_case( + tmpdir: &AbsolutePath, + fixture_path: &std::path::Path, + filter: Option<&str>, + test_bin_path: &Arc, +) { let fixture_name = fixture_path.file_name().unwrap().to_str().unwrap(); if fixture_name.starts_with('.') { return; // skip hidden files like .DS_Store @@ -229,7 +234,7 @@ fn run_case(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, filter: Optio settings.set_prepend_module_to_snapshot(false); settings.remove_snapshot_suffix(); - settings.bind(|| run_case_inner(tmpdir, fixture_path, fixture_name)); + settings.bind(|| run_case_inner(tmpdir, fixture_path, fixture_name, test_bin_path)); } enum TerminationState { @@ -245,7 +250,12 @@ enum TerminationState { clippy::disallowed_types, reason = "Path required by insta::glob! callback; String required by from_utf8_lossy and string accumulation" )] -fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture_name: &str) { +fn run_case_inner( + tmpdir: &AbsolutePath, + fixture_path: &std::path::Path, + fixture_name: &str, + test_bin_path: &Arc, +) { // Copy the case directory to a temporary directory to avoid discovering workspace outside of the test case. let stage_path = tmpdir.join(fixture_name); CopyOptions::new().copy_tree(fixture_path, stage_path.as_path()).unwrap(); @@ -264,17 +274,6 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture Err(err) => panic!("Failed to read cases.toml for fixture {fixture_name}: {err}"), }; - // Navigate from runtime CARGO_MANIFEST_DIR to packages/tools at the repo root. - #[expect( - clippy::disallowed_types, - reason = "Path required for CARGO_MANIFEST_DIR path traversal" - )] - let repo_root = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()); - let repo_root = repo_root.parent().unwrap().parent().unwrap(); - let test_bin_path = Arc::::from( - repo_root.join("packages").join("tools").join("node_modules").join(".bin").into_os_string(), - ); - // Get shell executable for running steps let shell_exe = get_shell_exe(); @@ -287,8 +286,8 @@ fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture let vt_dir = vt_path.parent().unwrap(); vt_dir.as_path().as_os_str().into() }, - // Include packages/tools to PATH so that e2e tests can run utilities such as replace-file-content. - test_bin_path, + // Include tool binaries (print, json-edit, etc.) from repo root node_modules. + Arc::clone(test_bin_path), ] .into_iter() .chain( @@ -477,10 +476,22 @@ fn main() { clippy::disallowed_types, reason = "Path required for CARGO_MANIFEST_DIR path traversal" )] - let fixtures_dir = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()) - .join("tests") - .join("e2e_snapshots") - .join("fixtures"); + let (repo_root, fixtures_dir) = { + let manifest_dir = + std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let repo_root = manifest_dir.join("../..").canonicalize().unwrap(); + let fixtures_dir = manifest_dir.join("tests/e2e_snapshots/fixtures"); + (repo_root, fixtures_dir) + }; + + // Copy .node-version to the tmp dir so version manager shims can resolve the correct Node.js + // binary when running task commands. + std::fs::copy(repo_root.join(".node-version"), tmp_dir.path().join(".node-version")).unwrap(); + + // Include packages/tools to PATH so that e2e tests can run utilities such as print, json-edit, etc. + let test_bin_path: Arc = + Arc::from(repo_root.join("packages/tools/node_modules/.bin").into_os_string()); + let mut fixture_paths = std::fs::read_dir(fixtures_dir) .unwrap() .map(|entry| entry.unwrap().path()) @@ -488,7 +499,7 @@ fn main() { fixture_paths.sort(); for case_path in &fixture_paths { - run_case(&tmp_dir_path, case_path, filter.as_deref()); + run_case(&tmp_dir_path, case_path, filter.as_deref(), &test_bin_path); } #[expect(clippy::print_stdout, reason = "test summary")] { From eda35235eb77a307bd64cc0f250888d8698dc9d5 Mon Sep 17 00:00:00 2001 From: branchseer Date: Wed, 18 Mar 2026 23:20:04 +0800 Subject: [PATCH 2/2] refactor: revert test_bin_path hoisting, keep it in run_case_inner Co-Authored-By: Claude Opus 4.6 (1M context) --- .../vite_task_bin/tests/e2e_snapshots/main.rs | 52 +++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/crates/vite_task_bin/tests/e2e_snapshots/main.rs b/crates/vite_task_bin/tests/e2e_snapshots/main.rs index aed47b50..47529395 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/main.rs +++ b/crates/vite_task_bin/tests/e2e_snapshots/main.rs @@ -207,12 +207,7 @@ struct SnapshotsFile { } #[expect(clippy::disallowed_types, reason = "Path required by insta::glob! callback signature")] -fn run_case( - tmpdir: &AbsolutePath, - fixture_path: &std::path::Path, - filter: Option<&str>, - test_bin_path: &Arc, -) { +fn run_case(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, filter: Option<&str>) { let fixture_name = fixture_path.file_name().unwrap().to_str().unwrap(); if fixture_name.starts_with('.') { return; // skip hidden files like .DS_Store @@ -234,7 +229,7 @@ fn run_case( settings.set_prepend_module_to_snapshot(false); settings.remove_snapshot_suffix(); - settings.bind(|| run_case_inner(tmpdir, fixture_path, fixture_name, test_bin_path)); + settings.bind(|| run_case_inner(tmpdir, fixture_path, fixture_name)); } enum TerminationState { @@ -250,12 +245,7 @@ enum TerminationState { clippy::disallowed_types, reason = "Path required by insta::glob! callback; String required by from_utf8_lossy and string accumulation" )] -fn run_case_inner( - tmpdir: &AbsolutePath, - fixture_path: &std::path::Path, - fixture_name: &str, - test_bin_path: &Arc, -) { +fn run_case_inner(tmpdir: &AbsolutePath, fixture_path: &std::path::Path, fixture_name: &str) { // Copy the case directory to a temporary directory to avoid discovering workspace outside of the test case. let stage_path = tmpdir.join(fixture_name); CopyOptions::new().copy_tree(fixture_path, stage_path.as_path()).unwrap(); @@ -274,6 +264,17 @@ fn run_case_inner( Err(err) => panic!("Failed to read cases.toml for fixture {fixture_name}: {err}"), }; + // Navigate from runtime CARGO_MANIFEST_DIR to packages/tools at the repo root. + #[expect( + clippy::disallowed_types, + reason = "Path required for CARGO_MANIFEST_DIR path traversal" + )] + let repo_root = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()); + let repo_root = repo_root.parent().unwrap().parent().unwrap(); + let test_bin_path = Arc::::from( + repo_root.join("packages").join("tools").join("node_modules").join(".bin").into_os_string(), + ); + // Get shell executable for running steps let shell_exe = get_shell_exe(); @@ -286,8 +287,8 @@ fn run_case_inner( let vt_dir = vt_path.parent().unwrap(); vt_dir.as_path().as_os_str().into() }, - // Include tool binaries (print, json-edit, etc.) from repo root node_modules. - Arc::clone(test_bin_path), + // Include packages/tools to PATH so that e2e tests can run utilities such as replace-file-content. + test_bin_path, ] .into_iter() .chain( @@ -476,21 +477,18 @@ fn main() { clippy::disallowed_types, reason = "Path required for CARGO_MANIFEST_DIR path traversal" )] - let (repo_root, fixtures_dir) = { + let fixtures_dir = { let manifest_dir = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap()); - let repo_root = manifest_dir.join("../..").canonicalize().unwrap(); - let fixtures_dir = manifest_dir.join("tests/e2e_snapshots/fixtures"); - (repo_root, fixtures_dir) - }; - // Copy .node-version to the tmp dir so version manager shims can resolve the correct Node.js - // binary when running task commands. - std::fs::copy(repo_root.join(".node-version"), tmp_dir.path().join(".node-version")).unwrap(); + // Copy .node-version to the tmp dir so version manager shims can resolve the correct + // Node.js binary when running task commands. + let repo_root = manifest_dir.join("../..").canonicalize().unwrap(); + std::fs::copy(repo_root.join(".node-version"), tmp_dir.path().join(".node-version")) + .unwrap(); - // Include packages/tools to PATH so that e2e tests can run utilities such as print, json-edit, etc. - let test_bin_path: Arc = - Arc::from(repo_root.join("packages/tools/node_modules/.bin").into_os_string()); + manifest_dir.join("tests/e2e_snapshots/fixtures") + }; let mut fixture_paths = std::fs::read_dir(fixtures_dir) .unwrap() @@ -499,7 +497,7 @@ fn main() { fixture_paths.sort(); for case_path in &fixture_paths { - run_case(&tmp_dir_path, case_path, filter.as_deref(), &test_bin_path); + run_case(&tmp_dir_path, case_path, filter.as_deref()); } #[expect(clippy::print_stdout, reason = "test summary")] {