fix: preserve conda environments on transient discovery failure - #20
fix: preserve conda environments on transient discovery failure#20StellaHuang95 wants to merge 2 commits into
Conversation
|
🔒 Automated review in progress — @StellaHuang95 is auto-reviewing this PR. |
|
GitHub cannot anchor PR review comments to unchanged lines in the diff. Falling back to a general PR comment for src/managers/conda/condaUtils.ts:L865.
📍 src/managers/conda/condaUtils.ts:862 [verified] |
8d4f02e to
d7bbf65
Compare
|
Re: non-array PET output at The non-array/malformed guard now returns the Added a regression test — |
d7bbf65 to
8519a5f
Compare
8519a5f to
6f16555
Compare
|
GitHub cannot anchor PR review comments to unchanged lines in the diff. Falling back to a general PR comment for src/managers/conda/condaEnvManager.ts:L336.
A verified interleaving lets a failed refresh announce a persisted environment and then lets this successful path announce it again by reading shared [verified] |
6f16555 to
f5f5f62
Compare
|
Re: double-announce at The interleaving is real: the success path sets Fix (the second option you suggested — no serialization): the success path now announces only its own authoritative results plus what its own this.collection = refreshed;
const refreshedAdds = refreshed.map((env) => ({ kind: EnvironmentChangeKind.add, environment: env }));
const appended = await this.loadEnvMap();
const args = [
...discard.map((env) => ({ kind: EnvironmentChangeKind.remove, environment: env })),
...refreshedAdds,
...appended.map((env) => ({ kind: EnvironmentChangeKind.add, environment: env })),
];
this._onDidChangeEnvironments.fire(args);
Regression added — This was a general PR comment (GitHub could not anchor it to an unchanged line), so it has no review thread to resolve; posting this as the disposition. |
f5f5f62 to
ab5e67a
Compare
|
GitHub cannot anchor PR review comments to unchanged lines in the diff. Falling back to a general PR comment for src/managers/conda/condaEnvManager.ts:L343.
📍 src/managers/conda/condaEnvManager.ts:322 [verified] |
ab5e67a to
cf02f70
Compare
cf02f70 to
2bf9603
Compare
|
Re: reverse mixed-outcome race at
Deterministic regression added — |
refreshCondaEnvs now returns undefined when the native finder throws/rejects or returns a non-array/malformed value, distinct from an authoritative successful empty array. CondaEnvManager guards its initialize, refresh, and background-get discovery paths so a transient failure preserves the known-good collection and emits no removals, while a successful empty result still removes stale environments normally. Persisted global/workspace selections are restored via loadEnvMap on the failure path; loadEnvMap returns the environments it appended so only those are announced, it re-checks membership by exact path after resolution so overlapping failed refreshes cannot double-append, and the failure path revalidates each appended environment against the current collection before announcing so a concurrent successful refresh that replaced the collection cannot produce a stale add. The successful discovery paths announce only their authoritative refresh results plus the environments their own loadEnvMap invocation appended, so an environment appended and announced by a concurrent failed refresh is not announced a second time. A successful refresh reconciles its add/remove events by normalized path, and treats a same-path environment as continuous only when its observable metadata is unchanged, so a path present before and after the refresh is neither removed nor re-added when nothing consumers observe changed, preventing add/remove/add churn and a transient disappearance when a delayed successful refresh captures a path a prior failed refresh already announced, while a same-path environment whose observable metadata changed still emits an exact remove of the old followed by an add of the new. When initialization''s own discovery fails, it resolves current waiters without throwing and then clears the initialization state only if that failed attempt still owns it, so the next ordinary get retries discovery and a later successful attempt stays initialized. A background initialization triggered by the fast path likewise propagates a failed transient discovery as a rejected outcome so the initialization state is reset rather than marked permanently complete, letting the next get or initialize retry. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
2bf9603 to
c3b0011
Compare
…ation The same-path continuity check used by a successful conda refresh only compared a subset of fields (name, displayName, version, description, sysPrefix, error, run.executable), so a same-path environment whose other consumer-visible metadata changed (shortDisplayName, displayPath, tooltip, iconPath, group, or any execInfo activation/deactivation command or shell map) updated the collection without emitting a remove/add, leaving consumers stale. Reconciliation now compares every public PythonEnvironmentInfo field except environmentPath (already matched by normalized path) and the manager-generated random envId.id, using typed structural equality for arrays, Maps, and Uri/MarkdownString/ThemeIcon values. Truly equivalent same-path resolutions (differing only by the random id) still suppress churn, preserving the reverse-race guarantee, while any observable metadata change emits an exact remove of the old followed by an add of the new. The empty-payload fire on a no-op refresh is retained to stay consistent with the sibling venv/poetry/pyenv managers, which all fire unconditionally. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Context
Conda environment discovery runs the native finder (PET). When that refresh throws/rejects — a
transient failure —
refreshCondaEnvspreviously returned[], the same value it returns for agenuine "no conda environments" result.
CondaEnvManagercould not tell the two apart, so atransient failure replaced the known-good collection with
[]and emitted spuriousremoveevents, making previously-discovered environments disappear from the UI until the next successful
refresh.
Root cause
refreshCondaEnvscollapsed distinct outcomes into[]:Every manager discovery path (
initialize,refresh, backgroundget) then assignedthis.collection = resultand emitted add/remove events, so a failure wiped the collection.Fix
refreshCondaEnvsnow returnsPythonEnvironment[] | undefined:undefined= discovery failed (native finder threw/rejected, or returned a non-array value).[]) = authoritative success;[]means genuinely no environments.undefined:loadEnvMap()so persisted global/workspace selections are restored (
loadEnvMapPreservingCollection).loadEnvMap()returns the exact environments it appended, and only those are announced asadds — so an overlapping successful refresh that replaces the collection cannot be
double-announced.
[]): replace the collection and emit the normal add/remove events.Legitimate deletion is unchanged: a successful
[]is authoritative and removes stale environmentsexactly as before. Only a
undefinedfailure is treated as "preserve".Tests
condaUtils.refreshCondaEnvs.unit.test.ts: the utility returnsundefinedon a rejected refreshand on a non-array/malformed result, and
[]on a successful empty discovery.condaEnvManager.resultPreservation.unit.test.ts:refresh/initializepreserves the known-good collection and emits no changes whennothing persisted resolves;
[]empties the collection and emits removals; a successful non-empty resultreplaces the collection and emits removals + adds;
addition and never a removal, and retains it across
getcalls;loadEnvMap()paused, a concurrent refresh that replaces the collection announces onlythe environments this resolution appended (no duplicate add).