From 675e0cb5c9ab7faafd51cea26862df1e21b67abf Mon Sep 17 00:00:00 2001 From: wan9chi Date: Sat, 13 Jun 2026 18:33:40 +0800 Subject: [PATCH] fix(cache): provide runner IPC for explicit-input tasks Motivation: Cached tasks already expose runner IPC for disableCache, but the server was only bound when fspy auto-input inference was enabled. Tasks with explicit inputs therefore lost the IPC handoff and runner-aware tools silently fell back to no-op behavior. Scope: Bind the IPC server for every cached execution, keep fspy tracking optional inside that cached tracking state, and leave uncached executions without IPC injection. This PR does not add env APIs or new cache fingerprint fields. Verification: - UPDATE_SNAPSHOTS=1 cargo test -p vite_task_bin --test e2e_snapshots disable_cache_works_with_explicit_inputs -- --ignored - cargo test -p vite_task_bin --test e2e_snapshots disable_cache_works_with_explicit_inputs -- --ignored --- .../src/session/execute/cache_update.rs | 17 +++--- crates/vite_task/src/session/execute/mod.rs | 60 ++++++++++--------- .../fixtures/ipc_client_test/snapshots.toml | 24 ++++++++ ...isable_cache_works_with_explicit_inputs.md | 39 ++++++++++++ .../fixtures/ipc_client_test/vite-task.json | 5 ++ 5 files changed, 109 insertions(+), 36 deletions(-) create mode 100644 crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/disable_cache_works_with_explicit_inputs.md diff --git a/crates/vite_task/src/session/execute/cache_update.rs b/crates/vite_task/src/session/execute/cache_update.rs index 45b679e5..e2e86a5c 100644 --- a/crates/vite_task/src/session/execute/cache_update.rs +++ b/crates/vite_task/src/session/execute/cache_update.rs @@ -53,7 +53,8 @@ pub(super) async fn update_cache( cancelled: bool, ) -> (CacheUpdateStatus, Option) { let CacheState { metadata, globbed_inputs, std_outputs, tracking } = state; - let fspy_negatives = tracking.as_ref().map(|t| t.input_negative_globs.as_slice()); + let fspy = tracking.fspy.as_ref(); + let input_negative_globs = fspy.map(|t| t.input_negative_globs.as_slice()); if let Some(reports) = reports && reports.cache_disabled @@ -73,7 +74,7 @@ pub(super) async fn update_cache( return (CacheUpdateStatus::NotUpdated(CacheNotUpdatedReason::NonZeroExitStatus), None); } - let fspy_outcome = observe_fspy(outcome, fspy_negatives, workspace_root); + let fspy_outcome = observe_fspy(outcome, input_negative_globs, workspace_root); if let Some(TrackingOutcome { read_write_overlap: Some(path), .. }) = &fspy_outcome { // fspy-inferred read-write overlap: the task wrote to a file it also @@ -89,7 +90,7 @@ pub(super) async fn update_cache( ); } - if fspy_outcome.is_none() && fspy_negatives.is_some() { + if fspy_outcome.is_none() && fspy.is_some() { // Task requested fspy auto-inference but this binary was built without // `cfg(fspy)`. Task ran, but we can't compute a valid cache entry // without tracked path accesses. @@ -139,20 +140,20 @@ pub(super) async fn update_cache( } /// Summarize the run's fspy observations. `Some` iff tracking was both -/// requested (`fspy_negatives.is_some()`) and compiled in (`cfg(fspy)`). On a +/// requested (`input_negative_globs.is_some()`) and compiled in (`cfg(fspy)`). On a /// `cfg(not(fspy))` build this is always `None`, and [`update_cache`] /// short-circuits to `FspyUnsupported` when tracking was needed. fn observe_fspy( outcome: &ChildOutcome, - fspy_negatives: Option<&[wax::Glob<'static>]>, + input_negative_globs: Option<&[wax::Glob<'static>]>, workspace_root: &AbsolutePath, ) -> Option { #[cfg(fspy)] { use super::tracked_accesses::TrackedPathAccesses; - outcome.path_accesses.as_ref().zip(fspy_negatives).map(|(raw, negs)| { - let tracked = TrackedPathAccesses::from_raw(raw, workspace_root, negs); + outcome.path_accesses.as_ref().zip(input_negative_globs).map(|(raw, negatives)| { + let tracked = TrackedPathAccesses::from_raw(raw, workspace_root, negatives); let read_write_overlap = tracked.path_reads.keys().find(|p| tracked.path_writes.contains(*p)).cloned(); TrackingOutcome { path_reads: tracked.path_reads, read_write_overlap } @@ -160,7 +161,7 @@ fn observe_fspy( } #[cfg(not(fspy))] { - let _ = (outcome, fspy_negatives, workspace_root); + let _ = (outcome, input_negative_globs, workspace_root); None } } diff --git a/crates/vite_task/src/session/execute/mod.rs b/crates/vite_task/src/session/execute/mod.rs index 7b550877..e6f73650 100644 --- a/crates/vite_task/src/session/execute/mod.rs +++ b/crates/vite_task/src/session/execute/mod.rs @@ -88,21 +88,27 @@ struct CacheState<'a> { /// Captured stdout/stderr for cache replay. Written in place during drain; /// always present (possibly empty) once we reach the cache-update phase. std_outputs: Vec, - /// `Some` iff auto-input tracking is on (`input.includes_auto` + successful - /// IPC bind). Bundles fspy's input negative globs with the per-task IPC - /// server that runner-aware tools talk to. Parts are borrowed in place - /// during the wait/join; the struct is never moved out. - tracking: Option, + /// Runner-aware tracking for cached tasks: an IPC server is always + /// available, and fspy path tracing is attached only when auto input + /// inference needs it. Parts are borrowed in place during the wait/join; + /// the struct is never moved out. + tracking: Tracking, } /// The IPC server's driver future: resolves with the recorded reports after /// [`StopAccepting::signal`] fires and all in-flight clients drain. type IpcDriver = LocalBoxFuture<'static, Result>; -/// Per-task tracking: fspy input-negative globs + IPC server handle. +/// fspy path-tracking state, present only when a cached task needs automatic +/// input inference. +struct FspyTracking { + input_negative_globs: Vec>, +} + +/// Per-task runner-aware tracking: IPC server handle plus optional fspy state. /// Lifetime-tied to a single `execute_spawn` call. struct Tracking { - input_negative_globs: Vec>, + fspy: Option, ipc_envs: Vec<(&'static OsStr, OsString)>, ipc_server_fut: IpcDriver, stop_accepting: StopAccepting, @@ -127,7 +133,7 @@ struct RunHandles<'m> { /// Pipe writers + capture slot. `None` only in the inherited-uncached /// case, where there are no pipes to drain. sinks: Option>, - /// The IPC server's handles. `None` iff tracking is off. + /// The IPC server's handles. `None` iff execution is uncached. ipc: Option>, } @@ -158,7 +164,7 @@ impl<'a> ExecutionMode<'a> { }); }; - let tracking = if metadata.input_config.includes_auto { + let fspy = if metadata.input_config.includes_auto { // Resolve input negative globs for fspy path filtering (already // workspace-root-relative). let negatives = metadata @@ -168,21 +174,19 @@ impl<'a> ExecutionMode<'a> { .map(|p| Ok(wax::Glob::new(p.as_str())?.into_owned())) .collect::>>() .map_err(ExecutionError::PostRunFingerprint)?; - // fspy + IPC are bundled. If binding the IPC server fails we abort - // the execution — tools that rely on IPC would otherwise silently - // diverge from the cache. - let (envs, ServerHandle { driver, stop_accepting }) = - serve(Recorder::new()).map_err(ExecutionError::IpcServerBind)?; - Some(Tracking { - input_negative_globs: negatives, - ipc_envs: envs.collect(), - ipc_server_fut: driver, - stop_accepting, - }) + Some(FspyTracking { input_negative_globs: negatives }) } else { None }; + // Bind runner IPC for every cached task. The merged cache-control API + // (`disableCache`) must work even when a task uses explicit inputs and + // therefore does not need fspy auto-input inference. + let (ipc_envs, ServerHandle { driver, stop_accepting }) = + serve(Recorder::new()).map_err(ExecutionError::IpcServerBind)?; + let tracking = + Tracking { fspy, ipc_envs: ipc_envs.collect(), ipc_server_fut: driver, stop_accepting }; + Ok(Self::Cached { pipe_writers: stdio_config.writers, state: CacheState { metadata, globbed_inputs, std_outputs: Vec::new(), tracking }, @@ -190,11 +194,11 @@ impl<'a> ExecutionMode<'a> { } /// The extra envs to inject into the child: IPC connection info + the - /// napi addon path runner-aware tools `require()`. Empty when tracking - /// is off. + /// napi addon path runner-aware tools `require()`. Empty when execution + /// is uncached. fn injected_envs(&self) -> Vec<(&OsStr, &OsStr)> { match self { - Self::Cached { state: CacheState { tracking: Some(t), .. }, .. } => { + Self::Cached { state: CacheState { tracking: t, .. }, .. } => { let mut envs: Vec<(&OsStr, &OsStr)> = t.ipc_envs.iter().map(|(k, v)| (*k, v.as_os_str())).collect(); envs.push(( @@ -203,7 +207,7 @@ impl<'a> ExecutionMode<'a> { )); envs } - _ => Vec::new(), + Self::Uncached { .. } => Vec::new(), } } @@ -211,7 +215,7 @@ impl<'a> ExecutionMode<'a> { /// whether fspy tracking is on. const fn spawn_config(&self) -> (SpawnStdio, bool) { match self { - Self::Cached { state, .. } => (SpawnStdio::Piped, state.tracking.is_some()), + Self::Cached { state, .. } => (SpawnStdio::Piped, state.tracking.fspy.is_some()), Self::Uncached { pipe_writers: Some(_) } => (SpawnStdio::Piped, false), Self::Uncached { pipe_writers: None } => (SpawnStdio::Inherited, false), } @@ -228,9 +232,9 @@ impl<'a> ExecutionMode<'a> { stderr_writer: &mut pipe_writers.stderr_writer, capture: Some(&mut state.std_outputs), }); - let ipc = state.tracking.as_mut().map(|t| IpcHandles { - stop_accepting: &t.stop_accepting, - driver: &mut t.ipc_server_fut, + let ipc = Some(IpcHandles { + stop_accepting: &state.tracking.stop_accepting, + driver: &mut state.tracking.ipc_server_fut, }); RunHandles { sinks, ipc } } diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml index 88e64c93..b21b88e3 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml @@ -22,3 +22,27 @@ steps = [ "--last-details", ], comment = "summary names the opt-out as the not-cached reason" }, ] + +[[e2e]] +name = "disable_cache_works_with_explicit_inputs" +comment = """ +Exercises `disableCache` on a cached task with explicit inputs. The runner must still inject IPC even when fspy auto-input inference is disabled, or the tool's cache opt-out becomes a no-op and the second run incorrectly hits. +""" +ignore = true +steps = [ + { argv = [ + "vt", + "run", + "disable-cache-explicit-input", + ], comment = "first run uses input: [] and asks the runner not to cache" }, + { argv = [ + "vt", + "run", + "disable-cache-explicit-input", + ], comment = "re-executes because the first run was not cached" }, + { argv = [ + "vt", + "run", + "--last-details", + ], comment = "summary names the opt-out as the not-cached reason" }, +] diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/disable_cache_works_with_explicit_inputs.md b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/disable_cache_works_with_explicit_inputs.md new file mode 100644 index 00000000..96e62345 --- /dev/null +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots/disable_cache_works_with_explicit_inputs.md @@ -0,0 +1,39 @@ +# disable_cache_works_with_explicit_inputs + +Exercises `disableCache` on a cached task with explicit inputs. The runner must still inject IPC even when fspy auto-input inference is disabled, or the tool's cache opt-out becomes a no-op and the second run incorrectly hits. + +## `vt run disable-cache-explicit-input` + +first run uses input: [] and asks the runner not to cache + +``` +$ node scripts/disable_cache.mjs +``` + +## `vt run disable-cache-explicit-input` + +re-executes because the first run was not cached + +``` +$ node scripts/disable_cache.mjs +``` + +## `vt run --last-details` + +summary names the opt-out as the not-cached reason + +``` + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + Vite+ Task Runner • Execution Summary +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Statistics: 1 tasks • 0 cache hits • 1 cache misses +Performance: 0% cache hit rate + +Task Details: +──────────────────────────────────────────────── + [1] ipc-client-test#disable-cache-explicit-input: $ node scripts/disable_cache.mjs ✓ + → Not cached: the task opted out of caching +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` diff --git a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json index a9199101..91f0d226 100644 --- a/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json +++ b/crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json @@ -3,6 +3,11 @@ "disable-cache": { "command": "node scripts/disable_cache.mjs", "cache": true + }, + "disable-cache-explicit-input": { + "command": "node scripts/disable_cache.mjs", + "input": [], + "cache": true } } }