Skip to content

Commit e9111bd

Browse files
committed
fix(plan): resolve runner getEnv from spawn env context
Motivation: Runner-served getEnv values need to match the command context that planning used, including prefix envs from the command itself and enclosing nested vp run expansions. Reading only the filtered child env loses undeclared outer-prefix values. Scope: Add SpawnCommand::full_envs as an execution-only field, thread the planning env context into each spawn, and serve getEnv from that full context. This PR does not add cache fingerprinting for getEnv reads. Verification: - UPDATE_SNAPSHOTS=1 cargo test -p vite_task_bin --test e2e_snapshots fetch_env_sees_command_prefix_env -- --ignored - UPDATE_SNAPSHOTS=1 cargo test -p vite_task_bin --test e2e_snapshots fetch_env_sees_intermediate_prefix_envs -- --ignored - cargo test -p vite_task_bin --test e2e_snapshots fetch_env_sees_command_prefix_env -- --ignored - cargo test -p vite_task_bin --test e2e_snapshots fetch_env_sees_intermediate_prefix_envs -- --ignored - cargo test -p vite_task_plan plan_spawn_execution
1 parent 1174ec4 commit e9111bd

10 files changed

Lines changed: 113 additions & 10 deletions

File tree

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ impl<'a> ExecutionMode<'a> {
157157
cache_metadata: Option<&'a CacheMetadata>,
158158
stdio_config: StdioConfig,
159159
globbed_inputs: BTreeMap<RelativePathBuf, u64>,
160-
envs: &BTreeMap<Arc<OsStr>, Arc<OsStr>>,
160+
envs: &Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
161161
) -> Result<Self, ExecutionError> {
162162
let Some(metadata) = cache_metadata else {
163163
return Ok(Self::Uncached {
@@ -184,10 +184,8 @@ impl<'a> ExecutionMode<'a> {
184184
// Bind runner IPC for every cached task. The merged cache-control API
185185
// (`disableCache`) must work even when a task uses explicit inputs and
186186
// therefore does not need fspy auto-input inference.
187-
let ipc_env_map: FxHashMap<Arc<OsStr>, Arc<OsStr>> =
188-
envs.iter().map(|(name, value)| (Arc::clone(name), Arc::clone(value))).collect();
189187
let (ipc_envs, ServerHandle { driver, stop_accepting }) =
190-
serve(Recorder::new(Arc::new(ipc_env_map))).map_err(ExecutionError::IpcServerBind)?;
188+
serve(Recorder::new(Arc::clone(envs))).map_err(ExecutionError::IpcServerBind)?;
191189
let tracking =
192190
Tracking { fspy, ipc_envs: ipc_envs.collect(), ipc_server_fut: driver, stop_accepting };
193191

@@ -413,7 +411,7 @@ async fn run(
413411
cache_metadata,
414412
stdio_config,
415413
globbed_inputs,
416-
&spawn_execution.spawn_command.all_envs,
414+
&spawn_execution.spawn_command.full_envs,
417415
)
418416
.map_err(Report::failed)?;
419417

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::default(),
484485
cwd: test_path(),
485486
},
486487
})),
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { getEnv } from '@voidzero-dev/vite-task-client';
2+
3+
const servedA = getEnv('PREFIXED_A') ?? '(unset)';
4+
const servedB = getEnv('PREFIXED_B') ?? '(unset)';
5+
const ownA = process.env.PREFIXED_A ?? '(unset)';
6+
const ownB = process.env.PREFIXED_B ?? '(unset)';
7+
8+
console.log(`served A=${servedA} B=${servedB}`);
9+
console.log(`process.env A=${ownA} B=${ownB}`);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getEnv } from '@voidzero-dev/vite-task-client';
2+
3+
const served = getEnv('PREFIXED_ENV') ?? '(unset)';
4+
const own = process.env.PREFIXED_ENV ?? '(unset)';
5+
6+
console.log(`served=${served}`);
7+
console.log(`process.env=${own}`);

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,36 @@ steps = [
6969
],
7070
], comment = "runner serves PROBE_ENV from the spawned task env map" },
7171
]
72+
73+
[[e2e]]
74+
name = "fetch_env_sees_command_prefix_env"
75+
comment = """
76+
A command-prefixed env (`PREFIXED_ENV=from-command node ...`) is part of the
77+
spawn's full env context. `getEnv('PREFIXED_ENV')` must serve the same value
78+
the child process sees in `process.env`.
79+
"""
80+
ignore = true
81+
steps = [
82+
{ argv = [
83+
"vt",
84+
"run",
85+
"fetch-prefixed-env",
86+
], comment = "tool asks the runner for an env the command prefix sets" },
87+
]
88+
89+
[[e2e]]
90+
name = "fetch_env_sees_intermediate_prefix_envs"
91+
comment = """
92+
Prefix envs accumulate through nested runs: `outer-prefixed` is
93+
`PREFIXED_A=a vt run inner-prefixed`, and `inner-prefixed` is
94+
`PREFIXED_B=b node ...`. The inner tool's `getEnv` must see both, even though
95+
only the inner prefix reaches the child process env after cache env filtering.
96+
"""
97+
ignore = true
98+
steps = [
99+
{ argv = [
100+
"vt",
101+
"run",
102+
"outer-prefixed",
103+
], comment = "outer prefix env reaches the inner tool via the runner" },
104+
]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# fetch_env_sees_command_prefix_env
2+
3+
A command-prefixed env (`PREFIXED_ENV=from-command node ...`) is part of the spawn's full env context. `getEnv('PREFIXED_ENV')` must serve the same value the child process sees in `process.env`.
4+
5+
## `vt run fetch-prefixed-env`
6+
7+
tool asks the runner for an env the command prefix sets
8+
9+
```
10+
$ PREFIXED_ENV=from-command node scripts/fetch_prefixed_env.mjs
11+
served=from-command
12+
process.env=from-command
13+
```
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# fetch_env_sees_intermediate_prefix_envs
2+
3+
Prefix envs accumulate through nested runs: `outer-prefixed` is `PREFIXED_A=a vt run inner-prefixed`, and `inner-prefixed` is `PREFIXED_B=b node ...`. The inner tool's `getEnv` must see both, even though only the inner prefix reaches the child process env after cache env filtering.
4+
5+
## `vt run outer-prefixed`
6+
7+
outer prefix env reaches the inner tool via the runner
8+
9+
```
10+
$ PREFIXED_B=b node scripts/fetch_intermediate_envs.mjs
11+
served A=a B=b
12+
process.env A=(unset) B=b
13+
```

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
@@ -13,6 +13,18 @@
1313
"command": "node scripts/fetch_env.mjs",
1414
"env": ["PROBE_ENV"],
1515
"cache": true
16+
},
17+
"fetch-prefixed-env": {
18+
"command": "PREFIXED_ENV=from-command node scripts/fetch_prefixed_env.mjs",
19+
"cache": true
20+
},
21+
"outer-prefixed": {
22+
"command": "PREFIXED_A=a vt run inner-prefixed",
23+
"cache": true
24+
},
25+
"inner-prefixed": {
26+
"command": "PREFIXED_B=b node scripts/fetch_intermediate_envs.mjs",
27+
"cache": true
1628
}
1729
}
1830
}

