Skip to content

Commit bb795f0

Browse files
committed
feat(tests): add case-insensitive env Proxy for Windows compatibility
Implement createEnvProxy() helper that provides: 1. **Live environment in tests**: In VITEST mode, constants.processEnv is a Proxy that always reads from process.env (not a snapshot) 2. **Case-insensitive access**: Handles PATH, TEMP, HOME, etc. with any casing (PATH vs Path vs path) to match Windows behavior 3. **Smart priority**: - Overrides (spawnEnv) take precedence - Exact key matches in base env - Case-insensitive fallback for known Windows vars 4. **Proper Proxy handlers**: - get: Handles lookups with case fallback - ownKeys: Merges all keys from base + overrides - getOwnPropertyDescriptor: Returns proper descriptors - has: Case-insensitive membership checks - set: Allows setting overrides Benefits: - No more snapshot vs live env issues - Windows case-sensitivity handled properly - Works seamlessly with socket-lib@1.3.5 fix - Tests stay consistent across platforms
1 parent 28bad9e commit bb795f0

File tree

1 file changed

+152
-6
lines changed

1 file changed

+152
-6
lines changed

packages/cli/test/utils.mts

Lines changed: 152 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,152 @@ if (!process.env['VITEST']) {
2424
process.env['VITEST'] = '1'
2525
}
2626

27+
/**
28+
* Create a case-insensitive environment variable Proxy for Windows compatibility.
29+
* On Windows, environment variables are case-insensitive (PATH vs Path vs path).
30+
* This Proxy provides consistent access regardless of case, with priority given
31+
* to exact matches, then case-insensitive matches for known vars.
32+
*
33+
* @param base - Base environment object (usually process.env)
34+
* @param overrides - Optional overrides to merge
35+
* @returns Proxy that handles case-insensitive env var access
36+
*/
37+
function createEnvProxy(
38+
base: NodeJS.ProcessEnv,
39+
overrides?: Record<string, string | undefined>,
40+
): NodeJS.ProcessEnv {
41+
// Common environment variables that have case sensitivity issues on Windows.
42+
const caseInsensitiveKeys = new Set([
43+
'PATH',
44+
'TEMP',
45+
'TMP',
46+
'HOME',
47+
'USERPROFILE',
48+
'APPDATA',
49+
'LOCALAPPDATA',
50+
'PROGRAMFILES',
51+
'SYSTEMROOT',
52+
'WINDIR',
53+
])
54+
55+
return new Proxy(
56+
{},
57+
{
58+
get(_target, prop) {
59+
if (typeof prop !== 'string') {
60+
return undefined
61+
}
62+
63+
// Priority 1: Check overrides for exact match.
64+
if (overrides && prop in overrides) {
65+
return overrides[prop]
66+
}
67+
68+
// Priority 2: Check base for exact match.
69+
if (prop in base) {
70+
return base[prop]
71+
}
72+
73+
// Priority 3: Case-insensitive lookup for known keys.
74+
const upperProp = prop.toUpperCase()
75+
if (caseInsensitiveKeys.has(upperProp)) {
76+
// Check overrides with case variations.
77+
if (overrides) {
78+
for (const key of Object.keys(overrides)) {
79+
if (key.toUpperCase() === upperProp) {
80+
return overrides[key]
81+
}
82+
}
83+
}
84+
// Check base with case variations.
85+
for (const key of Object.keys(base)) {
86+
if (key.toUpperCase() === upperProp) {
87+
return base[key]
88+
}
89+
}
90+
}
91+
92+
return undefined
93+
},
94+
95+
ownKeys(_target) {
96+
const keys = new Set<string>([
97+
...Object.keys(base),
98+
...(overrides ? Object.keys(overrides) : []),
99+
])
100+
return [...keys]
101+
},
102+
103+
getOwnPropertyDescriptor(_target, prop) {
104+
if (typeof prop !== 'string') {
105+
return undefined
106+
}
107+
108+
// Use the same lookup logic as get().
109+
const value = this.get?.(_target, prop, _target)
110+
return value !== undefined
111+
? {
112+
enumerable: true,
113+
configurable: true,
114+
writable: true,
115+
value,
116+
}
117+
: undefined
118+
},
119+
120+
has(_target, prop) {
121+
if (typeof prop !== 'string') {
122+
return false
123+
}
124+
125+
// Check overrides.
126+
if (overrides && prop in overrides) {
127+
return true
128+
}
129+
130+
// Check base.
131+
if (prop in base) {
132+
return true
133+
}
134+
135+
// Case-insensitive check.
136+
const upperProp = prop.toUpperCase()
137+
if (caseInsensitiveKeys.has(upperProp)) {
138+
if (overrides) {
139+
for (const key of Object.keys(overrides)) {
140+
if (key.toUpperCase() === upperProp) {
141+
return true
142+
}
143+
}
144+
}
145+
for (const key of Object.keys(base)) {
146+
if (key.toUpperCase() === upperProp) {
147+
return true
148+
}
149+
}
150+
}
151+
152+
return false
153+
},
154+
155+
set(_target, prop, value) {
156+
if (typeof prop === 'string' && overrides) {
157+
overrides[prop] = value
158+
return true
159+
}
160+
return false
161+
},
162+
},
163+
) as NodeJS.ProcessEnv
164+
}
165+
27166
// Backward compatibility object for tests.
167+
// In VITEST mode, use a Proxy to keep env vars live and handle case-sensitivity.
28168
const constants = {
29169
execPath,
30-
processEnv: process.env,
170+
processEnv: process.env['VITEST']
171+
? createEnvProxy(process.env)
172+
: process.env,
31173
}
32174

33175
// The asciiUnsafeRegexp match characters that are:
@@ -266,13 +408,17 @@ export async function spawnSocketCli(
266408
const commandArgs = isJsFile ? [entryPath, ...args] : args
267409

268410
try {
411+
// Create a Proxy env that handles Windows case-insensitivity issues.
412+
// This ensures PATH, TEMP, and other Windows env vars work regardless
413+
// of case (PATH vs Path vs path).
414+
const env = createEnvProxy(
415+
constants.processEnv,
416+
spawnEnv as Record<string, string | undefined>,
417+
)
418+
269419
const output = await spawn(command, commandArgs, {
270420
cwd,
271-
env: {
272-
...process.env,
273-
...constants.processEnv,
274-
...spawnEnv,
275-
},
421+
env,
276422
...restOptions,
277423
// Close stdin to prevent tests from hanging
278424
// when commands wait for input. Must be after restOptions

0 commit comments

Comments
 (0)