Skip to content

fix: formalize refresh locator state#388

Merged
karthiknadig merged 1 commit intofeature/issue-383from
bug/issue-387
Apr 4, 2026
Merged

fix: formalize refresh locator state#388
karthiknadig merged 1 commit intofeature/issue-383from
bug/issue-387

Conversation

@karthiknadig
Copy link
Copy Markdown
Member

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.

  • add RefreshStatePersistence and scope-aware sync_refresh_state_from() to the core Locator contract
  • classify config-only, self-hydrating, and synchronized locators across Conda, Poetry, PyEnv, PipEnv, Uv, Linux global Python, Windows Registry, and Windows Store
  • make refresh-state handoff generic in jsonrpc.rs, including partial-refresh handling and workspace-scope Poetry merges
  • align config-carrying locators with replace semantics so cleared configure inputs do not leave stale executables or workspace directories behind
  • add unit coverage for Conda partial-refresh behavior, Poetry workspace merges and configure clearing, PyEnv self-hydration, and Windows Registry/Store sync semantics

Fixes #387

@karthiknadig karthiknadig requested a review from Copilot April 4, 2026 07:35
@karthiknadig karthiknadig marked this pull request as ready for review April 4, 2026 07:37
@karthiknadig karthiknadig merged commit a27f4ea into feature/issue-383 Apr 4, 2026
6 checks passed
@karthiknadig karthiknadig deleted the bug/issue-387 branch April 4, 2026 07:37
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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’s Locator trait with RefreshStatePersistence + scope-aware sync_refresh_state_from() to define post-refresh state rules explicitly.
  • Replace the hard-coded Conda/Poetry refresh-state handoff in jsonrpc.rs with 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

Comment on lines +523 to +541
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"
);
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants