Skip to content

Commit 9f02db0

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 6f47765 commit 9f02db0

12 files changed

Lines changed: 132 additions & 11 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vite_task/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ tracing = { workspace = true }
4545
twox-hash = { workspace = true }
4646
materialized_artifact = { workspace = true }
4747
uuid = { workspace = true, features = ["v4"] }
48+
vite_glob = { workspace = true }
4849
vite_path = { workspace = true }
4950
vite_select = { workspace = true }
5051
vite_str = { workspace = true }

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`);
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_plan/src/context.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ pub struct PlanContext<'a> {
3030
/// The environment variables for the current execution context.
3131
///
3232
/// `Arc`-shared with copy-on-write semantics: [`duplicate`](Self::duplicate)
33-
/// and downstream consumers (`ScriptCommand::envs`) share the map;
34-
/// mutations ([`add_envs`](Self::add_envs),
35-
/// [`prepend_path`](Self::prepend_path)) clone only when the map is
36-
/// currently shared.
33+
/// and downstream consumers (`ScriptCommand::envs`,
34+
/// `SpawnCommand::full_envs`) share the map; mutations
35+
/// ([`add_envs`](Self::add_envs), [`prepend_path`](Self::prepend_path))
36+
/// clone only when the map is currently shared.
3737
envs: Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
3838

3939
/// The callbacks for loading task graphs and parsing commands.

crates/vite_task_plan/src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ pub struct SpawnCommand {
5050
#[serde(serialize_with = "serialize_envs")]
5151
pub all_envs: Arc<BTreeMap<Arc<OsStr>, Arc<OsStr>>>,
5252

53+
/// The command's full env context: the planning context's envs, which
54+
/// accumulate the prefix envs of this command and of enclosing nested
55+
/// `vp run` expansions on top of the session envs. Resolves runner-aware
56+
/// `getEnv`/`getEnvs` requests and validates tracked envs at cache lookup.
57+
///
58+
/// Not passed to the child process — that is the filtered
59+
/// [`all_envs`](Self::all_envs). Skipped in serialized plans: it mirrors the ambient
60+
/// environment, which is not part of the plan's identity and would bloat
61+
/// snapshots nondeterministically.
62+
#[serde(skip)]
63+
pub full_envs: Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
64+
5365
/// Current working directory
5466
pub cwd: Arc<AbsolutePath>,
5567
}

0 commit comments

Comments
 (0)