Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 2 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 40 additions & 5 deletions src/managers/builtin/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ export const SYSTEM_GLOBAL_KEY = `${ENVS_EXTENSION_ID}:system:GLOBAL_SELECTED`;

export async function clearSystemEnvCache(): Promise<void> {
const workspaceState = await getWorkspacePersistentState();
await workspaceState.clear([SYSTEM_WORKSPACE_KEY]);
// The global system-Python selection is mirrored into workspaceState (see
// getSystemEnvForGlobal/setSystemEnvForGlobal) so that the cross-session
// cache survives on fresh remotes where globalState is cold. Clear both
// the workspace-scoped map and the mirrored global key here.
await workspaceState.clear([SYSTEM_WORKSPACE_KEY, SYSTEM_GLOBAL_KEY]);
const globalState = await getGlobalPersistentState();
await globalState.clear([SYSTEM_GLOBAL_KEY]);
}
Expand Down Expand Up @@ -48,12 +52,43 @@ export async function setSystemEnvForWorkspaces(fsPath: string[], envPath: strin
await state.set(SYSTEM_WORKSPACE_KEY, data);
}

/**
* Returns the cached path of the discovered global system Python.
*
* Reads from `workspaceState` first and falls back to `globalState`. The
* workspace layer is the primary cache because, in remote scenarios
* (SSH / WSL / dev containers / codespaces), `context.globalState` is scoped
* to the remote machine and starts empty on every fresh remote. The
* workspaceState mirror survives across sessions of the same workspace
* folder on the same remote, so the foreground env-selection fast path can
* hit the cache without waiting on a full PET refresh. See PR #1455 and
* the cross-session cache acceptance criteria.
*/
export async function getSystemEnvForGlobal(): Promise<string | undefined> {
const state = await getGlobalPersistentState();
return await state.get(SYSTEM_GLOBAL_KEY);
const workspaceState = await getWorkspacePersistentState();
const workspaceValue = await workspaceState.get<string>(SYSTEM_GLOBAL_KEY);
if (workspaceValue) {
return workspaceValue;
}
const globalState = await getGlobalPersistentState();
return await globalState.get<string>(SYSTEM_GLOBAL_KEY);
}

/**
* Persists the cached path of the discovered global system Python.
*
* Writes to BOTH `workspaceState` and `globalState` so the cache survives a
* cold `globalState` on remotes (see `getSystemEnvForGlobal` for context).
* Passing `undefined` invalidates both layers, keeping stale-path cleanup
* consistent.
*/
export async function setSystemEnvForGlobal(envPath: string | undefined): Promise<void> {
const state = await getGlobalPersistentState();
await state.set(SYSTEM_GLOBAL_KEY, envPath);
const [workspaceState, globalState] = await Promise.all([
getWorkspacePersistentState(),
getGlobalPersistentState(),
]);
await Promise.all([
workspaceState.set(SYSTEM_GLOBAL_KEY, envPath),
globalState.set(SYSTEM_GLOBAL_KEY, envPath),
]);
Comment on lines +86 to +93
}
195 changes: 195 additions & 0 deletions src/test/managers/builtin/cache.systemEnvGlobal.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import assert from 'assert';
import * as sinon from 'sinon';
import * as persistentState from '../../../common/persistentState';
import {
clearSystemEnvCache,
getSystemEnvForGlobal,
setSystemEnvForGlobal,
SYSTEM_GLOBAL_KEY,
SYSTEM_WORKSPACE_KEY,
} from '../../../managers/builtin/cache';

