fix(quickfiler): correct WebView2 breadcrumb host lifecycle, thread marshalling and initializer contract - #658
Merged
drmoisan merged 10 commits intoAug 27, 2026
Conversation
…vidence Phase 0 of the atomic plan for epic child 476 (WebView2 breadcrumb host and core-initializer defects). Records the nine mandated policy and requirement reads, the toolchain resolution, and all four baseline toolchain commands with their observed exit codes. Baseline figures: - csharpier check . EXIT 0, zero files reported (no pre-existing debt) - analyzers /t:Rebuild EXIT 0, 0 errors, 5 pre-existing packaging warnings - nullable /t:Rebuild EXIT 0, 0 errors - full suite with coverage EXIT 0, 6701 tests, 6701 passed, 0 failed - repository line rate 0.851302, branch rate 0.791973 - the three in-scope production files are ABSENT from the baseline Cobertura, as expected from their class-level coverage exemptions No production or test source file is modified by this phase. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T45irz1DRk6bPnxx5ifqFA
Phase 1 of the atomic plan for epic child 476. Authored before any production change, per the Bugfix Workflow in CLAUDE.md. Tests added: - three argument-guard tests on the concrete WebView2CoreInitializer (#477 defect 2) - one structural backing-field test for the initialization flag (#476 defect 2) - two single-Post marshalling tests through a recording SynchronizationContext (#476 defect 1) - three owner-registry tests: predecessor detach, null-core tolerance, disposed self-detach (#458) - two dispatcher-installation tests covering install-from-uiSyncContext and preservation of an injected dispatcher (#476 defect 1) Observed red states: - P1-T1: 3 discovered, 3 failed, 0 passed - P1-T2: 1 discovered, 1 failed, 0 passed - P1-T3 through P1-T8: compile-red, CS1729 on the 3-argument constructor and CS1061 on IsAttached and HasUiDispatcher Two contiguous Compile Include entries were added to QuickFiler.Test.csproj immediately after the Controllers\WebView2CoreInitializerTests.cs entry; the ItemGroup is not re-sorted and no line is moved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T45irz1DRk6bPnxx5ifqFA
…contract Phase 2 of the atomic plan for epic child 476. Production edits are confined to the three in-scope files. WebView2BreadcrumbHost.cs - #458: per-control ConditionalWeakTable owner registry plus a static gate; the constructor performs lookup-detach-replace under that gate using only TryGetValue, Remove and Add. DetachCore performs the real unhook on the predecessor instance and tolerates a null CoreWebView2. A Disposed handler detaches and evicts the registry entry when this instance is still the owner. The dead constructor-side unhook and its misleading comment are removed. - #476 defect 1: internal three-argument constructor with the public two-argument constructor chaining to it unchanged; NavigateToString and PostMessageJson each route through exactly one BreadcrumbUiDispatcher.Dispatch callback, with an inline fallback for the pre-initialization window. The dispatcher is installed in InitializeAsync from its uiSyncContext argument and only when none was injected. DispatchValue is not used and CaptureCurrent is not called. - #476 defect 2: IsCoreInitialized is backed by an explicit private field read through Volatile.Read; the write uses Volatile.Write and stays strictly between the WebMessageReceived subscription (line 311) and CoreInitialized (line 317). WebView2CoreInitializer.cs - #477 defect 2: guards for a null and a whitespace cacheFolder and for a null control, all before any SDK call. environment and options stay unguarded. - #477 defect 1: exemption rationale restated on the external Evergreen runtime plus user-data-folder ground; no residual forwarding claim. IWebViewCoreInitializer.cs - Documentation only. The forwarding claim is removed, the unconditional null browserExecutableFolder is documented as a deliberate Evergreen-only decision, and exception documentation is added for the guards. No signature changed. Gates: eleven fail-before failures recorded before any fix; every targeted test green afterwards; nullable /t:Rebuild EXIT 0 with zero CS86xx and zero skipped CoreCompile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T45irz1DRk6bPnxx5ifqFA
…nd members Phase 3 of the atomic plan for epic child 476. The class-level exemptions became false the moment Phase 2 landed, because the internal constructor, the dispatcher routing decisions, the registry detach path and the state accessor are now all reachable from tests. Leaving them would recreate the false-rationale defect #477 reports. WebView2BreadcrumbHost - class-level ExcludeFromCodeCoverage removed; the type summary and remarks no longer assert a 1:1 SDK forward and now state accurately what remains exempt - the two SDK forwards extracted into ForwardNavigateToString and ForwardWebMessage so each routing decision stays measured - member-level exemptions applied to exactly four members: the two SDK event handlers and the two extracted forwards. InitializeAsync stays measured because its only SDK-reaching statements go through the mockable seam. WebView2CoreInitializer - class-level ExcludeFromCodeCoverage removed; the two SDK calls extracted into ForwardCreateEnvironmentAsync and ForwardEnsureCoreWebView2Async, which carry the attribute; the argument guards are measured. Three reflection contract tests pin the allocation. All fifteen feature tests run green in one invocation: 15 discovered, 15 passed, 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T45irz1DRk6bPnxx5ifqFA
Committed by the epic-orchestrator parent, not by the feature's own child, to remove a data-loss risk after the child was killed by an API spend limit with 7 uncommitted files on top of its 4 existing commits. No review, no toolchain pass, and no acceptance-criteria verification has been performed on this state. The resuming child owns validating, completing and if necessary amending it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ration' into bug/webview2-host-initializer-defects-476-exec
Merge integration tip 69e8317 into the feature branch and record the reconciliation. The branch was 5 ahead / 28 behind and is now 6 ahead / 0 behind with no conflicts. The pure-deletion query returns no rows, so no file loses content the base gained. Uncheck [P4-T1] through [P4-T3]. Their gate artifacts predate the merge and no longer describe the tree under test, so the final QC loop restarts from the formatter against merge commit 9cb2c4f. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d base The qa-1 through qa-3 artifacts recorded at 20-49..20-51 predate the merge of the integration base at 9cb2c4f and no longer describe the tree, so Phase 4 was restarted from [P4-T1] and every gate re-run from a clean start. Those stale artifacts are left on disk as history. Results: csharpier check . reports zero files; analyzer rebuild 0 errors / 5 non-code warnings; nullable rebuild 0 errors with zero CS86xx; full suite 6734/6734 passed, 0 failed. Both msbuild runs used /t:Rebuild and logged zero "Skipping target CoreCompile" occurrences against 36 csc.exe invocations, so neither gate was vacuous. No source file was rewritten across the phase. Repository line coverage 85.1302% -> 85.1435% (+0.0133 pp) and branch 79.1973% -> 79.2018% (+0.0045 pp), both above every floor the baseline met. One gate is not met and is recorded rather than remediated: the 90% floor on newly measured members falls short for NavigateToString (62.50%), DetachCore (66.67%), CreateEnvironmentAsync (83.33%) and EnsureCoreWebView2Async (66.67%). Every uncovered line in those four members is a statement that reaches the WebView2 SDK, which a unit test may not depend on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… evidence Phase 5 checks off every criterion in spec.md individually, each against a concrete artifact or diff. 37 of 37 satisfied, 0 unchecked. Four reconciliations are recorded rather than glossed: - CaptureCurrent returns 1 raw match in WebView2BreadcrumbHost.cs, inside a comment explaining why the member is not used; comment-stripped count is 0 and there is no call. - The csproj :159 anchor drifted to :173 through three sibling features' insertions; the two entries sit immediately after the entry that line number denotes, and the hunk is 2 added / 0 removed with no moved line. - Formatting was applied file-scoped and verified repository-wide with csharpier check ., and tests ran through Invoke-MSTestWithCoverage.ps1 rather than a bare vstest.console.exe, which would omit the LiveOutlook filter and launch a real Outlook process. - Five of fifteen tests use a hand-written recording SynchronizationContext rather than Moq, because criterion 12 mandates that shape by name. The change inventory distinguishes this feature's own change set (base..HEAD, 78 paths, production classification exactly the three in-scope files) from the diff against the pre-merge BASELINE_SHA (250 paths), which conflates this branch with the twenty-eight commits the base merge brought in. Blocking finding carried out of the plan: the 90% floor on newly measured members is not met for NavigateToString (62.50%), DetachCore (66.67%), CreateEnvironmentAsync (83.33%) and EnsureCoreWebView2Async (66.67%). Every uncovered line is a statement reaching the WebView2 SDK. No spec criterion asserts that floor, so none is left unchecked on its account. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…re audits Feature review against HEAD d1dcabd returns zero blocking findings in all three artifacts. Policy audit: 1 non-blocking (the newly-measured-member coverage floor), 1 advisory (the standing 80/90 versus 85/75 threshold contradiction). Code review: 1 non-blocking (DetachCore does not remove the predecessor Disposed subscription), 4 advisory. Feature audit: 1 advisory, and all 37 spec criteria substantiated against source, diff or evidence. The 90 percent floor over the eleven members newly entering measurement is dispositioned non-blocking: ten of the ten uncovered lines were structurally verified to be SDK-reaching statements or their closing braces, and the floor binds members that are added rather than pre-existing members entering measurement through a correctly narrowed exemption. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
drmoisan
merged commit Aug 27, 2026
5793b8c
into
epic/quickfiler-bug-family-integration
5 checks passed
drmoisan
added a commit
that referenced
this pull request
Aug 28, 2026
…vidence Re-merged epic/quickfiler-bug-family-integration tip 5793b8c (sibling 476, PR #658) into bug/breadcrumb-coordinator-hub-defects-501 and re-ran the full C# toolchain against the reconciled head. - csharpier check: 1547 files, 0 rewrites - msbuild /t:Rebuild analyzers: 0 errors, 0 CoreCompile skips, 51 CoreCompile targets - msbuild /t:Rebuild TreatWarningsAsErrors: 0 errors, 0 CoreCompile skips - MSTest with coverage: 6745/6745 passed, line-rate 85.1494%, branch-rate 79.1998% Base reconciliation proves 0 behind after the merge, justifies the single pure-deletion file as a partial-class relocation, and shows all twelve merged sibling entries (493, 444, 476) intact in QuickFiler.Test.csproj. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 28, 2026
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.
Suggested title
fix(quickfiler): correct WebView2 breadcrumb host lifecycle, thread marshalling and initializer contract
Summary
ConditionalWeakTableowner registry with a static gate lets a supersededWebView2BreadcrumbHostdetach its own subscription, replacing a constructor-side unhook that could never remove a predecessor's delegate (Bug: webview2breadcrumbhost-handler-retention-pooled-viewer #458).SynchronizationContextinstead of calling it on whatever thread the caller happens to be on, and publishesIsCoreInitializedthrough aVolatilefield rather than an unsynchronized auto-property (Bug: webview2breadcrumbhost-unmarshalled-sdk-call-and-unsynchronized-state #476).IWebViewCoreInitializercontract: the false "1:1 forward" claim is replaced with documented behaviour, and both implementations now validate their arguments (Bug: iwebviewcoreinitializer-contract-defects #477).[ExcludeFromCodeCoverage]from whole classes down to six genuinely host-bound members, so eleven previously unmeasured members now enter the coverage denominator.Why
Three separately reported defects share one file set and one lifecycle area, so they are fixed together rather than in three passes over the same code.
WebView2BreadcrumbHostis constructed against a pooledWebView2control. The previous constructor tried to unhook a handler from the control before subscribing, but the delegate it unhooked belonged to the incoming instance, not to the predecessor that had actually subscribed.-=against a different target removes nothing, so each reuse of the pool left another host reachable from the control. The registry makes the predecessor detach itself, which is the only instance that can.Separately, the host called into the WebView2 SDK directly from the calling thread and published its initialization state through a plain auto-property. WebView2 is thread-affine, and an unsynchronized bool gives no ordering guarantee between the writer that completes initialization and a reader on another thread.
IWebViewCoreInitializerdocumented itself as a 1:1 forward of the SDK surface while hard-coding one of the SDK's arguments and validating none of its own.What Changed
Core fix (
QuickFiler/Viewers/)WebView2BreadcrumbHost.cs(+269 / -38 across the file): owner registry and static gate; constructor lookup-detach-replace sequence; UI-dispatcher installation with an inline fallback; marshalledNavigateToStringandPostMessageJson;Volatile-publishedIsCoreInitialized; tolerantDetachCorefor a null or already-disposed core.WebView2CoreInitializer.cs: argument validation, and the SDK-reaching bodies extracted into thin[ExcludeFromCodeCoverage]forwards so the surrounding logic stays measured.IWebViewCoreInitializer.cs: contract documentation corrected; the Evergreen-only decision recorded on the interface rather than left implicit.Tests (
QuickFiler.Test/)Viewers/WebView2BreadcrumbHostTests.cs(new, 440 lines) andViewers/WebView2BreadcrumbHostContractTests.cs(new, 201 lines).Controllers/WebView2CoreInitializerTests.cs(+154 / -3).QuickFiler.Test.csprojgains exactly two<Compile Include>entries, both under this feature's ownedViewers\WebView2*prefix.Documentation and evidence — 85 paths under the feature folder: the atomic plan (88 tasks, all checked), the spec (37 criteria, all checked), the three review artifacts, and the baseline, regression and QA-gate evidence trees.
Architecture / How It Fits Together
The host owns no WebView2 lifetime of its own; it attaches to a control the viewer pool owns and outlives. The registry keyed on that control is therefore the only place where "who is currently attached" can live, because neither the host nor the control can answer it alone. On construction a host takes the static gate, asks the registry who holds the control, tells that predecessor to detach itself, and records itself as the new owner.
Thread affinity is handled at the boundary rather than at each call site: the host captures the UI
SynchronizationContextonce at construction, and the two SDK-facing methods post through it, falling back to inline execution when no context is available.IsCoreInitializedis the one piece of state read across threads, so it is the one field published withVolatile.IWebViewCoreInitializerremains the seam through which the item controller reaches the SDK, which is what makes the surrounding controller logic mockable.Verification
Completed (full toolchain re-run after the base merge, in the mandated order, against this head)
dotnet tool run csharpier check .msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=truemsbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=trueInvoke-MSTestWithCoverage.ps1System.Reactivepackages.configdiagnostic that predates this branch. Type check: 0 errors, 0CS86xx./t:Rebuildand recorded zeroSkipping target "CoreCompile"lines, so neither gate was vacuous. Non-vacuity is further evidenced by 36csc.exeinvocations and 65CoreCompile:headers over 19 projects.Recommended
Backward Compatibility / Migration Notes
No public API is removed or renamed.
IWebViewCoreInitializermethod signatures are unchanged; the change is documentation plus argument validation, so an existing implementation continues to compile. Callers that previously passed a null or whitespacecacheFolderand relied on the SDK's own behaviour will now receive anArgumentNullExceptionorArgumentExceptionnaming the parameter. No consumer in this repository does so.Detach ordering for pooled viewers changes: the predecessor host now detaches at the point its successor is constructed rather than never.
Risks and Mitigations
SynchronizationContextpost rather than running inline. The inline fallback preserves the old behaviour where no UI context exists, and both paths are covered.Review Guide
QuickFiler/Viewers/WebView2BreadcrumbHost.cs— the substance of the change; read the constructor andDetachCorefirst.QuickFiler/Viewers/WebView2CoreInitializer.csandIWebViewCoreInitializer.cs— the seam contract.QuickFiler.Test/Viewers/WebView2BreadcrumbHostTests.csandWebView2BreadcrumbHostContractTests.cs.docs/features/active/webview2-host-initializer-defects-476/code-review.2026-08-27T23-46.md— the review findings and their disposition.The 85 documentation and evidence paths are additive and mechanical; they can be skimmed.
Follow-ups
Two non-blocking findings are recorded but deliberately not fixed here, to avoid widening the change beyond the reviewed tree. Both are committed on this branch and therefore survive into the integration branch for the epic capstone.
code-review.2026-08-27T23-46.md):DetachCoredoes not remove the predecessor's_control.Disposed += OnControlDisposedsubscription, so one edge from the control to a superseded host survives until the control is disposed. The spec's residual-risk item 3 is overstated by exactly this edge. One-line fix; recommended for promotion at epic close.evidence/other/followup-promotion-handoff.2026-08-27T23-31.md): the EFC item controller reaches the WebView2 SDK directly at four sites instead of going through this seam, so it receives none of the guards added here. That file is outside this feature's writable set and is owned by a concurrent sibling feature; it must be promoted separately.The plan's own 90% floor over the eleven members newly entering measurement is not met (86/99 = 86.87%). The four short members are
NavigateToString,DetachCore,CreateEnvironmentAsyncandEnsureCoreWebView2Async; every uncovered line was structurally verified to be an SDK-reaching statement or its closing brace, which cannot execute without the Evergreen runtime. Review dispositioned this non-blocking on the ground that the 90% floor binds members that are added, not pre-existing members entering measurement because a class-level exemption was correctly narrowed. Worth maintainer ratification in the coverage-uplift tracking.The
CLAUDE.md80/90 and.claude/rules/general-unit-test.md85/75 coverage thresholds contradict each other. The stricter of each pair was applied and the contradiction changed no verdict here, but it remains unresolved repository-wide.GitHub Auto-close
All three were confirmed OPEN with
gh issue viewimmediately before this body was written. This pull request targetsepic/quickfiler-bug-family-integrationrather than the repository default branch, so GitHub will not act on these keywords at merge time; they record linkage for the epic capstone, which is responsible for the actual closure.