Skip to content

Commit 3b75f16

Browse files
committed
Fix env variable caching and add testing arch for environment variable functionality
1 parent e245e59 commit 3b75f16

File tree

12 files changed

+862
-96
lines changed

12 files changed

+862
-96
lines changed

.github/instructions/testing-workflow.instructions.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,3 +574,10 @@ envConfig.inspect
574574
- Untestable Node.js APIs → Create proxy abstraction functions (use function overloads to preserve intelligent typing while making functions mockable)
575575

576576
## 🧠 Agent Learnings
577+
578+
- VS Code file watchers only monitor workspace folders, not external temp directories (1)
579+
- Use fixture-based testing with real files instead of mocking fs-extra, which has non-configurable property descriptors that prevent stubbing (1)
580+
- Extension tests (.test.ts) should use real filesystem operations; unit tests (.unit.test.ts) should mock dependencies (1)
581+
- Use `as unknown as TargetType` for type casting instead of `as any` to maintain type safety and avoid 'any' violations
582+
- If tests frequently need private access consider that maybe methods should be protected, or public test utilities should exist for testing (1)
583+
- When making systematic changes across many similar locations, fix one instance completely first to validate the approach before applying the pattern everywhere (1)

src/features/terminal/terminalEnvVarInjector.ts

Lines changed: 125 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import * as fse from 'fs-extra';
55
import * as path from 'path';
66
import {
77
Disposable,
8+
EnvironmentVariableCollection,
89
EnvironmentVariableScope,
910
GlobalEnvironmentVariableCollection,
11+
Uri,
1012
window,
1113
workspace,
1214
WorkspaceFolder,
@@ -23,6 +25,13 @@ import { EnvVarManager } from '../execution/envVariableManager';
2325
export class TerminalEnvVarInjector implements Disposable {
2426
private disposables: Disposable[] = [];
2527

28+
/**
29+
* Track which environment variables we've set per workspace to properly handle
30+
* variables that are removed/commented out in .env files.
31+
* Key: workspace fsPath, Value: Set of env var keys we've set for that workspace
32+
*/
33+
private readonly trackedEnvVars: Map<string, Set<string>> = new Map();
34+
2635
constructor(
2736
private readonly envVarCollection: GlobalEnvironmentVariableCollection,
2837
private readonly envVarManager: EnvVarManager,
@@ -134,48 +143,22 @@ export class TerminalEnvVarInjector implements Disposable {
134143
*/
135144
private async injectEnvironmentVariablesForWorkspace(workspaceFolder: WorkspaceFolder): Promise<void> {
136145
const workspaceUri = workspaceFolder.uri;
137-
try {
138-
const envVars = await this.envVarManager.getEnvironmentVariables(workspaceUri);
146+
const workspaceKey = workspaceUri.fsPath;
139147

140-
// use scoped environment variable collection
148+
try {
141149
const envVarScope = this.getEnvironmentVariableCollectionScoped({ workspaceFolder });
142150

143-
// Check if env file injection is enabled
144-
const config = getConfiguration('python', workspaceUri);
145-
const useEnvFile = config.get<boolean>('terminal.useEnvFile', false);
146-
const envFilePath = config.get<string>('envFile');
147-
148-
if (!useEnvFile) {
149-
traceVerbose(
150-
`TerminalEnvVarInjector: Env file injection disabled for workspace: ${workspaceUri.fsPath}`,
151-
);
151+
// Check if we should inject and get the env file path
152+
const shouldInject = await this.shouldInjectEnvVars(workspaceUri, envVarScope, workspaceKey);
153+
if (!shouldInject) {
152154
return;
153155
}
154156

155-
// Track which .env file is being used for logging
156-
const resolvedEnvFilePath: string | undefined = envFilePath
157-
? path.resolve(resolveVariables(envFilePath, workspaceUri))
158-
: undefined;
159-
const defaultEnvFilePath: string = path.join(workspaceUri.fsPath, '.env');
160-
161-
let activeEnvFilePath: string = resolvedEnvFilePath || defaultEnvFilePath;
162-
if (activeEnvFilePath && (await fse.pathExists(activeEnvFilePath))) {
163-
traceVerbose(`TerminalEnvVarInjector: Using env file: ${activeEnvFilePath}`);
164-
} else {
165-
traceVerbose(
166-
`TerminalEnvVarInjector: No .env file found for workspace: ${workspaceUri.fsPath}, not injecting environment variables.`,
167-
);
168-
return; // No .env file to inject
169-
}
157+
// Get environment variables from the .env file
158+
const envVars = await this.envVarManager.getEnvironmentVariables(workspaceUri);
170159

171-
for (const [key, value] of Object.entries(envVars)) {
172-
if (value === undefined) {
173-
// Remove the environment variable if the value is undefined
174-
envVarScope.delete(key);
175-
} else {
176-
envVarScope.replace(key, value);
177-
}
178-
}
160+
// Apply the environment variable changes
161+
this.applyEnvVarChanges(envVarScope, envVars, workspaceKey);
179162
} catch (error) {
180163
traceError(
181164
`TerminalEnvVarInjector: Error injecting environment variables for workspace ${workspaceUri.fsPath}:`,
@@ -184,6 +167,91 @@ export class TerminalEnvVarInjector implements Disposable {
184167
}
185168
}
186169

170+
/**
171+
* Check if environment variables should be injected for the workspace.
172+
* Returns true if injection should proceed, false otherwise.
173+
*/
174+
private async shouldInjectEnvVars(
175+
workspaceUri: Uri,
176+
envVarScope: EnvironmentVariableCollection,
177+
workspaceKey: string,
178+
): Promise<boolean> {
179+
const config = getConfiguration('python', workspaceUri);
180+
const useEnvFile = config.get<boolean>('terminal.useEnvFile', false);
181+
const envFilePath = config.get<string>('envFile');
182+
183+
if (!useEnvFile) {
184+
traceVerbose(`TerminalEnvVarInjector: Env file injection disabled for workspace: ${workspaceUri.fsPath}`);
185+
this.cleanupTrackedVars(envVarScope, workspaceKey);
186+
return false;
187+
}
188+
189+
// Determine which .env file to use
190+
const resolvedEnvFilePath: string | undefined = envFilePath
191+
? path.resolve(resolveVariables(envFilePath, workspaceUri))
192+
: undefined;
193+
const defaultEnvFilePath: string = path.join(workspaceUri.fsPath, '.env');
194+
const activeEnvFilePath: string = resolvedEnvFilePath || defaultEnvFilePath;
195+
196+
if (activeEnvFilePath && (await fse.pathExists(activeEnvFilePath))) {
197+
traceVerbose(`TerminalEnvVarInjector: Using env file: ${activeEnvFilePath}`);
198+
return true;
199+
} else {
200+
traceVerbose(
201+
`TerminalEnvVarInjector: No .env file found for workspace: ${workspaceUri.fsPath}, not injecting environment variables.`,
202+
);
203+
this.cleanupTrackedVars(envVarScope, workspaceKey);
204+
return false;
205+
}
206+
}
207+
208+
/**
209+
* Apply environment variable changes by comparing current and previous state.
210+
*/
211+
private applyEnvVarChanges(
212+
envVarScope: EnvironmentVariableCollection,
213+
envVars: { [key: string]: string | undefined },
214+
workspaceKey: string,
215+
): void {
216+
const previousKeys = this.trackedEnvVars.get(workspaceKey) || new Set<string>();
217+
const currentKeys = new Set<string>();
218+
219+
// Update/add current env vars from .env file
220+
for (const [key, value] of Object.entries(envVars)) {
221+
if (value === undefined || value === '') {
222+
// Variable explicitly unset in .env (e.g., VAR=)
223+
envVarScope.delete(key);
224+
} else {
225+
envVarScope.replace(key, value);
226+
currentKeys.add(key);
227+
}
228+
}
229+
230+
// Delete keys that were previously set but are now gone from .env
231+
for (const oldKey of previousKeys) {
232+
if (!currentKeys.has(oldKey)) {
233+
traceVerbose(
234+
`TerminalEnvVarInjector: Removing previously set env var '${oldKey}' from workspace ${workspaceKey}`,
235+
);
236+
envVarScope.delete(oldKey);
237+
}
238+
}
239+
240+
// Update our tracking for this workspace
241+
this.trackedEnvVars.set(workspaceKey, currentKeys);
242+
}
243+
244+
/**
245+
* Clean up previously tracked environment variables for a workspace.
246+
*/
247+
private cleanupTrackedVars(envVarScope: EnvironmentVariableCollection, workspaceKey: string): void {
248+
const previousKeys = this.trackedEnvVars.get(workspaceKey);
249+
if (previousKeys) {
250+
previousKeys.forEach((key) => envVarScope.delete(key));
251+
this.trackedEnvVars.delete(workspaceKey);
252+
}
253+
}
254+
187255
/**
188256
* Dispose of the injector and clean up resources.
189257
*/
@@ -192,8 +260,20 @@ export class TerminalEnvVarInjector implements Disposable {
192260
this.disposables.forEach((disposable) => disposable.dispose());
193261
this.disposables = [];
194262

195-
// Clear all environment variables from the collection
196-
this.envVarCollection.clear();
263+
// Clear only the environment variables that we've set, preserving others (e.g., BASH_ENV)
264+
for (const [workspaceKey, trackedKeys] of this.trackedEnvVars.entries()) {
265+
try {
266+
// Try to find the workspace folder for proper scoping
267+
const workspaceFolder = workspace.workspaceFolders?.find((wf) => wf.uri.fsPath === workspaceKey);
268+
if (workspaceFolder) {
269+
const scope = this.getEnvironmentVariableCollectionScoped({ workspaceFolder });
270+
trackedKeys.forEach((key) => scope.delete(key));
271+
}
272+
} catch (error) {
273+
traceError(`Failed to clean up environment variables for workspace ${workspaceKey}:`, error);
274+
}
275+
}
276+
this.trackedEnvVars.clear();
197277
}
198278

199279
private getEnvironmentVariableCollectionScoped(scope: EnvironmentVariableScope = {}) {
@@ -205,9 +285,16 @@ export class TerminalEnvVarInjector implements Disposable {
205285
* Clear all environment variables for a workspace.
206286
*/
207287
private clearWorkspaceVariables(workspaceFolder: WorkspaceFolder): void {
288+
const workspaceKey = workspaceFolder.uri.fsPath;
208289
try {
209290
const scope = this.getEnvironmentVariableCollectionScoped({ workspaceFolder });
210-
scope.clear();
291+
292+
// Only delete env vars that we've set, not ones set by other managers (e.g., BASH_ENV)
293+
const trackedKeys = this.trackedEnvVars.get(workspaceKey);
294+
if (trackedKeys) {
295+
trackedKeys.forEach((key) => scope.delete(key));
296+
this.trackedEnvVars.delete(workspaceKey);
297+
}
211298
} catch (error) {
212299
traceError(`Failed to clear environment variables for workspace ${workspaceFolder.uri.fsPath}:`, error);
213300
}

0 commit comments

Comments
 (0)