suite('builtin cache - system global env two-tier lookup', () => {
let workspaceMock: { get: sinon.SinonStub; set: sinon.SinonStub; clear: sinon.SinonStub };
let globalMock: { get: sinon.SinonStub; set: sinon.SinonStub; clear: sinon.SinonStub };
let getWorkspaceStub: sinon.SinonStub;
let getGlobalStub: sinon.SinonStub;

setup(() => {
workspaceMock = {
get: sinon.stub(),
set: sinon.stub().resolves(),
clear: sinon.stub().resolves(),
};
globalMock = {
get: sinon.stub(),
set: sinon.stub().resolves(),
clear: sinon.stub().resolves(),
};
getWorkspaceStub = sinon
.stub(persistentState, 'getWorkspacePersistentState')
.resolves(workspaceMock as unknown as persistentState.PersistentState);
getGlobalStub = sinon
.stub(persistentState, 'getGlobalPersistentState')
.resolves(globalMock as unknown as persistentState.PersistentState);
});

teardown(() => {
sinon.restore();
});

suite('getSystemEnvForGlobal', () => {
test('returns workspaceState value when present (primary layer)', async () => {
workspaceMock.get.withArgs(SYSTEM_GLOBAL_KEY).resolves('/ws/python');
globalMock.get.withArgs(SYSTEM_GLOBAL_KEY).resolves('/global/python');

const result = await getSystemEnvForGlobal();

assert.strictEqual(result, '/ws/python', 'workspaceState should take priority');
assert.ok(workspaceMock.get.calledWith(SYSTEM_GLOBAL_KEY), 'should query workspaceState');
assert.ok(globalMock.get.notCalled, 'should not query globalState when workspace has a value');
});

test('falls back to globalState when workspaceState is empty (cold workspace mirror)', async () => {
workspaceMock.get.withArgs(SYSTEM_GLOBAL_KEY).resolves(undefined);
globalMock.get.withArgs(SYSTEM_GLOBAL_KEY).resolves('/global/python');

const result = await getSystemEnvForGlobal();

assert.strictEqual(result, '/global/python', 'should fall back to globalState');
assert.ok(workspaceMock.get.calledWith(SYSTEM_GLOBAL_KEY), 'should query workspaceState first');
assert.ok(globalMock.get.calledWith(SYSTEM_GLOBAL_KEY), 'should then query globalState');
});

test('returns undefined when both layers are empty (true cold cache)', async () => {
workspaceMock.get.withArgs(SYSTEM_GLOBAL_KEY).resolves(undefined);
globalMock.get.withArgs(SYSTEM_GLOBAL_KEY).resolves(undefined);

const result = await getSystemEnvForGlobal();

assert.strictEqual(result, undefined, 'should return undefined when nothing cached');
});

test('falls back to globalState when workspaceState value is empty string', async () => {
// An empty string is falsy and should not count as a cached selection.
workspaceMock.get.withArgs(SYSTEM_GLOBAL_KEY).resolves('');
globalMock.get.withArgs(SYSTEM_GLOBAL_KEY).resolves('/global/python');

const result = await getSystemEnvForGlobal();

assert.strictEqual(result, '/global/python');
});

test('does not interact with persistent state when called (sanity: lazy/awaited only)', async () => {
// Smoke check that the deferred-resolved helpers are awaited.
workspaceMock.get.resolves('/ws/python');
await getSystemEnvForGlobal();
assert.ok(getWorkspaceStub.called, 'workspace persistent state accessor invoked');
});
});

suite('setSystemEnvForGlobal', () => {
test('writes the same value to BOTH workspaceState and globalState', async () => {
await setSystemEnvForGlobal('/some/python');

assert.ok(
workspaceMock.set.calledWith(SYSTEM_GLOBAL_KEY, '/some/python'),
'workspaceState should receive the mirrored write',
);
assert.ok(
globalMock.set.calledWith(SYSTEM_GLOBAL_KEY, '/some/python'),
'globalState should receive the canonical write',
);
assert.ok(getWorkspaceStub.called && getGlobalStub.called, 'both persistent state accessors invoked');
});

test('invalidates BOTH layers when called with undefined (stale-path invalidation)', async () => {
await setSystemEnvForGlobal(undefined);

assert.ok(
workspaceMock.set.calledWith(SYSTEM_GLOBAL_KEY, undefined),
'workspaceState mirror should be cleared',
);
assert.ok(
globalMock.set.calledWith(SYSTEM_GLOBAL_KEY, undefined),
'globalState should be cleared',
);
});

test('round-trip: after set, get returns the value from workspaceState (primary)', async () => {
// Simulate persistent state by wiring set to update get's resolved value.
let workspaceStored: string | undefined;
let globalStored: string | undefined;
workspaceMock.set.callsFake(async (_key: string, value: string | undefined) => {
workspaceStored = value;
});
globalMock.set.callsFake(async (_key: string, value: string | undefined) => {
globalStored = value;
});
workspaceMock.get.withArgs(SYSTEM_GLOBAL_KEY).callsFake(async () => workspaceStored);
globalMock.get.withArgs(SYSTEM_GLOBAL_KEY).callsFake(async () => globalStored);

await setSystemEnvForGlobal('/round/trip/python');
const result = await getSystemEnvForGlobal();

assert.strictEqual(result, '/round/trip/python');
assert.strictEqual(workspaceStored, '/round/trip/python');
assert.strictEqual(globalStored, '/round/trip/python');
});
});

suite('clearSystemEnvCache', () => {
test('clears workspace-scoped key, workspace-mirrored global key, and globalState global key', async () => {
await clearSystemEnvCache();

assert.ok(workspaceMock.clear.calledOnce, 'workspaceState.clear should be called once');
const workspaceClearedKeys = workspaceMock.clear.firstCall.args[0] as string[];
assert.deepStrictEqual(
[...workspaceClearedKeys].sort(),
[SYSTEM_GLOBAL_KEY, SYSTEM_WORKSPACE_KEY].sort(),
'workspaceState.clear should clear both the workspace map and the mirrored global key',
);

assert.ok(globalMock.clear.calledOnce, 'globalState.clear should be called once');
assert.deepStrictEqual(
globalMock.clear.firstCall.args[0],
[SYSTEM_GLOBAL_KEY],
'globalState.clear should clear the global key',
);
});

test('after clear, get returns undefined (full invalidation through both layers)', async () => {
// Simulate the in-memory backing for both layers.
const workspaceBacking: { [key: string]: unknown } = {
[SYSTEM_GLOBAL_KEY]: '/cached/ws/python',
[SYSTEM_WORKSPACE_KEY]: { '/proj': '/cached/proj/python' },
};
const globalBacking: { [key: string]: unknown } = {
[SYSTEM_GLOBAL_KEY]: '/cached/global/python',
};
workspaceMock.get.callsFake(async (key: string) => workspaceBacking[key]);
globalMock.get.callsFake(async (key: string) => globalBacking[key]);
workspaceMock.clear.callsFake(async (keys: string[]) => {
for (const k of keys) {
delete workspaceBacking[k];
}
});
globalMock.clear.callsFake(async (keys: string[]) => {
for (const k of keys) {
delete globalBacking[k];
}
});

// Pre-condition: get sees the workspace-mirrored value.
assert.strictEqual(await getSystemEnvForGlobal(), '/cached/ws/python');

await clearSystemEnvCache();

assert.strictEqual(
await getSystemEnvForGlobal(),
undefined,
'both layers should be cleared, so global lookup is empty',
);
});
});
});