Skip to content

Bug: qfc-item-controller-init-tests-flaky-window-handle #571

Description

@drmoisan
  • Work Mode: full-bug

Summary

Two tests in QuickFiler.Controllers.Tests.QfcItemController_InitializationTests
fail intermittently during a full-suite run with
InvalidOperationException: Invoke or BeginInvoke cannot be called on a control until the window handle has been created, but pass every time when the class is
run in isolation. The tests exercise a real WinForms Control.Invoke path with
no seam, so they depend on whether the control's window handle happens to exist
when the test reaches it.

Environment

  • OS/version: Windows 11 Pro 10.0.26200
  • Python version: n/a (C# / .NET Framework 4.8.1, MSTest)
  • Command/flags used: vstest.console.exe <9 test assemblies> /EnableCodeCoverage /InIsolation /Logger:trx /TestCaseFilter:"TestCategory!=LiveOutlook"
  • Data source or fixture: none; the failure is in test-host state, not data

Steps to Reproduce

  1. Build the solution in Debug.
  2. Run the full suite across all nine *.Test.dll assemblies with the command
    above.
  3. Repeat. The two tests below fail on some runs and pass on others.

Observed on 2026-08-15: run 1 passed both tests, run 2 failed both, run 3 (full
suite) passed both. Running only
/TestCaseFilter:"FullyQualifiedName~QfcItemController_InitializationTests"
passed 9 of 9 on every attempt.

Expected Behavior

Per .claude/rules/general-unit-test.md, tests are deterministic: identical
inputs and environment produce identical results, and the suite does not depend
on ordering or on ambient UI state.

Actual Behavior

Both of the following fail non-deterministically:

  • InitializeNineArgOverload_ThroughThePumpHost_SavesParametersAndDelegates
  • InitializeBool_ThroughThePumpHost_CompletesAndInitializesState
System.InvalidOperationException: Invoke or BeginInvoke cannot be called on a
control until the window handle has been created.
   at System.Windows.Forms.Control.MarshaledInvoke(...)
   at System.Windows.Forms.Control.Invoke(Delegate method, Object[] args)
   at QuickFiler.ItemViewer.QuickFiler.IItemViewer.Invoke(Delegate method)
   at QuickFiler.Controllers.QfcItemController.InvokeBeginInvoke(Boolean async, Action action)
      in QuickFiler\Controllers\QfcItemController.FocusAndTheme.cs:line 256
   at QuickFiler.Controllers.QfcItemController.ToggleTips(Boolean async, ToggleState desiredState)
      in QuickFiler\Controllers\QfcItemController.FocusAndTheme.cs:line 204

Logs / Screenshots

  • Attached minimal logs or screenshot
  • Snippet: stack trace above, extracted from the TRX of the failing run on
    2026-08-15.

Impact / Severity

  • Blocker
  • High
  • Medium
  • Low

The failure is intermittent and does not indicate a production defect, but a
flaky test in a protected gate is corrosive: it trains reviewers to re-run
rather than investigate, and it can fail an otherwise-green CI run at random.

Source

From: docs/features/potential/2026-08-15-qfc-item-controller-init-tests-flaky-window-handle.md

Activity

  1. drmoisan commented on Aug 22, 2026

    @drmoisan
    OwnerAuthor

    Premise correction from the epic execution run (2026-08-22)

    Investigated under epic quickfiler-suite-determinism-foundation. The child halted rather than land a fix, because the stated root cause is falsified by measurement. Recording it here so the issue text is not acted on as written.

    What was measured

    WinFormsPumpHost.RunPumpThread does call Application.Run(new ApplicationContext()) without ever adding a form or control. But that does not mean the viewer lacks a window handle:

    • QuickFiler.ItemViewer's constructor calls InitializeComponent(), which runs ((ISupportInitialize)_l0v2h2_WebView2).BeginInit() / .EndInit() and the same pair for _l0vhBreadcrumb_WebView2 — QuickFiler/Viewers/ItemViewer.Designer.cs:89-90 and :6166-6167.
    • EndInit creates the WebView2 child window handles, and WinForms creates a parent's handle when a child's handle is created. The ItemViewer's own handle therefore exists the instant construction returns.
    • A bare new QuickFiler.ItemViewer() on the pump thread — no harness, no SaveParameters, no .Handle read — already reports both children as handle-created.

    So Control.Invoke was never reached without a handle in this fixture, and the proposed remedy (forcing the handle on the pump thread before the act) is a measured no-op: with the statement present and with it commented out, the measured handle state is identical.

    What the real failure looks like

    The one genuine pre-fix failure was seven expiries at the 60,000 ms PumpTimeoutMs under machine load. A missing handle makes Control.Invoke throw immediately; it does not hang for sixty seconds. Handle-forcing cannot address a timeout.

    The post-fix evidence does not demonstrate efficacy either. Pre-fix run-level failure rate is roughly 1 in 21 (~4.8%), so thirty consecutive clean runs has probability ≈ 0.952^30 ≈ 0.23 under the null hypothesis of no effect — about a one-in-four chance of producing that record with no fix at all. Under the 17-node MSBuild contention that reproduced the original failure, a supplementary pass was only 8 of 10 green: the suite still fails under load.

    Where the work is preserved

    Branch bug/winformspumphost-suite-determinism-511-exec, commit 53a2a08f — 36 markdown evidence artifacts including evidence/regression-testing/webview-child-handle-measurement.2026-08-21T18-10.md, which records the four measurements. Nothing was merged. No production file was modified, and both coverage-bearing production files hash to their exact merge-base blobs, so the coverage justifications this issue's sibling depends on are untouched.

    The real defect is tracked separately; see the linked issue.

  2. added a commit that references this issue on Aug 22, 2026
  3. drmoisan commented on Aug 23, 2026

    @drmoisan
    OwnerAuthor

    Closing as superseded by #592 — the symptom is real and still tracked

    To be unambiguous: the flakiness described here is real and is not dismissed. It is measured at roughly a 1-in-21 (~4.8%) run-level failure rate, and reproduces under load at 8 of 10 green under induced 17-node MSBuild contention. What is wrong is the diagnosis, and that is why this issue is being closed rather than left open.

    Why closing rather than correcting in place

    Three reasons.

    1. The stated root cause is falsified. This issue attributes the failure to a missing window handle on ItemViewer, and its remedy forces handle creation. Verified against source:

    • ItemViewer() calls InitializeComponent() — QuickFiler/Viewers/ItemViewer.cs:25
    • InitializeComponent runs BeginInit() on both WebView2 children — QuickFiler/Viewers/ItemViewer.Designer.cs:89-90
    • and EndInit() on them — :6166-6167
    • EndInit creates the child handles, and WinForms creates a parent's handle when a child's handle is created

    The viewer's handle therefore already exists the instant construction returns. The remedy would force a handle that is already present — a no-op.

    2. The failure signature contradicts the diagnosis. PumpTimeoutMs = 60000 (QuickFiler.Test/Controllers/QfcItemController.InitializationTests.cs:38, applied as [Timeout(PumpTimeoutMs)]). The one genuine pre-fix failure was seven expiries at 60,000 ms, not an exception. A missing handle makes Control.Invoke throw immediately; it does not hang for sixty seconds.

    3. A premise correction in a comment is not enough. The issue body still reads authoritatively, and a body is what gets implemented. This has already happened once: the epic-planning pass was given the seam-dependency premise straight from this issue text and had to refute it against source. Leaving the issue open with a corrective comment preserves the trap for the next reader.

    #511 and #571 also contradict each other

    Independently of the false premise, these two cannot both be implemented as written. #511 proposes replacing the real message pump with an injectable context; executed literally that deletes the very tests #571 exists to stabilize. That conflict disappears once both are superseded by a single correctly-diagnosed issue.

    Where the work went

    Reopen this issue if the #592 investigation shows the handle attribution was correct after all.

  4. drmoisan commented on Sep 13, 2026

    @drmoisan
    OwnerAuthor

    Reconciliation from issue #743 (2026-09-13)

    This comment reconciles the closing state of #571 against the current tree and the resolution delivered under #743.

    (a) The premise correction stands

    The premise correction recorded above on 2026-08-22, which refutes the window-handle root cause, still holds against the current tree and was not in error. Quoting it: "EndInit creates the WebView2 child window handles, and WinForms creates a parent's handle when a child's handle is created. The ItemViewer's own handle therefore exists the instant construction returns." Re-verified on branch bug/quickfiler-itemviewer-ui-marshalling-seam-743: ItemViewer() calls InitializeComponent() at QuickFiler/Viewers/ItemViewer.cs:25; InitializeComponent runs BeginInit() on both WebView2 children at QuickFiler/Viewers/ItemViewer.Designer.cs:89-90 and EndInit() on them at QuickFiler/Viewers/ItemViewer.Designer.cs:6165 and :6166. One citation correction only: the two earlier comments cite the EndInit() pair as lines 6166-6167; in the current tree the pair sits at lines 6165 and 6166 (line 6169 is the _topicThread EndInit()). The window-handle root cause remains refuted, and forcing the handle remains a measured no-op.

    (b) Forward pointer

    The forward pointer to #592 is stale: #592 is closed and consolidated into #743 (2026-09-02-quickfiler-itemviewer-ui-marshalling-seam-743). #743 carries the resolution: QfcItemController.ResolveControlGroupsAsync is widened from the concrete ItemViewer to IItemViewer through two additive interface members (DescendantControls() and ItemNumberLabel), and the AssignControlsAsync marshal is routed through the injected IUiDispatcher seam with the same null tolerance the other seam sites carry. The member is therefore driven from a deterministic seam test class, QuickFiler.Test/Controllers/QfcItemController.SeamMarshallingTests.cs, with no pump host and no concrete viewer (fail-before three of three runs with InvalidCastException, pass-after three of three, then 62 consecutive targeted runs with zero failures). The retained pump-hosted test ResolveControlGroupsAsync_ThroughThePumpHost_PopulatesTipsAndControlGroups, which #571 exists to stabilize, is unchanged. Evidence lives under docs/features/active/2026-09-02-quickfiler-itemviewer-ui-marshalling-seam-743/evidence/ on that branch.

    (c) The gate hypothesis is superseded

    The UiThreadDispatcherGate / SwapUiThreadDispatcher hypothesis restated in the closing comment above is superseded. Per correction C1 in the #743 spec, those two identifiers exist in zero .cs files in the current tree (they were removed under #493; the mechanism that exists today is UiThreadDispatcherFixture / UiThreadDispatcherTransaction). The mechanism was then identified by measurement rather than inference (AC1 verdict artifact evidence/baseline/ac1-mechanism-verdict.2026-09-12T17-00.md): three monotonic counters on the one-permit TransactionGate recorded, in a serial-regime run of the whole QuickFiler.Test assembly, acquisitions=11 releases=10 contended=0, with a balance test holding the permit and asserting acquisitions minus releases equals exactly 1. A serial run cannot queue a second live holder, so a contended count of zero rejects the gate-leak hypothesis (H-LEAK) by direct observation. The surviving mechanism is elapsed pump-hosted fixture cost under load (H-COST): the six ThroughThePumpHost tests measured 68-125 ms serially and up to 6,460 ms under class-level parallelism alone, and the recorded 6x-26x load multiplier applied to the latter exceeds the 60,000 ms PumpTimeoutMs bound. No expiry was reproduced during the instrumented runs; that is recorded as a negative result, and the identification rests on the counter observable rather than on an observed expiry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions