Skip to content

Commit 2e6ce79

Browse files
committed
Address follow-up persistence review
Publish same-path version rebuilds, emit completed multi-manager unset groups consistently, and preserve cold associations across transient rehydration failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1a9f6ba1-9bd3-4664-bc25-a0d34d7a2e91
1 parent 1573f35 commit 2e6ce79

4 files changed

Lines changed: 142 additions & 22 deletions

File tree

src/features/envManagers.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,6 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
522522
});
523523
}
524524
} else {
525-
const events: DidChangeEnvironmentEventArgs[] = [];
526525
if (Array.isArray(scope) && scope.every((s) => s instanceof Uri)) {
527526
const groupedScopes = new Map<InternalEnvironmentManager, Uri[]>();
528527
scope.forEach((uri) => {
@@ -532,6 +531,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
532531
}
533532
});
534533
for (const [manager, uris] of groupedScopes) {
534+
const events: DidChangeEnvironmentEventArgs[] = [];
535535
const selections = uris.map((uri) => this.beginPendingSelection(uri, manager));
536536
await manager.set(uris);
537537
await Promise.all(
@@ -551,8 +551,10 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
551551
}
552552
}),
553553
);
554+
await this.fireActiveEnvironmentEvents(events);
554555
}
555556
} else if (typeof scope === 'string' && scope === 'global') {
557+
const events: DidChangeEnvironmentEventArgs[] = [];
556558
const manager = this.getEnvironmentManager(undefined);
557559
if (manager) {
558560
const operation = this.beginSelectionOperation('global');
@@ -566,18 +568,7 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
566568
}
567569
}
568570
}
569-
}
570-
if (events.length > 0) {
571-
await new Promise<void>((resolve, reject) => {
572-
setImmediate(() => {
573-
try {
574-
events.forEach((e) => this._onDidChangeActiveEnvironment.fire(e));
575-
resolve();
576-
} catch (err) {
577-
reject(err);
578-
}
579-
});
580-
});
571+
await this.fireActiveEnvironmentEvents(events);
581572
}
582573
}
583574
}
@@ -768,10 +759,27 @@ export class PythonEnvironmentManagers implements EnvironmentManagers {
768759
return false;
769760
}
770761
return first.envId.managerId === INLINE_SCRIPT_MANAGER_ID
771-
? normalizePath(first.environmentPath.fsPath) === normalizePath(second.environmentPath.fsPath)
762+
? normalizePath(first.environmentPath.fsPath) === normalizePath(second.environmentPath.fsPath) &&
763+
first.version === second.version
772764
: first.envId.id === second.envId.id;
773765
}
774766