crates/vite_task_plan/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ 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 prefix envs from this command and enclosing nested `vp run`
55+
/// expansions on top of the session envs. Runner-aware `getEnv` resolves
56+
/// against this map.
57+
///
58+
/// This is not passed to the child process; that remains
59+
/// [`all_envs`](Self::all_envs). It is skipped in serialized plans because
60+
/// it mirrors the ambient environment and would make snapshots noisy.
61+
#[serde(skip)]
62+
pub full_envs: Arc<FxHashMap<Arc<OsStr>, Arc<OsStr>>>,
63+
5364
/// Current working directory
5465
pub cwd: Arc<AbsolutePath>,
5566
}

crates/vite_task_plan/src/plan.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,11 @@ async fn plan_task_as_execution_node(
143143

144144
// This command's env context: extend the planning context with
145145
// the command's prefix envs (`FOO=1 tool ...`). Everything that
146-
// interprets the command reads this one context program
147-
// lookup, plan-request callbacks, and nested-run planning. The
148-
// per-and-item duplicate above scopes the extension to this
149-
// command, matching shell semantics (`FOO=1 a && b` does not
150-
// set `FOO` for `b`).
146+
// interprets the command reads this one context: program
147+
// lookup, plan-request callbacks, nested-run planning, and the
148+
// spawned process's `full_envs`. The per-and-item duplicate
149+
// above scopes the extension to this command, matching shell
150+
// semantics (`FOO=1 a && b` does not set `FOO` for `b`).
151151
context.add_envs(and_item.envs.iter());
152152

153153
let mut args = and_item.args;
@@ -677,6 +677,11 @@ fn plan_spawn_execution(
677677
}
678678
}
679679

680+
// The runner-side full env context is the command's env context before
681+
// child-env filtering. The caller already extended it with this command's
682+
// prefix envs.
683+
let full_envs = Arc::clone(envs);
684+
680685
// Add prefix envs to all envs
681686
all_envs.extend(prefix_envs.iter().map(|(name, value)| {
682687
(OsStr::new(name.as_str()).into(), OsStr::new(value.as_str()).into())
@@ -688,6 +693,7 @@ fn plan_spawn_execution(
688693
args: Arc::clone(&args),
689694
cwd,
690695
all_envs: Arc::new(all_envs.into_iter().collect()),
696+
full_envs,
691697
},
692698
cache_metadata: resolved_cache_metadata,
693699
})

0 commit comments

Comments
 (0)