fix(781): prove ItemViewer UI-boundary ownership by owner thread, not SynchronizationContext identity - #785
Merged
drmoisan merged 2 commits intoSep 6, 2026
Conversation
- Replace ItemViewer.ThrowIfOffUiBoundary's SynchronizationContext reference comparison with an owner-thread identity check via the captured Dispatcher.CheckAccess(), since a WPF dispatcher operation installs a DispatcherSynchronizationContext that never matches the UI thread's ambient context again, causing the guard to reject calls made on the owning thread - Update the guard's XML docs and the fixed-statement-order comment to describe the new thread-identity check - Add ItemViewerBreadcrumbThreadAffinityTests with seven regression tests and remove the two obsolete reference-comparison tests from ItemViewerBreadcrumbLifecycleRegressionTests - Add baseline, regression, and QA evidence plus the finalized atomic plan and AC check-offs under the active bug feature folder, and record related notes in subagent memory - Untrack artifacts/orchestration/orchestrator-state.json, a gitignored session checkpoint that should not have been committed Refs: #781 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVVYdzxky9mmdcUVaYnH6k
- Add policy-audit, code-review, and feature-audit artifacts (2026-09-05T17-29) for the #781 feature review - Append an orchestrator correction to coverage-delta.2026-09-05T10-49.md recording the reviewer's class-by-class Cobertura finding - Add promoted potential bug entry for the UiThread SynchronizationContext awaiter reference-comparison defect (issue #784) - Update feature-review subagent memory notes under .claude/agent-memory/feature-review/ - Untrack artifacts/pr_context.summary.txt and artifacts/pr_context.appendix.txt, regenerated collector outputs already covered by .gitignore Refs: #781 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVVYdzxky9mmdcUVaYnH6k
drmoisan
force-pushed
the
bug/breadcrumb-ui-boundary-guard-rejects-dispatcher-built-viewers-781
branch
from
September 6, 2026 00:16
b808815 to
c1653ec
Compare
drmoisan
enabled auto-merge
September 6, 2026 00:19
drmoisan
deleted the
bug/breadcrumb-ui-boundary-guard-rejects-dispatcher-built-viewers-781
branch
September 6, 2026 00:24
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(781): prove ItemViewer UI-boundary ownership by owner thread, not SynchronizationContext identity
Summary
ItemViewer.ThrowIfOffUiBoundary(issue Bug: itemviewer-breadcrumb-pipeline-lifecycle #488 defect D4) now proves UI-thread ownership withDispatcher.CheckAccess()on theDispatchercaptured in the viewer constructor, replacing aSynchronizationContextreference comparison that rejected calls made on the owning UI thread.ItemVieweris constructed inside a WPFDispatcher.Invoke/InvokeAsyncoperation (ItemViewerQueueviaUiThread.Dispatcher), and WPF installs aDispatcherSynchronizationContextfor the duration of that operation. The captured context is never the UI thread's ambientWindowsFormsSynchronizationContextagain, so the old guard threwInvalidOperationExceptionfromInitializeBreadcrumbPipelineon every QuickFiler launch that produced folder suggestions.QuickFiler.Test/Viewers/ItemViewerBreadcrumbThreadAffinityTests.cs, including the production-shape reproduction (viewer built insideDispatcher.Invoke, pipeline initialized under a different ambient context) and two cross-thread cases that still expect the boundary exception. Two obsolete tests that encoded the old reference comparison are removed.EnsureBreadcrumbResourceOwnershipstatement-order comment are rewritten to describe the thread-identity contract and why context reference equality was unsuitable.artifacts/orchestration/orchestrator-state.json,artifacts/pr_context.summary.txt, andartifacts/pr_context.appendix.txtare untracked. All three are generated session artifacts under the git-ignoredartifacts/tree that had been committed earlier; the tracked checkpoint content was stale state from an unrelated feature.Why
QuickFiler high-confidence launch failed with an unhandled
InvalidOperationException("InitializeBreadcrumbPipeline must be called on the thread that owns this ItemViewer") fromQfcCollectionController.LoadSecondaryAsync -> QfcItemController.AssignFolderComboBox -> EnsureBreadcrumbPipeline -> ItemViewer.InitializeBreadcrumbPipeline. The failure was deterministic for dispatcher-built viewers and was not a race or anInvokeRequiredfalse negative; the thread-pool frames at the bottom of the reported trace are theasync voidrethrow path, not the throw site. A runtime probe on .NET Framework 4.8 (STA) recorded inevidence/other/dispatcher-synccontext-probe.2026-09-05T10-40.mdshowsInvoke ctx == outer ambient : False, which is the mechanism. The standard launch path reaches the same sequence and was expected to fail identically.Constraints honored: the fix is confined to
QuickFiler/Viewers/ItemViewer.Breadcrumb.cs;QfcCollectionController.LoadSecondaryAsync,QfcItemController.AssignFolderComboBox, andQfcItemController.EnsureBreadcrumbPipelineare unchanged (AC7, verified byevidence/qa-gates/scope-boundary.2026-09-05T10-49.md). The #488 D3 (second-provider fail-fast) and D5 (disposal) behaviors and their tests are unchanged and passing.What Changed
Core fix
QuickFiler/Viewers/ItemViewer.Breadcrumb.cs:ThrowIfOffUiBoundaryreadsUiDispatcher, returns without effect when it is null (keepsFormatterServices.GetUninitializedObject-built test viewers inert), and throwsInvalidOperationExceptionwhenCheckAccess()is false.using System.Threading;removed;using System.Windows.Threading;added. Exception message second sentence now states that the calling thread is not the constructing thread.UiSyncContextand every other member are unchanged.Tests
QuickFiler.Test/Viewers/ItemViewerBreadcrumbThreadAffinityTests.cs(new, 7 tests, MSTest + FluentAssertions, Arrange-Act-Assert; no sleeps, timers, temporary files, or message pump): same-thread success under a null ambient context, a different plain context, and aDispatcherSynchronizationContext; the dispatcher-constructed production shape;Task.Runworker rejection forInitializeBreadcrumbPipelineand the three-argumentConfigureBreadcrumbDropDown; and the null-owning-dispatcher escape.QuickFiler.Test/Viewers/ItemViewerBreadcrumbLifecycleRegressionTests.cs: removedInitializeBreadcrumbPipeline_AmbientContextNull_ThrowsBoundaryDiagnosticandInitializeBreadcrumbPipeline_DifferentNonNullContext_ThrowsBoundaryDiagnostic; corrected one helper comment that described the old guard.QuickFiler.Test/QuickFiler.Test.csproj: one<Compile Include>for the new test file.Docs and evidence
docs/features/active/2026-09-05-breadcrumb-ui-boundary-guard-rejects-dispatcher-built-viewers-781/:issue.md(AC1 through AC8 checked), atomic plan version 1.3, baseline / regression-testing / qa-gates / issue-updates / other evidence, and the feature-review artifactspolicy-audit,code-review, andfeature-audit(timestamp 2026-09-05T17-29).docs/features/potential/promoted/: promoted bug entries for this issue and for the follow-up issue Bug: uithread-synccontext-awaiter-always-posts-for-dispatcher-built-viewers #784..claude/agent-memory/: subagent memory notes recorded during planning, execution, and review.Architecture / How It Fits Together
ItemViewer()captures_uiDispatcher = Dispatcher.CurrentDispatcher(and still captures_context = SynchronizationContext.Current, whichUiSyncContextcontinues to expose). The four guarded members (InitializeBreadcrumbPipeline, bothConfigureBreadcrumbDropDownoverloads,EnsureBreadcrumbResourceOwnership) callThrowIfOffUiBoundaryas their first statement. The guard now asks the capturedDispatcherwhether the calling thread is its owner, which is true for every call made on the UI thread regardless of whichSynchronizationContextinstance is ambient at the call site, and false for a thread-pool worker.BreadcrumbUiDispatcherand the rest of the breadcrumb pipeline are unchanged.Verification
Completed (from
evidence/qa-gates/andevidence/regression-testing/, all commands run from the repository root in PowerShell):dotnet tool run csharpier format .thendotnet tool run csharpier check .: exit 0, 0 files needing formatting.msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true: exit 0, 0 errors, 3 warnings (all MSB3061 file locks from a running Outlook; equal to baseline).msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true: exit 0, 0 errors, no CS86xx.vstest.console.exe QuickFiler.Test.dll /EnableCodeCoverage /InIsolation: 1339/1339 passed.dotnet-coverage collectaroundvstest.console.exe ... /InIsolation: 9 assemblies, 6997/6997 passed; line-rate 0.848347 -> 0.848316 on an identical 64740-line denominator (delta -0.000031 against a 0.005 tolerance); branch-rate 0.791542 -> 0.791421. FourUtilitiesCS.Testshell-icon classes were excluded locally because of a workstation-levelSHGetFileInfohang; CI covers them.regression-fail-before); after the fix, 33/33 across the four affected classes (regression-pass-after). The feature reviewer independently reproduced both results by swapping the production file to the merge-base version and back.CHANGED-CODE COVERAGE: NOT MEASURABLE.QuickFiler/Viewers/ItemViewer.csline 20 carries[ExcludeFromCodeCoverage]on the partial class (the ratified WinForms UserControl exemption), so the changed lines are outside the coverage denominator. Substitute evidence: every outcome of both conditionals in the rewritten guard has a named passing test (evidence/qa-gates/changed-code-coverage.2026-09-05T10-49.md).Recommended:
gh pr checks --requiredon this PR (the S9 CI-green gate is applied before merge).Backward Compatibility / Migration Notes
UiSyncContext,UiDispatcher, and all guarded member signatures are unchanged.SynchronizationContext; a call from another thread still throwsInvalidOperationException. The exception message's second sentence changed wording; no code asserted on it outside the two removed tests.artifacts/files are removed from the index. No workflow reads them from git.Risks and Mitigations
regression-fail-before.GetUninitializedObject) have a nullUiDispatcher. Mitigation: the null-owner escape is retained and pinned byInitializeBreadcrumbPipeline_NullOwningDispatcher_DoesNotThrow..claude/rules/quality-tiers.mdwhile above the 80 percent floor inCLAUDE.md. Mitigation: the figure is pre-existing and this change adds no line to the denominator; recorded as a non-blocking finding in the policy audit.Review Guide
QuickFiler/Viewers/ItemViewer.Breadcrumb.cs: the 41-line diff is the entire production change; readThrowIfOffUiBoundary, its<remarks>, and the statement-order comment inEnsureBreadcrumbResourceOwnership.QuickFiler.Test/Viewers/ItemViewerBreadcrumbThreadAffinityTests.cs: the seven tests and their private helpers.QuickFiler.Test/Viewers/ItemViewerBreadcrumbLifecycleRegressionTests.cs: deletions plus one comment correction.code-review.2026-09-05T17-29.mdandpolicy-audit.2026-09-05T17-29.mdfor the reviewer's independent RED/GREEN reproduction and coverage analysis.Follow-ups
UtilitiesCS.UiThread.SynchronizationContextAwaiter.IsCompleteduses the same reference comparison, soawait viewer.UiSyncContextalways posts for a dispatcher-built viewer (extra hop, not a failure). The same issue lists two review nits to ride along: the guard's<remarks>cites managed-thread-id uniqueness whileCheckAccess()comparesThreadreferences, andDrainableSynchronizationContext.Drain()in the regression test file is now unused.DictionaryExtensions_Tests.TryAddValuesAsync_UpdatesExistingValueis intermittently flaky under parallel coverage runs; it passed in both runs here.artifacts/pr_body_*file pairs remain tracked under the ignoredartifacts/tree and can be removed in a separate chore.GitHub Auto-close
Other numbers in the collector's author-asserted list (#475, #488, #780, #784) are references, not closures.
🤖 Generated with Claude Code
https://claude.ai/code/session_01BVVYdzxky9mmdcUVaYnH6k