feat: use cached ALCops analyzers immediately and refresh from NuGet in the background - #29
Merged
Merged
Conversation
…Get in the background When a valid cached version exists for the target TFM, Ready completes from it without any network I/O. A background task then checks NuGet for a newer version; if found, it is downloaded and cached for the next start. On a cold cache (first run) or with a pinned version, the behaviour is unchanged: fetch-then-cache. Other changes in this commit: - FindNewestCachedVersion accepts includePrerelease: Latest mode skips cached prereleases; Prerelease mode uses the highest cached version of any kind. FallbackToCacheOrWarn keeps includePrerelease: true so a cached prerelease with a warning beats nothing when offline. - SweepStaleTempDirectories uses BcToolsLocator.SafeEnumerateDirectories (now internal) instead of duplicating the safe-enumerate pattern. - The lock probe in SweepStaleTempDirectories catches UnauthorizedAccessException alongside IOException. - AcquireInUseLock documents the FileShare.None cross-process lock assumption (flock on Unix, reliable on local FS, advisory on NFS). - AlcopsAnalyzerProvisionerStartup documents that StopAsync drains the background refresh through ProvisionAsync. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GetConfigAsync and BuildAlMcpArgs now return the same memoized WorkspaceStartupConfig instance instead of re-walking settings.json, re-resolving every analyzer spec and re-reading PE metadata on each call. With _provisioner == null (the public constructor tests use) the task completes synchronously, so existing tests are unaffected. The sync BuildAlMcpArgs now throws InvalidOperationException when a provisioner is attached and not yet ready; no production caller does that (AlMcpProxy.StartCoreAsync uses BuildAlMcpArgsAsync). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Update README.md and AGENTS.md to reflect the cache-first provisioning strategy: later starts use the newest cached version immediately and check NuGet in the background; a freshly published version is first used on the second start. Document the BackgroundRefresh task, the .in-use lock assumption, TimeoutException fallback behaviour, and SemVer 2 version ordering. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…release offline fallback - Wrap SweepStaleTempDirectories body in outer try/catch so lazy enumeration failures (concurrent deletion, permission change, AV lock) cannot set Ready to null; the per-directory catch is kept so one bad directory does not abort the rest - Add doc comment on FallbackToCacheOrWarn explaining the deliberate prerelease inclusion; log a specific "no stable version cached" warning when the fallback is a prerelease under Latest mode Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rerelease test accurately - Restore per-TFM-folder try/catch in SweepStaleTempDirectories so a failure enumerating one folder (e.g. unreadable) does not abort the sweep for remaining folders. Three-level error isolation: method-level (cache root enumeration), per-TFM-folder (*.tmp-* enumeration), and per-directory (probe/delete). Extend the sweep test to verify a stale dir in a separate TFM folder is still cleaned when another folder fails. - Rename Fallback_Latest_StableAndPrereleaseCached_PrefersStable to Provision_Latest_StableAndPrereleaseCached_FastPathUsesStable_NoLastResortWarning: the test exercises the cache-first fast path, not the FallbackToCacheOrWarn code path. Add Assert.Empty(handler.RequestUrls) to make the fast-path claim explicit. - Add Fallback_Prerelease_PicksHighestCachedAcrossStableAndPrerelease: a real fallback-ordering test that reaches FallbackToCacheOrWarn via a Pinned mode with an unresolvable version, proving the last-resort behaviour picks the highest cached version (1.3.0-preview.1 > 1.2.0 in SemVer 2) and logs a "using cached version" warning. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Readycompletes immediately from cache andalmcplaunches without waiting on NuGet. A background task checks NuGet for a newer version; if found, it is downloaded and cached for the next start.WorkspaceStartupResolver.GetConfigAsync()is now memoized viaLazy<Task<WorkspaceStartupConfig>>, so the settings.json walk, analyzer resolution and PE metadata reads happen once instead of on every call.What changed
Cache-first provisioning (
AlcopsAnalyzerProvisioner)ProvisionCoreAsyncchecksFindNewestCachedVersionbefore any network call forLatest/Prereleasemodes.FetchAsync/FetchOrFallbackAsync/RefreshCacheAsyncsplit the old monolithic resolve-download block into composable pieces.RefreshCacheAsyncnever throws; it catches cancellation and exceptions, logging a warning on failure.ProvisionAsyncawaitsBackgroundRefreshafter settingReady, soAlcopsAnalyzerProvisionerStartup.StopAsyncdrains both provisioning and the background refresh.FindNewestCachedVersionacceptsincludePrerelease:Latestskips cached prereleases;Prereleaseuses the highest version of any kind.FallbackToCacheOrWarnkeepsincludePrerelease: true(a cached prerelease with a warning beats nothing when offline).Carry-overs from review
AcquireInUseLockdocuments theFileShare.Nonecross-process lock assumption (flock on Unix).SweepStaleTempDirectorieslock probe now catchesUnauthorizedAccessExceptionalongsideIOException.SweepStaleTempDirectoriesusesBcToolsLocator.SafeEnumerateDirectories(nowinternal) instead of duplicating the safe-enumerate pattern.SweepStaleTempDirectorieshas three-level error isolation: outer (guards cache root enumeration), per-TFM-folder (guards*.tmp-*enumeration within each TFM folder), and per-directory (guards probe/delete). A failure in one TFM folder never aborts the sweep for any other.FallbackToCacheOrWarnhas a doc comment explaining the deliberate prerelease inclusion; when the fallback is a prerelease underLatestmode, it logs a specific "no stable version cached" warning instead of the generic "using cached version" line.Config memoization (
WorkspaceStartupResolver)_fullConfigis aLazy<Task<WorkspaceStartupConfig>>initialized in the constructor.GetConfigAsync()returns the memoized task;BuildAlMcpArgsuses the completed result or throws if the provisioner is still running._provisioner == null(the public constructor tests use) the task completes synchronously — existing tests are unaffected.Behaviour changes
ALCops.Analyzersversion is first used on the second start after it ships (visible through the Information log line). First start after publication downloads it in the background.BuildAlMcpArgsnow throwsInvalidOperationExceptionwhen a provisioner is attached and not yet ready; no production caller does that (AlMcpProxy.StartCoreAsyncusesBuildAlMcpArgsAsync).Test plan
dotnet build --configuration Release— 0 warnings, 0 errorsdotnet test --configuration Release— 159 tests pass (11 new, 1 extended, 1 renamed)Provision_WarmCache_ReadyFromCache_RefreshDownloadsNewerForNextStart— verifies Ready completes from cache while background refresh is in-flight, then the newer version is cachedProvision_WarmCache_Latest_UsesNewestCachedStable_NotPrerelease— Latest mode skips cached prereleasesProvision_WarmCache_Prerelease_UsesNewestCachedAny— Prerelease mode uses highest cached version of any kindProvision_WarmCache_RefreshFailure_NeverThrows— background refresh failure is swallowed with a warningProvision_WarmCache_ShutdownCancelsRefresh— cancelling the token stops the background refreshProvision_Pinned_Cached_ZeroRequests— pinned version in cache makes zero HTTP requestsProvision_ExtractsCorrectTfm_And_WritesManifest— assertsBackgroundRefresh.IsCompletedon cold-cache pathGetConfigAsync_IsMemoized— config resolution is not repeated after settings.json changesSweep_EnumerationFailure_DoesNotFailProvisioning— locked stale dir survives sweep; a second TFM folder's stale dir is still cleaned; on Unix, unreadable TFM folder exercises the per-folder catchFallback_Latest_OnlyPrereleaseCached_UsesItWithExplicitWarning— prerelease-only cache under Latest logs "no stable version cached"Provision_Latest_StableAndPrereleaseCached_FastPathUsesStable_NoLastResortWarning(wasFallback_…PrefersStable) — verifies the cache-first fast path, not fallback; asserts zero NuGet requestsFallback_Prerelease_PicksHighestCachedAcrossStableAndPrerelease— Pinned mode with unresolvable version falls back to highest cached (1.3.0-preview.1 > 1.2.0 in SemVer 2); logs "using cached version"[AlMcpFact]tests (almcp-backed) remain green🤖 Generated with Claude Code