Skip to content

Commit 9641f06

Browse files
wan9chiclaude
andcommitted
fix(ipc): serve getEnv(s) from the spawn's full env context saved in the plan
A command-prefixed env (PREFIXED_ENV=x node tool.mjs) is part of the spawn's env, but getEnv resolved against the session snapshot only and answered (unset) — and recorded that wrong value into the post-run fingerprint. The plan now saves two env maps per spawn: all_envs (filtered, passed to the child — unchanged) and full_envs, the planning context's envs overlaid with the spawn's own additions. Nested expansions already thread prefix envs into the context, so full_envs picks up enclosing runs' prefixes too (taskA: PREFIXED_A=a vt run taskB → taskB's getEnv sees PREFIXED_A). full_envs is serde-skipped: it mirrors the ambient env and is not part of the plan's identity. The Recorder and tracked-env validation both use full_envs, keeping record and lookup symmetric per cache key (prefix envs are in the spawn fingerprint, so a changed prefix is a different entry). e2e: fetch_env_sees_command_prefix_env (was failing, now green) and fetch_env_sees_intermediate_prefix_envs (nested prefix accumulation + cache-hit validation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 76348ae commit 9641f06

14 files changed

Lines changed: 212 additions & 19 deletions

File tree

crates/vite_task/src/session/execute/mod.rs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,6 @@ struct ExecutionContext<'a> {
8686
/// Workspace root that relative paths in cache entries (inputs, outputs,
8787
/// archives) are resolved against.
8888
workspace_root: &'a Arc<AbsolutePath>,
89-
/// The session env snapshot the plan was bootstrapped from. Serves IPC
90-
/// `getEnv`/`getEnvs` and validates tracked envs at cache lookup — the
91-
/// live process env is never re-read during execution.
92-
envs: &'a Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
9389
/// Directory where cache files (db, archives) are stored.
9490
cache_dir: &'a AbsolutePath,
9591
/// Public-facing program name (e.g. `vp`), used in user-facing error
@@ -252,7 +248,6 @@ impl ExecutionContext<'_> {
252248
spawn_execution,
253249
self.cache,
254250
self.workspace_root,
255-
self.envs,
256251
self.cache_dir,
257252
self.program_name,
258253
self.fast_fail_token.clone(),
@@ -364,13 +359,17 @@ pub async fn execute_spawn(
364359
spawn_execution: &SpawnExecution,
365360
cache: &ExecutionCache,
366361
workspace_root: &Arc<AbsolutePath>,
367-
envs: &Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
368362
cache_dir: &AbsolutePath,
369363
program_name: &str,
370364
fast_fail_token: CancellationToken,
371365
interrupt_token: CancellationToken,
372366
) -> SpawnOutcome {
373367
let cache_metadata = spawn_execution.cache_metadata.as_ref();
368+
// The spawn's full env context from the plan (session envs plus prefix
369+
// envs of this command and of enclosing nested runs). Resolves IPC
370+
// `getEnv`/`getEnvs` and validates tracked envs — the live process env is
371+
// never re-read during execution.
372+
let envs = &spawn_execution.spawn_command.full_envs;
374373

375374
// 1. Determine cache status FIRST by trying cache hit.
376375
// We need to know the status before calling start() so the reporter
@@ -510,11 +509,11 @@ pub async fn execute_spawn(
510509
// we abort the execution — tools that rely on IPC would
511510
// otherwise silently diverge from the cache.
512511
//
513-
// The IPC `getEnv` endpoint serves values from the session
514-
// env snapshot (not the task's filtered `all_envs`), so a
515-
// tool can ask for vars the user never declared and have
512+
// The IPC `getEnv` endpoint serves values from the spawn's
513+
// full env context (not the task's filtered `all_envs`), so
514+
// a tool can ask for vars the user never declared and have
516515
// them fingerprinted via the tool's `tracked: true` flag.
517-
// The same snapshot validates these envs at cache lookup.
516+
// The same map validates these envs at cache lookup.
518517
match serve(Recorder::new(Arc::clone(envs))) {
519518
Ok((envs, ServerHandle { driver, stop_accepting })) => Some(Tracking {
520519
input_negative_globs: negatives,
@@ -1073,7 +1072,6 @@ impl Session<'_> {
10731072
reporter: &reporter,
10741073
cache,
10751074
workspace_root: &self.workspace_path,
1076-
envs: &self.envs,
10771075
cache_dir: &self.cache_path,
10781076
program_name: self.program_name.as_str(),
10791077
fast_fail_token: CancellationToken::new(),

crates/vite_task/src/session/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,7 +726,6 @@ impl<'a> Session<'a> {
726726
&spawn_execution,
727727
cache,
728728
&self.workspace_path,
729-
&self.envs,
730729
&self.cache_path,
731730
self.program_name.as_str(),
732731
tokio_util::sync::CancellationToken::new(),

crates/vite_task/src/session/reporter/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@ pub mod test_fixtures {
481481
program_path: test_path(),
482482
args: Arc::from([]),
483483
all_envs: Arc::new(BTreeMap::new()),
484+
full_envs: Arc::new(rustc_hash::FxHashMap::default()),
484485
cwd: test_path(),
485486
},
486487
})),
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { getEnv } from '@voidzero-dev/vite-task-client';
2+
import { writeFileSync, mkdirSync } from 'node:fs';
3+
4+
// Reached via `outer-prefixed` (`PREFIXED_A=a vt run inner-prefixed`): the
5+
// nested run carries PREFIXED_A into the planning context, and our own
6+
// command (`PREFIXED_B=b node ...`) prefixes PREFIXED_B. getEnv must serve
7+
// both. Only PREFIXED_B reaches this process's env — PREFIXED_A is
8+
// undeclared, so the spawn env filters it; the runner still knows it.
9+
const servedA = getEnv('PREFIXED_A', { tracked: true }) ?? '(unset)';
10+
const servedB = getEnv('PREFIXED_B', { tracked: true }) ?? '(unset)';
11+
const ownA = process.env.PREFIXED_A ?? '(unset)';
12+
const ownB = process.env.PREFIXED_B ?? '(unset)';
13+
14+
mkdirSync('dist', { recursive: true });
15+
writeFileSync(
16+
'dist/out.txt',
17+
`served A=${servedA} B=${servedB}\nprocess.env A=${ownA} B=${ownB}\n`,
18+
);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { getEnv } from '@voidzero-dev/vite-task-client';
2+
import { writeFileSync, mkdirSync } from 'node:fs';
3+
4+
// The task command prefixes `PREFIXED_ENV=from-command` (see vite-task.json),
5+
// so this process's own env carries that value. `getEnv` must serve the same
6+
// value: the runner resolves from the spawn's env context (session env
7+
// overlaid with the command's prefix envs), not the session env alone.
8+
const served = getEnv('PREFIXED_ENV', { tracked: true }) ?? '(unset)';
9+
const own = process.env.PREFIXED_ENV ?? '(unset)';
10+
11+
mkdirSync('dist', { recursive: true });
12+
writeFileSync('dist/out.txt', `served=${served}\nprocess.env=${own}\n`);

crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/snapshots.toml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,56 @@ steps = [
172172
],
173173
], comment = "cache miss: envs changed (PROBE_ENV changed)" },
174174
]
175+
176+
[[e2e]]
177+
name = "fetch_env_sees_command_prefix_env"
178+
comment = """
179+
A command-prefixed env (`PREFIXED_ENV=from-command node ...`) is part of the
180+
spawn's env context: the child process sees it in `process.env`, so
181+
`getEnv('PREFIXED_ENV')` must serve the same value. Today the runner resolves
182+
`getEnv` against the session env snapshot only, so it answers `(unset)` while
183+
the process env says `from-command`.
184+
"""
185+
ignore = true
186+
steps = [
187+
{ argv = [
188+
"vt",
189+
"run",
190+
"fetch-prefixed-env",
191+
], comment = "tool asks the runner for an env the command prefix sets" },
192+
{ argv = [
193+
"vtt",
194+
"print-file",
195+
"dist/out.txt",
196+
], comment = "served value must match the process env value" },
197+
]
198+
199+
[[e2e]]
200+
name = "fetch_env_sees_intermediate_prefix_envs"
201+
comment = """
202+
Prefix envs accumulate through nested runs: `outer-prefixed` is
203+
`PREFIXED_A=a vt run inner-prefixed`, and `inner-prefixed` is
204+
`PREFIXED_B=b node ...`. The inner tool's `getEnv` must see both — the
205+
nested expansion threads PREFIXED_A into the inner spawn's env context even
206+
though the (undeclared) var never reaches the inner process env. The second
207+
run validates the tracked values against the same plan-derived context and
208+
hits the cache.
209+
"""
210+
ignore = true
211+
steps = [
212+
{ argv = [
213+
"vt",
214+
"run",
215+
"outer-prefixed",
216+
], comment = "outer prefix env reaches the inner tool via the runner" },
217+
{ argv = [
218+
"vtt",
219+
"print-file",
220+
"dist/out.txt",
221+
], comment = "both prefix envs served; only the inner one is in process.env" },
222+
{ argv = [
223+
"vt",
224+
"run",
225+
"outer-prefixed",
226+
], comment = "cache hit: tracked values validate against the plan's env context" },
227+
]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# fetch_env_sees_command_prefix_env
2+
3+
A command-prefixed env (`PREFIXED_ENV=from-command node ...`) is part of the
4+
spawn's env context: the child process sees it in `process.env`, so
5+
`getEnv('PREFIXED_ENV')` must serve the same value. Today the runner resolves
6+
`getEnv` against the session env snapshot only, so it answers `(unset)` while
7+
the process env says `from-command`.
8+
9+
## `vt run fetch-prefixed-env`
10+
11+
tool asks the runner for an env the command prefix sets
12+
13+
```
14+
$ PREFIXED_ENV=from-command node scripts/fetch_prefixed_env.mjs
15+
```
16+
17+
## `vtt print-file dist/out.txt`
18+
19+
served value must match the process env value
20+
21+
```
22+
served=from-command
23+
process.env=from-command
24+
```
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# fetch_env_sees_intermediate_prefix_envs
2+
3+
Prefix envs accumulate through nested runs: `outer-prefixed` is
4+
`PREFIXED_A=a vt run inner-prefixed`, and `inner-prefixed` is
5+
`PREFIXED_B=b node ...`. The inner tool's `getEnv` must see both — the
6+
nested expansion threads PREFIXED_A into the inner spawn's env context even
7+
though the (undeclared) var never reaches the inner process env. The second
8+
run validates the tracked values against the same plan-derived context and
9+
hits the cache.
10+
11+
## `vt run outer-prefixed`
12+
13+
outer prefix env reaches the inner tool via the runner
14+
15+
```
16+
$ PREFIXED_B=b node scripts/fetch_intermediate_envs.mjs
17+
```
18+
19+
## `vtt print-file dist/out.txt`
20+
21+
both prefix envs served; only the inner one is in process.env
22+
23+
```
24+
served A=a B=b
25+
process.env A=(unset) B=b
26+
```
27+
28+
## `vt run outer-prefixed`
29+
30+
cache hit: tracked values validate against the plan's env context
31+
32+
```
33+
$ PREFIXED_B=b node scripts/fetch_intermediate_envs.mjs ◉ cache hit, replaying
34+
35+
---
36+
vt run: cache hit.
37+
```

crates/vite_task_bin/tests/e2e_snapshots/fixtures/ipc_client_test/vite-task.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@
1919
"fetch-envs": {
2020
"command": "node scripts/fetch_envs.mjs",
2121
"cache": true
22+
},
23+
"fetch-prefixed-env": {
24+
"command": "PREFIXED_ENV=from-command node scripts/fetch_prefixed_env.mjs",
25+
"cache": true
26+
},
27+
"outer-prefixed": {
28+
"command": "PREFIXED_A=a vt run inner-prefixed",
29+
"cache": true
30+
},
31+
"inner-prefixed": {
32+
"command": "PREFIXED_B=b node scripts/fetch_intermediate_envs.mjs",
33+
"cache": true
2234
}
2335
}
2436
}

crates/vite_task_client_napi/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ impl RunnerClient {
120120
/// IPC env is missing or the connection fails.
121121
#[napi]
122122
pub fn load() -> Result<RunnerClient> {
123+
#[expect(
124+
clippy::disallowed_methods,
125+
reason = "client load needs the live process env to discover runner IPC connection vars"
126+
)]
123127
let client = Client::from_envs(std::env::vars_os())
124128
.map_err(|err| {
125129
err_string(vite_str::format!("vp run client: failed to connect to runner IPC: {err}"))

0 commit comments

Comments
 (0)