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 } } }