feat(quickfiler-test): add WinForms message-pump test seam, reduce QfcItemController exemption boundary 19 to 11 (#230) - #479
Merged
Conversation
- Add WinFormsPumpHost STA background-thread Application.Run() seam in QuickFiler.Test/TestSupport, analogous to StartRunningDispatcher - Add optional seam parameters to QfcItemController CreateAsync and CreateSequentialAsync factories to make them injectable - Remove ExcludeFromCodeCoverage from 8 previously unreachable QfcItemController orchestration members and add covering tests - Fix static UiThread.Dispatcher swap race between test classes using a SemaphoreSlim gate with idempotent restore - Add feature folder artifacts (issue, spec, user story, plan, research, and QA-gate evidence) for the change Refs: #230 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7hgSxkKfeXX3gn8jdmo4v
Policy audit, code review, and feature audit for the WinForms message-pump test seam (19/19 AC PASS; GO recommendation with maintainer boundary re-ratification). Includes feature-review agent memory updates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7hgSxkKfeXX3gn8jdmo4v
…493, #494) Record the three latent defects surfaced during issue #230 execution as promoted potential-bug entries so they survive the feature-folder merge: - #492 nullable gate reports EXIT 0 without compiling UtilitiesCS because the prescribed command uses /t:Build; a forced /t:Rebuild yields 195 errors, all in UtilitiesCS.csproj - #493 EnsureUiThreadDispatcher mutates the process-wide static UiThread._dispatcher without restoring it, which caused a real deadlock in #230 Phase 8 iteration 1 - #494 CLAUDE.md and .claude/rules disagree on coverage thresholds (80/90 vs 85/75) and on exclusion policy Refs: #230 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7hgSxkKfeXX3gn8jdmo4v
Record the maintainer's ratification of the QfcItemController coverage-exemption boundary reduced from 19 sites to 11 by PR #479, superseding the 19-member boundary ratified 2026-07-02 under #227. Discloses two limits rather than smoothing them over: - EnsureBreadcrumbPipeline was added by #351 after the 2026-07-02 ratification, so the branch carried 19 sites against a historically ratified 18; the count of 11 is measured against the actual 19. - CreateAsync and InitializeAsync have a structurally unreachable terminal statement under unit-test conditions, so their per-member coverage is partial by construction. Refs: #230 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y7hgSxkKfeXX3gn8jdmo4v
5 tasks
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
WinFormsPumpHost, a test-support seam that runs a real WinFormsApplication.Run()message pump on a dedicated STA background thread, so tests can deterministically await continuations posted through aWindowsFormsSynchronizationContextinstead of hanging indefinitely.QfcItemControllercoverage-exemption boundary from 19 sites to 11, de-exempting 8 of the 9 members named in issue Build a WinForms message-pump test seam (Application.Run() background thread) to unblock 9 QfcItemController orchestration members #230. Each[ExcludeFromCodeCoverage]attribute was removed in the same change as the test that covers it.CreateAsync/CreateSequentialAsyncstatic factories inQfcItemController, mirroring the primary constructor's existing pattern. This is additive and non-breaking; no existing call site changes.UtilitiesCS.UiThread.Dispatchercould deadlock under class-level parallelization.InitializeWebViewAsyncremains exempt by design — see Backward Compatibility / Scope Notes.Why
Issue #227 reduced
QfcItemController's exemption boundary from 103 to 19 members. The 9 residual members were all blocked by one structural gap: their continuations are posted through aWindowsFormsSynchronizationContext, and no thread in an MSTest run pumps WinForms messages, so awaiting them hangs indefinitely.The repository had already solved the equivalent WPF problem (
StaDispatcherHostinUtilitiesCS.Test/Threading/WpfUiDispatcherTests.cs;StartRunningDispatcher()inQuickFiler.Test/Controllers/QfcItemController.TestSupport.cs), but the WinForms analogue was explicitly deferred out of #227 cycle 5 into this issue.Until it existed, those members stayed exempt for an infrastructure reason rather than a genuine testability barrier — and every contributor touching WinForms-context async code faced the same hang hazard with no sanctioned tool.
What Changed
Core test infrastructure
QuickFiler.Test/TestSupport/WinFormsPumpHost.cs(new) —internal sealed classimplementingIDisposable. Dedicated background thread,IsBackground = true, named,SetApartmentState(ApartmentState.STA)beforeStart(). The thread body explicitly installs aWindowsFormsSynchronizationContext, signals readiness, then entersApplication.Run(new ApplicationContext()). ExposesSyncContext,ThreadId, twoInvokeAsyncoverloads, twoRunAsyncoverloads,StopAsync, and an idempotentDispose.QuickFiler.Test/TestSupport/WinFormsPumpHostTests.cs(new) — self-tests for the seam: thread-id assertions proving work executes on the pump thread, fault marshalling, readiness handshake, and deterministic shutdown.Production change (additive)
QuickFiler/Controllers/QfcItemController.Initialization.cs— optional seam parameters onCreateAsync/CreateSequentialAsync; 7[ExcludeFromCodeCoverage]removals. This file now carries zero exemption sites.QuickFiler/Controllers/QfcItemController.ViewerSetup.cs— 1[ExcludeFromCodeCoverage]removal.Tests
QfcItemController.InitializationTests.Part2.cs(new) — shared pump fixture and theSemaphoreSlim(1,1)isolation gate.QfcItemController.InitializationTests.Part3.cs(new) — 5 pump-driven tests.QfcItemController.SeamFactoryTests.cs,QfcItemController.InitializationTests.cs,QfcItemController.ViewerSetupTests.cs— extended.QuickFiler.Test/QuickFiler.Test.csproj—<Compile Include>wiring for all 4 new files. This project is legacy-format, so an unlisted.csfile silently does not compile.Docs and evidence
Feature folder
docs/features/active/2026-08-07-winforms-message-pump-test-seam-230/: issue, spec, user story, atomic plan, research artifact, three review audits, and 25 evidence artifacts including baseline and final Cobertura coverage XMLs.Architecture / How It Fits Together
ItemViewercapturesSynchronizationContext.Currentin its constructor and exposes it asUiSyncContext. WinForms auto-install replaces the ambient context duringControlconstruction, so the captured context is aWindowsFormsSynchronizationContextwhose posted continuations are drained only by a message loop running on the constructing thread.WinFormsPumpHostsupplies that loop. Tests construct theItemVieweron the pump thread viahost.InvokeAsync, so the context it captures belongs to a thread that is actively pumping. Continuations then drain normally and the awaiting test thread completes deterministically. NoSynchronizationContextis ever mutated on the MSTest thread itself.Coordination is entirely event-based:
ManualResetEventSlimfor readiness,TaskCompletionSource<T>withRunContinuationsAsynchronouslyfor completion and shutdown. Shutdown postsApplication.ExitThread, awaits the stopped signal, joins the thread, and asserts liveness.Verification
Completed
TreatWarningsAsErrors)[TestMethod]names, 0 missing from/ListTestsdiscovery.csfiles; all 21 new tests carry[Timeout]The coverage denominator grew by 172 lines from de-exemption, so the raw before/after comparison is not denominator-stable; both figures are reported above and in
evidence/qa-gates/coverage-delta.2026-08-08T00-15.md.The Phase 8 toolchain loop failed its first iteration and restarted, as the plan requires. See Risks and Mitigations.
Recommended
Backward Compatibility / Scope Notes
InitializeWebViewAsyncremains exempt — a documented non-goal, not an omission. It cannot be covered without the real WebView2 runtime: after the mockedIWebViewCoreInitializercalls,((ItemViewer)_itemViewer).L0v2h2_WebView2.CoreWebView2is null, and the unit-test policy bars external-dependency tests. Issue Build a WinForms message-pump test seam (Application.Run() background thread) to unblock 9 QfcItemController orchestration members #230 flags this member's residual barrier as tracked separately. Achievable outcome is therefore 8 of 9 members.CreateAsyncandInitializeAsynchave a structurally unreachable terminal statement under unit-test conditions (the tail afterInitializeWebViewAsync), so their per-member coverage is partial by construction rather than incomplete by neglect.EnsureBreadcrumbPipeline(QfcItemController.ViewerSetup.cs) was added by Feature: quickfiler-breadcrumb-webview2 #351 after that ratification; it is documented as out of scope and is unchanged by this PR.Risks and Mitigations
[Timeout]; faults are marshalled from the pump thread to the awaiting test thread viaTaskCompletionSourceIsBackground = true; shutdown postsApplication.ExitThread, awaits the stopped signal, joins, and asserts liveness;Disposeis idempotentSynchronizationContextbleed into sibling testsItemVieweris constructed on the pump thread viaInvokeAsyncUiThread.Dispatcherswap race between parallel test classes[Timeout]expiries: one class's restore reverted the static to a never-pumped dispatcher while another was still awaiting a dispatcher operation. Fixed with a staticSemaphoreSlim(1,1)gate held from fixture build through an idempotent restorevstest /ListTestsdiscovery of every statically-enumerated new[TestMethod]nameRollback: revert the two commits. The production change is additive, so reverting restores the prior exemption boundary and factory signatures without call-site changes.
Review Guide
Suggested order:
QuickFiler.Test/TestSupport/WinFormsPumpHost.cs— the core seam; read the readiness handshake and shutdown paths first.QuickFiler.Test/TestSupport/WinFormsPumpHostTests.cs— confirms the seam behaves as claimed.QuickFiler/Controllers/QfcItemController.Initialization.cs— the additive factory seam plus 7 attribute removals.QfcItemController.InitializationTests.Part2.cs— the isolation gate (the subtlest part of the change).evidence/qa-gates/coverage-delta.2026-08-08T00-15.mdandexclusion-census-post.2026-08-07T23-30.mdfor the numbers.Diff noise: two committed Cobertura XMLs (
coverage-baseline.cobertura.xml,coverage-final.cobertura.xml) account for ~374,000 of the ~379,000 added lines. They are raw coverage evidence, consistent with the precedent set by #420 and #424. Everything else is ~5,000 lines.Follow-ups
Out-of-scope findings recorded during execution, to be routed as separate issues:
UtilitiesCSnullable debt. A forced-t:Rebuildunder/p:Nullable=enable /p:TreatWarningsAsErrors=trueproduces 195 errors, all inUtilitiesCS.csproj(0 inQuickFiler/QuickFiler.Test). The prescribed/t:Buildgate passes only because the incremental up-to-date check does not recompile, so the repo-wide nullable gate is weaker than it appears. Recorded inevidence/baseline/baseline-nullable.2026-08-07T21-45.md.CS2002duplicate<Compile>entry forUtilitiesCS.Test/OutlookObjects/Folder/PercentageFormatterTests.cs— pre-existing, present at baseline and after.QfcItemControllerTestSupport.EnsureUiThreadDispatchermutates the process-wideUiThread._dispatcherwithout restoring it. This pre-existing pattern is what made the deadlock above possible; this PR's fixture restores, but the shared helper still does not.WinFormsPumpHostpromotion to a shared test-support project ifUtilitiesCS.Testgains a consumer (spec Non-Goal 3).QfcItemController.ViewerSetup.csremains at 81.88% line / 62.96% branch, below the per-file floor but improved from 74.37% / 56.00% with zero changed-line regression. The residue is pre-existing Refactor: com-vsto-coverage-exemption #197 debt in untouched members; the reviewer dispositioned it non-blocking.GitHub Auto-close