fix: formalize refresh locator state#388
Conversation
There was a problem hiding this comment.
Pull request overview
This PR formalizes how locator state is allowed to persist across JSONRPC refresh boundaries by extending the core Locator contract with explicit refresh-state persistence and synchronization semantics, then applying that contract generically when copying state from transient refresh locators back into the long-lived shared locator graph.
Changes:
- Extend
pet-core’sLocatortrait withRefreshStatePersistence+ scope-awaresync_refresh_state_from()to define post-refresh state rules explicitly. - Replace the hard-coded Conda/Poetry refresh-state handoff in
jsonrpc.rswith a generic per-locator sync pass, including workspace vs global filtered scopes. - Classify and update multiple locators (Conda/Poetry/PyEnv/PipEnv/Uv/LinuxGlobal/Windows Registry/Windows Store) to reflect configured-only, self-hydrating, or synced discovery state; add unit tests for key sync semantics.
Show a summary per file
| File | Description |
|---|---|
| crates/pet/src/jsonrpc.rs | Introduces generic refresh-state synchronization across locator graphs and adds tests for partial-refresh Conda behavior. |
| crates/pet-core/src/lib.rs | Adds RefreshStatePersistence, RefreshStateSyncScope, and sync_refresh_state_from() to the Locator contract (plus downcasting support). |
| crates/pet-conda/src/lib.rs | Implements synced discovery-state classification and scope-aware refresh-state syncing; aligns configure semantics with replacement. |
| crates/pet-poetry/src/lib.rs | Implements synced discovery-state + workspace merge behavior; adjusts configure/clear semantics and adds tests for merges and configure clearing. |
| crates/pet-pipenv/src/lib.rs | Marks locator as configured-only and updates configure to clear stale configured executable when unset. |
| crates/pet-uv/src/lib.rs | Marks locator as configured-only and updates configure to clear stale workspace directories when unset. |
| crates/pet-pyenv/src/lib.rs | Marks locator as self-hydrating cache (no refresh sync required). |
| crates/pet-pyenv/tests/pyenv_test.rs | Adds unit coverage asserting PyEnv can self-hydrate without refresh-state sync. |
| crates/pet-linux-global-python/src/lib.rs | Marks locator as self-hydrating cache. |
| crates/pet-windows-store/src/lib.rs | Marks locator as synced discovery-state; implements scope-aware refresh sync and adds tests. |
| crates/pet-windows-registry/src/lib.rs | Marks locator as synced discovery-state; implements scope-aware refresh sync and adds tests. |
Copilot's findings
- Files reviewed: 11/11 changed files
- Comments generated: 1
| fn sync_refresh_locator_state( | ||
| target_locators: &[Arc<dyn Locator>], | ||
| source_locators: &[Arc<dyn Locator>], | ||
| search_scope: Option<&SearchScope>, | ||
| ) { | ||
| let sync_scope = refresh_state_sync_scope(search_scope); | ||
|
|
||
| assert_eq!( | ||
| target_locators.len(), | ||
| source_locators.len(), | ||
| "refresh locator graphs drifted" | ||
| ); | ||
|
|
||
| for (target, source) in target_locators.iter().zip(source_locators.iter()) { | ||
| assert_eq!( | ||
| target.get_kind(), | ||
| source.get_kind(), | ||
| "refresh locator order drifted" | ||
| ); |
There was a problem hiding this comment.
sync_refresh_locator_state() uses assert_eq! to enforce locator graph length/order. If this ever drifts in a future change (e.g., platform-conditional locators or reordered create_locators()), the refresh thread will panic and the JSONRPC refresh request will fail (caught by catch_unwind, but still returns an error to all waiters). Consider handling mismatches without panicking (e.g., build a kind→locator map and sync by LocatorKind, or log an error and skip syncing) and/or downgrading these to debug_assert! so production refreshes don’t hard-fail on contract drift.
See below for a potential fix:
debug_assert_eq!(
target_locators.len(),
source_locators.len(),
"refresh locator graphs drifted"
);
if target_locators.len() != source_locators.len() {
warn!(
"Refresh locator graphs drifted: target has {} locators, source has {} locators",
target_locators.len(),
source_locators.len()
);
}
for target in target_locators {
let target_kind = target.get_kind();
let source = source_locators
.iter()
.find(|source| source.get_kind() == target_kind);
debug_assert!(
source.is_some(),
"refresh locator kind missing from source graph: {:?}",
target_kind
);
let Some(source) = source else {
warn!(
"Skipping refresh state sync for locator {:?}: no matching source locator",
target_kind
);
continue;
};
if !matches!(target.refresh_state(), RefreshStatePersistence::Stateless) {
trace!(
"Applying refresh state contract for locator {:?}: {:?}",
target_kind,
This follow-up to #383 turns the implicit post-refresh state rules into an explicit locator contract so transient refresh graphs do not silently regress later resolve, find, or telemetry behavior.
RefreshStatePersistenceand scope-awaresync_refresh_state_from()to the coreLocatorcontractjsonrpc.rs, including partial-refresh handling and workspace-scope Poetry mergesFixes #387