767+
private async fireActiveEnvironmentEvents(events: readonly DidChangeEnvironmentEventArgs[]): Promise<void> {
768+
if (events.length === 0) {
769+
return;
770+
}
771+
await new Promise<void>((resolve, reject) => {
772+
setImmediate(() => {
773+
try {
774+
events.forEach((event) => this._onDidChangeActiveEnvironment.fire(event));
775+
resolve();
776+
} catch (error) {
777+
reject(error);
778+
}
779+
});
780+
});
781+
}
782+
775783
getProjectEnvManagers(uris: Uri[]): InternalEnvironmentManager[] {
776784
const projectEnvManagers: InternalEnvironmentManager[] = [];
777785
uris.forEach((uri) => {

src/managers/builtin/inlineScript/envManager.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -570,13 +570,21 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
570570
return undefined;
571571
}
572572

573-
const resolved = await resolveVenvPythonEnvironmentPath(
574-
environmentPath,
575-
this.nativeFinder,
576-
this.api,
577-
this,
578-
this.baseManager,
579-
);
573+
let resolved: PythonEnvironment | undefined;
574+
try {
575+
resolved = await resolveVenvPythonEnvironmentPath(
576+
environmentPath,
577+
this.nativeFinder,
578+
this.api,
579+
this,
580+
this.baseManager,
581+
);
582+
} catch (error) {
583+
this.log.warn(
584+
`Unable to resolve persisted inline-script environment ${environmentPath}: ${getErrorMessage(error)}`,
585+
);
586+
return undefined;
587+
}
580588
if (!resolved) {
581589
// PET/API resolution can fail transiently. Keep the association for a later retry.
582590
return undefined;
@@ -585,7 +593,15 @@ export class InlineScriptEnvManager implements EnvironmentManager, Disposable {
585593
if (!this.isCurrentAssociationRevision(scriptPath, revision)) {
586594
return this.fsPathToEnv.get(scriptPath);
587595
}
588-
const ownership = await this.inspectAssociationOwnership(resolved);
596+
let ownership: CacheEnvironmentInspection;
597+
try {
598+
ownership = await this.inspectAssociationOwnership(resolved);
599+
} catch (error) {
600+
this.log.warn(
601+
`Unable to inspect persisted inline-script environment ${environmentPath}: ${getErrorMessage(error)}`,
602+
);
603+
return undefined;
604+
}
589605
if (ownership === 'stale') {
590606
await this.removeStalePersistedAssociation(scriptPath, environmentPath, revision, scriptUri);
591607
return undefined;

src/test/features/envManagers.lastKnown.unit.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,68 @@ suite('PythonEnvironmentManagers getLastKnownEnvironment', () => {
301301
assert.deepStrictEqual(events.map((event) => event.new), [first, second]);
302302
});
303303

304+
test('publishes same-path inline rebuilds but ignores generated-ID-only changes', async () => {
305+
const scope = Uri.file('/workspace/script.py');
306+
const managerId = registerManager(async () => undefined, async () => undefined, 'inline-script');
307+
const environmentPath = Uri.file('/env/inline/python');
308+
const first = {
309+
...makeEnv('first'),
310+
envId: { id: 'first', managerId },
311+
environmentPath,
312+
version: '3.12.0',
313+
};
314+
const regenerated = {
315+
...first,
316+
envId: { id: 'regenerated', managerId },
317+
};
318+
const rebuilt = {
319+
...regenerated,
320+
envId: { id: 'rebuilt', managerId },
321+
version: '3.13.0',
322+
};
323+
const events: DidChangeEnvironmentEventArgs[] = [];
324+
envManagers.onDidChangeActiveEnvironment((event) => events.push(event));
325+
326+
await envManagers.setEnvironment(scope, first, false);
327+
await envManagers.setEnvironment(scope, regenerated, false);
328+
await envManagers.setEnvironment(scope, rebuilt, false);
329+
330+
assert.strictEqual(envManagers.getLastKnownEnvironment(scope), rebuilt);
331+
assert.deepStrictEqual(events.map((event) => event.new), [first, rebuilt]);
332+
});
333+
334+
test('publishes completed manager groups before a later group rejects', async () => {
335+
const firstScope = Uri.file('/workspace/first.py');
336+
const secondScope = Uri.file('/workspace/second.py');
337+
const firstProject = { name: 'first.py', uri: firstScope };
338+
const secondProject = { name: 'second.py', uri: secondScope };
339+
projectsByUri.set(firstScope.toString(), firstProject);
340+
projectsByUri.set(secondScope.toString(), secondProject);
341+
const firstSet = sinon.stub().resolves();
342+
const firstId = registerManager(async () => undefined, firstSet, 'first-manager');
343+
const secondSet = sinon.stub();
344+
secondSet.onFirstCall().resolves();
345+
secondSet.onSecondCall().rejects(new Error('second group rejected'));
346+
const secondId = registerManager(async () => undefined, secondSet, 'second-manager');
347+
const firstEnvironment = { ...makeEnv('first'), envId: { id: 'first', managerId: firstId } };
348+
const secondEnvironment = { ...makeEnv('second'), envId: { id: 'second', managerId: secondId } };
349+
await envManagers.setEnvironment(firstScope, firstEnvironment, false);
350+
await envManagers.setEnvironment(secondScope, secondEnvironment, false);
351+
exactManagerSettings.set(firstScope.toString(), firstId);
352+
exactManagerSettings.set(secondScope.toString(), secondId);
353+
const events: DidChangeEnvironmentEventArgs[] = [];
354+
envManagers.onDidChangeActiveEnvironment((event) => events.push(event));
355+
356+
await assert.rejects(
357+
envManagers.setEnvironments([firstScope, secondScope], undefined, false),
358+
/second group rejected/,
359+
);
360+
361+
assert.strictEqual(envManagers.getLastKnownEnvironment(firstScope), undefined);
362+
assert.strictEqual(envManagers.getLastKnownEnvironment(secondScope), secondEnvironment);
363+
assert.deepStrictEqual(events, [{ uri: firstScope, old: firstEnvironment, new: undefined }]);
364+
});
365+
304366
test('tracks inline-script selections independently for scripts in the same project', async () => {
305367
const firstUri = Uri.file('/workspace/first.py');
306368
const secondUri = Uri.file('/workspace/second.py');

src/test/managers/builtin/inlineScript/envManager.unit.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1573,6 +1573,40 @@ suite('InlineScriptEnvManager', () => {
15731573
restarted.dispose();
15741574
});
15751575

1576+
test('preserves and retries a cold association when resolution rejects', async () => {
1577+
const uri = scriptUri();
1578+
const environment = await createOwnedEnvironment();
1579+
persistedAssociations = { [normalizePath(uri.fsPath)]: environment.environmentPath.fsPath };
1580+
resolveVenvStub.onFirstCall().rejects(new Error('resolver unavailable'));
1581+
resolveVenvStub.onSecondCall().resolves(environment);
1582+
1583+
assert.strictEqual(await manager.get(uri), undefined);
1584+
assert.deepStrictEqual(persistedAssociations, {
1585+
[normalizePath(uri.fsPath)]: environment.environmentPath.fsPath,
1586+
});
1587+
assert.strictEqual(await manager.get(uri), environment);
1588+
});
1589+
1590+
test('preserves and retries a cold association when ownership inspection rejects', async () => {
1591+
const uri = scriptUri();
1592+
const environment = await createOwnedEnvironment();
1593+
persistedAssociations = { [normalizePath(uri.fsPath)]: environment.environmentPath.fsPath };
1594+
resolveVenvStub.resolves(environment);
1595+
const inspectionManager = manager as unknown as {
1596+
inspectAssociationOwnership(
1597+
candidate: PythonEnvironment,
1598+
): Promise<'expected' | 'stale' | 'uncertain'>;
1599+
};
1600+
const ownershipStub = sinon.stub(inspectionManager, 'inspectAssociationOwnership').callThrough();
1601+
ownershipStub.onFirstCall().rejects(new Error('filesystem unavailable'));
1602+
1603+
assert.strictEqual(await manager.get(uri), undefined);
1604+
assert.deepStrictEqual(persistedAssociations, {
1605+
[normalizePath(uri.fsPath)]: environment.environmentPath.fsPath,
1606+
});
1607+
assert.strictEqual(await manager.get(uri), environment);
1608+
});
1609+
15761610
test('notifies when a slow persisted association finishes rehydrating', async () => {
15771611
const uri = scriptUri();
15781612
const environment = await createOwnedEnvironment();

0 commit comments

Comments
 (0)