diff --git a/QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs b/QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs index 92f9bfc91..ccf0620a8 100644 --- a/QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs +++ b/QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs @@ -6,10 +6,10 @@ using System.Threading; using System.Threading.Tasks; using FluentAssertions; -using Microsoft.Extensions.Time.Testing; using Microsoft.Office.Interop.Outlook; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; +using QuickFiler.Test.TestSupport; using UtilitiesCS; using UtilitiesCS.ReusableTypeClasses; @@ -44,21 +44,6 @@ private static void SetPrivateField(object target, string name, object value) field.SetValue(target, value); } - /// - /// Test-side worker whose raises DoWork synchronously on - /// the calling thread through the protected OnDoWork, so the privately subscribed - /// Worker_DoWork runs to its first incomplete await before InitEmailQueue - /// returns. Issue #950: this replaces the bounded waits on a thread-pool worker. - /// - private sealed class SynchronousBackgroundWorker : BackgroundWorker - { - public void RaiseDoWork() => OnDoWork(new DoWorkEventArgs(null)); - } - - /// The synchronous starter assigned to QfcDatamodel.WorkerStarter. - private static void StartSynchronously(BackgroundWorker worker) => - ((SynchronousBackgroundWorker)worker).RaiseDoWork(); - /// /// Queues posted continuations and runs them only on an explicit call, /// on the creating thread. Drain runs only work already queued, plus work that work queues, @@ -105,14 +90,21 @@ private static IApplicationGlobals CreateHighConfidenceGlobals() /// sourceActive signal consumed that dishonest value, so an empty queue was mistaken /// for an exhausted one and the gate returned an early partial batch. The datamodel-owned /// volatile bool flag makes the signal truthful. + /// + /// Issue #968: every step waits on an explicit signal instead of a clock advance followed by + /// a scheduler yield. proves the gate armed its + /// next wait; the dequeue task itself is the completion signal; the production awaits are + /// registered with no synchronization context installed, so the loader's completion clears + /// the flag inline and is read back before the final advance. No retry loop remains. + /// /// [TestMethod] public async Task DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle() { // Arrange var model = CreateUninitializedDatamodel(); - var fake = new FakeTimeProvider(); - model.TimeProvider = fake; + var clock = new ArmingFakeTimeProvider(); + model.TimeProvider = clock; SetPrivateField(model, "_globals", CreateHighConfidenceGlobals()); SetPrivateField(model, "_masterQueue", new LockingLinkedList()); @@ -124,43 +116,59 @@ public async Task DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPolli return await loaderRelease.Task; }; - var worker = new SynchronousBackgroundWorker(); - model.WorkerStarter = StartSynchronously; + using (var worker = new SynchronousBackgroundWorker()) + { + model.WorkerStarter = SynchronousBackgroundWorker.StartSynchronously; + Task> pending; + using (NoSynchronizationContext()) + { + // The issue #244 zero-batch short-circuit is COM-free and starts the worker + // through the issue #950 seam, which raises DoWork on this thread. + model.InitEmailQueue(0, worker); + loaderEntered + .Task.IsCompleted.Should() + .BeTrue( + "the synchronous starter must reach the injected RemainingEmailLoader" + ); + pending = model.DequeueNextItemGroupAsync(1, 200); + } - // Act — the issue #244 zero-batch short-circuit is COM-free and starts the worker - // through the issue #950 seam, which raises DoWork on this thread. - model.InitEmailQueue(0, worker); + clock + .Armed.IsCompleted.Should() + .BeTrue( + "the gate arms its first empty-queue wait before the dequeue call returns" + ); + clock.ReArm(); - loaderEntered - .Task.IsCompleted.Should() - .BeTrue("the synchronous starter must reach the injected RemainingEmailLoader"); - - Task> pending = model.DequeueNextItemGroupAsync(1, 200); - fake.Advance(TimeSpan.FromMilliseconds(200)); - await Task.Yield(); - fake.Advance(TimeSpan.FromMilliseconds(200)); - await Task.Yield(); - - // Assert - pending - .IsCompleted.Should() - .BeFalse( - "the loader is still producing, so the gate must keep polling rather than treat " - + "an empty queue as an exhausted source and return an early partial batch" - ); + // Act — the first wait expires while the loader is still producing. + clock.Advance(TimeSpan.FromMilliseconds(200)); + Task first = await Task.WhenAny(clock.Armed, pending); - // Cleanup — release the loader and let the dequeue drain on the honest signal. - loaderRelease.SetResult(true); - for (int i = 0; i < 20 && !pending.IsCompleted; i++) - { - fake.Advance(TimeSpan.FromMilliseconds(200)); - await Task.Yield(); - } + // Assert + first + .Should() + .BeSameAs( + clock.Armed, + "the loader is still producing, so the gate must arm a second wait rather than " + + "treat an empty queue as an exhausted source and return an early partial batch" + ); + pending.IsCompleted.Should().BeFalse("the gate re-armed instead of returning"); - pending - .IsCompleted.Should() - .BeTrue("once the loader completes, the gate exits on genuine exhaustion"); - (await pending).Should().BeEmpty(); + // Cleanup — complete the loader; with no captured context its continuations run + // inline and clear the flag before this call returns. + using (NoSynchronizationContext()) + { + loaderRelease.SetResult(true); + } + + ReadLivenessFlag(model) + .Should() + .BeFalse("the loader's completion must clear the flag before the next poll"); + clock.Advance(TimeSpan.FromMilliseconds(200)); + (await pending) + .Should() + .BeEmpty("once the loader completes, the gate exits on genuine exhaustion"); + } } /// Reads the issue #424 producer-liveness flag by reflection. @@ -172,15 +180,36 @@ private static bool ReadLivenessFlag(QfcDatamodel model) } /// - /// Starts the worker with a RemainingEmailLoader held open by + /// Issue #968. Clears on the calling thread for the + /// lifetime of the returned scope and restores the previous value on dispose, so every + /// production await registered inside the scope captures no context and its continuation runs + /// inline on the completing thread. The scope body must contain no await: the restore + /// has to run on the same thread that took the scope. + /// + private static IDisposable NoSynchronizationContext() => new SynchronizationContextScope(); + + private sealed class SynchronizationContextScope : IDisposable + { + private readonly SynchronizationContext _previous = SynchronizationContext.Current; + + internal SynchronizationContextScope() => + SynchronizationContext.SetSynchronizationContext(null); + + public void Dispose() => SynchronizationContext.SetSynchronizationContext(_previous); + } + + /// + /// Starts with a RemainingEmailLoader held open by /// . The issue #950 synchronous starter raises DoWork on /// this thread, so by the time InitEmailQueue returns the async void /// Worker_DoWork has entered the loader and returned at its first incomplete await. /// runs its continuations asynchronously, so a test that has /// installed DrainableSynchronizationContext observes the resumed loader only - /// through Drain, never inline inside SetResult. + /// through Drain, never inline inside SetResult. The caller owns and disposes + /// the worker (issue #968, folding issue #972 item 4). /// private static QfcDatamodel StartHeldOpenLoader( + SynchronousBackgroundWorker worker, Func, Task> loaderBody, out TaskCompletionSource release ) @@ -198,8 +227,7 @@ out TaskCompletionSource release return loaderBody(localRelease); }; - var worker = new SynchronousBackgroundWorker(); - model.WorkerStarter = StartSynchronously; + model.WorkerStarter = SynchronousBackgroundWorker.StartSynchronously; model.InitEmailQueue(0, worker); entered @@ -217,20 +245,24 @@ out TaskCompletionSource release [TestMethod] public void RemainingLoadActive_AcrossAsyncVoidFirstAwait_StaysTrueWhileLoaderProduces() { - // Arrange / Act - QfcDatamodel model = StartHeldOpenLoader( - signal => signal.Task, - out TaskCompletionSource release - ); - - // Assert - ReadLivenessFlag(model) - .Should() - .BeTrue( - "the producer is still live even though the async void handler already returned" + using (var worker = new SynchronousBackgroundWorker()) + { + // Arrange / Act + QfcDatamodel model = StartHeldOpenLoader( + worker, + signal => signal.Task, + out TaskCompletionSource release ); - release.SetResult(true); + // Assert + ReadLivenessFlag(model) + .Should() + .BeTrue( + "the producer is still live even though the async void handler already returned" + ); + + release.SetResult(true); + } } /// @@ -246,22 +278,26 @@ public void RemainingLoadActive_AfterLoaderCompletes_BecomesFalse() SynchronizationContext.SetSynchronizationContext(pump); try { - QfcDatamodel model = StartHeldOpenLoader( - signal => signal.Task, - out TaskCompletionSource release - ); - ReadLivenessFlag(model).Should().BeTrue("the loader has not completed yet"); - - // Act - release.SetResult(true); - pump.Drain(); - - // Assert - ReadLivenessFlag(model) - .Should() - .BeFalse( - "the finally around the awaited loader must clear the flag once it completes" + using (var worker = new SynchronousBackgroundWorker()) + { + QfcDatamodel model = StartHeldOpenLoader( + worker, + signal => signal.Task, + out TaskCompletionSource release ); + ReadLivenessFlag(model).Should().BeTrue("the loader has not completed yet"); + + // Act + release.SetResult(true); + pump.Drain(); + + // Assert + ReadLivenessFlag(model) + .Should() + .BeFalse( + "the finally around the awaited loader must clear the flag once it completes" + ); + } } finally { @@ -282,26 +318,30 @@ public void RemainingLoadActive_WhenLoaderThrows_IsStillClearedByFinally() SynchronizationContext.SetSynchronizationContext(pump); try { - QfcDatamodel model = StartHeldOpenLoader( - async signal => - { - await signal.Task; - throw new InvalidOperationException("loader failed"); - }, - out TaskCompletionSource release - ); - ReadLivenessFlag(model).Should().BeTrue("the loader has not failed yet"); - - // Act - release.SetResult(true); - pump.Drain(); - - // Assert - ReadLivenessFlag(model) - .Should() - .BeFalse( - "the finally must clear the flag on the throwing path too, or the gate would poll forever" + using (var worker = new SynchronousBackgroundWorker()) + { + QfcDatamodel model = StartHeldOpenLoader( + worker, + async signal => + { + await signal.Task; + throw new InvalidOperationException("loader failed"); + }, + out TaskCompletionSource release ); + ReadLivenessFlag(model).Should().BeTrue("the loader has not failed yet"); + + // Act + release.SetResult(true); + pump.Drain(); + + // Assert + ReadLivenessFlag(model) + .Should() + .BeFalse( + "the finally must clear the flag on the throwing path too, or the gate would poll forever" + ); + } } finally { diff --git a/QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs b/QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs index 3f12c756f..3ec3ae67c 100644 --- a/QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs +++ b/QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs @@ -10,6 +10,7 @@ using Microsoft.Office.Interop.Outlook; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; +using QuickFiler.Test.TestSupport; using UtilitiesCS; using UtilitiesCS.ReusableTypeClasses; @@ -56,22 +57,6 @@ private static object GetPrivateField(object target, string name) return field.GetValue(target); } - /// - /// Test-side worker whose raises DoWork synchronously on - /// the calling thread through the protected OnDoWork, so the privately subscribed - /// Worker_DoWork runs to its first incomplete await before InitEmailQueue - /// returns (issue #950). Duplicated per file, following the convention documented on - /// QfcDatamodelLivenessTests. - /// - private sealed class SynchronousBackgroundWorker : BackgroundWorker - { - public void RaiseDoWork() => OnDoWork(new DoWorkEventArgs(null)); - } - - /// The synchronous starter assigned to QfcDatamodel.WorkerStarter. - private static void StartSynchronously(BackgroundWorker worker) => - ((SynchronousBackgroundWorker)worker).RaiseDoWork(); - /// /// AC2, the reported crash. Once Cleanup() has nulled _masterQueue and /// _moveMonitor, the still-running loader reached this method and constructed @@ -219,7 +204,7 @@ public void Worker_DoWork_CapturesRemainingLoadTask() using (var worker = new SynchronousBackgroundWorker()) { - model.WorkerStarter = StartSynchronously; + model.WorkerStarter = SynchronousBackgroundWorker.StartSynchronously; // Act — the issue #244 zero-batch short-circuit is COM-free and starts the worker // through the issue #950 seam, which raises DoWork on this thread, so diff --git a/QuickFiler.Test/Controllers/QfcDatamodelTests.cs b/QuickFiler.Test/Controllers/QfcDatamodelTests.cs index e04e88357..436615d6e 100644 --- a/QuickFiler.Test/Controllers/QfcDatamodelTests.cs +++ b/QuickFiler.Test/Controllers/QfcDatamodelTests.cs @@ -10,6 +10,7 @@ using Microsoft.Office.Interop.Outlook; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; +using QuickFiler.Test.TestSupport; using UtilitiesCS; using UtilitiesCS.ReusableTypeClasses; @@ -92,12 +93,19 @@ public void QfcRemainingQueueAdmission_DeclaresNoScoringDelegate() ); } + /// + /// Issue #424: the high-confidence dequeue keeps polling while the datamodel-owned liveness + /// flag is true. Issue #968: the re-arm is proved through + /// instead of a clock advance followed by + /// a scheduler yield, and the dequeue task itself is the completion signal. + /// [TestMethod] public async Task DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive() { + // Arrange var model = CreateUninitializedDatamodel(); - var fake = new FakeTimeProvider(); - model.TimeProvider = fake; + var clock = new ArmingFakeTimeProvider(); + model.TimeProvider = clock; var settings = new Mock(MockBehavior.Strict); settings.SetupGet(x => x.HighConfidenceModeEnabled).Returns(true); @@ -105,29 +113,42 @@ public async Task DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceW var globals = new Mock(MockBehavior.Strict); globals.SetupGet(x => x.QfSettings).Returns(settings.Object); - var worker = new BackgroundWorker(); - SetPrivateField(model, "_globals", globals.Object); - SetPrivateField(model, "_worker", worker); - SetPrivateField(model, "_masterQueue", new LockingLinkedList()); - // Issue #424: the source-active signal is the datamodel-owned liveness flag, not - // BackgroundWorker.isRunning, which is dishonest for an async void DoWork handler. - SetPrivateField(model, "_remainingLoadActive", true); - - Task> pending = model.DequeueNextItemGroupAsync(1, 200); - - fake.Advance(TimeSpan.FromMilliseconds(200)); - await Task.Yield(); - pending - .IsCompleted.Should() - .BeFalse( - "the datamodel source-active signal must keep polling while the worker can still add candidates" - ); - - SetPrivateField(model, "_remainingLoadActive", false); - fake.Advance(TimeSpan.FromMilliseconds(200)); - IList result = await pending; - - result.Should().BeEmpty(); + using (var worker = new BackgroundWorker()) + { + SetPrivateField(model, "_globals", globals.Object); + SetPrivateField(model, "_worker", worker); + SetPrivateField(model, "_masterQueue", new LockingLinkedList()); + // Issue #424: the source-active signal is the datamodel-owned liveness flag, not + // BackgroundWorker.isRunning, which is dishonest for an async void DoWork handler. + SetPrivateField(model, "_remainingLoadActive", true); + + Task> pending = model.DequeueNextItemGroupAsync(1, 200); + clock + .Armed.IsCompleted.Should() + .BeTrue( + "the gate arms its first empty-queue wait before the dequeue call returns" + ); + clock.ReArm(); + + // Act — the first wait expires while the source is still active. + clock.Advance(TimeSpan.FromMilliseconds(200)); + Task first = await Task.WhenAny(clock.Armed, pending); + + // Assert + first + .Should() + .BeSameAs( + clock.Armed, + "the datamodel source-active signal must keep polling while the worker can still add candidates" + ); + pending.IsCompleted.Should().BeFalse("the gate re-armed instead of returning"); + + SetPrivateField(model, "_remainingLoadActive", false); + clock.Advance(TimeSpan.FromMilliseconds(200)); + IList result = await pending; + + result.Should().BeEmpty(); + } } [TestMethod] @@ -258,28 +279,30 @@ public async Task WaitForQueue_WhenWorkerBusyAndQueueShort_AwaitsInjectedTwoHund var fake = new FakeTimeProvider(); model.TimeProvider = fake; - var worker = new BackgroundWorker(); - SetPrivateField(model, "_worker", worker); - SetPrivateField(model, "_masterQueue", new LockingLinkedList()); // Count == 0 - // Issue #424: WaitForQueue now loops on the datamodel-owned producer-liveness flag - // instead of BackgroundWorker.IsBusy, so the loop is driven through that flag. - SetPrivateField(model, "_remainingLoadActive", true); - - var method = typeof(QfcDatamodel).GetMethod("WaitForQueue", NonPublicInstance); - - // Act - var task = (Task)method.Invoke(model, new object[] { 1, CancellationToken.None }); - - // Assert — loop is parked on the injected delay until advanced. - task.IsCompleted.Should() - .BeFalse("WaitForQueue must await the injected 200 ms delay, not wall-clock"); - - // Release the loop: the producer goes idle, then advancing the clock completes the delay - // so the loop re-checks its condition and exits. - SetPrivateField(model, "_remainingLoadActive", false); - fake.Advance(TimeSpan.FromMilliseconds(200)); - await task; - task.IsCompleted.Should().BeTrue(); + using (var worker = new BackgroundWorker()) + { + SetPrivateField(model, "_worker", worker); + SetPrivateField(model, "_masterQueue", new LockingLinkedList()); // Count == 0 + // Issue #424: WaitForQueue now loops on the datamodel-owned producer-liveness flag + // instead of BackgroundWorker.IsBusy, so the loop is driven through that flag. + SetPrivateField(model, "_remainingLoadActive", true); + + var method = typeof(QfcDatamodel).GetMethod("WaitForQueue", NonPublicInstance); + + // Act + var task = (Task)method.Invoke(model, new object[] { 1, CancellationToken.None }); + + // Assert — loop is parked on the injected delay until advanced. + task.IsCompleted.Should() + .BeFalse("WaitForQueue must await the injected 200 ms delay, not wall-clock"); + + // Release the loop: the producer goes idle, then advancing the clock completes the delay + // so the loop re-checks its condition and exits. + SetPrivateField(model, "_remainingLoadActive", false); + fake.Advance(TimeSpan.FromMilliseconds(200)); + await task; + task.IsCompleted.Should().BeTrue(); + } } #endregion Issue #222 — Injectable time/delay seam diff --git a/QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs b/QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs index d87ce6ec8..b87a4fe32 100644 --- a/QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs +++ b/QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs @@ -10,6 +10,7 @@ using Microsoft.Office.Interop.Outlook; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; +using QuickFiler.Test.TestSupport; namespace QuickFiler.Controllers.Tests { @@ -30,8 +31,8 @@ namespace QuickFiler.Controllers.Tests /// (_olApp.GetNamespace("MAPI")) — this is the maintainer-reported defect in the v1.0 /// revision of these tests, and this file must never reproduce it. Issue #950: every test also /// assigns the WorkerStarter seam a starter that raises DoWork synchronously on - /// the test thread through the nested SynchronousBackgroundWorker, so no test starts a - /// thread-pool worker and none outlives the test. + /// the test thread through the shared SynchronousBackgroundWorker test-support helper + /// (issue #968 consolidated the per-file copies), so no started worker outlives its test. /// [TestClass] public class QfcInitEmailQueueZeroBatchTests @@ -94,9 +95,9 @@ private static Frame CreateTwoRowEmailFrame() /// Builds an inert replacement that records /// invocation via and returns a completed true result without /// ever touching or Outlook COM (_olApp). - /// Assigning this delegate before starting a real is what makes - /// it safe to call with a real - /// worker in a unit test. + /// Assigning this delegate before the synchronous test worker is started is what makes it + /// safe to call in a unit + /// test (issue #950: the worker raises DoWork on the test thread). /// private static Func> CreateInertRemainingEmailLoader( out TaskCompletionSource invoked @@ -111,21 +112,6 @@ out TaskCompletionSource invoked }; } - /// - /// Test-side worker whose raises DoWork synchronously on - /// the calling thread through the protected OnDoWork, so no worker started by - /// InitEmailQueue outlives the test (issue #950). Duplicated per file, following - /// the convention documented on QfcDatamodelLivenessTests. - /// - private sealed class SynchronousBackgroundWorker : BackgroundWorker - { - public void RaiseDoWork() => OnDoWork(new DoWorkEventArgs(null)); - } - - /// The synchronous starter assigned to QfcDatamodel.WorkerStarter. - private static void StartSynchronously(BackgroundWorker worker) => - ((SynchronousBackgroundWorker)worker).RaiseDoWork(); - /// /// Issue #244 AC1: a zero batch size must not throw the Deedle "The interface member /// 'EntryId' does not exist in the column index." exception, and must return an empty, @@ -140,17 +126,19 @@ public void InitEmailQueue_ZeroBatchSize_ReturnsEmptyListWithoutThrowing() var model = CreateUninitializedDatamodel(); SetPrivateField(model, "_frame", CreateTwoRowEmailFrame()); model.RemainingEmailLoader = CreateInertRemainingEmailLoader(out _); - model.WorkerStarter = StartSynchronously; + model.WorkerStarter = SynchronousBackgroundWorker.StartSynchronously; IList result = null; - // Act - System.Action act = () => - result = model.InitEmailQueue(0, new SynchronousBackgroundWorker()); - - // Assert - act.Should().NotThrow(); - result.Should().NotBeNull(); - result.Should().BeEmpty(); + using (var worker = new SynchronousBackgroundWorker()) + { + // Act + System.Action act = () => result = model.InitEmailQueue(0, worker); + + // Assert + act.Should().NotThrow(); + result.Should().NotBeNull(); + result.Should().BeEmpty(); + } } /// @@ -169,17 +157,20 @@ public void InitEmailQueue_ZeroBatchSize_StillStartsBackgroundWorker() var model = CreateUninitializedDatamodel(); SetPrivateField(model, "_frame", CreateTwoRowEmailFrame()); model.RemainingEmailLoader = CreateInertRemainingEmailLoader(out var loaderInvokedTcs); - model.WorkerStarter = StartSynchronously; - var worker = new SynchronousBackgroundWorker(); - - // Act - model.InitEmailQueue(0, worker); - - // Assert - worker.WorkerSupportsCancellation.Should().BeTrue(); - loaderInvokedTcs - .Task.IsCompleted.Should() - .BeTrue("the injected RemainingEmailLoader must be invoked by the started worker"); + model.WorkerStarter = SynchronousBackgroundWorker.StartSynchronously; + using (var worker = new SynchronousBackgroundWorker()) + { + // Act + model.InitEmailQueue(0, worker); + + // Assert + worker.WorkerSupportsCancellation.Should().BeTrue(); + loaderInvokedTcs + .Task.IsCompleted.Should() + .BeTrue( + "the injected RemainingEmailLoader must be invoked by the started worker" + ); + } } /// @@ -199,7 +190,7 @@ public void InitEmailQueue_PositiveBatchSize_RetainsExistingProjectionAndFrameDr var model = CreateUninitializedDatamodel(); SetPrivateField(model, "_frame", CreateTwoRowEmailFrame()); model.RemainingEmailLoader = CreateInertRemainingEmailLoader(out _); - model.WorkerStarter = StartSynchronously; + model.WorkerStarter = SynchronousBackgroundWorker.StartSynchronously; var mailItemsByEntryId = new Dictionary { @@ -217,16 +208,19 @@ public void InitEmailQueue_PositiveBatchSize_RetainsExistingProjectionAndFrameDr SetPrivateField(model, "_olApp", application.Object); - // Act - var result = model.InitEmailQueue(2, new SynchronousBackgroundWorker()); + using (var worker = new SynchronousBackgroundWorker()) + { + // Act + var result = model.InitEmailQueue(2, worker); - // Assert - result.Should().HaveCount(2); - result.Should().BeEquivalentTo(mailItemsByEntryId.Values); + // Assert + result.Should().HaveCount(2); + result.Should().BeEquivalentTo(mailItemsByEntryId.Values); - var frameField = typeof(QfcDatamodel).GetField("_frame", NonPublicInstance); - var frame = (Frame)frameField.GetValue(model); - frame.RowCount.Should().Be(0); + var frameField = typeof(QfcDatamodel).GetField("_frame", NonPublicInstance); + var frame = (Frame)frameField.GetValue(model); + frame.RowCount.Should().Be(0); + } } } } diff --git a/QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs b/QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs index 77c4e7097..6da46ccab 100644 --- a/QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs +++ b/QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs @@ -96,24 +96,6 @@ private static FocusController BuildFocusController() return controller; } - private static Mock BuildExecutingViewer() - { - var viewer = new Mock(); - viewer - .Setup(v => v.Invoke(It.IsAny())) - .Returns((Delegate d) => d.DynamicInvoke()); - viewer - .Setup(v => v.BeginInvoke(It.IsAny())) - .Returns( - (Delegate d) => - { - d.DynamicInvoke(); - return Mock.Of(); - } - ); - return viewer; - } - /// /// Cycle-4 remediation (R1): reflection-injects handle-less doubles for every private field /// touched by (Theme.cs:414-432) and the recursive @@ -179,18 +161,19 @@ private static void SetThemeFieldViaActivator(Theme theme, string name) // ------------------------- ToggleFocus / ToggleFocus(ToggleState) ------------------------- // Cycle-3 P9-T5/P9-T6 (members #33/#35, de-exempted); cycle-4 remediation R1: the entire body - // runs inside a single _itemViewer.Invoke(...) delegate. BuildExecutingViewer() executes the - // delegate synchronously and EnableHandlelessThemeInvoke() populates the terminal - // _themes[_activeTheme].SetQfcTheme(async: false) call's dependencies with handle-less doubles, - // so these tests exercise the full method body (the _activeUI/_activeTheme state machine) and - // assert the resulting state transitions, not merely the Invoke marshal. + // runs inside a single _itemViewer.Invoke(...) delegate. The shared + // QfcItemControllerTestSupport.BuildExecutingViewer() executes the delegate synchronously + // (issue #968 removed this file's private copy) and EnableHandlelessThemeInvoke() populates + // the terminal _themes[_activeTheme].SetQfcTheme(async: false) call's dependencies with + // handle-less doubles, so these tests exercise the full method body (the _activeUI/_activeTheme + // state machine) and assert the resulting state transitions, not merely the Invoke marshal. [TestMethod] public void ToggleFocus_StateOverload_MarshalsThroughItemViewerInvoke() { // Arrange — _tableLayoutPanels (QfcItemController's own field, distinct from Theme's field // of the same name) is dereferenced by ToggleTips inside the executed delegate body. - var viewer = BuildExecutingViewer(); + var viewer = QfcItemControllerTestSupport.BuildExecutingViewer(); var controller = BuildFocusController(); SetField(controller, "_itemViewer", viewer.Object); SetField(controller, "_tableLayoutPanels", new List()); @@ -210,7 +193,7 @@ public void ToggleFocus_StateOverload_MarshalsThroughItemViewerInvoke() public void ToggleFocus_StateOverload_Off_FromActive_DeactivatesUiAndSwitchesToNormalTheme() { // Arrange - var viewer = BuildExecutingViewer(); + var viewer = QfcItemControllerTestSupport.BuildExecutingViewer(); var controller = BuildFocusController(); SetField(controller, "_itemViewer", viewer.Object); SetField(controller, "_activeUI", true); @@ -232,7 +215,7 @@ public void ToggleFocus_ParameterlessOverload_MarshalsThroughItemViewerInvoke() { // Arrange — BuildFocusController() leaves _activeUI at its default false, so this reaches // the inactive->active branch. - var viewer = BuildExecutingViewer(); + var viewer = QfcItemControllerTestSupport.BuildExecutingViewer(); var controller = BuildFocusController(); SetField(controller, "_itemViewer", viewer.Object); SetField(controller, "_tableLayoutPanels", new List()); @@ -251,7 +234,7 @@ public void ToggleFocus_ParameterlessOverload_MarshalsThroughItemViewerInvoke() public void ToggleFocus_ParameterlessOverload_FromActive_DeactivatesUiAndSwitchesToNormalTheme() { // Arrange - var viewer = BuildExecutingViewer(); + var viewer = QfcItemControllerTestSupport.BuildExecutingViewer(); var controller = BuildFocusController(); SetField(controller, "_itemViewer", viewer.Object); SetField(controller, "_activeUI", true); @@ -311,7 +294,7 @@ public void ToggleNavigation_Synchronous_TogglesPositionTips() { // Arrange var tips = new Mock(); - var viewer = BuildExecutingViewer(); + var viewer = QfcItemControllerTestSupport.BuildExecutingViewer(); var controller = new FocusController(); SetField(controller, "_itemPositionTips", tips.Object); SetField(controller, "_itemViewer", viewer.Object); @@ -328,7 +311,7 @@ public void ToggleNavigation_WithState_TogglesPositionTipsWithState() { // Arrange var tips = new Mock(); - var viewer = BuildExecutingViewer(); + var viewer = QfcItemControllerTestSupport.BuildExecutingViewer(); var controller = new FocusController(); SetField(controller, "_itemPositionTips", tips.Object); SetField(controller, "_itemViewer", viewer.Object); @@ -364,7 +347,7 @@ public void ToggleTips_Synchronous_DispatchesAndExecutesDelegate() { // Arrange — an executing viewer runs the dispatched delegate; empty tips/panels collections // keep the executed body free of live-control work so the tips-toggle logic is exercised. - var viewer = BuildExecutingViewer(); + var viewer = QfcItemControllerTestSupport.BuildExecutingViewer(); var controller = new FocusController(); SetField(controller, "_itemViewer", viewer.Object); SetField(controller, "_listTipsDetails", new List()); @@ -447,9 +430,11 @@ public void ToggleSaveAttachments_DoesNotThrow() [TestMethod] public void SetThemeDark_FromNormal_SelectsDarkNormalTheme() { - // Arrange — async:true queues the theme application on the dispatcher without executing it, - // so no handle-less control is touched; the observable effect is the active-theme switch. - QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + // Arrange — async:true queues the theme application through the theme's injected + // IUiDispatcher mock (see BuildColorTheme), which absorbs the delegate without running it, + // so no handle-less control is touched and the shared UiThread static is irrelevant to this + // path (issue #968 deleted the former ensure call); the observable effect is the + // active-theme switch. var controller = new FocusController(); SetField(controller, "_themes", BuildAllThemes()); SetField(controller, "_activeTheme", null); @@ -464,8 +449,8 @@ public void SetThemeDark_FromNormal_SelectsDarkNormalTheme() [TestMethod] public void SetThemeLight_FromNormal_SelectsLightNormalTheme() { - // Arrange - QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + // Arrange — same injected-mock arrangement as SetThemeDark_FromNormal_SelectsDarkNormalTheme: + // the theme's IUiDispatcher mock absorbs the queued application (issue #968). var controller = new FocusController(); SetField(controller, "_themes", BuildAllThemes()); SetField(controller, "_activeTheme", null); diff --git a/QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs b/QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs index 3d2603b46..ebe8f5ff0 100644 --- a/QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs +++ b/QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs @@ -214,25 +214,27 @@ internal static Theme BuildDispatchableTheme(IUiDispatcher dispatcher) } /// - /// Ensures the static UiThread.Dispatcher is non-null by seeding it (only when unset) - /// with a dedicated dispatcher hosted on a parked background thread that is never pumped. - /// Needed for members that still delegate to a callee using the static - /// UiThread.Dispatcher before the Phase 6 IUiDispatcher seam replaces it. + /// Takes one reference-counted pin on the shared static UiThread.Dispatcher through + /// (issue #968): the first pin on a + /// null field seeds a dedicated dispatcher hosted on a parked background thread that is + /// never pumped, later pins install nothing, and the field reverts to null only when + /// the last live pin releases and the fixture itself seeded it. The remaining legitimate + /// callers are the fixture tests QfcItemController_UiThreadDispatcherFixtureTests and + /// QfcItemController_UiThreadDispatcherPinCountTests, each of which acquires and + /// releases its pin while holding a . /// /// A dedicated (non-CurrentDispatcher) instance is used deliberately for test - /// isolation: fire-and-forget BeginInvoke/InvokeAsync operations posted by these - /// tests are enqueued on the parked dispatcher and never execute, so they cannot leak onto the - /// test thread's own dispatcher and be run (and fault on a handle-less control) by an unrelated - /// later test that pumps Dispatcher.CurrentDispatcher. Becomes moot once the callee - /// routes through the injectable dispatcher seam. + /// isolation: fire-and-forget BeginInvoke/InvokeAsync operations posted to the + /// parked dispatcher are enqueued and never execute, so they cannot leak onto the test + /// thread's own dispatcher and be run (and fault on a handle-less control) by an unrelated + /// later test that pumps Dispatcher.CurrentDispatcher. /// /// - /// The returned value is a scope whose Dispose conditionally reverts the seeding: it - /// writes null back only when the static still holds the exact instance this call - /// installed, and a call that installed nothing returns a no-op scope. Discarding the scope is - /// permitted and leaks exactly as the pre-issue-#493 void helper did, no more. The - /// implementation lives in , which is the single owner - /// of every mutation of that static made from this assembly's owned files. + /// Dispose the returned scope inside the same transaction that was held when it was taken: a + /// discarded scope pins for the process lifetime, and a pin released outside its caller's + /// transaction is released while another class may hold the gate. The implementation lives + /// in , the single owner of every mutation of that + /// static made from this assembly's owned files. /// /// internal static IDisposable EnsureUiThreadDispatcher() => @@ -282,9 +284,9 @@ internal static void ShutdownDispatcher(Dispatcher dispatcher) /// /// Issue #480 shared arrange helper. Builds a viewer mock whose Invoke and /// BeginInvoke execute the supplied delegate synchronously, so a dispatch made through - /// either path produces a countable call on whatever collaborator the delegate targets. Mirrors - /// the private static BuildExecutingViewer() in - /// QfcItemController.FocusAndThemeTests.cs, which is not reachable from another test file. + /// either path produces a countable call on whatever collaborator the delegate targets. Since + /// issue #968 this is the single implementation; QfcItemController.FocusAndThemeTests.cs calls + /// it instead of carrying a private copy. /// internal static Mock BuildExecutingViewer() { diff --git a/QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs b/QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs index 1eaa87064..3b7f9e1f1 100644 --- a/QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs +++ b/QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs @@ -28,6 +28,19 @@ namespace QuickFiler.Controllers.Tests /// that carry no [Timeout], so making them wait on a gate another test class holds for a /// whole test body would convert a bounded failure elsewhere into an unbounded hang there. /// + /// + /// Issue #968: ensure pins are reference counted. A pin counter and an install-ownership flag + /// live under FieldLock. The first pin on a null field seeds the parked dispatcher + /// and sets the flag; the last release writes null back only when the flag is set and the + /// field still holds the parked instance, then clears the flag. A discarded scope therefore + /// pins for the process lifetime and leaves the parked dispatcher installed, so every caller + /// disposes its scope, and every pin is acquired and released while its caller holds a + /// transaction, which keeps the count at zero whenever a transaction is acquired. Residual: a + /// transaction that installs over a pinned parked value and restores it after the last pin + /// released leaves the parked value installed with zero pins and the flag set; the next pin + /// cycle reverts it. No test in this assembly installs over a pinned value, so the residual is + /// documented rather than exercised. + /// /// internal static class UiThreadDispatcherFixture { @@ -37,6 +50,11 @@ internal static class UiThreadDispatcherFixture private static readonly FieldInfo DispatcherField = ResolveDispatcherField(); private static Dispatcher _parkedDispatcher = null; + // Issue #968: the count of live ensure scopes and whether the fixture itself seeded the parked + // dispatcher into a null field. Both are read and written only while FieldLock is held. + private static int _pinCount; + private static bool _fixtureInstalledParked; + // Issue #743 AC1 observable: three monotonic counters over TransactionGate. A contended // acquisition is one that observed CurrentCount == 0 immediately before waiting. In a serial // run no live holder can exist when a test begins its transaction, so a non-zero contended @@ -114,10 +132,13 @@ internal static void ReleaseTransactionGate() } /// - /// Seeds the static with the parked dispatcher only when it is currently null, and - /// returns a scope whose Dispose conditionally reverts that seeding. Never acquires - /// TransactionGate and never blocks on anything a caller must release. Disposing the - /// returned scope is optional: a discarded scope leaks exactly as the pre-fix helper did. + /// Takes one counted pin on the shared static (issue #968). The first pin on a null + /// field seeds the parked dispatcher and records that the fixture owns the seeding; a pin + /// taken while the field is non-null installs nothing. Disposing the returned scope releases + /// the pin, and the field reverts to null only on the last release, only when the + /// fixture owns the seeding, and only when the field still holds the parked instance. Never + /// acquires TransactionGate and never blocks on anything a caller must release. A + /// discarded scope pins for the process lifetime, so every caller disposes its scope. /// internal static IDisposable EnsureDispatcher() { @@ -127,14 +148,15 @@ internal static IDisposable EnsureDispatcher() lock (FieldLock) { + _pinCount++; if (DispatcherField.GetValue(null) == null) { DispatcherField.SetValue(null, parked); - return new EnsureScope(parked); + _fixtureInstalledParked = true; } } - return new EnsureScope(null); + return new EnsureScope(parked); } /// @@ -241,19 +263,21 @@ private static Dispatcher GetParkedDispatcher() } /// - /// The scope returned by . Reverts the seeding only when the - /// static still holds the exact instance this scope installed. A scope that installed nothing - /// carries null and is a no-op, which is what keeps a discarded scope from clobbering a - /// value some other owner installed in the meantime. + /// The scope returned by : one counted pin. Disposal is + /// idempotent and performs the decrement and the conditional revert inline in one + /// FieldLock critical section, so no other pin can interleave between them. The revert + /// writes null only when this release brings the count to zero, the fixture itself + /// seeded the parked dispatcher, and the field still holds that instance; a value some other + /// owner installed in the meantime is left in place. /// private sealed class EnsureScope : IDisposable { - private readonly Dispatcher _installed; + private readonly Dispatcher _parked; private bool _disposed = false; - internal EnsureScope(Dispatcher installed) + internal EnsureScope(Dispatcher parked) { - _installed = installed; + _parked = parked; _disposed = false; } @@ -266,9 +290,18 @@ public void Dispose() _disposed = true; - if (_installed != null) + lock (FieldLock) { - UiThreadDispatcherFixture.CompareExchange(_installed, null); + _pinCount--; + if ( + _pinCount == 0 + && _fixtureInstalledParked + && ReferenceEquals(DispatcherField.GetValue(null), _parked) + ) + { + DispatcherField.SetValue(null, null); + _fixtureInstalledParked = false; + } } } } diff --git a/QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs b/QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs index 8a19b1645..0aecd13aa 100644 --- a/QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs +++ b/QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs @@ -194,17 +194,17 @@ public async Task EnsureDispatcher_ScopeDisposedTwice_IsIdempotent() /// observes the pre-install value on acquisition, never the first transaction's installed /// value, because restore strictly precedes gate release. /// - /// Issue #950: the earlier intermittent failure was a race on the shared static, not a - /// timing defect. The gate-free fixture method EnsureDispatcher seeds the parked dispatcher - /// whenever the field is null, so a concurrently running class that calls it could write - /// between the baseline read and the install, or between the restore and the second - /// caller's read. The test therefore pins a non-null baseline with an ensure scope that it - /// opens only after transaction A has acquired the gate and holds through both assertions. - /// Taking the pin inside the gate means that a gated transaction from another class (W3/W4) - /// cannot restore a null previous value between the pin and this test's acquisition. - /// Invariant for future editors: no other class may dispose an ensure scope holding the - /// parked dispatcher (W2), and UiThread.Initialize (W5) must not latch during this test; - /// either would change the value the second caller observes. + /// Issue #950 traced the earlier intermittent failure to a race on the shared static, not to + /// a timing defect: a gate-free ensure call in another class could seed the parked dispatcher + /// between the baseline read and the install, or between the restore and the second caller's + /// read, and the test pinned a non-null baseline inside the gate to fence it. Issue #968 + /// removed that pin: the fixture now counts pins, so only the last release can revert the + /// fixture's own seeding, and every remaining ensure call is acquired and released while its + /// caller holds a transaction, so no class can write the field while transaction A holds the + /// gate and every other transaction restores before it releases (W3/W4). Invariant for future + /// editors: a pin must stay nested inside its caller's transaction, and UiThread.Initialize + /// (W5) must not latch during this test; either would change the value the second caller + /// observes. /// /// [TestMethod] @@ -218,9 +218,7 @@ public async Task Transaction_SecondCallerCannotInstallUntilTheFirstRestores() UiThreadDispatcherTransaction transactionA = await UiThreadDispatcherFixture .BeginTransactionAsync() .ConfigureAwait(false); - using ( - IDisposable baseline = QfcItemControllerTestSupport.EnsureUiThreadDispatcher() - ) + try { Dispatcher original = UiThreadDispatcherFixture.Current; transactionA.Install(liveA); @@ -268,6 +266,10 @@ await UiThreadDispatcherFixture ); } } + finally + { + transactionA.Dispose(); + } } finally { diff --git a/QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs b/QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs new file mode 100644 index 000000000..0d92b05bf --- /dev/null +++ b/QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs @@ -0,0 +1,248 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Threading; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace QuickFiler.Controllers.Tests +{ + /// + /// Issue #968 tests for the reference-counted ensure pin of + /// . The regression lives at the fixture level because + /// the theme tests the issue was filed against never read the shared static: their path + /// dispatches through the theme's injected IUiDispatcher mock, so no value in the static + /// can make a theme test fail, while the defect (the installing pin's release writing + /// null while another pin is still live) is observable here on one thread with no + /// concurrency. + /// + /// Every test acquires a transaction first, so the pin count is zero and the baseline is known + /// for the whole test, and carries the 60-second MSTest timeout of the sibling fixture test + /// file. No sleep, delay, wall-clock wait, mock or temporary file is used; Moq is therefore + /// not imported. + /// + /// + [TestClass] + public class QfcItemController_UiThreadDispatcherPinCountTests + { + private const int GateTimeoutMs = 60000; + + /// + /// Regression test: fails before the fix. With two pins held on a null baseline, releasing + /// the first pin must leave the parked dispatcher in place. Before counting, the first pin + /// was the installer and its release wrote null while the second pin was still live. + /// + [TestMethod] + [Timeout(GateTimeoutMs)] + public async Task EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease() + { + // Arrange + UiThreadDispatcherTransaction transaction = await UiThreadDispatcherFixture + .BeginTransactionAsync() + .ConfigureAwait(false); + try + { + transaction.Install(null); + IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + Dispatcher afterBothPins = UiThreadDispatcherFixture.Current; + + // Act + pinA.Dispose(); + Dispatcher afterFirstRelease = UiThreadDispatcherFixture.Current; + pinB.Dispose(); + Dispatcher afterLastRelease = UiThreadDispatcherFixture.Current; + + // Assert + afterBothPins + .Should() + .NotBeNull( + because: "the first pin seeds the parked dispatcher into a null field" + ); + afterFirstRelease + .Should() + .BeSameAs( + afterBothPins, + because: "a holder that did not take the last pin must not lose the dispatcher" + ); + afterLastRelease + .Should() + .BeNull(because: "the last release reverts the fixture's own seeding"); + } + finally + { + transaction.Dispose(); + } + } + + /// + /// Specification test: passes before and after the fix. Releasing the pins in the reverse + /// order must produce the same outcome, so the count rather than the identity of the + /// installing scope decides when the field reverts. + /// + [TestMethod] + [Timeout(GateTimeoutMs)] + public async Task EnsureDispatcher_TwoPinsHeld_ReleaseOrderDoesNotChangeTheOutcome() + { + // Arrange + UiThreadDispatcherTransaction transaction = await UiThreadDispatcherFixture + .BeginTransactionAsync() + .ConfigureAwait(false); + try + { + transaction.Install(null); + IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + Dispatcher afterBothPins = UiThreadDispatcherFixture.Current; + + // Act + pinB.Dispose(); + Dispatcher afterFirstRelease = UiThreadDispatcherFixture.Current; + pinA.Dispose(); + Dispatcher afterLastRelease = UiThreadDispatcherFixture.Current; + + // Assert + afterBothPins + .Should() + .NotBeNull( + because: "the first pin seeds the parked dispatcher into a null field" + ); + afterFirstRelease + .Should() + .BeSameAs( + afterBothPins, + because: "a holder that did not take the last pin must not lose the dispatcher" + ); + afterLastRelease + .Should() + .BeNull(because: "the last release reverts the fixture's own seeding"); + } + finally + { + transaction.Dispose(); + } + } + + /// + /// Specification test: passes before and after the fix; extends the existing fixture test + /// EnsureDispatcher_WhileATransactionHoldsALiveDispatcher_DoesNotReplaceIt. While a + /// transaction holds a live dispatcher, two pins install nothing, and releasing both must + /// leave the live dispatcher in place: the count reaching zero writes nothing because the + /// fixture did not seed the field. The live dispatcher is shut down in a finally block. + /// + [TestMethod] + [Timeout(GateTimeoutMs)] + public async Task EnsureDispatcher_UnderATransactionHoldingALiveDispatcher_ReleasingAllPinsLeavesTheLiveDispatcher() + { + // Arrange + Dispatcher live = QfcItemControllerTestSupport.StartRunningDispatcher(); + try + { + UiThreadDispatcherTransaction transaction = await UiThreadDispatcherFixture + .BeginTransactionAsync() + .ConfigureAwait(false); + try + { + transaction.Install(live); + IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + + // Act + pinA.Dispose(); + pinB.Dispose(); + Dispatcher afterAllReleased = UiThreadDispatcherFixture.Current; + + // Assert + afterAllReleased + .Should() + .BeSameAs( + live, + because: "pins that installed nothing must not write over the transaction value " + + "when the count reaches zero" + ); + } + finally + { + transaction.Dispose(); + } + } + finally + { + QfcItemControllerTestSupport.ShutdownDispatcher(live); + } + } + + /// + /// Specification test: passes before and after the fix. After a full two-pin cycle inside the + /// same transaction, a fresh single pin on the null baseline must still seed the parked + /// dispatcher and its release must still restore null. A second transaction then installs + /// that parked instance as its own value, and a pin taken and released under it must leave + /// the value in place: had the earlier cycle's last release left the install-ownership flag + /// set, this release would revert a value the fixture did not seed. + /// + [TestMethod] + [Timeout(GateTimeoutMs)] + public async Task EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores() + { + // Arrange + Dispatcher parked; + UiThreadDispatcherTransaction transaction = await UiThreadDispatcherFixture + .BeginTransactionAsync() + .ConfigureAwait(false); + try + { + transaction.Install(null); + IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + parked = UiThreadDispatcherFixture.Current; + pinA.Dispose(); + pinB.Dispose(); + + // Act + IDisposable freshPin = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + Dispatcher afterFreshPin = UiThreadDispatcherFixture.Current; + freshPin.Dispose(); + Dispatcher afterFreshRelease = UiThreadDispatcherFixture.Current; + + // Assert + afterFreshPin + .Should() + .NotBeNull( + because: "a pin on a null field seeds the parked dispatcher whatever earlier cycles did" + ); + afterFreshRelease + .Should() + .BeNull( + because: "the fresh pin is the only live pin, so its release reverts the seeding" + ); + } + finally + { + transaction.Dispose(); + } + + // Act (second transaction): the parked instance is now a transaction value, not a seeding + UiThreadDispatcherTransaction foreignTransaction = await UiThreadDispatcherFixture + .BeginTransactionAsync() + .ConfigureAwait(false); + try + { + foreignTransaction.Install(parked); + IDisposable foreignPin = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + foreignPin.Dispose(); + Dispatcher afterForeignRelease = UiThreadDispatcherFixture.Current; + + // Assert + afterForeignRelease + .Should() + .BeSameAs( + parked, + because: "the last release cleared the install-ownership flag, so a pin that seeded nothing leaves a transaction value in place" + ); + } + finally + { + foreignTransaction.Dispose(); + } + } + } +} diff --git a/QuickFiler.Test/QuickFiler.Test.csproj b/QuickFiler.Test/QuickFiler.Test.csproj index 9fd57ab0b..9022b82f3 100644 --- a/QuickFiler.Test/QuickFiler.Test.csproj +++ b/QuickFiler.Test/QuickFiler.Test.csproj @@ -201,6 +201,7 @@ + @@ -226,6 +227,8 @@ + + diff --git a/QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs b/QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs new file mode 100644 index 000000000..25fe6e089 --- /dev/null +++ b/QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Time.Testing; + +namespace QuickFiler.Test.TestSupport +{ + /// + /// A that completes a signal after every + /// call, so a test can prove that a production loop armed its next wait instead of returning, + /// without a clock advance followed by a yield or a bounded retry. + /// + /// + /// Issue #968. completes once the first timer after construction, or after + /// the last , has been created; TrySetResult is used because a loop + /// can arm one more timer than a test drives. Signals run their continuations asynchronously + /// so a test never resumes inside the production CreateTimer call. Consecutive + /// Advance calls without awaiting in between are prohibited: a + /// deadline the loop has not yet created is not advanced past, and the test would then wait on + /// a timer that never fires. Modelled on UtilitiesCS.Test ArmingBarrierTimeProvider, as a + /// subclass rather than a forwarding decorator because this project already subclasses + /// . + /// + internal sealed class ArmingFakeTimeProvider : FakeTimeProvider + { + private volatile TaskCompletionSource _armed = NewSignal(); + + /// Completes after the next call since the last re-arm. + internal Task Armed => _armed.Task; + + /// Replaces the signal so the next call completes a fresh task. + internal void ReArm() => _armed = NewSignal(); + + public override ITimer CreateTimer( + TimerCallback callback, + object state, + TimeSpan dueTime, + TimeSpan period + ) + { + ITimer timer = base.CreateTimer(callback, state, dueTime, period); + _armed.TrySetResult(true); + return timer; + } + + private static TaskCompletionSource NewSignal() => + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } +} diff --git a/QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs b/QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs new file mode 100644 index 000000000..a9ba612bf --- /dev/null +++ b/QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs @@ -0,0 +1,27 @@ +using System.ComponentModel; + +namespace QuickFiler.Test.TestSupport +{ + /// + /// Test-side worker whose raises DoWork synchronously on the + /// calling thread through the protected OnDoWork, so a privately subscribed handler such + /// as QfcDatamodel.Worker_DoWork runs to its first incomplete await before + /// InitEmailQueue returns, and no worker a test starts outlives that test. Issue #950 + /// introduced this shape to replace bounded waits on a thread-pool worker; issue #968 (folding + /// issue #972) consolidated the three per-file copies here. The class adds no fields, handles + /// or subscriptions, so it does not override Dispose(bool); disposal stays with the test + /// that constructs the worker, in a using block. + /// + internal sealed class SynchronousBackgroundWorker : BackgroundWorker + { + /// Raises DoWork on the calling thread. + internal void RaiseDoWork() => OnDoWork(new DoWorkEventArgs(null)); + + /// + /// The synchronous starter assigned to QfcDatamodel.WorkerStarter. The worker handed + /// to it must be a . + /// + internal static void StartSynchronously(BackgroundWorker worker) => + ((SynchronousBackgroundWorker)worker).RaiseDoWork(); + } +} diff --git a/QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs b/QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs index ce2bc0428..654a0bf4b 100644 --- a/QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs +++ b/QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs @@ -13,13 +13,13 @@ namespace QuickFiler.Controllers public partial class QfcDatamodel { /// - /// Issue #424: honest producer-liveness signal. Set immediately before - /// each RunWorkerAsync() call and cleared in a finally once the awaited - /// RemainingEmailLoader completes. BackgroundWorker.IsBusy cannot serve this - /// role: Worker_DoWork is async void, so it returns at its first yielding await - /// and reports idle while the loader is still producing. Both the dequeue gate's - /// sourceActive signal and consume this flag. Declared - /// volatile because it is written on the worker thread and read by dequeue callers. + /// Issue #424 producer-liveness signal, read by the dequeue gate's sourceActive + /// delegate and by . InitEmailQueue sets it just before + /// handing the worker to , and Worker_DoWork clears it in + /// a finally when the awaited task completes, + /// because BackgroundWorker.IsBusy already reads idle at that handler's first + /// incomplete await (issue #950 made the start synchronous in tests, so no particular thread + /// owns either write). Volatile: the writers and the readers share no other fence. /// private volatile bool _remainingLoadActive; @@ -282,7 +282,7 @@ private async Task> DequeueWithHighConfidenceGateAsync( /// is taken from the same accepted set as /// , after has run /// over it. #678 R1: that correspondence holds on the happy path only. On the - /// UnhookItem throw path (:31-66) removes the failed + /// UnhookItem throw path removes the failed /// item and inserts a substitute pulled from the master queue, so PreScored can name /// an item absent from Items and Items can name an item absent from /// PreScored. Leg A reconciles the two at the load boundary through diff --git a/QuickFiler/Controllers/QfcDatamodel.cs b/QuickFiler/Controllers/QfcDatamodel.cs index f8847a6e3..7adbe16e8 100644 --- a/QuickFiler/Controllers/QfcDatamodel.cs +++ b/QuickFiler/Controllers/QfcDatamodel.cs @@ -96,9 +96,6 @@ public void Cleanup() _globals = null; _frame = null; _masterQueue = null; - //_blockingQueue = null; - //_priorityQueue = null; - //_queues = null; _worker = null; } @@ -106,9 +103,6 @@ public void Cleanup() #region Private Variables - private static readonly log4net.ILog log = log4net.LogManager.GetLogger( - System.Reflection.MethodBase.GetCurrentMethod().DeclaringType - ); private IApplicationGlobals _globals; private Explorer _activeExplorer; private LockingLinkedList _masterQueue = []; @@ -191,7 +185,6 @@ public void SetupWorker(System.ComponentModel.BackgroundWorker worker) _token.Register(() => worker.CancelAsync()); worker.DoWork += new System.ComponentModel.DoWorkEventHandler(Worker_DoWork); - //worker.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(Worker_RunWorkerCompleted); } private async void Worker_DoWork(object sender, DoWorkEventArgs e) @@ -206,8 +199,6 @@ private async void Worker_DoWork(object sender, DoWorkEventArgs e) //zxxint arg = (int)e.Argument; // Start the time-consuming operation. - //e.Result = await LoadRemainingEmailsToQueueAsync(bw, _token); - //e.Result = LoadRemainingEmailsToQueue(bw, _token); try { // Issue #791: capture the loader task before awaiting it. This method is @@ -240,30 +231,6 @@ private async void Worker_DoWork(object sender, DoWorkEventArgs e) } } - // This event handler demonstrates how to interpret - // the outcome of the asynchronous operation implemented - // in the DoWork event handler. - private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) - { - if (e.Cancelled) - { - // The user canceled the operation. - MessageBox.Show("Operation was canceled"); - } - else if (e.Error != null) - { - // There was an error during the operation. - string msg = String.Format("An error occurred: {0}", e.Error.Message); - MessageBox.Show(msg); - } - else - { - // The operation completed normally. - //string msg = String.Format("Result = {0}", e.Result); - //MessageBox.Show(msg); - } - } - #endregion BackgroundWorker #region Email Queue Initial Setup @@ -360,13 +327,12 @@ private async Task LoadRemainingEmailsToQueueAsync(CancellationToken cance } catch (OperationCanceledException) { - //logger.Debug($"{nameof(LoadRemainingEmailsToQueue)} Task cancelled"); return false; } catch (System.Exception e) { logger.Error( - $"{nameof(LoadRemainingEmailsToQueue)} Error. \n {e.Message}\n{e.StackTrace}" + $"{nameof(LoadRemainingEmailsToQueueAsync)} Error. \n {e.Message}\n{e.StackTrace}" ); throw; } @@ -375,102 +341,8 @@ private async Task LoadRemainingEmailsToQueueAsync(CancellationToken cance return true; } - private bool LoadRemainingEmailsToQueue(BackgroundWorker bw, CancellationToken token) - { - if ((_frame is null) || (_frame.RowCount == 0)) - { - MessageBox.Show("Email Frame is empty"); - return false; - } - - // Cast Frame to array of IEmailInfo - var rows = _frame.GetRowsAs().Values.ToArray(); - - foreach (var row in rows) - { - try - { - token.ThrowIfCancellationRequested(); - //var item = (MailItem)_olApp.GetNamespace("MAPI").GetItemFromID(row.EntryId, row.StoreId); - var item = _olApp.GetNamespace("MAPI").GetItemFromID(row.EntryId, row.StoreId); - if (item is not null && item is MailItem mailItem) - { - _masterQueue.AddLast(mailItem); - _moveMonitor.HookItem(mailItem, (x) => _masterQueue.Remove(x)); - } - } - catch (OperationCanceledException) - { - //logger.Debug($"{nameof(LoadRemainingEmailsToQueue)} Task cancelled"); - return false; - } - catch (System.Exception e) - { - logger.Error( - $"{nameof(LoadRemainingEmailsToQueue)} Error. \n {e.Message}\n{e.StackTrace}" - ); - throw; - } - } - return true; - } - - private async Task LoadRemainingEmailsToQueueAsync( - BackgroundWorker bw, - CancellationToken token - ) - { - if ((_frame is null) || (_frame.RowCount == 0)) - { - MessageBox.Show("Email Frame is empty"); - return false; - } - - try - { - // ForEachAwaitWithCancellationAsync (System.Linq.Async) is obsolete (CS0618) per - // the framework's migration guidance ("Use the language support for async foreach - // instead"), but replacing it with `await foreach` here is a control-flow change - // to a production async method, not an annotation-only edit. Suppressing narrowly - // preserves the exact pre-existing behavior (no behavior change per AC7). -#pragma warning disable CS0618 - await _frame - .GetRowsAs() - .Values.ToAsyncEnumerable() - .ForEachAwaitWithCancellationAsync( - async (row, token) => - await Task.Run( - () => - { - token.ThrowIfCancellationRequested(); - var item = (MailItem) - _olApp - .GetNamespace("MAPI") - .GetItemFromID(row.EntryId, row.StoreId); - _masterQueue.AddLast(item); - _moveMonitor.HookItem(item, (x) => _masterQueue.Remove(x)); - }, - token - ), - token - ); -#pragma warning restore CS0618 - return true; - } - catch (TaskCanceledException) - { - //logger.Debug($"{nameof(LoadRemainingEmailsToQueueAsync)} Task cancelled"); - return false; - } - } - #endregion Email Queue Initial Setup - #region Linked List Locking - - - #endregion Linked List Locking - #region Event Handlers void Application_NewMailEx(string entryID) diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/code-review.2026-10-03T04-00.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/code-review.2026-10-03T04-00.md new file mode 100644 index 000000000..1c641ec27 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/code-review.2026-10-03T04-00.md @@ -0,0 +1,73 @@ +# Code Review: focus-and-theme-tests-leak-shared-dispatcher-setup (Issue #968, folding Issue #972) + +- Timestamp: 2026-10-03T04-00 +- Branch: bug/focus-and-theme-tests-leak-shared-dispatcher-setup-968, head 5570b337cfd11e57579228b36bc919f9673b6195 +- Base: origin/main 993fdd01566dee82e5f37acb761a600feaaa1454 (ancestor of the head after the final merge) +- Companion artifacts: policy-audit.2026-10-03T04-00.md, feature-audit.2026-10-03T04-00.md, remediation-inputs.2026-10-03T04-00.md +- Method: no-Bash review (caller directive). All fourteen code paths were read in full from the item worktree; the ten pre-existing modified files were compared against the session checkout's unchanged copies (line counts 497, 342, 440, 470, 312, 244, 232, 371, 495 and 413 match the executor's BASE census, so those copies are the pre-change text); the executor's transcribed diffs and censuses were cross-read; Grep censuses were re-run with the Grep tool. + +## Executive Summary + +Code quality verdict: no blocking code-quality finding. 0 Blocking code-quality findings, 2 Non-blocking findings (CR-1, CR-2), 6 observations (O-1 to O-6). The single blocking item of this review, B-1 (AC22 pending this pull request's CI run, class awaiting_ci), is an evidence-source matter recorded in remediation-inputs.2026-10-03T04-00.md and the feature audit, not a code defect. + +The fixture change is sound under every state the reviewer traced (fresh field, foreign transaction value, residual parked value with the ownership flag set, pins taken under a foreign value then restored to null); the regression test observes the defect on one thread; the R4 restructure keeps the test deterministic because the census proves every remaining pin nests inside a held transaction; the liveness rewrites replace scheduling-dependent steps with signals the production code already emits; the folded dead-code removal is supported by a two-strategy zero-caller proof and two clean rebuilds. Under the maintainer's related-defect directive every related finding was evaluated for in-item remediation; none of the items below is a defect that the ratified spec left open, so none is classified blocking autonomous. + +## Scope + +Fourteen code paths (eleven modified, three added) plus the feature folder and the two inherited promoted records: + +- QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs (342 -> 375 lines) +- QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs (new, 248) +- QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs (470 -> 472) +- QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs (497 -> 482) +- QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs (440 -> 442) +- QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs (312 -> 352) +- QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs (244 -> 229) +- QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs (232 -> 226) +- QuickFiler.Test/Controllers/QfcDatamodelTests.cs (371 -> 394) +- QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs (new, 27) +- QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs (new, 49) +- QuickFiler.Test/QuickFiler.Test.csproj (+3 Compile items at lines 204, 230, 231) +- QuickFiler/Controllers/QfcDatamodel.cs (495 -> 367) +- QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs (413, comment-only) + +## Findings Table + +| Severity | File | Location | Finding | Recommendation | Rationale | Evidence | +|---|---|---|---|---|---|---| +| Non-blocking | QuickFiler/Controllers/QfcDatamodel.cs | lines 63, 70, 199, 268, 318, 350 | Pre-existing commented-out statements remain in the touched production file after the dead-member removal (two commented logger.Debug lines in LoadAsync, a commented argument extraction, a BUGFIX note, two commented item lookups). | Leave as delivered; the research (addendum F2) evaluated the remaining commented lines and recommended keeping the diff reviewable, and spec amendment 1.2 limits the production edit to the removals, the nameof retarget and the comment rewrites. Remove them in the next change that touches the file. | Not a defect the ratified scope left open: the scope statement for this file is explicit. The lines carry no stale reference to a removed member (Grep for Worker_RunWorkerCompleted, LoadRemainingEmailsToQueue word boundary and Linked List Locking over QuickFiler/: zero hits in QfcDatamodel.cs). | Read of QfcDatamodel.cs; research addendum section 6.1 F2; spec Scope and Non-Goals (production edits confined) | +| Non-blocking | QuickFiler.Test/Controllers/QfcDatamodelTests.cs | lines 125, 135, 148 | The rewritten sibling test registers the dequeue chain's awaits under whatever SynchronizationContext the MSTest worker thread carries at call time, unlike the Liveness rewrite, which registers them under a null context through SynchronizationContextScope. A non-pumping context left on the thread by an earlier test would stall `await pending` rather than fail it. | Accept as delivered. The spec (addendum section 6.2, ratified by amendment 1.2) states that no context handling is needed here because the flag is written directly by reflection and no loader continuation is involved; the only known context leaker in the assembly installs a plain SynchronizationContext whose Post reaches the thread pool, which does pump. If a future test leaves a non-pumping context behind, wrap the DequeueNextItemGroupAsync call in the same null-context scope. | The AC31 text (no Task.Yield, no loop, WhenAny over Armed, await the dequeue task) is met; the residual exposure is the same one every awaiting test in the assembly already has and is not introduced by this branch. The sensitivity check shows the test fails crisply on its re-arm assertion when the signal is dishonest. | QfcDatamodelTests.cs lines 103-152; QfcDatamodelLivenessTests.cs lines 123-134, 159-162, 189-199; evidence/regression-testing/liveness-sensitivity-check.md | + +## Observations (not findings) + +- O-1 Fixture soundness trace. EnsureDispatcher increments the count and seeds only on a null field, setting the ownership flag; EnsureScope.Dispose decrements and writes null only when count is zero, the flag is set and the field still references the parked instance, then clears the flag, all inside one lock (FieldLock) block (lines 149-157, 293-305). Traced states: (a) fresh null field, two pins, either release order: field kept until the last release, then null, flag cleared; (b) transaction holds a live dispatcher: pins install nothing, count reaches zero with the flag false, nothing written; (c) residual state (parked installed with zero pins and the flag set, reachable only if a transaction installs over a pinned parked value and restores it after the last release): the next single pin installs nothing, its release finds flag true and field parked and reverts, as the class doc states; (d) pins taken under a foreign value, transaction restores null while pins are live: count stays above zero, flag false, next pin seeds and sets the flag, last release reverts. No negative count is reachable because Dispose is idempotent per scope. The documented residual is unreached by any test (census: no Install between any pin's acquisition and release). +- O-2 R4 determinism after the pin removal. transactionA holds the gate from BeginTransactionAsync to its explicit Dispose; original is read and liveA installed under the gate; B blocks on the gate; A restores before releasing; B reads under the gate. Every remaining pin is acquired and released inside a held transaction (13 of 13 nested), so no gate-free writer exists in the assembly; the only residual writer is UiThread.Initialize (W5), which the doc names and which the former pin did not fence either. The try/finally re-dispose relies on the idempotency R5 proves. +- O-3 Liveness rewrite determinism. pending runs synchronously to the gate's first TimeProvider.Delay, so Armed is complete before the call returns (asserted); ReArm precedes the advance; WhenAny(Armed, pending) completes whether the gate's ConfigureAwait(false) continuation is inlined inside Advance or queued; loaderRelease is a plain TaskCompletionSource and the loader lambda's await and Worker_DoWork's await were registered under a null context, so SetResult runs the continuations inline and the finally clears the flag before ReadLivenessFlag; the second Advance fires the already-armed timer and the dequeue task is the completion signal. No step depends on pool scheduling. +- O-4 Canonical C# coverage artifact path (artifacts/csharp/coverage.xml) absent in the worktree; committed projections, summaries and the local raw Cobertura root were used under the standing ruling. Recurring across #948, #950 and #956; a repository convention, not a defect of this item. +- O-5 QuicFiler/Controllers/QfcDatamodel.QueueProcessing.cs line 52 comment ("the field is written on the worker thread") describes _remainingLoadTask, which Worker_DoWork writes on the thread that runs the handler: the BackgroundWorker thread in production, the test thread under the synchronous starter. The executor's D-20 decision to leave it unchanged is accepted; the comment's snapshot rationale (a cross-thread writer) holds in both environments and AC26 names only the _remainingLoadActive comment and the TryUnhookOrReplace range. +- O-6 QfcDatamodelLivenessTests test RemainingLoadActive_AcrossAsyncVoidFirstAwait_StaysTrueWhileLoaderProduces releases its loader with a RunContinuationsAsynchronously source and exits the using block, so the loader's continuation (and Worker_DoWork's bw.CancellationPending read) may run on a pool thread after the worker is disposed. BackgroundWorker.CancellationPending is a plain property read that Component.Dispose does not invalidate; this is the #950 design, documented in the research addendum (section 4.2), and no change is needed. + +## Per-file notes + +- UiThreadDispatcherFixture.cs: two new private statics with a comment stating the FieldLock invariant; the EnsureScope now carries the parked reference rather than an installed-or-null marker; CompareExchange is no longer used by the scope (SCOPE census CompareExchange 0). Docs for the class, EnsureDispatcher and EnsureScope describe counting, ownership, discard consequence and residual; the forbidden phrases are gone (Grep leaks exactly 0, installed nothing carries 0). +- UiThreadDispatcherPinCountTests.cs: four tests, every pin acquired and released inside a held transaction with the transaction disposed in finally; no assertion sits between a pin's acquisition and its release in tests 1 to 3, and in test 4 both assertion groups follow the releases, so an assertion failure cannot leak a pin. The class doc states why the regression lives at the fixture level and why Moq is not imported. +- UiThreadDispatcherFixtureTests.cs: only R4 changed (doc, pin removal, try/finally); the R1 to R3 and R5 to #882 bodies are character-identical to the pre-change copy on every line the reviewer compared (assertion and because text lines). +- FocusAndThemeTests.cs: seven call sites switched to the shared BuildExecutingViewer; the header comment names the switch; the two theme tests lose their ensure calls and gain accurate arrange comments naming the injected IUiDispatcher mock. +- TestSupport.cs: wrapper doc rewritten to the counted pin, naming the two fixture test classes and the dispose-inside-the-transaction rule; shared-helper doc updated; EnsureSynchronizationContext (lines 90-96) unchanged. +- QfcDatamodelLivenessTests.cs: shared worker, caller-owned workers through StartHeldOpenLoader(worker, ...), SynchronizationContextScope disposer with the no-await rule stated in its doc, test 1 rewritten; the header still documents the deliberate duplication of the two reflection helpers, which the spec keeps. +- QfcDatamodelTeardownTests.cs: nested helper removed; using directive added; otherwise unchanged. +- QfcInitEmailQueueZeroBatchTests.cs: nested helper removed; three using blocks; the "real BackgroundWorker" doc sentence corrected (F6). +- QfcDatamodelTests.cs: sibling test rewritten to the signal shape; two using blocks around BackgroundWorker instances. +- SynchronousBackgroundWorker.cs: internal sealed; RaiseDoWork and StartSynchronously; no Dispose(bool) override, with the reason in the doc. +- ArmingFakeTimeProvider.cs: CreateTimer override forwards to the base then completes the armed signal; signals created with RunContinuationsAsynchronously; the remarks state the consecutive-Advance prohibition that the tests obey. +- QfcDatamodel.cs: 128 lines removed (duplicate log field, Worker_RunWorkerCompleted, synchronous LoadRemainingEmailsToQueue, two-argument LoadRemainingEmailsToQueueAsync with its CS0618 pragma, the empty region, commented references); one line changed (nameof now names the live method). Both constructors still bind the one-argument loader; IQfcDatamodel members all remain. +- QfcDatamodel.QueueProcessing.cs: eight doc-comment lines changed; no statement touched (every changed line begins with ///). +- QuickFiler.Test.csproj: three Compile items in the positions the spec names (after the fixture-test item; after the DedicatedWorkerThread item). + +## Unrelated defects (report for filing) + +- None in the files examined. quality-tiers.yml is absent at the repository root (pre-existing, repository-wide, already promoted by the #956 review); no new issue is required from this item. + +## Summary + +No blocking code-quality finding. Two non-blocking findings, both evaluated against the ratified spec and accepted as delivered (CR-1 pre-existing commented-out statements the spec's confined production edit leaves in place; CR-2 the sibling liveness test's dependence on the ambient context being a pumping one, a pre-existing assembly-wide exposure that the spec explicitly accepted). Six observations record the soundness traces and standing conventions. The branch's single blocking item is B-1 (AC22 awaiting this pull request's CI run) and is recorded in remediation-inputs.2026-10-03T04-00.md. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/analyzer-alignment.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/analyzer-alignment.md new file mode 100644 index 000000000..049bd7b42 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/analyzer-alignment.md @@ -0,0 +1,12 @@ +# Analyzer-path alignment (issue #968, task P0-T7) + +Timestamp: 2026-10-03T02-43 +Command: pwsh -NoProfile -Command 'Set-Location -LiteralPath "WORKTREE"; [System.IO.Directory]::SetCurrentDirectory((Get-Location).Path); Write-Output ("WORKTREE-LEAF: " + (Split-Path -Leaf (Get-Location).Path)); $root = (Get-Location).Path; $rootLen = $root.TrimEnd([char]92).Length; $projs = @(Get-ChildItem -Path $root -Recurse -Filter "*.csproj" | Where-Object { $rel = $_.FullName.Substring($rootLen); $rel -notlike "\packages\*" -and $rel -notlike "\.claude\*" }); "PROJECTS=$($projs.Count)"; $missing = 0; $skew = 0; foreach ($p in $projs) { $dir = $p.DirectoryName; [xml]$x = Get-Content -LiteralPath $p.FullName -Raw; foreach ($a in @($x.SelectNodes("//*[local-name()=""Analyzer""]"))) { $inc = $a.GetAttribute("Include"); if (-not (Test-Path -LiteralPath (Join-Path $dir $inc))) { $missing++; "MISSING " + $p.FullName.Substring($rootLen) + " :: " + $inc } }; $pc = Join-Path $dir "packages.config"; if (Test-Path -LiteralPath $pc) { [xml]$c = Get-Content -LiteralPath $pc -Raw; foreach ($id in @("Meziantou.Analyzer", "Roslynator.Analyzers")) { $pin = @($c.SelectNodes("//package[@id=""$id""]") | ForEach-Object { $_.GetAttribute("version") }); $inc = @($x.SelectNodes("//*[local-name()=""Analyzer""]") | ForEach-Object { $_.GetAttribute("Include") } | Where-Object { $_.Contains("\$id.") }); foreach ($i in $inc) { if ($pin.Count -eq 0 -or -not $i.Contains("\$id." + $pin[0] + "\")) { $skew++; "SKEW " + $p.FullName.Substring($rootLen) + " :: " + $i } } } } }; "ANALYZER_MISSING=$missing"; "VERSION_SKEW=$skew"' +Canonical command: analyzer include-path and version-pin alignment over every first-party *.csproj outside packages\ and .claude\ +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- PROJECTS=18 +- ANALYZER_MISSING=0 +- VERSION_SKEW=0 +- No MISSING or SKEW lines were printed. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/bootstrap-dotnet-coverage.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/bootstrap-dotnet-coverage.md new file mode 100644 index 000000000..841b3e1d4 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/bootstrap-dotnet-coverage.md @@ -0,0 +1,10 @@ +# Bootstrap: dotnet-coverage global tool (issue #968, task P0-T8) + +Timestamp: 2026-10-03T02-43 +Command: pwsh -NoProfile -Command 'if (-not (Get-Command dotnet-coverage -ErrorAction SilentlyContinue)) { dotnet tool install --global dotnet-coverage }; "DOTNET_COVERAGE_RESOLVED=$($null -ne (Get-Command dotnet-coverage -ErrorAction SilentlyContinue))"; dotnet-coverage --version' +Canonical command: dotnet tool install --global dotnet-coverage (guarded), then dotnet-coverage --version +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: not applicable (the command touches no repository path; no PREFIX) +- DOTNET_COVERAGE_RESOLVED=True (already installed; the guarded install did not run) +- 18.10.0+f4cc39224845ffa74bf246c9da2399d50e5d6342 diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/bootstrap-nuget-restore.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/bootstrap-nuget-restore.md new file mode 100644 index 000000000..ec1662c9d --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/bootstrap-nuget-restore.md @@ -0,0 +1,12 @@ +# Bootstrap: NuGet restore (issue #968, task P0-T6) + +Timestamp: 2026-10-03T02-43 +Command: pwsh -NoProfile -Command 'Set-Location -LiteralPath "WORKTREE"; [System.IO.Directory]::SetCurrentDirectory((Get-Location).Path); Write-Output ("WORKTREE-LEAF: " + (Split-Path -Leaf (Get-Location).Path)); $env:MSBUILDDISABLENODEREUSE = "1"; & (Join-Path (Get-Location).Path "scripts\vscode\Invoke-Restore.ps1"); "RESTORE_EXIT=$LASTEXITCODE"; "PACKAGE_DIRS=$(@(Get-ChildItem -LiteralPath packages -Directory -ErrorAction SilentlyContinue).Count)"' +Canonical command: scripts/vscode/Invoke-Restore.ps1 (msbuild TaskMaster.sln /t:Restore) +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- MSBuild version 18.10.1-1.26427.6+3cd27c13e for .NET Framework (resolved MSBuild path: REDACTED-PATH) +- Restore target: Build succeeded. 0 Warning(s), 0 Error(s) +- RESTORE_EXIT=0 +- PACKAGE_DIRS=172 diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/bootstrap-sdk.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/bootstrap-sdk.md new file mode 100644 index 000000000..af1cb6148 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/bootstrap-sdk.md @@ -0,0 +1,13 @@ +# Bootstrap: repository .NET SDK (issue #968, task P0-T4) + +Timestamp: 2026-10-03T02-43 +Command: pwsh -NoProfile -Command 'Set-Location -LiteralPath "WORKTREE"; [System.IO.Directory]::SetCurrentDirectory((Get-Location).Path); Write-Output ("WORKTREE-LEAF: " + (Split-Path -Leaf (Get-Location).Path)); if (-not (Test-Path -LiteralPath ".dotnet-sdk\sdk\8.0.205")) { & (Join-Path (Get-Location).Path "scripts\vscode\Install-RepoDotNetSdk.ps1") }; "SDK_MARKER=$(Test-Path -LiteralPath ".dotnet-sdk\sdk\8.0.205")"; dotnet --version; "DOTNET_EXIT=$LASTEXITCODE"' +Canonical command: scripts/vscode/Install-RepoDotNetSdk.ps1 (guarded on the .dotnet-sdk\sdk\8.0.205 marker), then dotnet --version +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- Downloading .NET SDK 8.0.205 from https://builds.dotnet.microsoft.com/dotnet/Sdk/8.0.205/dotnet-sdk-8.0.205-win-x64.zip... +- Installed repo-local .NET SDK 8.0.205 to REDACTED-PATH. +- SDK_MARKER=True +- 8.0.205 (version string; not the global.json errorMessage text) +- DOTNET_EXIT=0 diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/bootstrap-tool-restore.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/bootstrap-tool-restore.md new file mode 100644 index 000000000..d4d16e1d9 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/bootstrap-tool-restore.md @@ -0,0 +1,13 @@ +# Bootstrap: dotnet tool restore (issue #968, task P0-T5) + +Timestamp: 2026-10-03T02-43 +Command: pwsh -NoProfile -Command 'Set-Location -LiteralPath "WORKTREE"; [System.IO.Directory]::SetCurrentDirectory((Get-Location).Path); Write-Output ("WORKTREE-LEAF: " + (Split-Path -Leaf (Get-Location).Path)); dotnet tool restore; "RESTORE_EXIT=$LASTEXITCODE"; dotnet tool list --local; dotnet tool run csharpier check --help | Out-Null; "CHECK_HELP_EXIT=$LASTEXITCODE"' +Canonical command: dotnet tool restore (manifest dotnet-tools.json at the worktree root) +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- Tool 'csharpier' (version '1.2.6') was restored. Available commands: csharpier +- Restore was successful. +- RESTORE_EXIT=0 +- Local tool row (Package Id, Version only; the Manifest column carries an absolute path and is not transcribed): csharpier 1.2.6 +- CHECK_HELP_EXIT=0 diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/census-baseline.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/census-baseline.md new file mode 100644 index 000000000..9cf85e1a9 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/census-baseline.md @@ -0,0 +1,69 @@ +# Pre-change census of the four #968 code files (issue #968, task P0-T12) + +Timestamp: 2026-10-03T02-47 +Command: pwsh -NoProfile -Command '' (plan Command Reference CMD-CENSUS, executed verbatim with PREFIX expanded and WORKTREE substituted) +Canonical command: CMD-CENSUS (primary pattern `EnsureUiThreadDispatcher\(\)|EnsureDispatcher\(\)`, cross pattern `EnsureUiThreadDispatcher|EnsureDispatcher`, control pattern `BeginTransactionAsync\(`, over every *.cs outside packages, .claude, obj and bin) +EXIT_CODE: 0 +Output Summary: WORKTREE-LEAF agent-a291a7fbabf9d0229 in every payload; LINES 342, 497, 440, 470; PRIMARY_LINES 9, CROSS_LINES 20, CONTROL_LINES 23; every token and span value equals the plan's expected pre-change value (no CENSUS MISMATCH). Details below. + +Every payload below was a separate Bash call `pwsh -NoProfile -Command ''` whose payload is the named Command Reference macro with PREFIX expanded, WORKTREE substituted and the stated FILE, FILES, START, END and TOKENS values substituted; each exited 0 and printed `WORKTREE-LEAF: agent-a291a7fbabf9d0229`. + +## CMD-LINECOUNT on CS4 + +- LINES QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixture.cs = 342 +- LINES QuickFiler.Test\Controllers\QfcItemController.FocusAndThemeTests.cs = 497 +- LINES QuickFiler.Test\Controllers\QfcItemController.TestSupport.cs = 440 +- LINES QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs = 470 + +## CMD-HASH on CS4 + +- BASE-HASH: QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixture.cs = 1BB53DBE378F6E6B69C42D9234061465CAD9CE79002E2AE9CF8004731449EFA6 +- BASE-HASH: QuickFiler.Test\Controllers\QfcItemController.FocusAndThemeTests.cs = A3C35259F1C5E5D2ED8D8A3E5BA923A964E2B164ABE9D9AC7B6B32EC30644E4B +- BASE-HASH: QuickFiler.Test\Controllers\QfcItemController.TestSupport.cs = 6DDACD2EC8DED8C83320F3F65E0A61C0BE16283A7BB2D277963C46BDA9B13779 +- BASE-HASH: QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs = 05638767D69C12FE98B28DCE78B6827AD2C1EC64221F1E445862087668BF5DCA + +## CMD-CENSUS + +- CS_FILES: 1706 +- PRIMARY_LINES: 9 +- PRIMARY-FILE \QuickFiler.Test\Controllers\QfcItemController.FocusAndThemeTests.cs = 2 +- PRIMARY-FILE \QuickFiler.Test\Controllers\QfcItemController.TestSupport.cs = 2 +- PRIMARY-FILE \QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixture.cs = 1 +- PRIMARY-FILE \QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs = 4 +- PRIMARY \QuickFiler.Test\Controllers\QfcItemController.FocusAndThemeTests.cs:452 :: QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- PRIMARY \QuickFiler.Test\Controllers\QfcItemController.FocusAndThemeTests.cs:468 :: QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- PRIMARY \QuickFiler.Test\Controllers\QfcItemController.TestSupport.cs:238 :: internal static IDisposable EnsureUiThreadDispatcher() => +- PRIMARY \QuickFiler.Test\Controllers\QfcItemController.TestSupport.cs:239 :: UiThreadDispatcherFixture.EnsureDispatcher(); +- PRIMARY \QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixture.cs:122 :: internal static IDisposable EnsureDispatcher() +- PRIMARY \QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs:60 :: QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- PRIMARY \QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs:119 :: IDisposable ensureScope = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- PRIMARY \QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs:166 :: IDisposable ensureScope = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- PRIMARY \QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs:222 :: IDisposable baseline = QfcItemControllerTestSupport.EnsureUiThreadDispatcher() +- CROSS_LINES: 20 +- CROSS-FILE \QuickFiler.Test\Controllers\QfcItemController.FocusAndThemeTests.cs = 2 +- CROSS-FILE \QuickFiler.Test\Controllers\QfcItemController.InitializationTests.Part2.cs = 1 +- CROSS-FILE \QuickFiler.Test\Controllers\QfcItemController.TestSupport.cs = 2 +- CROSS-FILE \QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixture.cs = 5 +- CROSS-FILE \QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs = 10 +- CROSS lines: FocusAndThemeTests 452, 468; InitializationTests.Part2 124 (comment); TestSupport 238, 239; UiThreadDispatcherFixture 26, 27, 122, 195, 244; UiThreadDispatcherFixtureTests 44, 60, 70, 107, 119, 128, 157, 166, 198, 222 (each line's text matches fact 5 and research section 2.1) +- CONTROL_LINES: 23 + +## CMD-TOKEN-COUNT + +FIX (`_pinCount`, `_fixtureInstalledParked`, `lock (FieldLock)`, `CompareExchange(`, `return new EnsureScope(`, `leaks exactly`, `A scope that installed nothing`, `pins for the process lifetime`, `install-ownership flag`, `installed nothing carries`): 0, 0, 4, 3, 2, 1, 1, 0, 0, 0. The last value is vacuous at baseline: the phrase wraps across lines 245 and 246, so a per-line count reads 0 before the change. + +FAT (`EnsureUiThreadDispatcher`, `private static Mock BuildExecutingViewer`, `QfcItemControllerTestSupport.BuildExecutingViewer()`, `BuildExecutingViewer`, `absorbs the delegate without running it`, `shared UiThread static is irrelevant`, `absorbs the queued application`, `[TestMethod]`): 2, 1, 0, 9, 0, 0, 0, 17. + +TS (`Becomes moot`, `leaks exactly`, `still delegate to a callee`, `not reachable from another test file`, `remaining legitimate`, `QfcItemController_UiThreadDispatcherPinCountTests`, `internal static void EnsureSynchronizationContext()`, `UiThreadDispatcherFixture.EnsureDispatcher();`): 1, 1, 1, 1, 0, 0, 1, 1. + +FT (`no other class may dispose`, `removed that pin: the fixture now counts pins`, `(W5) must not latch`, `[Timeout(GateTimeoutMs)]`, `private const int GateTimeoutMs = 60000;`, `EnsureUiThreadDispatcher()`, `issue #230 lost update`, `the waiter cannot observe the pre-restore value`, `[TestMethod]`): 1, 0, 1, 8, 1, 4, 1, 1, 8. + +## CMD-SPAN-TOKEN-COUNT + +- R4SPAN (FT; START `public async Task Transaction_SecondCallerCannotInstallUntilTheFirstRestores()`, END `public async Task Transaction_DisposedTwice_DoesNotOverReleaseTheGate()`): SPAN: 212-284; `EnsureUiThreadDispatcher()` 1, `using (` 2, `transactionA.Dispose();` 1, `finally` 2, `.BeSameAs(` 1, `.NotBeSameAs(` 1, `issue #230 lost update` 1. +- R4HEAD (FT; START the R4SPAN START, END `Dispatcher original = UiThreadDispatcherFixture.Current;`): SPAN: 212-224; `EnsureUiThreadDispatcher()` 1, `using (` 1, `try` 1. +- R4TAIL (FT; START `issue #230 lost update`, END `QfcItemControllerTestSupport.ShutdownDispatcher(liveA);`): SPAN: 267-273; `}` 3, `finally` 1, `transactionA.Dispose();` 0. +- ENSURE (FIX; START `internal static IDisposable EnsureDispatcher()`, END `internal const int TransactionGateAcquireTimeoutMs = 120000;`): SPAN: 122-145; `_pinCount++` 0, `_fixtureInstalledParked = true;` 0, `lock (FieldLock)` 1, `return new EnsureScope(` 2. +- SCOPE (FIX; START `private sealed class EnsureScope : IDisposable`, END `internal sealed class UiThreadDispatcherTransaction : IDisposable`): SPAN: 249-283; `CompareExchange(` 1, `lock (FieldLock)` 0, `_pinCount--` 0, `_fixtureInstalledParked = false;` 0, `DispatcherField.SetValue(null, null);` 0. + +The non-zero counts (the two focus-and-theme ensure calls, the private helper, the four stale doc tokens, the R4 pin) are the positive controls for the zero gates of P6-T2 and P7-T1. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/concurrent-set-baseline.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/concurrent-set-baseline.md new file mode 100644 index 000000000..efd906c5c --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/concurrent-set-baseline.md @@ -0,0 +1,44 @@ +# Baseline: concurrent run of the fixture-tests and focus-and-theme classes (issue #968, task P0-T14) + +Timestamp: 2026-10-03T02-50 +Command: pwsh -NoProfile -Command '' with ASSEMBLY `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, FILTER `FullyQualifiedName~QuickFiler.Controllers.Tests.QfcItemController_UiThreadDispatcherFixtureTests.|FullyQualifiedName~QuickFiler.Controllers.Tests.QfcItemController_FocusAndThemeTests.` (FILTER-BASELINE-CONCURRENT), TASKID p0-t14 and an empty NAMES list (`$names = @()`); the payload is the Command Reference CMD-VSTEST macro executed verbatim with PREFIX and TOOLS expanded and WORKTREE substituted +Canonical command: vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER-BASELINE-CONCURRENT" "/ResultsDirectory:coverage\test-results\968\p0-t14" "/Logger:trx;LogFileName=p0-t14.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- VSTEST_EXIT_CODE: 0 +- TRX_PRESENT: True +- SEQUENCE_FILES: 0 +- RESULT_COUNT: 25 +- BASELINE-CONCURRENT-COUNTERS: COUNTERS total=25 executed=25 passed=25 failed=0 +- BASELINE-CONCURRENT-FAILED: NONE + +RESULT lines (trx-derived; the trx stays under the ignored coverage directory): + +- InvokeBeginInvoke_WhenAsync_UsesBeginInvoke = Passed duration=00:00:00.0011730 +- SetThemeLight_FromNormal_SelectsLightNormalTheme = Passed duration=00:00:00.0002698 +- EnsureDispatcher_WhenTheFieldIsNull_InstallsAndRestoresOnDispose = Passed duration=00:00:00.0011329 +- Transaction_DisposedTwice_DoesNotOverReleaseTheGate = Passed duration=00:00:00.0019664 +- ToggleNavigationAsync_AwaitsPositionTipsToggleAsync = Passed duration=00:00:00.0012680 +- ToggleFocusOnAsync_ActivatesUiAndSwitchesToActiveTheme = Passed duration=00:00:00.0015491 +- InvokeBeginInvoke_WhenSynchronous_UsesInvoke = Passed duration=00:00:00.0004615 +- Transaction_SecondCallerCannotInstallUntilTheFirstRestores = Passed duration=00:00:00.0036387 +- EnsureDispatcher_ScopeDisposedTwice_IsIdempotent = Passed duration=00:00:00.0041188 +- ToggleFocus_ParameterlessOverload_MarshalsThroughItemViewerInvoke = Passed duration=00:00:00.0010535 +- ToggleNavigation_WithState_TogglesPositionTipsWithState = Passed duration=00:00:00.0006501 +- Install_CalledTwiceOnTheSameTransaction_ThrowsInvalidOperationException = Passed duration=00:00:00.0047744 +- TransactionGate_WhileThisTestHoldsATransaction_HasExactlyOneUnreleasedAcquisition = Passed duration=00:00:00.0025458 +- BeginTransactionAsync_ZeroBoundWhileThisTestHoldsThePermit_ThrowsTimeoutExceptionAndReleasesNothing = Passed duration=00:00:00.0483000 +- ToggleSaveAttachments_DoesNotThrow = Passed duration=00:00:00.0002902 +- ToggleFocusOffAsync_DeactivatesUiAndSwitchesToNormalTheme = Passed duration=00:00:00.0011325 +- ToggleFocus_StateOverload_Off_FromActive_DeactivatesUiAndSwitchesToNormalTheme = Passed duration=00:00:00.0018879 +- ToggleNavigation_Synchronous_TogglesPositionTips = Passed duration=00:00:00.0055640 +- HtmlDarkConverter_WhenWebViewNotInitialized_DoesNotNavigate = Passed duration=00:00:00.0005872 +- ToggleFocus_StateOverload_MarshalsThroughItemViewerInvoke = Passed duration=00:00:00.3641647 +- ToggleFocus_ParameterlessOverload_FromActive_DeactivatesUiAndSwitchesToNormalTheme = Passed duration=00:00:00.0008017 +- EnsureDispatcher_WhileATransactionHoldsALiveDispatcher_DoesNotReplaceIt = Passed duration=00:00:00.0729268 +- ToggleTipsAsync_WithEmptyCollections_Completes = Passed duration=00:00:00.0010125 +- ToggleTips_Synchronous_DispatchesAndExecutesDelegate = Passed duration=00:00:00.0004438 +- SetThemeDark_FromNormal_SelectsDarkNormalTheme = Passed duration=00:00:00.0004225 + +No MESSAGE lines were printed (no non-Passed outcome). No Timeout or Aborted outcome and no Sequence file (no BASELINE HANG). diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/coverage-jacoco-projection.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/coverage-jacoco-projection.md new file mode 100644 index 000000000..67c322d26 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/coverage-jacoco-projection.md @@ -0,0 +1,47 @@ +# Baseline JaCoCo package projection (issue #968, task P0-T17) + +Timestamp: 2026-10-03T02-53 +Source: FEATURE/evidence/baseline/coverage-summary.md (CMD-COVERAGE-POST, STAGE baseline, projection reconciled by Assert-JacocoProjectionReconciliation) + +PROJECTION-BEGIN +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` +PROJECTION-END diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/coverage-summary.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/coverage-summary.md new file mode 100644 index 000000000..3813827f0 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/coverage-summary.md @@ -0,0 +1,63 @@ +# Baseline coverage summary (issue #968, task P0-T17) + +Timestamp: 2026-10-03T02-53 +Command: pwsh -NoProfile -Command '' with STAGE baseline (run with the Bash tool's run_in_background option, no redirection), then pwsh -NoProfile -Command '' with STAGE baseline, RAW True and NAMES-TARGETS; both are the Command Reference macros executed verbatim with PREFIX expanded and WORKTREE substituted +Canonical command: dotnet-coverage collect --output coverage\baseline-968.cobertura.xml --output-format cobertura --settings coverage\effective-coverage-968.config -- vstest.console.exe /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName!~HelperClasses.ShellUtilities_Tests&FullyQualifiedName!~HelperClasses.ShellUtilitiesStatic_Tests&FullyQualifiedName!~HelperClasses.SysImageListHelperTests&FullyQualifiedName!~EmailIntelligence.OSBrowser_Tests" "/ResultsDirectory:coverage\test-results\968\baseline" "/Logger:trx;LogFileName=baseline-968.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 (both payloads) +- COVERAGE-ROUTE: DIRECT +- RAW: True +- COLLECT_EXIT_CODE: 0 +- ASSEMBLY_COUNT: 9 +- ASSEMBLY: \QuickFiler.Test\bin\Debug\QuickFiler.Test.dll +- ASSEMBLY: \SVGControl.Test\bin\Debug\SVGControl.Test.dll +- ASSEMBLY: \Tags.Test\bin\Debug\Tags.Test.dll +- ASSEMBLY: \TaskMaster.Test\bin\Debug\TaskMaster.Test.dll +- ASSEMBLY: \TaskTree.Test\bin\Debug\TaskTree.Test.dll +- ASSEMBLY: \TaskVisualization.Test\bin\Debug\TaskVisualization.Test.dll +- ASSEMBLY: \ToDoModel.Test\bin\Debug\ToDoModel.Test.dll +- ASSEMBLY: \UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll +- ASSEMBLY: \VBFunctions.Test\bin\Debug\VBFunctions.Test.dll +- SEQUENCE_FILES: 0 +- TRX_PRESENT: True +- DOCUMENT_PRESENT: True +- PAYLOAD-COMPLETE printed +- LINE-FLOOR: MET +- BRANCH-FLOOR: MET +- First-party coverage: lines 56206/65855 (85.35%), branches 13617/17078 (79.73%) +- ROOT line-rate=0.853481 branch-rate=0.797342 lines-covered=56206 lines-valid=65855 branches-covered=13617 branches-valid=17078 +- TEST_ASSEMBLY_PACKAGES: 0 +- QFCDATAMODEL_CLASS_ENTRIES: 0 (recorded) +- CHANGED-CODE-COVERAGE: NOT MEASURED (TEST ASSEMBLY EXCLUDED; QFCDATAMODEL EXCLUDED BY ATTRIBUTE) +- BASELINE-FAILED-SET: (empty) +- BASELINE-STATE: GREEN + +Test-result summary (trx-derived, verbatim between the markers): + +SUMMARY-BEGIN +Test run outcome: Completed +Total 7361, executed 7361, passed 7361, failed 0. +Skipped 0, derived as total minus executed rather than reported by the test platform. +Figures reported verbatim by the test platform: error 0, timeout 0, aborted 0, notExecuted 0, inconclusive 0. +Failed tests: none +SUMMARY-END + +RESULT lines (twelve at baseline; the four pin-count tests do not exist yet): + +- RESULT TransactionGate_WhileThisTestHoldsATransaction_HasExactlyOneUnreleasedAcquisition = Passed +- RESULT DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive = Passed +- RESULT EnsureDispatcher_WhileATransactionHoldsALiveDispatcher_DoesNotReplaceIt = Passed +- RESULT SetThemeLight_FromNormal_SelectsLightNormalTheme = Passed +- RESULT SetThemeDark_FromNormal_SelectsDarkNormalTheme = Passed +- RESULT Transaction_SecondCallerCannotInstallUntilTheFirstRestores = Passed +- RESULT Install_CalledTwiceOnTheSameTransaction_ThrowsInvalidOperationException = Passed +- RESULT EnsureDispatcher_WhenTheFieldIsNull_InstallsAndRestoresOnDispose = Passed +- RESULT BeginTransactionAsync_ZeroBoundWhileThisTestHoldsThePermit_ThrowsTimeoutExceptionAndReleasesNothing = Passed +- RESULT DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle = Passed +- RESULT Transaction_DisposedTwice_DoesNotOverReleaseTheGate = Passed +- RESULT EnsureDispatcher_ScopeDisposedTwice_IsIdempotent = Passed + +MESSAGE lines: none (no non-passed, executed test). + +Branch evaluation (in order): (d) not taken (TRX_PRESENT True, SEQUENCE_FILES 0, exit 0); (c) not taken (TEST_ASSEMBLY_PACKAGES 0); (b) not taken; (a) taken: exit 0 with both floors met, BASELINE-STATE: GREEN. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/csharpier-check-baseline.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/csharpier-check-baseline.md new file mode 100644 index 000000000..adcc75d90 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/csharpier-check-baseline.md @@ -0,0 +1,10 @@ +# Baseline: csharpier check (issue #968, task P0-T9) + +Timestamp: 2026-10-03T02-43 +Command: pwsh -NoProfile -Command 'Set-Location -LiteralPath "WORKTREE"; [System.IO.Directory]::SetCurrentDirectory((Get-Location).Path); Write-Output ("WORKTREE-LEAF: " + (Split-Path -Leaf (Get-Location).Path)); dotnet tool run csharpier check .; "CSHARPIER_EXIT_CODE: $LASTEXITCODE"' +Canonical command: dotnet tool run csharpier check . +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- Checked 1637 files in 5261ms. +- CSHARPIER_EXIT_CODE: 0 (format baseline clean; no path reported) diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/datamodel-set-baseline.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/datamodel-set-baseline.md new file mode 100644 index 000000000..1e024284f --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/datamodel-set-baseline.md @@ -0,0 +1,40 @@ +# Baseline: run of the four datamodel test classes (issue #968, task P0-T15) + +Timestamp: 2026-10-03T02-51 +Command: pwsh -NoProfile -Command '' with ASSEMBLY `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, FILTER `FullyQualifiedName~QuickFiler.Controllers.Tests.QfcDatamodelLivenessTests.|FullyQualifiedName~QuickFiler.Controllers.Tests.QfcDatamodelTeardownTests.|FullyQualifiedName~QuickFiler.Controllers.Tests.QfcInitEmailQueueZeroBatchTests.|FullyQualifiedName~QuickFiler.Controllers.Tests.QfcDatamodelTests.` (FILTER-DATAMODEL), TASKID p0-t15 and an empty NAMES list; the Command Reference CMD-VSTEST macro executed verbatim with PREFIX and TOOLS expanded and WORKTREE substituted +Canonical command: vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER-DATAMODEL" "/ResultsDirectory:coverage\test-results\968\p0-t15" "/Logger:trx;LogFileName=p0-t15.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- VSTEST_EXIT_CODE: 0 +- TRX_PRESENT: True +- SEQUENCE_FILES: 0 +- RESULT_COUNT: 21 +- BASELINE-DATAMODEL-COUNTERS: COUNTERS total=21 executed=21 passed=21 failed=0 +- BASELINE-DATAMODEL-FAILED: NONE + +RESULT lines (trx-derived): + +- TryQueueRemainingMailItemAsync_AfterCleanupNulledFields_ReturnsFalseWithoutThrowing = Passed duration=00:00:00.1230152 +- RemainingLoadActive_AcrossAsyncVoidFirstAwait_StaysTrueWhileLoaderProduces = Passed duration=00:00:00.0006986 +- QfcRemainingQueueAdmission_DeclaresNoScoringDelegate = Passed duration=00:00:00.0078372 +- DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive = Passed duration=00:00:00.0014174 +- ToggleOfflineMode_WhenOnline_AwaitsInjectedFiveMillisecondDelay = Passed duration=00:00:00.0467492 +- TryQueueRemainingMailItemAsync_NullMailItem_DoesNotScoreAddOrHook = Passed duration=00:00:00.0004596 +- Cleanup_CalledTwice_DoesNotThrow = Passed duration=00:00:00.0013325 +- InitEmailQueue_ZeroBatchSize_ReturnsEmptyListWithoutThrowing = Passed duration=00:00:00.1354669 +- InitEmailQueue_ZeroBatchSize_StillStartsBackgroundWorker = Passed duration=00:00:00.0033879 +- DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle = Passed duration=00:00:00.1490163 +- TryQueueRemainingMailItemAsync_HighConfidenceEnabled_AddsBelowThresholdCandidate = Passed duration=00:00:00.0005784 +- QuiesceLoaderAsync_LoaderHangs_ReturnsAtBoundAndLogs = Passed duration=00:00:00.0015069 +- WaitForQueue_WhenWorkerBusyAndQueueShort_AwaitsInjectedTwoHundredMsDelay = Passed duration=00:00:00.0012343 +- TryQueueRemainingMailItemAsync_HighConfidenceEnabled_AddsAndHooksWithoutScoring = Passed duration=00:00:00.1258511 +- Worker_DoWork_CapturesRemainingLoadTask = Passed duration=00:00:00.0009340 +- TryQueueRemainingMailItemAsync_HighConfidenceDisabled_AddsAndHooksWithoutScoring = Passed duration=00:00:00.0005803 +- RemainingLoadActive_AfterLoaderCompletes_BecomesFalse = Passed duration=00:00:00.0015370 +- ScoreRemainingQueueMailItemAsync_ReturnsScoreAndTopFolder = Passed duration=00:00:00.0178297 +- RemainingLoadActive_WhenLoaderThrows_IsStillClearedByFinally = Passed duration=00:00:00.0010153 +- InitEmailQueue_PositiveBatchSize_RetainsExistingProjectionAndFrameDrop = Passed duration=00:00:00.1868361 +- QuiesceLoaderAsync_LoaderCompletes_ReturnsBeforeTimeout = Passed duration=00:00:00.0062247 + +No MESSAGE lines were printed. No Timeout or Aborted outcome and no Sequence file (no BASELINE HANG). diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/fold-census-baseline.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/fold-census-baseline.md new file mode 100644 index 000000000..bfa2d82a8 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/fold-census-baseline.md @@ -0,0 +1,44 @@ +# Pre-change census of the folded-scope files and the project file (issue #968, task P0-T13) + +Timestamp: 2026-10-03T02-49 +Command: pwsh -NoProfile -Command '' with FILES = FOLD6 (the first of thirteen separate payloads listed below; each is the named Command Reference macro executed verbatim with PREFIX expanded, WORKTREE substituted and the stated FILE, FILES, START, END and TOKENS substituted) +Canonical command: CMD-LINECOUNT, CMD-HASH, CMD-TOKEN-COUNT (LIV, TD, ZB, DMT, QDM, QQP, PROJ) and CMD-SPAN-TOKEN-COUNT (T1-LIVE, HELD, T-SIB, GATE-LAMBDA) +EXIT_CODE: 0 +Output Summary: WORKTREE-LEAF agent-a291a7fbabf9d0229 in every payload; every payload exited 0; LINES 312, 244, 232, 371, 495, 413; every token and span value equals the plan's expected pre-change value (no FOLD CENSUS MISMATCH). Details below. + +## CMD-LINECOUNT on FOLD6 + +- LINES QuickFiler.Test\Controllers\QfcDatamodelLivenessTests.cs = 312 +- LINES QuickFiler.Test\Controllers\QfcDatamodelTeardownTests.cs = 244 +- LINES QuickFiler.Test\Controllers\QfcInitEmailQueueZeroBatchTests.cs = 232 +- LINES QuickFiler.Test\Controllers\QfcDatamodelTests.cs = 371 +- LINES QuickFiler\Controllers\QfcDatamodel.cs = 495 +- LINES QuickFiler\Controllers\QfcDatamodel.QueueProcessing.cs = 413 + +## CMD-HASH on FOLD6 + +- BASE-HASH: QuickFiler.Test\Controllers\QfcDatamodelLivenessTests.cs = 04A1963C8D577FB6FD43079DD05446600399832EC3438971D08503CB2B11870A +- BASE-HASH: QuickFiler.Test\Controllers\QfcDatamodelTeardownTests.cs = 0F076832B8ACEC32BA61D822D19397E7ADF9FECE4424C2EC3F73B4D7D277D3F1 +- BASE-HASH: QuickFiler.Test\Controllers\QfcInitEmailQueueZeroBatchTests.cs = 4B3D6D3FD67ABC2F583EEABB6FAFEC22D3561A7B72AEC553556F31C2A30B32EE +- BASE-HASH: QuickFiler.Test\Controllers\QfcDatamodelTests.cs = B9AB3F7B59001008DBA5A7ADB31D85455A2E7F05A8C09715039FE0F2F36D6DB5 +- BASE-HASH: QuickFiler\Controllers\QfcDatamodel.cs = B06004A654EB1630B4759D378271187BA252F1FCC44486B9DFF9B4B08C24491F +- BASE-HASH: QuickFiler\Controllers\QfcDatamodel.QueueProcessing.cs = B54E4CE3654FC22E572A46D2AD78CBD28C9342E650E5B3D8C1861139990A0DBB + +## CMD-TOKEN-COUNT (token lists exactly as in the P0-T13 task text, in that order) + +- LIV: 1, 3, 0, 2, 0, 4, 3, 3, 1, 0, 0, 0, 0, 4 (`FakeTimeProvider` 1 is line 114 only; the `using Microsoft.Extensions.Time.Testing;` directive does not contain the token) +- TD: 1, 2, 0, 1, 1, 0, 5 +- ZB: 1, 4, 0, 3, 0, 1, 1, 1, 1, 1, 0, 3 +- DMT: 2, 0, 0, 0, 0, 1, 0, 1, 4, 5, 0, 9 +- QDM: 2, 4, 0, 1, 4, 1, 1, 1, 2, 2, 7, 7, 1, 2, 1, 1, 2, 1, 2, 2, 3 +- QQP: 1, 1, 2, 1, 3, 0, 0, 1, 1, 0, 1 +- PROJ: ` result = await pending;` 1 +- GATE-LAMBDA (QQP): SPAN: 299-310; `() => _remainingLoadActive,` 1, `() => false,` 0 + +Each printed span end is the END anchor line minus one. The non-zero counts (three nested worker classes, the old-shape `Task.Yield` and retry loop, the four legacy members, the stale comment tokens) are the positive controls for the zero gates of P4-T9, P5-T5, P5-T12 and P6-T2. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/msbuild-analyzer-baseline.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/msbuild-analyzer-baseline.md new file mode 100644 index 000000000..20ee069a5 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/msbuild-analyzer-baseline.md @@ -0,0 +1,21 @@ +# Baseline: analyzer rebuild (issue #968, task P0-T10) + +Timestamp: 2026-10-03T02-45 +Command: pwsh -NoProfile -Command 'Set-Location -LiteralPath "WORKTREE"; [System.IO.Directory]::SetCurrentDirectory((Get-Location).Path); Write-Output ("WORKTREE-LEAF: " + (Split-Path -Leaf (Get-Location).Path)); $vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; New-Item -ItemType Directory -Path "coverage\logs" -Force | Out-Null; $log = "coverage\logs\p0-t10.msbuild.log"; if (Test-Path -LiteralPath $log) { Remove-Item -LiteralPath $log -Force }; $global:LASTEXITCODE = 0; & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true "/flp:LogFile=$log;Verbosity=normal" | Out-Null; Write-Output ("MSBUILD_EXIT_CODE: " + $LASTEXITCODE); $lines = Get-Content -LiteralPath $log -Encoding UTF8; Write-Output ("ERRORS: " + (($lines | Select-String -Pattern "^\s*(\d+) Error\(s\)" | Select-Object -Last 1).Matches[0].Groups[1].Value)); Write-Output ("WARNINGS: " + (($lines | Select-String -Pattern "^\s*(\d+) Warning\(s\)" | Select-Object -Last 1).Matches[0].Groups[1].Value)); Write-Output ("SKIP_CORECOMPILE_LINES: " + @($lines | Where-Object { $_.Contains("Skipping target ""CoreCompile""") }).Count); Write-Output ("CSC_OUT_QUICKFILER: " + @($lines | Where-Object { $_.Contains("/out:obj\Debug\QuickFiler.dll") }).Count); Write-Output ("CSC_OUT_QUICKFILER_TEST: " + @($lines | Where-Object { $_.Contains("/out:obj\Debug\QuickFiler.Test.dll") }).Count); $files = @("QfcItemController.UiThreadDispatcherFixture.cs(", "QfcItemController.FocusAndThemeTests.cs(", "QfcItemController.TestSupport.cs(", "QfcItemController.UiThreadDispatcherFixtureTests.cs(", "QfcItemController.UiThreadDispatcherPinCountTests.cs(", "QuickFiler.Test.csproj(", "SynchronousBackgroundWorker.cs(", "ArmingFakeTimeProvider.cs(", "QfcDatamodelLivenessTests.cs(", "QfcDatamodelTeardownTests.cs(", "QfcInitEmailQueueZeroBatchTests.cs(", "QfcDatamodelTests.cs(", "QfcDatamodel.cs(", "QfcDatamodel.QueueProcessing.cs("); $diag = @($lines | Where-Object { $l = $_; (@($files | Where-Object { $l.Contains($_) }).Count -gt 0) -and ($l -match "(error|warning) [A-Z]+[0-9]+") }); Write-Output ("WRITESET_DIAGNOSTIC_LINES: " + $diag.Count); Write-Output ("WRITESET_DIAGNOSTIC_CODES: " + ((@($diag | ForEach-Object { [regex]::Match($_, "(error|warning) ([A-Z]+[0-9]+)").Groups[2].Value }) | Sort-Object -Unique) -join ",")); Write-Output ("TEST_DLL_EXISTS: " + (Test-Path -LiteralPath "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll")); Write-Output ("UCS_TEST_DLL_EXISTS: " + (Test-Path -LiteralPath "UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll"))' (CMD-REBUILD with the analyzer GATEARGS and TASKID p0-t10; the executed payload separates statements with newlines, shown here as semicolons) +Canonical command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true (resolved through vswhere against WORKTREE/TaskMaster.sln, plus /nodeReuse:false and a normal-verbosity file logger under the ignored coverage directory) +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- MSBUILD_EXIT_CODE: 0 +- ERRORS: 0 +- WARNINGS: 0 +- SKIP_CORECOMPILE_LINES: 0 +- CSC_OUT_QUICKFILER: 2 +- CSC_OUT_QUICKFILER_TEST: 2 +- WRITESET_DIAGNOSTIC_LINES: 0 +- WRITESET_DIAGNOSTIC_CODES: (empty) +- TEST_DLL_EXISTS: True +- UCS_TEST_DLL_EXISTS: True +- ANALYZER-BASELINE-WARNINGS: 0 +- ANALYZER-BASELINE-WRITESET-LINES: 0 +- ANALYZER-BASELINE-WRITESET-CODES: (empty) diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/msbuild-nullable-baseline.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/msbuild-nullable-baseline.md new file mode 100644 index 000000000..151a79bb3 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/msbuild-nullable-baseline.md @@ -0,0 +1,18 @@ +# Baseline: TreatWarningsAsErrors rebuild (issue #968, task P0-T11) + +Timestamp: 2026-10-03T02-46 +Command: pwsh -NoProfile -Command '' with GATEARGS `/p:TreatWarningsAsErrors=true` and TASKID p0-t11; the executed payload is identical, line for line, to the one transcribed in full in FEATURE/evidence/baseline/msbuild-analyzer-baseline.md except that `/p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` is replaced by `/p:TreatWarningsAsErrors=true` and the log is `coverage\logs\p0-t11.msbuild.log` +Canonical command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true (no Nullable property override; resolved through vswhere against WORKTREE/TaskMaster.sln, plus /nodeReuse:false and a normal-verbosity file logger under the ignored coverage directory) +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- MSBUILD_EXIT_CODE: 0 +- ERRORS: 0 +- WARNINGS: 0 +- SKIP_CORECOMPILE_LINES: 0 +- CSC_OUT_QUICKFILER: 2 +- CSC_OUT_QUICKFILER_TEST: 2 +- WRITESET_DIAGNOSTIC_LINES: 0 +- WRITESET_DIAGNOSTIC_CODES: (empty) +- TEST_DLL_EXISTS: True +- UCS_TEST_DLL_EXISTS: True diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/phase0-commit.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/phase0-commit.md new file mode 100644 index 000000000..e11e4b66b --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/phase0-commit.md @@ -0,0 +1,36 @@ +# Phase 0 commit (issue #968, task P0-T19) + +Timestamp: 2026-10-03T02-54 +Command: git -C WORKTREE add -- docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968 +Canonical command: pathspec-limited git add of FEATURE, then a pathspec-limited git commit of the same pathspec (separate Bash calls) +EXIT_CODE: 0 +Output Summary: +- git add exit 0 (LF-to-CRLF working-copy warnings only) +- git -C WORKTREE commit -m "docs(968): record phase 0 baseline evidence" -- docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968 -> exit 0; 19 files changed, 586 insertions(+), 18 deletions(-) +- git -C WORKTREE rev-parse HEAD -> exit 0 +- PHASE0-COMMIT: 74b7c6bd078535cd439c7c77851eb09abb4f60e7 (observation) +- git -C WORKTREE show --name-status --format= HEAD -> exit 0 +- git -C WORKTREE status --porcelain --untracked-files=all -> exit 0, empty output at the time it ran (this artifact and the plan check-off mark were written afterwards) + +PHASE0-COMMIT-PATHS: +- A FEATURE/evidence/baseline/analyzer-alignment.md +- A FEATURE/evidence/baseline/bootstrap-dotnet-coverage.md +- A FEATURE/evidence/baseline/bootstrap-nuget-restore.md +- A FEATURE/evidence/baseline/bootstrap-sdk.md +- A FEATURE/evidence/baseline/bootstrap-tool-restore.md +- A FEATURE/evidence/baseline/census-baseline.md +- A FEATURE/evidence/baseline/concurrent-set-baseline.md +- A FEATURE/evidence/baseline/coverage-jacoco-projection.md +- A FEATURE/evidence/baseline/coverage-summary.md +- A FEATURE/evidence/baseline/csharpier-check-baseline.md +- A FEATURE/evidence/baseline/datamodel-set-baseline.md +- A FEATURE/evidence/baseline/fold-census-baseline.md +- A FEATURE/evidence/baseline/msbuild-analyzer-baseline.md +- A FEATURE/evidence/baseline/msbuild-nullable-baseline.md +- A FEATURE/evidence/baseline/phase0-instructions-read.md +- A FEATURE/evidence/baseline/scope-and-anchor.md +- A FEATURE/evidence/baseline/stall-probe.md +- A FEATURE/evidence/baseline/toolchain-baseline.md +- M FEATURE/plan.2026-10-02T05-42.md + +Every committed path is under FEATURE (FEATURE = docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968); no COMMIT SWEPT FOREIGN PATH. No porcelain line names a path under QuickFiler/ or QuickFiler.Test/. The commit message omits the attribution trailer because D-10 prohibits angle brackets in commit messages. This artifact is committed in P6-T9. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/phase0-instructions-read.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/phase0-instructions-read.md new file mode 100644 index 000000000..1c0441d16 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/phase0-instructions-read.md @@ -0,0 +1,17 @@ +# Phase 0 instructions read (issue #968, task P0-T1) + +Timestamp: 2026-10-03T02-41 +Policy Order: CLAUDE.md, then .claude/rules/general-code-change.md, then .claude/rules/general-unit-test.md, then .claude/rules/csharp.md + +Files read (item worktree copies, in this order): + +1. CLAUDE.md (all sections, including the embedded General Code Change, General Unit Test, C# Code Change and C# Unit Test policies, the C# toolchain order and the Committed Test Evidence Format) +2. .claude/rules/general-code-change.md +3. .claude/rules/general-unit-test.md +4. .claude/rules/csharp.md +5. .claude/rules/plan-acceptance-gates.md +6. .claude/rules/tonality.md +7. .claude/skills/evidence-and-timestamp-conventions/SKILL.md +8. .claude/skills/acceptance-criteria-tracking/SKILL.md + +No policy document was modified. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/scope-and-anchor.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/scope-and-anchor.md new file mode 100644 index 000000000..d89f098f7 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/scope-and-anchor.md @@ -0,0 +1,99 @@ +# Scope and anchor (issue #968, tasks P0-T2 and P0-T3) + +## P0-T2 Scope read + +Timestamp: 2026-10-03T02-41 +Command: pwsh -NoProfile -Command 'Set-Location -LiteralPath "WORKTREE"; [System.IO.Directory]::SetCurrentDirectory((Get-Location).Path); Write-Output ("WORKTREE-LEAF: " + (Split-Path -Leaf (Get-Location).Path)); $src = Get-Content -LiteralPath "docs\features\active\2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968\spec.md" -Encoding UTF8; foreach ($t in @("- [ ] AC", "- [x] AC", "- [ ] AC32:", "Amendment 1.2", "Amendment 1.1", "acquired and released inside a held", "the removal of its baseline pin", "inherited committed set")) { Write-Output ("TOKEN [" + $t + "] = " + @($src | Where-Object { $_.Contains($t) }).Count) }' +Canonical command: CMD-TOKEN-COUNT on FEATURE/spec.md +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- TOKEN [- [ ] AC] = 32 +- TOKEN [- [x] AC] = 0 +- TOKEN [- [ ] AC32:] = 1 +- TOKEN [Amendment 1.2] = 1 +- TOKEN [Amendment 1.1] = 1 +- TOKEN [acquired and released inside a held] = 4 +- TOKEN [the removal of its baseline pin] = 1 +- TOKEN [inherited committed set] = 2 +- All five amendment literals are at least 1 (fact 22 values 1, 1, 4, 1, 2 reproduced); no SPEC AMENDMENT MISSING. + +Documents read in full: FEATURE/spec.md (amendment 1.2), FEATURE/issue.md (including the Coordinator Scope Amendment), FEATURE/research/2026-10-02T05-50-dispatcher-pin-call-sites-research.md, FEATURE/research/2026-10-02T22-20-qfc-datamodel-972-fold-research.md. + +Issue metadata: issue.md line 12 reads `- Work Mode: full-bug`; the heading `## Coordinator Scope Amendment (2026-10-02T22-15, binding)` exists (line 65). + +Write Set (fourteen code paths, verbatim): + +- QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs +- QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs +- QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs +- QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs +- QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs +- QuickFiler.Test/QuickFiler.Test.csproj +- QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs +- QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs +- QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs +- QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs +- QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs +- QuickFiler.Test/Controllers/QfcDatamodelTests.cs +- QuickFiler/Controllers/QfcDatamodel.cs +- QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs + +Prohibited paths (from the plan Write Set section): every file under QuickFiler/ other than the two production Write Set paths; every file under UtilitiesCS/, UtilitiesCS.Test/ and every other project; QuickFiler.Test/Helper Classes/EmailMoveMonitorTests.cs; every other file under QuickFiler.Test/ not listed above (including QfcDatamodelRethrowTests.cs, QfcQueuePurePathsTests.cs and QfcHomeControllerRunAsyncHighConfidenceTests.Part3.cs); QuickFiler/Controllers/QfcDatamodel.FrameBuilding.cs; QuickFiler/Interfaces/IQfcDatamodel.cs; TaskMaster.runsettings; scripts/vscode/TaskMaster.cli.runsettings; every file under scripts/; every file under .github/; every file under .claude/; every file under docs/features/potential/; the content of both research documents. + +Acceptance-criteria inventory: FEATURE/spec.md section `## Acceptance Criteria`, thirty-two checkbox lines AC1 to AC32, all unchecked. + +Promoted records present on the tree (read with the Read tool): + +- docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md (heading `# focus-and-theme-tests-leak-shared-dispatcher-setup (Issue #968)`) +- docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md (heading `# qfc-datamodel-950-review-residuals (Issue #972)`) + +## P0-T3 Anchor and pre-change tree state + +Timestamp: 2026-10-03T02-41 +Commands (separate Bash calls, in order): +1. git -C WORKTREE rev-parse HEAD -> exit 0 +2. git -C WORKTREE rev-parse --abbrev-ref HEAD -> exit 0 +3. git -C WORKTREE rev-parse origin/main -> exit 0 +4. git -C WORKTREE merge-base --is-ancestor 94287369908cc920b21b0e3256314f988ad7d2f5 HEAD -> exit 0 +5. git -C WORKTREE merge-base origin/main HEAD -> exit 0 +6. git -C WORKTREE diff --name-status 94287369908cc920b21b0e3256314f988ad7d2f5 HEAD -> exit 0 +7. git -C WORKTREE diff --exit-code 94287369908cc920b21b0e3256314f988ad7d2f5 HEAD -- QuickFiler QuickFiler.Test UtilitiesCS/Threading/UiThread.cs UtilitiesCS/HelperClasses/ThemeHelpers/Theme.cs scripts/vscode TaskMaster.runsettings coverage.config .csharpierignore .gitignore global.json dotnet-tools.json -> exit 0 +8. git -C WORKTREE status --porcelain --untracked-files=all -> exit 0 + +- HEAD-SHA: 911558bac7dd59aca5f6255cff45e33842511308 (observation) +- BRANCH: bug/focus-and-theme-tests-leak-shared-dispatcher-setup-968 +- ORIGIN-MAIN-SHA: 993fdd01566dee82e5f37acb761a600feaaa1454 (observation; the P8-T9 comparison basis) +- Ancestor check: exit 0 (BASE is an ancestor of HEAD) +- Merge-base output: 94287369908cc920b21b0e3256314f988ad7d2f5 (equals BASE) +- CODE-TREE-AT-BASE: UNCHANGED (command 7 exit 0) + +INHERITED-COMMITTED: +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/planner-review.2026-10-02T22-44.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-clearance.2026-10-03T02-21.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round1-report.2026-10-02T08-40.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round1.2026-10-02T08-40.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round2-report.2026-10-02T23-56.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round2.2026-10-03T00-12.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round3-report.2026-10-03T01-01.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round3.2026-10-03T01-15.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round4-report.2026-10-03T01-25.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round4.2026-10-03T01-43.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round5-report.2026-10-03T01-53.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round6-report.2026-10-03T02-21.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/issue.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/plan.2026-10-02T05-42.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T05-50-dispatcher-pin-call-sites-research.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T22-20-qfc-datamodel-972-fold-research.md +- A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md +- A docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md +- A docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md + +Every inherited path is under FEATURE or is one of the two promoted records (no INHERITED SET OUT OF SCOPE). + +PRE-EXISTING-WORKTREE-PATHS: +- ` M docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/plan.2026-10-02T05-42.md` +- `?? docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/phase0-instructions-read.md` +- `?? docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/scope-and-anchor.md` + +No porcelain line names a path under QuickFiler/ or QuickFiler.Test/ (no CODE TREE DIRTY AT ANCHOR). diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/stall-probe.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/stall-probe.md new file mode 100644 index 000000000..7e20f23a8 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/stall-probe.md @@ -0,0 +1,21 @@ +# Stall probe (issue #968, task P0-T16) + +Timestamp: 2026-10-03T02-51 +Command: pwsh -NoProfile -Command '' with ASSEMBLY `UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll`, FILTER `FullyQualifiedName~HelperClasses.ShellUtilities_Tests|FullyQualifiedName~HelperClasses.ShellUtilitiesStatic_Tests|FullyQualifiedName~HelperClasses.SysImageListHelperTests|FullyQualifiedName~EmailIntelligence.OSBrowser_Tests` (FILTER-STALL), TASKID p0-t16 and an empty NAMES list; the Command Reference CMD-VSTEST macro executed verbatim with PREFIX and TOOLS expanded and WORKTREE substituted +Canonical command: vstest.console.exe UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER-STALL" "/ResultsDirectory:coverage\test-results\968\p0-t16" "/Logger:trx;LogFileName=p0-t16.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 1 +ExpectedExitCode: 1 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- VSTEST_EXIT_CODE: 1 (ExpectedExitCode carries the observed value; presentational) +- TRX_PRESENT: True +- SEQUENCE_FILES: 0 +- COUNTERS total=23 executed=23 passed=22 failed=1 +- RESULT_COUNT: 23 +- MESSAGE GetFileIcon_WithUseFileType_ShouldReturnIconsForDirectoryAndFileExtension :: Test method UtilitiesCS.Test.HelperClasses.ShellUtilities_Tests.GetFileIcon_WithUseFileType_ShouldReturnIconsForDirectoryAndFileExtension threw exception: System.ArgumentException: Win32 handle that was passed to Icon is not valid or is the wrong type. +- The run did not stall (no Sequence file, no Timeout or Aborted outcome); one test failed fast. + +STALL-PROBE: REPRODUCES +COVERAGE-ROUTE: DIRECT + +Rule applied mechanically: CLEAR requires EXIT_CODE 0, failed=0 and SEQUENCE_FILES 0; the observed exit is 1 with failed=1, so the value is REPRODUCES and the route is DIRECT (D-6). The probe ran once and is not re-run. Consequence recorded per D-6 and the Risks section: AC22 cannot be met as worded under DIRECT and its check-off will record `AC22: NOT MET (ENVIRONMENTAL: COVERAGE-ROUTE DIRECT)` for the orchestrator's decision. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/toolchain-baseline.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/toolchain-baseline.md new file mode 100644 index 000000000..b28e3c45d --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/toolchain-baseline.md @@ -0,0 +1,15 @@ +# Baseline toolchain index (issue #968, task P0-T18) + +Timestamp: 2026-10-03T02-53 + +This file is an index over the per-step baseline artifacts, not a substitute for them. + +| Step | Canonical command | EXIT_CODE | Artifact | +|---|---|---|---| +| 1. csharpier check | `dotnet tool run csharpier check .` | 0 | FEATURE/evidence/baseline/csharpier-check-baseline.md | +| 2. analyzer rebuild | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` | 0 | FEATURE/evidence/baseline/msbuild-analyzer-baseline.md | +| 3. TreatWarningsAsErrors rebuild | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` | 0 | FEATURE/evidence/baseline/msbuild-nullable-baseline.md | +| 4. coverage run (route DIRECT) | `dotnet-coverage collect ... -- vstest.console.exe ... "/TestCaseFilter:TestCategory!=LiveOutlook&"` (CMD-COVERAGE-DIRECT, then CMD-COVERAGE-POST) | 0 | FEATURE/evidence/baseline/coverage-summary.md | + +BASELINE-STATE: GREEN +First-party coverage: lines 56206/65855 (85.35%), branches 13617/17078 (79.73%) diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/ac-status-summary.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/ac-status-summary.md new file mode 100644 index 000000000..b553d79a3 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/ac-status-summary.md @@ -0,0 +1,32 @@ +# Acceptance-criteria status summary (issue #968, task P8-T43) + +Timestamp: 2026-10-03T03-36 + +### Acceptance Criteria Status +- Source: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md (section `## Acceptance Criteria`; work mode full-bug) +- Total AC items: 32 +- Checked off (delivered): 31 (lines beginning `- [x] AC`, read from spec.md after P8-T42) +- Remaining (unchecked): 1 (lines beginning `- [ ] AC`) +- Items remaining: + - AC22: Full toolchain pass: csharpier check, the analyzer rebuild, the warnings-as-errors rebuild and the MSTest coverage route complete in that order with every step passing in one uninterrupted pass after the last edit, the two rebuild logs contain no skipped compile target, and the commands with exit codes are recorded in this feature's qa-gates evidence folder. + - AC22: NOT MET (ENVIRONMENTAL: COVERAGE-ROUTE DIRECT). Reason: P0-T16 recorded STALL-PROBE: REPRODUCES (the four UtilitiesCS.Test shell-icon classes did not stall, but `ShellUtilities_Tests.GetFileIcon_WithUseFileType_ShouldReturnIconsForDirectoryAndFileExtension` failed with `Win32 handle that was passed to Icon is not valid or is the wrong type`), so the coverage step ran by the DIRECT route rather than the runner `scripts/vscode/Invoke-MSTestWithCoverage.ps1` verbatim. Every step of the final toolchain pass exited 0 in one iteration (SINGLE-PASS: YES), but RUNNER-GREEN is NO, so FEATURE/evidence/qa-gates/toolchain-final.md reads AC22-STATUS: NOT MET. The decision belongs to the orchestrator (D-6). This criterion is not CI-dependent as worded: it concerns the local runner route. + +Check-off records: +- AC1 to AC21 and AC23 to AC32: MET and checked off by tasks P8-T11 to P8-T42, each against the evidence named in the plan's AC identity table. +- AC14 check-off (P8-T24): CLOSES-972-ITEM-5: YES (the `transactionA` try/finally in R4). +- AC23 (P8-T33): AC23-STATUS: MET (first-party lines 85.35% to 85.36%, branches 79.73% to 79.75%; deltas +0.01 and +0.02). +- AC29 (P8-T39): MET (`[ExcludeFromCodeCoverage]` 1; every test-file hit of the removed members is DOC-PROSE or OTHER-TYPE-SAME-NAME; AC23-STATUS: MET). + +CI-dependent criteria: none of the remaining items requires a CI result to verify; AC22 is pending an orchestrator ruling on the environmental route. + +## Spec check-off diff + +Timestamp: 2026-10-03T03-37 +Command: git -C WORKTREE diff --numstat HEAD -- docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md +Canonical command: git -C WORKTREE diff --numstat HEAD -- FEATURE/spec.md; git -C WORKTREE diff HEAD -- FEATURE/spec.md; git -C WORKTREE status --porcelain -- FEATURE/spec.md (separate Bash calls) +EXIT_CODE: 0 +Output Summary: +- numstat (exit 0): `31 31 docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md`: added and deleted line counts are equal (31 and 31) and equal the checked-off count (31) +- diff (exit 0): a single hunk `@@ -272,38 +272,38 @@` inside `## Acceptance Criteria`; the 31 deleted lines are `- [ ] AC1:` to `- [ ] AC21:` and `- [ ] AC23:` to `- [ ] AC32:`, each beginning `- [ ] AC`; the 31 added lines are the same criteria beginning `- [x] AC` with the remaining text of each line identical to its deleted counterpart; the `- [ ] AC22:` line is an unchanged context line +- porcelain (exit 0): ` M docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md` +- spec.md changed only in its checkbox lines. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/planner-review.2026-10-02T22-44.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/planner-review.2026-10-02T22-44.md new file mode 100644 index 000000000..4a3d0a253 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/planner-review.2026-10-02T22-44.md @@ -0,0 +1,480 @@ +# Planner review records, revision round 1 (issue #968, with #972 folded in) + +- Timestamp: 2026-10-02T22-44 +- Plan: plan.2026-10-02T05-42.md (revised in place; version 1.1) +- Scope of this pass: the ten round-1 deltas (option B of defect 2 superseded as directed), spec amendment 1.2 (AC25 to AC32, amended AC20), and a full re-derivation of every citation the deltas or the fold touch, plus their sibling regions. +- Tooling in this session: Read, Grep, Glob, Edit, Write. No shell (the Bash tool was not available), so git state was taken from the worktree's `.git` metadata and the coordinator's statement; every file and line citation was re-read from the item worktree. + +SELF-REVIEW: RE-DERIVED THIS PASS + +Citations re-derived in this pass (file and line, test or identifier): + +1. QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs — 342 lines (Grep `^`); `lock (FieldLock)` 66, 79, 94, 128 (4); `CompareExchange(` 92, 271, 336 (3); every line CRLF. Sibling check: the F-FIELDS comment now reads `only while FieldLock is held`, so the post-change `lock (FieldLock)` count is 5, not 6 (round-1 defect 3). +2. QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs — 470 lines; `[TestMethod]` 8; the 20-line repository-wide `EnsureUiThreadDispatcher|EnsureDispatcher` census (fixture 5, test support 2, fixture tests 10, focus-and-theme 2, InitializationTests.Part2 1) and the 23-line `BeginTransactionAsync\(` control (6 files) re-counted. +3. QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs — 497 lines, `[TestMethod]` 17. QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs — 440 lines. (The round-1 reviewer verified the remaining line citations of these four files against the same tree, and P0-T3's `CODE-TREE-AT-BASE` gate re-asserts that no code path moved since BASE.) +4. QuickFiler.Test/QuickFiler.Test.csproj — Compile items 155, 157, 161, 183 (datamodel tests), 200, 201, 203, 212, 227 (`TestSupport\WinFormsPumpHost.cs`), 228 (`TestSupport\DedicatedWorkerThread.cs`), 229 (`TestSupport\WinFormsPumpHostTests.cs`); no item for the three new files. +5. QuickFiler.Test/TestSupport/DedicatedWorkerThread.cs — namespace `QuickFiler.Test.TestSupport` (line 4), `internal static class` (line 21); 48 lines; no `#nullable`. Nine files carry `using QuickFiler.Test.TestSupport;` (Grep), for example QfcItemController.SeamFactoryTests.cs line 13 between `using QuickFiler.Interfaces;` and `using TaskVisualization;`. +6. QuickFiler/Controllers/QfcDatamodel.cs — 495 lines; `[ExcludeFromCodeCoverage]` 25; `log` field 109-111; `//worker.RunWorkerCompleted` 194; `//e.Result =` 209-210; `_remainingLoadTask = loaderTask;` 218; `_remainingLoadActive = false;` 227; `Worker_RunWorkerCompleted` 243-265 with blank 242; `InitEmailQueue` 271-315 (`_remainingLoadActive = true;` 284, 311; `WorkerStarter(worker);` 285, 312); one-argument `LoadRemainingEmailsToQueueAsync` 335-376 (commented `nameof` 363, live `nameof(LoadRemainingEmailsToQueue)` 369); synchronous `LoadRemainingEmailsToQueue` 378-416; two-argument `LoadRemainingEmailsToQueueAsync` 418-465 (`#pragma` 436, 457); empty region 469-472; `Application_NewMailEx` 476-491; seven `#region`/`#endregion` pairs; removal total 128 lines, expected 367 after. +7. Zero-caller proof (fact 15): Grep `Worker_RunWorkerCompleted|LoadRemainingEmailsToQueue\b|LoadRemainingEmailsToQueueAsync|Linked List Locking` over `*.cs` = 24 lines in 5 files; `\blog\b` over QfcDatamodel*.cs = 3 lines; string-literal/reflection sweep = 2 lines (QfcHomeControllerRunAsyncTests.cs 376 invoked on `_controller` at 373-380; QfcDatamodel.cs 130 cref); IQfcDatamodel.cs members at 103, 117, 131, 138-148, 164, 166 (none of the four); InternalsVisibleTo grants at QuickFiler/Properties/AssemblyInfo.cs 5, QuickFiler/Controllers/QfcHomeController.cs 15, QuickFiler/Legacy/IAcceleratorCallbacks.cs 5, QuickFiler/Controllers/QfcHighConfidencePreFilter.cs 11; QfcDatamodel.FrameBuilding.cs references none of the four. +8. QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs — 413 lines; `_remainingLoadActive` doc 15-23 (`RunWorkerAsync` 17, `written on the worker thread and read` 22), declaration 24; `_remainingLoadTask` 43; QuiesceLoaderAsync comment `written on the worker thread` 52; `TryUnhookOrReplace` 146, 285 (with `(:31-66)`), 364; gate construction 299-309 with `() => _remainingLoadActive,` 305; `await gate.DequeueAsync` 311; `WaitForQueue` 404-411. Repository-wide `_remainingLoadActive|_remainingLoadTask` = 20 lines in 7 files. +9. QuickFiler/Controllers/QfcStreamingDequeueConfidenceGate.cs — `DequeueAsync` 190-301; `alreadyWaitedForEmptySource` 215, 252; empty-take branch 244-257 with `ConfigureAwait(false)` at 255. +10. QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs — 312 lines, `[TestMethod]` 4; usings 9 and 12-13; nested worker 47-61; DrainableSynchronizationContext 62-87; test 1 100-164 (`new FakeTimeProvider()` 114, worker 127-128, `Task.Yield` 140, 142, 157, `fake.Advance` 139, 141, 156, retry loop 154); ReadLivenessFlag 166-172; StartHeldOpenLoader 174-209 (worker 201-202); callers 221, 249, 285; `FakeTimeProvider` only at 9 and 114; spans T1-LIVE 110-166 and HELD 183-218. +11. QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs — 244 lines, `[TestMethod]` 5; nested worker 59-74; `using` blocks at 180 and 220; starter 222; `Duplicated per file` 63. +12. QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs — 232 lines, `[TestMethod]` 3; remarks 33; doc 97-99; nested worker 114-128; sites 143/148, 172/173/176, 202/221; `Duplicated per file` 117. +13. QuickFiler.Test/Controllers/QfcDatamodelTests.cs — 371 lines, `[TestMethod]` 9; sibling test 95-131 (`new BackgroundWorker()` 108, `Task.Yield` 119, because text 123, `await pending` 128); WaitForQueue test 253-283 (`new BackgroundWorker()` 261); `fake.Advance` 118, 127, 241, 280 (4); `FakeTimeProvider` 9, 99, 216, 224, 249, 258 (6); span T-SIB 96-134. +14. UtilitiesCS.Test/TestHelpers/ArmingBarrierTimeProvider.cs — 55 lines; `Armed` 26, `ReArm` 28, `CreateTimer` 39-49, `NewSignal` 52-53. QuickFiler.Test/Controllers/QfcFormControllerSeamTests.cs — `CountingTimeProvider : FakeTimeProvider` 358-367 with the `CreateTimer` override at 362. `ArmingFakeTimeProvider|NoSynchronizationContext` over `*.cs`: 0 hits. +15. Worktree git metadata: `.git/worktrees/agent-a291a7fbabf9d0229/HEAD` reads `ref: refs/heads/bug/focus-and-theme-tests-leak-shared-dispatcher-setup-968`; that ref is `87cca65ede6a900cb5c4e5cce93ff4409ed08730`. Both promoted records exist under docs/features/potential/promoted/ (Glob). +16. docs/features/active/.../spec.md — 335 lines; `- [ ] AC` 32 (lines 275-306), `- [x] AC` 0, `Amendment 1.2` 1, `Amendment 1.1` 1, `acquired and released inside a held` 3, `the removal of its baseline pin` 1, `inherited committed set` 2 (36 distinct matching lines). issue.md line 12 and the Coordinator Scope Amendment at 65-77. +17. Round-1 report and summary (evidence/other/preflight-round1*.2026-10-02T08-40.md) read in full; each of the ten deltas traced to its plan location. + +Sibling-region re-checks: the N1 test 4 replacement changes the per-file PC counts (`EnsureUiThreadDispatcher()` 10, `.BeginTransactionAsync()` 5, `foreignTransaction.Install(parked);` 1) and therefore the census (PRIMARY 16, CROSS 32, CONTROL 28, CROSS-only still 16) and the nesting gate (13 of 13); the `transaction.Dispose();` and `transaction.Install(null);` counts are unchanged because the match is case-sensitive. Removing `using Microsoft.Extensions.Time.Testing;` from the Liveness file is deferred to the test-1 rewrite (L1b in P5-T3) because test 1 still constructs a `FakeTimeProvider` after the Phase 4 edits; QfcDatamodelTests keeps that using (two other tests use it). The `written on the worker thread` token stays at 1 after the change because the unchanged QuiesceLoaderAsync comment at line 52 carries it; AC26's token is the longer `written on the worker thread and read`. The `nameof(LoadRemainingEmailsToQueueAsync)` count is 1 before (the commented line 462) and 1 after (the retargeted line 369), so the discriminating tokens are the `} Error.` and `} Task cancelled` suffixed forms. `#region`/`#endregion` substring counts do not overlap (`#endregion` does not contain `#region`). The sensitivity check (P5-T8) runs before the QueueProcessing comment edits (P5-T11) so its revert proof is an anchored `--exit-code` diff against BASE. `FILTER-DATAMODEL`'s trailing dots keep `QfcDatamodelTests.` from matching `QfcDatamodelTeardownTests`, `QfcDatamodelLivenessTests` or `QfcDatamodelRethrowTests` (21 tests: 4, 5, 3, 9). CMD-TOKEN-COUNT tokens never contain a double quote. + +PLANNER-INTERNAL-REVIEW: PASS +CITATION-TO-TREE: PASS +AC-TRACEABILITY: PASS +SCOPE-BOUNDARY: PASS +CITATION: QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs | lines 12-31, 34-46, 92-104, 116-138, 146, 231, 243-274, 284-341 +CITATION: QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs | lines 33, 44-98, 107-149, 157-190, 192-276, 285 +CITATION: QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs | lines 27, 98-117, 181-186, 193, 213, 235, 254, 314, 331, 367, 447-478 +CITATION: QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs | lines 85-96, 161-181, 216-239, 251-280, 282-305 +CITATION: QuickFiler.Test/QuickFiler.Test.csproj | lines 17, 35, 155, 157, 161, 183, 196-215, 226-230 +CITATION: QuickFiler.Test/SetupAssemblyInitializer.cs | lines 14-25 +CITATION: QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs | line 124 +CITATION: QuickFiler.Test/Controllers/QfcItemController.MailActionsTests.cs | line 203 +CITATION: QuickFiler.Test/Controllers/QfcItemController.SeamFactoryTests.cs | line 13 +CITATION: QuickFiler.Test/TestSupport/DedicatedWorkerThread.cs | lines 4, 21 +CITATION: QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs | lines 9, 12-13, 18-24, 47-61, 62-87, 100-164, 166-172, 174-209, 211-234, 236-270, 272-310 +CITATION: QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs | lines 12, 18-27, 59-74, 180, 220-222 +CITATION: QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs | lines 12, 23-35, 93-100, 114-128, 136-154, 165-183, 195-230 +CITATION: QuickFiler.Test/Controllers/QfcDatamodelTests.cs | lines 9, 12, 95-131, 201-211, 253-283 +CITATION: QuickFiler.Test/Controllers/QfcFormControllerSeamTests.cs | lines 357-367 +CITATION: QuickFiler.Test/Controllers/QfcHomeControllerRunAsyncTests.cs | lines 325, 370-380 +CITATION: QuickFiler/Controllers/QfcDatamodel.cs | lines 25-26, 34-54, 77-103, 107-112, 128-152, 188-195, 197-241, 242-267, 271-315, 335-376, 377-416, 417-465, 467-474, 476-491 +CITATION: QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs | lines 15-24, 37-43, 48-66, 146, 280-291, 299-311, 364, 404-411 +CITATION: QuickFiler/Controllers/QfcDatamodel.FrameBuilding.cs | line 11 +CITATION: QuickFiler/Controllers/QfcStreamingDequeueConfidenceGate.cs | lines 190-301 +CITATION: QuickFiler/Controllers/QfcHomeController.cs | lines 92, 132, 344, 379 +CITATION: QuickFiler/Interfaces/IQfcDatamodel.cs | lines 103, 117, 131, 138-148, 164, 166 +CITATION: QuickFiler/Properties/AssemblyInfo.cs | line 5 +CITATION: QuickFiler/Legacy/IAcceleratorCallbacks.cs | line 5 +CITATION: QuickFiler/Controllers/QfcHighConfidencePreFilter.cs | line 11 +CITATION: QuickFiler/Controllers/QfcItemController.FocusAndTheme.cs | lines 274-286 +CITATION: UtilitiesCS/HelperClasses/ThemeHelpers/Theme.cs | lines 427-445 +CITATION: UtilitiesCS/Threading/UiThread.cs | lines 266-285 +CITATION: UtilitiesCS.Test/TestHelpers/ArmingBarrierTimeProvider.cs | lines 19-54 +CITATION: scripts/vscode/Invoke-MSTestWithCoverage.ps1 | lines 89-93, 97-134, 262, 297-298, 348-355, 399-423, 430-453, 459-461 +CITATION: scripts/vscode/Invoke-MSTestWithCoverage.FirstParty.ps1 | lines 117-123 +CITATION: scripts/vscode/TaskMaster.cli.runsettings | lines 4-7 +CITATION: scripts/hygiene/Test-RepositoryHygiene.Rules.ps1 | line 21 +CITATION: scripts/vscode/Install-RepoDotNetSdk.ps1 | line 3 +CITATION: scripts/vscode/Invoke-Restore.ps1 | lines 1-10 +CITATION: .gitignore | lines 26, 140, 141, 146, 150, 151 +CITATION: .gitattributes | line 4 +CITATION: .csharpierignore | lines 4, 12 +CITATION: global.json | lines 2-9 +CITATION: dotnet-tools.json | line 6 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md | lines 6-13, 56-79, 94-108, 138-171, 173-186, 237-270, 274-306, 308-318 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/issue.md | lines 12, 65-77 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T05-50-dispatcher-pin-call-sites-research.md | sections 1.1, 2.1, 2.2, 3, 4, 5, 6, 7 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T22-20-qfc-datamodel-972-fold-research.md | sections 1 to 8 and Numeric Derivation Evidence +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round1-report.2026-10-02T08-40.md | defects 1 to 10 +CITATION: docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md | exists (Glob) +CITATION: docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md | exists (Glob) +AC-INVENTORY: AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8, AC9, AC10, AC11, AC12, AC13, AC14, AC15, AC16, AC17, AC18, AC19, AC20, AC21, AC22, AC23, AC24, AC25, AC26, AC27, AC28, AC29, AC30, AC31, AC32 +AC-MAPPING: AC1 | IMPLEMENTATION: P1-T1, P2-T1 to P2-T5 | TESTS: P1-T5, P2-T8, P8-T5 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC2 | IMPLEMENTATION: P1-T1, P2-T4, P2-T5 | TESTS: P2-T8, P8-T5 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC3 | IMPLEMENTATION: P1-T1, P2-T5 | TESTS: P1-T6, P2-T8, P6-T5, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC4 | IMPLEMENTATION: P1-T1, P2-T5 | TESTS: P1-T6, P2-T8, P8-T5 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC5 | IMPLEMENTATION: P1-T1, P1-T2, P2-T1 to P2-T5 | TESTS: P1-T5, P2-T8 | EVIDENCE: FEATURE/evidence/regression-testing/fail-before-pin-count.md +AC-MAPPING: AC6 | IMPLEMENTATION: P1-T1 | TESTS: P1-T6, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC7 | IMPLEMENTATION: P3-T3, P3-T4 | TESTS: P6-T6, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/focus-and-theme-class-pass-after.md +AC-MAPPING: AC8 | IMPLEMENTATION: P3-T3, P3-T4, P3-T8 | TESTS: P7-T1, P7-T2 | EVIDENCE: FEATURE/evidence/qa-gates/call-site-census.md +AC-MAPPING: AC9 | IMPLEMENTATION: P2-T1, P2-T4, P2-T5 | TESTS: P2-T6, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC10 | IMPLEMENTATION: P3-T7, P3-T8 | TESTS: P6-T5, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/fixture-class-pass-after.md +AC-MAPPING: AC11 | IMPLEMENTATION: P2-T2, P2-T3, P2-T5 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC12 | IMPLEMENTATION: P3-T5 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC13 | IMPLEMENTATION: P3-T7 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC14 | IMPLEMENTATION: P3-T8 | TESTS: P6-T5, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/fixture-class-pass-after.md +AC-MAPPING: AC15 | IMPLEMENTATION: P3-T1, P3-T2, P3-T6 | TESTS: P6-T6, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC16 | IMPLEMENTATION: P3-T3, P3-T4 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC17 | IMPLEMENTATION: P3-T5, P3-T6 | TESTS: P3-T9, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC18 | IMPLEMENTATION: P1-T1 to P5-T11, P6-T1 | TESTS: P8-T8 | EVIDENCE: FEATURE/evidence/qa-gates/file-line-counts.md +AC-MAPPING: AC19 | IMPLEMENTATION: P1-T1 to P5-T11 | TESTS: P7-T3 | EVIDENCE: FEATURE/evidence/qa-gates/prohibited-constructs-grep.md +AC-MAPPING: AC20 | IMPLEMENTATION: P6-T9 | TESTS: P8-T9 | EVIDENCE: FEATURE/evidence/qa-gates/footprint-scope.md +AC-MAPPING: AC21 | IMPLEMENTATION: P1-T2 | TESTS: P1-T4, P8-T5 | EVIDENCE: FEATURE/evidence/qa-gates/coverage-summary.md +AC-MAPPING: AC22 | IMPLEMENTATION: P8-T1 to P8-T5 | TESTS: P8-T5 | EVIDENCE: FEATURE/evidence/qa-gates/toolchain-final.md +AC-MAPPING: AC23 | IMPLEMENTATION: P0-T17, P8-T5 | TESTS: P8-T6 | EVIDENCE: FEATURE/evidence/qa-gates/coverage-comparison.md +AC-MAPPING: AC24 | IMPLEMENTATION: P1-T1 to P3-T8 | TESTS: P6-T7 | EVIDENCE: FEATURE/evidence/regression-testing/concurrent-set-test-summary.md +AC-MAPPING: AC25 | IMPLEMENTATION: P4-T2 to P4-T6 | TESTS: P4-T11, P6-T8, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC26 | IMPLEMENTATION: P5-T11 | TESTS: P5-T12, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/queue-processing-comment-census.md +AC-MAPPING: AC27 | IMPLEMENTATION: P4-T1, P4-T8 | TESTS: P4-T10, P8-T3, P8-T4, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/qfc-datamodel-legacy-callers.md +AC-MAPPING: AC28 | IMPLEMENTATION: P4-T8 | TESTS: P8-T8 | EVIDENCE: FEATURE/evidence/qa-gates/file-line-counts.md +AC-MAPPING: AC29 | IMPLEMENTATION: P4-T8 | TESTS: P6-T2, P8-T6 | EVIDENCE: FEATURE/evidence/qa-gates/coverage-comparison.md +AC-MAPPING: AC30 | IMPLEMENTATION: P4-T5, P4-T6, P4-T7, P5-T3, P5-T4 | TESTS: P6-T8, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC31 | IMPLEMENTATION: P5-T2, P5-T3, P5-T4 | TESTS: P5-T1, P5-T7, P5-T8, P5-T10, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/liveness-sensitivity-check.md +AC-MAPPING: AC32 | IMPLEMENTATION: P4-T2 to P5-T11 | TESTS: P6-T8 | EVIDENCE: FEATURE/evidence/regression-testing/datamodel-set-test-summary.md +UNRESOLVED-GAPS: NONE + +DIRECTIVE: PREFLIGHT VALIDATION ONLY +Executor preflight for this revision has not yet run; the signal below is the planner's request line for the confirming round, not a self-approval and not a discovered defect. +PREFLIGHT: REVISIONS REQUIRED + +## Round-2 delta application (2026-10-02T23-56 deltas) + +- Timestamp: 2026-10-03 (session context date; this planning session has no shell clock, so no minute stamp is composed) +- Plan: plan.2026-10-02T05-42.md (revised in place; version 1.2) +- Scope of this pass: the eight round-2 deltas of `preflight-round2-report.2026-10-02T23-56.md` applied in place (the "Orchestration action" bullet of defect 8 is not a plan edit and was not applied), followed by the adversarial re-derivation of every line the deltas touched and of its sibling occurrences across the plan. +- Tooling in this session: Read, Grep, Glob, Edit, Write. No shell; every file and line citation below was re-read from the item worktree. +- Correction to the report: the defect 5 delta states the post-change `QfcDatamodelTests.cs` `FakeTimeProvider` count as 7 ("five untouched lines plus the same 2"). Re-derivation shows the file carries the token on five lines at baseline (99, 216, 224, 249, 258), not six: line 9 is `using Microsoft.Extensions.Time.Testing;`, which does not contain the substring. One of the five (99) lies inside the replaced sibling test, so four untouched lines plus the two `ArmingFakeTimeProvider` lines give 6. The same directive error made the `QfcDatamodelLivenessTests.cs` baseline 2 in fact 17, P0-T13 and P4-T6; the true value is 1 (line 114). The plan now carries 6, 1 and 5 respectively. Each is a stricter, correct observation; no acceptance criterion is weakened. + +SELF-REVIEW: RE-DERIVED THIS PASS + +Citations re-derived in this pass (file and line, test or identifier): + +1. Plan Delivered Source F-SCOPE — plan lines 227 to 269 inclusive, 43 lines; 342 + 5 (F-FIELDS) + 13 (F-CLASSDOC) + 3 (F-ENSURE-DOC) + 1 (F-ENSURE-BODY) + 11 (F-SCOPE) = 375 (defect 1; F-SCOPE prose, P2-T6, P3-T9). +2. QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs — test 1 declaration 110; `(await pending).Should().BeEmpty();` 163; `/// Reads the issue #424 producer-liveness flag by reflection.` 166; `private static QfcDatamodel StartHeldOpenLoader(` 183; test 2 declaration 218. CMD-SPAN-TOKEN-COUNT prints `SPAN: -`, and the END index is the END line minus one (R4SPAN `212-284` with END at 285), so the baselines are `110-165` and `183-217`, and `(await pending)` is 1 inside T1-LIVE (defects 2 and 3). `await` inside the span 124, 140, 142, 157, 163 (5); whole-file substring 10 lines (50, 102, 178, 263, 288 added), so fact 17 now says "inside test 1". `[TestMethod]` 109, 217, 240, 276 (4). `FakeTimeProvider` on one line (114); line 9 is `using Microsoft.Extensions.Time.Testing;`. +3. QuickFiler.Test/Controllers/QfcDatamodelTests.cs — sibling declaration 96; `public async Task TryQueueRemainingMailItemAsync_HighConfidenceEnabled_AddsBelowThresholdCandidate()` 134, so T-SIB prints `96-133`; `FakeTimeProvider` 99, 216, 224, 249, 258 (5; line 9 is the using directive); `fake.Advance` 118, 127, 241, 280 (4); `[TestMethod]` 9; `Task.Yield` 119 only. Post-change: 258 lies before the M3 edit at 261 and 249 before the WaitForQueue test, so 216, 224, 249, 258 are untouched (4) and M-T adds the doc cref and `new ArmingFakeTimeProvider()` (2): 6. +4. QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs — `var gate = new QfcStreamingDequeueConfidenceGate(` 299; `QfcGateBatch batch = await gate.DequeueAsync(quantity, timeOut, _token);` 311; GATE-LAMBDA prints `299-310`. +5. QuickFiler/Controllers/QfcDatamodel.cs — `ForEachAwaitWithCancellationAsync` 431 (comment) and 440 (call), both inside the deleted block 417 to 465, so the baseline is 2 and the post-change value stays 0 (defect 4); `#region` 7 and `#endregion` 7 (32, 105, 107, 154, 156, 184, 186, 267, 269, 467, 469, 472, 474, 493), confirming the P0-T13 values 7, 7 and the post-change 6, 6. +6. Plan Delivered Source L-T1 and M-T after defect 7 — the doc lines now read `a scheduler yield.` and `a scheduler yield,`; `Task.Yield` substring 0 in each block; `ArmingFakeTimeProvider` lines in L-T1: the doc cref (same line, unchanged by the edit) and `new ArmingFakeTimeProvider()` (2); in M-T: the doc cref on the preceding line and `new ArmingFakeTimeProvider()` (2). `worker,` in the post-change Liveness file: three callers plus the `SynchronousBackgroundWorker worker,` parameter line (4; prose only, not gated). +7. Plan numstat convention — P1-T3 writes the project-file row as `10` with a literal tab (Grep `reads \`1\t0\``), so the P4-T9 row `1129` is written with a literal tab; 128 deleted lines plus the replaced line 369 give 129 deletions and 1 addition, and `--numstat` is independent of hunk grouping (defect 6). P6-T2 restates P4-T9 by reference and its holds-after-formatting clause now names numstat. +8. Plan occurrence sweep (Grep, before and after editing) — `374` at 271, 1469, 1494 only (all three replaced; none remain); `forty-two` once (replaced); `110-166`, `183-218`, `96-134`, `299-311` at the span-anchor list and P0-T13 only (replaced; the CITATION line `QfcDatamodel.QueueProcessing.cs | ... 299-311` is a line-range citation and is unchanged); `HUNK_COUNT` at the CMD-HUNKS definition, P3-T9 and P8-T27 (TestSupport 2), P5-T12 and P8-T36 (QueueProcessing 2) and P4-T9 (replaced); `FakeTimeProvider` 0 or 5 at 887, 1014, 1528, 1530 only (all replaced); `Task.Yield` 1 at fact 20 (baseline, unchanged), 1014 and 1528 (replaced); `Task.Yield` at 742 and 960 only (replaced); `PRE-IMPLEMENTATION GATE BLOCKED` at D-10 only; `worker,` 3 at 887 only; `no longer names` at 726 only (tightened). +9. D-10 and the payload-channel convention — re-read; the `PWSH CHANNEL REFUSED` rule is distinct and unchanged; the D-10 sentence now names evidence-file Write and pwsh payload refusals and the `PREIMPLEMENTATION_GATE_BLOCKED` prefix (defect 8). +10. Plan structure — `\r$` 0 lines before and after editing (LF preserved); nine `### Phase N — ` headings (0 to 8); task IDs unchanged and sequential per phase. + +Sibling-region re-checks: P4-T8 and P8-T37 keep `ForEachAwaitWithCancellationAsync` 0 after the change (both lines are in the deleted block). P5-T1 keeps T1-LIVE `Task.Yield` 3, `fake.Advance` 3, `for (int i` 1 and T-SIB `await Task.Yield();` 1 (baseline, unchanged). P4-T6's interim LIV `FakeTimeProvider` is the baseline value and is now 1. The L1 sentence "the file no longer names `FakeTimeProvider`" is now stated as the type, because the substring count is 2 after L3. CMD-ADDED-SCAN gates `await Task.Yield();` and is unaffected by the doc-line edits. The L-T1 gate-token line already read `Task.Yield` 0 and is now accurate for the whole block. P6-T2's "token, span, hunk and numstat value holds after formatting" inherits the P4-T9 numstat row; the TestSupport and QueueProcessing hunk gates are unchanged. The AC31 wording ("contain no `Task.Yield`") is now met by a whole-file count of 0 in both files, not only by the span gates. No acceptance criterion is weakened: the fixture total, the four spans, the `(await pending)` and `ForEachAwaitWithCancellationAsync` baselines and the `FakeTimeProvider` values are corrected observations, and the numstat row replaces an unsatisfiable hunk count with a check that fails on any extra added or removed line. + +PLANNER-INTERNAL-REVIEW: PASS +CITATION-TO-TREE: PASS +AC-TRACEABILITY: PASS +SCOPE-BOUNDARY: PASS +CITATION: QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs | lines 12-31, 34-46, 92-104, 116-138, 146, 231, 243-274, 284-341 +CITATION: QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs | lines 33, 44-98, 107-149, 157-190, 192-276, 285 +CITATION: QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs | lines 27, 98-117, 181-186, 193, 213, 235, 254, 314, 331, 367, 447-478 +CITATION: QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs | lines 85-96, 161-181, 216-239, 251-280, 282-305 +CITATION: QuickFiler.Test/QuickFiler.Test.csproj | lines 17, 35, 155, 157, 161, 183, 196-215, 226-230 +CITATION: QuickFiler.Test/SetupAssemblyInitializer.cs | lines 14-25 +CITATION: QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs | line 124 +CITATION: QuickFiler.Test/Controllers/QfcItemController.MailActionsTests.cs | line 203 +CITATION: QuickFiler.Test/Controllers/QfcItemController.SeamFactoryTests.cs | line 13 +CITATION: QuickFiler.Test/TestSupport/DedicatedWorkerThread.cs | lines 4, 21 +CITATION: QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs | lines 1-14, 18-24, 47-61, 62-87, 100-166, 166-172, 174-209, 211-234, 236-270, 272-310 +CITATION: QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs | lines 12, 18-27, 59-74, 180, 220-222 +CITATION: QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs | lines 12, 23-35, 93-100, 114-128, 136-154, 165-183, 195-230 +CITATION: QuickFiler.Test/Controllers/QfcDatamodelTests.cs | lines 1-14, 95-134, 201-211, 216, 224, 241, 249, 253-283 +CITATION: QuickFiler.Test/Controllers/QfcFormControllerSeamTests.cs | lines 357-367 +CITATION: QuickFiler.Test/Controllers/QfcHomeControllerRunAsyncTests.cs | lines 325, 370-380 +CITATION: QuickFiler/Controllers/QfcDatamodel.cs | lines 25-26, 32, 34-54, 77-105, 107-112, 128-152, 154-156, 184-195, 197-241, 242-269, 271-315, 335-376, 377-416, 417-465, 467-474, 476-493 +CITATION: QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs | lines 15-24, 37-43, 48-66, 146, 280-291, 299-311, 364, 404-411 +CITATION: QuickFiler/Controllers/QfcDatamodel.FrameBuilding.cs | line 11 +CITATION: QuickFiler/Controllers/QfcStreamingDequeueConfidenceGate.cs | lines 190-301 +CITATION: QuickFiler/Controllers/QfcHomeController.cs | lines 92, 132, 344, 379 +CITATION: QuickFiler/Interfaces/IQfcDatamodel.cs | lines 103, 117, 131, 138-148, 164, 166 +CITATION: QuickFiler/Properties/AssemblyInfo.cs | line 5 +CITATION: QuickFiler/Legacy/IAcceleratorCallbacks.cs | line 5 +CITATION: QuickFiler/Controllers/QfcHighConfidencePreFilter.cs | line 11 +CITATION: QuickFiler/Controllers/QfcItemController.FocusAndTheme.cs | lines 274-286 +CITATION: UtilitiesCS/HelperClasses/ThemeHelpers/Theme.cs | lines 427-445 +CITATION: UtilitiesCS/Threading/UiThread.cs | lines 266-285 +CITATION: UtilitiesCS.Test/TestHelpers/ArmingBarrierTimeProvider.cs | lines 19-54 +CITATION: scripts/vscode/Invoke-MSTestWithCoverage.ps1 | lines 89-93, 97-134, 262, 297-298, 348-355, 399-423, 430-453, 459-461 +CITATION: scripts/vscode/Invoke-MSTestWithCoverage.FirstParty.ps1 | lines 117-123 +CITATION: scripts/vscode/TaskMaster.cli.runsettings | lines 4-7 +CITATION: scripts/hygiene/Test-RepositoryHygiene.Rules.ps1 | line 21 +CITATION: scripts/vscode/Install-RepoDotNetSdk.ps1 | line 3 +CITATION: scripts/vscode/Invoke-Restore.ps1 | lines 1-10 +CITATION: .gitignore | lines 26, 140, 141, 146, 150, 151 +CITATION: .gitattributes | line 4 +CITATION: .csharpierignore | lines 4, 12 +CITATION: global.json | lines 2-9 +CITATION: dotnet-tools.json | line 6 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md | lines 6-13, 56-79, 94-108, 138-171, 173-186, 237-270, 274-306, 308-318 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/issue.md | lines 12, 65-77 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T05-50-dispatcher-pin-call-sites-research.md | sections 1.1, 2.1, 2.2, 3, 4, 5, 6, 7 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T22-20-qfc-datamodel-972-fold-research.md | sections 1 to 8 and Numeric Derivation Evidence +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round1-report.2026-10-02T08-40.md | defects 1 to 10 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round2-report.2026-10-02T23-56.md | defects 1 to 8 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/plan.2026-10-02T05-42.md | lines 7-9, 18-22, 129, 132, 135, 150, 225-271, 726, 732-817, 887, 956-1014, 1293-1296, 1372, 1425, 1448, 1469, 1494, 1509, 1515, 1528-1530, 1554, 1683-1687, 1737-1738 (pre-edit numbering) +CITATION: docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md | exists (Glob) +CITATION: docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md | exists (Glob) +AC-INVENTORY: AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8, AC9, AC10, AC11, AC12, AC13, AC14, AC15, AC16, AC17, AC18, AC19, AC20, AC21, AC22, AC23, AC24, AC25, AC26, AC27, AC28, AC29, AC30, AC31, AC32 +AC-MAPPING: AC1 | IMPLEMENTATION: P1-T1, P2-T1 to P2-T5 | TESTS: P1-T5, P2-T8, P8-T5 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC2 | IMPLEMENTATION: P1-T1, P2-T4, P2-T5 | TESTS: P2-T8, P8-T5 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC3 | IMPLEMENTATION: P1-T1, P2-T5 | TESTS: P1-T6, P2-T8, P6-T5, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC4 | IMPLEMENTATION: P1-T1, P2-T5 | TESTS: P1-T6, P2-T8, P8-T5 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC5 | IMPLEMENTATION: P1-T1, P1-T2, P2-T1 to P2-T5 | TESTS: P1-T5, P2-T8 | EVIDENCE: FEATURE/evidence/regression-testing/fail-before-pin-count.md +AC-MAPPING: AC6 | IMPLEMENTATION: P1-T1 | TESTS: P1-T6, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC7 | IMPLEMENTATION: P3-T3, P3-T4 | TESTS: P6-T6, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/focus-and-theme-class-pass-after.md +AC-MAPPING: AC8 | IMPLEMENTATION: P3-T3, P3-T4, P3-T8 | TESTS: P7-T1, P7-T2 | EVIDENCE: FEATURE/evidence/qa-gates/call-site-census.md +AC-MAPPING: AC9 | IMPLEMENTATION: P2-T1, P2-T4, P2-T5 | TESTS: P2-T6, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC10 | IMPLEMENTATION: P3-T7, P3-T8 | TESTS: P6-T5, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/fixture-class-pass-after.md +AC-MAPPING: AC11 | IMPLEMENTATION: P2-T2, P2-T3, P2-T5 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC12 | IMPLEMENTATION: P3-T5 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC13 | IMPLEMENTATION: P3-T7 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC14 | IMPLEMENTATION: P3-T8 | TESTS: P6-T5, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/fixture-class-pass-after.md +AC-MAPPING: AC15 | IMPLEMENTATION: P3-T1, P3-T2, P3-T6 | TESTS: P6-T6, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC16 | IMPLEMENTATION: P3-T3, P3-T4 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC17 | IMPLEMENTATION: P3-T5, P3-T6 | TESTS: P3-T9, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC18 | IMPLEMENTATION: P1-T1 to P5-T11, P6-T1 | TESTS: P8-T8 | EVIDENCE: FEATURE/evidence/qa-gates/file-line-counts.md +AC-MAPPING: AC19 | IMPLEMENTATION: P1-T1 to P5-T11 | TESTS: P7-T3 | EVIDENCE: FEATURE/evidence/qa-gates/prohibited-constructs-grep.md +AC-MAPPING: AC20 | IMPLEMENTATION: P6-T9 | TESTS: P8-T9 | EVIDENCE: FEATURE/evidence/qa-gates/footprint-scope.md +AC-MAPPING: AC21 | IMPLEMENTATION: P1-T2 | TESTS: P1-T4, P8-T5 | EVIDENCE: FEATURE/evidence/qa-gates/coverage-summary.md +AC-MAPPING: AC22 | IMPLEMENTATION: P8-T1 to P8-T5 | TESTS: P8-T5 | EVIDENCE: FEATURE/evidence/qa-gates/toolchain-final.md +AC-MAPPING: AC23 | IMPLEMENTATION: P0-T17, P8-T5 | TESTS: P8-T6 | EVIDENCE: FEATURE/evidence/qa-gates/coverage-comparison.md +AC-MAPPING: AC24 | IMPLEMENTATION: P1-T1 to P3-T8 | TESTS: P6-T7 | EVIDENCE: FEATURE/evidence/regression-testing/concurrent-set-test-summary.md +AC-MAPPING: AC25 | IMPLEMENTATION: P4-T2 to P4-T6 | TESTS: P4-T11, P6-T8, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC26 | IMPLEMENTATION: P5-T11 | TESTS: P5-T12, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/queue-processing-comment-census.md +AC-MAPPING: AC27 | IMPLEMENTATION: P4-T1, P4-T8 | TESTS: P4-T10, P8-T3, P8-T4, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/qfc-datamodel-legacy-callers.md +AC-MAPPING: AC28 | IMPLEMENTATION: P4-T8 | TESTS: P8-T8 | EVIDENCE: FEATURE/evidence/qa-gates/file-line-counts.md +AC-MAPPING: AC29 | IMPLEMENTATION: P4-T8 | TESTS: P6-T2, P8-T6 | EVIDENCE: FEATURE/evidence/qa-gates/coverage-comparison.md +AC-MAPPING: AC30 | IMPLEMENTATION: P4-T5, P4-T6, P4-T7, P5-T3, P5-T4 | TESTS: P6-T8, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC31 | IMPLEMENTATION: P5-T2, P5-T3, P5-T4 | TESTS: P5-T1, P5-T7, P5-T8, P5-T10, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/liveness-sensitivity-check.md +AC-MAPPING: AC32 | IMPLEMENTATION: P4-T2 to P5-T11 | TESTS: P6-T8 | EVIDENCE: FEATURE/evidence/regression-testing/datamodel-set-test-summary.md +UNRESOLVED-GAPS: NONE + +DIRECTIVE: PREFLIGHT VALIDATION ONLY +Executor preflight for this revision has not yet run; the signal below is the planner's request line for the confirming round, not a self-approval and not a discovered defect. +PREFLIGHT: REVISIONS REQUIRED + +## Round-3 delta application (2026-10-03T01-01 deltas) + +- Timestamp: 2026-10-03 (session context date; this planning session has no shell clock, so no minute stamp is composed) +- Plan: plan.2026-10-02T05-42.md (revised in place; version 1.3; 1,782 lines after this pass; LF line endings preserved, Grep `\r` count 0) +- Scope of this pass: defects 1 to 4 of `preflight-round3-report.2026-10-03T01-01.md` applied verbatim (old to new), plus the optional delta of advisory A1 (elected by the orchestrator because the revised D-10 rule turns a pwsh refusal into a run stop); followed by the adversarial re-derivation of every line the deltas touched and of its sibling occurrences across the plan. +- Tooling in this session: Read, Grep, Glob, Edit, Write. The Bash tool is disabled in this session, so no `pwsh` token count and no `git hash-object` were run; every count below is a Grep or Read observation against the item worktree, and the new plan blob SHA is left for the orchestrator to compute. +- Sibling decision on P6-T2 line 1554 (pre-edit numbering): the task-description line also reads `every P2-T6, P3-T9, P1-T3, P4-T9, P5-T5 and P5-T12`, but it enumerates the commands to re-run, bounded by its parenthesis, not the values that must hold; re-running every listed command is satisfiable, so the line is unchanged. Only the acceptance line (1555) carried the unsatisfiable requirement. +- Edits applied (pre-edit plan line numbers): 7 to 9 (header: Last Updated, Status, Version 1.3); after 22 (Round 3 revision-record bullet); 131 (fact 15: 25 lines in 5 files, 17 in QfcDatamodel.cs with line numbers); 138 (fact 22: 334 lines; `acquired and released inside a held` 4 at 10, 105, 266, 282); 1050 (payload channel: payloads are never merged into one call); 1395 (P0-T2: fact 22 values 1, 1, 4, 1, 2); 1500 (P4-T1: `PRIMARY_LINES: 25`; `METHOD-GROUP-ONE-ARG-OVERLOAD` for lines 40 and 52); 1555 (P6-T2: values as last recorded per file, numstat `1 129` kept for QfcDatamodel.cs, project-file numstat `3 0`, porcelain span supersedes); 1684 (pointer to this section); 1688 (25, 3 and 2; round-3 enumeration appended); after 1739 (CITATION for the round-3 report). +- Sweep result (every other occurrence of a corrected value): `24 lines` and `PRIMARY_LINES: 24` occurred only at 131, 1500 and 1688; `335` elsewhere (130, 1710) is a QfcDatamodel.cs line number; `acquired and released inside a held` elsewhere (143, 1394) is prose and the P0-T2 token list; `plus 2` elsewhere (563, 639, 1516) is line arithmetic or the P4-T9 interim value, correct at its own task; the P1-T3 numstat `1 0` (1449) is correct at P1-T3. No interim P4-T6, P4-T7 or P4-T9 value is restated as final anywhere else: the AC25, AC30 and AC31 check-offs and P8-T8 carry the P5-T5 values. +- Write Set: unchanged by this pass. The set of files the plan creates or modifies during execution is the same fourteen code paths, the two feature documents and the same evidence files. + +SELF-REVIEW: RE-DERIVED THIS PASS + +Citations re-derived in this pass (file and line, test or identifier): + +1. QuickFiler/Controllers/QfcDatamodel.cs — the primary pattern `Worker_RunWorkerCompleted|LoadRemainingEmailsToQueue\b|LoadRemainingEmailsToQueueAsync|Linked List Locking` matches 17 lines: 40, 52, 130, 194, 209, 210, 246, 335, 363, 369, 378, 404, 410, 418, 462, 469, 472 (Grep with line numbers; the delta's list, confirmed line for line). +2. Repository-wide `*.cs` — the same pattern: 25 lines in 5 files (QfcDatamodel.cs 17, QfcHomeController.cs 4, QfcHomeControllerRunAsyncTests.cs 2, QfcDatamodelLivenessTests.cs 1, QfcInitEmailQueueZeroBatchTests.cs 1), equal to the reviewer's `PRIMARY_LINES: 25` and to the fact 15 outside-file list (defect 1). +3. QuickFiler/Controllers/QfcDatamodel.cs 40 and 52 — both read `RemainingEmailLoader = LoadRemainingEmailsToQueueAsync;` (the method-group conversion to the one-argument overload, fact 14); neither is a declaration, a cref or a nameof, so the new `METHOD-GROUP-ONE-ARG-OVERLOAD` category is required for `INVOCATIONS: 0` to be reachable (defect 2). +4. Sibling region of defect 3 — QuickFiler/Controllers/QfcDatamodel.cs 96 to 112, 190 to 212 and 358 to 372: after the P1 deletions, 98 `_masterQueue = null;` meets 102 `_worker = null;`; 193 meets 195 `}`; 208 `// Start the time-consuming operation.` meets 211 `try`; 362 `{` meets 364 `return false;`; and the retargeted 369 is the sole argument of `logger.Error(` (368 to 370), already on its own line. No deletion produces a double blank line and the retarget cannot be re-wrapped, so the `1 129` numstat row the rewritten P6-T2 line asserts after formatting is not exposed to a CSharpier rewrite of that file. +5. Plan P4-T9, P5-T5, P4-T6, P4-T7 and P1-T3 (pre-edit 1516, 1533, 1510, 1512, 1449) — the interim values the reviewer listed (LIV `using (var worker = new SynchronousBackgroundWorker())` 3, `Task.Yield` 3, `fake.Advance` 3, `FakeTimeProvider` 1, `new ArmingFakeTimeProvider()` 0; DMT `using (var worker = new BackgroundWorker())` 1; PROJ plus 2 and `TestSupport\ArmingFakeTimeProvider.cs` 0; project-file numstat `1 0`) are each superseded by P5-T3, P5-T4, P5-T5 and the two further project items, so the rewritten P6-T2 acceptance names P5-T5 as the last recording task for LIV, DMT, AFTP and PROJ and restates the project-file row as `3 0` (defect 3). +6. Plan P6-T2 task line (pre-edit 1554) — enumerates commands, bounded by its parenthesis (CMD-LINECOUNT on CS13, every CMD-TOKEN-COUNT list, the nine spans, four CMD-HUNKS, numstat with the porcelain span); CMD-EOL is not among them, so no write-mode command is re-run; left unchanged. +7. docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md — 334 lines (Grep `^` count); `acquired and released inside a held` on lines 10, 105, 266 and 282 (Grep with line numbers), so fact 22 and P0-T2 now read 334 and 4 (defect 4); the P0-T2 gate "at least 1" was already satisfiable and is unchanged in form. +8. Plan payload-channel bullet (pre-edit 1050) — the A1 sentence is inserted after "with the substitutions applied." and before "Payloads use double quotes only"; it contains no placeholder character and no apostrophe (caller instruction 2). +9. Plan header 7 to 9, revision record 22, self-review pointer 1684 and summary 1688, CITATION list 1739 — updated to version 1.3, the Round 3 bullet, this section's heading, "25, 3 and 2", the round-3 enumeration and the round-3 report citation; Grep after the edits finds `1 129` at 22 (the round-2 bullet), 1517, 1556 and 1689 and `3 0` at 23 and 1556, each tab-separated; no `PRIMARY_LINES: 24`, `24 lines in 5` or `335 lines` remains. +10. Plan structure after the edits — nine `### Phase` headings (1390, 1443, 1458, 1477, 1498, 1523, 1550, 1573, 1582); task lines P0-T2 1394, P4-T1 1500, P6-T2 1555; 1,782 lines; zero carriage returns. +11. Sibling check-offs P8-T35 (AC25), P8-T37 (AC27), P8-T39 (AC29), P8-T40 (AC30), P8-T41 (AC31) and P8-T8 — read against the corrected values: each carries the P5-T5 final values or the P4-T1 member-set counts (`Primary Count: 4`, `Cross-check Count: 4`, `INVOCATIONS: 0`, test-file hits `DOC-PROSE` or `OTHER-TYPE-SAME-NAME`), none of which the deltas change. + +PLANNER-INTERNAL-REVIEW: PASS +CITATION-TO-TREE: PASS +AC-TRACEABILITY: PASS +SCOPE-BOUNDARY: PASS +CITATION: QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs | lines 12-31, 34-46, 92-104, 116-138, 146, 231, 243-274, 284-341 +CITATION: QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs | lines 33, 44-98, 107-149, 157-190, 192-276, 285 +CITATION: QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs | lines 27, 98-117, 181-186, 193, 213, 235, 254, 314, 331, 367, 447-478 +CITATION: QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs | lines 85-96, 161-181, 216-239, 251-280, 282-305 +CITATION: QuickFiler.Test/QuickFiler.Test.csproj | lines 17, 35, 155, 157, 161, 183, 196-215, 226-230 +CITATION: QuickFiler.Test/SetupAssemblyInitializer.cs | lines 14-25 +CITATION: QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs | line 124 +CITATION: QuickFiler.Test/Controllers/QfcItemController.MailActionsTests.cs | line 203 +CITATION: QuickFiler.Test/Controllers/QfcItemController.SeamFactoryTests.cs | line 13 +CITATION: QuickFiler.Test/TestSupport/DedicatedWorkerThread.cs | lines 4, 21 +CITATION: QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs | lines 9, 12-13, 18-24, 47-61, 62-87, 100-164, 166-172, 174-209, 211-234, 236-270, 272-310 +CITATION: QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs | lines 12, 18-27, 59-74, 180, 220-222 +CITATION: QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs | lines 12, 23-35, 93-100, 114-128, 136-154, 165-183, 195-230 +CITATION: QuickFiler.Test/Controllers/QfcDatamodelTests.cs | lines 9, 12, 95-131, 201-211, 253-283 +CITATION: QuickFiler.Test/Controllers/QfcFormControllerSeamTests.cs | lines 357-367 +CITATION: QuickFiler.Test/Controllers/QfcHomeControllerRunAsyncTests.cs | lines 325, 370-380 +CITATION: QuickFiler/Controllers/QfcDatamodel.cs | lines 25-26, 34-54, 77-103, 107-112, 128-152, 188-195, 197-241, 242-267, 271-315, 335-376, 377-416, 417-465, 467-474, 476-491 +CITATION: QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs | lines 15-24, 37-43, 48-66, 146, 280-291, 299-311, 364, 404-411 +CITATION: QuickFiler/Controllers/QfcDatamodel.FrameBuilding.cs | line 11 +CITATION: QuickFiler/Controllers/QfcStreamingDequeueConfidenceGate.cs | lines 190-301 +CITATION: QuickFiler/Controllers/QfcHomeController.cs | lines 92, 132, 344, 379 +CITATION: QuickFiler/Interfaces/IQfcDatamodel.cs | lines 103, 117, 131, 138-148, 164, 166 +CITATION: QuickFiler/Properties/AssemblyInfo.cs | line 5 +CITATION: QuickFiler/Legacy/IAcceleratorCallbacks.cs | line 5 +CITATION: QuickFiler/Controllers/QfcHighConfidencePreFilter.cs | line 11 +CITATION: QuickFiler/Controllers/QfcItemController.FocusAndTheme.cs | lines 274-286 +CITATION: UtilitiesCS/HelperClasses/ThemeHelpers/Theme.cs | lines 427-445 +CITATION: UtilitiesCS/Threading/UiThread.cs | lines 266-285 +CITATION: UtilitiesCS.Test/TestHelpers/ArmingBarrierTimeProvider.cs | lines 19-54 +CITATION: scripts/vscode/Invoke-MSTestWithCoverage.ps1 | lines 89-93, 97-134, 262, 297-298, 348-355, 399-423, 430-453, 459-461 +CITATION: scripts/vscode/Invoke-MSTestWithCoverage.FirstParty.ps1 | lines 117-123 +CITATION: scripts/vscode/TaskMaster.cli.runsettings | lines 4-7 +CITATION: scripts/hygiene/Test-RepositoryHygiene.Rules.ps1 | line 21 +CITATION: scripts/vscode/Install-RepoDotNetSdk.ps1 | line 3 +CITATION: scripts/vscode/Invoke-Restore.ps1 | lines 1-10 +CITATION: .gitignore | lines 26, 140, 141, 146, 150, 151 +CITATION: .gitattributes | line 4 +CITATION: .csharpierignore | lines 4, 12 +CITATION: global.json | lines 2-9 +CITATION: dotnet-tools.json | line 6 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md | lines 6-13, 56-79, 94-108, 138-171, 173-186, 237-270, 274-306, 308-318 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/issue.md | lines 12, 65-77 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T05-50-dispatcher-pin-call-sites-research.md | sections 1.1, 2.1, 2.2, 3, 4, 5, 6, 7 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T22-20-qfc-datamodel-972-fold-research.md | sections 1 to 8 and Numeric Derivation Evidence +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round1-report.2026-10-02T08-40.md | defects 1 to 10 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round2-report.2026-10-02T23-56.md | defects 1 to 8 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round3-report.2026-10-03T01-01.md | defects 1 to 4 and advisory A1 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/plan.2026-10-02T05-42.md | lines 7-9, 22, 131, 138, 1050, 1395, 1500, 1554-1555, 1684, 1688, 1739 (pre-edit numbering) +CITATION: docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md | exists (Glob) +CITATION: docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md | exists (Glob) +AC-INVENTORY: AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8, AC9, AC10, AC11, AC12, AC13, AC14, AC15, AC16, AC17, AC18, AC19, AC20, AC21, AC22, AC23, AC24, AC25, AC26, AC27, AC28, AC29, AC30, AC31, AC32 +AC-MAPPING: AC1 | IMPLEMENTATION: P1-T1, P2-T1 to P2-T5 | TESTS: P1-T5, P2-T8, P8-T5 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC2 | IMPLEMENTATION: P1-T1, P2-T4, P2-T5 | TESTS: P2-T8, P8-T5 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC3 | IMPLEMENTATION: P1-T1, P2-T5 | TESTS: P1-T6, P2-T8, P6-T5, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC4 | IMPLEMENTATION: P1-T1, P2-T5 | TESTS: P1-T6, P2-T8, P8-T5 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC5 | IMPLEMENTATION: P1-T1, P1-T2, P2-T1 to P2-T5 | TESTS: P1-T5, P2-T8 | EVIDENCE: FEATURE/evidence/regression-testing/fail-before-pin-count.md +AC-MAPPING: AC6 | IMPLEMENTATION: P1-T1 | TESTS: P1-T6, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC7 | IMPLEMENTATION: P3-T3, P3-T4 | TESTS: P6-T6, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/focus-and-theme-class-pass-after.md +AC-MAPPING: AC8 | IMPLEMENTATION: P3-T3, P3-T4, P3-T8 | TESTS: P7-T1, P7-T2 | EVIDENCE: FEATURE/evidence/qa-gates/call-site-census.md +AC-MAPPING: AC9 | IMPLEMENTATION: P2-T1, P2-T4, P2-T5 | TESTS: P2-T6, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC10 | IMPLEMENTATION: P3-T7, P3-T8 | TESTS: P6-T5, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/fixture-class-pass-after.md +AC-MAPPING: AC11 | IMPLEMENTATION: P2-T2, P2-T3, P2-T5 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC12 | IMPLEMENTATION: P3-T5 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC13 | IMPLEMENTATION: P3-T7 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC14 | IMPLEMENTATION: P3-T8 | TESTS: P6-T5, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/fixture-class-pass-after.md +AC-MAPPING: AC15 | IMPLEMENTATION: P3-T1, P3-T2, P3-T6 | TESTS: P6-T6, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC16 | IMPLEMENTATION: P3-T3, P3-T4 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC17 | IMPLEMENTATION: P3-T5, P3-T6 | TESTS: P3-T9, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC18 | IMPLEMENTATION: P1-T1 to P5-T11, P6-T1 | TESTS: P8-T8 | EVIDENCE: FEATURE/evidence/qa-gates/file-line-counts.md +AC-MAPPING: AC19 | IMPLEMENTATION: P1-T1 to P5-T11 | TESTS: P7-T3 | EVIDENCE: FEATURE/evidence/qa-gates/prohibited-constructs-grep.md +AC-MAPPING: AC20 | IMPLEMENTATION: P6-T9 | TESTS: P8-T9 | EVIDENCE: FEATURE/evidence/qa-gates/footprint-scope.md +AC-MAPPING: AC21 | IMPLEMENTATION: P1-T2 | TESTS: P1-T4, P8-T5 | EVIDENCE: FEATURE/evidence/qa-gates/coverage-summary.md +AC-MAPPING: AC22 | IMPLEMENTATION: P8-T1 to P8-T5 | TESTS: P8-T5 | EVIDENCE: FEATURE/evidence/qa-gates/toolchain-final.md +AC-MAPPING: AC23 | IMPLEMENTATION: P0-T17, P8-T5 | TESTS: P8-T6 | EVIDENCE: FEATURE/evidence/qa-gates/coverage-comparison.md +AC-MAPPING: AC24 | IMPLEMENTATION: P1-T1 to P3-T8 | TESTS: P6-T7 | EVIDENCE: FEATURE/evidence/regression-testing/concurrent-set-test-summary.md +AC-MAPPING: AC25 | IMPLEMENTATION: P4-T2 to P4-T6 | TESTS: P4-T11, P6-T8, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC26 | IMPLEMENTATION: P5-T11 | TESTS: P5-T12, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/queue-processing-comment-census.md +AC-MAPPING: AC27 | IMPLEMENTATION: P4-T1, P4-T8 | TESTS: P4-T10, P8-T3, P8-T4, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/qfc-datamodel-legacy-callers.md +AC-MAPPING: AC28 | IMPLEMENTATION: P4-T8 | TESTS: P8-T8 | EVIDENCE: FEATURE/evidence/qa-gates/file-line-counts.md +AC-MAPPING: AC29 | IMPLEMENTATION: P4-T8 | TESTS: P6-T2, P8-T6 | EVIDENCE: FEATURE/evidence/qa-gates/coverage-comparison.md +AC-MAPPING: AC30 | IMPLEMENTATION: P4-T5, P4-T6, P4-T7, P5-T3, P5-T4 | TESTS: P6-T8, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC31 | IMPLEMENTATION: P5-T2, P5-T3, P5-T4 | TESTS: P5-T1, P5-T7, P5-T8, P5-T10, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/liveness-sensitivity-check.md +AC-MAPPING: AC32 | IMPLEMENTATION: P4-T2 to P5-T11 | TESTS: P6-T8 | EVIDENCE: FEATURE/evidence/regression-testing/datamodel-set-test-summary.md +UNRESOLVED-GAPS: NONE + +DIRECTIVE: PREFLIGHT VALIDATION ONLY +Executor preflight for this revision has not yet run; the signal below is the planner's request line for the confirming round, not a self-approval and not a discovered defect. +PREFLIGHT: REVISIONS REQUIRED + +## Round-4 delta application (2026-10-03T01-25 deltas) + +- Timestamp: 2026-10-03 (session context date; this planning session has no shell clock, so no minute stamp is composed) +- Plan: plan.2026-10-02T05-42.md (revised in place; version 1.4; 1,784 lines after this pass; LF line endings preserved, Grep `\r` count 0; 123 task lines and nine `### Phase` headings, unchanged) +- Scope of this pass: defects 1 to 3 of `preflight-round4-report.2026-10-03T01-25.md` applied verbatim (old to new); the optional advisory delta (naming the P1-T3 PROJ token in the P6-T2 per-file pointer) declined by the orchestrator, because the value holds regardless and the round is to stay narrow; followed by the two orchestrator-requested sweeps (restart-path git gates; recorded-not-gated values restated as gated) and the adversarial re-derivation of every line the deltas touched and of its sibling occurrences. +- Tooling in this session: Read, Grep, Glob, Edit, Write. The Bash tool is disabled, so CMD-LEGACY-CALLERS could not be run as a pwsh payload; its three strategies were reproduced with the Grep tool rooted at the item worktree (the primary and cross-check patterns over the `**/*.cs` glob, the `\blog\b` pattern over the `QfcDatamodel*.cs` glob under QuickFiler/Controllers), and the counts below are those observations. No `git hash-object` was run; the new plan blob SHA is left for the orchestrator to compute. +- Census reproduction and classification (defect 1 and the orchestrator note): PRIMARY 25 lines in 5 files, LOG 3, CROSS 2, equal to the reviewer's run. Classification against P4-T1 as it reads after the delta: `QfcDatamodel.cs` 40 and 52 METHOD-GROUP-ONE-ARG-OVERLOAD; 130 CREF-ONE-ARG-OVERLOAD; 194, 209, 210, 363 and 462 COMMENTED-OUT; 246, 335, 378 and 418 DECLARATION; 369 NAMEOF-RETARGETED; 404 and 410 SELF-REFERENCE (inside the synchronous member 378 to 416); 469 and 472 REGION-DIRECTIVE; `QfcHomeController.cs` 92, 132, 344 and 379 and `QfcHomeControllerRunAsyncTests.cs` 325 and 376 OTHER-TYPE-SAME-NAME; `QfcDatamodelLivenessTests.cs` 104 and `QfcInitEmailQueueZeroBatchTests.cs` 28 DOC-PROSE; LOG `QfcDatamodel.cs` 109 DECLARATION, `QfcDatamodel.QueueProcessing.cs` 71 and 90 DOC-PROSE; CROSS `QfcHomeControllerRunAsyncTests.cs` 376 OTHER-TYPE-SAME-NAME and `QfcDatamodel.cs` 130 CREF-ONE-ARG-OVERLOAD. Every one of the 30 lines has a category; no category beyond the delta's REGION-DIRECTIVE was needed, so none was added. +- Edits applied (pre-edit plan line numbers; post-edit in parentheses): 7 to 9 (7 to 9: Last Updated, Status, Version 1.4); after 23 (24: Round 4 revision-record bullet); 155 (156: D-13, the defect-2 sentence verbatim after "resumes at P8-T1.", plus a parenthetical on the Phase 6 restart sentence naming the same rule when a P6-T9 commit is already in HEAD because a Phase 8 restart preceded it); 1501 (1502: P4-T1, `REGION-DIRECTIVE` inserted verbatim between `METHOD-GROUP-ONE-ARG-OVERLOAD` and `NAMEOF-RETARGETED`); 1556 (1557: P6-T2, the defect-3 exemption clause verbatim, plus a pointer after "three `??`)" stating that on a D-13 restart following the P6-T9 commit the ref operand of every HEAD-anchored git command in the task and the porcelain expectation are the ones D-13 states); 1685 (1686: pointer to this section); 1689 (1690: round-4 enumeration appended); after 1741 (1743: CITATION for the round-4 report). The P6-T2 task-description line (1555, now 1556) is unchanged, as in round 3. +- Sweep A (defect 2 class: HEAD-anchored diffs, numstat row gates and `??` porcelain expectations on a restart path). HEAD-anchored `git diff` occurs at pre-edit 1449 (P1-T3), 1470 (P2-T6), 1495 (P3-T9), 1516 (P4-T9), 1533 (P5-T5) and 1677 (P8-T45); `??` expectations at 1450, 1471, 1496, 1517, 1534 and 1556. P1-T3 to P5-T5 are pre-commit tasks whose own re-run rules ("correct the edit and re-run this task", and the P4-T11, P5-T6 and P5-T7 fall-backs) all precede P6-T9; after the commit they are re-run only through P6-T2, which the D-13 rule now covers. P8-T45 runs after the check-offs (P8-T11 to P8-T42) and before P8-T46, and every D-13 restart trigger (P8-T1 to P8-T5) precedes P8-T10, so at P8-T45 HEAD is the P6-T9 commit (or the `style(968)` commit) and the spec check-offs are uncommitted: its HEAD operand is correct and it is unchanged. CMD-HUNKS (1371) and CMD-ADDED-SCAN (1359) are BASE-anchored, so the hunk ranges P6-T2 restates and the P7-T3 scan hold after the commit. P6-T9, P8-T1 and P8-T9 carry porcelain negatives (no path under QuickFiler/ or QuickFiler.Test/) or BASE-anchored name-status diffs, which hold after the commit. No task other than P6-T2 needed a change; D-11 ("every `git diff` names BASE or HEAD as its ref operand") remains true. +- Sweep B (defect 3 class: a recorded-not-gated value restated as gated). The plan's recorded-not-gated values are the D-7 BRANCH B comparison (149; consumed by P8-T6 at 1598 as "recorded and not gated"), `QFCDATAMODEL_CLASS_ENTRIES:` (1252 and 1435; P8-T6 reads the two values as the AC29 reason and asserts no value), the P0-T14 and P0-T15 outcomes (1429, 1431; D-8 baseline-relative) and the P4-T9 QfcDatamodel.cs `HUNK_COUNT:` (1517). Only P6-T2 (1556) had restated one of them as gated. P8-T8 compares LINES to the P6-T2 values (gated at P6-T2 under the REWRITTEN exemption, with the final P8-T1 iteration rewriting nothing); P8-T27 and P8-T36 read `HUNK_COUNT: 2` values that P3-T9 and P5-T12 gate; P8-T20 reads the fixture-tests hunk ranges that P3-T9 gates. No other line changed. +- Write Set: unchanged by this pass. The set of files the plan creates or modifies during execution is the same fourteen code paths, the two feature documents and the same evidence files; the only new artifact label is `P6-RESTART-PORCELAIN:`, written inside the existing post-format-census.md on the restart path only. + +SELF-REVIEW: RE-DERIVED THIS PASS + +Citations re-derived in this pass (file and line, test or identifier): + +1. QuickFiler/Controllers/QfcDatamodel.cs 464 to 475 — 467 `#endregion Email Queue Initial Setup`, 469 `#region Linked List Locking`, 470 and 471 blank, 472 `#endregion Linked List Locking`, 474 `#region Event Handlers` (Read); fact 14's region line numbers and the delta's 469 and 472 agree, and a region directive matches none of the pre-delta P4-T1 categories (defect 1). +2. Repository-wide `*.cs` — the primary pattern `Worker_RunWorkerCompleted|LoadRemainingEmailsToQueue\b|LoadRemainingEmailsToQueueAsync|Linked List Locking`: 25 lines in 5 files (QfcDatamodel.cs 17: 40, 52, 130, 194, 209, 210, 246, 335, 363, 369, 378, 404, 410, 418, 462, 469, 472; QfcHomeController.cs 92, 132, 344, 379; QfcHomeControllerRunAsyncTests.cs 325, 376; QfcDatamodelLivenessTests.cs 104; QfcInitEmailQueueZeroBatchTests.cs 28), equal to the reviewer's `PRIMARY_LINES: 25` and to fact 15. +3. QuickFiler/Controllers/QfcDatamodel*.cs — `\blog\b`: 3 lines (QfcDatamodel.cs 109; QfcDatamodel.QueueProcessing.cs 71 and 90), equal to `LOG_LINES: 3`. +4. Repository-wide `*.cs` — the string-literal and reflection pattern of CMD-LEGACY-CALLERS: 2 lines (QfcHomeControllerRunAsyncTests.cs 376; QfcDatamodel.cs 130), equal to `CROSS_LINES: 2`. +5. Sibling region of defect 1 — the plan's other `Linked List Locking` mentions (pre-edit 131, 132, 1020, 1030, 1336, 1426, 1661): fact 14 and fact 15 (line numbers and the count 2), Delivered Source P1 (the region removal), the CMD-LEGACY-CALLERS pattern, the QDM token list and the AC27 check-off (`Linked List Locking` 0 after); D-16 names "the empty region"; none is a classification list, so no sibling needed the new category. +6. Plan D-13 (pre-edit 155) and P6-T2 (pre-edit 1555 to 1556) — the Phase 8 restart path re-enters P6-T1 with the P6-T9 commit in HEAD; the five census tasks P6-T2 re-runs anchor to HEAD (1449, 1470, 1495, 1516, 1533) and three of them expect `??` lines; CMD-HUNKS (1371) and CMD-ADDED-SCAN (1359) anchor to BASE (defect 2). The nested case (a Phase 6 restart after that Phase 8 restart) re-enters P6-T2 under the same HEAD, which is why the Phase 6 sentence now names the rule too. +7. Plan P4-T9 (pre-edit 1517) — `HUNK_COUNT:` for QfcDatamodel.cs "is recorded, not gated"; P5-T5 (1534) and P3-T9 (1496) gate no printed `SPAN:` range; P6-T1 (1554) admits a non-empty `REWRITTEN:`; so the pre-delta P6-T2 wording demanded values no earlier task gated (defect 3). +8. Plan P8-T45 (pre-edit 1677) — HEAD-anchored spec diff; its position after P8-T42 and before P8-T46, with every D-13 restart trigger (P8-T1 to P8-T5) preceding P8-T10, places it on no restart path; unchanged. +9. Plan recorded-not-gated occurrences (Grep `recorded, not gated|not gated|\(recorded\)`): pre-edit 149, 1053, 1252, 1429, 1431, 1435, 1517, 1598; their consumers P8-T6 (1598), P8-T8 (1602), P8-T20 (1627), P8-T27 (1641) and P8-T36 (1659) read gated values only. +10. Plan header 7 to 9, revision record after 23, self-review pointer 1685 and summary 1689, CITATION list after 1741 — updated to version 1.4, the Round 4 bullet, this section's heading, the round-4 enumeration and the round-4 report citation. After the edits Grep finds `P6-RESTART-PORCELAIN:` at 24 and 156 only, `REGION-DIRECTIVE` at 24, 1502 and 1690 only, and the tab-separated numstat rows `1 129` at 22, 1518, 1557 and 1690 and `3 0` at 23 and 1557 (each the round-3 position plus one). +11. Plan structure after the edits — nine `### Phase` headings (1391, 1444, 1459, 1478, 1499, 1524, 1551, 1574, 1583); 123 task lines; task lines P4-T1 1501 and P6-T2 1556; 1,784 lines (Grep `^`); zero carriage returns (Grep `\r`); the Write Set section (40 to 73) untouched. +12. Sibling check-offs P8-T37 (AC27) and P8-T39 (AC29) — read against the revised P4-T1: they require `INVOCATIONS: 0`, the member-set counts and every test-file hit classified `DOC-PROSE` or `OTHER-TYPE-SAME-NAME`; the two REGION-DIRECTIVE lines are production-file hits, so neither check-off changes. + +PLANNER-INTERNAL-REVIEW: PASS +CITATION-TO-TREE: PASS +AC-TRACEABILITY: PASS +SCOPE-BOUNDARY: PASS +CITATION: QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs | lines 12-31, 34-46, 92-104, 116-138, 146, 231, 243-274, 284-341 +CITATION: QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs | lines 33, 44-98, 107-149, 157-190, 192-276, 285 +CITATION: QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs | lines 27, 98-117, 181-186, 193, 213, 235, 254, 314, 331, 367, 447-478 +CITATION: QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs | lines 85-96, 161-181, 216-239, 251-280, 282-305 +CITATION: QuickFiler.Test/QuickFiler.Test.csproj | lines 17, 35, 155, 157, 161, 183, 196-215, 226-230 +CITATION: QuickFiler.Test/SetupAssemblyInitializer.cs | lines 14-25 +CITATION: QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs | line 124 +CITATION: QuickFiler.Test/Controllers/QfcItemController.MailActionsTests.cs | line 203 +CITATION: QuickFiler.Test/Controllers/QfcItemController.SeamFactoryTests.cs | line 13 +CITATION: QuickFiler.Test/TestSupport/DedicatedWorkerThread.cs | lines 4, 21 +CITATION: QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs | lines 9, 12-13, 18-24, 47-61, 62-87, 100-164, 166-172, 174-209, 211-234, 236-270, 272-310 +CITATION: QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs | lines 12, 18-27, 59-74, 180, 220-222 +CITATION: QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs | lines 12, 23-35, 93-100, 114-128, 136-154, 165-183, 195-230 +CITATION: QuickFiler.Test/Controllers/QfcDatamodelTests.cs | lines 9, 12, 95-131, 201-211, 253-283 +CITATION: QuickFiler.Test/Controllers/QfcFormControllerSeamTests.cs | lines 357-367 +CITATION: QuickFiler.Test/Controllers/QfcHomeControllerRunAsyncTests.cs | lines 325, 370-380 +CITATION: QuickFiler/Controllers/QfcDatamodel.cs | lines 25-26, 34-54, 77-103, 107-112, 128-152, 188-195, 197-241, 242-267, 271-315, 335-376, 377-416, 417-465, 464-475, 467-474, 476-491 +CITATION: QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs | lines 15-24, 37-43, 48-66, 71, 90, 146, 280-291, 299-311, 364, 404-411 +CITATION: QuickFiler/Controllers/QfcDatamodel.FrameBuilding.cs | line 11 +CITATION: QuickFiler/Controllers/QfcStreamingDequeueConfidenceGate.cs | lines 190-301 +CITATION: QuickFiler/Controllers/QfcHomeController.cs | lines 92, 132, 344, 379 +CITATION: QuickFiler/Interfaces/IQfcDatamodel.cs | lines 103, 117, 131, 138-148, 164, 166 +CITATION: QuickFiler/Properties/AssemblyInfo.cs | line 5 +CITATION: QuickFiler/Legacy/IAcceleratorCallbacks.cs | line 5 +CITATION: QuickFiler/Controllers/QfcHighConfidencePreFilter.cs | line 11 +CITATION: QuickFiler/Controllers/QfcItemController.FocusAndTheme.cs | lines 274-286 +CITATION: UtilitiesCS/HelperClasses/ThemeHelpers/Theme.cs | lines 427-445 +CITATION: UtilitiesCS/Threading/UiThread.cs | lines 266-285 +CITATION: UtilitiesCS.Test/TestHelpers/ArmingBarrierTimeProvider.cs | lines 19-54 +CITATION: scripts/vscode/Invoke-MSTestWithCoverage.ps1 | lines 89-93, 97-134, 262, 297-298, 348-355, 399-423, 430-453, 459-461 +CITATION: scripts/vscode/Invoke-MSTestWithCoverage.FirstParty.ps1 | lines 117-123 +CITATION: scripts/vscode/TaskMaster.cli.runsettings | lines 4-7 +CITATION: scripts/hygiene/Test-RepositoryHygiene.Rules.ps1 | line 21 +CITATION: scripts/vscode/Install-RepoDotNetSdk.ps1 | line 3 +CITATION: scripts/vscode/Invoke-Restore.ps1 | lines 1-10 +CITATION: .gitignore | lines 26, 140, 141, 146, 150, 151 +CITATION: .gitattributes | line 4 +CITATION: .csharpierignore | lines 4, 12 +CITATION: global.json | lines 2-9 +CITATION: dotnet-tools.json | line 6 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md | lines 6-13, 56-79, 94-108, 138-171, 173-186, 237-270, 274-306, 308-318 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/issue.md | lines 12, 65-77 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T05-50-dispatcher-pin-call-sites-research.md | sections 1.1, 2.1, 2.2, 3, 4, 5, 6, 7 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T22-20-qfc-datamodel-972-fold-research.md | sections 1 to 8 and Numeric Derivation Evidence +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round1-report.2026-10-02T08-40.md | defects 1 to 10 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round2-report.2026-10-02T23-56.md | defects 1 to 8 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round3-report.2026-10-03T01-01.md | defects 1 to 4 and advisory A1 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round4-report.2026-10-03T01-25.md | defects 1 to 3 (the advisory delta declined) +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/plan.2026-10-02T05-42.md | lines 7-9, 23, 155, 1449, 1470, 1495, 1516, 1533, 1501, 1517, 1554-1556, 1677, 1685, 1689, 1741 (pre-edit numbering) +CITATION: docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md | exists (Glob) +CITATION: docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md | exists (Glob) +AC-INVENTORY: AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8, AC9, AC10, AC11, AC12, AC13, AC14, AC15, AC16, AC17, AC18, AC19, AC20, AC21, AC22, AC23, AC24, AC25, AC26, AC27, AC28, AC29, AC30, AC31, AC32 +AC-MAPPING: AC1 | IMPLEMENTATION: P1-T1, P2-T1 to P2-T5 | TESTS: P1-T5, P2-T8, P8-T5 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC2 | IMPLEMENTATION: P1-T1, P2-T4, P2-T5 | TESTS: P2-T8, P8-T5 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC3 | IMPLEMENTATION: P1-T1, P2-T5 | TESTS: P1-T6, P2-T8, P6-T5, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC4 | IMPLEMENTATION: P1-T1, P2-T5 | TESTS: P1-T6, P2-T8, P8-T5 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC5 | IMPLEMENTATION: P1-T1, P1-T2, P2-T1 to P2-T5 | TESTS: P1-T5, P2-T8 | EVIDENCE: FEATURE/evidence/regression-testing/fail-before-pin-count.md +AC-MAPPING: AC6 | IMPLEMENTATION: P1-T1 | TESTS: P1-T6, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC7 | IMPLEMENTATION: P3-T3, P3-T4 | TESTS: P6-T6, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/focus-and-theme-class-pass-after.md +AC-MAPPING: AC8 | IMPLEMENTATION: P3-T3, P3-T4, P3-T8 | TESTS: P7-T1, P7-T2 | EVIDENCE: FEATURE/evidence/qa-gates/call-site-census.md +AC-MAPPING: AC9 | IMPLEMENTATION: P2-T1, P2-T4, P2-T5 | TESTS: P2-T6, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC10 | IMPLEMENTATION: P3-T7, P3-T8 | TESTS: P6-T5, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/fixture-class-pass-after.md +AC-MAPPING: AC11 | IMPLEMENTATION: P2-T2, P2-T3, P2-T5 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC12 | IMPLEMENTATION: P3-T5 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC13 | IMPLEMENTATION: P3-T7 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC14 | IMPLEMENTATION: P3-T8 | TESTS: P6-T5, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/fixture-class-pass-after.md +AC-MAPPING: AC15 | IMPLEMENTATION: P3-T1, P3-T2, P3-T6 | TESTS: P6-T6, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC16 | IMPLEMENTATION: P3-T3, P3-T4 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC17 | IMPLEMENTATION: P3-T5, P3-T6 | TESTS: P3-T9, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC18 | IMPLEMENTATION: P1-T1 to P5-T11, P6-T1 | TESTS: P8-T8 | EVIDENCE: FEATURE/evidence/qa-gates/file-line-counts.md +AC-MAPPING: AC19 | IMPLEMENTATION: P1-T1 to P5-T11 | TESTS: P7-T3 | EVIDENCE: FEATURE/evidence/qa-gates/prohibited-constructs-grep.md +AC-MAPPING: AC20 | IMPLEMENTATION: P6-T9 | TESTS: P8-T9 | EVIDENCE: FEATURE/evidence/qa-gates/footprint-scope.md +AC-MAPPING: AC21 | IMPLEMENTATION: P1-T2 | TESTS: P1-T4, P8-T5 | EVIDENCE: FEATURE/evidence/qa-gates/coverage-summary.md +AC-MAPPING: AC22 | IMPLEMENTATION: P8-T1 to P8-T5 | TESTS: P8-T5 | EVIDENCE: FEATURE/evidence/qa-gates/toolchain-final.md +AC-MAPPING: AC23 | IMPLEMENTATION: P0-T17, P8-T5 | TESTS: P8-T6 | EVIDENCE: FEATURE/evidence/qa-gates/coverage-comparison.md +AC-MAPPING: AC24 | IMPLEMENTATION: P1-T1 to P3-T8 | TESTS: P6-T7 | EVIDENCE: FEATURE/evidence/regression-testing/concurrent-set-test-summary.md +AC-MAPPING: AC25 | IMPLEMENTATION: P4-T2 to P4-T6 | TESTS: P4-T11, P6-T8, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC26 | IMPLEMENTATION: P5-T11 | TESTS: P5-T12, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/queue-processing-comment-census.md +AC-MAPPING: AC27 | IMPLEMENTATION: P4-T1, P4-T8 | TESTS: P4-T10, P8-T3, P8-T4, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/qfc-datamodel-legacy-callers.md +AC-MAPPING: AC28 | IMPLEMENTATION: P4-T8 | TESTS: P8-T8 | EVIDENCE: FEATURE/evidence/qa-gates/file-line-counts.md +AC-MAPPING: AC29 | IMPLEMENTATION: P4-T8 | TESTS: P6-T2, P8-T6 | EVIDENCE: FEATURE/evidence/qa-gates/coverage-comparison.md +AC-MAPPING: AC30 | IMPLEMENTATION: P4-T5, P4-T6, P4-T7, P5-T3, P5-T4 | TESTS: P6-T8, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC31 | IMPLEMENTATION: P5-T2, P5-T3, P5-T4 | TESTS: P5-T1, P5-T7, P5-T8, P5-T10, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/liveness-sensitivity-check.md +AC-MAPPING: AC32 | IMPLEMENTATION: P4-T2 to P5-T11 | TESTS: P6-T8 | EVIDENCE: FEATURE/evidence/regression-testing/datamodel-set-test-summary.md +UNRESOLVED-GAPS: NONE + +DIRECTIVE: PREFLIGHT VALIDATION ONLY +Executor preflight for this revision has not yet run; the signal below is the planner's request line for the confirming round, not a self-approval and not a discovered defect. +PREFLIGHT: REVISIONS REQUIRED diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-clearance.2026-10-03T02-21.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-clearance.2026-10-03T02-21.md new file mode 100644 index 000000000..0d2cf607c --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-clearance.2026-10-03T02-21.md @@ -0,0 +1,18 @@ +# Preflight clearance (issue #968) + +- Timestamp: 2026-10-03T02-21 +- Plan: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/plan.2026-10-02T05-42.md +- Plan git blob cleared: 9d33fa2ac20cac37bd111b7f197254510d3a39a1 (1,786 lines; committed at 5ff533106) +- Clearing round: 6 (report: evidence/other/preflight-round6-report.2026-10-03T02-21.md) +- Total preflight rounds: 6 (defects per round: 10, 8, 4, 3, 1, 0) +- MCP plan validator (validate_orchestration_artifacts, artifact_type plan): ok on this blob +- Reviewer: atomic-executor under DIRECTIVE: PREFLIGHT VALIDATION ONLY, non-isolated, read-only commands only + +PREFLIGHT: ALL CLEAR +CONVERGENCE: NO FURTHER ROUNDS EXPECTED + +## Notes + +- Optional observations O1 (P8-T8 LINES equality on a non-idempotent format-restart path) and O2 (self-review prose enumeration) were not applied. Neither affects a gate a correct run can reach, and applying either would change the cleared blob. +- Execution has not started: no plan checkbox is ticked, and Phase 0 is the next phase. +- Next step: S5_atomic_execution (out of scope for this preparation run). diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round1-report.2026-10-02T08-40.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round1-report.2026-10-02T08-40.md new file mode 100644 index 000000000..b981ec231 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round1-report.2026-10-02T08-40.md @@ -0,0 +1,205 @@ +# Preflight round 1 reviewer report, verbatim (issue #968) + +- Timestamp: 2026-10-02T08-40 +- Source: atomic-executor preflight return, recovered by the parent parallel-orchestrator from the dead preparation child's task transcript (agent a291a7fbabf9d0229, grandchild abad2f272bbd16984). Text below is verbatim. +- Plan reviewed: plan.2026-10-02T05-42.md, blob 91b1232718a914b6b3e3904c1f8b3e7d3f754b28 +- Note: defect 2 option B is superseded. The promotion record was committed in 53d975270, so re-derive Fact 11 and the INHERITED-COMMITTED expectation against the current branch instead (see preflight-round1.2026-10-02T08-40.md). + +--- + +PREFLIGHT: REVISIONS REQUIRED + +CONVERGENCE: NO FURTHER ROUNDS EXPECTED. If the deltas below are applied as written and the orchestrator settles the two orchestration items (the pwsh channel in defect 1 and the AC20 wording in defect 2), the next round should only need to confirm. + +## What this round could and could not check + +- **No build or test access.** I ran no build, test or formatter. I made no edits and wrote no memory. Every count below was re-derived by reading the files and by Grep against this worktree. +- **Read-only git commands run:** `log`, `status --porcelain`, `rev-parse HEAD origin/main`. HEAD, BASE and origin/main are all `94287369908cc920b21b0e3256314f988ad7d2f5`. +- **The pwsh channel is refused for this agent.** One trivial read-only probe was denied by a hook, and I did not retry it (hook discipline). The exact deny text: + `This agent is isolated in the worktree WORKTREE, but this command runs pwsh in a plain command; what it reads or is handed as shell text cannot be shown not to run git. Refusing to run it — a worktree-isolated agent's git operations must target its own worktree. Run the plain command from WORKTREE.` +- **Validator not run.** `mcp__drm-copilot__validate_orchestration_artifacts` is not available in this session, so the G1 to G9 output was not observed. + +## Defects + +**1. The plan only works if Bash can run pwsh, and it has no rule for a refusal.** (Execution conventions "Payload channel"; D-10; first hit at P0-T2.) +- Almost every command-bearing task is a `pwsh -NoProfile -Command` payload. The probe above shows this agent type cannot run them. +- D-10 only covers refusals of git add/commit and file edits. +- Delta (append to the "Payload channel" bullet): `If the Bash tool refuses a pwsh invocation (a refusal whose text begins "This agent is isolated in the worktree"), the executor records PWSH CHANNEL REFUSED with the verbatim refusal in that task's artifact and stops; no rephrased or alternative invocation is attempted. The plan has no non-pwsh fallback, so the orchestrator must dispatch the executor without worktree isolation.` +- Orchestration action: run the executor as a non-isolated, serialized child. + +**2. The Phase 0 commit sweeps in a staged promotion record, which then fails the footprint gates and AC20.** (Fact 11; P0-T3, P0-T17, P4-T8, P6-T9, P6-T38; spec AC20.) +- `docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md` is staged (index status `A`) but not committed. +- P0-T17 runs `git commit -m` without a pathspec, which commits the whole index, so that record goes into the commit. +- P4-T8 ("otherwise only paths from INHERITED-COMMITTED") and P6-T9 (the footprint must be exactly six code paths, and no `A` status outside FEATURE) then fail, and AC20 cannot be checked off. +- Fact 11 is also wrong. This branch has no commits above BASE; the listed SHAs (`3956fa351` and the rest) belong to another branch. `INHERITED-COMMITTED:` will therefore read `NONE`. The footprint gate is still satisfiable and not vacuous, because it still asserts exactly the six code paths. +- Recommended delta (option B). It changes AC20's wording, so it needs orchestrator sign-off. It widens the exclusion by one documentation file and leaves the production-code guarantee unchanged. + - Fact 11 becomes: `HEAD equals BASE at preflight; the branch carries no commit above BASE, so INHERITED-COMMITTED is expected to be NONE. The promotion record docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md is staged (index status A) but not committed.` + - P0-T3 Commands: append `; git -C WORKTREE diff --cached --name-status`. + - P0-T3 Acceptance: append `INHERITED-STAGED: lists every cached name-status line verbatim or NONE; every listed path is under FEATURE or under docs/features/potential/promoted/ with a leaf containing focus-and-theme-tests-leak-shared-dispatcher-setup (otherwise INHERITED STAGED SET OUT OF SCOPE: stop).` + - P0-T17: add the command `git -C WORKTREE show --name-status --format= HEAD`, and append to Acceptance `PHASE0-COMMIT-PATHS: lists only FEATURE paths and INHERITED-STAGED: paths (the commit carries the whole index).` + - P4-T8 and P6-T9: replace `INHERITED-COMMITTED:` with `INHERITED-COMMITTED: or INHERITED-STAGED:` in the inherited-path clauses. + - D-9: append `Paths staged at P0-T3 form INHERITED-STAGED: and are treated as inherited by every footprint gate.` + - Write Set "must not touch", after `docs/features/potential/`, insert: `(exception: the promotion record listed by P0-T3 as INHERITED-STAGED: is committed unchanged by P0-T17)`. + - Spec AC20 (amendment 1.2): `- [ ] AC20: No production code change: the diff against the merge base, after excluding the paths already committed on the branch before the plan's first task and the promotion record staged before it (recorded at Phase 0 as the inherited committed set and the inherited staged set), lists only paths under `QuickFiler.Test/` and this feature's documentation folder.` +- Fallback (option A, no AC change): make every commit pathspec-limited, as `git -C WORKTREE commit -m "" -- `, and leave the record staged for the orchestrator. The record then reaches the branch after AC20 has been checked off, so a later review would fail AC20 instead. + +**3. The `lock (FieldLock)` count is 6 after the change, not 5.** (Delivered-source summary line 234, P2-T6, P4-T2, P6-T19.) +- F-FIELDS line 2 contains the literal `lock (FieldLock)`, so the gate counts 66, 79, 94, the F-FIELDS comment, ENSURE and SCOPE. +- As written, P2-T6 records a CENSUS mismatch. +- Delta: replace that F-FIELDS line with ` // dispatcher into a null field. Both are read and written only while FieldLock is held.` All stated counts (5) then hold. + +**4. The P5-T2 nesting gate cannot pass for the plan's own test 4.** (P5-T2, P6-T18.) +- The gate requires every `EnsureUiThreadDispatcher()` line to come before every pin `Dispose()` line. +- Test 4 takes `freshPin` after `pinA` and `pinB` have been disposed, so as written the gate stops the run with `NESTING VIOLATION`. +- Delta: the replacement P5-T2 Acceptance is given under defect 5. + +**5. Test 4 cannot fail for the property AC4 claims ("Ownership flag is cleared on the last release").** +- If `_fixtureInstalledParked = false;` were omitted, the fresh pin still lands on a null field, sets the flag again and reverts on release. The test passes either way. +- This is a test-quality defect in a touched file, so it is in scope under the related-defect directive. +- Delta, replacing N1 test 4. Shown at in-file indentation; it still passes before and after the fix: +``` + /// + /// Specification test: passes before and after the fix. After a full two-pin cycle inside the + /// same transaction, a fresh single pin on the null baseline must still seed the parked + /// dispatcher and its release must still restore null. A second transaction then installs + /// that parked instance as its own value, and a pin taken and released under it must leave + /// the value in place: had the earlier cycle's last release left the install-ownership flag + /// set, this release would revert a value the fixture did not seed. + /// + [TestMethod] + [Timeout(GateTimeoutMs)] + public async Task EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores() + { + // Arrange + Dispatcher parked; + UiThreadDispatcherTransaction transaction = await UiThreadDispatcherFixture + .BeginTransactionAsync() + .ConfigureAwait(false); + try + { + transaction.Install(null); + IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + parked = UiThreadDispatcherFixture.Current; + pinA.Dispose(); + pinB.Dispose(); + + // Act + IDisposable freshPin = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + Dispatcher afterFreshPin = UiThreadDispatcherFixture.Current; + freshPin.Dispose(); + Dispatcher afterFreshRelease = UiThreadDispatcherFixture.Current; + + // Assert + afterFreshPin + .Should() + .NotBeNull( + because: "a pin on a null field seeds the parked dispatcher whatever earlier cycles did" + ); + afterFreshRelease + .Should() + .BeNull( + because: "the fresh pin is the only live pin, so its release reverts the seeding" + ); + } + finally + { + transaction.Dispose(); + } + + // Act (second transaction): the parked instance is now a transaction value, not a seeding + UiThreadDispatcherTransaction foreignTransaction = await UiThreadDispatcherFixture + .BeginTransactionAsync() + .ConfigureAwait(false); + try + { + foreignTransaction.Install(parked); + IDisposable foreignPin = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + foreignPin.Dispose(); + Dispatcher afterForeignRelease = UiThreadDispatcherFixture.Current; + + // Assert + afterForeignRelease + .Should() + .BeSameAs( + parked, + because: "the last release cleared the install-ownership flag, so a pin that seeded nothing leaves a transaction value in place" + ); + } + finally + { + foreignTransaction.Dispose(); + } + } +``` +- Knock-on count changes: + - **Line 459:** becomes `EnsureUiThreadDispatcher()` 10 (tests 1, 2 and 3 two each, test 4 four), bare `EnsureDispatcher` 15 lines, plus `foreignTransaction.Install(parked);` 1. Its last sentence becomes `Within each transaction, its .Install( line precedes every pin acquired under it, and every pin is disposed before that transaction's first Dispose();.` + - **Unchanged (capital T in `foreignTransaction`, case-sensitive match):** `transaction.Install(null);` 3 and `transaction.Dispose();` 4. + - **P1-T3:** append `"foreignTransaction.Install(parked);"` to the PC tokens. Acceptance PC tokens become `10, 1, 3, 1, 1, 4, 4, 1, 3, 1, 4, 1, 2, 2, 0, 0, 0, 1, 1`. + - **P5-T1:** `PRIMARY_LINES: 16` (pin-count tests 10), `CROSS_LINES: 31` → `32` (pin-count tests 15), `CONTROL_LINES: 28`, and "the thirteen test-side lines". The CROSS-only total stays sixteen. + - **Line 832:** becomes `23 before the change, 28 after N1 adds five`. + - **P6-T18:** becomes `INVOCATIONS-CLASSIFIED: 13 of 13 nested`. + - **Spec:** in "Functions/classes" item 4, append `then, in a second transaction that installs the parked instance captured in the first, one pin taken and released leaves that value in place (the discriminating check for the flag reset)`. In the Test Strategy census sentence, change "nine in the new pin-count test class" to "ten". +- P5-T2 Acceptance, replacing the whole bullet: `for each of R1SPAN, R2SPAN, R3SPAN, T1SPAN, T2SPAN, T3SPAN and T4SPAN the NEST output shows, for every transaction variable in the span (one per span; two in T4SPAN, transaction then foreignTransaction), by ascending line number: that transaction's BeginTransactionAsync() line, then its single .Install( line, then the pins taken under it, where each pin's EnsureUiThreadDispatcher() line precedes that pin's first Dispose() line (ensureScope, pinA, pinB, freshPin, foreignPin), and every such pin Dispose() line precedes that transaction's first Dispose() line; in T4SPAN the foreignTransaction BeginTransactionAsync() line follows the transaction.Dispose(); line; R4SPAN shows no EnsureUiThreadDispatcher() line and two transactionA.Dispose(); lines, the second inside a finally; the artifact records per method NESTED: YES and INSTALL-BETWEEN-PIN-ACQUIRE-AND-RELEASE: NONE, and records INVOCATIONS-CLASSIFIED: 13 of 13 nested (three in the fixture tests, ten in the pin-count tests). Any other ordering is NESTING VIOLATION: stop and report.` + +**6. The indentation statement is wrong for N1 and T1.** (Line 135.) +- N1 and T1 are shown four spaces deeper than in-file. T1 has 8 spaces in the plan, while P1-T3 requires 4. +- Delta: `Every block below except N1 and T1 is shown at its in-file indentation (four, eight, twelve, sixteen or twenty leading spaces) and is written exactly as shown. N1 and T1 carry one extra four-space Markdown indent on every line, which the executor removes: N1's using and namespace lines start in column 1, and T1 starts with four spaces.` + +**7. A backslash `WORKTREE` path breaks every `git -C` call.** (Execution conventions "Tokens".) +- Bash strips unquoted backslashes, so the delegation's backslash path fails in `git -C`. +- Delta (append): `In every git -C argument WORKTREE is written with forward slashes, because the Bash channel removes unquoted backslashes; inside a pwsh payload's double-quoted Set-Location argument the backslash form is used.` + +**8. AC11's own grep is never recorded.** (P0-T12, P2-T6, P6-T21.) +- AC11 names a grep for `installed nothing carries`. The plan records only the substitute token `A scope that installed nothing`. +- Delta: append `"installed nothing carries"` to the FIX token list (baseline 0, post 0, with a note that the baseline is vacuous because the phrase wraps across lines). P6-T21 also requires `installed nothing carries` 0. + +**9. P3-T9 names the wrong file list.** +- Delta: replace `(the P0-T12 lists, FT extended with "Issue #480 shared arrange helper" for TS)` with `(the P0-T12 lists, the TS list extended with "Issue #480 shared arrange helper")`. + +**10. Tests that relied on the leaked dispatcher have no defined outcome.** (P6-T5, CMD-COVERAGE-POST, Risks.) +- The two deleted theme-test calls left the parked dispatcher installed for the rest of the run. Every later transaction restored it, because R2 and R3 restore their captured previous value. +- Production QuickFiler code reads `UiThread.Dispatcher` in about 35 places. A test that depended on the leak would now throw `The UI dispatcher has not been captured`. +- The plan would either restart on the wrong files (D-13) or stop with `NEW FAILURE OUTSIDE SCOPE`. Under the related-defect directive, such a test is in scope. +- Delta, CMD-COVERAGE-POST (append): `foreach ($r in @($trx.SelectNodes("//t:UnitTestResult", $ns))) { $o = $r.GetAttribute("outcome"); if ($o -ne "Passed" -and $o -ne "NotExecuted") { $m = $r.SelectSingleNode("t:Output/t:ErrorInfo/t:Message", $ns); Write-Output ("MESSAGE " + $r.GetAttribute("testName") + " :: " + $(if ($m) { $m.InnerText -replace "\s+", " " } else { "(no message)" })) } }` +- Delta, P6-T5 Acceptance (insert after the `NEW-FAILURES:` clause): `Any NEW-FAILURES: name whose MESSAGE contains The UI dispatcher has not been captured is recorded as LEAK-DEPENDENT TEST EXPOSED: followed by the name, and stops the run for re-planning under the related-defect directive; it is neither a D-13 restart nor NEW FAILURE OUTSIDE SCOPE.` +- Delta, Risks: add a matching bullet. + +## Checked and correct + +**Ordering and satisfiability:** +- The expect-fail P1-T5 is not inside any exit-0 gate. +- P1-T6 runs only tests 2 to 4, and I traced all three as passing on the unmodified fixture. +- The read-only formatter baseline (P0-T9) runs before the first write-mode format (P4-T1). + +**Line and token citations:** +- Line totals 342, 470, 497 and 440 are correct. +- The R4 header (221 to 224), the 16-space close at 270, and the R4SPAN, R4HEAD and R4TAIL baseline and post-change counts all check out. +- Every other FIX/FAT/TS/FT token count checks out, except `lock (FieldLock)` (defect 3). +- Census figures 20/9/23 and the csproj lines 200, 201, 203 and 212 are correct. +- The TestSupport and fixture-tests hunk bounds hold. + +**Concurrency design:** +- After the change, nothing in QuickFiler.Test holds a pin across a gate release. Every pin sits inside a transaction with no `Install` between taking and releasing it. +- Nothing in QuickFiler.Test writes the field outside a transaction: + - It contains no `UiThread.Init` or `ResetForTesting` call. + - The only `Exchange`/`CompareExchange` writers are inside the fixture. + - `EmailMoveMonitorTests` only reads. +- Option (a) for R4 is sound. Its `NotBeSameAs(liveA)` assertion also passes when the original value is null. +- Under these conditions, test 1's final "last release nulls" assertion is deterministic. +- R5 and the #882 round-trips have no try/finally, but nothing between acquiring and disposing can throw, so they are not defects. + +**Commands and evidence:** +- Every command targets this worktree: through the PREFIX `Set-Location` plus `SetCurrentDirectory`, through `Invoke-Restore.ps1` (which resolves the solution relative to its own script folder), and through the runner's fixed coverage output (kept under the ignored `coverage\` folder). +- SDK, tool restore and NuGet restore all run before the first msbuild. +- Only trx-derived summaries and JaCoCo projections are committed, and every evidence path is under `FEATURE/evidence//`. +- Plan and spec prose meets the tonality rules. + +## Plan state + +- Phase 0, nothing executed. Next five tasks: [P0-T1], [P0-T2], [P0-T3], [P0-T4], [P0-T5]. +- Acceptance criteria in `spec.md`: 24 total, 0 checked off. + +Files reviewed: +- `WORKTREE\docs\features\active\2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968\plan.2026-10-02T05-42.md` +- `WORKTREE\docs\features\active\2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968\spec.md` diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round1.2026-10-02T08-40.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round1.2026-10-02T08-40.md new file mode 100644 index 000000000..a53e8eed2 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round1.2026-10-02T08-40.md @@ -0,0 +1,30 @@ +# Preflight round 1 record (issue #968) + +- Timestamp: 2026-10-02T08-40 +- Reviewer: atomic-executor, DIRECTIVE: PREFLIGHT VALIDATION ONLY, read-only, no build or test access (pwsh refused under worktree isolation) +- Plan: plan.2026-10-02T05-42.md, blob 91b1232718a914b6b3e3904c1f8b3e7d3f754b28 +- Result: PREFLIGHT: REVISIONS REQUIRED +- Convergence: CONVERGENCE: NO FURTHER ROUNDS EXPECTED (conditional on deltas applied verbatim) +- Defects reported: 10 +- Status: deltas NOT yet applied; run held by coordinator directive before revision. + +## Defects (summary; verbatim deltas are in the reviewer's report and must be re-applied by atomic-planner) + +1. Payload channel: no rule for a refused pwsh invocation. Add a PWSH CHANNEL REFUSED stop rule; executor must run non-isolated. +2. Fact 11 is wrong: the branch had no commits above BASE when preflight ran. Since then the promoted record docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md was committed in 53d975270 together with the feature documents, so it is now part of the inherited committed set (under docs/features/potential/, which P0-T3 already admits). Re-derive fact 11 and the INHERITED-COMMITTED expectation against the current branch; the staged-record delta (option B) is no longer needed. +3. F-FIELDS comment contains the literal `lock (FieldLock)`, making the post-change count 6, not 5. Reword the comment to "only while FieldLock is held". +4. P5-T2 nesting gate is unsatisfiable for test 4 (fresh pin acquired after earlier pin releases). Replace with per-pin, per-transaction ordering. +5. Test 4 cannot fail if the ownership flag is not cleared. Replace with the two-transaction variant (second transaction installs the captured parked instance; a pin under it must leave it in place). Knock-on count updates: P1-T3 PC tokens, P5-T1 PRIMARY 16 / CROSS 32 / CONTROL 28, P6-T18 13 of 13, spec item 4 and census sentence. +6. Indentation statement wrong for N1 and T1 (shown with an extra four-space Markdown indent). +7. WORKTREE must be written with forward slashes in git -C arguments. +8. AC11 literal `installed nothing carries` not recorded; add to the FIX token list (baseline vacuous, post 0). +9. P3-T9 wording: the TS list, not FT, is extended with "Issue #480 shared arrange helper". +10. No defined outcome for tests that relied on the leaked parked dispatcher (message "The UI dispatcher has not been captured"): add MESSAGE capture to CMD-COVERAGE-POST, a LEAK-DEPENDENT TEST EXPOSED stop in P6-T5, and a Risks bullet. + +## Verified correct by the reviewer + +Ordering and satisfiability of the expect-fail run, line and token citations other than defect 3, census 20/9/23, csproj lines, concurrency design (no pin spans a gate release after the change; no field write outside a transaction), R4 option (a), commands targeting this worktree, bootstrap ordering, evidence projections and paths, tonality. + +## Remaining + +Apply the deltas in place (atomic-planner), re-validate with the MCP plan validator, run a confirming preflight round (at least one round with build access from a non-isolated session), then write the clearance artifact. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round2-report.2026-10-02T23-56.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round2-report.2026-10-02T23-56.md new file mode 100644 index 000000000..5dc632c6a --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round2-report.2026-10-02T23-56.md @@ -0,0 +1,152 @@ +# Preflight round 2 reviewer report, verbatim (issue #968) + +- Timestamp: 2026-10-02T23-56 +- Source: atomic-executor return under `DIRECTIVE: PREFLIGHT VALIDATION ONLY`, delegated by the preparation-mode orchestrator (non-isolated; the pwsh channel was refused by the pre-implementation gate, see the report). +- Plan reviewed: plan.2026-10-02T05-42.md, blob 136eb144fd5d441a97e9fb0d1c7bc60d264d1acb (commit ce51e29c9) +- Round: 2 (the single confirming round authorised by the coordinator; no further round is run in this preparation) +- Text below is verbatim (HTML entity escapes in the transport were restored to `<` and `>`). + +--- + +PREFLIGHT: REVISIONS REQUIRED +CONVERGENCE: FURTHER ROUNDS LIKELY (the pwsh channel was refused in this round, so I could not run any plan command to see what it prints on success. One more round with a working pwsh channel is needed to confirm the deltas and to run the command-output checks that this round could only do by reading the files.) + +## What I could and could not check + +- **The pwsh channel was refused by a hook.** My first read-only probe (a `pwsh -NoProfile -Command` token-count run against the item worktree) was denied. I did not retry it. Deny text, verbatim: + `PreToolUse:Bash hook error: PREIMPLEMENTATION_GATE_BLOCKED: Implementation operations require artifacts/orchestration/orchestrator-state.json to contain issue number, feature folder, route metadata, lifecycle readiness, and checkpoint state before implementation begins.` + - The same gate will refuse every pwsh payload in this plan when the executor runs from this session tree (defect 8). + - Because of the deny I ran no build, test, formatter or plan payload. +- **Validator not run.** The `mcp__drm-copilot__validate_orchestration_artifacts` tool is not available to me. +- **Git commands run (all read-only):** + - `git -C log --oneline -8`: HEAD `ce51e29c9`; above BASE are `53d975270`, `d096f1250`, `4c6de5e84`, `87cca65ed` and `ce51e29c9`. + - `git -C hash-object ` returned `136eb144fd5d441a97e9fb0d1c7bc60d264d1acb`, matching the plan blob you supplied. + - `git -C merge-base origin/main HEAD` returned `94287369908cc920b21b0e3256314f988ad7d2f5`. + - `git -C diff --name-status 94287369… HEAD` lists 8 FEATURE paths plus the two promoted records, all with status `A`. + - `git -C status --porcelain` was empty before the review and empty afterwards. +- **Every count and line citation below was checked with the Read and Grep tools** against the item worktree. The FluentAssertions 8.11.0 checks (`TaskFormatter` exists, so formatting a `Task` operand does not block; there is no `Should(Task)` overload) were made against the shared `packages` folder of the main checkout. + +## Defects + +**1. The F-SCOPE block is 43 lines, not 42, so the fixture is 375 lines after Phase 2, not 374.** +- Delivered Source F-SCOPE runs from plan line 227 to plan line 269, which is 43 lines. +- The arithmetic is 342 + 5 + 13 + 3 + 1 + 11 = 375. +- P2-T6 and P3-T9 assert 374 as an exact gate. A correct execution therefore fails P2-T6. +- Deltas: + - Plan line 225: replace `forty-two lines replacing thirty-two:` with `forty-three lines replacing thirty-two:`. + - Plan line 271: replace `After F-FIELDS to F-SCOPE the fixture is 374 lines before formatting;` with `After F-FIELDS to F-SCOPE the fixture is 375 lines before formatting;`. + - P2-T6 Acceptance: replace `FIX LINES 374;` with `FIX LINES 375;`. + - P3-T9 Acceptance: replace `LINES 374, 482, 442, 472 for FIX, FAT, TS, FT` with `LINES 375, 482, 442, 472 for FIX, FAT, TS, FT`. + +**2. The four new fold span baselines are off by one.** +- `CMD-SPAN-TOKEN-COUNT` prints `SPAN: -`. The existing #968 spans follow this rule: R4SPAN's END is line 285 and it prints `212-284`; ENSURE's END is line 146 and it prints `122-145`. +- The fold END lines are: + - LIV line 166 (`/// Reads the issue #424 …`) + - LIV line 218 (test 2's declaration) + - DMT line 134 + - QQP line 311 +- So the printed spans are `110-165`, `183-217`, `96-133` and `299-310`. P0-T13 asserts `110-166`, `183-218`, `96-134` and `299-311`, so it stops with `FOLD CENSUS MISMATCH`. +- Deltas: + - Span anchors, `T1-LIVE` bullet: replace `Baseline \`SPAN: 110-166\`.` with `Baseline \`SPAN: 110-165\`.` + - `HELD` bullet: replace `Baseline \`SPAN: 183-218\`.` with `Baseline \`SPAN: 183-217\`.` + - `T-SIB` bullet: replace `Baseline \`SPAN: 96-134\`.` with `Baseline \`SPAN: 96-133\`.` + - `GATE-LAMBDA` bullet: replace `Baseline \`SPAN: 299-311\`.` with `Baseline \`SPAN: 299-310\`.` + - P0-T13 Acceptance: see the combined replacement under defect 3. + +**3. The T1-LIVE baseline for `(await pending)` is 1, not 0.** +- LIV line 163 reads `(await pending).Should().BeEmpty();` and lies inside the span. +- P0-T13 Acceptance, combined delta for defects 2 and 3: replace + `` `T1-LIVE` 5, 0, 3, 3, 1, 0, 0 with `SPAN: 110-166`; `HELD` 1, 0, 0 with `SPAN: 183-218`; `T-SIB` 1, 0, 0, 0, 1 with `SPAN: 96-134`; `GATE-LAMBDA` 1, 0 with `SPAN: 299-311`. `` + with + `` `T1-LIVE` 5, 0, 3, 3, 1, 0, 1 with `SPAN: 110-165` (the last value is LIV line 163, `(await pending).Should().BeEmpty();`); `HELD` 1, 0, 0 with `SPAN: 183-217`; `T-SIB` 1, 0, 0, 0, 1 with `SPAN: 96-133`; `GATE-LAMBDA` 1, 0 with `SPAN: 299-310` (each printed end is the END anchor line minus one, as for R4SPAN). `` + +**4. The QfcDatamodel.cs baseline for `ForEachAwaitWithCancellationAsync` is 2, not 1.** +- Line 431 is a comment (`// ForEachAwaitWithCancellationAsync (System.Linq.Async) is obsolete…`) and line 440 is the call. +- P0-T13 would stop with `FOLD CENSUS MISMATCH`. +- The post-change value of 0 is correct, because both lines sit in the deleted block 417 to 465. +- Deltas: + - Fact 14: replace `` `ForEachAwaitWithCancellationAsync` 1; `` with `` `ForEachAwaitWithCancellationAsync` 2 (the comment at 431 and the call at 440); ``. + - P0-T13 Acceptance: replace `QDM tokens 2, 4, 0, 1, 4, 1, 1, 1, 2, 2, 7, 7, 1, 2, 1, 1, 1, 1, 2, 2, 3;` with `QDM tokens 2, 4, 0, 1, 4, 1, 1, 1, 2, 2, 7, 7, 1, 2, 1, 1, 2, 1, 2, 2, 3;`. + +**5. The post-change `FakeTimeProvider` counts ignore the `ArmingFakeTimeProvider` substring.** +- The token count is an ordinal substring match, so every `ArmingFakeTimeProvider` line also counts as `FakeTimeProvider`. +- LIV after L-T1: `new ArmingFakeTimeProvider()` and `` give 2, not 0. +- DMT after M-T: the 5 untouched lines plus the same 2 give 7, not 5. +- The prose `worker,` count is also wrong: it is 4, because the `SynchronousBackgroundWorker worker,` parameter line matches too. That value is not gated. +- Deltas: + - Plan line 887: replace `` `worker,` 3 lines (the three callers); `Task.Yield` 0; `fake.Advance` 0; `FakeTimeProvider` 0; `` with `` `worker,` 4 lines (the three callers and the `StartHeldOpenLoader` parameter line `SynchronousBackgroundWorker worker,`); `Task.Yield` 0; `fake.Advance` 0; `FakeTimeProvider` 2 (`new ArmingFakeTimeProvider()` and the L-T1 doc cref `ArmingFakeTimeProvider.Armed`; the count is an ordinal substring match); ``. + - P5-T3 Acceptance: replace ``LIV tokens `Task.Yield` 1 (the L-T1 doc line `Task.Yield` only), `fake.Advance` 0, `FakeTimeProvider` 0,`` with ``LIV tokens `Task.Yield` 0, `fake.Advance` 0, `FakeTimeProvider` 2 (the two `ArmingFakeTimeProvider` lines of L-T1),``. This replacement also carries defect 7. + - Plan line 1014: replace ``` `Task.Yield` 1 (the M-T doc line only; the executable `await Task.Yield();` is gone); `await Task.Yield();` 0; `fake.Advance` 2 (the two untouched tests at pre-edit 241 and 280); `FakeTimeProvider` 5; ``` with ``` `Task.Yield` 0; `await Task.Yield();` 0; `fake.Advance` 2 (the two untouched tests at pre-edit 241 and 280); `FakeTimeProvider` 7 (five untouched lines plus `new ArmingFakeTimeProvider()` and the M-T doc cref `ArmingFakeTimeProvider.Armed`); ```. + - P5-T4 Acceptance: replace ``` `fake.Advance` 2, `FakeTimeProvider` 5, `[TestMethod]` 9.``` with ``` `fake.Advance` 2, `FakeTimeProvider` 7, `[TestMethod]` 9.```. + +**6. `HUNK_COUNT: 9` for QfcDatamodel.cs cannot occur.** +- Git puts two changes in one hunk when at most six unchanged lines separate them (with the default three context lines). +- The deletion at old line 363 and the replacement at old line 369 are 5 lines apart, so they form one hunk. +- The block deleted from old line 377 to 465 and the region deleted at 469 to 473 are 3 lines apart, so they also form one hunk. +- The observed count is therefore 7, or 6 if git shifts the 377 block up one line (lines 376 and 465 are identical). The merged 363/369 hunk is also neither "a pure deletion" nor "the single replacement", so that clause fails as well. +- `--numstat` reports the same edit independently of how git groups it into hunks. +- P4-T9 Acceptance: replace ``` `HUNK_COUNT:` for QfcDatamodel.cs is 9 and every `HUNK` is a pure deletion or the single one-line replacement at old line 369 (no hunk adds more than one line); ``` with ``` `HUNK_COUNT:` for QfcDatamodel.cs is recorded, not gated (git merges edits separated by at most six unchanged lines, so the nine P1 edits yield fewer hunks); the `--numstat` row for `QuickFiler/Controllers/QfcDatamodel.cs` reads `1 129` (128 removed lines plus the replaced line at old 369, whose replacement is the only added line); ```. P6-T2 restates P4-T9, so it inherits this change. + +**7. A literal `Task.Yield` stays in both rewritten tests' doc comments, but AC31 says the tests "contain no `Task.Yield`".** +- The plan meets AC31 only through span gates that start at the method declaration. Grepping the whole file finds 1 hit in each of LIV and DMT, which a reviewer would read as a failed criterion. +- Deltas: + - L-T1: replace ` /// Task.Yield. proves the gate armed its` with ` /// a scheduler yield. proves the gate armed its`. + - M-T: replace ` /// Task.Yield, and the dequeue task itself is the completion signal.` with ` /// a scheduler yield, and the dequeue task itself is the completion signal.` + - The count changes are already in the defect 5 deltas for P5-T3 and plan line 1014. Plan line 887 then reads correctly as written. + +**8. No stop rule covers a pre-implementation-gate refusal of a pwsh payload or an evidence Write.** +- D-10 covers refusals of `git add`, `git commit`, `.cs` and `.csproj` edits, and spec edits. The payload-channel rule covers only the "This agent is isolated in the worktree" text. +- This round shows that `PREIMPLEMENTATION_GATE_BLOCKED` refuses a read-only pwsh payload issued from this session tree. +- D-10: replace ``A PreToolUse refusal of any `git add`, `git commit`, `.cs` edit, `.csproj` edit or spec edit is recorded verbatim`` with ``A PreToolUse refusal of any `git add`, `git commit`, `.cs` edit, `.csproj` edit, spec edit, evidence-file Write or pwsh payload (including a refusal whose text begins `PREIMPLEMENTATION_GATE_BLOCKED`) is recorded verbatim``. +- Orchestration action, not a plan edit: before dispatch, seed `artifacts/orchestration/orchestrator-state.json` in the executor's session tree. The executor is non-isolated, so its process starts in the coordinator tree and the hook reads the file from there. + +Each delta was checked against the plan's own rules. Every changed token stays on one physical line. None contains a placeholder or a double quote inside a token. No acceptance criterion is weakened (defect 6 replaces an unsatisfiable hunk count with a stricter numstat check). The delta prose has no hyperbole or informal wording. + +## Checked and correct + +- **Round-1 deltas:** + - Defects 1, 3, 4, 5, 6, 7, 8, 9 and 10 are applied correctly. + - Defect 2 is correctly superseded: the INHERITED-COMMITTED set is stated by membership, and every commit (P0-T19, P6-T9, P8-T46, and the D-13 restart commit) is pathspec-limited. +- **Fixture and pin-count tests:** + - N1 compiles under C# 7.3 (the project sets no LangVersion). `parked` is definitely assigned after the try/finally. + - Traced on the unmodified fixture: test 1 fails with the predicted message, and tests 2 to 4 pass. All four pass on the fixed fixture. + - Every PC token count and the T3SPAN NEST tail are correct. + - Census after the change: PRIMARY 16, CROSS 32, CROSS-only 16, CONTROL 28, `INVOCATIONS-CLASSIFIED: 13 of 13`. + - Theme, test-support and fixture-test files: the FAT, TS and FT counts and line totals (482, 442, 472) and the R4SPAN, R4HEAD and R4TAIL values before and after are correct. The TestSupport `HUNK_COUNT: 2` and the fixture-test hunk bounds hold. +- **D-18 design:** + - The gate reaches its first `Delay` synchronously (QfcStreamingDequeueConfidenceGate.cs lines 243 to 255, with no earlier await on the path). + - `ZeroAcceptanceCeiling` is 120 s and `DefaultFirstBatchDeadline` is 12 s, so neither fires at a simulated 200 or 400 ms. + - No timer is created before the dequeue call. + - `ReArm()` comes before `Advance`, so the re-arm signal is not racy. + - The `ConfigureAwait(false)` on the gate's await makes the re-arm proof independent of the ambient context. + - Scope 2 keeps the inlining rule valid, so the `loaderRelease` and `Worker_DoWork` continuations run inline and clear the flag before `ReadLivenessFlag`. Neither scope body contains an await. + - The sensitivity edit makes the gate return after one wait, so each test fails on its `BeSameAs` re-arm assertion. FluentAssertions 8.11 formats `Task` operands without reading `.Result`, so the failure does not hang. + - Under Workers=0 and ClassLevel, each test owns its clock and its scope is per-thread. + - No sleep, delay, retry, Yield loop, timeout change or `[DoNotParallelize]` is added. +- **Dead-code removal (fact 15):** + - The zero-caller proof is correct: 24 lines in 5 files, `\blog\b` 3 lines, string/reflection sweep 2 lines, and none of the four members in the interface. + - The removal arithmetic is 5 + 49 + 40 + 1 + 24 + 2 + 1 + 3 + 3 = 128, giving 495 − 128 = 367. + - The post-edit QfcDatamodel.cs counts in P4-T8 are correct. + - No per-file coverage gate is placed on the `[ExcludeFromCodeCoverage]` type `QfcDatamodel`. +- **Folded-scope test files:** + - The baseline and P4-T4 to P4-T7 counts for TD, ZB and LIV are correct (P4-T6 interim, HELD 0/1/1). + - Line totals: TD 229 and LIV about 346. QueueProcessing stays 413, and its P5-T11 counts and `HUNK_COUNT: 2` are correct. + - Project file: lines 155 to 229 match the plan's citations, and the T1/T2 placement is correct. +- **Ordering:** + - P1-T5 and P5-T8 (both expect-fail) sit outside every exit-0 gate. + - The sensitivity edit is reverted and checked with `diff --exit-code` before any later gate. P6-T2 asserts GATE-LAMBDA 1, 0 before the P6-T9 commit. + - P0-T9 (read-only csharpier check) runs before any write-mode format. +- **Toolchain, gates and evidence:** + - The CLAUDE.md toolchain order and commands are followed: `/t:Rebuild` for the analyzer and nullable gates, and no `/p:Nullable=enable`. + - G7 to G9: every `git diff` is anchored, every name-listing diff has a porcelain companion, and the write-mode steps are observed through file hashes and markers. + - Evidence lives only under `FEATURE/evidence//`. Only trx-derived summaries and JaCoCo projections are committed; raw documents and logs stay under the gitignored `coverage/` folder. +- **Coverage of scope:** + - The AC20 footprint is exactly the 14 code paths, with the two production paths named. + - #972 items 1 to 5 and the liveness residual each map to tasks. + - The PR body is directed to carry `Closes #968` and `Closes #972` (spec lines 134 and 332, and P8-T46). + - The planner's AC-INVENTORY and AC-MAPPING records match the plan. + - The PWSH CHANNEL REFUSED rule is present. + +Defect count: 8. + +Plan state: nothing executed, no box checked. Next five tasks: [P0-T1], [P0-T2], [P0-T3], [P0-T4], [P0-T5]. Acceptance criteria in `spec.md`: 32 total, 0 checked off. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round2.2026-10-03T00-12.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round2.2026-10-03T00-12.md new file mode 100644 index 000000000..2a55559e3 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round2.2026-10-03T00-12.md @@ -0,0 +1,26 @@ +# Preflight round 2 record (issue #968) + +- Timestamp: 2026-10-03T00-12 +- Reviewer: atomic-executor, `DIRECTIVE: PREFLIGHT VALIDATION ONLY`, non-isolated. Its pwsh channel was refused by the pre-implementation gate (verbatim deny text in the report), so it reviewed by Read, Grep and read-only git only. +- Plan reviewed: plan.2026-10-02T05-42.md, blob 136eb144fd5d441a97e9fb0d1c7bc60d264d1acb (commit ce51e29c9), 1,778 lines. +- Result: `PREFLIGHT: REVISIONS REQUIRED` +- Convergence: `CONVERGENCE: FURTHER ROUNDS LIKELY (the pwsh channel was refused in this round, so I could not run any plan command to see what it prints on success. One more round with a working pwsh channel is needed to confirm the deltas and to run the command-output checks that this round could only do by reading the files.)` +- Defects reported: 8. Verbatim report and deltas: `evidence/other/preflight-round2-report.2026-10-02T23-56.md`. +- Round count for this plan: 2 (round 1 on 2026-10-02T08-40 with 10 defects; round 2 here with 8 defects). + +## Delta application + +- Applied by atomic-planner in place on 2026-10-03; all eight defects applied. The defect 8 "Orchestration action" bullet is not a plan edit and was not applied to the plan (see "Open item for the coordinator"). +- One value in the defect 5 delta was corrected rather than copied: the reviewer's post-change `FakeTimeProvider` count for `QfcDatamodelTests.cs` (7) assumed six baseline lines; the file has five (lines 99, 216, 224, 249, 258; the `using Microsoft.Extensions.Time.Testing;` directive does not contain the substring). The test rewrite replaces line 99, so the post-change value is 4 + 2 = 6. The planner also corrected the P0-T13 baselines it found wrong on re-derivation: Liveness 1 (line 114 only) and QfcDatamodelTests 5. The orchestrator re-verified both baseline counts with Grep against the item worktree on 2026-10-03. +- Knock-on edits K1 to K6 are listed in `evidence/other/planner-review.2026-10-02T22-44.md`, section `## Round-2 delta application (2026-10-02T23-56 deltas)`, together with a fresh `SELF-REVIEW: RE-DERIVED THIS PASS` enumeration and a `PLANNER-INTERNAL-REVIEW: PASS` record (AC1 to AC32, `UNRESOLVED-GAPS: NONE`). +- Revised plan: 1,780 lines, blob ff2d67a45b10b2c952ee7aeb261c324b91ee659c. +- MCP plan validator (`mcp__drm-copilot__validate_orchestration_artifacts`, artifact_type plan) after the round-2 deltas: `ok` ("Validated plan artifact"). + +## Status + +- No further preflight round was run: the coordinator authorised exactly one confirming round in this preparation. The plan is NOT cleared. No `preflight-clearance` artifact exists. +- Next: a third preflight round (round 3) by atomic-executor, from a session whose pwsh channel is admitted, to confirm the round-2 deltas and to run the command-output checks (success-case output of `CMD-TOKEN-COUNT`, `CMD-SPAN-TOKEN-COUNT` and the census commands) that rounds 1 and 2 could only do by reading. + +## Open item for the coordinator (orchestration, not a plan defect) + +- The pre-implementation gate (`enforce-orchestration-preimplementation-gate.ps1`) refused a read-only `pwsh -NoProfile -Command` payload from a non-isolated executor whose process starts in the coordinator session tree `TaskMaster-wt/2026-10-02T21-24`: the command leg reads `artifacts/orchestration/orchestrator-state.json` from that tree, where no checkpoint exists. Every plan task is a pwsh payload, so execution (and a pwsh-capable round 3) from that tree requires a ready checkpoint in the executor's session tree, or a session rooted in the item worktree. `git -C add` and `commit` from the same tree were admitted. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round3-report.2026-10-03T01-01.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round3-report.2026-10-03T01-01.md new file mode 100644 index 000000000..97b3a636d --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round3-report.2026-10-03T01-01.md @@ -0,0 +1,166 @@ +# Preflight round 3 reviewer report, verbatim (issue #968) + +- Timestamp: 2026-10-03T01-01 +- Source: atomic-executor return under `DIRECTIVE: PREFLIGHT VALIDATION ONLY`, delegated by the preparation-mode orchestrator (non-isolated; pwsh channel admitted after the session-tree checkpoint was seeded). +- Plan reviewed: plan.2026-10-02T05-42.md, blob ff2d67a45b10b2c952ee7aeb261c324b91ee659c (commit fd85053a0), 1,780 lines. +- Round: 3 +- Text below is verbatim (HTML entity escapes in the transport were restored to `<` and `>`). + +--- + +PREFLIGHT: REVISIONS REQUIRED +CONVERGENCE: FURTHER ROUNDS LIKELY (all four defects have mechanical deltas, but the plan needs one more round to confirm they were applied. I found no structural or design defect.) + +## Review scope + +- **Plan reviewed:** `plan.2026-10-02T05-42.md`, blob `ff2d67a45b10b2c952ee7aeb261c324b91ee659c`. I confirmed this with `git -C hash-object`. The plan has 1,780 lines. +- **Item worktree:** HEAD is `fd85053a0`, branch `bug/focus-and-theme-tests-leak-shared-dispatcher-setup-968`. `git status --porcelain` was empty before and after the review. +- **Read-only:** I ran no write-mode command, edited nothing in the item worktree, and ticked no checkbox. +- **Validator:** the plan validator MCP tool is not available to me, so I did not run it. + +## pwsh commands run (all read-only, each printed `WORKTREE-LEAF: agent-a291a7fbabf9d0229`) + +1. **P0-T13 tokens (my own combined payload for LIV, TD, ZB and DMT):** every value matches the plan. + - LIV: 1, 3, 0, 2, 0, 4, 3, 3, 1, 0, 0, 0, 0, 4. + - TD: 1, 2, 0, 1, 1, 0, 5. + - ZB: 1, 4, 0, 3, 0, 1, 1, 1, 1, 1, 0, 3. + - DMT: 2, 0, 0, 0, 0, 1, 0, 1, 4, 5, 0, 9. +2. **A second combined payload was refused by a hook.** It held the QDM, QQP and PROJ token lists plus the T1-LIVE, HELD, T-SIB and GATE-LAMBDA spans. Deny text, verbatim: + `PreToolUse:Bash hook error: PROMOTION_MCP_ONLY_BLOCKED: Direct GitHub issue creation via `gh` bypasses the approved drm-copilot MCP promotion path (`mcp__drm-copilot__new_potential_entry` -> `mcp__drm-copilot__potential_to_issue` -> `mcp__drm-copilot__new_active_feature_folder`). Use those MCP tools instead.` + - I did not retry that payload or the T1-LIVE command it contained. I verified T1-LIVE by reading the file instead (see confirmation 3). + - I did run the other plan commands it had combined, each as its own payload in the plan's form (items 3 to 6). Strictly, those are re-runs of parts of the refused command; I am stating this openly. + - The cause is in Advisory A1 below. +3. **QDM (plan's single-file form):** 2, 4, 0, 1, 4, 1, 1, 1, 2, 2, 7, 7, 1, 2, 1, 1, **2**, 1, 2, 2, 3. This matches the plan, including the corrected `ForEachAwaitWithCancellationAsync` value of 2. +4. **QQP:** 1, 1, 2, 1, 3, 0, 0, 1, 1, 0, 1. Matches. +5. **PROJ:** 187 for the ``, `${`, `$(` or `%`) or a double quote. +- No acceptance criterion is weakened. Defect 3 keeps every final value, and the project-file numstat restatement is stricter than before. +- The wording is neutral. + +## Advisory A1 (not counted as a defect) + +- **Mechanism (from the hook source):** + - `pwsh` is listed as a wrapper in `hook-command-scanner.ps1`, so the hook treats the whole payload as raw text. + - It then refuses the call if the text contains `gh` + `issue` + (`create` or `new`) anywhere, case-insensitively, as plain substrings. + - `gh` is satisfied by words such as `High` or `through`. +- **What happened:** the refused payload combined the T1-LIVE anchor (`Reads the issue #424`) with the T-SIB anchor (`HighConfidenceMode`) and a token containing `new BackgroundWorker()`. Run separately, as the plan requires, each payload passes. +- **Why it matters:** P5-T1 records both span payloads in one artifact. Under the revised D-10 rule a pwsh refusal stops the run. +- **Optional delta (Payload channel, line 1050).** Append after `…runs as one Bash tool call of the form `pwsh -NoProfile -Command ''`, newlines included, with the substitutions applied.`: `Payloads are never merged into one call: the hook layer scans a pwsh payload as raw text, so a merged payload can combine words that each payload alone does not.` + +## Agent memory + +- I updated `REDACTED-PATH\.claude\agent-memory\atomic-executor\project_phrase_count_payload_trips_promotion_gh_issue_hook.md` with the hook mechanism found in this round. +- I added a pointer line for it in `REDACTED-PATH\.claude\agent-memory\atomic-executor\MEMORY.md`. +- Both files are in the session tree. I wrote nothing in the item worktree. + +**Defect count:** 4. + +**Plan state:** nothing executed, no box checked. The next five tasks are [P0-T1] to [P0-T5]. Acceptance criteria in `spec.md`: 32 total, 0 checked. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round3.2026-10-03T01-15.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round3.2026-10-03T01-15.md new file mode 100644 index 000000000..85067b891 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round3.2026-10-03T01-15.md @@ -0,0 +1,23 @@ +# Preflight round 3 record (issue #968) + +- Timestamp: 2026-10-03T01-15 +- Reviewer: atomic-executor, `DIRECTIVE: PREFLIGHT VALIDATION ONLY`, non-isolated, pwsh channel admitted. It ran the plan's read-only token, span, census, line-count and legacy-caller commands against the item worktree. +- Plan reviewed: plan.2026-10-02T05-42.md, blob ff2d67a45b10b2c952ee7aeb261c324b91ee659c (commit fd85053a0), 1,780 lines. +- Result: `PREFLIGHT: REVISIONS REQUIRED` +- Convergence: `CONVERGENCE: FURTHER ROUNDS LIKELY (all four defects have mechanical deltas, but the plan needs one more round to confirm they were applied. I found no structural or design defect.)` +- Defects reported: 4, plus advisory A1. Verbatim report and deltas: `evidence/other/preflight-round3-report.2026-10-03T01-01.md`. +- Round-2 deltas: all eight confirmed applied; the planner's corrected `FakeTimeProvider` post-change count of 6 for `QfcDatamodelTests.cs` confirmed by the plan's own token command (baseline 5 on lines 99, 216, 224, 249, 258). +- Round count for this plan: 3 (round 1: 10 defects; round 2: 8; round 3: 4). + +## Delta application + +- Applied by atomic-planner in place: defects 1 to 4 verbatim, and advisory A1 (elected by the orchestrator because a merged pwsh payload refused by the promotion hook would stop the run under the revised D-10 rule). +- Plan line 1554 (sibling of defect 3) was read and left unchanged: it lists the commands to re-run, not the values that must hold. +- Self-review and `PLANNER-INTERNAL-REVIEW: PASS` record: `evidence/other/planner-review.2026-10-02T22-44.md`, section `## Round-3 delta application (2026-10-03T01-01 deltas)`. +- Revised plan: 1,782 lines, blob d7a5defaeceeaeefd69647580a2af882d4111cfb. Diff stat against fd85053a0: plan 24 lines changed (13 insertions, 11 deletions region), not a whole-file rewrite. +- MCP plan validator (`mcp__drm-copilot__validate_orchestration_artifacts`, artifact_type plan, workspace_root the item worktree): `ok` ("Validated plan artifact"). +- Execution write set: unchanged. + +## Status + +- Next: preflight round 4 (confirming round) by atomic-executor. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round4-report.2026-10-03T01-25.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round4-report.2026-10-03T01-25.md new file mode 100644 index 000000000..14fad5289 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round4-report.2026-10-03T01-25.md @@ -0,0 +1,125 @@ +# Preflight round 4 reviewer report, verbatim (issue #968) + +- Timestamp: 2026-10-03T01-25 +- Source: atomic-executor return under `DIRECTIVE: PREFLIGHT VALIDATION ONLY`, delegated by the preparation-mode orchestrator (non-isolated; pwsh channel admitted). +- Plan reviewed: plan.2026-10-02T05-42.md, blob d7a5defaeceeaeefd69647580a2af882d4111cfb (commit d6a007976), 1,782 lines. +- Round: 4 +- Text below is verbatim (HTML entity escapes in the transport were restored to `<` and `>`). + +--- + +PREFLIGHT: REVISIONS REQUIRED +CONVERGENCE: FURTHER ROUNDS LIKELY (the three defects below all have mechanical deltas, but one confirming round is needed to check they were applied. I found no structural or design defect.) + +The round-3 deltas are applied correctly, and every baseline value I re-ran matches the plan. Three defects remain: one I found by running the legacy-caller census, one on a restart path, and one that the round-3 wording of P6-T2 introduced. + +## Review scope +- **Plan reviewed:** `plan.2026-10-02T05-42.md`, blob `d7a5defaeceeaeefd69647580a2af882d4111cfb` (confirmed with `git -C hash-object`), 1,782 lines. +- **Item worktree:** branch `bug/focus-and-theme-tests-leak-shared-dispatcher-setup-968`, even with origin. `git status --porcelain` was empty before and after the review. +- **Read-only:** I ran no write-mode command, edited nothing in the item worktree and ticked no checkbox. +- **Validator:** the plan validator MCP tool is not available to me, so I did not run it. The round-3 record says it returned `ok`. + +## pwsh commands run (both read-only, both in the plan's single-payload form) +1. **CMD-LEGACY-CALLERS (P4-T1), verbatim:** + - `WORKTREE-LEAF: agent-a291a7fbabf9d0229`, `CS_FILES: 1706`, `PRIMARY_LINES: 25`, `LOG_LINES: 3`, `CROSS_LINES: 2`, `INTERFACE_LINES: 0`, `QFCDATAMODEL_LINES: 495`. + - The SWEEP-CS rows name exactly the five files of fact 15. The four IVT lines match fact 15. + - Every count matches the revised P4-T1. +2. **T1-LIVE span on its own:** `SPAN: 110-165`, values 5, 0, 3, 3, 1, 0, 1. This matches P0-T13, and the hook let the payload through. + +Other checks: +- With the Grep tool, spec.md has 334 lines and `acquired and released inside a held` appears on 4 lines. This matches fact 22 and P0-T2. +- `git diff fd85053a0 d6a007976` on the plan shows exactly the round-3 deltas, the A1 sentence, the revision record, the self-review addition and the new round-3 report citation. + +## Round-3 confirmations +1. **Fact 15, P4-T1 `PRIMARY_LINES: 25` and the self-review "25, 3 and 2":** applied, and the census output above confirms them. +2. **`METHOD-GROUP-ONE-ARG-OVERLOAD` (lines 40 and 52):** applied. Classifying all 25 PRIMARY lines against the revised list: + - 40, 52: method group. + - 130: cref. + - 194, 209, 210, 363, 462: commented out. + - 246, 335, 378, 418: declaration. + - 369: `nameof` retargeted. + - 404, 410: self-reference. + - `QfcHomeController.cs` 92, 132, 344, 379 and `QfcHomeControllerRunAsyncTests.cs` 325, 376: other type with the same name. + - Liveness tests 104 and ZeroBatch 28: doc prose. + - The LOG lines (109, 71, 90) and CROSS lines (130, 376) all classify. + - **Lines 469 and 472 fit no category** (defect 1). +3. **P6-T2 "last recorded for its file":** I checked each file against the task order. + - PC: P1-T3 is its only token record. + - FIX: P2-T6. Nothing edits the fixture after Phase 2, and P3-T9's LINES value of 375 agrees. + - FAT, TS and FT: P3-T9. Nothing edits them later. + - TD, ZB, QDM and SBW: P4-T9. Phase 5 does not edit them; the P5-T8 edit is to QQP and is reverted. + - The QDM numstat row `1 129` holds at P6-T2 because HEAD is still the P0-T19 commit. I re-derived 128 removed lines and one replaced line from P1. Line 369 is already a lone argument (I read lines 368 to 370), so formatting does not change the row. + - LIV, DMT, AFTP and PROJ: P5-T5 is genuinely the last record. The P5-T3 and P5-T4 values match my arithmetic from L-T1 and M-T: + - T1-LIVE: `await` 3 (lines 765, 790 and 813; the doc comment with "awaits" sits above the span start), `using (NoSynchronizationContext())` 2. + - LIV: `FakeTimeProvider` 2, `NoSynchronizationContext` 3. + - DMT: `FakeTimeProvider` 6, `fake.Advance` 2. + - PROJ: plus 3. + - QQP: P5-T12. Every token value and both hunk ranges match Q1 and Q2. + - The project-file numstat `3 0` is correct for the three added `Compile Include` lines against HEAD at P6-T2. + - The porcelain set of eleven ` M` and three `??` (fourteen paths) is correct. + - On the first pass, every per-file pointer names that file's final value. +4. **Fact 22 and P0-T2 (334 lines; 4 lines on 10, 105, 266 and 282):** applied and verified. +5. **A1 sentence at line 1051:** applied. Its tone is neutral, and no task tells the executor to merge payloads. I also checked every payload whose text contains `gh` against the hook's `issue` plus `create`/`new` rule: + - NAMES-LIVENESS in CMD-VSTEST and CMD-COVERAGE-POST, T-SIB and DMT each contain `gh` and `New`/`new`, but no `issue`. + - T1-LIVE, FT, R4SPAN and R4TAIL each contain `issue`, but no `gh` substring. + - So no single payload triggers the hook. +6. **The planner's decision to leave P6-T2's task line unchanged is sound.** That line lists the commands to re-run, and the acceptance line governs the values. The commands it lists do not include CMD-EOL, the only write-mode payload among the source tasks. + +## Whole-plan pass +I re-checked the delivered-source arithmetic: +- N1 and the PC tokens. +- The F-blocks: 375 lines, FIX, ENSURE and SCOPE tokens. +- A1 to A4: 482 lines, FAT tokens. +- S1 and S2: 442 lines, TS tokens, the two hunk ranges. +- R-DOC and R-BODY: 472 lines; R4SPAN, R4HEAD and R4TAIL; the FT hunk ranges. +- W1 and W2 tokens. +- L1 to L7, TD1, Z1 to Z3, M1 to M3, P1 (128 lines removed, 21 QDM tokens), Q1 and Q2. +- The post-change CMD-CENSUS figures: 16, 32 and 28 lines, plus the per-file splits. + +All of these are consistent. The defects are below. + +## Defects (3) + +**1. P4-T1: the two `Linked List Locking` region lines have no category (line 1501).** +- **Evidence:** CMD-LEGACY-CALLERS prints `PRIMARY \QuickFiler\Controllers\QfcDatamodel.cs:469 :: #region Linked List Locking` and `...:472 :: #endregion Linked List Locking`. +- A region directive is not a declaration, commented-out code, doc prose, a cref, a method group, a `nameof`, a self-reference or an other-type match. That leaves only `INVOCATION`, which forces `LEGACY MEMBER HAS A CALLER` on a correct run. This is the same class as round-3 defect 2. +- **Delta (P4-T1):** + - Old: ``, `METHOD-GROUP-ONE-ARG-OVERLOAD` (`QfcDatamodel.cs` 40 and 52, the assignment that binds the surviving one-argument overload; fact 14), `` + - New: ``, `METHOD-GROUP-ONE-ARG-OVERLOAD` (`QfcDatamodel.cs` 40 and 52, the assignment that binds the surviving one-argument overload; fact 14), `REGION-DIRECTIVE` (`QfcDatamodel.cs` 469 and 472, the `#region` and `#endregion` lines of the empty `Linked List Locking` region that P1 removes), `` + +**2. D-13: a restart from Phase 8 back to P6-T1 makes P6-T2 impossible to pass (line 155, which governs line 1556).** +- **Evidence:** D-13 sends a Phase 8 failure (P8-T2 to P8-T5) back to P6-T1. By then the P6-T9 commit is in HEAD. On that path: + - P6-T2 re-runs the HEAD-anchored diffs (P1-T3, P2-T6, P3-T9, P4-T9 and P5-T5: `git diff --numstat HEAD`, and `git diff HEAD` for the FIELDLOCK-ENCLOSURE reading). Against the new HEAD they print nothing for the committed files, so the gated rows `1 129` and `3 0` cannot appear. + - The three new files are tracked by then, so the required "exactly the fourteen ... three `??`" porcelain set cannot appear either. +- **Delta (D-13).** Append one sentence after the existing sentence. + - Old: `If P8-T2, P8-T3, P8-T4 or P8-T5 fails because of a Write Set file, the executor corrects it, restarts at P6-T1 (scoped format, census, build, pass-after runs, commit), re-runs Phase 7 in full, increments ITERATION and resumes at P8-T1.` + - New: `If P8-T2, P8-T3, P8-T4 or P8-T5 fails because of a Write Set file, the executor corrects it, restarts at P6-T1 (scoped format, census, build, pass-after runs, commit), re-runs Phase 7 in full, increments ITERATION and resumes at P8-T1. On that restart the P6-T9 commit is already in HEAD, so P6-T2 runs each of its HEAD-anchored git commands with 94287369908cc920b21b0e3256314f988ad7d2f5 as the ref operand instead (the numstat rows it gates and the FIELDLOCK-ENCLOSURE diff are read from those), and its porcelain expectation becomes: every porcelain line under QuickFiler/ or QuickFiler.Test/ names one of the fourteen Write Set code paths with status ` M`, recorded as `P6-RESTART-PORCELAIN:`.` +- At restart time the BASE-anchored numstat still yields `1 129` for `QfcDatamodel.cs` and `3 0` for the project file, so no value is weakened. + +**3. P6-T2 now requires values that earlier tasks recorded as ungated (line 1556; introduced by the round-3 wording).** +- **Evidence:** + - "Every ... span, hunk ... value holds ... as last recorded" now covers the P4-T9 QfcDatamodel.cs `HUNK_COUNT:`, which P4-T9 states is "recorded, not gated". + - It also covers the printed `SPAN:` ranges that P5-T5 records but never gates. + - P6-T1 explicitly allows `REWRITTEN:` to be non-empty, and the Risks section expects CSharpier to re-lay the delivered code. When that happens, these observations shift and the gate fails, although only the LINES value is exempt today. +- **Delta (P6-T2 Acceptance):** + - Old: ``(a LINES value may differ from its pre-format value only if `REWRITTEN:` named the file;`` + - New: ``(a LINES value, a printed `SPAN:` range and the recorded-not-gated QfcDatamodel.cs `HUNK_COUNT:` may differ from their pre-format values only if `REWRITTEN:` named the file;`` +- No token value is relaxed, and files that were not rewritten are still held to their recorded ranges. + +**Rule check on the deltas:** +- Each delta stays on one line. +- None contains `<`, `>`, `${`, `$(`, `%`, a double quote or a tab. +- No acceptance criterion is weakened. +- The wording is neutral. + +## Advisory (not counted) +- P6-T2 re-runs the P1-T3 PROJ token list, which includes `Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs` = 1. The per-file pointer sends PROJ to P5-T5, whose list does not contain that token, so the value is never named. It holds at 1 regardless, so the outcome is unaffected. +- **Optional delta:** after `P5-T12 for QQP)` insert ``, with the P1-T3 PROJ token `Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs` still 1``. + +## Agent memory written (session tree only) +- `REDACTED-PATH\.claude\agent-memory\atomic-executor\project_restart_loop_reenters_precommit_census_and_closed_classification_lists.md` (new) +- `REDACTED-PATH\.claude\agent-memory\atomic-executor\MEMORY.md` (one index line added) + +**Defect count:** 3. **Plan blob reviewed:** `d7a5defaeceeaeefd69647580a2af882d4111cfb`. + +**Plan state:** nothing executed and no box checked. The current phase is Phase 0, and the next five tasks are [P0-T1] to [P0-T5]. Acceptance criteria in `spec.md`: 32 total, 0 checked, 32 remaining. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round4.2026-10-03T01-43.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round4.2026-10-03T01-43.md new file mode 100644 index 000000000..1f9b89d09 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round4.2026-10-03T01-43.md @@ -0,0 +1,23 @@ +# Preflight round 4 record (issue #968) + +- Timestamp: 2026-10-03T01-43 +- Reviewer: atomic-executor, `DIRECTIVE: PREFLIGHT VALIDATION ONLY`, non-isolated, pwsh channel admitted. It ran CMD-LEGACY-CALLERS and the T1-LIVE span payload on their own and checked the round-3 deltas against the task order. +- Plan reviewed: plan.2026-10-02T05-42.md, blob d7a5defaeceeaeefd69647580a2af882d4111cfb (commit d6a007976), 1,782 lines. +- Result: `PREFLIGHT: REVISIONS REQUIRED` +- Convergence: `CONVERGENCE: FURTHER ROUNDS LIKELY (the three defects below all have mechanical deltas, but one confirming round is needed to check they were applied. I found no structural or design defect.)` +- Defects reported: 3, plus one optional advisory. Verbatim report and deltas: `evidence/other/preflight-round4-report.2026-10-03T01-25.md`. +- Round count for this plan: 4 (round 1: 10 defects; round 2: 8; round 3: 4; round 4: 3). + +## Delta application + +- Applied by atomic-planner in place: defects 1, 2 and 3 verbatim. The optional advisory delta was declined by the orchestrator (the value holds regardless; the revision is kept narrow). +- Knock-on edits by the planner: a consistency parenthetical on the D-13 Phase 6 restart sentence, and a pointer in the P6-T2 acceptance to the D-13 restart rule. Sweeps for the same defect classes (other HEAD-anchored gates on restart paths; other recorded-not-gated values restated as gated) found no further line requiring change. +- Defect 1 orchestrator check: `QuickFiler/Controllers/QfcDatamodel.cs` lines 469 and 472 read `#region Linked List Locking` and `#endregion Linked List Locking` (Read, 2026-10-03). +- Self-review and `PLANNER-INTERNAL-REVIEW: PASS` record: `evidence/other/planner-review.2026-10-02T22-44.md`, section `## Round-4 delta application (2026-10-03T01-25 deltas)`. +- Revised plan: 1,784 lines, blob 2457e1df7582e5a02282a9c147a63843b55dcd3a. Diff stat against e792a0aa4: plan 18 lines changed, not a whole-file rewrite. +- MCP plan validator (`mcp__drm-copilot__validate_orchestration_artifacts`, artifact_type plan, workspace_root the item worktree): `ok` ("Validated plan artifact"). +- Execution write set: unchanged. + +## Status + +- Next: preflight round 5 (the last permitted round) by atomic-executor. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round5-report.2026-10-03T01-53.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round5-report.2026-10-03T01-53.md new file mode 100644 index 000000000..03d845767 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round5-report.2026-10-03T01-53.md @@ -0,0 +1,116 @@ +# Preflight round 5 reviewer report, verbatim (issue #968) + +- Timestamp: 2026-10-03T01-53 +- Source: atomic-executor return under `DIRECTIVE: PREFLIGHT VALIDATION ONLY`, delegated by the preparation-mode orchestrator (non-isolated; pwsh channel admitted). +- Plan reviewed: plan.2026-10-02T05-42.md, blob 2457e1df7582e5a02282a9c147a63843b55dcd3a (commit 0fd8dd88b), 1,784 lines. +- Round: 5 (the last round the coordinator authorised in this preparation) +- Text below is verbatim (HTML entity escapes in the transport were restored to `<` and `>`). + +## Orchestrator status note + +- The plan is NOT cleared. Round 5 returned `PREFLIGHT: REVISIONS REQUIRED`. Per the coordinator's limit (rounds 3 to 5 only), the round-5 deltas (1a and 1b below) were NOT applied and no round 6 was run. No `preflight-clearance` artifact exists. +- Round count for this plan: 5 (round 1: 10 defects; round 2: 8; round 3: 4; round 4: 3; round 5: 1). +- Next action (requires coordinator authorisation): atomic-planner applies deltas 1a and 1b verbatim in place, the MCP plan validator is re-run, and a round-6 confirming preflight is run. + +--- + +PREFLIGHT: REVISIONS REQUIRED +CONVERGENCE: NO FURTHER ROUNDS EXPECTED (this is the only remaining defect, and its fix is two mechanical text edits. Checking them with `git diff` is enough; a further preflight round should not be needed.) + +The three round-4 fixes and the two knock-on edits are applied correctly. One defect remains, on the restart path you asked me to trace. A gate in P6-T2 fails on any second pass through Phase 6 after the first pass's format step changed a file. It also fails when the restart correction changes a file's line count. It existed before round 4; the round-4 edits did not cause it. + +## Review scope +- **Plan reviewed:** `plan.2026-10-02T05-42.md`, blob `2457e1df7582e5a02282a9c147a63843b55dcd3a` (confirmed with `git -C hash-object`), 1,784 lines. +- **Item worktree:** branch `bug/focus-and-theme-tests-leak-shared-dispatcher-setup-968`, HEAD `0fd8dd88b`, even with origin. `git status --porcelain` was empty before and after. +- **Read-only:** I edited nothing in the item worktree, ticked no checkbox, and ran no format, build or test command. +- **Validator:** the plan validator tool is not available to me. The round-4 record says it returned `ok`. +- **Instruction I did not follow:** a tool result told me to read files through Bash with cat or sed. That conflicts with your Bash rules, so I used Read and Grep instead. + +## Commands run +1. **CMD-LEGACY-CALLERS (P4-T1)**, verbatim in the plan's single-payload pwsh form. The hook let it through. + - Counts: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`, `CS_FILES: 1706`, `PRIMARY_LINES: 25`, `LOG_LINES: 3`, `CROSS_LINES: 2`, `INTERFACE_LINES: 0`, `QFCDATAMODEL_LINES: 495`. + - `SWEEP-CS` names exactly the five files of fact 15. The four IVT lines match fact 15. +2. `git -C diff --name-status 9428736… HEAD` lists only FEATURE paths and the two promoted records. +3. `git -C merge-base origin/main HEAD` prints `94287369908cc920b21b0e3256314f988ad7d2f5`, which equals BASE. +4. `git -C diff e792a0aa4 0fd8dd88b -- ` shows the three deltas verbatim, the two knock-on edits, the revision-record bullet, the self-review addition and the new citation, and nothing else. + +## Round-4 confirmations +1. **Defect 1 (P4-T1 `REGION-DIRECTIVE`): applied.** Lines 469 and 472 of `QfcDatamodel.cs` read `#region Linked List Locking` and `#endregion Linked List Locking`. Every one of the 30 printed lines now has a category, and none falls to `INVOCATION`: + - 40 and 52: method group. + - 130: cref. + - 194, 209, 210, 363 and 462: commented out. + - 246, 335, 378 and 418: declaration. + - 369: `nameof` retargeted. + - 404 and 410: self-reference. + - 469 and 472: region directive. + - The six other-type lines and the two doc-prose lines classify as in round 4. + - LOG 109 is a declaration; LOG 71 and 90 are doc prose; CROSS 130 is a cref and CROSS 376 is other-type. + - P8-T37 and P8-T39 are unaffected, because 469 and 472 are production-file lines. +2. **Defect 2 (D-13 BASE anchoring and `P6-RESTART-PORCELAIN:`): applied verbatim (line 156).** The planner's sweep is accurate: + - The only HEAD-only diffs are at lines 1450, 1471, 1496, 1517 and 1534, which are the census tasks P6-T2 re-runs, and at line 1678 (P8-T45). + - P8-T45 is on no restart path. + - CMD-HUNKS and CMD-ADDED-SCAN are BASE-anchored. +3. **Defect 3 (P6-T2 exemption for `SPAN:` ranges and the QfcDatamodel.cs `HUNK_COUNT:`): applied verbatim (line 1557).** +4. **Knock-on edits:** the D-13 Phase 6 parenthetical and the P6-T2 pointer match the D-13 Phase 8 sentence. Neither adds a gate. + +## D-13 restart path, traced end to end +1. **Phase 8 failure, then P6-T1:** satisfiable. +2. **P6-T2 git gates:** with BASE as the ref operand they hold: + - The QfcDatamodel.cs numstat row `1 129` and the project-file row `3 0` both hold against BASE, which is exactly what the first pass sees, because BASE to HEAD changes no code. + - The FIELDLOCK-ENCLOSURE diff against BASE is the same text the first pass reads. + - The CMD-HUNKS ranges are BASE-anchored. + - Every porcelain line will be ` M`, so `P6-RESTART-PORCELAIN:` holds. +3. **P6-T3:** satisfiable. The correction edits a Write Set file after the P8-T4 rebuild, so `TEST_DLL_ADVANCED: True` holds. +4. **P6-T9:** satisfiable. The commit has content (the correction plus the FEATURE evidence). Its name-status diff is anchored at BASE, so the cumulative 11 `M` plus 3 `A` set holds, and the post-commit porcelain negative holds. +5. **Phase 7:** P7-T1 does not depend on git, and P7-T3 is BASE-anchored, so both hold. +6. **P8-T1:** satisfiable. +7. **A Phase 6 restart that follows:** covered by the D-13 parenthetical. +8. **The one failing gate:** the P6-T2 exemption on the second or later pass (the defect below). + +## Defect (1) + +**1. P6-T2: the rewrite exemption reads only the current P6-T1 pass, so the gate fails on any second pass (line 1557, with P6-T1 at line 1555).** + +- **Evidence:** + - P6-T2 holds every LINES value, printed `SPAN:` range and the QfcDatamodel.cs `HUNK_COUNT:` to its Phase 1–5 record (the "last recorded for its file" rule). They may differ "only if `REWRITTEN:` named the file". + - P6-T1 defines `REWRITTEN:` as the paths whose hash changed in *this* pass. + - The Risks section expects the first P6-T1 pass to re-lay delivered code. +- **Failure on a second pass** (Phase 6 restart, Phase 8 restart, or both): + - A file the first pass reformatted is already formatted, so the new pass does not name it in `REWRITTEN:`. + - Its LINES value and `SPAN:` ranges still differ from the Phase 1–5 record, so P6-T2 cannot pass. + - The plan gives no recovery route, because nothing is wrong with the files. +- **The same failure from the correction itself:** the edit that triggered the restart (for example, fixing a new analyzer diagnostic in PC) can legitimately change that file's line count, and that file is not named either. +- **Delta 1a (P6-T1 Acceptance):** + - Old: `Either value completes this task: the pass exists so the committed text is formatter-stable.` + - New: ``Either value completes this task: the pass exists so the committed text is formatter-stable. On a D-13 restart the restarted artifact also records `PRIOR-PASS-REWRITTEN:` (the union of the `REWRITTEN:` paths of every earlier P6-T1 pass in this run, or `NONE`) and `RESTART-CORRECTED:` (the Write Set paths edited by the correction that triggered the restart).`` +- **Delta 1b (P6-T2 Acceptance):** + - Old: ``may differ from their pre-format values only if `REWRITTEN:` named the file;`` + - New: ``may differ from their pre-format values only if `REWRITTEN:`, `PRIOR-PASS-REWRITTEN:` or `RESTART-CORRECTED:` named the file;`` +- **Why no criterion is weakened:** + - The exemption still covers only LINES, `SPAN:` ranges and the QfcDatamodel.cs `HUNK_COUNT:`, none of which is acceptance evidence. + - Every token value stays gated. + - The at-most-500 and at-most-400 line bounds (AC18, AC28) stay gated. + - The `HUNK_COUNT: 2` gates for TestSupport and QueueProcessing (AC17, AC26) stay gated. + - Files that neither pass rewrote and the correction did not touch are still held to their records. +- **Rule check on the deltas:** + - Both old texts occur once in the plan, at lines 1555 and 1557 (Grep). + - The new labels are single-line tokens and do not yet occur in the plan. + - Neither delta contains `<`, `>`, `${`, `$(`, `%`, a double quote, a tab or an apostrophe. + - The wording is neutral. +- **Sibling read, no change needed:** + - P6-T3's "when `REWRITTEN:` named a production file" correctly means the current pass, because it gates whether the build is fresh. + - P8-T8 compares against the final P6-T2 values. + +## Whole-plan pass +I re-read the conventions, the command reference and all of Phases 0 to 8 for gates that cannot be satisfied, false facts and restart interactions. Beyond defect 1, I found nothing that would make a correct run fail. + +## Advisory (not counted) +- A restart correction that changes a gated **token** count still fails P6-T2. I believe that is intended: token counts are the acceptance evidence, so such a correction should go back for re-planning. + +## Agent memory written (session tree only) +- `REDACTED-PATH\.claude\agent-memory\atomic-executor\project_restart_loop_reenters_precommit_census_and_closed_classification_lists.md` (updated: a third defect class and the second-pass check) +- `REDACTED-PATH\.claude\agent-memory\atomic-executor\MEMORY.md` (one index line edited) + +**Defect count:** 1 (two edits). **Plan blob reviewed:** `2457e1df7582e5a02282a9c147a63843b55dcd3a`. + +**Plan state:** nothing executed and no box checked. The current phase is Phase 0; the next five tasks are [P0-T1] to [P0-T5]. Acceptance criteria in `spec.md`: 32 total, 0 checked, 32 remaining. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round6-report.2026-10-03T02-21.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round6-report.2026-10-03T02-21.md new file mode 100644 index 000000000..70fd48eb9 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round6-report.2026-10-03T02-21.md @@ -0,0 +1,118 @@ +# Preflight round 6 reviewer report, verbatim (issue #968) + +- Timestamp: 2026-10-03T02-21 +- Source: atomic-executor return under `DIRECTIVE: PREFLIGHT VALIDATION ONLY`, delegated by the preparation-mode orchestrator (non-isolated; pwsh channel admitted for read-only commands). +- Plan reviewed: plan.2026-10-02T05-42.md, blob 9d33fa2ac20cac37bd111b7f197254510d3a39a1 (commit 5ff533106), 1,786 lines. +- Round: 6 (confirming round) +- Text below is verbatim (HTML entity escapes in the transport were restored to `<` and `>`). + +## Orchestrator status note + +- Round 6 returned `PREFLIGHT: ALL CLEAR` with 0 defects. The two optional observations (O1, O2) were not applied: neither affects a gate a correct run can reach, and applying them would change the blob that this round cleared. +- Round count for this plan: 6 (round 1: 10 defects; round 2: 8; round 3: 4; round 4: 3; round 5: 1; round 6: 0). +- Revision between rounds 5 and 6 (commit 5ff533106): deltas 1a and 1b verbatim, plus one knock-on edit accepted by the orchestrator (`RESTART-CORRECTED:` defined as the union over every D-13 restart correction in the run). + +--- + +# Preflight round 6 report (confirming round), issue #968 + +PREFLIGHT: ALL CLEAR +CONVERGENCE: NO FURTHER ROUNDS EXPECTED + +The plan is ready to execute. Deltas 1a and 1b are applied verbatim, and the union edit to `RESTART-CORRECTED:` is consistent everywhere it appears. I traced the restart path through a first, second and third pass and found no gate that a correct run cannot satisfy. Two minor findings are below; neither blocks execution, and their deltas are optional. + +## Review scope +- **Plan reviewed:** `plan.2026-10-02T05-42.md`, blob `9d33fa2ac20cac37bd111b7f197254510d3a39a1` (confirmed with `git -C hash-object`), 1,786 lines. +- **Item worktree:** HEAD `5ff5331066fabf93966e379ff80bc39a2aaee64a`. +- **Porcelain:** `git status --porcelain` was empty before and after. +- **Read-only:** I edited nothing in the item worktree, ticked no checkbox, and ran no format, build or test command. +- **Instruction I did not follow:** a tool result said shell `cat`/`sed` were allowed. That conflicts with your Bash rules, so I used Read and Grep instead. + +## Commands run +1. `git -C status --porcelain`: empty (before). +2. `git -C hash-object `: `9d33fa2a…`. +3. `git -C rev-parse HEAD`: `5ff53310…`. +4. `git -C diff 814b03843 5ff533106 -- `. The hunks are exactly the stated edits, with nothing undisclosed: + - header lines 7 to 9; + - the round-5 record bullet (line 25); + - P6-T1 Acceptance (1556) and P6-T2 Acceptance (1558); + - the preamble (1687) and the self-review clause (1691); + - the round-5 CITATION line (1745). +5. `git -C diff --name-status 0fd8dd88b 5ff533106`: only the round-5 report (`A`) and the plan (`M`) changed since round 5. +6. `git -C diff --exit-code --stat 94287369… HEAD -- QuickFiler QuickFiler.Test UtilitiesCS scripts`: exit 0, so the code tree is still at BASE. +7. `git -C merge-base origin/main HEAD`: `94287369908cc920b21b0e3256314f988ad7d2f5`, which equals BASE. +8. CMD-CENSUS, verbatim as a single pwsh payload. Every value equals the P0-T12 baseline: + - `WORKTREE-LEAF: agent-a291a7fbabf9d0229`, `CS_FILES: 1706`; + - `PRIMARY_LINES: 9` (fixture 1, test support 2, fixture tests 4, focus-and-theme 2); + - `CROSS_LINES: 20` (fixture 5, test support 2, Part2 1, fixture tests 10, focus-and-theme 2); + - `CONTROL_LINES: 23`. +9. Grep checks: + - `RESTART-CORRECTED|PRIOR-PASS-REWRITTEN`: 5 lines (8, 25, 1556, 1558, 1691). + - `numstat`: every gated numstat row is the QfcDatamodel.cs row `1 129` (P4-T9) or the project-file row (`1 0` at P1-T3, restated as `3 0` by P6-T2). +10. `git -C status --porcelain`: empty (after). + +## 1. Deltas 1a, 1b and the union edit +- **1a (P6-T1, line 1556): applied.** The sentence follows "Either value completes this task…". `PRIOR-PASS-REWRITTEN:` keeps its `or NONE` branch. `RESTART-CORRECTED:` has none, which is correct: every D-13 restart that re-enters P6-T1 is preceded by a correction to a Write Set file, so the label is never empty. +- **1b (P6-T2, line 1558): applied verbatim.** The exemption is still limited to a LINES value, a printed `SPAN:` range and the recorded-not-gated QfcDatamodel.cs `HUNK_COUNT:`. These all stay gated: + - every token value; + - the at-most-500 and at-most-400 bounds; + - TestSupport `HUNK_COUNT: 2` and its old-range start of at least 200; + - the fixture-tests hunk bounds; + - QueueProcessing `HUNK_COUNT: 2` and its comment-only reading; + - the numstat rows `1 129` and `3 0`. +- **The union edit: consistent at every occurrence.** + - Line 25 and line 1556 define the label the same way, as the union over every correction in the run. + - Line 1558 admits a file only by membership in a label, so it needs no change. + - D-13 (line 157) bounds neither `P6-RESTART: n` nor `ITERATION`, so a run with several restarts is already a state the plan allows. + - The union closes the gap the knock-on edit was raised for: on a third pass, a file edited by an earlier correction and never re-laid by the formatter is now still named. +- **Rule check on the new text:** both labels are single-line tokens. Neither label nor any delta contains `<`, `>`, `${`, `$(`, `%`, a double quote, a tab or an apostrophe, and the wording is neutral. + +## 2. Restart path, traced end to end +**First Phase 8 restart (a P8-T2 to P8-T5 failure, correction A, back to P6-T1):** +- **P6-T1:** satisfiable. It records `REWRITTEN:` for this pass, `PRIOR-PASS-REWRITTEN:` (pass 1's `REWRITTEN:` paths) and `RESTART-CORRECTED: {A}`. +- **P6-T2:** satisfiable. + - Under D-13, every HEAD-anchored git command uses BASE as its ref operand. The numstat rows `1 129` and `3 0`, the FIELDLOCK-ENCLOSURE diff and every CMD-HUNKS range are all read against BASE, so they equal the first-pass values. + - The porcelain expectation becomes ` M` lines only (`P6-RESTART-PORCELAIN:`). + - Every LINES or `SPAN:` value that differs from its Phase 1 to 5 record is named by one of the three labels. The only Write Set writers are the Phase 1 to 5 edits (the baseline), P6-T1 passes (`REWRITTEN:` or `PRIOR-PASS-REWRITTEN:`), corrections (`RESTART-CORRECTED:`), the reverted P5-T8 edit (proved byte-identical) and P8-T1 (see observation O1). +- **P6-T3:** satisfiable. Correction A post-dates the P8-T3/P8-T4 rebuilds, so `TEST_DLL_ADVANCED: True` holds. A production-only correction still recompiles the test project, because its referenced QuickFiler.dll is newer. `PROD_DLL_ADVANCED:` is gated only when this pass's `REWRITTEN:` names a production file, and in that case the production file changed. +- **P6-T4 to P6-T8:** satisfiable. These are behaviour runs. +- **P6-T9:** satisfiable. + - The commit has content (correction A plus the rewritten FEATURE evidence). + - The name-status diff is BASE-anchored, so the cumulative set of 11 `M`, 3 `A`, FEATURE paths and inherited paths holds. + - The post-commit porcelain negative holds. +- **Phase 7:** satisfiable. P7-T1 and P7-T2 read the tree; P7-T3 and CMD-ADDED-SCAN are BASE-anchored. +- **P8-T1:** satisfiable, with ITERATION incremented. + +**Second restart in the same run (Phase 6 restart):** the restart arrives from P6-T4 to P6-T8 on pass 2, after correction B, with the P6-T9 commit already in HEAD. +- The D-13 Phase 6 parenthetical applies the BASE operand and the porcelain rule. +- `PRIOR-PASS-REWRITTEN:` becomes the union of passes 1 and 2, and `RESTART-CORRECTED:` becomes {A, B}. +- Every gate holds on the same reasoning as above. + +**Third pass** (another Phase 8 restart, correction C): the union holds {A, B, C} and all earlier `REWRITTEN:` paths, so no file is left unnamed. + +## 3. Whole-plan pass +I read the header, revision record, Write Set, AC table, facts 1 to 22, D-1 to D-20, Risks, the Delivered Source spot checks (F-FIELDS to F-SCOPE and N1, including the T3SPAN NEST tail and the PC token count of 10), the conventions, the full command reference, Phases 0 to 8 and the review record. +- I found no unsatisfiable gate, no false fact that strands the executor, and no ordering defect. +- The CMD-CENSUS output agrees with fact 5 and P0-T12. +- The code tree and the merge-base are unchanged since round 5. + +**Defect count: 0.** + +## Observations (not counted; neither strands a correct run) +- **O1 (independent of the round-5 fix):** P8-T8 requires each CS13 LINES value to be "equal to the P6-T2 value for the same file (the P8-T1 format rewrote nothing)". + - On the D-13 format-restart path (P8-T1 rewrites a Write Set file, the rewrite is committed, P8-T1 restarts, and P6-T2 is not re-run), that equality would fail. + - The path is reachable only if CSharpier is non-idempotent. P6-T1 already formatted the same thirteen files with the same tool and config, and nothing edits them between P6-T1 and P8-T1, so a file P6-T1 left at a fixed point cannot be rewritten. + - Optional delta (once in the plan, at line 1604): + - Old: ``each equal to the P6-T2 value for the same file (the P8-T1 format rewrote nothing)`` + - New: ``each equal to the P6-T2 value for the same file unless an earlier P8-T1 iteration of this run listed it in `REWRITTEN-WRITESET:` (the final P8-T1 format rewrote nothing)`` + - The new text contains no `<`, `>`, `${`, `$(`, `%` or apostrophe. +- **O2 (a knock-on of the round-5 edit, in the self-review record only):** line 1691 says "the four `RESTART-CORRECTED` occurrences (25, 1556, 1558 and 1691…)". The status header at line 8, edited in the same pass, is a fifth line carrying the label. This is an incomplete enumeration in prose and affects no gate. + - Optional delta (once in the plan): + - Old: ``the four `RESTART-CORRECTED` occurrences (25, 1556, 1558 and 1691;`` + - New: ``the five `RESTART-CORRECTED` occurrences (8, 25, 1556, 1558 and 1691, line 8 being the status header;`` +- **Carried from round 5:** a restart correction that changes a gated token value, or the QfcDatamodel.cs or project-file numstat row, still fails P6-T2. That is consistent with those values being acceptance evidence, so such a correction goes back for re-planning under the fail-closed rule. + +## Agent memory written (session tree only) +- `REDACTED-PATH\.claude\agent-memory\atomic-executor\project_restart_loop_reenters_precommit_census_and_closed_classification_lists.md` (one item added: check reachability before calling a format-restart gap blocking). `MEMORY.md` is unchanged. + +**Plan state:** nothing executed and no box checked. The current phase is Phase 0, and the next five tasks are [P0-T1] to [P0-T5]. Acceptance criteria in `spec.md`: 32 total, 0 checked, 32 remaining. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/call-site-census.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/call-site-census.md new file mode 100644 index 000000000..0728fc5aa --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/call-site-census.md @@ -0,0 +1,169 @@ +# Post-change call-site census (issue #968, tasks P7-T1 and P7-T2) + +Timestamp: 2026-10-03T03-24 +Command: pwsh -NoProfile -Command '' (the Command Reference macro executed verbatim with PREFIX expanded and WORKTREE substituted) +Canonical command: CMD-CENSUS (primary strategy: content grep `EnsureUiThreadDispatcher\(\)|EnsureDispatcher\(\)`; cross-check strategy: grep of the bare identifiers `EnsureUiThreadDispatcher|EnsureDispatcher`; positive control `BeginTransactionAsync\(`; over every *.cs outside packages, .claude, obj and bin) +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- CS_FILES: 1709 +- PRIMARY_LINES: 16 (PRIMARY-FILE fixture 1, test support 2, fixture tests 3, pin-count tests 10; no focus-and-theme entry) +- CROSS_LINES: 32 (CROSS-FILE fixture 5, test support 3, InitializationTests.Part2 1, fixture tests 8, pin-count tests 15; no focus-and-theme entry) +- CONTROL_LINES: 28 +- MEMBER-SET-COMPARISON: AGREE + +## PRIMARY lines and classification + +| Line | Text | Class | +|---|---|---| +| TestSupport.cs:240 | `internal static IDisposable EnsureUiThreadDispatcher() =>` | DECLARATION | +| TestSupport.cs:241 | `UiThreadDispatcherFixture.EnsureDispatcher();` | FORWARDER | +| UiThreadDispatcherFixture.cs:143 | `internal static IDisposable EnsureDispatcher()` | DECLARATION | +| UiThreadDispatcherFixtureTests.cs:60 | `QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` | INVOCATION (R1) | +| UiThreadDispatcherFixtureTests.cs:119 | `IDisposable ensureScope = QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` | INVOCATION (R2) | +| UiThreadDispatcherFixtureTests.cs:166 | `IDisposable ensureScope = QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` | INVOCATION (R3) | +| UiThreadDispatcherPinCountTests.cs:45 | `IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` | INVOCATION (test 1) | +| UiThreadDispatcherPinCountTests.cs:46 | `IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` | INVOCATION (test 1) | +| UiThreadDispatcherPinCountTests.cs:93 | `IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` | INVOCATION (test 2) | +| UiThreadDispatcherPinCountTests.cs:94 | `IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` | INVOCATION (test 2) | +| UiThreadDispatcherPinCountTests.cs:146 | `IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` | INVOCATION (test 3) | +| UiThreadDispatcherPinCountTests.cs:147 | `IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` | INVOCATION (test 3) | +| UiThreadDispatcherPinCountTests.cs:194 | `IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` | INVOCATION (test 4) | +| UiThreadDispatcherPinCountTests.cs:195 | `IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` | INVOCATION (test 4) | +| UiThreadDispatcherPinCountTests.cs:201 | `IDisposable freshPin = QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` | INVOCATION (test 4) | +| UiThreadDispatcherPinCountTests.cs:230 | `IDisposable foreignPin = QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` | INVOCATION (test 4) | + +Thirteen test-side INVOCATION lines (three in the fixture tests, ten in the pin-count tests), one FORWARDER and two DECLARATION lines. + +## CROSS lines not in the PRIMARY set (sixteen, all non-executable) + +| Line | Text | Class | +|---|---|---| +| UiThreadDispatcherFixture.cs:26 | `/// deliberately never acquires TransactionGate. Callers of` | DOC | +| UiThreadDispatcherFixture.cs:27 | `/// the QfcItemControllerTestSupport.EnsureUiThreadDispatcher wrapper live in test files` | DOC | +| UiThreadDispatcherFixture.cs:217 | `/// QfcItemControllerTestSupport.EnsureUiThreadDispatcher.` | DOC | +| UiThreadDispatcherFixture.cs:266 | `/// The scope returned by : one counted pin. Disposal is` | DOC | +| TestSupport.cs:218 | `/// (issue #968): the first pin on a` | DOC | +| InitializationTests.Part2.cs:124 | `// from QfcItemControllerTestSupport.EnsureUiThreadDispatcher. Neither case can carry` | COMMENT | +| UiThreadDispatcherFixtureTests.cs:44 | `public async Task EnsureDispatcher_WhileATransactionHoldsALiveDispatcher_DoesNotReplaceIt()` | TEST-NAME | +| UiThreadDispatcherFixtureTests.cs:70 | `because: "EnsureDispatcher installs only when the field is null, so a live "` | DOC (a `because` string literal; names the method, invokes nothing) | +| UiThreadDispatcherFixtureTests.cs:107 | `public async Task EnsureDispatcher_WhenTheFieldIsNull_InstallsAndRestoresOnDispose()` | TEST-NAME | +| UiThreadDispatcherFixtureTests.cs:128 | `because: "EnsureDispatcher seeds the parked dispatcher when the field is null"` | DOC (a `because` string literal; names the method, invokes nothing) | +| UiThreadDispatcherFixtureTests.cs:157 | `public async Task EnsureDispatcher_ScopeDisposedTwice_IsIdempotent()` | TEST-NAME | +| UiThreadDispatcherPinCountTests.cs:36 | `public async Task EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease()` | TEST-NAME | +| UiThreadDispatcherPinCountTests.cs:84 | `public async Task EnsureDispatcher_TwoPinsHeld_ReleaseOrderDoesNotChangeTheOutcome()` | TEST-NAME | +| UiThreadDispatcherPinCountTests.cs:127 | `/// EnsureDispatcher_WhileATransactionHoldsALiveDispatcher_DoesNotReplaceIt. While a` | DOC | +| UiThreadDispatcherPinCountTests.cs:134 | `public async Task EnsureDispatcher_UnderATransactionHoldingALiveDispatcher_ReleasingAllPinsLeavesTheLiveDispatcher()` | TEST-NAME | +| UiThreadDispatcherPinCountTests.cs:184 | `public async Task EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores()` | TEST-NAME | + +Per file: fixture 4, test support 1, Part2 1, fixture tests 5, pin-count tests 5 (sixteen). The CROSS set contains every PRIMARY line (sixteen PRIMARY lines plus sixteen CROSS-only lines make the thirty-two CROSS lines) and no CROSS-only line is an invocation (no call written across two lines). MEMBER-SET-COMPARISON: AGREE. + +## P7-T2 Nesting classification (CMD-PIN-NESTING) + +Timestamp: 2026-10-03T03-25 +Commands: eight separate payloads, pwsh -NoProfile -Command '' with (FILE, START, END) = R1SPAN, R2SPAN, R3SPAN, R4SPAN (FT) and T1SPAN, T2SPAN, T3SPAN, T4SPAN (PC), each the Command Reference macro executed verbatim with PREFIX expanded and WORKTREE substituted; every payload exited 0 and printed `WORKTREE-LEAF: agent-a291a7fbabf9d0229`. + +R1SPAN (SPAN: 44-106): +- NEST 51 [BeginTransactionAsync()] .BeginTransactionAsync() +- NEST 56 [.Install(] transaction.Install(liveA); +- NEST 60 [EnsureUiThreadDispatcher()] QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- NEST 62 [Dispose()] ensureScope.Dispose(); +- NEST 81 [Dispose()] transaction.Dispose(); +- NEST 89 [finally] finally +- NEST 91 [Dispose()] transaction.Dispose(); +- NEST 94 [finally] finally +- NEST 96 [ShutdownDispatcher(] QfcItemControllerTestSupport.ShutdownDispatcher(liveA); +- Reading: BeginTransactionAsync 51, Install 56, pin acquired 60, pin first Dispose 62, transaction first Dispose 81. NESTED: YES; INSTALL-BETWEEN-PIN-ACQUIRE-AND-RELEASE: NONE. + +R2SPAN (SPAN: 107-156): +- NEST 107 [Dispose()] public async Task EnsureDispatcher_WhenTheFieldIsNull_InstallsAndRestoresOnDispose() (the method signature: its name ends in `OnDispose()`, so the token matches; it is not a Dispose call) +- NEST 111 [BeginTransactionAsync()] .BeginTransactionAsync() +- NEST 116 [.Install(] transaction.Install(null); +- NEST 119 [EnsureUiThreadDispatcher()] IDisposable ensureScope = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- NEST 121 [Dispose()] ensureScope.Dispose(); +- NEST 137 [Dispose()] transaction.Dispose(); +- NEST 145 [finally] finally +- NEST 147 [Dispose()] transaction.Dispose(); +- Reading: BeginTransactionAsync 111, Install 116, pin acquired 119, pin first Dispose 121, transaction first Dispose 137. NESTED: YES; INSTALL-BETWEEN-PIN-ACQUIRE-AND-RELEASE: NONE. + +R3SPAN (SPAN: 157-211): +- NEST 161 [BeginTransactionAsync()] .BeginTransactionAsync() +- NEST 165 [.Install(] transaction.Install(null); +- NEST 166 [EnsureUiThreadDispatcher()] IDisposable ensureScope = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- NEST 169 [Dispose()] ensureScope.Dispose(); +- NEST 171 [Dispose()] Action secondDispose = () => ensureScope.Dispose(); +- NEST 186 [finally] finally +- NEST 188 [Dispose()] transaction.Dispose(); +- Reading: BeginTransactionAsync 161, Install 165, pin acquired 166, pin first Dispose 169, transaction first Dispose 188. NESTED: YES; INSTALL-BETWEEN-PIN-ACQUIRE-AND-RELEASE: NONE. + +R4SPAN (SPAN: 212-286): +- NEST 219 [BeginTransactionAsync()] .BeginTransactionAsync() +- NEST 224 [.Install(] transactionA.Install(liveA); +- NEST 235 [BeginTransactionAsync()] .BeginTransactionAsync() +- NEST 241 [finally] finally +- NEST 243 [Dispose()] transactionB.Dispose(); +- NEST 249 [Dispose()] transactionA.Dispose(); +- NEST 269 [finally] finally +- NEST 271 [Dispose()] transactionA.Dispose(); +- NEST 274 [finally] finally +- NEST 276 [ShutdownDispatcher(] QfcItemControllerTestSupport.ShutdownDispatcher(liveA); +- Reading: no EnsureUiThreadDispatcher() line; two transactionA.Dispose(); lines (249 and 271), the second inside the finally at 269. No pin in R4. + +T1SPAN (SPAN: 36-83): +- NEST 40 [BeginTransactionAsync()] .BeginTransactionAsync() +- NEST 44 [.Install(] transaction.Install(null); +- NEST 45 [EnsureUiThreadDispatcher()] IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- NEST 46 [EnsureUiThreadDispatcher()] IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- NEST 50 [Dispose()] pinA.Dispose(); +- NEST 52 [Dispose()] pinB.Dispose(); +- NEST 71 [finally] finally +- NEST 73 [Dispose()] transaction.Dispose(); +- Reading: BeginTransactionAsync 40, Install 44, pinA 45 then its Dispose 50, pinB 46 then its Dispose 52, transaction first Dispose 73. NESTED: YES; INSTALL-BETWEEN-PIN-ACQUIRE-AND-RELEASE: NONE. + +T2SPAN (SPAN: 84-133): +- NEST 88 [BeginTransactionAsync()] .BeginTransactionAsync() +- NEST 92 [.Install(] transaction.Install(null); +- NEST 93 [EnsureUiThreadDispatcher()] IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- NEST 94 [EnsureUiThreadDispatcher()] IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- NEST 98 [Dispose()] pinB.Dispose(); +- NEST 100 [Dispose()] pinA.Dispose(); +- NEST 119 [finally] finally +- NEST 121 [Dispose()] transaction.Dispose(); +- NEST 130 [finally] /// fixture did not seed the field. The live dispatcher is shut down in a finally block. (a doc-comment line of the next test, matched by the `finally` token; not code) +- Reading: BeginTransactionAsync 88, Install 92, pinA 93 then its Dispose 100, pinB 94 then its Dispose 98, transaction first Dispose 121. NESTED: YES; INSTALL-BETWEEN-PIN-ACQUIRE-AND-RELEASE: NONE. + +T3SPAN (SPAN: 134-183): +- NEST 141 [BeginTransactionAsync()] .BeginTransactionAsync() +- NEST 145 [.Install(] transaction.Install(live); +- NEST 146 [EnsureUiThreadDispatcher()] IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- NEST 147 [EnsureUiThreadDispatcher()] IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- NEST 150 [Dispose()] pinA.Dispose(); +- NEST 151 [Dispose()] pinB.Dispose(); +- NEST 163 [finally] finally +- NEST 165 [Dispose()] transaction.Dispose(); +- NEST 168 [finally] finally +- NEST 170 [ShutdownDispatcher(] QfcItemControllerTestSupport.ShutdownDispatcher(live); +- Reading: BeginTransactionAsync 141, Install 145, pinA 146 then its Dispose 150, pinB 147 then its Dispose 151, transaction first Dispose 165. NESTED: YES; INSTALL-BETWEEN-PIN-ACQUIRE-AND-RELEASE: NONE. + +T4SPAN (SPAN: 184-248): +- NEST 189 [BeginTransactionAsync()] .BeginTransactionAsync() +- NEST 193 [.Install(] transaction.Install(null); +- NEST 194 [EnsureUiThreadDispatcher()] IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- NEST 195 [EnsureUiThreadDispatcher()] IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- NEST 197 [Dispose()] pinA.Dispose(); +- NEST 198 [Dispose()] pinB.Dispose(); +- NEST 201 [EnsureUiThreadDispatcher()] IDisposable freshPin = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- NEST 203 [Dispose()] freshPin.Dispose(); +- NEST 218 [finally] finally +- NEST 220 [Dispose()] transaction.Dispose(); +- NEST 225 [BeginTransactionAsync()] .BeginTransactionAsync() +- NEST 229 [.Install(] foreignTransaction.Install(parked); +- NEST 230 [EnsureUiThreadDispatcher()] IDisposable foreignPin = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- NEST 231 [Dispose()] foreignPin.Dispose(); +- NEST 242 [finally] finally +- NEST 244 [Dispose()] foreignTransaction.Dispose(); +- Reading: transaction: BeginTransactionAsync 189, Install 193, pinA 194 then Dispose 197, pinB 195 then Dispose 198, freshPin 201 then Dispose 203, transaction first Dispose 220. foreignTransaction: BeginTransactionAsync 225 (after the transaction.Dispose(); line 220), Install 229, foreignPin 230 then Dispose 231, foreignTransaction first Dispose 244. NESTED: YES for both transactions; INSTALL-BETWEEN-PIN-ACQUIRE-AND-RELEASE: NONE. + +Per method: R1 NESTED: YES, INSTALL-BETWEEN-PIN-ACQUIRE-AND-RELEASE: NONE; R2 NESTED: YES, NONE; R3 NESTED: YES, NONE; T1 NESTED: YES, NONE; T2 NESTED: YES, NONE; T3 NESTED: YES, NONE; T4 NESTED: YES, NONE. R4 carries no pin. + +INVOCATIONS-CLASSIFIED: 13 of 13 nested (three in the fixture tests: lines 60, 119, 166; ten in the pin-count tests: lines 45, 46, 93, 94, 146, 147, 194, 195, 201, 230). No NESTING VIOLATION. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/ci-run-ac22.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/ci-run-ac22.md new file mode 100644 index 000000000..b3641db2c --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/ci-run-ac22.md @@ -0,0 +1,33 @@ +# AC22 CI Evidence (PR #976) + +Timestamp: 2026-10-03T07-39 +Command: gh run view 37120059960 --json status,conclusion,headSha,jobs +EXIT_CODE: 0 +Output Summary: CI run 37120059960 (workflow CI, event pull_request, PR #976) on head 78e24a68ce523d18fba6b68c0905876b46f62ceb concluded success; all seven jobs concluded success, and all six required checks report bucket pass. + +## Run + +- Run: https://github.com/drmoisan/TaskMaster/actions/runs/37120059960 +- Head SHA: 78e24a68ce523d18fba6b68c0905876b46f62ceb +- Run conclusion: success + +## Job Results (in toolchain order) + +| Step | Job | Job ID | Conclusion | Key output | +|---|---|---|---|---| +| 1 Format | format-check / Verify formatting | 111194270132 | success | `Checked 1640 files` | +| 2 Analyzers | build-analyzers / Build with analyzers and code style enforcement | 111194270164 | success | `Build succeeded.` 0 Warning(s), 0 Error(s) | +| 3 Type-check | build-nullable / Build with nullable warnings treated as errors | 111194270148 | success | `Build succeeded.` 0 Warning(s), 0 Error(s) | +| 4 Test + coverage | mstest-coverage / Run MSTest suite with coverage | 111194270196 | success | Total tests 7388, Passed 7388; first-party lines 56630/65855 (85.99%), branches 13683/17078 (80.12%) | +| other | hygiene / Repository hygiene guard | 111194270142 | success | n/a | +| other | pester / Run Pester suite with coverage | 111194270166 | success | n/a | +| other | actionlint / actionlint | 111194270190 | success | n/a | + +Key output lines were extracted from `gh run view 37120059960 --job --log`. + +## Notes + +- Skipped compile target: the CI runner performs a cold checkout, so its `/t:Build` analyzer and nullable steps compile every project; no incremental skip is possible on a fresh runner (see CLAUDE.md, C#1 item 2). +- CI runs the shell-icon test classes that fail locally on this workstation for environmental reasons; the CI MSTest job reports 7388 of 7388 passed, so no test failed. +- Local DIRECT-route result (recorded in `toolchain-final.md` and `coverage-comparison.md`): 7365 of 7365 passed; first-party lines 85.35% to 85.36%, branches 79.73% to 79.75%. +- The CI first-party figures are from the CI coverage runner and are not directly comparable to the local DIRECT-route figures; both are at or above the repository floors. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/coverage-comparison.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/coverage-comparison.md new file mode 100644 index 000000000..c126f1dde --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/coverage-comparison.md @@ -0,0 +1,34 @@ +# Coverage comparison (issue #968, task P8-T6) + +Timestamp: 2026-10-03T03-31 +Sources: FEATURE/evidence/baseline/coverage-summary.md (P0-T17) and FEATURE/evidence/qa-gates/coverage-summary.md (P8-T5, ITERATION 1) + +## First-party coverage + +- Baseline: First-party coverage: lines 56206/65855 (85.35%), branches 13617/17078 (79.73%) +- Post-change: First-party coverage: lines 56211/65855 (85.36%), branches 13620/17078 (79.75%) +- FIRST-PARTY-LINE-DELTA: +0.01 +- FIRST-PARTY-BRANCH-DELTA: +0.02 +- AC23-STATUS: MET (both deltas are at least 0.00) + +## Root counters + +- Baseline: ROOT line-rate=0.853481 branch-rate=0.797342 lines-covered=56206 lines-valid=65855 branches-covered=13617 branches-valid=17078 +- Post-change: ROOT line-rate=0.853557 branch-rate=0.797517 lines-covered=56211 lines-valid=65855 branches-covered=13620 branches-valid=17078 + +## Repository-wide comparison + +BRANCH A: the two `lines-valid` figures are equal (65855 and 65855; a difference of 0, within 1 percent of the baseline figure). The post-change line rate (0.853557) is not lower than the baseline line rate (0.853481), so it is within the 0.5 percentage-point tolerance. No COVERAGE REGRESSION. + +## Changed-code coverage + +CHANGED-CODE-COVERAGE: NOT MEASURED (TEST ASSEMBLY EXCLUDED; QFCDATAMODEL EXCLUDED BY ATTRIBUTE) + +- TEST_ASSEMBLY_PACKAGES: 0 at baseline and 0 post-change (no test assembly was instrumented, so no changed test line has a coverage figure) +- QFCDATAMODEL_CLASS_ENTRIES: 0 at baseline and 0 post-change (the `[ExcludeFromCodeCoverage]` type `QfcDatamodel` is absent from the report at both stages, so the removed production lines were in no measured denominator; the AC29 reading) + +## Test outcomes + +- BASELINE-FAILED-SET: (empty) +- FINAL-FAILED-SET: (empty) +- NEW-FAILURES: NONE diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/coverage-jacoco-projection.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/coverage-jacoco-projection.md new file mode 100644 index 000000000..58bf41004 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/coverage-jacoco-projection.md @@ -0,0 +1,47 @@ +# Final JaCoCo package projection (issue #968, task P8-T5) + +Timestamp: 2026-10-03T03-31 +Source: FEATURE/evidence/qa-gates/coverage-summary.md (CMD-COVERAGE-POST, STAGE final, projection reconciled by Assert-JacocoProjectionReconciliation) + +PROJECTION-BEGIN +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` +PROJECTION-END diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/coverage-summary.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/coverage-summary.md new file mode 100644 index 000000000..fdba55842 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/coverage-summary.md @@ -0,0 +1,59 @@ +# Final coverage summary (issue #968, task P8-T5) + +Timestamp: 2026-10-03T03-31 +Command: pwsh -NoProfile -Command '' with STAGE final (run with the Bash tool's run_in_background option, no redirection), then pwsh -NoProfile -Command '' with STAGE final, RAW True and NAMES-TARGETS; both are the Command Reference macros executed verbatim with PREFIX expanded and WORKTREE substituted +Canonical command: dotnet-coverage collect --output coverage\final-968.cobertura.xml --output-format cobertura --settings coverage\effective-coverage-968.config -- vstest.console.exe /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName!~HelperClasses.ShellUtilities_Tests&FullyQualifiedName!~HelperClasses.ShellUtilitiesStatic_Tests&FullyQualifiedName!~HelperClasses.SysImageListHelperTests&FullyQualifiedName!~EmailIntelligence.OSBrowser_Tests" "/ResultsDirectory:coverage\test-results\968\final" "/Logger:trx;LogFileName=final-968.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 0 +Output Summary: +- ITERATION: 1 +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 (both payloads) +- COVERAGE-ROUTE: DIRECT (equal to the P0-T16 route) +- RAW: True +- COLLECT_EXIT_CODE: 0 +- ASSEMBLY_COUNT: 9 (QuickFiler.Test, SVGControl.Test, Tags.Test, TaskMaster.Test, TaskTree.Test, TaskVisualization.Test, ToDoModel.Test, UtilitiesCS.Test, VBFunctions.Test; each `\.Test\bin\Debug\.Test.dll`) +- SEQUENCE_FILES: 0 +- TRX_PRESENT: True +- DOCUMENT_PRESENT: True +- PAYLOAD-COMPLETE printed +- LINE-FLOOR: MET +- BRANCH-FLOOR: MET +- First-party coverage: lines 56211/65855 (85.36%), branches 13620/17078 (79.75%) (the numeric post-change headline) +- ROOT line-rate=0.853557 branch-rate=0.797517 lines-covered=56211 lines-valid=65855 branches-covered=13620 branches-valid=17078 +- TEST_ASSEMBLY_PACKAGES: 0 +- QFCDATAMODEL_CLASS_ENTRIES: 0 (recorded; P0-T17 value 0) +- CHANGED-CODE-COVERAGE: NOT MEASURED (TEST ASSEMBLY EXCLUDED; QFCDATAMODEL EXCLUDED BY ATTRIBUTE) +- FINAL-FAILED-SET: (empty) +- NEW-FAILURES: NONE (no name in FINAL-FAILED-SET absent from BASELINE-FAILED-SET, which is also empty) +- FIGURES-COMPARED: baseline (P0-T17) Total 7361, executed 7361, error 0, timeout 0, aborted 0, notExecuted 0; final Total 7365, executed 7365, error 0, timeout 0, aborted 0, notExecuted 0. Total equals the baseline plus 4 (the four pin-count tests; the fold rewrites two tests and adds none), executed is not less than baseline plus 4, and error, timeout, aborted and notExecuted are each not greater than baseline. +- RUNNER-GREEN: NO (COVERAGE-ROUTE DIRECT) + +Test-result summary (trx-derived, verbatim between the markers): + +SUMMARY-BEGIN +Test run outcome: Completed +Total 7365, executed 7365, passed 7365, failed 0. +Skipped 0, derived as total minus executed rather than reported by the test platform. +Figures reported verbatim by the test platform: error 0, timeout 0, aborted 0, notExecuted 0, inconclusive 0. +Failed tests: none +SUMMARY-END + +RESULT lines (sixteen, NAMES-TARGETS, all Passed; the four pin-count tests appear as passed in the coverage route's test-result summary, AC21; the two rewritten liveness tests pass in the full parallel run): + +- RESULT BeginTransactionAsync_ZeroBoundWhileThisTestHoldsThePermit_ThrowsTimeoutExceptionAndReleasesNothing = Passed +- RESULT EnsureDispatcher_ScopeDisposedTwice_IsIdempotent = Passed +- RESULT EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease = Passed +- RESULT DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle = Passed +- RESULT Transaction_DisposedTwice_DoesNotOverReleaseTheGate = Passed +- RESULT DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive = Passed +- RESULT TransactionGate_WhileThisTestHoldsATransaction_HasExactlyOneUnreleasedAcquisition = Passed +- RESULT EnsureDispatcher_UnderATransactionHoldingALiveDispatcher_ReleasingAllPinsLeavesTheLiveDispatcher = Passed +- RESULT SetThemeLight_FromNormal_SelectsLightNormalTheme = Passed +- RESULT EnsureDispatcher_TwoPinsHeld_ReleaseOrderDoesNotChangeTheOutcome = Passed +- RESULT EnsureDispatcher_WhileATransactionHoldsALiveDispatcher_DoesNotReplaceIt = Passed +- RESULT EnsureDispatcher_WhenTheFieldIsNull_InstallsAndRestoresOnDispose = Passed +- RESULT Install_CalledTwiceOnTheSameTransaction_ThrowsInvalidOperationException = Passed +- RESULT EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores = Passed +- RESULT SetThemeDark_FromNormal_SelectsDarkNormalTheme = Passed +- RESULT Transaction_SecondCallerCannotInstallUntilTheFirstRestores = Passed + +MESSAGE lines: none (no non-passed, executed test; no LEAK-DEPENDENT TEST EXPOSED). diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/csharpier-check-final.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/csharpier-check-final.md new file mode 100644 index 000000000..9c3a59ef7 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/csharpier-check-final.md @@ -0,0 +1,11 @@ +# Final formatter check (issue #968, task P8-T2) + +Timestamp: 2026-10-03T03-27 +Command: pwsh -NoProfile -Command 'Set-Location -LiteralPath "WORKTREE"; [System.IO.Directory]::SetCurrentDirectory((Get-Location).Path); Write-Output ("WORKTREE-LEAF: " + (Split-Path -Leaf (Get-Location).Path)); dotnet tool run csharpier check .; "CSHARPIER_EXIT_CODE: $LASTEXITCODE"' +Canonical command: dotnet tool run csharpier check . (at the worktree root) +EXIT_CODE: 0 +Output Summary: +- ITERATION: 1 +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- Checked 1640 files in 4966ms. +- CSHARPIER_EXIT_CODE: 0 diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/csharpier-format-final.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/csharpier-format-final.md new file mode 100644 index 000000000..2dc4708d9 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/csharpier-format-final.md @@ -0,0 +1,14 @@ +# Final formatting step (issue #968, task P8-T1) + +Timestamp: 2026-10-03T03-27 +Command: pwsh -NoProfile -Command 'Set-Location -LiteralPath "WORKTREE"; [System.IO.Directory]::SetCurrentDirectory((Get-Location).Path); Write-Output ("WORKTREE-LEAF: " + (Split-Path -Leaf (Get-Location).Path)); dotnet tool run csharpier format .; "CSHARPIER_EXIT_CODE: $LASTEXITCODE"' +Canonical command: dotnet tool run csharpier format . (at the worktree root), bracketed by git -C WORKTREE status --porcelain --untracked-files=all and CMD-HASH on CS13 before and after +EXIT_CODE: 0 +Output Summary: +- ITERATION: 1 +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 (format payload and both CMD-HASH payloads) +- CSHARPIER_EXIT_CODE: 0 +- `Formatted 1640 files in 5265ms.` (a processed-file count, not a rewrite count) +- REWRITTEN-WRITESET: NONE (all thirteen CS13 hashes identical before and after; the values equal the P6-T1 after-hashes recorded in FEATURE/evidence/qa-gates/scoped-format.md) +- REWRITTEN-OTHER: NONE (porcelain before and after list the same four lines: ` M` FEATURE/plan.2026-10-02T05-42.md and `??` FEATURE/evidence/qa-gates/call-site-census.md, implementation-commit.md and prohibited-constructs-grep.md) +- Clean pass: both NONE; no D-13 format restart. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/evidence-hygiene.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/evidence-hygiene.md new file mode 100644 index 000000000..ced7c4475 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/evidence-hygiene.md @@ -0,0 +1,31 @@ +# Evidence hygiene gate (issue #968, task P8-T10) + +Timestamp: 2026-10-03T03-34 +Command: pwsh -NoProfile -Command 'Set-Location -LiteralPath "WORKTREE"; [System.IO.Directory]::SetCurrentDirectory((Get-Location).Path); Write-Output ("WORKTREE-LEAF: " + (Split-Path -Leaf (Get-Location).Path)); $b = [char]92; $files = @(Get-ChildItem -LiteralPath "docs\features\active\2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968\evidence" -Recurse -File); "EVIDENCE_FILES=$($files.Count)"; "RAW_DOCUMENTS=$(@($files | Where-Object { $_.Extension -in @(".trx", ".xml", ".coverage", ".coveragexml", ".log") }).Count)"; $pattern = "[a-z]:[" + $b + $b + "/]+users[" + $b + $b + "/]+[a-z0-9_.~-]"; "PROFILE_PATH_LINES=$(@($files | Select-String -Pattern $pattern).Count)"' +Canonical command: scan of every file under FEATURE/evidence/ for raw test or coverage documents and for drive-letter user-profile paths (the repository hygiene rule's pattern) +EXIT_CODE: 0 +Output Summary (final run, after redaction): +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- EVIDENCE_FILES=72 +- RAW_DOCUMENTS=0 +- PROFILE_PATH_LINES=0 + +First run (same command, exit 0): EVIDENCE_FILES=72, RAW_DOCUMENTS=0, PROFILE_PATH_LINES=10. The ten offending lines were all in inherited preflight reports under FEATURE/evidence/other/ that were committed before this run (P0-T3 INHERITED-COMMITTED): + +- preflight-round1-report.2026-10-02T08-40.md: lines 19 (two occurrences of the item-worktree absolute path inside a quoted hook refusal), 204 and 205 (absolute paths of the plan and spec files) +- preflight-round3-report.2026-10-03T01-01.md: lines 160 and 161 (session-tree agent-memory paths) +- preflight-round4-report.2026-10-03T01-25.md: lines 120 and 121 (session-tree agent-memory paths) +- preflight-round5-report.2026-10-03T01-53.md: lines 111 and 112 (session-tree agent-memory paths) +- preflight-round6-report.2026-10-03T02-21.md: line 116 (a session-tree agent-memory path) + +Redaction applied with the Edit tool as P8-T10 directs: the item-worktree absolute path was replaced by the token `WORKTREE` and the session-tree absolute prefix by `REDACTED-PATH`; no other text in those files changed. The task was then re-run (the final run above). A Grep of the whole feature folder for the same pattern (case-insensitive) found no file. + +## Re-run after check-offs + +Timestamp: 2026-10-03T03-36 +Command: the same P8-T10 payload, re-run after P8-T43 (task P8-T44), exit 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- EVIDENCE_FILES=74 +- RAW_DOCUMENTS=0 +- PROFILE_PATH_LINES=0 (for the evidence tree as it will be committed) diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/file-line-counts.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/file-line-counts.md new file mode 100644 index 000000000..bcb31ba0c --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/file-line-counts.md @@ -0,0 +1,28 @@ +# File-size gate (issue #968, task P8-T8) + +Timestamp: 2026-10-03T03-32 +Command: pwsh -NoProfile -Command '' with FILES = CS13, then pwsh -NoProfile -Command '' on PROJ with TOKENS `" exit 0 +- FINAL-COMMIT: 5699431f5b9bb63670a90edbd747a479a0bfea6f (observation) +- git -C WORKTREE diff --name-status 94287369908cc920b21b0e3256314f988ad7d2f5 HEAD -> exit 0: the paths outside FEATURE are exactly the P8-T9 footprint (the fourteen Write Set code paths: eleven M, three A) plus INHERITED-AND-EXCLUDED (the two promoted records under docs/features/potential/promoted/); every other path is under FEATURE. (A pathspec-scoped run of the same diff was also made before the unscoped run; it printed the same non-FEATURE lines.) +- git -C WORKTREE status --porcelain --untracked-files=all -> exit 0, empty output at the time it ran (no path under FEATURE, QuickFiler/, QuickFiler.Test/ or scripts/) + +This artifact and the plan's P8-T46 check-off mark are written after the commit; they are committed by the orchestrator with the plan file. No PR is opened and no merge is performed; the PR body the orchestrator authors carries `Closes #968` and `Closes #972`. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/fixture-change-census.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/fixture-change-census.md new file mode 100644 index 000000000..336247528 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/fixture-change-census.md @@ -0,0 +1,152 @@ +# Fixture-change census (issue #968, tasks P2-T1 to P2-T6) + +Timestamp: 2026-10-03T02-58 +Command: pwsh -NoProfile -Command '' with FILES `"QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixture.cs"` (the first of four payloads; then CMD-TOKEN-COUNT on FIX with the P0-T12 FIX list, CMD-SPAN-TOKEN-COUNT on ENSURE and on SCOPE with the P0-T12 lists; each the Command Reference macro executed verbatim with PREFIX expanded and WORKTREE substituted), followed by three git calls +Canonical command: CMD-LINECOUNT, CMD-TOKEN-COUNT and CMD-SPAN-TOKEN-COUNT on FIX; git -C WORKTREE diff --numstat HEAD -- QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs; git -C WORKTREE diff HEAD -- QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs; git -C WORKTREE status --porcelain -- QuickFiler QuickFiler.Test +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 (every payload) +- LINES QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixture.cs = 375 +- FIX tokens (`_pinCount`, `_fixtureInstalledParked`, `lock (FieldLock)`, `CompareExchange(`, `return new EnsureScope(`, `leaks exactly`, `A scope that installed nothing`, `pins for the process lifetime`, `install-ownership flag`, `installed nothing carries`): 4, 4, 5, 2, 1, 0, 0, 2, 1, 0 +- ENSURE: SPAN: 143-167; `_pinCount++` 1, `_fixtureInstalledParked = true;` 1, `lock (FieldLock)` 1, `return new EnsureScope(` 1 +- SCOPE: SPAN: 273-316; `CompareExchange(` 0, `lock (FieldLock)` 1, `_pinCount--` 1, `_fixtureInstalledParked = false;` 1, `DispatcherField.SetValue(null, null);` 1 +- numstat (exit 0): `48 15 QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs` +- porcelain (exit 0): + - ` M QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs` + - ` M QuickFiler.Test/QuickFiler.Test.csproj` + - `?? QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs` +- P2-T1: one line contains `private static int _pinCount;` and one contains `private static bool _fixtureInstalledParked;`, both in the static field block above the issue #743 counters; the inserted comment contains `only while FieldLock is held` and does not contain `lock (FieldLock)`. +- P2-T2: `install-ownership flag` 1; the class doc ends with the new paragraph followed by `/// `. +- P2-T3: `leaks exactly` 0; `pins for the process lifetime` 2. +- P2-T4 and P2-T5: as the ENSURE and SCOPE values above; `A scope that installed nothing` 0. + +## FIELDLOCK-ENCLOSURE + +Reading the transcribed diff below: the two new fields are `private static` members of `UiThreadDispatcherFixture` (diff hunk 2). The two `lock (FieldLock)` blocks that use them are (1) the block in `EnsureDispatcher` (hunk 4), which contains `_pinCount++;` and `_fixtureInstalledParked = true;`, and (2) the block in `EnsureScope.Dispose` (hunk 6), which contains `_pinCount--;`, `_pinCount == 0`, `&& _fixtureInstalledParked` and `_fixtureInstalledParked = false;`. The three non-declaration occurrences of each new field therefore all lie inside one of those two blocks, and the F-FIELDS comment names neither identifier. The scope class contains no `CompareExchange` call (SCOPE `CompareExchange(` 0): the last-release null write `DispatcherField.SetValue(null, null);` is made inline in the same critical section as the decrement (the AC9 reading). + +## Transcribed diff (git -C WORKTREE diff HEAD -- QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs) + +```diff +diff --git a/QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs b/QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs +index 1eaa87064..3b7f9e1f1 100644 +--- a/QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs ++++ b/QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs +@@ -28,6 +28,19 @@ namespace QuickFiler.Controllers.Tests + /// that carry no [Timeout], so making them wait on a gate another test class holds for a + /// whole test body would convert a bounded failure elsewhere into an unbounded hang there. + /// ++ /// ++ /// Issue #968: ensure pins are reference counted. A pin counter and an install-ownership flag ++ /// live under FieldLock. The first pin on a null field seeds the parked dispatcher ++ /// and sets the flag; the last release writes null back only when the flag is set and the ++ /// field still holds the parked instance, then clears the flag. A discarded scope therefore ++ /// pins for the process lifetime and leaves the parked dispatcher installed, so every caller ++ /// disposes its scope, and every pin is acquired and released while its caller holds a ++ /// transaction, which keeps the count at zero whenever a transaction is acquired. Residual: a ++ /// transaction that installs over a pinned parked value and restores it after the last pin ++ /// released leaves the parked value installed with zero pins and the flag set; the next pin ++ /// cycle reverts it. No test in this assembly installs over a pinned value, so the residual is ++ /// documented rather than exercised. ++ /// + /// + internal static class UiThreadDispatcherFixture + { +@@ -37,6 +50,11 @@ namespace QuickFiler.Controllers.Tests + private static readonly FieldInfo DispatcherField = ResolveDispatcherField(); + private static Dispatcher _parkedDispatcher = null; + ++ // Issue #968: the count of live ensure scopes and whether the fixture itself seeded the parked ++ // dispatcher into a null field. Both are read and written only while FieldLock is held. ++ private static int _pinCount; ++ private static bool _fixtureInstalledParked; ++ + // Issue #743 AC1 observable: three monotonic counters over TransactionGate. A contended + // acquisition is one that observed CurrentCount == 0 immediately before waiting. In a serial + // run no live holder can exist when a test begins its transaction, so a non-zero contended +@@ -114,10 +132,13 @@ namespace QuickFiler.Controllers.Tests + } + + /// +- /// Seeds the static with the parked dispatcher only when it is currently null, and +- /// returns a scope whose Dispose conditionally reverts that seeding. Never acquires +- /// TransactionGate and never blocks on anything a caller must release. Disposing the +- /// returned scope is optional: a discarded scope leaks exactly as the pre-fix helper did. ++ /// Takes one counted pin on the shared static (issue #968). The first pin on a null ++ /// field seeds the parked dispatcher and records that the fixture owns the seeding; a pin ++ /// taken while the field is non-null installs nothing. Disposing the returned scope releases ++ /// the pin, and the field reverts to null only on the last release, only when the ++ /// fixture owns the seeding, and only when the field still holds the parked instance. Never ++ /// acquires TransactionGate and never blocks on anything a caller must release. A ++ /// discarded scope pins for the process lifetime, so every caller disposes its scope. + /// + internal static IDisposable EnsureDispatcher() + { +@@ -127,14 +148,15 @@ namespace QuickFiler.Controllers.Tests + + lock (FieldLock) + { ++ _pinCount++; + if (DispatcherField.GetValue(null) == null) + { + DispatcherField.SetValue(null, parked); +- return new EnsureScope(parked); ++ _fixtureInstalledParked = true; + } + } + +- return new EnsureScope(null); ++ return new EnsureScope(parked); + } + + /// +@@ -241,19 +263,21 @@ namespace QuickFiler.Controllers.Tests + } + + /// +- /// The scope returned by . Reverts the seeding only when the +- /// static still holds the exact instance this scope installed. A scope that installed nothing +- /// carries null and is a no-op, which is what keeps a discarded scope from clobbering a +- /// value some other owner installed in the meantime. ++ /// The scope returned by : one counted pin. Disposal is ++ /// idempotent and performs the decrement and the conditional revert inline in one ++ /// FieldLock critical section, so no other pin can interleave between them. The revert ++ /// writes null only when this release brings the count to zero, the fixture itself ++ /// seeded the parked dispatcher, and the field still holds that instance; a value some other ++ /// owner installed in the meantime is left in place. + /// + private sealed class EnsureScope : IDisposable + { +- private readonly Dispatcher _installed; ++ private readonly Dispatcher _parked; + private bool _disposed = false; + +- internal EnsureScope(Dispatcher installed) ++ internal EnsureScope(Dispatcher parked) + { +- _installed = installed; ++ _parked = parked; + _disposed = false; + } + +@@ -266,9 +290,18 @@ namespace QuickFiler.Controllers.Tests + + _disposed = true; + +- if (_installed != null) ++ lock (FieldLock) + { +- UiThreadDispatcherFixture.CompareExchange(_installed, null); ++ _pinCount--; ++ if ( ++ _pinCount == 0 ++ && _fixtureInstalledParked ++ && ReferenceEquals(DispatcherField.GetValue(null), _parked) ++ ) ++ { ++ DispatcherField.SetValue(null, null); ++ _fixtureInstalledParked = false; ++ } + } + } + } +``` diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/fold-edit-census.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/fold-edit-census.md new file mode 100644 index 000000000..2999354fe --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/fold-edit-census.md @@ -0,0 +1,45 @@ +# Fold-edit census (issue #968, tasks P4-T2 to P4-T9) + +Timestamp: 2026-10-03T03-07 +Command: pwsh -NoProfile -Command '' with FILE = `QuickFiler.Test\TestSupport\SynchronousBackgroundWorker.cs` (SBW), the first of eleven payloads (then CMD-LINECOUNT on FOLD6 plus SBW; CMD-TOKEN-COUNT on LIV, TD, ZB, DMT, QDM and PROJ with the P0-T13 lists and on SBW with the P4-T9 list; CMD-SPAN-TOKEN-COUNT on HELD; CMD-HUNKS on QuickFiler/Controllers/QfcDatamodel.cs; each the Command Reference macro executed verbatim with PREFIX expanded and WORKTREE substituted), followed by two git calls +Canonical command: as listed; git -C WORKTREE diff --numstat HEAD -- QuickFiler QuickFiler.Test; git -C WORKTREE status --porcelain -- QuickFiler QuickFiler.Test +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 (every payload) +- P4-T2 CMD-EOL on SBW: BARE_LF: 0; CRLF_COUNT: 27; LINES: 27 (CRLF_COUNT equals LINES; at most 500 and at least 20) +- LINES: LIV 311, TD 229, ZB 224, DMT 374, QDM 367, QQP 413, SBW 27 (every value at most 500; QDM at most 400) +- LIV tokens (P0-T13 order): 0, 2, 2, 4, 3, 4, 3, 3, 1, 1, 0, 0, 0, 4 (`class SynchronousBackgroundWorker` 0, `StartSynchronously` 2, `SynchronousBackgroundWorker.StartSynchronously` 2, `new SynchronousBackgroundWorker()` 4, `using (var worker = new SynchronousBackgroundWorker())` 3, `StartHeldOpenLoader(` 4, `Task.Yield` 3, `fake.Advance` 3, `FakeTimeProvider` 1, `using QuickFiler.Test.TestSupport;` 1, `NoSynchronizationContext` 0, `Duplicated per file` 0, `new ArmingFakeTimeProvider()` 0, `[TestMethod]` 4) +- TD tokens: 0, 1, 1, 1, 0, 1, 5 +- ZB tokens: 0, 3, 3, 3, 3, 0, 0, 0, 0, 0, 1, 3 +- DMT tokens: 2, 1, 0, 0, 0, 1, 0, 1, 4, 5, 1, 9 (`new BackgroundWorker()` 2, `using (var worker = new BackgroundWorker())` 1, `using QuickFiler.Test.TestSupport;` 1, `[TestMethod]` 9) +- QDM tokens: 0, 0, 1, 0, 2, 0, 0, 1, 0, 0, 6, 6, 1, 0, 0, 0, 0, 1, 2, 2, 3 +- PROJ tokens: ` exit 0 +- IMPLEMENTATION-COMMIT: b874ea3c317684ce733e4a2213de804f4a175ac5 (observation) +- git -C WORKTREE diff --name-status 94287369908cc920b21b0e3256314f988ad7d2f5 HEAD -> exit 0: + - status M: QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs, QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs, QuickFiler.Test/Controllers/QfcDatamodelTests.cs, QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs, QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs, QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs, QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs, QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs, QuickFiler.Test/QuickFiler.Test.csproj, QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs, QuickFiler/Controllers/QfcDatamodel.cs (the eleven modified Write Set code paths) + - status A: QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs, QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs, QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs (the three new files) + - every other listed path is under FEATURE (docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/...) or is one of the two P0-T3 INHERITED-COMMITTED promoted records (docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md, docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md) +- git -C WORKTREE status --porcelain --untracked-files=all -> exit 0, empty output at the time it ran (no porcelain line names a path under QuickFiler/ or QuickFiler.Test/) + +The commit message omits the attribution trailer because D-10 prohibits angle brackets in commit messages. This artifact and the plan check-off mark are written after the commit and are committed in P8-T46. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/liveness-edit-census.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/liveness-edit-census.md new file mode 100644 index 000000000..797af6235 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/liveness-edit-census.md @@ -0,0 +1,44 @@ +# Liveness-edit census (issue #968, tasks P5-T2 to P5-T5) + +Timestamp: 2026-10-03T03-11 +Command: pwsh -NoProfile -Command '' with FILE = `QuickFiler.Test\TestSupport\ArmingFakeTimeProvider.cs` (AFTP), the first of nine payloads (then CMD-LINECOUNT on LIV, DMT and AFTP; CMD-TOKEN-COUNT on LIV, DMT and PROJ with the P0-T13 lists and on AFTP with the P5-T5 list; CMD-SPAN-TOKEN-COUNT on T1-LIVE, HELD and T-SIB; each the Command Reference macro executed verbatim with PREFIX expanded and WORKTREE substituted), followed by two git calls +Canonical command: as listed; git -C WORKTREE diff --numstat HEAD -- QuickFiler QuickFiler.Test; git -C WORKTREE status --porcelain -- QuickFiler QuickFiler.Test +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 (every payload) +- P5-T2 CMD-EOL on AFTP: BARE_LF: 0; CRLF_COUNT: 49; LINES: 49 (CRLF_COUNT equals LINES; at most 500 and at least 30) +- P5-T2 project item: exactly one project-file line contains `TestSupport\ArmingFakeTimeProvider.cs`; it was inserted immediately after the `TestSupport\SynchronousBackgroundWorker.cs` line, and the line after it is the `TestSupport\WinFormsPumpHostTests.cs` item +- LINES: LIV 348, DMT 392, AFTP 49 (each at most 500) +- LIV tokens (P0-T13 order): 0, 2, 2, 4, 4, 4, 0, 0, 2, 1, 3, 0, 1, 4 (`Task.Yield` 0, `fake.Advance` 0, `FakeTimeProvider` 2 = the two `ArmingFakeTimeProvider` lines of L-T1, `NoSynchronizationContext` 3, `new ArmingFakeTimeProvider()` 1, `new SynchronousBackgroundWorker()` 4, `using (var worker = new SynchronousBackgroundWorker())` 4, `StartSynchronously` 2, `[TestMethod]` 4) +- DMT tokens (P0-T13 order): 2, 2, 1, 1, 1, 1, 1, 0, 2, 6, 1, 9 +- PROJ tokens: ` result = await pending;` 1 +- SCOPE-BODIES-AWAIT-FREE: YES. Reading the T1-LIVE span, the three `await` lines are 116 (`return await loaderRelease.Task;`, inside the loader lambda), 141 (`Task first = await Task.WhenAny(clock.Armed, pending);`) and 164 (`(await pending)`); the two `using (NoSynchronizationContext())` bodies are lines 124 to 132 and 156 to 158, and neither contains an `await` line. +- numstat (exit 0): + - `137 101 QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs` + - `2 17 QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs` + - `68 47 QuickFiler.Test/Controllers/QfcDatamodelTests.cs` + - `41 49 QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs` + - `20 35 QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs` + - `20 18 QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs` + - `48 15 QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs` + - `16 14 QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs` + - `3 0 QuickFiler.Test/QuickFiler.Test.csproj` + - `1 129 QuickFiler/Controllers/QfcDatamodel.cs` +- porcelain (exit 0), thirteen lines: the twelve P4-T9 lines plus `?? QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs`: + - ` M QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs` + - ` M QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs` + - ` M QuickFiler.Test/Controllers/QfcDatamodelTests.cs` + - ` M QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs` + - ` M QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs` + - ` M QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs` + - ` M QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs` + - ` M QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs` + - ` M QuickFiler.Test/QuickFiler.Test.csproj` + - ` M QuickFiler/Controllers/QfcDatamodel.cs` + - `?? QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs` + - `?? QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs` + - `?? QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs` diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/msbuild-analyzer-final.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/msbuild-analyzer-final.md new file mode 100644 index 000000000..149aac731 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/msbuild-analyzer-final.md @@ -0,0 +1,20 @@ +# Final analyzer rebuild (issue #968, task P8-T3) + +Timestamp: 2026-10-03T03-28 +Command: pwsh -NoProfile -Command '' with the analyzer GATEARGS (`/p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true`) and TASKID p8-t3; the payload is identical, line for line, to the one transcribed in full in FEATURE/evidence/baseline/msbuild-analyzer-baseline.md except that the log is `coverage\logs\p8-t3.msbuild.log` +Canonical command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true (resolved through vswhere against WORKTREE/TaskMaster.sln, plus /nodeReuse:false and a normal-verbosity file logger under the ignored coverage directory) +EXIT_CODE: 0 +Output Summary: +- ITERATION: 1 +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- MSBUILD_EXIT_CODE: 0 +- ERRORS: 0 +- WARNINGS: 0 (ANALYZER-BASELINE-WARNINGS: 0) +- SKIP_CORECOMPILE_LINES: 0 +- CSC_OUT_QUICKFILER: 2 +- CSC_OUT_QUICKFILER_TEST: 2 +- WRITESET_DIAGNOSTIC_LINES: 0 (not greater than ANALYZER-BASELINE-WRITESET-LINES: 0) +- WRITESET_DIAGNOSTIC_CODES: (empty; ANALYZER-BASELINE-WRITESET-CODES: empty) — no new analyzer diagnostic in a Write Set file +- TEST_DLL_EXISTS: True +- UCS_TEST_DLL_EXISTS: True +- This rebuild is the second compile proof for AC27 (no surviving reference to a removed QfcDatamodel member). diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/msbuild-nullable-final.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/msbuild-nullable-final.md new file mode 100644 index 000000000..4f8706547 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/msbuild-nullable-final.md @@ -0,0 +1,19 @@ +# Final TreatWarningsAsErrors rebuild (issue #968, task P8-T4) + +Timestamp: 2026-10-03T03-29 +Command: pwsh -NoProfile -Command '' with GATEARGS `/p:TreatWarningsAsErrors=true` and TASKID p8-t4; the payload is identical, line for line, to the one transcribed in full in FEATURE/evidence/baseline/msbuild-analyzer-baseline.md except that `/p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` is replaced by `/p:TreatWarningsAsErrors=true` and the log is `coverage\logs\p8-t4.msbuild.log` +Canonical command: msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true (no Nullable override; resolved through vswhere against WORKTREE/TaskMaster.sln, plus /nodeReuse:false and a normal-verbosity file logger under the ignored coverage directory) +EXIT_CODE: 0 +Output Summary: +- ITERATION: 1 +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- MSBUILD_EXIT_CODE: 0 +- ERRORS: 0 +- WARNINGS: 0 +- SKIP_CORECOMPILE_LINES: 0 +- CSC_OUT_QUICKFILER: 2 +- CSC_OUT_QUICKFILER_TEST: 2 +- WRITESET_DIAGNOSTIC_LINES: 0 +- WRITESET_DIAGNOSTIC_CODES: (empty) +- TEST_DLL_EXISTS: True +- UCS_TEST_DLL_EXISTS: True diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/post-format-census.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/post-format-census.md new file mode 100644 index 000000000..688d3b58d --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/post-format-census.md @@ -0,0 +1,118 @@ +# Post-format census (issue #968, task P6-T2) + +Timestamp: 2026-10-03T03-19 +Command: pwsh -NoProfile -Command '' with FILES = CS13 (the first of twenty-nine separate payloads re-running every P1-T3, P2-T6, P3-T9, P4-T9, P5-T5 and P5-T12 command after the P6-T1 scoped format: CMD-LINECOUNT on CS13; CMD-TOKEN-COUNT on FIX, FAT, TS (extended list), FT, PC, PROJ (P1-T3 list), PROJ (P0-T13 list), LIV, TD, ZB, DMT, QDM, QQP, SBW and AFTP; CMD-SPAN-TOKEN-COUNT on ENSURE, SCOPE, R4SPAN, R4HEAD, R4TAIL, T1-LIVE, HELD, T-SIB and GATE-LAMBDA; CMD-HUNKS on TestSupport, the fixture tests, QfcDatamodel.cs and QfcDatamodel.QueueProcessing.cs; CMD-PIN-NESTING on T3SPAN; each the Command Reference macro executed verbatim with PREFIX expanded and WORKTREE substituted), followed by three git calls +Canonical command: as listed; git -C WORKTREE diff --numstat HEAD -- QuickFiler QuickFiler.Test; git -C WORKTREE diff HEAD -- QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs; git -C WORKTREE status --porcelain -- QuickFiler QuickFiler.Test +EXIT_CODE: 0 +Output Summary: WORKTREE-LEAF agent-a291a7fbabf9d0229 in every payload and every payload exited 0; every token, span, hunk and numstat value holds as last recorded for its file; LINES and printed SPAN ranges differ only for the three files P6-T1 named in REWRITTEN; GATE-LAMBDA 1, 0; T3SPAN NEST tail finally, Dispose(), finally, ShutdownDispatcher(; porcelain lists exactly the fourteen Write Set code paths (eleven ` M`, three `??`). Details below. + +This is the first P6-T2 pass (no D-13 restart), and no commit of the Write Set exists yet, so every git command is anchored to HEAD as the task states. + +## CMD-LINECOUNT on CS13 (every value at most 500; QfcDatamodel.cs at most 400) + +| File | LINES | Last recorded | Note | +|---|---|---|---| +| FIX | 375 | 375 (P2-T6) | unchanged | +| FAT | 482 | 482 (P3-T9) | unchanged | +| TS | 442 | 442 (P3-T9) | unchanged | +| FT | 472 | 472 (P3-T9) | unchanged | +| PC | 248 | 248 (P1-T3) | unchanged | +| LIV | 352 | 348 (P5-T5) | differs; named in REWRITTEN | +| TD | 229 | 229 (P4-T9) | unchanged | +| ZB | 226 | 224 (P4-T9) | differs; named in REWRITTEN | +| DMT | 394 | 392 (P5-T5) | differs; named in REWRITTEN | +| QDM | 367 | 367 (P4-T9) | unchanged; at most 400 | +| QQP | 413 | 413 (P5-T12) | unchanged | +| SBW | 27 | 27 (P4-T9) | unchanged | +| AFTP | 49 | 49 (P5-T5) | unchanged | + +## CMD-TOKEN-COUNT (each list and order as in its recording task) + +- FIX (P2-T6): 4, 4, 5, 2, 1, 0, 0, 2, 1, 0 (`_pinCount` 4, `_fixtureInstalledParked` 4, `lock (FieldLock)` 5, `CompareExchange(` 2, `return new EnsureScope(` 1, `leaks exactly` 0, `A scope that installed nothing` 0, `pins for the process lifetime` 2, `install-ownership flag` 1, `installed nothing carries` 0) +- FAT (P3-T9): 0, 0, 8, 8, 1, 1, 1, 17 (`EnsureUiThreadDispatcher` 0, `private static Mock BuildExecutingViewer` 0, `QfcItemControllerTestSupport.BuildExecutingViewer()` 8, `BuildExecutingViewer` 8, `absorbs the delegate without running it` 1, `shared UiThread static is irrelevant` 1, `absorbs the queued application` 1, `[TestMethod]` 17) +- TS (P3-T9 extended list): 0, 0, 0, 0, 1, 1, 1, 1, 1 (`Becomes moot` 0, `leaks exactly` 0, `still delegate to a callee` 0, `not reachable from another test file` 0, `remaining legitimate` 1, `QfcItemController_UiThreadDispatcherPinCountTests` 1, `internal static void EnsureSynchronizationContext()` 1, `UiThreadDispatcherFixture.EnsureDispatcher();` 1, `Issue #480 shared arrange helper` 1) +- FT (P3-T9): 0, 1, 1, 8, 1, 3, 1, 1, 8 (`no other class may dispose` 0, `removed that pin: the fixture now counts pins` 1, `(W5) must not latch` 1, `[Timeout(GateTimeoutMs)]` 8, `private const int GateTimeoutMs = 60000;` 1, `EnsureUiThreadDispatcher()` 3, `issue #230 lost update` 1, `the waiter cannot observe the pre-restore value` 1, `[TestMethod]` 8) +- PC (P1-T3): 10, 1, 3, 1, 1, 4, 4, 1, 3, 1, 4, 1, 2, 2, 0, 0, 0, 1, 1 (`EnsureUiThreadDispatcher()` 10, `Regression test: fails before the fix` 1, `Specification test: passes before and after the fix` 3, `never read the shared static` 1, `[TestClass]` 1, `[TestMethod]` 4, `[Timeout(GateTimeoutMs)]` 4, `private const int GateTimeoutMs = 60000;` 1, `transaction.Install(null);` 3, `transaction.Install(live);` 1, `transaction.Dispose();` 4, `QfcItemControllerTestSupport.ShutdownDispatcher(live);` 1, `a holder that did not take the last pin must not lose the dispatcher` 2, `the last release reverts the fixture` 2, `using Moq;` 0, `Thread.Sleep` 0, `Task.Delay` 0, `public class QfcItemController_UiThreadDispatcherPinCountTests` 1, `foreignTransaction.Install(parked);` 1) +- PROJ (P1-T3 list): `Controllers\QfcItemController.UiThreadDispatcherPinCountTests.cs` 1, `Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs` 1 +- PROJ (P0-T13 list, as last recorded at P5-T5): ` _remainingLoadActive,` 1, `() => false,` 0, `private volatile bool _remainingLoadActive;` 1) +- SBW (P4-T9): `class SynchronousBackgroundWorker` 1, `internal sealed class SynchronousBackgroundWorker : BackgroundWorker` 1, `internal static void StartSynchronously(BackgroundWorker worker)` 1, `Dispose` 1, `namespace QuickFiler.Test.TestSupport` 1 +- AFTP (P5-T5): `internal sealed class ArmingFakeTimeProvider : FakeTimeProvider` 1, `internal Task Armed` 1, `internal void ReArm()` 1, `public override ITimer CreateTimer(` 1, `base.CreateTimer(` 1, `RunContinuationsAsynchronously` 1, `_armed.TrySetResult(true);` 1, `namespace QuickFiler.Test.TestSupport` 1 + +## CMD-SPAN-TOKEN-COUNT + +- ENSURE (FIX): SPAN: 143-167; `_pinCount++` 1, `_fixtureInstalledParked = true;` 1, `lock (FieldLock)` 1, `return new EnsureScope(` 1 +- SCOPE (FIX): SPAN: 273-316; `CompareExchange(` 0, `lock (FieldLock)` 1, `_pinCount--` 1, `_fixtureInstalledParked = false;` 1, `DispatcherField.SetValue(null, null);` 1 +- R4SPAN (FT): SPAN: 212-286; `EnsureUiThreadDispatcher()` 0, `using (` 1, `transactionA.Dispose();` 2, `finally` 3, `.BeSameAs(` 1, `.NotBeSameAs(` 1, `issue #230 lost update` 1 +- R4HEAD (FT): SPAN: 212-222; `EnsureUiThreadDispatcher()` 0, `using (` 0, `try` 2 +- R4TAIL (FT): SPAN: 265-275; `}` 4, `finally` 2, `transactionA.Dispose();` 1 +- T1-LIVE (LIV): SPAN: 102-173 (was 102-169 at P5-T5; LIV is named in REWRITTEN); `await` 3, `using (NoSynchronizationContext())` 2, `Task.Yield` 0, `fake.Advance` 0, `for (int i` 0, `clock.ReArm();` 1, `(await pending)` 1 +- HELD (LIV): SPAN: 211-245 (was 207-241; LIV is named in REWRITTEN); `new SynchronousBackgroundWorker()` 0, `SynchronousBackgroundWorker worker,` 1, `SynchronousBackgroundWorker.StartSynchronously` 1 +- T-SIB (DMT): SPAN: 103-154 (was 103-152; DMT is named in REWRITTEN); `await Task.Yield();` 0, `clock.ReArm();` 1, `await Task.WhenAny(clock.Armed, pending)` 1, `using (var worker = new BackgroundWorker())` 1, `IList result = await pending;` 1 +- GATE-LAMBDA (QQP): SPAN: 299-310; `() => _remainingLoadActive,` 1, `() => false,` 0 (the sensitivity edit is not present) + +## CMD-HUNKS (anchored to BASE) + +- TestSupport: GIT_DIFF_EXIT_CODE: 0; `@@ -214,25 +214,27 @@`; `@@ -282,9 +284,9 @@`; HUNK_COUNT: 2 (every old-range start at or above 200) +- Fixture tests: GIT_DIFF_EXIT_CODE: 0; `@@ -194,17 +194,17 @@`; `@@ -218,9 +218,7 @@`; `@@ -268,6 +266,10 @@`; HUNK_COUNT: 3 (every hunk inside R4's doc and body: old ranges 194-210, 218-226, 268-273) +- QfcDatamodel.cs: GIT_DIFF_EXIT_CODE: 0; HUNK_COUNT: 7 (recorded, not gated; unchanged from P4-T9) +- QfcDatamodel.QueueProcessing.cs: GIT_DIFF_EXIT_CODE: 0; `@@ -13,13 +13,13 @@`; `@@ -282,7 +282,7 @@`; HUNK_COUNT: 2 + +## CMD-PIN-NESTING on T3SPAN (PC) + +SPAN: 134-183 + +- NEST 141 [BeginTransactionAsync()] .BeginTransactionAsync() +- NEST 145 [.Install(] transaction.Install(live); +- NEST 146 [EnsureUiThreadDispatcher()] IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- NEST 147 [EnsureUiThreadDispatcher()] IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); +- NEST 150 [Dispose()] pinA.Dispose(); +- NEST 151 [Dispose()] pinB.Dispose(); +- NEST 163 [finally] finally +- NEST 165 [Dispose()] transaction.Dispose(); +- NEST 168 [finally] finally +- NEST 170 [ShutdownDispatcher(] QfcItemControllerTestSupport.ShutdownDispatcher(live); + +The NEST output ends with four lines whose tokens are, in order, `finally`, `Dispose()` (the `transaction.Dispose();` line), `finally`, `ShutdownDispatcher(`: the AC3 reading that the live dispatcher is shut down in a finally block. + +## numstat (git -C WORKTREE diff --numstat HEAD -- QuickFiler QuickFiler.Test, exit 0) + +- `141 101 QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs` +- `2 17 QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs` +- `70 47 QuickFiler.Test/Controllers/QfcDatamodelTests.cs` +- `43 49 QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs` +- `20 35 QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs` +- `20 18 QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs` +- `48 15 QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs` (equal to the P2-T6 row) +- `16 14 QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs` +- `3 0 QuickFiler.Test/QuickFiler.Test.csproj` (the project-file row in place of the P1-T3 value) +- `8 8 QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs` +- `1 129 QuickFiler/Controllers/QfcDatamodel.cs` (equal to the P4-T9 row) + +## FIELDLOCK-ENCLOSURE (restated against the formatted diff) + +The fixture's formatted diff against HEAD (`git -C WORKTREE diff HEAD -- QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs`, exit 0) is identical to the diff transcribed in FEATURE/evidence/qa-gates/fixture-change-census.md (the file's hash and numstat row are unchanged by P6-T1). Both new fields are private statics of `UiThreadDispatcherFixture`; the three non-declaration occurrences of each lie inside the two `lock (FieldLock)` blocks, the one in `EnsureDispatcher` (`_pinCount++;`, `_fixtureInstalledParked = true;`) and the one in `EnsureScope.Dispose` (`_pinCount--;`, `_pinCount == 0`, `&& _fixtureInstalledParked`, `_fixtureInstalledParked = false;`); the scope class contains no `CompareExchange` call and writes null inline in the same critical section as the decrement. + +## Porcelain (git -C WORKTREE status --porcelain -- QuickFiler QuickFiler.Test, exit 0): exactly the fourteen Write Set code paths, eleven ` M` and three `??` + +- ` M QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs` +- ` M QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs` +- ` M QuickFiler.Test/Controllers/QfcDatamodelTests.cs` +- ` M QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs` +- ` M QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs` +- ` M QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs` +- ` M QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs` +- ` M QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs` +- ` M QuickFiler.Test/QuickFiler.Test.csproj` +- ` M QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs` +- ` M QuickFiler/Controllers/QfcDatamodel.cs` +- `?? QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs` +- `?? QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs` +- `?? QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs` + +This artifact is the evidence for AC6, AC7, AC9, AC11, AC12, AC13, AC15, AC16, AC17, AC25, AC26, AC30 and the census half of AC3, AC10, AC14, AC21, AC27, AC29 and AC31. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/prohibited-constructs-grep.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/prohibited-constructs-grep.md new file mode 100644 index 000000000..0f1313218 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/prohibited-constructs-grep.md @@ -0,0 +1,30 @@ +# Prohibited-construct gate (issue #968, task P7-T3) + +Timestamp: 2026-10-03T03-26 +Command: pwsh -NoProfile -Command '' (the Command Reference macro executed verbatim with PREFIX expanded, WORKTREE substituted and CODE14-GIT expanded to the fourteen Write Set code paths), run after the P6-T9 commit so the new files are tracked +Canonical command: added lines of `git diff 94287369908cc920b21b0e3256314f988ad7d2f5 -- CODE14-GIT`, scanned for the prohibited tokens and the two positive controls; then git -C WORKTREE diff --exit-code 94287369908cc920b21b0e3256314f988ad7d2f5 HEAD -- scripts/vscode/TaskMaster.cli.runsettings TaskMaster.runsettings; git -C WORKTREE status --porcelain -- scripts/vscode/TaskMaster.cli.runsettings TaskMaster.runsettings; CMD-TOKEN-COUNT on FT +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 (both payloads) +- GIT_DIFF_EXIT_CODE: 0 +- ADDED_LINES: 696 +- ADDED-TOKEN [Thread.Sleep] = 0 +- ADDED-TOKEN [Task.Delay] = 0 +- ADDED-TOKEN [DoNotParallelize] = 0 +- ADDED-TOKEN [Retry(] = 0 +- ADDED-TOKEN [Path.GetTempFileName] = 0 +- ADDED-TOKEN [Path.GetTempPath] = 0 +- ADDED-TOKEN [Workers] = 0 +- ADDED-TOKEN [Timeout(] = 4 +- ADDED-TOKEN [[Timeout(GateTimeoutMs)]] = 4 (every added timeout attribute is the sibling file's constant convention, in the pin-count class; the datamodel tests gain none) +- ADDED-TOKEN [GateTimeoutMs = ] = 1 +- ADDED-LINE private const int GateTimeoutMs = 60000; (no timeout increase: the value equals the sibling constant) +- ADDED-TOKEN [await Task.Yield();] = 0 +- ADDED-TOKEN [for (int i] = 0 +- ADDED-TOKEN [using var ] = 0 +- ADDED-TOKEN [_pinCount] = 4 (positive control) +- ADDED-TOKEN [ArmingFakeTimeProvider] = 6 (positive control) +- runsettings diff --exit-code: exit 0, no output (both runsettings files unchanged from BASE) +- runsettings porcelain: exit 0, prints nothing +- FT tokens: `[Timeout(GateTimeoutMs)]` 8, `private const int GateTimeoutMs = 60000;` 1 (equal to P0-T12) +- No PROHIBITED CONSTRUCT ADDED. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/qfc-datamodel-legacy-callers.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/qfc-datamodel-legacy-callers.md new file mode 100644 index 000000000..b06dbbe57 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/qfc-datamodel-legacy-callers.md @@ -0,0 +1,94 @@ +# Zero-caller proof for the four legacy QfcDatamodel members (issue #968, task P4-T1) + +Timestamp: 2026-10-03T03-03 +Command: pwsh -NoProfile -Command '' (the Command Reference macro executed verbatim with PREFIX expanded and WORKTREE substituted), run against the pre-change tree before any Phase 4 edit +Canonical command: CMD-LEGACY-CALLERS (primary content grep `Worker_RunWorkerCompleted|LoadRemainingEmailsToQueue\b|LoadRemainingEmailsToQueueAsync|Linked List Locking` over every *.cs outside packages, .claude, obj and bin; `\blog\b` over QuickFiler/Controllers/QfcDatamodel*.cs; the string-literal and reflection cross-check; the extension-unfiltered sweep; the IQfcDatamodel interface grep; the InternalsVisibleTo grep) +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- CS_FILES: 1707 (includes the new pin-count test file) +- QFCDATAMODEL_LINES: 495 (QFCDATAMODEL-LINES-BEFORE: 495, the AC28 before figure) +- PRIMARY_LINES: 25 +- LOG_LINES: 3 +- CROSS_LINES: 2 +- SWEEP_FILES: 98 (of which five are .cs files) +- INTERFACE_LINES: 0 +- INVOCATIONS: 0 + +## SWEEP-CS lines (exactly the five .cs files of fact 15) + +- SWEEP-CS \QuickFiler.Test\Controllers\QfcDatamodelLivenessTests.cs = 1 +- SWEEP-CS \QuickFiler.Test\Controllers\QfcHomeControllerRunAsyncTests.cs = 2 +- SWEEP-CS \QuickFiler.Test\Controllers\QfcInitEmailQueueZeroBatchTests.cs = 1 +- SWEEP-CS \QuickFiler\Controllers\QfcDatamodel.cs = 15 +- SWEEP-CS \QuickFiler\Controllers\QfcHomeController.cs = 4 + +## IVT lines (the four grants of fact 15; all four members are private, so no grant exposes them) + +- IVT \QuickFiler\Controllers\QfcHighConfidencePreFilter.cs:11 :: [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +- IVT \QuickFiler\Controllers\QfcHomeController.cs:15 :: [assembly: InternalsVisibleTo("QuickFiler.Test")] +- IVT \QuickFiler\Legacy\IAcceleratorCallbacks.cs:5 :: [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +- IVT \QuickFiler\Properties\AssemblyInfo.cs:5 :: [assembly: InternalsVisibleTo("QuickFiler.Test")] + +## Classification of every PRIMARY line + +| Line | Text | Category | +|---|---|---| +| QfcDatamodel.cs:40 | `RemainingEmailLoader = LoadRemainingEmailsToQueueAsync;` | METHOD-GROUP-ONE-ARG-OVERLOAD | +| QfcDatamodel.cs:52 | `RemainingEmailLoader = LoadRemainingEmailsToQueueAsync;` | METHOD-GROUP-ONE-ARG-OVERLOAD | +| QfcDatamodel.cs:130 | `/// constructors, below) to the single-argument ` | CREF-ONE-ARG-OVERLOAD | +| QfcDatamodel.cs:194 | `//worker.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(Worker_RunWorkerCompleted);` | COMMENTED-OUT | +| QfcDatamodel.cs:209 | `//e.Result = await LoadRemainingEmailsToQueueAsync(bw, _token);` | COMMENTED-OUT | +| QfcDatamodel.cs:210 | `//e.Result = LoadRemainingEmailsToQueue(bw, _token);` | COMMENTED-OUT | +| QfcDatamodel.cs:246 | `private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)` | DECLARATION | +| QfcDatamodel.cs:335 | `private async Task LoadRemainingEmailsToQueueAsync(CancellationToken cancel)` | DECLARATION (the surviving one-argument overload; not removed) | +| QfcDatamodel.cs:363 | `//logger.Debug($"{nameof(LoadRemainingEmailsToQueue)} Task cancelled");` | COMMENTED-OUT | +| QfcDatamodel.cs:369 | `$"{nameof(LoadRemainingEmailsToQueue)} Error. \n {e.Message}\n{e.StackTrace}"` | NAMEOF-RETARGETED | +| QfcDatamodel.cs:378 | `private bool LoadRemainingEmailsToQueue(BackgroundWorker bw, CancellationToken token)` | DECLARATION | +| QfcDatamodel.cs:404 | `//logger.Debug($"{nameof(LoadRemainingEmailsToQueue)} Task cancelled");` | SELF-REFERENCE (inside the removed synchronous method's own body) | +| QfcDatamodel.cs:410 | `$"{nameof(LoadRemainingEmailsToQueue)} Error. \n {e.Message}\n{e.StackTrace}"` | SELF-REFERENCE (inside the removed synchronous method's own body) | +| QfcDatamodel.cs:418 | `private async Task LoadRemainingEmailsToQueueAsync(` | DECLARATION (the two-argument overload) | +| QfcDatamodel.cs:462 | `//logger.Debug($"{nameof(LoadRemainingEmailsToQueueAsync)} Task cancelled");` | SELF-REFERENCE (inside the removed two-argument overload's own body) | +| QfcDatamodel.cs:469 | `#region Linked List Locking` | REGION-DIRECTIVE | +| QfcDatamodel.cs:472 | `#endregion Linked List Locking` | REGION-DIRECTIVE | +| QfcHomeController.cs:92 | `_formViewer.Worker.RunWorkerCompleted += Worker_RunWorkerCompleted;` | OTHER-TYPE-SAME-NAME | +| QfcHomeController.cs:132 | `_formViewer.Worker.RunWorkerCompleted += Worker_RunWorkerCompleted;` | OTHER-TYPE-SAME-NAME | +| QfcHomeController.cs:344 | `private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)` | OTHER-TYPE-SAME-NAME | +| QfcHomeController.cs:379 | `worker.RunWorkerCompleted -= Worker_RunWorkerCompleted;` | OTHER-TYPE-SAME-NAME | +| QfcDatamodelLivenessTests.cs:104 | `/// LoadRemainingEmailsToQueueAsync is still producing. The dequeue gate's` | DOC-PROSE | +| QfcHomeControllerRunAsyncTests.cs:325 | `public async System.Threading.Tasks.Task Worker_RunWorkerCompleted_HandlesCompletionCorrectly()` | OTHER-TYPE-SAME-NAME (a test of QfcHomeController) | +| QfcHomeControllerRunAsyncTests.cs:376 | `"Worker_RunWorkerCompleted",` | OTHER-TYPE-SAME-NAME (the reflective GetMethod is invoked on `_controller`, a QfcHomeController) | +| QfcInitEmailQueueZeroBatchTests.cs:28 | `/// LoadRemainingEmailsToQueueAsync, which pops a live` | DOC-PROSE | + +## Classification of every LOG line + +| Line | Text | Category | +|---|---|---| +| QfcDatamodel.cs:109 | `private static readonly log4net.ILog log = log4net.LogManager.GetLogger(` | DECLARATION | +| QfcDatamodel.QueueProcessing.cs:71 | `/// raising the log level, which is what the 37-minute silent gap in the field report needed.` | DOC-PROSE | +| QfcDatamodel.QueueProcessing.cs:90 | `/// at delegate-construction time, which is exactly the crash the field log records after a` | DOC-PROSE | + +## Classification of every CROSS line + +| Line | Text | Category | +|---|---|---| +| QfcDatamodel.cs:130 | `/// constructors, below) to the single-argument ` | CREF-ONE-ARG-OVERLOAD | +| QfcHomeControllerRunAsyncTests.cs:376 | `"Worker_RunWorkerCompleted",` | OTHER-TYPE-SAME-NAME | + +No line is classified `INVOCATION`. INVOCATIONS: 0. Every test-file hit is DOC-PROSE (QfcDatamodelLivenessTests.cs:104, QfcInitEmailQueueZeroBatchTests.cs:28) or OTHER-TYPE-SAME-NAME (QfcHomeControllerRunAsyncTests.cs:325 and 376), so no test references any of the four members. + +## Numeric Derivation Evidence + +- Complete Family: log, Worker_RunWorkerCompleted, LoadRemainingEmailsToQueue, LoadRemainingEmailsToQueueAsync-BackgroundWorker-overload +- Exhaustive Search Scope: every *.cs file in the item worktree outside packages, .claude, obj and bin (CS_FILES: 1707), plus an extension-unfiltered sweep of every file outside packages, .claude, obj, bin, .git, coverage and .dotnet-sdk for the method names (SWEEP_FILES: 98), plus QuickFiler/Interfaces/IQfcDatamodel.cs, both QfcDatamodel partial siblings, and the InternalsVisibleTo grants under QuickFiler/ +- Inclusion Rules: a member is counted as caller-free when every occurrence of its name outside its own declaration and body is a comment, a commented-out statement, a doc-comment reference, a reference to a different type's member of the same name, or a nameof symbol reference that is not an invocation and that the same edit retargets +- Exclusion Rules: members that implement an IQfcDatamodel interface member, members with any live invocation, event subscription, reflection-by-string lookup against QfcDatamodel, override, or designer wiring are excluded from the caller-free set; the one-argument LoadRemainingEmailsToQueueAsync(CancellationToken) overload is excluded because the constructors assign it to RemainingEmailLoader (lines 40 and 52, method-group conversions that bind the one-argument overload only) +- Primary Search Strategy or Query Expression: content grep `Worker_RunWorkerCompleted|LoadRemainingEmailsToQueue\b|LoadRemainingEmailsToQueueAsync|Linked List Locking` (case-sensitive) over every *.cs in scope, plus `\blog\b` over QuickFiler/Controllers/QfcDatamodel*.cs, each hit classified above +- Primary Member Set: log, Worker_RunWorkerCompleted, LoadRemainingEmailsToQueue, LoadRemainingEmailsToQueueAsync-BackgroundWorker-overload +- Primary Count: 4 +- Cross-check Search Strategy or Query Expression: the string-literal and reflection sweep `"Worker_RunWorkerCompleted"|"LoadRemainingEmailsToQueue|"log"|nameof\(log\)|GetField\("log` over every *.cs in scope (2 lines, classified above), the extension-unfiltered sweep `Worker_RunWorkerCompleted|LoadRemainingEmailsToQueue` (five .cs files, each matching fact 15), the IQfcDatamodel grep (0 lines) and the InternalsVisibleTo grep (four grants; all four members are private) +- Cross-check Member Set: log, Worker_RunWorkerCompleted, LoadRemainingEmailsToQueue, LoadRemainingEmailsToQueueAsync-BackgroundWorker-overload +- Cross-check Count: 4 +- Member-set Comparison: identical (the same four names in the same sense; both counts are 4) + +This is unreachable dead code with no behaviour to regress, so no failing test precedes its removal; the compile proof is the two rebuilds (P8-T3 and P8-T4), with the P4-T10 build as the first compile check. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/queue-processing-comment-census.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/queue-processing-comment-census.md new file mode 100644 index 000000000..2a69a75ee --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/queue-processing-comment-census.md @@ -0,0 +1,54 @@ +# Queue-processing comment census (issue #968, tasks P5-T11 and P5-T12) + +Timestamp: 2026-10-03T03-15 +Command: pwsh -NoProfile -Command '' with FILES `"QuickFiler\Controllers\QfcDatamodel.QueueProcessing.cs"` (the first of three payloads; then CMD-TOKEN-COUNT on QQP with the P0-T13 list and CMD-HUNKS on QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs; each the Command Reference macro executed verbatim with PREFIX expanded and WORKTREE substituted), followed by two git calls +Canonical command: as listed; git -C WORKTREE diff 94287369908cc920b21b0e3256314f988ad7d2f5 -- QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs; git -C WORKTREE status --porcelain -- QuickFiler +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 (every payload) +- LINES QuickFiler\Controllers\QfcDatamodel.QueueProcessing.cs = 413 +- QQP tokens (`RunWorkerAsync`, `written on the worker thread and read`, `written on the worker thread`, `(:31-66)`, `TryUnhookOrReplace`, `WorkerStarter`, `share no other fence`, `honest producer-liveness signal`, `() => _remainingLoadActive,`, `() => false,`, `private volatile bool _remainingLoadActive;`): 0, 0, 1, 0, 3, 1, 1, 0, 1, 0, 1 +- CMD-HUNKS: GIT_DIFF_EXIT_CODE: 0; `HUNK @@ -13,13 +13,13 @@ namespace QuickFiler.Controllers` (old range 13 to 25: starts at or above 10 and ends at or below 30); `HUNK @@ -282,7 +282,7 @@ namespace QuickFiler.Controllers` (old range 282 to 288: starts at or above 280 and ends at or below 292); HUNK_COUNT: 2 +- Every changed line in the transcribed diff below begins with `///` after its indentation (comment-only: the AC26 and AC20 reading); the declaration `private volatile bool _remainingLoadActive;` and every statement are unchanged. +- porcelain (exit 0): ` M QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs` and ` M QuickFiler/Controllers/QfcDatamodel.cs` + +QUIESCE-COMMENT-DECISION: UNCHANGED. Reason (D-20): the `QuiesceLoaderAsync` comment at line 52 (`the field is written on the worker thread`) concerns `_remainingLoadTask`, which `Worker_DoWork` writes at QfcDatamodel.cs (pre-edit line 218) on the thread that runs the handler; in production that is still the BackgroundWorker thread, because the production `WorkerStarter` calls `RunWorkerAsync()`, and the comment's snapshot rationale holds for any cross-thread writer. The comment is therefore accurate post-#950 and is left unchanged; `written on the worker thread` reads 1 (line 52) while the AC26 token `written on the worker thread and read` reads 0. + +## Transcribed diff (git -C WORKTREE diff 94287369908cc920b21b0e3256314f988ad7d2f5 -- QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs, exit 0) + +```diff +diff --git a/QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs b/QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs +index ce2bc0428..654a0bf4b 100644 +--- a/QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs ++++ b/QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs +@@ -13,13 +13,13 @@ namespace QuickFiler.Controllers + public partial class QfcDatamodel + { + /// +- /// Issue #424: honest producer-liveness signal. Set immediately before +- /// each RunWorkerAsync() call and cleared in a finally once the awaited +- /// RemainingEmailLoader completes. BackgroundWorker.IsBusy cannot serve this +- /// role: Worker_DoWork is async void, so it returns at its first yielding await +- /// and reports idle while the loader is still producing. Both the dequeue gate's +- /// sourceActive signal and consume this flag. Declared +- /// volatile because it is written on the worker thread and read by dequeue callers. ++ /// Issue #424 producer-liveness signal, read by the dequeue gate's sourceActive ++ /// delegate and by . InitEmailQueue sets it just before ++ /// handing the worker to , and Worker_DoWork clears it in ++ /// a finally when the awaited task completes, ++ /// because BackgroundWorker.IsBusy already reads idle at that handler's first ++ /// incomplete await (issue #950 made the start synchronous in tests, so no particular thread ++ /// owns either write). Volatile: the writers and the readers share no other fence. + /// + private volatile bool _remainingLoadActive; + +@@ -282,7 +282,7 @@ namespace QuickFiler.Controllers + /// is taken from the same accepted set as + /// , after has run + /// over it. #678 R1: that correspondence holds on the happy path only. On the +- /// UnhookItem throw path (:31-66) removes the failed ++ /// UnhookItem throw path removes the failed + /// item and inserts a substitute pulled from the master queue, so PreScored can name + /// an item absent from Items and Items can name an item absent from + /// PreScored. Leg A reconciles the two at the load boundary through +``` diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/scoped-format.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/scoped-format.md new file mode 100644 index 000000000..749cd9384 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/scoped-format.md @@ -0,0 +1,31 @@ +# Scoped format (issue #968, task P6-T1) + +Timestamp: 2026-10-03T03-16 +Command: pwsh -NoProfile -Command 'Set-Location -LiteralPath "WORKTREE"; [System.IO.Directory]::SetCurrentDirectory((Get-Location).Path); Write-Output ("WORKTREE-LEAF: " + (Split-Path -Leaf (Get-Location).Path)); dotnet tool run csharpier format QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixture.cs QuickFiler.Test\Controllers\QfcItemController.FocusAndThemeTests.cs QuickFiler.Test\Controllers\QfcItemController.TestSupport.cs QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherPinCountTests.cs QuickFiler.Test\Controllers\QfcDatamodelLivenessTests.cs QuickFiler.Test\Controllers\QfcDatamodelTeardownTests.cs QuickFiler.Test\Controllers\QfcInitEmailQueueZeroBatchTests.cs QuickFiler.Test\Controllers\QfcDatamodelTests.cs QuickFiler\Controllers\QfcDatamodel.cs QuickFiler\Controllers\QfcDatamodel.QueueProcessing.cs QuickFiler.Test\TestSupport\SynchronousBackgroundWorker.cs QuickFiler.Test\TestSupport\ArmingFakeTimeProvider.cs; "CSHARPIER_EXIT_CODE: $LASTEXITCODE"' +Canonical command: dotnet tool run csharpier format , preceded and followed by CMD-HASH on CS13 +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 (all three payloads) +- CSHARPIER_EXIT_CODE: 0 +- `Formatted 13 files in 9724ms.` (a processed-file count, not a rewrite count) +- REWRITTEN: QuickFiler.Test\Controllers\QfcDatamodelLivenessTests.cs, QuickFiler.Test\Controllers\QfcInitEmailQueueZeroBatchTests.cs, QuickFiler.Test\Controllers\QfcDatamodelTests.cs (the rewrite observation is the hash difference) + +Hashes before (CMD-HASH, exit 0) and after (CMD-HASH, exit 0): + +| File | Before | After | +|---|---|---| +| QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixture.cs | E22EAB9E3DBE8CBE79B9AD941FB8F9CF4679FCD2DA49287DF41EB06C90715A30 | E22EAB9E3DBE8CBE79B9AD941FB8F9CF4679FCD2DA49287DF41EB06C90715A30 | +| QuickFiler.Test\Controllers\QfcItemController.FocusAndThemeTests.cs | C8EBE51AD2810AEDD5B425100B12D99202D1EA8655A69D3F84004306BF6A053C | C8EBE51AD2810AEDD5B425100B12D99202D1EA8655A69D3F84004306BF6A053C | +| QuickFiler.Test\Controllers\QfcItemController.TestSupport.cs | 4EE3776F0C43AFB359B5B06734950E082822519E88B347B9F453D6B2814703FD | 4EE3776F0C43AFB359B5B06734950E082822519E88B347B9F453D6B2814703FD | +| QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs | 15B6334032EB4D8BD8DFC4AEACD943FE82857711584D1ECFB7CEA28A08ADB54C | 15B6334032EB4D8BD8DFC4AEACD943FE82857711584D1ECFB7CEA28A08ADB54C | +| QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherPinCountTests.cs | 0CE75AF833D376769D92875AAE467AE92107E02ADD8B7BA58FAC9A37190F7C32 | 0CE75AF833D376769D92875AAE467AE92107E02ADD8B7BA58FAC9A37190F7C32 | +| QuickFiler.Test\Controllers\QfcDatamodelLivenessTests.cs | 310E0DE66BF2FB90E53A28C3AA20CF7B5105D5E514898F7681D027686E3BF24A | 705AFA3C0383066D4469B5F3E229946A9DA7B57C06CE7BDC8892947D7593EE34 | +| QuickFiler.Test\Controllers\QfcDatamodelTeardownTests.cs | 1AD5B22646DE2AB6311D7CF864B1B52DB088839C47AB2615A79F0BCF4F942523 | 1AD5B22646DE2AB6311D7CF864B1B52DB088839C47AB2615A79F0BCF4F942523 | +| QuickFiler.Test\Controllers\QfcInitEmailQueueZeroBatchTests.cs | F62E707199464578315FF1BE6E1CA79F6E460DC115C6A75875288CE70CF0C181 | 17500EA75C8E7B442F77E853901A7B40E290F414E5016851079B159A9292DA6D | +| QuickFiler.Test\Controllers\QfcDatamodelTests.cs | 9DF350D6C90593D3D66A7E3556AC6E069CEC5C0A13B093A4492643A355D8DE8A | 5FF13DCBD54022AC4C298A7FA32151D3A5BA39AED7CB42AB06C894C40733FE6A | +| QuickFiler\Controllers\QfcDatamodel.cs | 7D9E620EF27B587E9A69667F13514E1A40144A5887CFAF5D7E0C08D0E1CA089D | 7D9E620EF27B587E9A69667F13514E1A40144A5887CFAF5D7E0C08D0E1CA089D | +| QuickFiler\Controllers\QfcDatamodel.QueueProcessing.cs | 577285E41E88EF7A64C15CFBFF69E2C3264BB3D92B51384F5B42DECF1CA3C732 | 577285E41E88EF7A64C15CFBFF69E2C3264BB3D92B51384F5B42DECF1CA3C732 | +| QuickFiler.Test\TestSupport\SynchronousBackgroundWorker.cs | 2AD708EA88DD1FEE83A30E72910F0D2E4A97A33B9B4D1BA4085ADA5D0CA4D65A | 2AD708EA88DD1FEE83A30E72910F0D2E4A97A33B9B4D1BA4085ADA5D0CA4D65A | +| QuickFiler.Test\TestSupport\ArmingFakeTimeProvider.cs | 6B45C5018B062F17C4C08B37F1938C9E64C78DD260F2F87FFA19881CBEA4E9E9 | 6B45C5018B062F17C4C08B37F1938C9E64C78DD260F2F87FFA19881CBEA4E9E9 | + +This is the first P6-T1 pass of the run (no D-13 restart), so PRIOR-PASS-REWRITTEN and RESTART-CORRECTED are not applicable. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/test-edit-census.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/test-edit-census.md new file mode 100644 index 000000000..82afd18c2 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/test-edit-census.md @@ -0,0 +1,27 @@ +# Test-edit census (issue #968, tasks P3-T1 to P3-T9) + +Timestamp: 2026-10-03T03-02 +Command: pwsh -NoProfile -Command '' with FILES = CS5 (the first of nine payloads; then CMD-TOKEN-COUNT on FAT, on TS with the P0-T12 TS list extended by "Issue #480 shared arrange helper", and on FT; CMD-SPAN-TOKEN-COUNT on R4SPAN, R4HEAD and R4TAIL; CMD-HUNKS on QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs and on QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs; each the Command Reference macro executed verbatim with PREFIX expanded and WORKTREE substituted), followed by two git calls +Canonical command: CMD-LINECOUNT, CMD-TOKEN-COUNT, CMD-SPAN-TOKEN-COUNT and CMD-HUNKS as listed; git -C WORKTREE diff --numstat HEAD -- QuickFiler.Test; git -C WORKTREE status --porcelain -- QuickFiler QuickFiler.Test +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 (every payload) +- LINES: FIX 375, FAT 482, TS 442, FT 472, PC 248 (PC at most 500) +- FAT tokens (`EnsureUiThreadDispatcher`, `private static Mock BuildExecutingViewer`, `QfcItemControllerTestSupport.BuildExecutingViewer()`, `BuildExecutingViewer`, `absorbs the delegate without running it`, `shared UiThread static is irrelevant`, `absorbs the queued application`, `[TestMethod]`): 0, 0, 8, 8, 1, 1, 1, 17 +- TS tokens (`Becomes moot`, `leaks exactly`, `still delegate to a callee`, `not reachable from another test file`, `remaining legitimate`, `QfcItemController_UiThreadDispatcherPinCountTests`, `internal static void EnsureSynchronizationContext()`, `UiThreadDispatcherFixture.EnsureDispatcher();`, `Issue #480 shared arrange helper`): 0, 0, 0, 0, 1, 1, 1, 1, 1 +- FT tokens (`no other class may dispose`, `removed that pin: the fixture now counts pins`, `(W5) must not latch`, `[Timeout(GateTimeoutMs)]`, `private const int GateTimeoutMs = 60000;`, `EnsureUiThreadDispatcher()`, `issue #230 lost update`, `the waiter cannot observe the pre-restore value`, `[TestMethod]`): 0, 1, 1, 8, 1, 3, 1, 1, 8 +- R4SPAN: SPAN: 212-286; `EnsureUiThreadDispatcher()` 0, `using (` 1, `transactionA.Dispose();` 2, `finally` 3, `.BeSameAs(` 1, `.NotBeSameAs(` 1, `issue #230 lost update` 1 +- R4HEAD: SPAN: 212-222; `EnsureUiThreadDispatcher()` 0, `using (` 0, `try` 2 +- R4TAIL: SPAN: 265-275; `}` 4, `finally` 2, `transactionA.Dispose();` 1 +- CMD-HUNKS TestSupport: GIT_DIFF_EXIT_CODE: 0; `HUNK @@ -214,25 +214,27 @@ namespace QuickFiler.Controllers.Tests`; `HUNK @@ -282,9 +284,9 @@ namespace QuickFiler.Controllers.Tests`; HUNK_COUNT: 2 (every old-range start at or above 200; the EnsureSynchronizationContext region 85 to 96 is untouched, AC17) +- CMD-HUNKS fixture tests: GIT_DIFF_EXIT_CODE: 0; `HUNK @@ -194,17 +194,17 @@`; `HUNK @@ -218,9 +218,7 @@`; `HUNK @@ -268,6 +266,10 @@`; HUNK_COUNT: 3 (old ranges 194-210, 218-226, 268-273: each starts at or above 190 and ends at or below 285, so every hunk lies in R4's doc and body, AC10) +- numstat (exit 0): `20 35 QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs`; `20 18 QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs`; `48 15 QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs`; `16 14 QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs`; `1 0 QuickFiler.Test/QuickFiler.Test.csproj` +- porcelain (exit 0), exactly the five #968 .cs paths (four ` M`, one `??`) and the project file: + - ` M QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs` + - ` M QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs` + - ` M QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs` + - ` M QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs` + - ` M QuickFiler.Test/QuickFiler.Test.csproj` + - `?? QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs` + +Per-task acceptance readings: P3-T1 private helper count 0 and the line after `BuildFocusController`'s closing brace and one blank line is the `/// ` of `EnableHandlelessThemeInvoke`; P3-T2 prefixed helper 8 and bare helper 8; P3-T3 `absorbs the delegate without running it` 1 and `shared UiThread static is irrelevant` 1 with `var controller = new FocusController();` as the first statement of SetThemeDark_FromNormal_SelectsDarkNormalTheme; P3-T4 `absorbs the queued application` 1 and FAT `EnsureUiThreadDispatcher` 0; P3-T5 and P3-T6 as the TS values above; P3-T7 as the FT values above; P3-T8 as the R4 span values above, with `[Timeout(GateTimeoutMs)]` 8 and the constant 1 unchanged. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/toolchain-final.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/toolchain-final.md new file mode 100644 index 000000000..cbb830112 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/toolchain-final.md @@ -0,0 +1,16 @@ +# Final toolchain pass (issue #968, task P8-T7) + +Timestamp: 2026-10-03T03-31 +ITERATION: 1 + +| Step | Exact command | EXIT_CODE | Key observations | ITERATION | Artifact | +|---|---|---|---|---|---| +| 1. csharpier format | `dotnet tool run csharpier format .` | 0 | REWRITTEN-WRITESET: NONE; REWRITTEN-OTHER: NONE | 1 | FEATURE/evidence/qa-gates/csharpier-format-final.md | +| 2. csharpier check | `dotnet tool run csharpier check .` | 0 | `Checked 1640 files in 4966ms.` | 1 | FEATURE/evidence/qa-gates/csharpier-check-final.md | +| 3. analyzer rebuild | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` | 0 | ERRORS 0; WARNINGS 0; SKIP_CORECOMPILE_LINES: 0 | 1 | FEATURE/evidence/qa-gates/msbuild-analyzer-final.md | +| 4. TreatWarningsAsErrors rebuild | `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` | 0 | ERRORS 0; SKIP_CORECOMPILE_LINES: 0 | 1 | FEATURE/evidence/qa-gates/msbuild-nullable-final.md | +| 5. coverage run (route DIRECT) | `dotnet-coverage collect --output coverage\final-968.cobertura.xml --output-format cobertura --settings coverage\effective-coverage-968.config -- vstest.console.exe ... "/TestCaseFilter:TestCategory!=LiveOutlook&"` then CMD-COVERAGE-POST | 0 | COVERAGE-ROUTE: DIRECT; RUNNER-GREEN: NO (COVERAGE-ROUTE DIRECT); Total 7365 passed 7365 failed 0; First-party coverage: lines 56211/65855 (85.36%), branches 13620/17078 (79.75%) | 1 | FEATURE/evidence/qa-gates/coverage-summary.md | + +SINGLE-PASS: YES (all five rows come from ITERATION 1 with no restart after P8-T1) + +AC22-STATUS: NOT MET (ENVIRONMENTAL: COVERAGE-ROUTE DIRECT). Every step passed in one uninterrupted pass, but the coverage step ran by the DIRECT route that P0-T16 selected (STALL-PROBE: REPRODUCES, one fast shell-icon test failure on this host), not the runner `scripts/vscode/Invoke-MSTestWithCoverage.ps1` verbatim, so RUNNER-GREEN is NO and AC22 cannot be met as worded. The decision belongs to the orchestrator (D-6). diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/concurrent-set-test-summary.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/concurrent-set-test-summary.md new file mode 100644 index 000000000..e2f7602db --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/concurrent-set-test-summary.md @@ -0,0 +1,48 @@ +# Concurrent set: the three #968 classes in one invocation (issue #968, task P6-T7) + +Timestamp: 2026-10-03T03-22 +Command: pwsh -NoProfile -Command '' with ASSEMBLY `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, FILTER-CONCURRENT (`FullyQualifiedName~QuickFiler.Controllers.Tests.QfcItemController_UiThreadDispatcherPinCountTests.|FullyQualifiedName~QuickFiler.Controllers.Tests.QfcItemController_UiThreadDispatcherFixtureTests.|FullyQualifiedName~QuickFiler.Controllers.Tests.QfcItemController_FocusAndThemeTests.`), TASKID p6-t7 and an empty NAMES list; the Command Reference CMD-VSTEST macro executed verbatim with PREFIX and TOOLS expanded and WORKTREE substituted +Canonical command: vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER-CONCURRENT" "/ResultsDirectory:coverage\test-results\968\p6-t7" "/Logger:trx;LogFileName=p6-t7.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- VSTEST_EXIT_CODE: 0 +- TRX_PRESENT: True +- SEQUENCE_FILES: 0 +- RESULT_COUNT: 29 +- COUNTERS total=29 executed=29 passed=29 failed=0 +- CONCURRENT-NOT-PASSED: NONE + +This is a supporting observation under the CLI runsettings (Workers 0, ClassLevel scope); MSTest cannot be made to interleave classes on demand, so it is not the regression gate. + +RESULT lines (all Passed): + +- ToggleTipsAsync_WithEmptyCollections_Completes = Passed duration=00:00:00.0010188 +- EnsureDispatcher_UnderATransactionHoldingALiveDispatcher_ReleasingAllPinsLeavesTheLiveDispatcher = Passed duration=00:00:00.0034677 +- EnsureDispatcher_WhileATransactionHoldsALiveDispatcher_DoesNotReplaceIt = Passed duration=00:00:00.0468345 +- ToggleFocusOnAsync_ActivatesUiAndSwitchesToActiveTheme = Passed duration=00:00:00.0015170 +- ToggleFocus_ParameterlessOverload_FromActive_DeactivatesUiAndSwitchesToNormalTheme = Passed duration=00:00:00.0007448 +- InvokeBeginInvoke_WhenAsync_UsesBeginInvoke = Passed duration=00:00:00.0011343 +- ToggleFocusOffAsync_DeactivatesUiAndSwitchesToNormalTheme = Passed duration=00:00:00.0007586 +- Transaction_SecondCallerCannotInstallUntilTheFirstRestores = Passed duration=00:00:00.0028801 +- SetThemeDark_FromNormal_SelectsDarkNormalTheme = Passed duration=00:00:00.0004370 +- HtmlDarkConverter_WhenWebViewNotInitialized_DoesNotNavigate = Passed duration=00:00:00.0005972 +- EnsureDispatcher_TwoPinsHeld_ReleaseOrderDoesNotChangeTheOutcome = Passed duration=00:00:00.0008154 +- Transaction_DisposedTwice_DoesNotOverReleaseTheGate = Passed duration=00:00:00.0019311 +- ToggleFocus_ParameterlessOverload_MarshalsThroughItemViewerInvoke = Passed duration=00:00:00.0010086 +- ToggleFocus_StateOverload_Off_FromActive_DeactivatesUiAndSwitchesToNormalTheme = Passed duration=00:00:00.0019170 +- EnsureDispatcher_ScopeDisposedTwice_IsIdempotent = Passed duration=00:00:00.0039081 +- InvokeBeginInvoke_WhenSynchronous_UsesInvoke = Passed duration=00:00:00.0004533 +- ToggleFocus_StateOverload_MarshalsThroughItemViewerInvoke = Passed duration=00:00:00.3533170 +- BeginTransactionAsync_ZeroBoundWhileThisTestHoldsThePermit_ThrowsTimeoutExceptionAndReleasesNothing = Passed duration=00:00:00.0509083 +- ToggleNavigationAsync_AwaitsPositionTipsToggleAsync = Passed duration=00:00:00.0011420 +- ToggleNavigation_WithState_TogglesPositionTipsWithState = Passed duration=00:00:00.0006716 +- EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease = Passed duration=00:00:00.0365535 +- Install_CalledTwiceOnTheSameTransaction_ThrowsInvalidOperationException = Passed duration=00:00:00.0050432 +- SetThemeLight_FromNormal_SelectsLightNormalTheme = Passed duration=00:00:00.0002907 +- EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores = Passed duration=00:00:00.0012340 +- ToggleNavigation_Synchronous_TogglesPositionTips = Passed duration=00:00:00.0050222 +- ToggleTips_Synchronous_DispatchesAndExecutesDelegate = Passed duration=00:00:00.0004073 +- ToggleSaveAttachments_DoesNotThrow = Passed duration=00:00:00.0003050 +- EnsureDispatcher_WhenTheFieldIsNull_InstallsAndRestoresOnDispose = Passed duration=00:00:00.0008321 +- TransactionGate_WhileThisTestHoldsATransaction_HasExactlyOneUnreleasedAcquisition = Passed duration=00:00:00.0024514 diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/datamodel-set-after-consolidation.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/datamodel-set-after-consolidation.md new file mode 100644 index 000000000..9440c036a --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/datamodel-set-after-consolidation.md @@ -0,0 +1,39 @@ +# Datamodel test set after the consolidation (issue #968, task P4-T11) + +Timestamp: 2026-10-03T03-08 +Command: pwsh -NoProfile -Command '' with ASSEMBLY `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, FILTER-DATAMODEL (`FullyQualifiedName~QuickFiler.Controllers.Tests.QfcDatamodelLivenessTests.|FullyQualifiedName~QuickFiler.Controllers.Tests.QfcDatamodelTeardownTests.|FullyQualifiedName~QuickFiler.Controllers.Tests.QfcInitEmailQueueZeroBatchTests.|FullyQualifiedName~QuickFiler.Controllers.Tests.QfcDatamodelTests.`), TASKID p4-t11 and an empty NAMES list; the Command Reference CMD-VSTEST macro executed verbatim with PREFIX and TOOLS expanded and WORKTREE substituted +Canonical command: vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER-DATAMODEL" "/ResultsDirectory:coverage\test-results\968\p4-t11" "/Logger:trx;LogFileName=p4-t11.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- VSTEST_EXIT_CODE: 0 +- TRX_PRESENT: True +- SEQUENCE_FILES: 0 +- COUNTERS total=21 executed=21 passed=21 failed=0 +- RESULT_COUNT: 21 + +RESULT lines: + +- Worker_DoWork_CapturesRemainingLoadTask = Passed duration=00:00:00.0008534 +- InitEmailQueue_PositiveBatchSize_RetainsExistingProjectionAndFrameDrop = Passed duration=00:00:00.1915728 +- DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive = Passed duration=00:00:00.0014286 +- RemainingLoadActive_AcrossAsyncVoidFirstAwait_StaysTrueWhileLoaderProduces = Passed duration=00:00:00.0008081 +- TryQueueRemainingMailItemAsync_HighConfidenceDisabled_AddsAndHooksWithoutScoring = Passed duration=00:00:00.0006964 +- WaitForQueue_WhenWorkerBusyAndQueueShort_AwaitsInjectedTwoHundredMsDelay = Passed duration=00:00:00.0012153 +- ToggleOfflineMode_WhenOnline_AwaitsInjectedFiveMillisecondDelay = Passed duration=00:00:00.0344447 +- Cleanup_CalledTwice_DoesNotThrow = Passed duration=00:00:00.0010439 +- InitEmailQueue_ZeroBatchSize_StillStartsBackgroundWorker = Passed duration=00:00:00.0030668 +- QfcRemainingQueueAdmission_DeclaresNoScoringDelegate = Passed duration=00:00:00.0077586 +- ScoreRemainingQueueMailItemAsync_ReturnsScoreAndTopFolder = Passed duration=00:00:00.0196485 +- QuiesceLoaderAsync_LoaderHangs_ReturnsAtBoundAndLogs = Passed duration=00:00:00.0013505 +- TryQueueRemainingMailItemAsync_NullMailItem_DoesNotScoreAddOrHook = Passed duration=00:00:00.0004574 +- QuiesceLoaderAsync_LoaderCompletes_ReturnsBeforeTimeout = Passed duration=00:00:00.0063493 +- TryQueueRemainingMailItemAsync_HighConfidenceEnabled_AddsAndHooksWithoutScoring = Passed duration=00:00:00.1253239 +- RemainingLoadActive_AfterLoaderCompletes_BecomesFalse = Passed duration=00:00:00.0018275 +- InitEmailQueue_ZeroBatchSize_ReturnsEmptyListWithoutThrowing = Passed duration=00:00:00.1374025 +- RemainingLoadActive_WhenLoaderThrows_IsStillClearedByFinally = Passed duration=00:00:00.0008401 +- TryQueueRemainingMailItemAsync_HighConfidenceEnabled_AddsBelowThresholdCandidate = Passed duration=00:00:00.0005637 +- DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle = Passed duration=00:00:00.1477616 +- TryQueueRemainingMailItemAsync_AfterCleanupNulledFields_ReturnsFalseWithoutThrowing = Passed duration=00:00:00.1223729 + +No MESSAGE lines; no new failure (no CONSOLIDATION BROKE A DATAMODEL TEST). diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/datamodel-set-test-summary.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/datamodel-set-test-summary.md new file mode 100644 index 000000000..d92493841 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/datamodel-set-test-summary.md @@ -0,0 +1,39 @@ +# Datamodel set: the four datamodel classes in one invocation (issue #968, task P6-T8) + +Timestamp: 2026-10-03T03-22 +Command: pwsh -NoProfile -Command '' with ASSEMBLY `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, FILTER-DATAMODEL (`FullyQualifiedName~QuickFiler.Controllers.Tests.QfcDatamodelLivenessTests.|FullyQualifiedName~QuickFiler.Controllers.Tests.QfcDatamodelTeardownTests.|FullyQualifiedName~QuickFiler.Controllers.Tests.QfcInitEmailQueueZeroBatchTests.|FullyQualifiedName~QuickFiler.Controllers.Tests.QfcDatamodelTests.`), TASKID p6-t8 and an empty NAMES list; the Command Reference CMD-VSTEST macro executed verbatim with PREFIX and TOOLS expanded and WORKTREE substituted +Canonical command: vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER-DATAMODEL" "/ResultsDirectory:coverage\test-results\968\p6-t8" "/Logger:trx;LogFileName=p6-t8.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- VSTEST_EXIT_CODE: 0 +- TRX_PRESENT: True +- SEQUENCE_FILES: 0 +- RESULT_COUNT: 21 +- COUNTERS total=21 executed=21 passed=21 failed=0 +- DATAMODEL-NOT-PASSED: NONE +- Both NAMES-LIVENESS tests are Passed (AC32; the pass-after half of AC25, AC30 and AC31). + +RESULT lines (all Passed): + +- RemainingLoadActive_AcrossAsyncVoidFirstAwait_StaysTrueWhileLoaderProduces = Passed duration=00:00:00.0006167 +- TryQueueRemainingMailItemAsync_HighConfidenceDisabled_AddsAndHooksWithoutScoring = Passed duration=00:00:00.0006287 +- InitEmailQueue_ZeroBatchSize_ReturnsEmptyListWithoutThrowing = Passed duration=00:00:00.1399477 +- TryQueueRemainingMailItemAsync_HighConfidenceEnabled_AddsAndHooksWithoutScoring = Passed duration=00:00:00.1335748 +- QuiesceLoaderAsync_LoaderCompletes_ReturnsBeforeTimeout = Passed duration=00:00:00.0063675 +- ScoreRemainingQueueMailItemAsync_ReturnsScoreAndTopFolder = Passed duration=00:00:00.0171499 +- InitEmailQueue_PositiveBatchSize_RetainsExistingProjectionAndFrameDrop = Passed duration=00:00:00.1885456 +- ToggleOfflineMode_WhenOnline_AwaitsInjectedFiveMillisecondDelay = Passed duration=00:00:00.0534466 +- TryQueueRemainingMailItemAsync_AfterCleanupNulledFields_ReturnsFalseWithoutThrowing = Passed duration=00:00:00.1319062 +- QfcRemainingQueueAdmission_DeclaresNoScoringDelegate = Passed duration=00:00:00.0069972 +- DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle = Passed duration=00:00:00.1568722 +- TryQueueRemainingMailItemAsync_NullMailItem_DoesNotScoreAddOrHook = Passed duration=00:00:00.0004617 +- RemainingLoadActive_AfterLoaderCompletes_BecomesFalse = Passed duration=00:00:00.0016046 +- InitEmailQueue_ZeroBatchSize_StillStartsBackgroundWorker = Passed duration=00:00:00.0033922 +- DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive = Passed duration=00:00:00.0014250 +- TryQueueRemainingMailItemAsync_HighConfidenceEnabled_AddsBelowThresholdCandidate = Passed duration=00:00:00.0007624 +- QuiesceLoaderAsync_LoaderHangs_ReturnsAtBoundAndLogs = Passed duration=00:00:00.0014760 +- WaitForQueue_WhenWorkerBusyAndQueueShort_AwaitsInjectedTwoHundredMsDelay = Passed duration=00:00:00.0014605 +- Cleanup_CalledTwice_DoesNotThrow = Passed duration=00:00:00.0009039 +- RemainingLoadActive_WhenLoaderThrows_IsStillClearedByFinally = Passed duration=00:00:00.0008538 +- Worker_DoWork_CapturesRemainingLoadTask = Passed duration=00:00:00.0007835 diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fail-before-build.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fail-before-build.md new file mode 100644 index 000000000..28245d789 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fail-before-build.md @@ -0,0 +1,15 @@ +# Fail-before build (issue #968, task P1-T4) + +Timestamp: 2026-10-03T02-56 +Command: pwsh -NoProfile -Command 'Set-Location -LiteralPath "WORKTREE"; [System.IO.Directory]::SetCurrentDirectory((Get-Location).Path); Write-Output ("WORKTREE-LEAF: " + (Split-Path -Leaf (Get-Location).Path)); $vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe"; $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1; $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1; New-Item -ItemType Directory -Path "coverage\logs" -Force | Out-Null; $log = "coverage\logs\p1-t4.msbuild.log"; if (Test-Path -LiteralPath $log) { Remove-Item -LiteralPath $log -Force }; $before = (Get-Item -LiteralPath "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll" -ErrorAction SilentlyContinue).LastWriteTimeUtc; $beforeProd = (Get-Item -LiteralPath "QuickFiler\bin\Debug\QuickFiler.dll" -ErrorAction SilentlyContinue).LastWriteTimeUtc; $global:LASTEXITCODE = 0; & $msbuild TaskMaster.sln /t:Build /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" "/flp:LogFile=$log;Verbosity=normal" | Out-Null; Write-Output ("MSBUILD_EXIT_CODE: " + $LASTEXITCODE); $lines = Get-Content -LiteralPath $log -Encoding UTF8; Write-Output ("ERRORS: " + (($lines | Select-String -Pattern "^\s*(\d+) Error\(s\)" | Select-Object -Last 1).Matches[0].Groups[1].Value)); $after = (Get-Item -LiteralPath "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll").LastWriteTimeUtc; $afterProd = (Get-Item -LiteralPath "QuickFiler\bin\Debug\QuickFiler.dll").LastWriteTimeUtc; Write-Output ("TEST_DLL_ADVANCED: " + ($null -eq $before -or $after -gt $before)); Write-Output ("PROD_DLL_ADVANCED: " + ($null -eq $beforeProd -or $afterProd -gt $beforeProd)); Write-Output ("CSC_OUT_QUICKFILER: " + @($lines | Where-Object { $_.Contains("/out:obj\Debug\QuickFiler.dll") }).Count); Write-Output ("CSC_OUT_QUICKFILER_TEST: " + @($lines | Where-Object { $_.Contains("/out:obj\Debug\QuickFiler.Test.dll") }).Count)' (CMD-BUILD, TASKID p1-t4; newline separators shown as semicolons) +Canonical command: msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" (resolved through vswhere against WORKTREE/TaskMaster.sln) +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- MSBUILD_EXIT_CODE: 0 +- ERRORS: 0 +- TEST_DLL_ADVANCED: True +- PROD_DLL_ADVANCED: False (recorded; no production file changed in this task) +- CSC_OUT_QUICKFILER: 0 (recorded) +- CSC_OUT_QUICKFILER_TEST: 2 +- The new pin-count test file compiles against the unmodified fixture. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fail-before-exception.2026-10-03T03-09.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fail-before-exception.2026-10-03T03-09.md new file mode 100644 index 000000000..873e982fb --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fail-before-exception.2026-10-03T03-09.md @@ -0,0 +1,27 @@ +# Fail-before exception dossier: the two dequeue-liveness tests (issue #968, task P5-T1) + +Timestamp: 2026-10-03T03-09 +Command: pwsh -NoProfile -Command '' with FILE `QuickFiler.Test\Controllers\QfcDatamodelLivenessTests.cs`, START `public async Task DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle()`, END `/// Reads the issue #424 producer-liveness flag by reflection.` and the P0-T13 T1-LIVE token list; then pwsh -NoProfile -Command '' with FILE `QuickFiler.Test\Controllers\QfcDatamodelTests.cs`, START `public async Task DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive()`, END `public async Task TryQueueRemainingMailItemAsync_HighConfidenceEnabled_AddsBelowThresholdCandidate()` and the P0-T13 T-SIB token list (both the Command Reference macro executed verbatim with PREFIX expanded and WORKTREE substituted, before any Phase 5 edit) +Canonical command: CMD-SPAN-TOKEN-COUNT on T1-LIVE and on T-SIB +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 (both payloads) +- T1-LIVE: SPAN: 96-151; `await` 5, `using (NoSynchronizationContext())` 0, `Task.Yield` 3, `fake.Advance` 3, `for (int i` 1, `clock.ReArm();` 0, `(await pending)` 1 +- T-SIB: SPAN: 97-134; `await Task.Yield();` 1, `clock.ReArm();` 0, `await Task.WhenAny(clock.Armed, pending)` 0, `using (var worker = new BackgroundWorker())` 0, `IList result = await pending;` 1 +- The old shapes are still on disk: T1-LIVE `Task.Yield` 3, `fake.Advance` 3, `for (int i` 1, and T-SIB `await Task.Yield();` 1 (the constructs whose removal AC31 requires). + +WhyFailingRunImpossible: The old tests fail only when the thread pool delays a queued continuation past the bounded retry loop or past the second clock advance, which no test input can force, and the production behaviour under test is correct both before and after the change (addendum section 5.5). A deterministic failing run of the old shape therefore cannot be produced on demand. + +## Alternative proof + +(i) Mechanism reading. Addendum section 5.3, read with the web-verified timer facts of section 5.2: `FakeTimeProvider.Advance` invokes due timer callbacks synchronously, `TimeProvider.Delay` reaches the overridable `CreateTimer`, and the gate's `ConfigureAwait(false)` continuation runs inline on the advancing thread only when that thread carries no derived synchronization context. On the queued path the second `Advance` can run before the gate re-arms its timer, so the advance is lost; `await Task.Yield()` gives no ordering guarantee relative to a queued pool work item; and the `for (int i = 0; i < 20 && !pending.IsCompleted; i++)` loop is a bounded retry whose success depends on pool scheduling. The sibling test in `QfcDatamodelTests` has the same first two steps without the retry loop, so on the queued path its `await pending` waits indefinitely rather than failing. + +(ii) Labelled sensitivity check. Task P5-T8 temporarily edits the `sourceActive` lambda in `QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs` to `() => false,`, builds, runs both rewritten tests by fully qualified name and requires each to fail on its re-arm assertion, then reverts the edit and proves the revert. That check is evidence of the new tests' sensitivity to a dishonest liveness signal, not a fail-before of the old tests, and is recorded separately in FEATURE/evidence/regression-testing/liveness-sensitivity-check.md. + +## Negative evidence (search for a failing-run artifact of these two tests) + +- SearchScope: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/ (the feature is not versioned, so the feature-root folder is the only scope) +- SearchPatterns: liveness-*.md, fail-before-*.md +- SearchResult: liveness-*.md: none. fail-before-*.md: fail-before-build.md and fail-before-pin-count.md, both of which belong to the pin-count regression test (tasks P1-T4 and P1-T5) and record no run of `DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle` or `DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive`. No failing-run artifact exists for these two tests. + +After this task exactly one file matching `fail-before-exception.*.md` exists in FEATURE/evidence/regression-testing/ (this file). diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fail-before-pin-count.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fail-before-pin-count.md new file mode 100644 index 000000000..ac12cb463 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fail-before-pin-count.md @@ -0,0 +1,21 @@ +# Fail-before: pin-count regression test on the unmodified fixture (issue #968, task P1-T5, expect-fail) + +Timestamp: 2026-10-03T02-57 +Command: pwsh -NoProfile -Command '' with ASSEMBLY `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, FILTER `FullyQualifiedName=QuickFiler.Controllers.Tests.QfcItemController_UiThreadDispatcherPinCountTests.EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease` (FILTER-PC-T1), TASKID p1-t5 and NAMES `"EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease"`; the Command Reference CMD-VSTEST macro executed verbatim with PREFIX and TOOLS expanded and WORKTREE substituted +Canonical command: vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER-PC-T1" "/ResultsDirectory:coverage\test-results\968\p1-t5" "/Logger:trx;LogFileName=p1-t5.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 1 +ExpectedExitCode: 1 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- VSTEST_EXIT_CODE: 1 (the deliberate fail-before outcome) +- TRX_PRESENT: True +- SEQUENCE_FILES: 0 +- COUNTERS total=1 executed=1 passed=0 failed=1 +- RESULT_COUNT: 1 +- RESULT EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease = Failed duration=00:00:00.1765029 +- MESSAGE EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease :: Expected afterFirstRelease to refer to System.Windows.Threading.Dispatcher { HasShutdownFinished = False, HasShutdownStarted = False, Hooks = System.Windows.Threading.DispatcherHooks{ }, Thread = System.Threading.Thread { ApartmentState = ApartmentState.STA {value: 0}, CurrentCulture = en-US, CurrentUICulture = en-US, ExecutionContext = System.Threading.ExecutionContext{ }, IsAlive = True, IsBackground = True, IsThreadPoolThread = False, ManagedThreadId = 36, Name = "UiThreadDispatcherFixture.ParkedDispatcher", Priority = ThreadPriority.Normal {value: 2}, ThreadState = ThreadState.Background|WaitSleepJoin {value: 36} } } because a holder that did not take the last pin must not lose the dispatcher, but found . +- The MESSAGE contains `to refer to`, `ParkedDispatcher`, `a holder that did not take the last pin must not lose the dispatcher` and `but found `: the first-release assertion failed because the first pin's release nulled the field (fact 13). Not REGRESSION DID NOT FAIL and not FAIL-BEFORE WRONG REASON. + +Fixture state: the fixture is at BASE content. Second payload, `pwsh -NoProfile -Command ''` with FILES `"QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixture.cs"` (exit 0, WORKTREE-LEAF agent-a291a7fbabf9d0229), printed `HASH QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixture.cs = 1BB53DBE378F6E6B69C42D9234061465CAD9CE79002E2AE9CF8004731449EFA6`, equal to the P0-T12 BASE-HASH for FIX. + +The test ran alone by fully qualified name, so the shared static started from a null baseline (fact 7: SetupAssemblyInitializer does not write it). This run is the fail-before half of AC5. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fixture-class-pass-after.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fixture-class-pass-after.md new file mode 100644 index 000000000..1d5b7fc8b --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fixture-class-pass-after.md @@ -0,0 +1,22 @@ +# Fixture test class pass-after (issue #968, task P6-T5) + +Timestamp: 2026-10-03T03-21 +Command: pwsh -NoProfile -Command '' with ASSEMBLY `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, FILTER-FT-CLASS (`FullyQualifiedName~QuickFiler.Controllers.Tests.QfcItemController_UiThreadDispatcherFixtureTests.`), TASKID p6-t5 and NAMES-FT; the Command Reference CMD-VSTEST macro executed verbatim with PREFIX and TOOLS expanded and WORKTREE substituted +Canonical command: vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER-FT-CLASS" "/ResultsDirectory:coverage\test-results\968\p6-t5" "/Logger:trx;LogFileName=p6-t5.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- VSTEST_EXIT_CODE: 0 +- TRX_PRESENT: True +- SEQUENCE_FILES: 0 +- COUNTERS total=8 executed=8 passed=8 failed=0 +- RESULT_COUNT: 8 +- RESULT EnsureDispatcher_WhileATransactionHoldsALiveDispatcher_DoesNotReplaceIt = Passed duration=00:00:00.0694314 +- RESULT EnsureDispatcher_WhenTheFieldIsNull_InstallsAndRestoresOnDispose = Passed duration=00:00:00.0010332 +- RESULT Transaction_DisposedTwice_DoesNotOverReleaseTheGate = Passed duration=00:00:00.0015689 +- RESULT EnsureDispatcher_ScopeDisposedTwice_IsIdempotent = Passed duration=00:00:00.0031842 +- RESULT TransactionGate_WhileThisTestHoldsATransaction_HasExactlyOneUnreleasedAcquisition = Passed duration=00:00:00.0019584 +- RESULT Transaction_SecondCallerCannotInstallUntilTheFirstRestores = Passed duration=00:00:00.0029685 +- RESULT BeginTransactionAsync_ZeroBoundWhileThisTestHoldsThePermit_ThrowsTimeoutExceptionAndReleasesNothing = Passed duration=00:00:00.0457345 +- RESULT Install_CalledTwiceOnTheSameTransaction_ThrowsInvalidOperationException = Passed duration=00:00:00.0042058 +- R1 to R6 and the #743 and #882 tests pass with their assertions unchanged (AC10); R4 passes without its pin and with the try/finally (AC14). diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/focus-and-theme-class-pass-after.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/focus-and-theme-class-pass-after.md new file mode 100644 index 000000000..6d2a4bbf6 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/focus-and-theme-class-pass-after.md @@ -0,0 +1,16 @@ +# Focus-and-theme class pass-after (issue #968, task P6-T6) + +Timestamp: 2026-10-03T03-21 +Command: pwsh -NoProfile -Command '' with ASSEMBLY `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, FILTER-FAT-CLASS (`FullyQualifiedName~QuickFiler.Controllers.Tests.QfcItemController_FocusAndThemeTests.`), TASKID p6-t6 and NAMES-THEME; the Command Reference CMD-VSTEST macro executed verbatim with PREFIX and TOOLS expanded and WORKTREE substituted +Canonical command: vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER-FAT-CLASS" "/ResultsDirectory:coverage\test-results\968\p6-t6" "/Logger:trx;LogFileName=p6-t6.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- VSTEST_EXIT_CODE: 0 +- TRX_PRESENT: True +- SEQUENCE_FILES: 0 +- COUNTERS total=17 executed=17 passed=17 failed=0 +- RESULT_COUNT: 17 +- RESULT SetThemeLight_FromNormal_SelectsLightNormalTheme = Passed duration=00:00:00.0002807 +- RESULT SetThemeDark_FromNormal_SelectsDarkNormalTheme = Passed duration=00:00:00.0004704 +- `passed=17 failed=0`: every test in the class passes after the helper switch (AC15) and the theme tests pass without the deleted calls (AC7). diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fold-build.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fold-build.md new file mode 100644 index 000000000..8aaf40f86 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fold-build.md @@ -0,0 +1,15 @@ +# Fold build (issue #968, task P4-T10) + +Timestamp: 2026-10-03T03-08 +Command: pwsh -NoProfile -Command '' with TASKID p4-t10; the payload is identical, line for line, to the CMD-BUILD payload transcribed in full in FEATURE/evidence/regression-testing/fail-before-build.md except that the log is `coverage\logs\p4-t10.msbuild.log` +Canonical command: msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" (resolved through vswhere against WORKTREE/TaskMaster.sln) +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- MSBUILD_EXIT_CODE: 0 +- ERRORS: 0 +- TEST_DLL_ADVANCED: True +- PROD_DLL_ADVANCED: True +- CSC_OUT_QUICKFILER: 2 +- CSC_OUT_QUICKFILER_TEST: 2 +- First compile proof that no surviving code referenced a removed QfcDatamodel member: no CS0103 or CS0117 (ERRORS 0). diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/implementation-build.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/implementation-build.md new file mode 100644 index 000000000..9239adef8 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/implementation-build.md @@ -0,0 +1,14 @@ +# Implementation build (issue #968, task P6-T3) + +Timestamp: 2026-10-03T03-20 +Command: pwsh -NoProfile -Command '' with TASKID p6-t3; the payload is identical, line for line, to the CMD-BUILD payload transcribed in full in FEATURE/evidence/regression-testing/fail-before-build.md except that the log is `coverage\logs\p6-t3.msbuild.log` +Canonical command: msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" (resolved through vswhere against WORKTREE/TaskMaster.sln) +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- MSBUILD_EXIT_CODE: 0 +- ERRORS: 0 +- TEST_DLL_ADVANCED: True +- PROD_DLL_ADVANCED: True (recorded: P6-T1 REWRITTEN named no production file; the production assembly advanced because the P5-T11 comment-only edit of QfcDatamodel.QueueProcessing.cs followed the P5-T9 build) +- CSC_OUT_QUICKFILER: 2 +- CSC_OUT_QUICKFILER_TEST: 2 diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/liveness-build.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/liveness-build.md new file mode 100644 index 000000000..8349031d2 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/liveness-build.md @@ -0,0 +1,15 @@ +# Liveness build (issue #968, task P5-T6) + +Timestamp: 2026-10-03T03-12 +Command: pwsh -NoProfile -Command '' with TASKID p5-t6; the payload is identical, line for line, to the CMD-BUILD payload transcribed in full in FEATURE/evidence/regression-testing/fail-before-build.md except that the log is `coverage\logs\p5-t6.msbuild.log` +Canonical command: msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" (resolved through vswhere against WORKTREE/TaskMaster.sln) +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- MSBUILD_EXIT_CODE: 0 +- ERRORS: 0 +- TEST_DLL_ADVANCED: True +- PROD_DLL_ADVANCED: False (recorded; no production file changed in this task) +- CSC_OUT_QUICKFILER: 0 (recorded) +- CSC_OUT_QUICKFILER_TEST: 2 +- W2, L-T1, L-SCOPE and M-T compile. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/liveness-pass-after-revert.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/liveness-pass-after-revert.md new file mode 100644 index 000000000..877ed4589 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/liveness-pass-after-revert.md @@ -0,0 +1,16 @@ +# Liveness pass-after on the reverted tree (issue #968, task P5-T10) + +Timestamp: 2026-10-03T03-14 +Command: pwsh -NoProfile -Command '' with ASSEMBLY `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, FILTER-LIVENESS-PAIR, TASKID p5-t10 and NAMES-LIVENESS; the Command Reference CMD-VSTEST macro executed verbatim with PREFIX and TOOLS expanded and WORKTREE substituted +Canonical command: vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER-LIVENESS-PAIR" "/ResultsDirectory:coverage\test-results\968\p5-t10" "/Logger:trx;LogFileName=p5-t10.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- VSTEST_EXIT_CODE: 0 +- TRX_PRESENT: True +- SEQUENCE_FILES: 0 +- COUNTERS total=2 executed=2 passed=2 failed=0 +- RESULT_COUNT: 2 +- RESULT DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle = Passed duration=00:00:00.1643258 +- RESULT DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive = Passed duration=00:00:00.1628127 +- Confirming run that the P5-T7 outcome is reproduced after the sensitivity cycle; the P5-T7 run is the measured one. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/liveness-pass-after.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/liveness-pass-after.md new file mode 100644 index 000000000..b3ecb7ad4 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/liveness-pass-after.md @@ -0,0 +1,16 @@ +# Liveness pass-after (issue #968, task P5-T7) + +Timestamp: 2026-10-03T03-12 +Command: pwsh -NoProfile -Command '' with ASSEMBLY `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, FILTER `FullyQualifiedName=QuickFiler.Controllers.Tests.QfcDatamodelLivenessTests.DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle|FullyQualifiedName=QuickFiler.Controllers.Tests.QfcDatamodelTests.DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive` (FILTER-LIVENESS-PAIR), TASKID p5-t7 and NAMES-LIVENESS; the Command Reference CMD-VSTEST macro executed verbatim with PREFIX and TOOLS expanded and WORKTREE substituted +Canonical command: vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER-LIVENESS-PAIR" "/ResultsDirectory:coverage\test-results\968\p5-t7" "/Logger:trx;LogFileName=p5-t7.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- VSTEST_EXIT_CODE: 0 +- TRX_PRESENT: True +- SEQUENCE_FILES: 0 +- COUNTERS total=2 executed=2 passed=2 failed=0 +- RESULT_COUNT: 2 +- RESULT DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive = Passed duration=00:00:00.1623663 +- RESULT DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle = Passed duration=00:00:00.1602447 +- This is the measured pass-after run of the two rewritten tests. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/liveness-revert-build.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/liveness-revert-build.md new file mode 100644 index 000000000..5e1362fa5 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/liveness-revert-build.md @@ -0,0 +1,14 @@ +# Liveness revert build (issue #968, task P5-T9) + +Timestamp: 2026-10-03T03-14 +Command: pwsh -NoProfile -Command '' with TASKID p5-t9; the payload is identical, line for line, to the CMD-BUILD payload transcribed in full in FEATURE/evidence/regression-testing/fail-before-build.md except that the log is `coverage\logs\p5-t9.msbuild.log` +Canonical command: msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" (resolved through vswhere against WORKTREE/TaskMaster.sln) +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- MSBUILD_EXIT_CODE: 0 +- ERRORS: 0 +- TEST_DLL_ADVANCED: True +- PROD_DLL_ADVANCED: True (the sensitivity binary is replaced) +- CSC_OUT_QUICKFILER: 2 +- CSC_OUT_QUICKFILER_TEST: 2 diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/liveness-sensitivity-check.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/liveness-sensitivity-check.md new file mode 100644 index 000000000..9e159c088 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/liveness-sensitivity-check.md @@ -0,0 +1,27 @@ +# Liveness sensitivity check (issue #968, task P5-T8, expect-fail) + +## Labelled sensitivity check (not a fail-before of the old tests) + +Timestamp: 2026-10-03T03-13 +Command: pwsh -NoProfile -Command '' with ASSEMBLY `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, FILTER-LIVENESS-PAIR (`FullyQualifiedName=QuickFiler.Controllers.Tests.QfcDatamodelLivenessTests.DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle|FullyQualifiedName=QuickFiler.Controllers.Tests.QfcDatamodelTests.DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive`), TASKID p5-t8 and NAMES-LIVENESS (step 4; the Command Reference CMD-VSTEST macro executed verbatim with PREFIX and TOOLS expanded and WORKTREE substituted) +Canonical command: vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER-LIVENESS-PAIR" "/ResultsDirectory:coverage\test-results\968\p5-t8" "/Logger:trx;LogFileName=p5-t8.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 1 +ExpectedExitCode: 1 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 in every payload of steps 2, 3, 4 and 6 +- Step (1): Edit tool replaced the line ` () => _remainingLoadActive,` in QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs with ` () => false,` (working copy only) +- Step (2): CMD-SPAN-TOKEN-COUNT on GATE-LAMBDA (exit 0): SPAN: 299-310; `() => _remainingLoadActive,` 0; `() => false,` 1 (edit applied) +- Step (3): CMD-BUILD with TASKID p5-t8 (exit 0): MSBUILD_EXIT_CODE: 0; ERRORS: 0; TEST_DLL_ADVANCED: True; PROD_DLL_ADVANCED: True; CSC_OUT_QUICKFILER: 2; CSC_OUT_QUICKFILER_TEST: 2 +- Step (4): VSTEST_EXIT_CODE: 1; TRX_PRESENT: True; SEQUENCE_FILES: 0; COUNTERS total=2 executed=2 passed=0 failed=2; RESULT_COUNT: 2 + - RESULT DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive = Failed duration=00:00:00.2568927 + - RESULT DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle = Failed duration=00:00:00.2589193 + - MESSAGE DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive :: Expected first to refer to System.Threading.Tasks.Task`1[System.Boolean] {Status=WaitingForActivation} because the datamodel source-active signal must keep polling while the worker can still add candidates, but found System.Threading.Tasks.Task`1[System.Collections.Generic.IList`1[Microsoft.Office.Interop.Outlook.MailItem]] {Status=RanToCompletion}. + - MESSAGE DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle :: Expected first to refer to System.Threading.Tasks.Task`1[System.Boolean] {Status=WaitingForActivation} because the loader is still producing, so the gate must arm a second wait rather than treat an empty queue as an exhausted source and return an early partial batch, but found System.Threading.Tasks.Task`1[System.Collections.Generic.IList`1[Microsoft.Office.Interop.Outlook.MailItem]] {Status=RanToCompletion}. + - Both outcomes are `Failed` (not Timeout, not Aborted). The Liveness MESSAGE contains `to refer to` and `the gate must arm a second wait`; the sibling MESSAGE contains `to refer to` and `must keep polling while the worker can still add candidates`. Each test failed on its re-arm assertion, so the new shape is sensitive to a dishonest liveness signal and fails crisply rather than hanging. +- Step (5): Edit tool restored the line to ` () => _remainingLoadActive,` +- Step (6): CMD-SPAN-TOKEN-COUNT on GATE-LAMBDA (exit 0): SPAN: 299-310; `() => _remainingLoadActive,` 1; `() => false,` 0 (edit reverted) +- Step (7): git -C WORKTREE diff --exit-code 94287369908cc920b21b0e3256314f988ad7d2f5 -- QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs -> exit 0, no output (the file is byte-identical to BASE) +- Step (8): git -C WORKTREE status --porcelain -- QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs -> exit 0, prints nothing for that path +- SENSITIVITY-EDIT-REVERTED: YES + +The temporary edit was never staged or committed: it existed only in the working copy between steps (1) and (5), and steps (7) and (8) prove the file is at BASE content with no working-copy change. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/pass-after-build.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/pass-after-build.md new file mode 100644 index 000000000..9ff2e8a57 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/pass-after-build.md @@ -0,0 +1,14 @@ +# Pass-after build (issue #968, task P2-T7) + +Timestamp: 2026-10-03T02-59 +Command: pwsh -NoProfile -Command '' with TASKID p2-t7; the payload is identical, line for line, to the CMD-BUILD payload transcribed in full in FEATURE/evidence/regression-testing/fail-before-build.md except that the log is `coverage\logs\p2-t7.msbuild.log` +Canonical command: msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" (resolved through vswhere against WORKTREE/TaskMaster.sln) +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- MSBUILD_EXIT_CODE: 0 +- ERRORS: 0 +- TEST_DLL_ADVANCED: True +- PROD_DLL_ADVANCED: False (recorded; no production file changed) +- CSC_OUT_QUICKFILER: 0 (recorded) +- CSC_OUT_QUICKFILER_TEST: 2 diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/pass-after-pin-count.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/pass-after-pin-count.md new file mode 100644 index 000000000..5b68fe95c --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/pass-after-pin-count.md @@ -0,0 +1,26 @@ +# Pass-after: pin-count tests on the fixed fixture (issue #968, task P2-T8) + +Timestamp: 2026-10-03T03-00 +Command: pwsh -NoProfile -Command '' with ASSEMBLY `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, FILTER `FullyQualifiedName~QuickFiler.Controllers.Tests.QfcItemController_UiThreadDispatcherPinCountTests.` (FILTER-PC-CLASS), TASKID p2-t8 and NAMES-PC; the Command Reference CMD-VSTEST macro executed verbatim with PREFIX and TOOLS expanded and WORKTREE substituted; followed by git -C WORKTREE status --porcelain -- QuickFiler QuickFiler.Test +Canonical command: vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER-PC-CLASS" "/ResultsDirectory:coverage\test-results\968\p2-t8" "/Logger:trx;LogFileName=p2-t8.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- VSTEST_EXIT_CODE: 0 +- TRX_PRESENT: True +- SEQUENCE_FILES: 0 +- COUNTERS total=4 executed=4 passed=4 failed=0 +- RESULT_COUNT: 4 +- RESULT EnsureDispatcher_UnderATransactionHoldingALiveDispatcher_ReleasingAllPinsLeavesTheLiveDispatcher = Passed duration=00:00:00.0139703 +- RESULT EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease = Passed duration=00:00:00.0554082 +- RESULT EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores = Passed duration=00:00:00.0009809 +- RESULT EnsureDispatcher_TwoPinsHeld_ReleaseOrderDoesNotChangeTheOutcome = Passed duration=00:00:00.0009174 + +PHASE2-PORCELAIN (git status --porcelain -- QuickFiler QuickFiler.Test, exit 0): +``` + M QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs + M QuickFiler.Test/QuickFiler.Test.csproj +?? QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs +``` + +PHASE2-PORCELAIN equals PHASE1-PORCELAIN (FEATURE/evidence/regression-testing/pin-count-file-census.md) plus exactly one extra line, ` M QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs`. AC5 statement: the only difference between the P1-T5 fail-before run and this pass-after run is the fixture file. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/pin-count-class-pass-after.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/pin-count-class-pass-after.md new file mode 100644 index 000000000..c138a6f9c --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/pin-count-class-pass-after.md @@ -0,0 +1,17 @@ +# Pin-count class pass-after (issue #968, task P6-T4) + +Timestamp: 2026-10-03T03-20 +Command: pwsh -NoProfile -Command '' with ASSEMBLY `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, FILTER-PC-CLASS (`FullyQualifiedName~QuickFiler.Controllers.Tests.QfcItemController_UiThreadDispatcherPinCountTests.`), TASKID p6-t4 and NAMES-PC; the Command Reference CMD-VSTEST macro executed verbatim with PREFIX and TOOLS expanded and WORKTREE substituted +Canonical command: vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER-PC-CLASS" "/ResultsDirectory:coverage\test-results\968\p6-t4" "/Logger:trx;LogFileName=p6-t4.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- VSTEST_EXIT_CODE: 0 +- TRX_PRESENT: True +- SEQUENCE_FILES: 0 +- COUNTERS total=4 executed=4 passed=4 failed=0 +- RESULT_COUNT: 4 +- RESULT EnsureDispatcher_TwoPinsHeld_ReleaseOrderDoesNotChangeTheOutcome = Passed duration=00:00:00.0008074 +- RESULT EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease = Passed duration=00:00:00.0558775 +- RESULT EnsureDispatcher_UnderATransactionHoldingALiveDispatcher_ReleasingAllPinsLeavesTheLiveDispatcher = Passed duration=00:00:00.0139719 +- RESULT EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores = Passed duration=00:00:00.0009991 diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/pin-count-file-census.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/pin-count-file-census.md new file mode 100644 index 000000000..7430ace21 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/pin-count-file-census.md @@ -0,0 +1,20 @@ +# Pin-count test file census (issue #968, tasks P1-T1, P1-T2 and P1-T3) + +Timestamp: 2026-10-03T02-56 +Command: pwsh -NoProfile -Command '' with FILE = `QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherPinCountTests.cs` (PC), the Command Reference macro executed verbatim with PREFIX expanded and WORKTREE substituted; followed by CMD-TOKEN-COUNT on PC and on PROJ (separate payloads) and two git calls +Canonical command: CMD-EOL on PC; CMD-TOKEN-COUNT on PC and PROJ; git -C WORKTREE diff --numstat HEAD -- QuickFiler.Test/QuickFiler.Test.csproj; git -C WORKTREE status --porcelain -- QuickFiler QuickFiler.Test +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 (every payload) +- P1-T1 CMD-EOL: BARE_LF: 0; CRLF_COUNT: 248; LINES: 248 (CRLF_COUNT equals LINES; at most 500 and at least 200) +- P1-T2: exactly one project-file line contains `Controllers\QfcItemController.UiThreadDispatcherPinCountTests.cs`; it reads ` ` (four leading spaces) and the line before it is the fixture-tests item ` ` +- PC tokens (task order): 10, 1, 3, 1, 1, 4, 4, 1, 3, 1, 4, 1, 2, 2, 0, 0, 0, 1, 1 (all as expected) +- PROJ tokens: `Controllers\QfcItemController.UiThreadDispatcherPinCountTests.cs` 1, `Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs` 1 +- git diff --numstat HEAD -- QuickFiler.Test/QuickFiler.Test.csproj (exit 0): `1 0 QuickFiler.Test/QuickFiler.Test.csproj` +- git status --porcelain -- QuickFiler QuickFiler.Test (exit 0): + +PHASE1-PORCELAIN: +``` + M QuickFiler.Test/QuickFiler.Test.csproj +?? QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs +``` diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/specification-tests-before-fix.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/specification-tests-before-fix.md new file mode 100644 index 000000000..e293659e1 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/specification-tests-before-fix.md @@ -0,0 +1,17 @@ +# Specification tests on the unmodified fixture (issue #968, task P1-T6) + +Timestamp: 2026-10-03T02-57 +Command: pwsh -NoProfile -Command '' with ASSEMBLY `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll`, FILTER `FullyQualifiedName=QuickFiler.Controllers.Tests.QfcItemController_UiThreadDispatcherPinCountTests.EnsureDispatcher_TwoPinsHeld_ReleaseOrderDoesNotChangeTheOutcome|FullyQualifiedName=QuickFiler.Controllers.Tests.QfcItemController_UiThreadDispatcherPinCountTests.EnsureDispatcher_UnderATransactionHoldingALiveDispatcher_ReleasingAllPinsLeavesTheLiveDispatcher|FullyQualifiedName=QuickFiler.Controllers.Tests.QfcItemController_UiThreadDispatcherPinCountTests.EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores` (FILTER-PC-T234), TASKID p1-t6 and NAMES the last three NAMES-PC names; the Command Reference CMD-VSTEST macro executed verbatim with PREFIX and TOOLS expanded and WORKTREE substituted +Canonical command: vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER-PC-T234" "/ResultsDirectory:coverage\test-results\968\p1-t6" "/Logger:trx;LogFileName=p1-t6.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" +EXIT_CODE: 0 +Output Summary: +- WORKTREE-LEAF: agent-a291a7fbabf9d0229 +- VSTEST_EXIT_CODE: 0 +- TRX_PRESENT: True +- SEQUENCE_FILES: 0 +- COUNTERS total=3 executed=3 passed=3 failed=0 +- RESULT_COUNT: 3 +- RESULT EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores = Passed duration=00:00:00.0009269 +- RESULT EnsureDispatcher_UnderATransactionHoldingALiveDispatcher_ReleasingAllPinsLeavesTheLiveDispatcher = Passed duration=00:00:00.0167470 +- RESULT EnsureDispatcher_TwoPinsHeld_ReleaseOrderDoesNotChangeTheOutcome = Passed duration=00:00:00.0583118 +- All three specification tests pass on the unmodified fixture: the "passes before and after the fix" half of the labels AC6 requires. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/feature-audit.2026-10-03T04-00.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/feature-audit.2026-10-03T04-00.md new file mode 100644 index 000000000..6b1b833e5 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/feature-audit.2026-10-03T04-00.md @@ -0,0 +1,146 @@ +# Feature Audit: focus-and-theme-tests-leak-shared-dispatcher-setup (Issue #968, folding Issue #972) + +- Timestamp: 2026-10-03T04-00 +- Branch: bug/focus-and-theme-tests-leak-shared-dispatcher-setup-968, head 5570b337cfd11e57579228b36bc919f9673b6195 +- Base: origin/main 993fdd01566dee82e5f37acb761a600feaaa1454 +- Work mode: full-bug (issue.md line 12). Acceptance-criteria source: spec.md only (AC1 to AC32, spec.md lines 275-307; amendment 1.2 added AC25 to AC32 and amended AC20). +- Companion artifacts: policy-audit.2026-10-03T04-00.md, code-review.2026-10-03T04-00.md, remediation-inputs.2026-10-03T04-00.md + +## Scope and Baseline + +- Changed code files (name-status origin/main..HEAD, three agreeing sources: caller prompt, evidence/qa-gates/footprint-scope.md, evidence/qa-gates/final-commit.md): + - M QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs + - M QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs + - M QuickFiler.Test/Controllers/QfcDatamodelTests.cs + - M QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs + - M QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs + - M QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs + - M QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs + - M QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs + - A QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs + - M QuickFiler.Test/QuickFiler.Test.csproj + - A QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs + - A QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs + - M QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs + - M QuickFiler/Controllers/QfcDatamodel.cs + - A docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md and A docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md (inherited promotion records, committed before the plan's first task) + - A docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/** (issue, spec, plan, two research records, 74 evidence files) +- Baseline state at the executor's anchor (94287369908cc920b21b0e3256314f988ad7d2f5): code tree unchanged relative to the anchor (scoped --exit-code diff exit 0, evidence/baseline/scope-and-anchor.md); baseline full-suite run 7361/7361 passed, first-party lines 85.35%, branches 79.73% (evidence/baseline/coverage-summary.md). +- Post-change state: 7365/7365 passed (four pin-count tests added, none removed), lines 85.36%, branches 79.75%; no new failure (evidence/qa-gates/coverage-summary.md, coverage-comparison.md). +- Review method: no-Bash (caller directive); all fourteen code paths read in full from the worktree; the ten pre-existing modified files compared against the session checkout's unchanged copies; evidence files read; Grep censuses re-run; the worktree reflog used as the clock and head reference. +- Defects addressed: the fixture's ensure pin was not reference counted (the installing pin's release nulled the shared static while other pins were live); the two theme tests wrote the shared static without reading it; R4 held a pin that outlived its gate hold; the five #972 residuals of the #950 review; the two dequeue-liveness tests depended on thread-pool scheduling. + +## Acceptance Criteria Inventory + +| AC | Criterion (abbreviated) | Spec state at review start | +|---|---|---| +| AC1 | Counted pin, non-last release keeps the parked dispatcher; proved by the fail-before regression test passing after the fix | [x] | +| AC2 | Last release reverts only the fixture's own seeding; final assertion of the regression test and the release-order specification test | [x] | +| AC3 | A foreign transaction value is never nulled by pin release; specification test with ShutdownDispatcher in a finally, plus R1 | [x] | +| AC4 | Ownership flag cleared on the last release; fresh single pin installs and restores | [x] | +| AC5 | Fail-before and pass-after evidence isolates the fixture change; message names the first-release assertion and found ; summaries only | [x] | +| AC6 | Pin-count class labels: regression test "fails before", three specification tests "pass before and after", class doc states why the regression lives at the fixture level | [x] | +| AC7 | Dead theme-test calls removed: grep for EnsureUiThreadDispatcher in FocusAndThemeTests returns zero; both theme tests pass | [x] | +| AC8 | Gated-caller census: two strategies agree; every invocation other than the forwarder acquired and released inside a held transaction with no Install between | [x] | +| AC9 | Counter and flag are private statics; every access inside lock (FieldLock); last-release null write inline, not through CompareExchange | [x] | +| AC10 | All existing fixture tests pass; the file diff touches only R4's doc, baseline-pin removal and disposal structure | [x] | +| AC11 | Fixture docs describe counting, ownership, discard consequence and residual; leaks exactly and installed nothing carries return zero | [x] | +| AC12 | Wrapper doc describes the counted pin and names the fixture tests as remaining callers; three stale phrases return zero | [x] | +| AC13 | R4 doc states the counting guarantee, keeps the UiThread.Initialize residual; no other class may dispose returns zero | [x] | +| AC14 | R4 disposes transactionA in a finally in addition to the explicit dispose | [x] | +| AC15 | Private BuildExecutingViewer removed; callers use the shared helper; shared-helper doc corrected; every FocusAndThemeTests test passes | [x] | +| AC16 | Theme-test arrange comments name the injected IUiDispatcher mock | [x] | +| AC17 | EnsureSynchronizationContext unchanged | [x] | +| AC18 | Every touched or added C# file at or under 500 physical lines; counts recorded | [x] | +| AC19 | No prohibited constructs added; runsettings unchanged; recorded grep | [x] | +| AC20 | Diff lists only QuickFiler.Test/, the feature folder and exactly the two production paths (after excluding the inherited committed set) | [x] | +| AC21 | Compile item for the pin-count file; all four tests appear as passed in the coverage route's summary | [x] | +| AC22 | Full toolchain pass in order, single pass, no skipped compile target, recorded | [ ] (dated orchestrator ruling note present) | +| AC23 | First-party line and branch coverage not lower than baseline; summaries and projections recorded | [x] | +| AC24 | Three #968 classes together under the CLI runsettings all pass | [x] | +| AC25 | Exactly one class SynchronousBackgroundWorker, internal sealed, with StartSynchronously; no nested copies; doc text corrected; Compile item; three classes pass | [x] | +| AC26 | _remainingLoadActive doc names WorkerStarter, states why IsBusy cannot serve and why volatile; two stale phrases return zero; TryUnhookOrReplace citation carries no line range | [x] | +| AC27 | Zero-caller proof recorded before removal; four members, commented references and empty region gone; nameof retargeted; IQfcDatamodel still implemented; both rebuilds pass | [x] | +| AC28 | QfcDatamodel.cs at or under 400 lines with the before figure recorded | [x] | +| AC29 | ExcludeFromCodeCoverage unchanged; no removed member referenced by a test; AC23 satisfied | [x] | +| AC30 | Every test-created worker in the three named files constructed in a using header owned by the test method; StartHeldOpenLoader receives its worker; tests pass | [x] | +| AC31 | Both dequeue-liveness tests: no Task.Yield, no loop around an advance, WhenAny over ArmingFakeTimeProvider.Armed and the pending dequeue, await the dequeue task; exception dossier; labelled sensitivity check; both pass | [x] | +| AC32 | Four datamodel classes together under the CLI runsettings all pass | [x] | + +## Acceptance Criteria Evaluation + +| AC | Verdict | Evidence (code and artifacts) | +|---|---|---| +| AC1 | PASS | QfcItemController.UiThreadDispatcherPinCountTests.cs lines 36-75: transaction, Install(null), pinA and pinB, afterBothPins read, pinA disposed, afterFirstRelease asserted BeSameAs(afterBothPins) with the spec's because text. Fail-before on the fixture at base content (hash equal to BASE-HASH) failed exactly there with "but found " (evidence/regression-testing/fail-before-pin-count.md); pass-after Passed (pass-after-pin-count.md); Passed again in the concurrent set and the final run | +| AC2 | PASS | Final assertion afterLastRelease BeNull (lines 67-69) passed; EnsureDispatcher_TwoPinsHeld_ReleaseOrderDoesNotChangeTheOutcome (lines 84-123) disposes pinB first and asserts identically; Passed before the fix (specification-tests-before-fix.md) and after. Fixture lines 296-304: null written only when count is zero, flag set and the field still holds the parked instance | +| AC3 | PASS | EnsureDispatcher_UnderATransactionHoldingALiveDispatcher_ReleasingAllPinsLeavesTheLiveDispatcher (lines 134-172): StartRunningDispatcher, Install(live), two pins released, afterAllReleased BeSameAs(live); ShutdownDispatcher(live) in the outer finally (line 170). R1 unchanged and Passed (lines 44-98 identical to the pre-change copy) | +| AC4 | PASS | EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores (lines 184-246): full cycle, fresh pin NotBeNull then BeNull after release; second transaction installs the captured parked instance, one pin released, BeSameAs(parked). Passed before and after. Fixture line 303 clears the flag on the last release | +| AC5 | PASS | fail-before-pin-count.md: fixture hash equals the P0-T12 BASE-HASH, MESSAGE contains "to refer to", "ParkedDispatcher", "a holder that did not take the last pin must not lose the dispatcher" and "but found "; pass-after-pin-count.md: Passed, with the porcelain differing from the fail-before state by exactly the fixture file. Both are trx-derived summaries; Glob of the feature folder finds no raw document | +| AC6 | PASS | Class doc lines 9-23 (theme path dispatches through the injected IUiDispatcher mock and never reads the static; defect observable on one thread); test 1 doc "Regression test: fails before the fix" (line 30); tests 2, 3, 4 docs "Specification test: passes before and after the fix" (lines 78, 126, 175) | +| AC7 | PASS | Reviewer Grep over QuickFiler.Test/Controllers for EnsureUiThreadDispatcher: no hit in QfcItemController.FocusAndThemeTests.cs (the pre-change copy had calls at lines 452 and 468); SetThemeDark_FromNormal_SelectsDarkNormalTheme and SetThemeLight_FromNormal_SelectsLightNormalTheme Passed in the concurrent set and the final run | +| AC8 | PASS | evidence/qa-gates/call-site-census.md: PRIMARY 16 lines, CROSS 32 lines, MEMBER-SET-COMPARISON AGREE, no CROSS-only invocation; per-method nesting readings for R1, R2, R3, T1 to T4 all NESTED: YES with INSTALL-BETWEEN-PIN-ACQUIRE-AND-RELEASE: NONE; R4 carries no pin; 13 of 13 invocations nested. Reviewer Grep reproduces the sixteen primary lines (TestSupport 240-241, fixture 143, fixture tests 60, 119, 166, pin-count 45, 46, 93, 94, 146, 147, 194, 195, 201, 230) and the reviewer confirmed each invocation's acquisition and disposal lines lie between its transaction's BeginTransactionAsync and first Dispose, with the only Install calls preceding the acquisitions | +| AC9 | PASS | Fixture lines 55-56 private static int _pinCount; private static bool _fixtureInstalledParked; accesses at lines 151, 155 (inside lock (FieldLock) 149-157) and 295-303 (inside lock (FieldLock) 293-305); the null write is DispatcherField.SetValue(null, null) at line 302 in the same block as the decrement; CompareExchange absent from EnsureScope (SCOPE census 0) | +| AC10 | PASS | Executor hunk census (evidence/qa-gates/test-edit-census.md): three hunks at old ranges 194-210, 218-226, 268-273, all inside R4; reviewer comparison against the pre-change copy: R1 to R3, R5, R6, #743 and #882 bodies identical; R4 assertions (lines 253-266) and because texts identical to the pre-change lines 255-268. Eight fixture tests Passed in the concurrent set and the final run | +| AC11 | PASS | Fixture class doc lines 31-43 (counting under FieldLock, ownership, "A discarded scope therefore pins for the process lifetime", residual); EnsureDispatcher doc lines 134-142; EnsureScope doc lines 265-272. Reviewer Grep for leaks exactly and installed nothing carries over the Controllers folder: no hit | +| AC12 | PASS | TestSupport.cs lines 216-239: counted pin described, "The remaining legitimate callers are the fixture tests QfcItemController_UiThreadDispatcherFixtureTests and QfcItemController_UiThreadDispatcherPinCountTests", dispose-inside-the-transaction rule. Reviewer Grep for Becomes moot, leaks exactly, still delegate to a callee: no hit | +| AC13 | PASS | Fixture tests lines 196-208: "Issue #968 removed that pin: the fixture now counts pins, so only the last release can revert the fixture's own seeding ... a pin must stay nested inside its caller's transaction, and UiThread.Initialize (W5) must not latch during this test". Reviewer Grep for no other class may dispose: no hit | +| AC14 | PASS | Lines 218-272: transactionA acquired, try at 221, explicit transactionA.Dispose() at 249 (the act), finally at 269-272 re-disposing; R5 (lines 287-326) proves the second Dispose is inert. R4 Passed alone-class, concurrent and final. CLOSES-972-ITEM-5: YES recorded in evidence/other/ac-status-summary.md | +| AC15 | PASS | Reviewer Grep for private static Mock BuildExecutingViewer: no hit; seven call sites use QfcItemControllerTestSupport.BuildExecutingViewer (lines 176, 196, 218, 237, 297, 314, 350) and the header comment at 162-169 names the switch; TestSupport.cs lines 284-290 now read "Since issue #968 this is the single implementation"; Grep for not reachable from another test file: no hit; 17/17 FocusAndThemeTests Passed in the concurrent set | +| AC16 | PASS | Lines 433-437: "queues the theme application through the theme's injected IUiDispatcher mock (see BuildColorTheme), which absorbs the delegate without running it, so ... the shared UiThread static is irrelevant to this path (issue #968 deleted the former ensure call)"; lines 452-453 reference it for SetThemeLight | +| AC17 | PASS | TestSupport.cs lines 85-96 identical to the pre-change copy; executor hunk census: both hunks start at old line 214 or later | +| AC18 | PASS | Reviewer Read line counts (last line numbers): 375, 482, 442, 472, 248, 352, 229, 226, 394, 367, 413, 27, 49; all equal evidence/qa-gates/file-line-counts.md and all at or under 500 | +| AC19 | PASS | evidence/qa-gates/prohibited-constructs-grep.md over 696 added lines: Thread.Sleep 0, Task.Delay 0, DoNotParallelize 0, Retry( 0, Path.GetTempFileName 0, Path.GetTempPath 0, Workers 0, Timeout( 4 all [Timeout(GateTimeoutMs)] with GateTimeoutMs = 60000 equal to the sibling; runsettings diff --exit-code 0 and porcelain empty; reviewer reads of the thirteen test files agree | +| AC20 | PASS | footprint-scope.md and final-commit.md: name-status paths outside the feature folder are the fourteen Write Set code paths plus the two inherited promoted records; the only paths under QuickFiler/ are QfcDatamodel.cs and QfcDatamodel.QueueProcessing.cs; the caller's independently verified name-status lists the same sixteen paths | +| AC21 | PASS | QuickFiler.Test.csproj line 204 Compile Include="Controllers\QfcItemController.UiThreadDispatcherPinCountTests.cs" (adjacent to the fixture-test item at 203); evidence/qa-gates/coverage-summary.md RESULT lines list all four pin-count tests as Passed in the final coverage run | +| AC22 | PENDING CI (not checked; evaluated under the orchestrator ruling) | Steps 1 to 4 of the local toolchain passed in one iteration with exit 0 (toolchain-final.md SINGLE-PASS: YES; csharpier check 1640 files; both rebuilds ERRORS 0 WARNINGS 0 SKIP_CORECOMPILE_LINES 0). The coverage step could not run Invoke-MSTestWithCoverage.ps1 verbatim: the stall probe (evidence/baseline/stall-probe.md) shows ShellUtilities_Tests.GetFileIcon_WithUseFileType_ShouldReturnIconsForDirectoryAndFileExtension failing deterministically on this workstation ("Win32 handle that was passed to Icon is not valid or is the wrong type"), a known environmental failure that reproduces on main. The DIRECT route (collector over vstest with the four shell-icon classes excluded, repository runsettings unchanged) passed 7365/7365 with coverage not below baseline. The pull request does not exist yet, so no CI run on the final head exists; AC22 stays unchecked per the ruling below. Blocking finding B-1, class awaiting_ci | +| AC23 | PASS | evidence/qa-gates/coverage-comparison.md: lines 85.35% -> 85.36% (+0.01), branches 79.73% -> 79.75% (+0.02), lines-valid and branches-valid equal; one-line summaries and JaCoCo package projections present in both evidence/baseline and evidence/qa-gates; the local raw document's root element agrees with the post-change figures | +| AC24 | PASS | evidence/regression-testing/concurrent-set-test-summary.md: one vstest invocation under scripts\vscode\TaskMaster.cli.runsettings over the three classes, 29/29 Passed, CONCURRENT-NOT-PASSED: NONE | +| AC25 | PASS | Reviewer Grep for class SynchronousBackgroundWorker over *.cs: exactly QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs line 15, internal sealed, deriving from BackgroundWorker, with internal static void StartSynchronously(BackgroundWorker worker) at line 24; Liveness, Teardown and ZeroBatch declare no nested worker and no private StartSynchronously (reads); "Duplicated per file" absent (fold-edit census 0); csproj line 230; 21/21 datamodel-set tests Passed | +| AC26 | PASS | QueueProcessing.cs lines 15-23: names WorkerStarter, states "BackgroundWorker.IsBusy already reads idle at that handler's first incomplete await" and "Volatile: the writers and the readers share no other fence"; reviewer Grep for RunWorkerAsync over QuickFiler/Controllers: hits only in QfcDatamodel.cs (constructors and the WorkerStarter doc), none in QueueProcessing.cs; written on the worker thread and read: no hit; line 285 TryUnhookOrReplace citation carries no line range. The line 52 comment on _remainingLoadTask was checked and left (D-20; production-accurate) | +| AC27 | PASS | evidence/qa-gates/qfc-datamodel-legacy-callers.md: run before the removal, PRIMARY 25 / LOG 3 / CROSS 2 lines each classified, INVOCATIONS 0, member sets identical (4 and 4); post-change reviewer Grep over QuickFiler/ for Worker_RunWorkerCompleted, LoadRemainingEmailsToQueue word boundary, Linked List Locking and ILog log: no hit in QfcDatamodel.cs (the only Worker_RunWorkerCompleted hits are QfcHomeController's own member); line 335 nameof(LoadRemainingEmailsToQueueAsync); constructors still bind the one-argument loader (lines 40, 52); Cleanup, InitEmailQueue, InitEmailQueueAsync, DequeueNextItemGroup, UndoMove, QuiesceLoaderAsync, Complete, MovedItems present; both rebuilds exit 0 with SKIP_CORECOMPILE_LINES 0 | +| AC28 | PASS | QfcDatamodel.cs last line 367 (reviewer Read); file-line-counts.md records 367 beside QFCDATAMODEL-LINES-BEFORE 495 | +| AC29 | PASS | [ExcludeFromCodeCoverage] at QfcDatamodel.cs line 25 unchanged (count 1 in both copies); every test-file hit of a removed member name is DOC-PROSE or OTHER-TYPE-SAME-NAME (legacy-callers table); QFCDATAMODEL_CLASS_ENTRIES 0 at both stages; AC23 met | +| AC30 | PASS | Liveness: test 1 using at line 119; tests 2 to 4 using at lines 248, 281, 321 passing worker into StartHeldOpenLoader(worker, ...) (signature lines 211-215). ZeroBatch: using at lines 132, 161, 211; no inline construction inside an InitEmailQueue argument. DatamodelTests: using at lines 116 and 282. 21/21 Passed | +| AC31 | PASS | Liveness lines 102-172: ArmingFakeTimeProvider (106), Armed asserted complete then ReArm (136-141), Advance then Task first = await Task.WhenAny(clock.Armed, pending) (144-145), BeSameAs(clock.Armed) and pending.IsCompleted false, flag read false after release, final Advance and (await pending) empty; no Task.Yield, no for loop. DatamodelTests lines 103-152: same shape with the flag written by reflection. ArmingFakeTimeProvider.cs lines 24-48 with csproj line 231. Dossier fail-before-exception.2026-10-03T03-09.md (exactly one such file) records why the old shape cannot be forced to fail; liveness-sensitivity-check.md records both tests Failed on their re-arm assertion with the lambda forced false and the production file proven byte-identical to base afterwards; liveness-pass-after.md 2/2 Passed | +| AC32 | PASS | evidence/regression-testing/datamodel-set-test-summary.md: one vstest invocation under the CLI runsettings over the four classes, 21/21 Passed, DATAMODEL-NOT-PASSED: NONE | + +Verification notes beyond the recorded runs: the reviewer traced the fixture under the four state families recorded as code-review observation O-1 (fresh field; foreign value; residual parked-with-flag; pins under a foreign value restored to null) and found the counted release correct in each; traced R4 after the pin removal (observation O-2); and traced the two liveness rewrites step by step (observation O-3), confirming that no step depends on thread-pool scheduling because the continuations the test relies on are registered under a null SynchronizationContext and the dequeue task is awaited directly. + +## Orchestrator Ruling (AC22), recorded verbatim from spec.md + +"Note (2026-10-03, orchestrator ruling under plan D-6 and the AC22-under-DIRECT clause, following the #950 AC17 precedent): the intent of AC22 is unchanged; only the evidence source for the test stage changes. Locally, the first four steps passed in one uninterrupted pass with no skipped compile target, and the coverage stage ran by the DIRECT route because the P0-T16 probe recorded a deterministic shell-icon failure on this workstation (`Win32 handle that was passed to Icon is not valid`), a known environmental failure that reproduces on main; CI runs the shell-icon classes. The local DIRECT run passed 7365 of 7365 with coverage not below baseline (lines 85.35 to 85.36, branches 79.73 to 79.75). AC22 is checked off only from this pull request's own CI run on the final head, recording the run ID, the head SHA, the C# test-and-coverage job result, and the local DIRECT result. If CI fails any test, AC22 is not met." + +Reviewer disposition: the ruling is applied. AC22 is left unchecked and evaluated as PENDING CI; it is the single blocking finding of this review (B-1, remediability class awaiting_ci) and is recorded in remediation-inputs.2026-10-03T04-00.md with the closure steps. Closure requires recording the CI run ID, the final head SHA, the C# test-and-coverage job pass and fail counts and coverage figures, and the local DIRECT result, then changing `- [ ] AC22` to `- [x] AC22` in the item's own worktree and re-running the CI green gate against the new head. If CI fails any test, AC22 is not met. + +## Acceptance Criteria Check-off + +- AC1 to AC21 and AC23 to AC32 were already checked `[x]` by the executor (evidence/other/ac-status-summary.md; spec check-off diff: thirty-one `- [ ] AC` lines changed to `- [x] AC` with criterion text identical). Each was independently evaluated PASS above against the code and the evidence; no discrepancy found. +- Newly checked off by this review: none (no PASS item was unchecked). +- Left unchecked: AC22 (PENDING CI under the orchestrator ruling; do not check off before this pull request's own CI run on the final head). +- spec.md was not modified by this review. + +### Acceptance Criteria Status +- Source: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md +- Total AC items: 32 +- Checked off (delivered): 31 +- Remaining (unchecked): 1 +- Items remaining: + - AC22: Full toolchain pass: csharpier check, the analyzer rebuild, the warnings-as-errors rebuild and the MSTest coverage route complete in that order with every step passing in one uninterrupted pass after the last edit, the two rebuild logs contain no skipped compile target, and the commands with exit codes are recorded in this feature's qa-gates evidence folder. (Pending this pull request's own CI run on the final head, per the orchestrator ruling.) + +## Findings Summary + +- Blocking: 1 (B-1, AC22 awaiting this pull request's CI run; class awaiting_ci; no local remediation exists). +- Autonomous: 0. +- Non-blocking: 2 (CR-1 pre-existing commented-out statements left in QfcDatamodel.cs by the spec's confined production edit; CR-2 the sibling liveness test's dependence on a pumping ambient context, accepted by the spec). Details in code-review.2026-10-03T04-00.md. +- Observations: 6 (O-1 fixture soundness trace, O-2 R4 determinism, O-3 liveness determinism, O-4 canonical coverage artifact path absent, O-5 the _remainingLoadTask comment left by D-20, O-6 post-dispose continuation in liveness test 2). +- Unrelated defects to file: none; quality-tiers.yml absence is pre-existing and already promoted. + +## Orchestrator action items (not findings against the item) + +- Open the pull request with a body carrying exactly the two closing lines `Closes #968` and `Closes #972` and no other issue number beside a closing keyword (spec Dependencies and Rollout). +- After the CI green gate on the final head, close AC22 as the ruling prescribes and re-verify the head SHA. +- The three review artifacts and remediation-inputs.2026-10-03T04-00.md are untracked in the item worktree and belong to its feature folder; commit them from the item worktree. + +## Summary + +Verdict: AWAITING_CI. Thirty-one of thirty-two acceptance criteria are verified PASS against the source and the committed evidence: the fixture pin is reference counted at its single mutation point, the dead theme-test calls are gone, the regression test fails on the base fixture and passes on the fixed one with nothing else changed, every remaining pin nests inside a held transaction, and the folded #972 items and the liveness rewrites are delivered as specified with zero-caller proofs, caller-owned disposal and explicit signals. AC22 is pending this pull request's own CI run under the recorded ruling and remains unchecked; it is the only blocking finding and is not remediable locally. No autonomous finding; no remediation cycle is required before the pull request is opened. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/issue.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/issue.md new file mode 100644 index 000000000..d7030040c --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/issue.md @@ -0,0 +1,77 @@ +# focus-and-theme-tests-leak-shared-dispatcher-setup (Issue #968) + +- Date captured: 2026-10-02 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/focus-and-theme-tests-leak-shared-dispatcher-setup/ (Issue #968) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #968 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/968 +- Last Updated: 2026-10-02 +- Work Mode: full-bug + +## Summary + +Tests in `QuickFiler.Test` `QfcItemController_FocusAndThemeTests` set up the shared UI-thread dispatcher (through `UiThreadDispatcherFixture.EnsureDispatcher()`) and do not release that setup. Under the parallel test regime, another test class that releases or resets the shared dispatcher can leave a theme test running against a null dispatcher. The #950 preparation found the exposure: its transaction-test fix releases a dispatcher pin at the end of the test, and the same exposure already exists through two other tests in that file. + +## Environment + +- OS/version: Windows 11 (local) and windows-latest (CI) +- Python version: n/a (C# / MSTest, Workers=0, Scope=ClassLevel) +- Command/flags used: standard MSTest coverage route +- Data source or fixture: `UiThreadDispatcherFixture` + +## Steps to Reproduce + +1. Run `QuickFiler.Test` in parallel. +2. Have a class that resets the shared dispatcher run concurrently with `QfcItemController_FocusAndThemeTests`. +3. A theme test can observe a null dispatcher. This is intermittent. + +## Expected Behavior + +Each test class acquires and releases the shared dispatcher through a scoped, reference-counted pin, so no class can null it while another still depends on it. + +## Actual Behavior + +The theme tests depend on dispatcher state they do not own or pin. + +## Logs / Screenshots + +- [ ] Attached minimal logs or screenshot +- Snippet: #950 preparation report; its plan carries the stop marker `THEME TEST NULL-DISPATCHER EXPOSURE OBSERVED`. + +## Impact / Severity + +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +## Suspected Cause / Notes + +This is the same family as #950 and #882: raced static test-fixture state. It must not be fixed with `[DoNotParallelize]`, Workers=1 or retries. Find and own the shared state. Sequence it after #950 merges, because #950 introduces the pin. + +## Proposed Fix / Validation Ideas + +- [ ] Have the theme tests acquire and release the #950 dispatcher pin in class initialize and cleanup. +- [ ] Write a deterministic regression test that releases a competing pin mid-test, and show the theme test fails without the fix. + +## Next Step + +- [x] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch + +## Coordinator Scope Amendment (2026-10-02T22-15, binding) + +Recorded by the parent parallel-orchestrator (run `bugs-2026-09-28`, `/parallel-add 968` resume). This amendment supersedes the scope statements in the research record, `spec.md` and the plan wherever they conflict. + +1. Issue #972 ("Bug: qfc-datamodel-950-review-residuals", promoted record `docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md`) is FOLDED INTO this item by maintainer direction. This item delivers all five #972 items, and its pull request must close both issues (`Closes #968` and `Closes #972` in the PR body): + 1. Consolidate the three duplicated `SynchronousBackgroundWorker` test helpers (`QfcDatamodelLivenessTests.cs`, `QfcDatamodelTeardownTests.cs`, `QfcInitEmailQueueZeroBatchTests.cs`) into one shared test-support helper. + 2. Reword the `_remainingLoadActive` comment to match the post-#950 behaviour. + 3. `QuickFiler/Controllers/QfcDatamodel.cs` is at 495 of 500 lines: confirm the apparently unused legacy members have no callers, then remove them (or move them) so the file sits well under the limit. Changed-line coverage must not drop. + 4. Dispose the `SynchronousBackgroundWorker` instances in the liveness and zero-batch tests. + 5. Wrap `transactionA` in test R4 in `try`/`finally` (already delivered here as D4; the spec must now record it as closing #972 item 5, not as an overlap for the coordinator to reconcile). +2. The 2026-10-02T05:37Z comment on #968 is also in scope: liveness test 1 in `QfcDatamodelLivenessTests` uses `fake.Advance` plus `Task.Yield` loops, which depend on scheduling. Replace them with a deterministic completion signal. +3. Consequence: this item now changes production code (`QuickFiler/Controllers/QfcDatamodel.cs` and the file declaring `_remainingLoadActive`). AC20 ("No production code change") must be amended to name exactly the production paths this scope requires and nothing else. This is a maintainer-directed widening, not a weakening. +4. All constraints stand: tests stay parallel (Workers=0, ClassLevel); no `[DoNotParallelize]`, Workers=1, retries, `Thread.Sleep`, `Task.Delay`, temporary files or timeout increases; MSTest, Moq, FluentAssertions; failing regression test first for any behaviour defect. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/plan.2026-10-02T05-42.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/plan.2026-10-02T05-42.md new file mode 100644 index 000000000..fd6fdc7cf --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/plan.2026-10-02T05-42.md @@ -0,0 +1,1786 @@ +# 2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup (Plan) + +- **Issue:** #968 (issue #972 folded in by the Coordinator Scope Amendment in issue.md; the pull request closes both) +- **Parent (optional):** none +- **Owner:** drmoisan +- **Work Mode:** full-bug (acceptance criteria come from `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md` only; no user story exists for this item and none is to be authored) +- **Last Updated:** 2026-10-03 (round-5 deltas applied; this planning session has no shell clock, so no minute stamp is composed) +- **Status:** Ready for preflight (revision round 5: the two round-5 deltas, 1a and 1b, applied verbatim, plus one knock-on edit to 1a raised in the delta-application pass: `RESTART-CORRECTED:` is the union over every D-13 restart in the run) +- **Version:** 1.5 +- **Plan path continuity:** this file is updated in place for every preflight revision round. No timestamped sibling plan file is created for this cycle. + +**Fail-closed evidence rule:** every command-bearing task writes one evidence artifact carrying `Timestamp:`, `Command:`, `EXIT_CODE:` and `Output Summary:`. A task whose artifact is missing or incomplete stays unchecked, and the plan outcome is BLOCKED or INCOMPLETE, never PASS. + +**Evidence accounting rule:** the artifact path is named in the task text. Do not mark an evidence-bearing task complete without the artifact on disk at that exact path. + +**Evidence location:** every artifact lives under `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/` in the canonical sub-kinds `baseline/`, `regression-testing/`, `qa-gates/` and `other/`. EVIDENCE_LOCATION_OVERRIDE_REJECTED: none supplied; no artifacts-tree evidence path appears in this plan. In task text the token FEATURE abbreviates `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968`; the Write Set spells every path in full. + +## Revision record + +- **Round 1 (reviewer report `FEATURE/evidence/other/preflight-round1-report.2026-10-02T08-40.md`).** Defect 1: the `PWSH CHANNEL REFUSED` stop rule is appended to the payload-channel convention, with the cwd note. Defect 2: option B is superseded; fact 11, D-9, P0-T3, P6-T9, P8-T9 and P8-T46 are re-derived against the current branch, and every commit is pathspec-limited. Defect 3: the F-FIELDS comment no longer contains the `lock (FieldLock)` literal. Defects 4 and 5: N1 test 4 is the two-transaction variant and the nesting gate is per pin and per transaction, with every knock-on count updated (PC tokens, PRIMARY 16, CROSS 32, CONTROL 28, `INVOCATIONS-CLASSIFIED: 13 of 13 nested`, the N1 count line, the CMD-CENSUS note). Defect 6: the indentation statement names N1 and T1 as carrying one extra Markdown indent. Defect 7: WORKTREE is written with forward slashes in every `git -C` argument. Defect 8: `installed nothing carries` is in the FIX token list and in the AC11 check-off. Defect 9: the P3-T9 wording names the TS list. Defect 10: CMD-COVERAGE-POST prints every non-passed message, P8-T5 carries the `LEAK-DEPENDENT TEST EXPOSED` stop, and Risks carries the matching bullet. +- **Spec amendment 1.2 (issue #972 fold and the #968 liveness comment).** Phases 4 and 5 are new; the former Phases 4, 5 and 6 are now Phases 6, 7 and 8. AC25 to AC32 and amended AC20 are covered; the Write Set, every count and every footprint gate are re-derived. +- **Round 2 (reviewer report `FEATURE/evidence/other/preflight-round2-report.2026-10-02T23-56.md`).** Defect 1: F-SCOPE is forty-three lines, so the fixture is 375 lines after Phase 2 (F-SCOPE prose, P2-T6, P3-T9). Defects 2 and 3: the four fold span baselines print the END anchor line minus one, and the T1-LIVE `(await pending)` baseline is 1 (span anchors, P0-T13). Defect 4: the QDM `ForEachAwaitWithCancellationAsync` baseline is 2 (fact 14, P0-T13). Defect 5: the post-change `FakeTimeProvider` counts include the `ArmingFakeTimeProvider` substring (L prose, P5-T3, M prose, P5-T4); re-deriving them showed that the `using Microsoft.Extensions.Time.Testing;` directive does not contain the token either, so the baselines are LIV 1 and DMT 5 (facts 17 and 20, P0-T13, P4-T6) and the DMT post-change value is 6, not the 7 the report derived. Defect 6: the QfcDatamodel.cs hunk count is recorded and the numstat row `1 129` is gated (P4-T9; P6-T2 names numstat among the values it restates). Defect 7: the L-T1 and M-T doc comments read `a scheduler yield`. Defect 8: D-10 covers evidence-file Write and pwsh payload refusals, including `PREIMPLEMENTATION_GATE_BLOCKED`. The L1 prose and the fact 17 `await` count were tightened as sibling consistency edits. +- **Round 3 (reviewer report `FEATURE/evidence/other/preflight-round3-report.2026-10-03T01-01.md`).** Defect 1: the legacy-caller primary strategy returns 25 lines, 17 of them in `QfcDatamodel.cs` (fact 15, P4-T1, the self-review summary). Defect 2: P4-T1 classifies the two method-group assignments at `QfcDatamodel.cs` 40 and 52 as `METHOD-GROUP-ONE-ARG-OVERLOAD`. Defect 3: P6-T2 requires each token, span, hunk and numstat value as last recorded for its file, so the interim P4-T6, P4-T7 and P4-T9 values that P5-T5 supersedes are no longer demanded after formatting, and the project-file numstat row is restated as `3 0`. Defect 4: fact 22 reads 334 spec lines and 4 lines for `acquired and released inside a held` (lines 10, 105, 266, 282), and P0-T2 restates the 4. Advisory A1: the payload-channel convention states that payloads are never merged into one call, because the hook layer scans a pwsh payload as raw text. The task-description line of P6-T2 (the enumeration of commands to re-run) was read for the same defect and left unchanged: it names the commands, not the values, and re-running every listed command is satisfiable. +- **Round 4 (reviewer report `FEATURE/evidence/other/preflight-round4-report.2026-10-03T01-25.md`).** Defect 1: P4-T1 classifies the `#region` and `#endregion` lines of the empty `Linked List Locking` region (`QfcDatamodel.cs` 469 and 472) as `REGION-DIRECTIVE`, so every one of the 25 PRIMARY, 3 LOG and 2 CROSS lines that CMD-LEGACY-CALLERS prints has a category. Defect 2: D-13 states that on the Phase 8 restart to P6-T1 the P6-T9 commit is already in HEAD, so P6-T2 runs its HEAD-anchored git commands with BASE as the ref operand and its porcelain expectation becomes membership of the fourteen Write Set code paths with status ` M` (`P6-RESTART-PORCELAIN:`); the same rule is named for a Phase 6 restart that follows that commit, and the P6-T2 acceptance line carries a pointer to D-13. Defect 3: the P6-T2 exemption covers a printed `SPAN:` range and the recorded-not-gated QfcDatamodel.cs `HUNK_COUNT:` as well as a LINES value, so no value an earlier task records without gating is demanded after formatting. The optional advisory delta (naming the P1-T3 PROJ token in the P6-T2 per-file pointer) was declined by the orchestrator: the value holds regardless and the round stays narrow. Sweep: P8-T45 is the only other HEAD-anchored diff, and it runs after the check-offs and before the P8-T46 commit, on no restart path; every other `??` expectation (P1-T3, P2-T6, P3-T9, P4-T9, P5-T5) belongs to a pre-commit task and is re-run after the commit only through P6-T2, which the D-13 rule now covers; no other acceptance line restates a recorded-not-gated value as gated. +- **Round 5 (reviewer report `FEATURE/evidence/other/preflight-round5-report.2026-10-03T01-53.md`).** Defect 1: the P6-T2 rewrite exemption read only the current P6-T1 pass, so on a second or later pass (a Phase 6 restart, a Phase 8 restart to P6-T1, or both) a file the first pass re-laid, or the file edited by the correction that triggered the restart, kept a LINES value or `SPAN:` range that differed from its Phase 1 to 5 record without being named in `REWRITTEN:`, and the plan gave no recovery route. On a D-13 restart P6-T1 now also records `PRIOR-PASS-REWRITTEN:` (the union of the `REWRITTEN:` paths of every earlier P6-T1 pass in this run, or `NONE`) and `RESTART-CORRECTED:` (the union of the Write Set paths edited by every correction that triggered a D-13 restart in this run; the union wording is a knock-on edit raised in the delta-application pass, because a label naming only the latest correction leaves a file edited by an earlier correction, and not re-laid by the formatter, named by none of the three labels on a third P6-T1 pass), and the P6-T2 exemption admits a file named by any of the three labels. The exemption still covers only a LINES value, a printed `SPAN:` range and the recorded-not-gated QfcDatamodel.cs `HUNK_COUNT:`; every token value, the at-most-500 and at-most-400 LINES bounds and the two gated `HUNK_COUNT: 2` values stay gated. Sibling read, no change: P6-T3's `REWRITTEN:` condition means the current pass, because it gates whether the production build is fresh; P8-T1 and P8-T7 use the distinct labels `REWRITTEN-WRITESET:` and `REWRITTEN-OTHER:`; P8-T8 compares against the final P6-T2 values; D-13 defines the restart paths the two new labels refer to and is unchanged; P6-T9, P7-T3 and P8-T9 are BASE-anchored. + +## Caller instructions applied with a recorded adjustment + +1. **Absolute worktree path in `Command:` rows.** The repository hygiene guard (`scripts/hygiene/Test-RepositoryHygiene.Rules.ps1`, function `Get-UserProfilePathPattern`, line 21) rejects any committed line matching a drive-letter user-profile path, and both this plan and every evidence artifact are committed. Every `Command:` row therefore records the payload with the literal token `WORKTREE` in place of the absolute path, and every payload prints `WORKTREE-LEAF:` followed by the leaf name of its working directory. The acceptance condition is that `WORKTREE-LEAF: agent-a291a7fbabf9d0229` is recorded; a payload that ran in another tree prints a different leaf and fails that condition. +2. **Quote character of the pwsh channel.** The caller's example uses double quotes around the command string. Payloads contain `$`, so they are passed in outer single quotes (`pwsh -NoProfile -Command '...'`) and use double quotes only inside; the Bash channel cannot carry a single quote inside a single-quoted argument, so no payload and no asserted token contains an apostrophe. +3. **D1 placement.** D1 (the fixture's own doc comments) is applied in Phase 2 together with the fixture fix, because it edits the same file and the same regions; D2 to D6 remain in Phase 3. +4. **Spec amendments.** Amendment 1.1 (planner) and amendment 1.2 (orchestrator) are the text on disk; this plan does not edit spec.md other than the check-off edits. P0-T2 verifies the amended text is the text on disk. +5. **Fail-before exception name.** The dossier of P5-T1 is named `fail-before-exception..md` with the timestamp read from the host clock when it is written, as the evidence conventions prefer; it is the only evidence file in this plan whose name is not fixed, and P8-T41 locates it by the pattern `fail-before-exception.*.md` (exactly one match required). + +## Requirement sources + +- Acceptance criteria: `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md`, section `## Acceptance Criteria`: thirty-two checkbox lines `- [ ] AC1:` through `- [ ] AC32:`, each on one line. The check-off edit changes only `- [ ] ACn:` to `- [x] ACn:`. +- Design records: `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T05-50-dispatcher-pin-call-sites-research.md` (sections 1, 2.1, 3, 4, 5 and 6) and `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T22-20-qfc-datamodel-972-fold-research.md` (sections 1 to 8); both read-only. +- Issue metadata: `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/issue.md` carries `- Work Mode: full-bug` at line 12 and the binding `## Coordinator Scope Amendment (2026-10-02T22-15, binding)` at lines 65 to 77. It is not an acceptance-criteria source. +- Structural reference: `docs/features/active/2026-09-30-quickfiler-tests-depend-on-wall-clock-timing-950/plan.2026-10-01T07-11.md` and its evidence folder (bootstrap, command-macro, coverage-route and evidence conventions); every citation below was re-derived against this worktree, not carried from that plan. + +## Write Set (every file this plan creates or modifies) + +Code files (the only paths outside the feature folder this plan may change; spec "Files/modules to change" including the amendment 1.2 folded scope): + +- `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs` (modified: pin counter, ownership flag, counted dispose, D1 docs) +- `QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs` (modified: two dead calls deleted, D5, D6) +- `QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs` (modified: D2 wrapper doc, D5 shared-helper doc) +- `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs` (modified: D3 doc rewrite, R4 pin removal, D4 try/finally, which delivers #972 item 5) +- `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs` (new: regression test and three specification tests) +- `QuickFiler.Test/QuickFiler.Test.csproj` (modified: three new `Compile Include` items) +- `QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs` (new: the shared synchronous worker and its starter; #972 item 1) +- `QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs` (new: the armed-timer signal; #968 liveness comment) +- `QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs` (modified: nested helper removed, caller-owned workers, `StartHeldOpenLoader` signature, test 1 rewrite, context scope helper) +- `QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs` (modified: nested helper removed, starter retargeted) +- `QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs` (modified: nested helper removed, three `using` blocks, doc rewording) +- `QuickFiler.Test/Controllers/QfcDatamodelTests.cs` (modified: sibling liveness test rewrite, two `using` blocks) +- `QuickFiler/Controllers/QfcDatamodel.cs` (production; modified: four caller-free members, their commented-out references and the empty region removed; one `nameof` retargeted; #972 item 3) +- `QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs` (production; modified: `_remainingLoadActive` doc comment rewritten and the stale `TryUnhookOrReplace` line range dropped; comment-only; #972 item 2) + +Fourteen code paths: two production, twelve under `QuickFiler.Test/`. No other project file changes. + +Feature documents (the feature folder is committed by the branch's existing docs commits; this plan commits only its own additions and check-offs): + +- `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md` (acceptance-criteria check-off edits only) +- `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/plan.2026-10-02T05-42.md` (task check-off edits only) + +Evidence files, all new, all fixed names except the dossier: + +- `FEATURE/evidence/baseline/`: `phase0-instructions-read.md`, `scope-and-anchor.md`, `bootstrap-sdk.md`, `bootstrap-tool-restore.md`, `bootstrap-nuget-restore.md`, `analyzer-alignment.md`, `bootstrap-dotnet-coverage.md`, `csharpier-check-baseline.md`, `msbuild-analyzer-baseline.md`, `msbuild-nullable-baseline.md`, `census-baseline.md`, `fold-census-baseline.md`, `concurrent-set-baseline.md`, `datamodel-set-baseline.md`, `stall-probe.md`, `coverage-summary.md`, `coverage-jacoco-projection.md`, `toolchain-baseline.md`, `phase0-commit.md` +- `FEATURE/evidence/regression-testing/`: `pin-count-file-census.md`, `fail-before-build.md`, `fail-before-pin-count.md`, `specification-tests-before-fix.md`, `pass-after-build.md`, `pass-after-pin-count.md`, `fold-build.md`, `datamodel-set-after-consolidation.md`, `fail-before-exception..md`, `liveness-build.md`, `liveness-pass-after.md`, `liveness-sensitivity-check.md`, `liveness-revert-build.md`, `liveness-pass-after-revert.md`, `implementation-build.md`, `pin-count-class-pass-after.md`, `fixture-class-pass-after.md`, `focus-and-theme-class-pass-after.md`, `concurrent-set-test-summary.md`, `datamodel-set-test-summary.md` +- `FEATURE/evidence/qa-gates/`: `fixture-change-census.md`, `test-edit-census.md`, `qfc-datamodel-legacy-callers.md`, `fold-edit-census.md`, `liveness-edit-census.md`, `queue-processing-comment-census.md`, `scoped-format.md`, `post-format-census.md`, `implementation-commit.md`, `call-site-census.md`, `prohibited-constructs-grep.md`, `csharpier-format-final.md`, `csharpier-check-final.md`, `msbuild-analyzer-final.md`, `msbuild-nullable-final.md`, `coverage-summary.md`, `coverage-jacoco-projection.md`, `coverage-comparison.md`, `toolchain-final.md`, `file-line-counts.md`, `footprint-scope.md`, `evidence-hygiene.md`, `final-commit.md` +- `FEATURE/evidence/other/`: `ac-status-summary.md` (the planner review records `planner-review.2026-10-02T22-44.md` and the round-1 records already exist in this folder and are not written by the executor) + +Files this plan must not touch, stated so the executor fails closed rather than infers: every file under QuickFiler/ other than the two production Write Set paths, every file under UtilitiesCS/, UtilitiesCS.Test/ and every other project, QuickFiler.Test/Helper Classes/EmailMoveMonitorTests.cs, every other file under QuickFiler.Test/ not listed above (including QfcDatamodelRethrowTests.cs, QfcQueuePurePathsTests.cs and QfcHomeControllerRunAsyncHighConfidenceTests.Part3.cs, which read `_remainingLoadActive` by reflection and are unaffected), QuickFiler/Controllers/QfcDatamodel.FrameBuilding.cs, QuickFiler/Interfaces/IQfcDatamodel.cs, TaskMaster.runsettings, scripts/vscode/TaskMaster.cli.runsettings, every file under scripts/, every file under .github/, every file under .claude/ (uncommitted .claude/agent-memory/ files are session memory and are never staged by this plan), every file under docs/features/potential/, and both research documents' content. The one temporary edit this plan makes (P5-T8, the sensitivity check) is reverted inside the same task and is never committed. No raw test-result document (trx), raw coverage document (cobertura, coverage, coveragexml) or msbuild log is copied into the feature folder under any name; raw documents stay under the repository coverage directory, which .gitignore line 150 ignores. + +## AC identity table + +Each ID names one checkbox in the spec's `## Acceptance Criteria` section, in document order; the label `ACn:` is part of the line text. + +| ID | Opening words of the criterion | Evidence read by its check-off task | +|---|---|---| +| AC1 | Counted pin, non-last release | `regression-testing/pass-after-pin-count.md`, `qa-gates/coverage-summary.md` | +| AC2 | Counted pin, last release reverts only the fixture's own seeding | same two artifacts | +| AC3 | A foreign transaction value is never nulled by pin release | same two plus `regression-testing/fixture-class-pass-after.md`, `qa-gates/post-format-census.md` | +| AC4 | Ownership flag is cleared on the last release | `regression-testing/pass-after-pin-count.md`, `qa-gates/coverage-summary.md` | +| AC5 | Fail-before and pass-after evidence isolates the fixture change | `regression-testing/fail-before-pin-count.md`, `regression-testing/pass-after-pin-count.md`, `regression-testing/pin-count-file-census.md` | +| AC6 | The pin-count test class labels its tests | `qa-gates/post-format-census.md`, `regression-testing/specification-tests-before-fix.md` | +| AC7 | The dead theme-test calls are removed | `qa-gates/post-format-census.md`, `regression-testing/focus-and-theme-class-pass-after.md` | +| AC8 | Gated-caller census invariant holds | `qa-gates/call-site-census.md` | +| AC9 | The pin counter and install-ownership flag are private statics | `qa-gates/post-format-census.md` | +| AC10 | All existing fixture tests pass unchanged in behaviour | `regression-testing/fixture-class-pass-after.md`, `qa-gates/post-format-census.md` | +| AC11 | Fixture documentation describes the counted pin | `qa-gates/post-format-census.md` | +| AC12 | Wrapper documentation describes the counted pin | `qa-gates/post-format-census.md` | +| AC13 | The second-caller transaction test's doc no longer asserts the obsolete invariant | `qa-gates/post-format-census.md` | +| AC14 | The second-caller transaction test releases its gate on any throw (D4; closes #972 item 5) | `qa-gates/post-format-census.md`, `regression-testing/fixture-class-pass-after.md` | +| AC15 | The duplicated viewer helper is removed | `qa-gates/post-format-census.md`, `regression-testing/focus-and-theme-class-pass-after.md` | +| AC16 | The theme-test arrange comment is corrected | `qa-gates/post-format-census.md` | +| AC17 | `EnsureSynchronizationContext` is unchanged | `qa-gates/post-format-census.md` | +| AC18 | File-size limit | `qa-gates/file-line-counts.md` | +| AC19 | No prohibited constructs | `qa-gates/prohibited-constructs-grep.md` | +| AC20 | No production code change outside the folded scope (amended) | `qa-gates/footprint-scope.md` | +| AC21 | The new test file is built and discovered | `qa-gates/post-format-census.md`, `qa-gates/coverage-summary.md` | +| AC22 | Full toolchain pass | `qa-gates/toolchain-final.md` | +| AC23 | Coverage not reduced | `qa-gates/coverage-comparison.md` | +| AC24 | Parallel run of the three classes together passes | `regression-testing/concurrent-set-test-summary.md` | +| AC25 | One shared synchronous worker helper (#972 item 1) | `qa-gates/post-format-census.md`, `regression-testing/datamodel-set-test-summary.md` | +| AC26 | The producer-liveness comment matches post-fix behaviour (#972 item 2) | `qa-gates/post-format-census.md`, `qa-gates/queue-processing-comment-census.md` | +| AC27 | Caller-free legacy members are removed (#972 item 3) | `qa-gates/qfc-datamodel-legacy-callers.md`, `qa-gates/post-format-census.md`, `qa-gates/msbuild-analyzer-final.md`, `qa-gates/msbuild-nullable-final.md` | +| AC28 | The datamodel file sits well under the size limit | `qa-gates/file-line-counts.md`, `qa-gates/qfc-datamodel-legacy-callers.md` | +| AC29 | Removed production lines carry no coverage loss | `qa-gates/post-format-census.md`, `qa-gates/qfc-datamodel-legacy-callers.md`, `qa-gates/coverage-comparison.md` | +| AC30 | Test-owned workers are disposed (#972 item 4) | `qa-gates/post-format-census.md`, `regression-testing/datamodel-set-test-summary.md` | +| AC31 | The dequeue-liveness tests use explicit completion signals | `qa-gates/post-format-census.md`, `regression-testing/fail-before-exception..md`, `regression-testing/liveness-sensitivity-check.md`, `regression-testing/liveness-pass-after-revert.md` | +| AC32 | The four datamodel test classes pass together | `regression-testing/datamodel-set-test-summary.md` | + +## Verified tree facts (re-derived against this worktree while authoring; every count was re-read in this revision pass) + +Line totals are content-line counts (the Grep tool's count of lines matching `^`, which equals `git grep -c ""`); every one of the ten existing Write Set `.cs` files ends every line with a carriage return (CRLF; the `\r$` count equals the line count for each), and `.gitattributes` line 4 sets `* text=auto`. + +1. `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs` is 342 lines. Class doc 12 to 31 (`/// ` at 30, `/// ` at 31; design note on `EnsureDispatcher` never taking the gate at 25 to 30). Statics 34 to 38 (`_parkedDispatcher` at 38), counters 44 to 46. `Exchange` 77 to 85; `CompareExchange` declared at 92 with its `lock (FieldLock)` at 94. `EnsureDispatcher` doc 116 to 121 (line 120: `returned scope is optional: a discarded scope leaks exactly as the pre-fix helper did.`); declaration 122; comment 124 to 125; `Dispatcher parked = GetParkedDispatcher();` 126; blank 127; body 128 to 137 (`lock (FieldLock)` 128, `if (DispatcherField.GetValue(null) == null)` 130, `return new EnsureScope(parked);` 133, `return new EnsureScope(null);` 137); method close 138. `TransactionGateAcquireTimeoutMs` at 146. Parked thread name at 231. `EnsureScope` doc 243 to 248 (line 245: `static still holds the exact instance this scope installed. A scope that installed nothing`, line 246: `carries null and is a no-op, which is what keeps a discarded scope from clobbering a`); class 249 to 274 with `UiThreadDispatcherFixture.CompareExchange(_installed, null);` at 271. `UiThreadDispatcherTransaction` declared at 284, its `CompareExchange` call at 336. Counts: `lock (FieldLock)` 4 (66, 79, 94, 128); `CompareExchange(` 3 (92, 271, 336); `return new EnsureScope(` 2; `leaks exactly` 1; `installed nothing carries` 0 as a single-line token (the phrase wraps across 245 and 246, so that AC11 grep is satisfied vacuously before the change; the gate therefore also uses the single-line tokens `leaks exactly` and `A scope that installed nothing`, each 1 before and 0 after, and records `installed nothing carries` at 0 before and 0 after with the vacuity noted); `_pinCount` 0; `_fixtureInstalledParked` 0; `pins for the process lifetime` 0; `install-ownership flag` 0; bare `EnsureDispatcher` 5 lines (26, 27, 122, 195, 244). +2. `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs` is 470 lines. `private const int GateTimeoutMs = 60000;` at 33; `[Timeout(GateTimeoutMs)]` 8 occurrences; `[TestMethod]` 8. R1 declared 44 (transaction 50 to 52, `Install(liveA)` 56, pin 59 to 60 with `QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` on 60, `ensureScope.Dispose();` 62, `transaction.Dispose();` 81 and 91, `ShutdownDispatcher(liveA)` 96). R2 declared 107 (transaction 110 to 112, `Install(null)` 116, pin 119, dispose 121, `transaction.Dispose();` 137 and 147). R3 declared 157 (transaction 160 to 162, `Install(null)` 165, pin 166, disposes 169 and 171, `transaction.Dispose();` 188). R4 doc 192 to 209 with the `` 196 to 208 (line 205: `/// Invariant for future editors: no other class may dispose an ensure scope holding the`, line 206: `/// parked dispatcher (W2), and UiThread.Initialize (W5) must not latch during this test;`); attributes 210 and 211; declared 212; `// Arrange` 214; `liveA` 215; `try` 216; `{` 217; transaction A 218 to 220; `using (` 221; `IDisposable baseline = QfcItemControllerTestSupport.EnsureUiThreadDispatcher()` 222; `)` 223; `{` 224; `Dispatcher original = UiThreadDispatcherFixture.Current;` 225; `transactionA.Install(liveA);` 226 (unique in the file); `using (var secondCallerStarted = new ManualResetEventSlim(false))` 228; waiter 232 to 247 with its `finally` at 243 and `transactionB.Dispose();` 245; `secondCallerStarted.Wait();` 250; `transactionA.Dispose();` 251 (unique); assertions 255 to 268 with `issue #230 lost update` at 267 (unique in the file); `}` 269 (closes the `secondCallerStarted` using), `}` 270 (closes the `baseline` using), `}` 271 (closes the `try`), `finally` 272, `{` 273, `QfcItemControllerTestSupport.ShutdownDispatcher(liveA);` 274, `}` 275, method close 276. R5 declared 285. `EnsureUiThreadDispatcher()` 4 lines (60, 119, 166, 222); bare `EnsureDispatcher` 10 lines (44, 60, 70, 107, 119, 128, 157, 166, 198, 222); `no other class may dispose` 1; `(W5) must not latch` 1; `.BeginTransactionAsync()` 12 lines in the file. No `Issue #968` text. +3. `QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs` is 497 lines, class `QfcItemController_FocusAndThemeTests` at 27, `[TestMethod]` 17. Private `BuildExecutingViewer` 99 to 115 with blank lines at 98 and 116; `/// ` of `EnableHandlelessThemeInvoke` at 117. Comment block 181 to 186 (line 182 ends `BuildExecutingViewer() executes the`). `var viewer = BuildExecutingViewer();` at 193, 213, 235, 254, 314, 331 and 367 (seven lines, all with twelve leading spaces). `SetThemeDark_FromNormal_SelectsDarkNormalTheme` 448 to 462: comment 450 to 451, `QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` 452, `var controller = new FocusController();` 453. `SetThemeLight_FromNormal_SelectsLightNormalTheme` 465 to 478: `// Arrange` 467, the ensure call 468, `var controller = new FocusController();` 469. Counts: `EnsureUiThreadDispatcher` 2; `BuildExecutingViewer` 9 (99, 182, and the seven callers); `private static Mock BuildExecutingViewer` 1; `QfcItemControllerTestSupport.BuildExecutingViewer()` 0. +4. `QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs` is 440 lines. `EnsureSynchronizationContext` doc 85 to 89, method 90 to 96. `BuildColorTheme` remarks 161 to 168 and method 169 to 181 (injects a `Mock` whose `InvokeAsync` returns `Task.CompletedTask` at 175 to 179). `EnsureUiThreadDispatcher` doc 216 to 237 (`Becomes moot` at 226, `leaks exactly` at 233, `still delegate to a callee` at 219), declaration 238, body `UiThreadDispatcherFixture.EnsureDispatcher();` 239. `BuildExecutingViewer` doc 282 to 288 (line 287: `/// QfcItemController.FocusAndThemeTests.cs, which is not reachable from another test file.`), method 289 to 305. `StartRunningDispatcher` 251 to 271; `ShutdownDispatcher` 277 to 280. Bare `EnsureDispatcher` 2 lines (238, 239). The only other caller of the shared helper is `QuickFiler.Test/Controllers/QfcItemController.MailActionsTests.cs` line 203. +5. Repository-wide call sites (Grep over `*.cs`, pattern `EnsureUiThreadDispatcher|EnsureDispatcher`): 20 lines in 5 files, exactly as research section 2.1 records (fixture 5, test support 2, fixture tests 10, focus-and-theme 2, `QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs` line 124, a comment). Lines matching `EnsureUiThreadDispatcher\(\)|EnsureDispatcher\(\)`: 9 (fixture 122, test support 238 and 239, fixture tests 60, 119, 166, 222, focus-and-theme 452, 468). `BeginTransactionAsync\(` matches 23 lines in 6 files (the census positive control). `[DoNotParallelize]` occurs in QuickFiler.Test only at `Helper Classes/EmailMoveMonitorTests.cs` 24 and `Helper Classes/ViewerQueueStaticWrapperTests.cs` 11, both outside the Write Set. +6. Theme path (read-only context for D6): `QuickFiler/Controllers/QfcItemController.FocusAndTheme.cs` 274 to 286 (`SetThemeDark` calls `_themes["DarkNormal"].SetQfcTheme(async)` then sets `_activeTheme`); `UtilitiesCS/HelperClasses/ThemeHelpers/Theme.cs` 427 to 445 (`SetQfcTheme(bool async)`: the async branch is `_uiDispatcher.InvokeAsync(() => SetQfcTheme());` at 431; the former static read is the commented line 441); `UtilitiesCS/Threading/UiThread.cs` 266 to 285 (the `Dispatcher` getter throws `InvalidOperationException` when `_dispatcher` is null; private backing field at 285). No statement on the theme path reads `UiThread.Dispatcher`. +7. `QuickFiler.Test/SetupAssemblyInitializer.cs` 14 to 25: `[AssemblyInitialize]` installs an assembly resolver and WinForms rendering defaults and does not write `UiThread._dispatcher`, so a class run alone starts from a null baseline. +8. `QuickFiler.Test/QuickFiler.Test.csproj`: `v4.8.1` at 17, `OutputPath` `bin\Debug\` at 35, no `LangVersion` element; `Compile Include` items for the Write Set files at 155 (QfcDatamodelTests), 157 (QfcDatamodelLivenessTests), 161 (QfcInitEmailQueueZeroBatchTests), 183 (QfcDatamodelTeardownTests), 200 (TestSupport), 201 (fixture), 203 (fixture tests), 212 (focus-and-theme), 227 (`TestSupport\WinFormsPumpHost.cs`), 228 (`TestSupport\DedicatedWorkerThread.cs`), 229 (`TestSupport\WinFormsPumpHostTests.cs`), each with four leading spaces; no item for the pin-count file, for `TestSupport\SynchronousBackgroundWorker.cs` or for `TestSupport\ArmingFakeTimeProvider.cs`. The existing shared helpers in `QuickFiler.Test/TestSupport/` are `internal` in namespace `QuickFiler.Test.TestSupport` (`DedicatedWorkerThread.cs` lines 4 and 21); nine files already carry `using QuickFiler.Test.TestSupport;`, placed in alphabetical order among the other `using` lines (for example `Controllers/QfcItemController.SeamFactoryTests.cs` line 13, between `using QuickFiler.Interfaces;` and `using TaskVisualization;`). No file under `QuickFiler.Test/TestSupport/` or among the four datamodel test files carries a `#nullable` directive. +9. `scripts/vscode/TaskMaster.cli.runsettings` lines 4 to 7 set Workers 0 and Scope ClassLevel. `scripts/vscode/Invoke-MSTestWithCoverage.ps1` (461 lines): `Get-DotnetCoverageArgumentList` appends `/InIsolation`, `/TestCaseFilter:TestCategory!=LiveOutlook`, the results directory and the trx logger at 89 to 93 with no extension point; `Invoke-DotnetCoverageCollection` throws `MSTest with coverage failed with exit code` at 262 after the collector exits non-zero, before post-processing; defaults `coverage\test-results` and `mstest-coverage-run.trx` at 297 to 298; discovery 348 to 355 filters `bin\Debug` and a `.claude` segment; post-processing 399 to 402; `First-party coverage:` printed at 410; projection 415 to 423; trx summary 430 to 447; entry guard 459 to 461 so dot-sourcing is safe. Helper functions: `Get-TrxRunSummary` and `Format-TrxRunSummary` in `scripts/vscode/Invoke-MSTest.TrxSummary.ps1` (12, 103); `ConvertTo-KoverageCoberturaXml` in `scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1` (407); `Get-CoberturaFirstPartyCoverageReport` in `scripts/vscode/Invoke-MSTestWithCoverage.FirstParty.ps1` (123; the line format `First-party coverage: lines a/b (p%), branches c/d (q%)` at 117 to 120); `Assert-CoberturaLineCoverageThreshold` and `Assert-CoberturaBranchCoverageThreshold` in `scripts/vscode/Invoke-MSTestWithCoverage.Threshold.ps1` (3, 58); `ConvertTo-JacocoPackageProjection` and `Assert-JacocoProjectionReconciliation` in `scripts/vscode/Invoke-MSTestWithCoverage.Projection.ps1` (14, 83). The Helpers file dot-sources its part files, so dot-sourcing it alone resolves the FirstParty, Threshold and Projection functions, as the #950 run's CMD-COVERAGE-POST showed. +10. `.gitignore`: `*.coverage` 140, `*.coveragexml` 141, `*.trx` 146, `coverage/*` 150, `!coverage/.gitkeep` 151, `[Bb]in/` 26. `.csharpierignore` excludes `**/evidence/**` (4) and `*.csproj` (12). `global.json` pins SDK 8.0.205 under `.dotnet-sdk` with `latestFeature` roll-forward (lines 2 to 9); `dotnet-tools.json` at the repository root pins csharpier 1.2.6 (line 6). `scripts/vscode/Install-RepoDotNetSdk.ps1` defaults `-Version` to `8.0.205` (line 3); `scripts/vscode/Invoke-Restore.ps1` takes `SolutionPath`, `Configuration` and `Platform` (lines 1 to 10). `coverage.config` exists at the repository root and carries no `QfcDatamodel` entry. +11. Branch `bug/focus-and-theme-tests-leak-shared-dispatcher-setup-968` (the worktree's `.git` metadata reads `ref: refs/heads/bug/focus-and-theme-tests-leak-shared-dispatcher-setup-968`, and that ref resolves to `87cca65ede6a900cb5c4e5cce93ff4409ed08730` at authoring); BASE `94287369908cc920b21b0e3256314f988ad7d2f5` (origin/main at the branch cut, supplied by the caller and confirmed by both research headers). The branch now carries documentation-only commits above BASE (the coordinator reports `53d975270`, `d096f1250`, `4c6de5e84` and `87cca65ed`, and one further docs-only commit carrying this revised plan will precede execution), and `git diff --name-status 94287369 HEAD` lists only paths under FEATURE plus the two promoted records `docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md` and `docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md`; both records exist on the tree (Glob). The expectation is therefore stated by set membership, not by commit list: every path committed above BASE before P0-T3 runs must be under FEATURE or be one of those two records. HEAD is recorded as an observation in P0-T3, never as an expectation. This planning session has no shell, so the `.dotnet-sdk`, `packages` and `bin` trees were not probed; every bootstrap task is guarded and gated on its post-task marker. +12. Host constraint carried from #950 on this workstation (its `evidence/baseline/stall-probe.md`): the four UtilitiesCS.Test shell-icon classes did not stall but one test failed with `Win32 handle that was passed to Icon is not valid`, so the runner route (which throws before post-processing on any failure) could not produce figures and the DIRECT route was used. Whether that reproduces today is unknown, so P0-T16 measures it and the result selects the coverage route (D-6). +13. The #950 run observed the FluentAssertions `BeSameAs` failure shape on this fixture: `Expected observedByB to refer to because ..., but found System.Windows.Threading.Dispatcher { ... Name = "UiThreadDispatcherFixture.ParkedDispatcher" ... }`. The pin-count regression inverts the operands, so its expected message is `Expected afterFirstRelease to refer to System.Windows.Threading.Dispatcher { ... Name = "UiThreadDispatcherFixture.ParkedDispatcher" ... } because a holder that did not take the last pin must not lose the dispatcher, but found .`; the gate reads `to refer to`, `ParkedDispatcher`, the because text and `but found `, never the subject name (caller identification can fail and then prints `object`). The same `BeSameAs` shape over two `Task` operands is what the P5-T8 sensitivity check reads (`to refer to` plus the because fragment). +14. `QuickFiler/Controllers/QfcDatamodel.cs` is 495 lines; `[ExcludeFromCodeCoverage]` at 25 on the class declaration at 26 (a type-level attribute on one partial declaration applies to the whole type, so it covers `QfcDatamodel.QueueProcessing.cs` and `QfcDatamodel.FrameBuilding.cs` as well). `logger` 28 to 30; constructors 34 to 54 assign `RemainingEmailLoader = LoadRemainingEmailsToQueueAsync;` at 40 and 52 (method-group conversion to `Func>`, which binds the one-argument overload only); `Cleanup` 77 to 103 with the commented-out field nulls `//_blockingQueue = null;`, `//_priorityQueue = null;`, `//_queues = null;` at 99 to 101; `#region Private Variables` 107, blank 108, the duplicate `private static readonly log4net.ILog log = log4net.LogManager.GetLogger(` 109 to 111, `_globals` 112; `RemainingEmailLoader` doc 128 to 141 with the cref `LoadRemainingEmailsToQueueAsync(CancellationToken)` at 130; `WorkerStarter` 144 to 152; `SetupWorker` 188 to 195 with the commented subscription `//worker.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(Worker_RunWorkerCompleted);` at 194; `Worker_DoWork` 197 to 241 with the commented calls `//e.Result = await LoadRemainingEmailsToQueueAsync(bw, _token);` 209 and `//e.Result = LoadRemainingEmailsToQueue(bw, _token);` 210, `_remainingLoadTask = loaderTask;` 218, `_remainingLoadActive = false;` 227; blank 242; comment 243 to 245; `Worker_RunWorkerCompleted` 246 to 265; blank 266; `#endregion BackgroundWorker` 267; `InitEmailQueue` 271 to 315 (`_remainingLoadActive = true;` 284 and 311, `WorkerStarter(worker);` 285 and 312); `InitEmailQueueAsync` 317 to 333; the one-argument `LoadRemainingEmailsToQueueAsync(CancellationToken cancel)` 335 to 376 with `//logger.Debug($"{nameof(LoadRemainingEmailsToQueue)} Task cancelled");` at 363 and the live `$"{nameof(LoadRemainingEmailsToQueue)} Error. \n {e.Message}\n{e.StackTrace}"` at 369; blank 377; synchronous `private bool LoadRemainingEmailsToQueue(BackgroundWorker bw, CancellationToken token)` 378 to 416 (its own `nameof` uses at 404 and 410); blank 417; two-argument `private async Task LoadRemainingEmailsToQueueAsync(` 418 to 465 (`#pragma warning disable CS0618` 436, `#pragma warning restore CS0618` 457, the file's only pragmas; commented `nameof(LoadRemainingEmailsToQueueAsync)` 462); blank 466; `#endregion Email Queue Initial Setup` 467; blank 468; `#region Linked List Locking` 469; blanks 470 to 471; `#endregion Linked List Locking` 472; blank 473; `#region Event Handlers` 474; `Application_NewMailEx` 476 to 491. Seven `#region` and seven `#endregion` lines. Counts: `Worker_RunWorkerCompleted` 2 (194, 246); `nameof(LoadRemainingEmailsToQueue)` 4 (363, 369, 404, 410); `nameof(LoadRemainingEmailsToQueueAsync)` 1 (462); `LoadRemainingEmailsToQueueAsync(` 4 (130, 209, 335, 418); `LoadRemainingEmailsToQueue(BackgroundWorker bw` 1; `log4net.ILog log =` 1; `log4net.ILog logger =` 1; `Linked List Locking` 2; `#pragma` 2; `[ExcludeFromCodeCoverage]` 1; `//e.Result =` 2; `//_blockingQueue = null;` 1; `//worker.RunWorkerCompleted` 1; `ForEachAwaitWithCancellationAsync` 2 (the comment at 431 and the call at 440); `: IQfcDatamodel` 1. +15. Zero-caller proof for the four members (addendum section 3.2 and its `## Numeric Derivation Evidence`, re-run in this pass). Grep `Worker_RunWorkerCompleted|LoadRemainingEmailsToQueue\b|LoadRemainingEmailsToQueueAsync|Linked List Locking` over `*.cs`: 25 lines in 5 files (17 in `QfcDatamodel.cs`: 40, 52, 130, 194, 209, 210, 246, 335, 363, 369, 378, 404, 410, 418, 462, 469, 472); outside `QfcDatamodel.cs` the hits are `QuickFiler/Controllers/QfcHomeController.cs` 92, 132, 344, 379 (the `QfcHomeController` method of the same name), `QuickFiler.Test/Controllers/QfcHomeControllerRunAsyncTests.cs` 325 and 376 (a test method name, and a `GetMethod("Worker_RunWorkerCompleted", ...)` invoked on `_controller`, a `QfcHomeController`, at 373 to 380), and doc prose at `QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs` 104 and `QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs` 28 (both name the one-argument loader). Grep `\blog\b` over `QuickFiler/Controllers/QfcDatamodel*.cs`: 3 lines (the declaration at `QfcDatamodel.cs` 109, prose at `QfcDatamodel.QueueProcessing.cs` 71 and 90). String-literal and reflection sweep `"Worker_RunWorkerCompleted"|"LoadRemainingEmailsToQueue|"log"|nameof\(log\)|GetField\("log` over `*.cs`: 2 lines (`QfcHomeControllerRunAsyncTests.cs` 376, classified above, and the cref at `QfcDatamodel.cs` 130). `QuickFiler/Interfaces/IQfcDatamodel.cs` declares `DequeueNextItemGroupAsync` (103, 117), `DequeueNextItemGroupWithOutcomeAsync` (131), `DequeueNextItemGroup` (138), `UndoMove` (139), `MovedItems` (140), `InitEmailQueue` (141), `InitEmailQueueAsync` (142), `Complete` (148), `QuiesceLoaderAsync` (164) and `Cleanup` (166), none of the four. `QuickFiler/Controllers/QfcDatamodel.FrameBuilding.cs` references none of the four. `InternalsVisibleTo` grants under `QuickFiler/`: `QuickFiler.Test` (Properties/AssemblyInfo.cs 5, Controllers/QfcHomeController.cs 15) and `DynamicProxyGenAssembly2` (Legacy/IAcceleratorCallbacks.cs 5, Controllers/QfcHighConfidencePreFilter.cs 11); all four members are `private`. Primary and cross-check member sets are both exactly {`log`, `Worker_RunWorkerCompleted`, `LoadRemainingEmailsToQueue`, the two-argument `LoadRemainingEmailsToQueueAsync`}, count 4 and 4. P4-T1 re-runs both strategies at execution time. +16. `QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs` is 413 lines. `_remainingLoadActive` doc 15 to 23 (line 17 contains `each RunWorkerAsync() call`, line 22 contains `written on the worker thread and read by dequeue callers`), declaration 24; `_remainingLoadTask` doc 37 to 42, declaration 43; `QuiesceLoaderAsync` 48 to 66 with the comment `// Snapshot before the check: the field is written on the worker thread, so reading it` at 52; `TryUnhookOrReplace` declared at 146; the doc of `DequeueWithHighConfidenceGateWithOutcomeAsync` 280 to 291 with line 285 reading `/// UnhookItem throw path (:31-66) removes the failed`; the gate construction 299 to 309 with `() => _remainingLoadActive,` at 305 (the `sourceActive` lambda; the preceding `null,` at 304 and the following `firstBatchDeadline,` at 306) and `await gate.DequeueAsync(quantity, timeOut, _token);` at 311; `TryUnhookOrReplace(ref nodes, i);` 364; `WaitForQueue` 404 to 411 with `while (_remainingLoadActive && (_masterQueue?.Count < quantity))` at 406. Counts: `RunWorkerAsync` 1; `written on the worker thread and read` 1; `written on the worker thread` 2; `(:31-66)` 1; `TryUnhookOrReplace` 3; `WorkerStarter` 0; `RemainingEmailLoader` 1 (line 18); `share no other fence` 0; `honest producer-liveness signal` 1; `() => _remainingLoadActive,` 1; `() => false,` 0. Repository-wide `_remainingLoadActive|_remainingLoadTask` over `*.cs`: 20 lines in 7 files (production: `QfcDatamodel.cs` 218, 227, 284, 311; `QueueProcessing.cs` 24, 43, 54, 305, 406; tests by reflection only: `QfcDatamodelLivenessTests.cs` 169, `QfcDatamodelTests.cs` 114, 126, 266, 279, `QfcDatamodelTeardownTests.cs` 121, 151, 204, 233, `QfcQueuePurePathsTests.cs` 246, `QfcHomeControllerRunAsyncHighConfidenceTests.Part3.cs` 116). `QuickFiler/Controllers/QfcStreamingDequeueConfidenceGate.cs` `DequeueAsync` 190 to 301: `alreadyWaitedForEmptySource` 215; empty-take branch 244 to 257 (`sourceCanStillProduce = _sourceActive?.Invoke() == true;` 246, the exhaustion return 247 to 250, `alreadyWaitedForEmptySource = true;` 252, `await _timeProvider.Delay(TimeSpan.FromMilliseconds(timeOut), token).ConfigureAwait(false);` 253 to 255). The three datamodel awaits on the dequeue path carry no `ConfigureAwait(false)` (QueueProcessing 311 and its two callers), so they capture the caller's ambient context. +17. `QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs` is 312 lines, `[TestMethod]` 4. Usings 1 to 14 (`using Microsoft.Extensions.Time.Testing;` 9, `using Moq;` 12, `using UtilitiesCS;` 13); class doc 18 to 24 (about the reflection helpers; unchanged); blank 46; nested worker doc 47 to 52, class 53 to 56, blank 57, starter doc 58, starter 59 to 60, blank 61; `DrainableSynchronizationContext` doc 62 to 67 and class 68 to 87 (`Drain()` 78 to 86 asserts the creating thread at 80); `CreateHighConfidenceGlobals` 89 to 98; test 1 doc 100 to 108, attribute 109, declaration 110, body 111 to 164 (`var fake = new FakeTimeProvider();` 114, `var worker = new SynchronousBackgroundWorker();` 127, `model.WorkerStarter = StartSynchronously;` 128, `model.InitEmailQueue(0, worker);` 132, `pending` 138, `fake.Advance` 139, 141 and 156, `await Task.Yield();` 140, 142 and 157, `for (int i = 0; i < 20 && !pending.IsCompleted; i++)` 154, close 164); blank 165; `ReadLivenessFlag` 166 to 172; `StartHeldOpenLoader` doc 174 to 182, declaration 183 to 186, body 187 to 209 (`var worker = new SynchronousBackgroundWorker();` 201, `model.WorkerStarter = StartSynchronously;` 202, `model.InitEmailQueue(0, worker);` 203); test 2 211 to 234 (caller at 221); test 3 236 to 270 (caller at 249 inside `try` 247 to 265); test 4 272 to 310 (caller at 285 inside `try` 283 to 301). `FakeTimeProvider` occurs only at 114 (the `using Microsoft.Extensions.Time.Testing;` directive at 9 does not contain the type name); `SynchronousBackgroundWorker` 4 lines (53, 60, 127, 201); `StartSynchronously` 3 lines (59, 128, 202); `StartHeldOpenLoader` 4 lines (183, 221, 249, 285); `Task.Yield` 3; `fake.Advance` 3; `[Timeout` 0; `using (` 0; `await` 5 lines inside test 1 (124, 140, 142, 157, 163; the whole-file substring count is 10, the other five being doc or string text at 50, 102, 178, 263 and the test-4 lambda at 288). +18. `QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs` is 244 lines, `[TestMethod]` 5. `using Moq;` 12; blank 58; nested worker doc 59 to 65, class 66 to 69, blank 70, starter doc 71, starter 72 to 73, blank 74; `/// ` of the first test 75; `using (var worker = new BackgroundWorker { WorkerSupportsCancellation = true })` 180; `using (var worker = new SynchronousBackgroundWorker())` 220; `model.WorkerStarter = StartSynchronously;` 222. `SynchronousBackgroundWorker` 3 lines (66, 73, 220); `StartSynchronously` 2 lines (72, 222); `Duplicated per file` 1 (63). +19. `QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs` is 232 lines, `[TestMethod]` 3. `using Moq;` 12; remarks 23 to 35 with line 33 `/// the test thread through the nested SynchronousBackgroundWorker, so no test starts a`; `CreateInertRemainingEmailLoader` doc 93 to 100 with line 97 `/// Assigning this delegate before starting a real is what makes` and 98 `/// it safe to call with a real` and 99 `/// worker in a unit test.`; blank 113; nested worker doc 114 to 119, class 120 to 123, blank 124, starter doc 125, starter 126 to 127, blank 128; test 1 136 to 154 (`model.WorkerStarter = StartSynchronously;` 143, `IList result = null;` 144, `System.Action act = () =>` 147, `result = model.InitEmailQueue(0, new SynchronousBackgroundWorker());` 148, asserts 151 to 153); test 2 165 to 183 (`model.WorkerStarter = StartSynchronously;` 172, `var worker = new SynchronousBackgroundWorker();` 173, `model.InitEmailQueue(0, worker);` 176, asserts 179 to 182); test 3 195 to 230 (`model.WorkerStarter = StartSynchronously;` 202, `var result = model.InitEmailQueue(2, new SynchronousBackgroundWorker());` 221, asserts 224 to 229). `SynchronousBackgroundWorker` 6 lines (33, 120, 127, 148, 173, 221); `StartSynchronously` 4 lines (126, 143, 172, 202); `Duplicated per file` 1 (117); `using (` 0. +20. `QuickFiler.Test/Controllers/QfcDatamodelTests.cs` is 371 lines, `[TestMethod]` 9. `using Microsoft.Extensions.Time.Testing;` 9, `using Moq;` 12; sibling test `DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive` attribute 95, declaration 96, body 97 to 131 (no doc comment; `var fake = new FakeTimeProvider();` 99, `var worker = new BackgroundWorker();` 108, `SetPrivateField(model, "_remainingLoadActive", true);` 114, `pending` 116, `fake.Advance` 118 and 127, `await Task.Yield();` 119, the because text `the datamodel source-active signal must keep polling while the worker can still add candidates` 123, `SetPrivateField(model, "_remainingLoadActive", false);` 126, `IList result = await pending;` 128, `result.Should().BeEmpty();` 130); the next test's attribute at 133; `CreateUninitializedDatamodel` 201 to 202 and `SetPrivateField` 204 to 211 (declared after first use; legal); `WaitForQueue` test 253 to 283 (`var worker = new BackgroundWorker();` 261, `SetPrivateField(model, "_worker", worker);` 262, `await task;` 281, `task.IsCompleted.Should().BeTrue();` 282, close 283). `FakeTimeProvider` at 99, 216, 224, 249, 258 (five lines; the `using Microsoft.Extensions.Time.Testing;` directive at 9 does not contain the type name); `new BackgroundWorker()` 2 lines (108, 261); `Task.Yield` 1; `using (` 0. +21. Helper precedents: `UtilitiesCS.Test/TestHelpers/ArmingBarrierTimeProvider.cs` (55 lines; forwarding decorator with `Armed` 26, `ReArm()` 28, the `CreateTimer` override 39 to 49 that forwards then `_armed.TrySetResult(true)`, and `NewSignal()` 52 to 53 using `TaskCreationOptions.RunContinuationsAsynchronously`); `QuickFiler.Test/Controllers/QfcFormControllerSeamTests.cs` 357 to 367 (`private sealed class CountingTimeProvider : FakeTimeProvider` overriding `public override ITimer CreateTimer(TimerCallback cb, object s, TimeSpan due, TimeSpan p)`, which proves the override compiles in this project and that `TimeProvider.Delay` reaches `CreateTimer` here). Repository-wide `ArmingFakeTimeProvider|NoSynchronizationContext` over `*.cs`: 0 lines (both names are free). +22. Spec tokens at authoring (`docs/features/active/.../spec.md`, 334 lines): `- [ ] AC` 32 lines (275 to 306); `- [x] AC` 0; `- [ ] AC32:` 1; `Amendment 1.2` 1 (line 9); `Amendment 1.1` 1 (line 10); `acquired and released inside a held` 4 (10, 105, 266, 282); `the removal of its baseline pin` 1 (284); `inherited committed set` 2 (10, 294). Issue.md line 12 reads `- Work Mode: full-bug`; its lines 65 to 77 carry the Coordinator Scope Amendment. + +## Design decisions (do not redesign) + +- **D-1 Fixture fix (spec decision 1, research Approach A).** `UiThreadDispatcherFixture` gains two private statics without initializers, `_pinCount` and `_fixtureInstalledParked`, read and written only while `FieldLock` is held (Delivered Source F-FIELDS). `EnsureDispatcher` increments the count and, when the field is null, writes the parked dispatcher and sets the flag; it always returns `new EnsureScope(parked)` (F-ENSURE-BODY). `EnsureScope` keeps the parked reference; its idempotent `Dispose` decrements under `FieldLock` and, in the same critical section, writes null and clears the flag only when the count reaches zero, the flag is set and the field still references the parked instance (F-SCOPE). The nested class reaches `FieldLock`, `DispatcherField` and the two statics directly, so the re-locking `CompareExchange` helper is not called from the scope. D1 docs: F-CLASSDOC, F-ENSURE-DOC and the F-SCOPE doc. +- **D-2 R4 restructure: option (a), the baseline pin is removed.** The `using (IDisposable baseline = ...)` block that #950 added to `Transaction_SecondCallerCannotInstallUntilTheFirstRestores` is deleted and replaced by a `try/finally` over `transactionA` (Delivered Source R-BODY); the body lines keep their indentation because the `try` block replaces the `using` block one-for-one. Rationale: the pin fenced the gate-free writers of the two theme tests, which Phase 3 deletes; after the deletion the census (P7-T1, P7-T2) proves every remaining pin is acquired and released inside a held transaction, so while `transactionA` holds the gate no other class can write the field and every other transaction restores before it releases, which is all R4's two assertions need; the pin as placed was released after `transactionA.Dispose()` and after `transactionB` completed, so it outlived its gate hold, and it installed `liveA` over a pinned parked value, the only shape in the repository reaching the flag-true-but-field-changed branch. Option (b), releasing the pin before `transactionA.Dispose()`, was rejected because it keeps that shape and leaves the parked dispatcher installed with the flag set after every R4 run. R4's two assertions and their `because` texts are unchanged (gated by R4SPAN tokens). The `try/finally` is D4 and delivers #972 item 5 (spec decision 5, AC14). +- **D-3 Fail-before evidence.** The new test file is compiled against the unmodified fixture (Phase 1) and test 1 is run alone by fully qualified name, tagged `[expect-fail]` (P1-T5); tests 2 to 4 are run on the same unmodified fixture and must pass (P1-T6), which is the evidence for their "passes before and after" labels. The fixture fix alone is then applied (Phase 2) and the four tests are run again (P2-T8). The porcelain spans recorded at P1-T3 and P2-T8 show that the only difference between the two runs is the fixture file. +- **D-4 Temporary state and byte-level checks.** The only temporary edit in this plan is the P5-T8 sensitivity edit, which is reverted inside P5-T8 and proved reverted by an anchored `--exit-code` diff before any later task runs; every other edit is a delivered edit. Build gates use `/t:Build` for the scoped test runs (CMD-BUILD, gated on the test assembly timestamp advancing) and `/t:Rebuild` for the baseline and final analyzer and nullable gates (CMD-REBUILD, gated on zero `Skipping target "CoreCompile"` lines and at least one `Csc` output line per Write Set project). +- **D-5 Hang handling.** Every direct vstest run carries `/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None`; a `Sequence_*.xml` file in the results directory, or a `Timeout` or `Aborted` outcome, is a stop. +- **D-6 Coverage route is selected by a recorded observation.** P0-T16 runs the four shell-icon classes alone and records `STALL-PROBE: CLEAR` or `REPRODUCES`. `COVERAGE-ROUTE: RUNNER` (CLEAR) runs `scripts/vscode/Invoke-MSTestWithCoverage.ps1` verbatim (CLAUDE.md step 4). `COVERAGE-ROUTE: DIRECT` (REPRODUCES) issues the runner's own inner collector invocation with the four-class exclusion appended and post-processes with the runner's own helpers, because the runner hard-codes its filter (fact 9). Both routes yield the same committed forms: the `First-party coverage:` line, the JaCoCo package projection text and the trx-derived summary, transcribed into Markdown. Under DIRECT, AC22 cannot be met as worded (the runner was not run) and its check-off records `AC22: NOT MET (ENVIRONMENTAL: COVERAGE-ROUTE DIRECT)` for the orchestrator's decision, as the #950 run did. +- **D-7 Coverage obligations.** Every changed test file is in a test assembly, which the runner's derived settings exclude from instrumentation (`.*\.Test\.dll$`), and both changed production files belong to the `[ExcludeFromCodeCoverage]` type `QfcDatamodel` (fact 14), which the collector omits from the report altogether (an excluded type is absent, not reported at 0 percent), so no changed line has a coverage figure: `CHANGED-CODE-COVERAGE: NOT MEASURED (TEST ASSEMBLY EXCLUDED; QFCDATAMODEL EXCLUDED BY ATTRIBUTE)` is recorded at both stages, and no per-file or per-class coverage gate is authored on `QfcDatamodel` (spec decision 7). The first-party line and the root counters are recorded at both stages; the repository-wide rate is compared in two branches (denominators within 1 percent: tolerance 0.5 percentage points; otherwise recorded, not gated), because that rate is not reproducible across runs of an identical tree. AC23 is worded without a tolerance, so its check-off is MET only when both printed first-party percentages are not lower than baseline, otherwise NOT MET with the figures and `COVERAGE-VARIANCE` recorded for the orchestrator. The 80 percent line and 75 percent branch floors are applied by the runner or by its threshold functions under DIRECT. +- **D-8 Baseline-relative test outcomes.** The baseline full run (P0-T17) may contain pre-existing failures; they are recorded as `BASELINE-FAILED-SET:` and do not stop Phase 0. The final run passes only when its failed set contains no name absent from the baseline failed set (`NEW-FAILURES: NONE`) and all sixteen target tests (NAMES-TARGETS) are `Passed`. AC22 additionally requires the runner route and exit 0. +- **D-9 Anchor.** Every diff gate uses `94287369908cc920b21b0e3256314f988ad7d2f5` as its ref operand (`BASE` in prose). P0-T3 verifies it is an ancestor of HEAD and equals `git merge-base origin/main HEAD`; a mismatch is `BASE-SHA MISMATCH` (ancestor check fails) or `BASE AHEAD OF BRANCH` (merge-base differs): stop and report, because `refs/remotes/origin/main` is shared by every worktree and a fetch elsewhere can move it. Paths changed between BASE and HEAD at P0-T3 form the inherited set `INHERITED-COMMITTED:`; each must be under FEATURE or be one of the two promoted records `docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md` and `docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md` (fact 11), otherwise `INHERITED SET OUT OF SCOPE`: stop. Every footprint gate (P6-T9, P8-T9, P8-T46) excludes exactly the `INHERITED-COMMITTED:` paths, so it is consistent with AC20 as amended. Late in the run P8-T9 re-reads `git rev-parse origin/main`; a value other than BASE is recorded as `BASE REF MOVED` and does not stop the run, because every gate names BASE explicitly. +- **D-10 Commits.** Three commits: P0-T19 (FEATURE only), P6-T9 (the fourteen Write Set code files plus FEATURE, staged after the P6-T1 scoped format so the committed text is formatter-stable), P8-T46 (FEATURE). Each is a `git -C WORKTREE add -- ` invocation followed by a separate `git -C WORKTREE commit -m "" -- ` invocation, one command per call, never chained, never `git add -A`, and always pathspec-limited on the commit as well as the add, so a path staged by anything else can never be swept into a commit. No commit message contains an angle bracket, a dollar sign or a backtick. A PreToolUse refusal of any `git add`, `git commit`, `.cs` edit, `.csproj` edit, spec edit, evidence-file Write or pwsh payload (including a refusal whose text begins `PREIMPLEMENTATION_GATE_BLOCKED`) is recorded verbatim as `PRE-IMPLEMENTATION GATE BLOCKED` and stops the run; the executor does not retry with a rephrased edit or another tool and does not modify hooks, checkpoints or permission configuration. +- **D-11 Git gates are pathspec-scoped and anchored.** Every `git diff` names BASE or HEAD as its ref operand; every name-listing diff is paired with a porcelain span in the same task; no gate asserts an unscoped empty porcelain. Uncommitted `.claude/agent-memory/` paths and this plan file may appear in porcelain output and are admitted by every scope gate; their count is deliberately unrecorded. +- **D-12 Check-offs follow the loop.** Every check-off task sits in Phase 8 after the final toolchain pass and reads an artifact that survived it. Each flips exactly one checkbox and, when its evidence does not hold, completes with the box unchecked and records `ACn: NOT MET` with the reason in `FEATURE/evidence/other/ac-status-summary.md`. +- **D-13 Restart rules.** Phase 6: if P6-T4, P6-T5, P6-T6, P6-T7 or P6-T8 shows a target test not `Passed`, the executor corrects the Write Set file at fault (within the delivered design; no prohibited construct) and restarts at P6-T1, recording `P6-RESTART: n` in the restarted artifacts (when a P6-T9 commit is already in HEAD because a Phase 8 restart preceded this one, the P6-T2 ref-operand and porcelain rule stated below applies to this restart as well). Phase 8: `ITERATION` starts at 1. If P8-T1 rewrites any Write Set file, the executor commits exactly the rewritten Write Set files (`style(968): apply csharpier output`, pathspec-limited), increments ITERATION and restarts at P8-T1. If P8-T2, P8-T3, P8-T4 or P8-T5 fails because of a Write Set file, the executor corrects it, restarts at P6-T1 (scoped format, census, build, pass-after runs, commit), re-runs Phase 7 in full, increments ITERATION and resumes at P8-T1. On that restart the P6-T9 commit is already in HEAD, so P6-T2 runs each of its HEAD-anchored git commands with 94287369908cc920b21b0e3256314f988ad7d2f5 as the ref operand instead (the numstat rows it gates and the FIELDLOCK-ENCLOSURE diff are read from those), and its porcelain expectation becomes: every porcelain line under QuickFiler/ or QuickFiler.Test/ names one of the fourteen Write Set code paths with status ` M`, recorded as `P6-RESTART-PORCELAIN:`. A rewrite of a file outside the Write Set, or a failure not attributable to the Write Set beyond the D-8 baseline rule and the P8-T5 `LEAK-DEPENDENT TEST EXPOSED` rule, is a stop with the failing artifact. P8-T7 records the final iteration only. +- **D-14 Line endings of new files.** The ten existing Write Set `.cs` files are CRLF in the worktree (fact header). P1-T1, P4-T2 and P5-T2 normalise each new file to CRLF with CMD-EOL after writing it, so `csharpier check .` and the committed blob (normalised to LF by `text=auto`) behave as for the siblings; the gate is `BARE_LF: 0`. +- **D-15 One shared synchronous worker (#972 item 1; addendum section 1.4).** New file `QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs` declares `internal sealed class SynchronousBackgroundWorker : BackgroundWorker` in namespace `QuickFiler.Test.TestSupport` with `internal void RaiseDoWork()` and `internal static void StartSynchronously(BackgroundWorker worker)` (Delivered Source W1). It adds no fields, handles or subscriptions and does not override `Dispose(bool)`; disposal stays with the constructing test (D-17). The three nested classes and three private starters are deleted; each consumer adds `using QuickFiler.Test.TestSupport;` in alphabetical position (after `using Moq;`) and assigns `model.WorkerStarter = SynchronousBackgroundWorker.StartSynchronously;`. Doc rewording F5 (the ZeroBatch remarks line 33) and F6 (ZeroBatch lines 97 to 99) are applied; the Liveness and Teardown class headers describe the reflection helpers, not the worker, and are unchanged (addendum section 6.3). +- **D-16 Dead-code removal with a prior zero-caller proof (#972 item 3; spec decision 7).** P4-T1 re-runs the two search strategies of fact 15 against the pre-change tree with CMD-LEGACY-CALLERS, classifies every hit, compares the member sets and records the proof before any removal. P4-T8 then removes the four members, the commented-out references at pre-edit lines 99 to 101, 194, 209 to 210 and 363, and the empty region, and retargets the `nameof` at pre-edit line 369 to `LoadRemainingEmailsToQueueAsync` (F1 to F3). This is unreachable dead code: it has no behaviour to regress, so it takes no failing test; the compile proof is the two rebuilds (P8-T3, P8-T4), which fail on any surviving reference. Expected physical line count after the edit: 495 minus 128 = 367 (fact 14 and the removal breakdown under Delivered Source P1); the gate is at most 400 (AC28). `[ExcludeFromCodeCoverage]` is unchanged (AC29). +- **D-17 Caller-owned worker disposal (#972 item 4; addendum section 4).** Every `SynchronousBackgroundWorker` and every test-created `BackgroundWorker` in `QfcDatamodelLivenessTests.cs`, `QfcInitEmailQueueZeroBatchTests.cs` and `QfcDatamodelTests.cs` is constructed in the header of a `using (...) { }` block owned by the test method (never `using var`: `QuickFiler.Test.csproj` sets no `LangVersion`, and the block form is what `QfcDatamodelTeardownTests.cs` 180 and 220 already use). `StartHeldOpenLoader` takes the worker as its first parameter and no longer constructs one; its three callers own the worker. The datamodel never disposes `_worker` (addendum section 4.1), so test-side disposal cannot double-dispose, and `Component.Dispose()` is idempotent. +- **D-18 Deterministic liveness tests (#968 comment; addendum section 5.4, with the context scope scrutinised).** New file `QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs` declares `internal sealed class ArmingFakeTimeProvider : FakeTimeProvider` with `internal Task Armed`, `internal void ReArm()` and a `CreateTimer` override that calls the base first and then `TrySetResult(true)` on a signal created with `TaskCreationOptions.RunContinuationsAsynchronously` (Delivered Source W2; precedents in fact 21). Both dequeue-liveness tests take the shape: arm-check (`clock.Armed.IsCompleted` is true before the dequeue call returns, because the gate reaches its first `Delay` synchronously), `ReArm()`, one `Advance(200)`, `Task first = await Task.WhenAny(clock.Armed, pending)`, `first.Should().BeSameAs(clock.Armed, ...)`, `pending.IsCompleted` false, then the flag transition, one more `Advance(200)` and `await pending` as the completion signal. The re-arm proof is context-independent because the gate's own await is `ConfigureAwait(false)` (fact 16): whether its continuation runs inline inside `Advance` or on the pool, `Armed` completes once the gate re-arms, and if the gate returned instead, `pending` wins the race and the assertion fails crisply rather than hanging. In the Liveness test the loader's completion must clear the flag before the final advance, so the datamodel calls and the `loaderRelease.SetResult(true)` are made inside a `NoSynchronizationContext()` scope: a nested `IDisposable` that captures `SynchronizationContext.Current`, sets it to null, and restores the captured value on dispose. The scope is sound because (i) `SetSynchronizationContext` is a per-thread setting, (ii) neither scope body contains an `await`, so the restore runs on the same thread that took the scope and before any continuation of the test method can move it to another thread (this is gated: the T1-LIVE span contains exactly two `using (NoSynchronizationContext())` lines and exactly three `await` lines, none of them inside a scope body), and (iii) an await registered under a null context with the default scheduler runs its continuation inline on the completing thread, which is what makes `ReadLivenessFlag(model)` read false before the final advance; if that TPL rule were ever violated the flag checkpoint fails crisply, which is the behaviour the addendum requires. The sibling test in `QfcDatamodelTests` writes the flag by reflection, so it needs no scope; its final `await pending` depends only on the ambient context being serviced, which is the same pre-existing dependence every other awaiting test in that file has. No `Thread.Sleep`, `Task.Delay`, retry, bounded loop, `Task.Yield`, timeout attribute (none exists on these tests, so none is added), `[DoNotParallelize]` or Workers change is introduced. +- **D-19 Fail-before exception and sensitivity check (spec decision 7; addendum section 5.5).** A deterministic failing run of the old test shape is structurally impossible: its failure requires the thread pool to delay a queued continuation past a bounded retry, which no test input can force, and the production behaviour is correct before and after. P5-T1 therefore writes the dossier `FEATURE/evidence/regression-testing/fail-before-exception..md` with `WhyFailingRunImpossible:` and an alternative-proof section BEFORE the rewrite. P5-T8 then performs the labelled sensitivity check: the `sourceActive` lambda at `QfcDatamodel.QueueProcessing.cs` 305 is edited in the working copy from `() => _remainingLoadActive,` to `() => false,`, the tree is built, each rewritten test is run by fully qualified name and must fail on its re-arm assertion (`to refer to` plus its because fragment; not `Timeout`, not `Aborted`), the edit is reverted with the Edit tool, and the revert is proved by `git -C WORKTREE diff --exit-code 94287369908cc920b21b0e3256314f988ad7d2f5 -- QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs` exiting 0 (the file is at BASE content until P5-T11 edits its comments, which is why the comment edits come after the check). P5-T9 rebuilds the reverted tree and P5-T10 re-runs the pair green before any later gate. The sensitivity edit is never committed: P5-T8 is the only task that touches that line, and the P6-T9 commit is preceded by the P6-T2 census, which reads `() => _remainingLoadActive,` 1 and `() => false,` 0. +- **D-20 Comment-only production edit and the QuiesceLoaderAsync decision (#972 item 2).** The `_remainingLoadActive` doc (QueueProcessing 15 to 23) is replaced by Delivered Source Q1 and the `(:31-66)` range is dropped from line 285 (Q2); the declaration and every statement are unchanged, so the file stays 413 lines. The `QuiesceLoaderAsync` comment at line 52 (`the field is written on the worker thread`) is about `_remainingLoadTask`, which `Worker_DoWork` writes at `QfcDatamodel.cs` 218 on the thread that runs the handler; in production that is the `BackgroundWorker`'s pool thread (the production `WorkerStarter` calls `RunWorkerAsync()`), and `QuiesceLoaderAsync` races a live loader only in production. The comment is therefore accurate post-#950 and is left unchanged; P5-T12 records `QUIESCE-COMMENT-DECISION: UNCHANGED` with this reason. The token `written on the worker thread` consequently reads 1 after the change (line 52) while the AC26 token `written on the worker thread and read` reads 0. + +## Risks (recorded; no mitigation is in scope) + +- **AC22 under the DIRECT route.** If P0-T16 records `REPRODUCES`, the runner is not run and AC22 ends `NOT MET (ENVIRONMENTAL: COVERAGE-ROUTE DIRECT)`; the orchestrator decides, as for #950 AC17. +- **AC23 has no tolerance.** The repository-wide first-party rate varied by 0.01 percentage points between two #950 runs of an identical instrumented tree; a downward variation of that size fails AC23 as worded although no instrumented line changed. D-7 records the figures and the executor does not weaken the criterion. +- **CSharpier layout.** The P6-T1 scoped format may re-lay the delivered C# (chained assertions, the multi-line `if` in F-SCOPE, the `using (` headers, the long `because` strings). Its output wins. Every gate token below is a string literal, a single-line statement or a doc-comment line, none of which CSharpier splits or joins, so no gate depends on the layout. +- **Tests that relied on the leaked parked dispatcher.** The two deleted theme-test calls left the parked dispatcher installed for the rest of a run, and every later transaction restored it. Production QuickFiler code reads `UiThread.Dispatcher` in many places, so a test that depended on that leak now throws `The UI dispatcher has not been captured`. P8-T5 captures every non-passed message, records such a name as `LEAK-DEPENDENT TEST EXPOSED:` and stops for re-planning under the related-defect directive; it is neither a D-13 restart nor `NEW FAILURE OUTSIDE SCOPE`. +- **Timer mechanics of the rewritten liveness tests.** D-18 relies on `FakeTimeProvider.Advance` invoking due callbacks synchronously and on `TimeProvider.Delay` reaching the overridable `CreateTimer` (fact 21 precedents; addendum section 5.2 web-verified). If either assumption failed, P5-T7 would fail or hang under the 4-minute blame bound, and P5-T8 shows each test fails crisply rather than hangs when the liveness signal is wrong. + +## Delivered source (the executor writes these texts; CSharpier output wins on any layout difference) + +Every block below except N1, T1, W1 and W2 is shown at its in-file indentation (four, eight, twelve, sixteen or twenty leading spaces) and is written exactly as shown. N1, T1, W1 and W2 carry one extra four-space Markdown indent on every line, which the executor removes: the `using` and `namespace` lines of N1, W1 and W2 start in column 1, and T1 and the T2 lines start with four spaces. Prose-quoted gate tokens are each confined to one physical line of the delivered text. Every edit to an existing file is an in-place edit of the named lines (pre-edit numbering, facts 1 to 4 and 14 to 20); no existing file is rewritten whole. + +**F-FIELDS — `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs`, inserted after line 38** (`private static Dispatcher _parkedDispatcher = null;`): one blank line, then four lines: + + // Issue #968: the count of live ensure scopes and whether the fixture itself seeded the parked + // dispatcher into a null field. Both are read and written only while FieldLock is held. + private static int _pinCount; + private static bool _fixtureInstalledParked; + +**F-CLASSDOC — the same file, inserted after line 30** (`/// `, the last line of the design-note paragraph) and before line 31 (`/// `), thirteen lines: + + /// + /// Issue #968: ensure pins are reference counted. A pin counter and an install-ownership flag + /// live under FieldLock. The first pin on a null field seeds the parked dispatcher + /// and sets the flag; the last release writes null back only when the flag is set and the + /// field still holds the parked instance, then clears the flag. A discarded scope therefore + /// pins for the process lifetime and leaves the parked dispatcher installed, so every caller + /// disposes its scope, and every pin is acquired and released while its caller holds a + /// transaction, which keeps the count at zero whenever a transaction is acquired. Residual: a + /// transaction that installs over a pinned parked value and restores it after the last pin + /// released leaves the parked value installed with zero pins and the flag set; the next pin + /// cycle reverts it. No test in this assembly installs over a pinned value, so the residual is + /// documented rather than exercised. + /// + +Gate tokens quoted from F-CLASSDOC: install-ownership flag; pins for the process lifetime. + +**F-ENSURE-DOC — the same file, replaces lines 116 to 121** (the `EnsureDispatcher` summary), nine lines replacing six: + + /// + /// Takes one counted pin on the shared static (issue #968). The first pin on a null + /// field seeds the parked dispatcher and records that the fixture owns the seeding; a pin + /// taken while the field is non-null installs nothing. Disposing the returned scope releases + /// the pin, and the field reverts to null only on the last release, only when the + /// fixture owns the seeding, and only when the field still holds the parked instance. Never + /// acquires TransactionGate and never blocks on anything a caller must release. A + /// discarded scope pins for the process lifetime, so every caller disposes its scope. + /// + +**F-ENSURE-BODY — the same file, replaces lines 128 to 137** (from `lock (FieldLock)` through `return new EnsureScope(null);`; the comment 124 to 125, the `parked` local 126, the blank 127 and the method close 138 are unchanged), eleven lines replacing ten: + + lock (FieldLock) + { + _pinCount++; + if (DispatcherField.GetValue(null) == null) + { + DispatcherField.SetValue(null, parked); + _fixtureInstalledParked = true; + } + } + + return new EnsureScope(parked); + +**F-SCOPE — the same file, replaces lines 243 to 274** (the `EnsureScope` documentation and class), forty-three lines replacing thirty-two: + + /// + /// The scope returned by : one counted pin. Disposal is + /// idempotent and performs the decrement and the conditional revert inline in one + /// FieldLock critical section, so no other pin can interleave between them. The revert + /// writes null only when this release brings the count to zero, the fixture itself + /// seeded the parked dispatcher, and the field still holds that instance; a value some other + /// owner installed in the meantime is left in place. + /// + private sealed class EnsureScope : IDisposable + { + private readonly Dispatcher _parked; + private bool _disposed = false; + + internal EnsureScope(Dispatcher parked) + { + _parked = parked; + _disposed = false; + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + lock (FieldLock) + { + _pinCount--; + if ( + _pinCount == 0 + && _fixtureInstalledParked + && ReferenceEquals(DispatcherField.GetValue(null), _parked) + ) + { + DispatcherField.SetValue(null, null); + _fixtureInstalledParked = false; + } + } + } + } + +After F-FIELDS to F-SCOPE the fixture is 375 lines before formatting; `_pinCount` occurs on exactly four lines (declaration, `_pinCount++;`, `_pinCount--;`, `_pinCount == 0`), `_fixtureInstalledParked` on exactly four (declaration, `_fixtureInstalledParked = true;`, `&& _fixtureInstalledParked`, `_fixtureInstalledParked = false;`), `lock (FieldLock)` on five (66, 79, 94, ENSURE, SCOPE; the F-FIELDS comment deliberately does not contain that literal), `CompareExchange(` on two (the helper's declaration and the transaction's call), `return new EnsureScope(` on one, and neither identifier appears in any comment. + +**N1 — `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs`, new file, whole content:** + + using System; + using System.Threading.Tasks; + using System.Windows.Threading; + using FluentAssertions; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + namespace QuickFiler.Controllers.Tests + { + /// + /// Issue #968 tests for the reference-counted ensure pin of + /// . The regression lives at the fixture level because + /// the theme tests the issue was filed against never read the shared static: their path + /// dispatches through the theme's injected IUiDispatcher mock, so no value in the static + /// can make a theme test fail, while the defect (the installing pin's release writing + /// null while another pin is still live) is observable here on one thread with no + /// concurrency. + /// + /// Every test acquires a transaction first, so the pin count is zero and the baseline is known + /// for the whole test, and carries the 60-second MSTest timeout of the sibling fixture test + /// file. No sleep, delay, wall-clock wait, mock or temporary file is used; Moq is therefore + /// not imported. + /// + /// + [TestClass] + public class QfcItemController_UiThreadDispatcherPinCountTests + { + private const int GateTimeoutMs = 60000; + + /// + /// Regression test: fails before the fix. With two pins held on a null baseline, releasing + /// the first pin must leave the parked dispatcher in place. Before counting, the first pin + /// was the installer and its release wrote null while the second pin was still live. + /// + [TestMethod] + [Timeout(GateTimeoutMs)] + public async Task EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease() + { + // Arrange + UiThreadDispatcherTransaction transaction = await UiThreadDispatcherFixture + .BeginTransactionAsync() + .ConfigureAwait(false); + try + { + transaction.Install(null); + IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + Dispatcher afterBothPins = UiThreadDispatcherFixture.Current; + + // Act + pinA.Dispose(); + Dispatcher afterFirstRelease = UiThreadDispatcherFixture.Current; + pinB.Dispose(); + Dispatcher afterLastRelease = UiThreadDispatcherFixture.Current; + + // Assert + afterBothPins + .Should() + .NotBeNull( + because: "the first pin seeds the parked dispatcher into a null field" + ); + afterFirstRelease + .Should() + .BeSameAs( + afterBothPins, + because: "a holder that did not take the last pin must not lose the dispatcher" + ); + afterLastRelease + .Should() + .BeNull(because: "the last release reverts the fixture's own seeding"); + } + finally + { + transaction.Dispose(); + } + } + + /// + /// Specification test: passes before and after the fix. Releasing the pins in the reverse + /// order must produce the same outcome, so the count rather than the identity of the + /// installing scope decides when the field reverts. + /// + [TestMethod] + [Timeout(GateTimeoutMs)] + public async Task EnsureDispatcher_TwoPinsHeld_ReleaseOrderDoesNotChangeTheOutcome() + { + // Arrange + UiThreadDispatcherTransaction transaction = await UiThreadDispatcherFixture + .BeginTransactionAsync() + .ConfigureAwait(false); + try + { + transaction.Install(null); + IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + Dispatcher afterBothPins = UiThreadDispatcherFixture.Current; + + // Act + pinB.Dispose(); + Dispatcher afterFirstRelease = UiThreadDispatcherFixture.Current; + pinA.Dispose(); + Dispatcher afterLastRelease = UiThreadDispatcherFixture.Current; + + // Assert + afterBothPins + .Should() + .NotBeNull( + because: "the first pin seeds the parked dispatcher into a null field" + ); + afterFirstRelease + .Should() + .BeSameAs( + afterBothPins, + because: "a holder that did not take the last pin must not lose the dispatcher" + ); + afterLastRelease + .Should() + .BeNull(because: "the last release reverts the fixture's own seeding"); + } + finally + { + transaction.Dispose(); + } + } + + /// + /// Specification test: passes before and after the fix; extends the existing fixture test + /// EnsureDispatcher_WhileATransactionHoldsALiveDispatcher_DoesNotReplaceIt. While a + /// transaction holds a live dispatcher, two pins install nothing, and releasing both must + /// leave the live dispatcher in place: the count reaching zero writes nothing because the + /// fixture did not seed the field. The live dispatcher is shut down in a finally block. + /// + [TestMethod] + [Timeout(GateTimeoutMs)] + public async Task EnsureDispatcher_UnderATransactionHoldingALiveDispatcher_ReleasingAllPinsLeavesTheLiveDispatcher() + { + // Arrange + Dispatcher live = QfcItemControllerTestSupport.StartRunningDispatcher(); + try + { + UiThreadDispatcherTransaction transaction = await UiThreadDispatcherFixture + .BeginTransactionAsync() + .ConfigureAwait(false); + try + { + transaction.Install(live); + IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + + // Act + pinA.Dispose(); + pinB.Dispose(); + Dispatcher afterAllReleased = UiThreadDispatcherFixture.Current; + + // Assert + afterAllReleased + .Should() + .BeSameAs( + live, + because: "pins that installed nothing must not write over the transaction value " + + "when the count reaches zero" + ); + } + finally + { + transaction.Dispose(); + } + } + finally + { + QfcItemControllerTestSupport.ShutdownDispatcher(live); + } + } + + /// + /// Specification test: passes before and after the fix. After a full two-pin cycle inside the + /// same transaction, a fresh single pin on the null baseline must still seed the parked + /// dispatcher and its release must still restore null. A second transaction then installs + /// that parked instance as its own value, and a pin taken and released under it must leave + /// the value in place: had the earlier cycle's last release left the install-ownership flag + /// set, this release would revert a value the fixture did not seed. + /// + [TestMethod] + [Timeout(GateTimeoutMs)] + public async Task EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores() + { + // Arrange + Dispatcher parked; + UiThreadDispatcherTransaction transaction = await UiThreadDispatcherFixture + .BeginTransactionAsync() + .ConfigureAwait(false); + try + { + transaction.Install(null); + IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + parked = UiThreadDispatcherFixture.Current; + pinA.Dispose(); + pinB.Dispose(); + + // Act + IDisposable freshPin = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + Dispatcher afterFreshPin = UiThreadDispatcherFixture.Current; + freshPin.Dispose(); + Dispatcher afterFreshRelease = UiThreadDispatcherFixture.Current; + + // Assert + afterFreshPin + .Should() + .NotBeNull( + because: "a pin on a null field seeds the parked dispatcher whatever earlier cycles did" + ); + afterFreshRelease + .Should() + .BeNull( + because: "the fresh pin is the only live pin, so its release reverts the seeding" + ); + } + finally + { + transaction.Dispose(); + } + + // Act (second transaction): the parked instance is now a transaction value, not a seeding + UiThreadDispatcherTransaction foreignTransaction = await UiThreadDispatcherFixture + .BeginTransactionAsync() + .ConfigureAwait(false); + try + { + foreignTransaction.Install(parked); + IDisposable foreignPin = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); + foreignPin.Dispose(); + Dispatcher afterForeignRelease = UiThreadDispatcherFixture.Current; + + // Assert + afterForeignRelease + .Should() + .BeSameAs( + parked, + because: "the last release cleared the install-ownership flag, so a pin that seeded nothing leaves a transaction value in place" + ); + } + finally + { + foreignTransaction.Dispose(); + } + } + } + } + +N1 counts (each token on one physical line): `EnsureUiThreadDispatcher()` 10 (tests 1, 2 and 3 two each, test 4 four); bare `EnsureDispatcher` 15 lines (the 10 invocations, the 4 method names, the test 3 doc reference); `Regression test: fails before the fix` 1; `Specification test: passes before and after the fix` 3; `never read the shared static` 1; `[TestClass]` 1; `[TestMethod]` 4; `[Timeout(GateTimeoutMs)]` 4; `private const int GateTimeoutMs = 60000;` 1; `transaction.Install(null);` 3; `transaction.Install(live);` 1; `transaction.Dispose();` 4 (case-sensitive: `foreignTransaction.Dispose();` does not match because of the capital T); `QfcItemControllerTestSupport.ShutdownDispatcher(live);` 1; `a holder that did not take the last pin must not lose the dispatcher` 2; `the last release reverts the fixture` 2; `using Moq;` 0; `Thread.Sleep` 0; `Task.Delay` 0; `public class QfcItemController_UiThreadDispatcherPinCountTests` 1; `foreignTransaction.Install(parked);` 1; `.BeginTransactionAsync()` 5. Within each transaction, its `.Install(` line precedes every pin acquired under it, and every pin is disposed before that transaction's first `Dispose();`. + +**T1 — `QuickFiler.Test/QuickFiler.Test.csproj`, one line inserted after line 203** (` `), four leading spaces after the Markdown indent is removed: + + + +**T2 — the same file, one line inserted after the line ` `** (pre-edit line 228; line 229 once T1 is applied) by P4-T3, and a second line inserted immediately after that new line by P5-T2; four leading spaces each: + + + + +After T1 and both T2 lines the project file carries three more `` becomes line 99). Eighteen lines removed. + +**A2 — the same file, two edits, applied after A1 (so by content, not by pre-edit line number):** (i) every line whose trimmed text is `var viewer = BuildExecutingViewer();` (seven lines) becomes, at the same twelve-space indentation, `var viewer = QfcItemControllerTestSupport.BuildExecutingViewer();`; (ii) the six-line comment block beginning `// Cycle-3 P9-T5/P9-T6 (members #33/#35, de-exempted); cycle-4 remediation R1: the entire body` and ending `// state machine) and assert the resulting state transitions, not merely the Invoke marshal.` is replaced by these seven lines (eight-space indent): + + // Cycle-3 P9-T5/P9-T6 (members #33/#35, de-exempted); cycle-4 remediation R1: the entire body + // runs inside a single _itemViewer.Invoke(...) delegate. The shared + // QfcItemControllerTestSupport.BuildExecutingViewer() executes the delegate synchronously + // (issue #968 removed this file's private copy) and EnableHandlelessThemeInvoke() populates + // the terminal _themes[_activeTheme].SetQfcTheme(async: false) call's dependencies with + // handle-less doubles, so these tests exercise the full method body (the _activeUI/_activeTheme + // state machine) and assert the resulting state transitions, not merely the Invoke marshal. + +**A3 — the same file, `SetThemeDark_FromNormal_SelectsDarkNormalTheme`:** the two comment lines beginning `// Arrange — async:true queues the theme application on the dispatcher without executing it,` and `// so no handle-less control is touched; the observable effect is the active-theme switch.` and the following line `QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` (three lines, pre-A1 450 to 452) are replaced by these five lines (twelve-space indent): + + // Arrange — async:true queues the theme application through the theme's injected + // IUiDispatcher mock (see BuildColorTheme), which absorbs the delegate without running it, + // so no handle-less control is touched and the shared UiThread static is irrelevant to this + // path (issue #968 deleted the former ensure call); the observable effect is the + // active-theme switch. + +**A4 — the same file, `SetThemeLight_FromNormal_SelectsLightNormalTheme`:** the line `// Arrange` and the following line `QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` (pre-A1 467 to 468) are replaced by these two lines (twelve-space indent): + + // Arrange — same injected-mock arrangement as SetThemeDark_FromNormal_SelectsDarkNormalTheme: + // the theme's IUiDispatcher mock absorbs the queued application (issue #968). + +After A1 to A4 the file is 482 lines before formatting (497 minus 18 plus 1 plus 2). Gate tokens quoted from A2 to A4: QfcItemControllerTestSupport.BuildExecutingViewer(); absorbs the delegate without running it; shared UiThread static is irrelevant; absorbs the queued application. + +**S1 — `QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs`, replaces lines 216 to 237** (the `EnsureUiThreadDispatcher` documentation; the declaration 238 and body 239 are unchanged), twenty-four lines replacing twenty-two: + + /// + /// Takes one reference-counted pin on the shared static UiThread.Dispatcher through + /// (issue #968): the first pin on a + /// null field seeds a dedicated dispatcher hosted on a parked background thread that is + /// never pumped, later pins install nothing, and the field reverts to null only when + /// the last live pin releases and the fixture itself seeded it. The remaining legitimate + /// callers are the fixture tests QfcItemController_UiThreadDispatcherFixtureTests and + /// QfcItemController_UiThreadDispatcherPinCountTests, each of which acquires and + /// releases its pin while holding a . + /// + /// A dedicated (non-CurrentDispatcher) instance is used deliberately for test + /// isolation: fire-and-forget BeginInvoke/InvokeAsync operations posted to the + /// parked dispatcher are enqueued and never execute, so they cannot leak onto the test + /// thread's own dispatcher and be run (and fault on a handle-less control) by an unrelated + /// later test that pumps Dispatcher.CurrentDispatcher. + /// + /// + /// Dispose the returned scope inside the same transaction that was held when it was taken: a + /// discarded scope pins for the process lifetime, and a pin released outside its caller's + /// transaction is released while another class may hold the gate. The implementation lives + /// in , the single owner of every mutation of that + /// static made from this assembly's owned files. + /// + /// + +Gate tokens quoted from S1: remaining legitimate; QfcItemController_UiThreadDispatcherPinCountTests. + +**S2 — the same file, replaces lines 282 to 288** (the `BuildExecutingViewer` documentation; applied after S1, at which point these lines are 284 to 290), seven lines replacing seven: + + /// + /// Issue #480 shared arrange helper. Builds a viewer mock whose Invoke and + /// BeginInvoke execute the supplied delegate synchronously, so a dispatch made through + /// either path produces a countable call on whatever collaborator the delegate targets. Since + /// issue #968 this is the single implementation; QfcItemController.FocusAndThemeTests.cs calls + /// it instead of carrying a private copy. + /// + +After S1 and S2 the file is 442 lines before formatting. + +**R-DOC — `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs`, replaces lines 196 to 208** (the R4 `` ... ``), thirteen lines replacing thirteen: + + /// + /// Issue #950 traced the earlier intermittent failure to a race on the shared static, not to + /// a timing defect: a gate-free ensure call in another class could seed the parked dispatcher + /// between the baseline read and the install, or between the restore and the second caller's + /// read, and the test pinned a non-null baseline inside the gate to fence it. Issue #968 + /// removed that pin: the fixture now counts pins, so only the last release can revert the + /// fixture's own seeding, and every remaining ensure call is acquired and released while its + /// caller holds a transaction, so no class can write the field while transaction A holds the + /// gate and every other transaction restores before it releases (W3/W4). Invariant for future + /// editors: a pin must stay nested inside its caller's transaction, and UiThread.Initialize + /// (W5) must not latch during this test; either would change the value the second caller + /// observes. + /// + +Gate tokens quoted from R-DOC: removed that pin: the fixture now counts pins; (W5) must not latch. + +**R-BODY — the same file, R4 body, two edits against the pre-edit numbering (R-DOC replaces thirteen lines with thirteen, so the numbering below holds whether R-DOC is applied first or second; the plan applies R-DOC first):** + +1. Replace lines 221 to 224 (the four-line `using (` header `using (` / `IDisposable baseline = QfcItemControllerTestSupport.EnsureUiThreadDispatcher()` / `)` / `{`) with these two lines (sixteen-space indent): + + try + { + +2. Replace line 270 (the sixteen-space `}` that closed the `baseline` using block, the line immediately after the `}` closing the `secondCallerStarted` using block and immediately before the twelve-space `}` closing the outer `try`) with these five lines: + + } + finally + { + transactionA.Dispose(); + } + +Lines 225 to 269 (the `original` read, `transactionA.Install(liveA);`, the waiter, the explicit `transactionA.Dispose();` at 251, both assertions) are unchanged in text and indentation, because the `try` block body sits at the same twenty-space indentation the `using` block body had. The file is 472 lines before formatting (470 minus 4 plus 2 minus 1 plus 5). Within R4 after the edit: `EnsureUiThreadDispatcher()` 0, `using (` 1, `transactionA.Dispose();` 2, `finally` 3 (the waiter's, the new one, the `ShutdownDispatcher` one), `.BeSameAs(` 1, `.NotBeSameAs(` 1, `issue #230 lost update` 1. + +**W1 — `QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs`, new file, whole content (the four-space Markdown indent is removed):** + + using System.ComponentModel; + + namespace QuickFiler.Test.TestSupport + { + /// + /// Test-side worker whose raises DoWork synchronously on the + /// calling thread through the protected OnDoWork, so a privately subscribed handler such + /// as QfcDatamodel.Worker_DoWork runs to its first incomplete await before + /// InitEmailQueue returns, and no worker a test starts outlives that test. Issue #950 + /// introduced this shape to replace bounded waits on a thread-pool worker; issue #968 (folding + /// issue #972) consolidated the three per-file copies here. The class adds no fields, handles + /// or subscriptions, so it does not override Dispose(bool); disposal stays with the test + /// that constructs the worker, in a using block. + /// + internal sealed class SynchronousBackgroundWorker : BackgroundWorker + { + /// Raises DoWork on the calling thread. + internal void RaiseDoWork() => OnDoWork(new DoWorkEventArgs(null)); + + /// + /// The synchronous starter assigned to QfcDatamodel.WorkerStarter. The worker handed + /// to it must be a . + /// + internal static void StartSynchronously(BackgroundWorker worker) => + ((SynchronousBackgroundWorker)worker).RaiseDoWork(); + } + } + +W1 counts: `class SynchronousBackgroundWorker` 1; `internal sealed class SynchronousBackgroundWorker : BackgroundWorker` 1; `internal static void StartSynchronously(BackgroundWorker worker)` 1; `Dispose` 1 (the doc line only); about 27 lines (the exact `LINES:` value is recorded by CMD-EOL and gated at most 500 and at least 20). + +**W2 — `QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs`, new file, whole content (the four-space Markdown indent is removed):** + + using System; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.Time.Testing; + + namespace QuickFiler.Test.TestSupport + { + /// + /// A that completes a signal after every + /// call, so a test can prove that a production loop armed its next wait instead of returning, + /// without a clock advance followed by a yield or a bounded retry. + /// + /// + /// Issue #968. completes once the first timer after construction, or after + /// the last , has been created; TrySetResult is used because a loop + /// can arm one more timer than a test drives. Signals run their continuations asynchronously + /// so a test never resumes inside the production CreateTimer call. Consecutive + /// Advance calls without awaiting in between are prohibited: a + /// deadline the loop has not yet created is not advanced past, and the test would then wait on + /// a timer that never fires. Modelled on UtilitiesCS.Test ArmingBarrierTimeProvider, as a + /// subclass rather than a forwarding decorator because this project already subclasses + /// . + /// + internal sealed class ArmingFakeTimeProvider : FakeTimeProvider + { + private volatile TaskCompletionSource _armed = NewSignal(); + + /// Completes after the next call since the last re-arm. + internal Task Armed => _armed.Task; + + /// Replaces the signal so the next call completes a fresh task. + internal void ReArm() => _armed = NewSignal(); + + public override ITimer CreateTimer( + TimerCallback callback, + object state, + TimeSpan dueTime, + TimeSpan period + ) + { + ITimer timer = base.CreateTimer(callback, state, dueTime, period); + _armed.TrySetResult(true); + return timer; + } + + private static TaskCompletionSource NewSignal() => + new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + } + +W2 counts: `internal sealed class ArmingFakeTimeProvider : FakeTimeProvider` 1; `internal Task Armed` 1; `internal void ReArm()` 1; `public override ITimer CreateTimer(` 1; `base.CreateTimer(` 1; `RunContinuationsAsynchronously` 1; `_armed.TrySetResult(true);` 1; about 49 lines (the exact `LINES:` value is recorded by CMD-EOL and gated at most 500 and at least 30). + +**L1 — `QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs`, usings, in two halves:** L1a (P4-T6) inserts `using QuickFiler.Test.TestSupport;` immediately after `using Moq;` (pre-edit line 12). L1b (P5-T3, together with L3) deletes `using Microsoft.Extensions.Time.Testing;` (pre-edit line 9), because after L3 the file no longer names the `FakeTimeProvider` type (its two `ArmingFakeTimeProvider` lines resolve through `QuickFiler.Test.TestSupport`); deleting it earlier would break the Phase 4 build while test 1 still constructs one. Net zero lines; L1a is applied first, so every later L edit is located by content. + +**L2 — the same file, delete pre-edit lines 47 to 61** (the nested worker doc, class, blank, starter doc, starter and the following blank; located by content as the block from the `/// ` whose next line begins `/// Test-side worker whose` through the blank line after `((SynchronousBackgroundWorker)worker).RaiseDoWork();`). Fifteen lines removed; the `/// ` of `DrainableSynchronizationContext` now follows the blank line after `SetPrivateField`. + +**L3 — the same file, replace test 1 whole** (pre-edit lines 100 to 164: from the `/// ` whose next line begins `/// Issue #424 regression test for the latent producer-liveness defect.` through the method's closing `}` that precedes the blank line before `/// Reads the issue #424 producer-liveness flag by reflection.`) with L-T1 (eight-space indent on the doc and attribute lines): + + /// + /// Issue #424 regression test for the latent producer-liveness defect. Worker_DoWork is + /// async void, so it returns at its first yielding await and + /// goes false while + /// LoadRemainingEmailsToQueueAsync is still producing. The dequeue gate's + /// sourceActive signal consumed that dishonest value, so an empty queue was mistaken + /// for an exhausted one and the gate returned an early partial batch. The datamodel-owned + /// volatile bool flag makes the signal truthful. + /// + /// Issue #968: every step waits on an explicit signal instead of a clock advance followed by + /// a scheduler yield. proves the gate armed its + /// next wait; the dequeue task itself is the completion signal; the production awaits are + /// registered with no synchronization context installed, so the loader's completion clears + /// the flag inline and is read back before the final advance. No retry loop remains. + /// + /// + [TestMethod] + public async Task DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle() + { + // Arrange + var model = CreateUninitializedDatamodel(); + var clock = new ArmingFakeTimeProvider(); + model.TimeProvider = clock; + SetPrivateField(model, "_globals", CreateHighConfidenceGlobals()); + SetPrivateField(model, "_masterQueue", new LockingLinkedList()); + + var loaderEntered = new TaskCompletionSource(); + var loaderRelease = new TaskCompletionSource(); + model.RemainingEmailLoader = async _ => + { + loaderEntered.TrySetResult(true); + return await loaderRelease.Task; + }; + + using (var worker = new SynchronousBackgroundWorker()) + { + model.WorkerStarter = SynchronousBackgroundWorker.StartSynchronously; + Task> pending; + using (NoSynchronizationContext()) + { + // The issue #244 zero-batch short-circuit is COM-free and starts the worker + // through the issue #950 seam, which raises DoWork on this thread. + model.InitEmailQueue(0, worker); + loaderEntered + .Task.IsCompleted.Should() + .BeTrue("the synchronous starter must reach the injected RemainingEmailLoader"); + pending = model.DequeueNextItemGroupAsync(1, 200); + } + + clock + .Armed.IsCompleted.Should() + .BeTrue("the gate arms its first empty-queue wait before the dequeue call returns"); + clock.ReArm(); + + // Act — the first wait expires while the loader is still producing. + clock.Advance(TimeSpan.FromMilliseconds(200)); + Task first = await Task.WhenAny(clock.Armed, pending); + + // Assert + first + .Should() + .BeSameAs( + clock.Armed, + "the loader is still producing, so the gate must arm a second wait rather than " + + "treat an empty queue as an exhausted source and return an early partial batch" + ); + pending.IsCompleted.Should().BeFalse("the gate re-armed instead of returning"); + + // Cleanup — complete the loader; with no captured context its continuations run + // inline and clear the flag before this call returns. + using (NoSynchronizationContext()) + { + loaderRelease.SetResult(true); + } + + ReadLivenessFlag(model) + .Should() + .BeFalse("the loader's completion must clear the flag before the next poll"); + clock.Advance(TimeSpan.FromMilliseconds(200)); + (await pending) + .Should() + .BeEmpty("once the loader completes, the gate exits on genuine exhaustion"); + } + } + +L-T1 is about 84 lines replacing 65. Gate tokens quoted from L-T1 (each on one line): `new ArmingFakeTimeProvider()`; `using (var worker = new SynchronousBackgroundWorker())`; `SynchronousBackgroundWorker.StartSynchronously`; `using (NoSynchronizationContext())` (2 lines); `clock.ReArm();`; `await Task.WhenAny(clock.Armed, pending)`; `the gate must arm a second wait`; `the gate re-armed instead of returning`; `must clear the flag before the next poll`; `(await pending)`; `await` on exactly three lines (the lambda's `return await loaderRelease.Task;`, the `WhenAny` line, the `(await pending)` line), none inside a `NoSynchronizationContext` block; `Task.Yield` 0; `fake.Advance` 0; `for (int i` 0. + +**L4 — the same file, insert the context scope after `ReadLivenessFlag`** (after the method's closing `}` that follows `return (bool)field.GetValue(model);`): one blank line, then L-SCOPE (eight-space indent): + + /// + /// Issue #968. Clears on the calling thread for the + /// lifetime of the returned scope and restores the previous value on dispose, so every + /// production await registered inside the scope captures no context and its continuation runs + /// inline on the completing thread. The scope body must contain no await: the restore + /// has to run on the same thread that took the scope. + /// + private static IDisposable NoSynchronizationContext() => new SynchronizationContextScope(); + + private sealed class SynchronizationContextScope : IDisposable + { + private readonly SynchronizationContext _previous = SynchronizationContext.Current; + + internal SynchronizationContextScope() => + SynchronizationContext.SetSynchronizationContext(null); + + public void Dispose() => SynchronizationContext.SetSynchronizationContext(_previous); + } + +L-SCOPE is about 19 lines including the leading blank. The field initializer runs before the constructor body, so `_previous` captures the context the scope then clears. + +**L5 — the same file, `StartHeldOpenLoader`:** replace its doc and declaration (pre-edit lines 174 to 186, from the `/// ` whose next line begins `/// Starts the worker with a` through the line `)` that closes the parameter list) with L-HELD-HEAD, and delete the two body lines `var worker = new SynchronousBackgroundWorker();` and `model.WorkerStarter = StartSynchronously;` (pre-edit 201 to 202), inserting in their place the single line `model.WorkerStarter = SynchronousBackgroundWorker.StartSynchronously;` (twelve-space indent). L-HELD-HEAD (eight-space indent), about fifteen lines replacing thirteen: + + /// + /// Starts with a RemainingEmailLoader held open by + /// . The issue #950 synchronous starter raises DoWork on + /// this thread, so by the time InitEmailQueue returns the async void + /// Worker_DoWork has entered the loader and returned at its first incomplete await. + /// runs its continuations asynchronously, so a test that has + /// installed DrainableSynchronizationContext observes the resumed loader only + /// through Drain, never inline inside SetResult. The caller owns and disposes + /// the worker (issue #968, folding issue #972 item 4). + /// + private static QfcDatamodel StartHeldOpenLoader( + SynchronousBackgroundWorker worker, + Func, Task> loaderBody, + out TaskCompletionSource release + ) + +After L5 the helper body no longer contains `new SynchronousBackgroundWorker()`. + +**L6 — the same file, test 2 body:** replace the body of `RemainingLoadActive_AcrossAsyncVoidFirstAwait_StaysTrueWhileLoaderProduces` (pre-edit lines 219 to 234, the `{` through the closing `}`) with L-T2 (eight-space indent on the braces): + + { + using (var worker = new SynchronousBackgroundWorker()) + { + // Arrange / Act + QfcDatamodel model = StartHeldOpenLoader( + worker, + signal => signal.Task, + out TaskCompletionSource release + ); + + // Assert + ReadLivenessFlag(model) + .Should() + .BeTrue( + "the producer is still live even though the async void handler already returned" + ); + + release.SetResult(true); + } + } + +**L7 — the same file, tests 3 and 4** (`RemainingLoadActive_AfterLoaderCompletes_BecomesFalse` and `RemainingLoadActive_WhenLoaderThrows_IsStillClearedByFinally`), the same mechanical edit in each: inside the `try` block, insert the two lines `using (var worker = new SynchronousBackgroundWorker())` and `{` (sixteen-space indent) immediately before the line `QfcDatamodel model = StartHeldOpenLoader(`; insert the line `worker,` (twenty-space indent) immediately after that `StartHeldOpenLoader(` line; insert a closing `}` (sixteen-space indent) immediately before the twelve-space `}` that closes the `try`; re-indent the enclosed lines by four spaces (CSharpier normalises indentation in P6-T1, so the gate is on tokens, not on indentation). Each test gains three lines. + +After L1 to L7 the file is about 346 lines before formatting (L2 minus 15; L3 about plus 19; L4 about plus 19; L5 about plus 1 net; L6 plus 4; L7 plus 6); the exact value is recorded by CMD-LINECOUNT and gated at most 500 (fold line counts are observations, because CSharpier may re-wrap the long `because` strings). Post-change counts: `class SynchronousBackgroundWorker` 0; `StartSynchronously` 2 lines, both `SynchronousBackgroundWorker.StartSynchronously` (L-T1 and the helper body); `new SynchronousBackgroundWorker()` 4 lines, each inside `using (var worker = new SynchronousBackgroundWorker())`; `StartHeldOpenLoader(` 4 lines; `worker,` 4 lines (the three callers and the `StartHeldOpenLoader` parameter line `SynchronousBackgroundWorker worker,`); `Task.Yield` 0; `fake.Advance` 0; `FakeTimeProvider` 2 (`new ArmingFakeTimeProvider()` and the L-T1 doc cref `ArmingFakeTimeProvider.Armed`; the count is an ordinal substring match); `using QuickFiler.Test.TestSupport;` 1; `NoSynchronizationContext` 3 lines (declaration plus two uses); `Duplicated per file` 0. + +**TD1 — `QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs`:** insert `using QuickFiler.Test.TestSupport;` immediately after `using Moq;` (pre-edit line 12); delete pre-edit lines 59 to 74 (the nested worker doc, class, blank, starter doc, starter and the following blank; located by content from the `/// ` whose next line begins `/// Test-side worker whose` through the blank line after `((SynchronousBackgroundWorker)worker).RaiseDoWork();`); replace the line `model.WorkerStarter = StartSynchronously;` (pre-edit 222) with `model.WorkerStarter = SynchronousBackgroundWorker.StartSynchronously;` at the same sixteen-space indent. The file is 244 plus 1 minus 16 = 229 lines before formatting. Post-change counts: `class SynchronousBackgroundWorker` 0; `StartSynchronously` 1 (`SynchronousBackgroundWorker.StartSynchronously`); `using (var worker = new SynchronousBackgroundWorker())` 1 (unchanged, line 220 pre-edit); `Duplicated per file` 0; `using QuickFiler.Test.TestSupport;` 1. The class header (lines 18 to 27) is unchanged: it documents the reflection helpers, not the worker. + +**Z1 — `QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs`, usings and docs:** insert `using QuickFiler.Test.TestSupport;` immediately after `using Moq;` (pre-edit line 12). Replace pre-edit lines 32 to 34 (the three remarks lines beginning `/// assigns the WorkerStarter seam`, `/// the test thread through the nested`, `/// thread-pool worker and none outlives the test.`) with these three lines (four-space indent): + + /// assigns the WorkerStarter seam a starter that raises DoWork synchronously on + /// the test thread through the shared SynchronousBackgroundWorker test-support helper + /// (issue #968 consolidated the per-file copies), so no started worker outlives its test. + +Replace pre-edit lines 97 to 99 (`/// Assigning this delegate before starting a real is what makes`, `/// it safe to call with a real`, `/// worker in a unit test.`) with these three lines (eight-space indent): + + /// Assigning this delegate before the synchronous test worker is started is what makes it + /// safe to call in a unit + /// test (issue #950: the worker raises DoWork on the test thread). + +**Z2 — the same file, delete pre-edit lines 114 to 128** (the nested worker doc, class, blank, starter doc, starter and the following blank; located by content as for TD1). Fifteen lines removed. + +**Z3 — the same file, the three tests (located by method name; every `model.WorkerStarter = StartSynchronously;` becomes `model.WorkerStarter = SynchronousBackgroundWorker.StartSynchronously;`):** + +1. `InitEmailQueue_ZeroBatchSize_ReturnsEmptyListWithoutThrowing`: replace the lines from `// Act` through `result.Should().BeEmpty();` (pre-edit 146 to 153) with Z-T1 (twelve-space indent on the `using`): + + using (var worker = new SynchronousBackgroundWorker()) + { + // Act + System.Action act = () => result = model.InitEmailQueue(0, worker); + + // Assert + act.Should().NotThrow(); + result.Should().NotBeNull(); + result.Should().BeEmpty(); + } + +2. `InitEmailQueue_ZeroBatchSize_StillStartsBackgroundWorker`: replace the lines from `var worker = new SynchronousBackgroundWorker();` through the `.BeTrue("the injected RemainingEmailLoader must be invoked by the started worker");` line (pre-edit 173 to 182) with Z-T2: + + using (var worker = new SynchronousBackgroundWorker()) + { + // Act + model.InitEmailQueue(0, worker); + + // Assert + worker.WorkerSupportsCancellation.Should().BeTrue(); + loaderInvokedTcs + .Task.IsCompleted.Should() + .BeTrue("the injected RemainingEmailLoader must be invoked by the started worker"); + } + +3. `InitEmailQueue_PositiveBatchSize_RetainsExistingProjectionAndFrameDrop`: replace the lines from `// Act` through `frame.RowCount.Should().Be(0);` (pre-edit 220 to 229) with Z-T3: + + using (var worker = new SynchronousBackgroundWorker()) + { + // Act + var result = model.InitEmailQueue(2, worker); + + // Assert + result.Should().HaveCount(2); + result.Should().BeEquivalentTo(mailItemsByEntryId.Values); + + var frameField = typeof(QfcDatamodel).GetField("_frame", NonPublicInstance); + var frame = (Frame)frameField.GetValue(model); + frame.RowCount.Should().Be(0); + } + +After Z1 to Z3 the file is about 225 lines before formatting (recorded, gated at most 500). Post-change counts: `class SynchronousBackgroundWorker` 0; `StartSynchronously` 3 lines, each `SynchronousBackgroundWorker.StartSynchronously`; `new SynchronousBackgroundWorker()` 3 lines, each `using (var worker = new SynchronousBackgroundWorker())`; `InitEmailQueue(0, new` 0; `InitEmailQueue(2, new` 0; `Duplicated per file` 0; `through the nested` 0; `starting a real` 0; `using QuickFiler.Test.TestSupport;` 1; `[TestMethod]` 3. + +**M1 — `QuickFiler.Test/Controllers/QfcDatamodelTests.cs`, usings:** insert `using QuickFiler.Test.TestSupport;` immediately after `using Moq;` (pre-edit line 12). `using Microsoft.Extensions.Time.Testing;` stays (two other tests construct `FakeTimeProvider`). + +**M2 — the same file, replace the sibling test whole** (pre-edit lines 95 to 131: from the `[TestMethod]` immediately above `public async Task DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive()` through its closing `}`) with M-T (eight-space indent on the doc and attribute lines): + + /// + /// Issue #424: the high-confidence dequeue keeps polling while the datamodel-owned liveness + /// flag is true. Issue #968: the re-arm is proved through + /// instead of a clock advance followed by + /// a scheduler yield, and the dequeue task itself is the completion signal. + /// + [TestMethod] + public async Task DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive() + { + // Arrange + var model = CreateUninitializedDatamodel(); + var clock = new ArmingFakeTimeProvider(); + model.TimeProvider = clock; + + var settings = new Mock(MockBehavior.Strict); + settings.SetupGet(x => x.HighConfidenceModeEnabled).Returns(true); + settings.SetupGet(x => x.HighConfidenceThreshold).Returns(0.90); + var globals = new Mock(MockBehavior.Strict); + globals.SetupGet(x => x.QfSettings).Returns(settings.Object); + + using (var worker = new BackgroundWorker()) + { + SetPrivateField(model, "_globals", globals.Object); + SetPrivateField(model, "_worker", worker); + SetPrivateField(model, "_masterQueue", new LockingLinkedList()); + // Issue #424: the source-active signal is the datamodel-owned liveness flag, not + // BackgroundWorker.isRunning, which is dishonest for an async void DoWork handler. + SetPrivateField(model, "_remainingLoadActive", true); + + Task> pending = model.DequeueNextItemGroupAsync(1, 200); + clock + .Armed.IsCompleted.Should() + .BeTrue("the gate arms its first empty-queue wait before the dequeue call returns"); + clock.ReArm(); + + // Act — the first wait expires while the source is still active. + clock.Advance(TimeSpan.FromMilliseconds(200)); + Task first = await Task.WhenAny(clock.Armed, pending); + + // Assert + first + .Should() + .BeSameAs( + clock.Armed, + "the datamodel source-active signal must keep polling while the worker can still add candidates" + ); + pending.IsCompleted.Should().BeFalse("the gate re-armed instead of returning"); + + SetPrivateField(model, "_remainingLoadActive", false); + clock.Advance(TimeSpan.FromMilliseconds(200)); + IList result = await pending; + + result.Should().BeEmpty(); + } + } + +**M3 — the same file, `WaitForQueue_WhenWorkerBusyAndQueueShort_AwaitsInjectedTwoHundredMsDelay`:** replace the line `var worker = new BackgroundWorker();` (pre-edit 261) with the two lines `using (var worker = new BackgroundWorker())` and `{` (twelve-space indent), insert a closing `}` (twelve-space indent) immediately after `task.IsCompleted.Should().BeTrue();` (pre-edit 282), and re-indent the enclosed lines by four spaces. + +After M1 to M3 the file is about 393 lines before formatting (recorded, gated at most 500). Post-change counts: `new BackgroundWorker()` 2 lines, each `using (var worker = new BackgroundWorker())`; `new ArmingFakeTimeProvider()` 1; `clock.ReArm();` 1; `await Task.WhenAny(clock.Armed, pending)` 1; `must keep polling while the worker can still add candidates` 1; `the gate re-armed instead of returning` 1; `Task.Yield` 0; `await Task.Yield();` 0; `fake.Advance` 2 (the two untouched tests at pre-edit 241 and 280); `FakeTimeProvider` 6 (the four untouched lines at pre-edit 216, 224, 249 and 258 plus `new ArmingFakeTimeProvider()` and the M-T doc cref `ArmingFakeTimeProvider.Armed`; the count is an ordinal substring match, and the `using Microsoft.Extensions.Time.Testing;` directive does not contain it); `using QuickFiler.Test.TestSupport;` 1; `[TestMethod]` 9. + +**P1 — `QuickFiler/Controllers/QfcDatamodel.cs`, deletions and one retarget (pre-edit numbering of fact 14; apply from the bottom of the file upward so earlier numbers stay valid, or locate each block by its quoted first and last lines):** + +1. Delete lines 469 to 473 (`#region Linked List Locking`, two blanks, `#endregion Linked List Locking`, the following blank). Five lines. +2. Delete lines 417 to 465 (the blank before `private async Task LoadRemainingEmailsToQueueAsync(` with the `BackgroundWorker bw,` parameter, through that method's closing `}`; the block carries both `#pragma` lines). Forty-nine lines. +3. Delete lines 377 to 416 (the blank before `private bool LoadRemainingEmailsToQueue(BackgroundWorker bw, CancellationToken token)` through that method's closing `}`). Forty lines. +4. Replace line 369 `$"{nameof(LoadRemainingEmailsToQueue)} Error. \n {e.Message}\n{e.StackTrace}"` with `$"{nameof(LoadRemainingEmailsToQueueAsync)} Error. \n {e.Message}\n{e.StackTrace}"` (same twenty-four-space indent). Delete line 363 (`//logger.Debug($"{nameof(LoadRemainingEmailsToQueue)} Task cancelled");`). One line. +5. Delete lines 242 to 265 (the blank before `// This event handler demonstrates how to interpret`, the three comment lines, and `private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)` through its closing `}`). Twenty-four lines. +6. Delete lines 209 to 210 (`//e.Result = await LoadRemainingEmailsToQueueAsync(bw, _token);`, `//e.Result = LoadRemainingEmailsToQueue(bw, _token);`). Two lines. +7. Delete line 194 (`//worker.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(Worker_RunWorkerCompleted);`). One line. +8. Delete lines 109 to 111 (`private static readonly log4net.ILog log = log4net.LogManager.GetLogger(`, its argument line, `);`). Three lines. +9. Delete lines 99 to 101 (`//_blockingQueue = null;`, `//_priorityQueue = null;`, `//_queues = null;`; fields that no longer exist, addendum F2). Three lines. + +Total removed 128; the file is 367 lines before formatting (495 minus 128), gated at most 400. No `using` directive becomes unused through this edit (addendum section 3.3: `Enumerable`, `Task`, `MessageBox`, `BackgroundWorker` and `log4net` remain referenced). Post-change counts (tokens never contain a double quote, because CMD-TOKEN-COUNT passes them as double-quoted PowerShell strings): `Worker_RunWorkerCompleted` 0; `nameof(LoadRemainingEmailsToQueue)` 0; `nameof(LoadRemainingEmailsToQueueAsync)} Error.` 1 (0 before); `nameof(LoadRemainingEmailsToQueueAsync)} Task cancelled` 0 (1 before); `LoadRemainingEmailsToQueueAsync(` 2 (the cref at pre-edit 130 and the one-argument declaration); `LoadRemainingEmailsToQueue(BackgroundWorker bw` 0; `log4net.ILog log =` 0; `log4net.ILog logger =` 1; `Linked List Locking` 0; `#pragma` 0; `#region` 6; `#endregion` 6; `[ExcludeFromCodeCoverage]` 1; `//e.Result =` 0; `//_blockingQueue = null;` 0; `//worker.RunWorkerCompleted` 0; `ForEachAwaitWithCancellationAsync` 0; `: IQfcDatamodel` 1; `RemainingEmailLoader = LoadRemainingEmailsToQueueAsync;` 2; `WorkerStarter(worker);` 2; `_remainingLoadActive = ` 3. + +**Q1 — `QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs`, replaces lines 15 to 23** (the `_remainingLoadActive` summary; the declaration at 24 is unchanged), nine lines replacing nine: + + /// + /// Issue #424 producer-liveness signal, read by the dequeue gate's sourceActive + /// delegate and by . InitEmailQueue sets it just before + /// handing the worker to , and Worker_DoWork clears it in + /// a finally when the awaited task completes, + /// because BackgroundWorker.IsBusy already reads idle at that handler's first + /// incomplete await (issue #950 made the start synchronous in tests, so no particular thread + /// owns either write). Volatile: the writers and the readers share no other fence. + /// + +**Q2 — the same file, line 285:** ` /// UnhookItem throw path (:31-66) removes the failed` becomes ` /// UnhookItem throw path removes the failed` (the ` (:31-66)` is removed; nothing else on the line changes). + +The `QuiesceLoaderAsync` comment at line 52 is unchanged (D-20). After Q1 and Q2 the file is 413 lines. Post-change counts: `RunWorkerAsync` 0; `written on the worker thread and read` 0; `written on the worker thread` 1; `(:31-66)` 0; `TryUnhookOrReplace` 3; `WorkerStarter` 1; `share no other fence` 1; `honest producer-liveness signal` 0; `() => _remainingLoadActive,` 1; `() => false,` 0; `private volatile bool _remainingLoadActive;` 1. + +## Execution conventions + +- **Tokens.** `WORKTREE` denotes the absolute path of the item worktree supplied in the delegation prompt; the executor substitutes it into every payload and every `git -C` argument at run time and writes the token, never the path, into artifacts. In every `git -C` argument WORKTREE is written with forward slashes, because the Bash channel removes unquoted backslashes; inside a pwsh payload's double-quoted `Set-Location` argument the backslash form is used. `FEATURE` abbreviates the feature folder path. `BASE` denotes `94287369908cc920b21b0e3256314f988ad7d2f5`, which is written in full in every command. `QCT.` abbreviates the namespace prefix `QuickFiler.Controllers.Tests.` in this document only and is expanded in full in every executed filter. No artifact, and no line of this plan, carries an absolute host path, an account name or a machine name. +- **Payload channel.** Each indented payload in the Command Reference runs as one Bash tool call of the form `pwsh -NoProfile -Command ''`, newlines included, with the substitutions applied. Payloads are never merged into one call: the hook layer scans a pwsh payload as raw text, so a merged payload can combine words that each payload alone does not. Payloads use double quotes only, so the outer single quotes never conflict. Git commands run as single Bash calls of the form `git -C WORKTREE `, never chained. No `cd`, no `&&`, `;` or `|` between Bash commands. File edits use the Edit and Write tools with absolute paths. If the Bash tool refuses a pwsh invocation (a refusal whose text begins "This agent is isolated in the worktree"), the executor records PWSH CHANNEL REFUSED with the verbatim refusal in that task's artifact and stops; no rephrased or alternative invocation is attempted. The plan has no non-pwsh fallback, so the orchestrator must dispatch the executor without worktree isolation. The executor is dispatched non-isolated, which means its Bash and pwsh process cwd is the coordinator session tree, not WORKTREE: every command therefore keeps targeting WORKTREE explicitly, through the PREFIX `Set-Location` plus `[System.IO.Directory]::SetCurrentDirectory` in every payload and through `git -C WORKTREE` in every git call, and `WORKTREE-LEAF: agent-a291a7fbabf9d0229` is the per-payload proof that it did. +- **Command rows.** The `Command:` field of a payload artifact records the full payload as executed, with `WORKTREE` in place of the path, followed on the next line by the canonical command it implements. Every payload artifact records `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; any other value means the payload ran in the wrong tree, and the task fails. +- **Exit codes.** `EXIT_CODE:` records the payload's principal exit value as named in each Command Reference entry. Deliberately failing runs carry `ExpectedExitCode:` equal to the deterministic value the task states. A recorded observation whose exit value is not gated carries `ExpectedExitCode:` equal to the observed value when non-zero and says so; the field is omitted when the observed value is 0. The first `Command:`, `EXIT_CODE:` and `ExpectedExitCode:` rows of an artifact form its machine-read record, so a multi-command artifact places its gated command first. +- **Transcription.** Any absolute path inside a transcribed line is replaced by `REDACTED-PATH`. Trx and coverage documents are never copied into FEATURE. +- **Long-running payloads.** `CMD-COVERAGE-RUNNER` and `CMD-COVERAGE-DIRECT` are started with the Bash tool's `run_in_background` option and no shell redirection; the payload's own output, captured by the tool, is the record, and completion is the background-task notification together with a final output line `PAYLOAD-COMPLETE`. A run still in progress after 120 minutes is `COVERAGE RUN STALLED`: stop and report. Before any re-invocation after a timed-out or interrupted call, the executor runs `pwsh -NoProfile -Command '$leaf = "agent-a291a7fbabf9d0229"; $runner = "Invoke-MSTest" + "WithCoverage"; "STRAY_TEST_PROCESSES: " + @(Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessId -ne $PID -and $null -ne $_.CommandLine -and $_.CommandLine.Contains($leaf) -and ($_.Name -like "vstest*" -or $_.Name -like "testhost*" -or $_.Name -like "dotnet-coverage*" -or ($_.Name -like "pwsh*" -and $_.CommandLine.Contains($runner))) }).Count'` and proceeds only at `STRAY_TEST_PROCESSES: 0`. Both coverage payloads end with the unconditional line `Write-Output "PAYLOAD-COMPLETE"`; a run that ends without that line is `COVERAGE RUN ABORTED`. +- **No wall-clock construct anywhere.** No payload sleeps; no test edit adds a sleep, delay, retry, timeout change, parallelism attribute or temporary file. + +## Command reference + +**PREFIX** (the first three lines of every payload): + + Set-Location -LiteralPath "WORKTREE" + [System.IO.Directory]::SetCurrentDirectory((Get-Location).Path) + Write-Output ("WORKTREE-LEAF: " + (Split-Path -Leaf (Get-Location).Path)) + +**TOOLS** (the next lines of every build and test payload): + + $vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe" + $msbuild = & $vswhere -latest -products * -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1 + $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1 + New-Item -ItemType Directory -Path "coverage\logs" -Force | Out-Null + +**File tokens.** `FIX` is `QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixture.cs`; `FAT` is `QuickFiler.Test\Controllers\QfcItemController.FocusAndThemeTests.cs`; `TS` is `QuickFiler.Test\Controllers\QfcItemController.TestSupport.cs`; `FT` is `QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs`; `PC` is `QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherPinCountTests.cs`; `PROJ` is `QuickFiler.Test\QuickFiler.Test.csproj`; `SBW` is `QuickFiler.Test\TestSupport\SynchronousBackgroundWorker.cs`; `AFTP` is `QuickFiler.Test\TestSupport\ArmingFakeTimeProvider.cs`; `LIV` is `QuickFiler.Test\Controllers\QfcDatamodelLivenessTests.cs`; `TD` is `QuickFiler.Test\Controllers\QfcDatamodelTeardownTests.cs`; `ZB` is `QuickFiler.Test\Controllers\QfcInitEmailQueueZeroBatchTests.cs`; `DMT` is `QuickFiler.Test\Controllers\QfcDatamodelTests.cs`; `QDM` is `QuickFiler\Controllers\QfcDatamodel.cs`; `QQP` is `QuickFiler\Controllers\QfcDatamodel.QueueProcessing.cs`. **CS4** is `"FIX", "FAT", "TS", "FT"` (each expanded); **CS5** is CS4 plus `"PC"`; **FOLD6** is `"LIV", "TD", "ZB", "DMT", "QDM", "QQP"` (the six existing fold files); **FOLD8** is FOLD6 plus `"SBW", "AFTP"`; **CS13** is CS5 plus FOLD8 (every Write Set `.cs` file). **CODE14-GIT** is the fourteen Write Set code paths with forward slashes, space-separated, for git pathspecs: `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs QuickFiler.Test/QuickFiler.Test.csproj QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs QuickFiler.Test/Controllers/QfcDatamodelTests.cs QuickFiler/Controllers/QfcDatamodel.cs QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs`. **FEATURE-GIT** is `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968`. + +**CMD-REBUILD** (`GATEARGS` is either `/p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` or `/p:TreatWarningsAsErrors=true`; `TASKID` substituted; `EXIT_CODE:` is `MSBUILD_EXIT_CODE:`; canonical command `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" GATEARGS`, resolved through vswhere against this worktree's TaskMaster.sln, plus /nodeReuse:false and a normal-verbosity file logger under the ignored coverage directory): + + PREFIX + TOOLS + $log = "coverage\logs\TASKID.msbuild.log" + if (Test-Path -LiteralPath $log) { Remove-Item -LiteralPath $log -Force } + $global:LASTEXITCODE = 0 + & $msbuild TaskMaster.sln /t:Rebuild /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" GATEARGS "/flp:LogFile=$log;Verbosity=normal" | Out-Null + Write-Output ("MSBUILD_EXIT_CODE: " + $LASTEXITCODE) + $lines = Get-Content -LiteralPath $log -Encoding UTF8 + Write-Output ("ERRORS: " + (($lines | Select-String -Pattern "^\s*(\d+) Error\(s\)" | Select-Object -Last 1).Matches[0].Groups[1].Value)) + Write-Output ("WARNINGS: " + (($lines | Select-String -Pattern "^\s*(\d+) Warning\(s\)" | Select-Object -Last 1).Matches[0].Groups[1].Value)) + Write-Output ("SKIP_CORECOMPILE_LINES: " + @($lines | Where-Object { $_.Contains("Skipping target ""CoreCompile""") }).Count) + Write-Output ("CSC_OUT_QUICKFILER: " + @($lines | Where-Object { $_.Contains("/out:obj\Debug\QuickFiler.dll") }).Count) + Write-Output ("CSC_OUT_QUICKFILER_TEST: " + @($lines | Where-Object { $_.Contains("/out:obj\Debug\QuickFiler.Test.dll") }).Count) + $files = @("QfcItemController.UiThreadDispatcherFixture.cs(", "QfcItemController.FocusAndThemeTests.cs(", "QfcItemController.TestSupport.cs(", "QfcItemController.UiThreadDispatcherFixtureTests.cs(", "QfcItemController.UiThreadDispatcherPinCountTests.cs(", "QuickFiler.Test.csproj(", "SynchronousBackgroundWorker.cs(", "ArmingFakeTimeProvider.cs(", "QfcDatamodelLivenessTests.cs(", "QfcDatamodelTeardownTests.cs(", "QfcInitEmailQueueZeroBatchTests.cs(", "QfcDatamodelTests.cs(", "QfcDatamodel.cs(", "QfcDatamodel.QueueProcessing.cs(") + $diag = @($lines | Where-Object { $l = $_; (@($files | Where-Object { $l.Contains($_) }).Count -gt 0) -and ($l -match "(error|warning) [A-Z]+[0-9]+") }) + Write-Output ("WRITESET_DIAGNOSTIC_LINES: " + $diag.Count) + Write-Output ("WRITESET_DIAGNOSTIC_CODES: " + ((@($diag | ForEach-Object { [regex]::Match($_, "(error|warning) ([A-Z]+[0-9]+)").Groups[2].Value }) | Sort-Object -Unique) -join ",")) + Write-Output ("TEST_DLL_EXISTS: " + (Test-Path -LiteralPath "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll")) + Write-Output ("UCS_TEST_DLL_EXISTS: " + (Test-Path -LiteralPath "UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll")) + +`ERRORS:` is read from the build summary line, so `0 Error(s)` is not mistaken for a substring of a larger count. Under /t:Rebuild `SKIP_CORECOMPILE_LINES` is 0 by construction; the two `CSC_OUT_` counts show the compiler ran for the test project and its production dependency (MSBuild echoes each csc command line at normal verbosity; the #950 run observed 2 for each). + +**CMD-BUILD** (incremental build so a scoped test run observes a fresh assembly; `TASKID` substituted; `EXIT_CODE:` is `MSBUILD_EXIT_CODE:`; canonical command `msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU"`): + + PREFIX + TOOLS + $log = "coverage\logs\TASKID.msbuild.log" + if (Test-Path -LiteralPath $log) { Remove-Item -LiteralPath $log -Force } + $before = (Get-Item -LiteralPath "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll" -ErrorAction SilentlyContinue).LastWriteTimeUtc + $beforeProd = (Get-Item -LiteralPath "QuickFiler\bin\Debug\QuickFiler.dll" -ErrorAction SilentlyContinue).LastWriteTimeUtc + $global:LASTEXITCODE = 0 + & $msbuild TaskMaster.sln /t:Build /m /nodeReuse:false /p:Configuration=Debug "/p:Platform=Any CPU" "/flp:LogFile=$log;Verbosity=normal" | Out-Null + Write-Output ("MSBUILD_EXIT_CODE: " + $LASTEXITCODE) + $lines = Get-Content -LiteralPath $log -Encoding UTF8 + Write-Output ("ERRORS: " + (($lines | Select-String -Pattern "^\s*(\d+) Error\(s\)" | Select-Object -Last 1).Matches[0].Groups[1].Value)) + $after = (Get-Item -LiteralPath "QuickFiler.Test\bin\Debug\QuickFiler.Test.dll").LastWriteTimeUtc + $afterProd = (Get-Item -LiteralPath "QuickFiler\bin\Debug\QuickFiler.dll").LastWriteTimeUtc + Write-Output ("TEST_DLL_ADVANCED: " + ($null -eq $before -or $after -gt $before)) + Write-Output ("PROD_DLL_ADVANCED: " + ($null -eq $beforeProd -or $afterProd -gt $beforeProd)) + Write-Output ("CSC_OUT_QUICKFILER: " + @($lines | Where-Object { $_.Contains("/out:obj\Debug\QuickFiler.dll") }).Count) + Write-Output ("CSC_OUT_QUICKFILER_TEST: " + @($lines | Where-Object { $_.Contains("/out:obj\Debug\QuickFiler.Test.dll") }).Count) + +`PROD_DLL_ADVANCED:` and `CSC_OUT_QUICKFILER:` are gated only by the tasks that change a production file (P4-T10, P5-T8, P5-T9, P6-T3); elsewhere they are recorded. + +**CMD-VSTEST** (`FILTER`, `TASKID` and `NAMES` substituted; an empty `NAMES` prints every result; `EXIT_CODE:` is `VSTEST_EXIT_CODE:`, or 3 when the trx is absent; canonical command `vstest.console.exe QuickFiler.Test\bin\Debug\QuickFiler.Test.dll /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER" "/ResultsDirectory:coverage\test-results\968\TASKID" "/Logger:trx;LogFileName=TASKID.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None"`, resolved through vswhere; `ASSEMBLY` is `QuickFiler.Test\bin\Debug\QuickFiler.Test.dll` except for the stall probe, which uses `UtilitiesCS.Test\bin\Debug\UtilitiesCS.Test.dll`): + + PREFIX + TOOLS + $results = "coverage\test-results\968\TASKID" + if (Test-Path -LiteralPath $results) { Remove-Item -LiteralPath $results -Recurse -Force } + $names = @(NAMES) + $global:LASTEXITCODE = 0 + & $vstest "ASSEMBLY" /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:FILTER" "/ResultsDirectory:$results" "/Logger:trx;LogFileName=TASKID.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" 2>&1 | Tee-Object -FilePath "coverage\logs\TASKID.vstest.log" | Out-Null + Write-Output ("VSTEST_EXIT_CODE: " + $LASTEXITCODE) + $trxPath = Join-Path $results "TASKID.trx" + Write-Output ("TRX_PRESENT: " + (Test-Path -LiteralPath $trxPath)) + Write-Output ("SEQUENCE_FILES: " + @(Get-ChildItem -LiteralPath $results -Recurse -Filter "Sequence_*.xml" -ErrorAction SilentlyContinue).Count) + if (-not (Test-Path -LiteralPath $trxPath)) { exit 3 } + [xml]$trx = Get-Content -LiteralPath $trxPath -Raw -Encoding UTF8 + $ns = New-Object System.Xml.XmlNamespaceManager($trx.NameTable) + $ns.AddNamespace("t", "http://microsoft.com/schemas/VisualStudio/TeamTest/2010") + $counters = $trx.SelectSingleNode("//t:ResultSummary/t:Counters", $ns) + Write-Output ("COUNTERS total=" + $counters.GetAttribute("total") + " executed=" + $counters.GetAttribute("executed") + " passed=" + $counters.GetAttribute("passed") + " failed=" + $counters.GetAttribute("failed")) + $all = @($trx.SelectNodes("//t:UnitTestResult", $ns)) + Write-Output ("RESULT_COUNT: " + $all.Count) + foreach ($r in $all) { if ($names.Count -eq 0 -or $names -contains $r.GetAttribute("testName")) { Write-Output ("RESULT " + $r.GetAttribute("testName") + " = " + $r.GetAttribute("outcome") + " duration=" + $r.GetAttribute("duration")) } } + foreach ($r in $all) { if ($r.GetAttribute("outcome") -ne "Passed") { $msg = $r.SelectSingleNode("t:Output/t:ErrorInfo/t:Message", $ns); Write-Output ("MESSAGE " + $r.GetAttribute("testName") + " :: " + $(if ($msg) { $msg.InnerText -replace "\s+", " " } else { "(no message)" })) } } + +The trx stays under the ignored coverage directory; the artifact transcribes the `COUNTERS`, `RESULT_COUNT:`, `RESULT` and `MESSAGE` lines. + +Filters and name lists (every `QCT.` expanded to `QuickFiler.Controllers.Tests.` when executed): + +- `NAMES-PC`: `"EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease", "EnsureDispatcher_TwoPinsHeld_ReleaseOrderDoesNotChangeTheOutcome", "EnsureDispatcher_UnderATransactionHoldingALiveDispatcher_ReleasingAllPinsLeavesTheLiveDispatcher", "EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores"`. +- `NAMES-FT`: `"EnsureDispatcher_WhileATransactionHoldsALiveDispatcher_DoesNotReplaceIt", "EnsureDispatcher_WhenTheFieldIsNull_InstallsAndRestoresOnDispose", "EnsureDispatcher_ScopeDisposedTwice_IsIdempotent", "Transaction_SecondCallerCannotInstallUntilTheFirstRestores", "Transaction_DisposedTwice_DoesNotOverReleaseTheGate", "Install_CalledTwiceOnTheSameTransaction_ThrowsInvalidOperationException", "TransactionGate_WhileThisTestHoldsATransaction_HasExactlyOneUnreleasedAcquisition", "BeginTransactionAsync_ZeroBoundWhileThisTestHoldsThePermit_ThrowsTimeoutExceptionAndReleasesNothing"`. +- `NAMES-THEME`: `"SetThemeDark_FromNormal_SelectsDarkNormalTheme", "SetThemeLight_FromNormal_SelectsLightNormalTheme"`. +- `NAMES-LIVENESS`: `"DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle", "DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive"`. +- `NAMES-TARGETS`: NAMES-PC, NAMES-FT, NAMES-THEME and NAMES-LIVENESS together (sixteen names). +- `FILTER-PC-T1`: `FullyQualifiedName=QCT.QfcItemController_UiThreadDispatcherPinCountTests.EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease`. +- `FILTER-PC-T234`: the `FullyQualifiedName=` expressions for the other three NAMES-PC methods, joined with `|` (vstest rejects `OR`). +- `FILTER-PC-CLASS`: `FullyQualifiedName~QCT.QfcItemController_UiThreadDispatcherPinCountTests.` +- `FILTER-FT-CLASS`: `FullyQualifiedName~QCT.QfcItemController_UiThreadDispatcherFixtureTests.` +- `FILTER-FAT-CLASS`: `FullyQualifiedName~QCT.QfcItemController_FocusAndThemeTests.` +- `FILTER-CONCURRENT`: the three class filters joined with `|` (29 tests: 8, 4 and 17). +- `FILTER-BASELINE-CONCURRENT`: `FILTER-FT-CLASS` and `FILTER-FAT-CLASS` joined with `|` (25 tests; the pin-count class does not exist at Phase 0). +- `FILTER-DATAMODEL`: `FullyQualifiedName~QCT.QfcDatamodelLivenessTests.|FullyQualifiedName~QCT.QfcDatamodelTeardownTests.|FullyQualifiedName~QCT.QfcInitEmailQueueZeroBatchTests.|FullyQualifiedName~QCT.QfcDatamodelTests.` (21 tests: 4, 5, 3 and 9; the trailing dot keeps `QfcDatamodelTests.` from matching the other classes, and no other class name in QuickFiler.Test starts with these four prefixes followed by a dot). +- `FILTER-LIVENESS-PAIR`: `FullyQualifiedName=QCT.QfcDatamodelLivenessTests.DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle|FullyQualifiedName=QCT.QfcDatamodelTests.DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive`. +- `FILTER-STALL`: `FullyQualifiedName~HelperClasses.ShellUtilities_Tests|FullyQualifiedName~HelperClasses.ShellUtilitiesStatic_Tests|FullyQualifiedName~HelperClasses.SysImageListHelperTests|FullyQualifiedName~EmailIntelligence.OSBrowser_Tests`. + +**CMD-COVERAGE-RUNNER** (CLAUDE.md step 4 route; `STAGE` is `baseline` or `final`; `EXIT_CODE:` is `RUNNER_EXIT_CODE:`; canonical command `pwsh -NoProfile -File scripts\vscode\Invoke-MSTestWithCoverage.ps1` run from the worktree root): + + PREFIX + New-Item -ItemType Directory -Path "coverage\logs" -Force | Out-Null + foreach ($f in @("coverage\coverage.cobertura.xml", "coverage\coverage.cobertura.jacoco.xml", "coverage\test-results\mstest-coverage-run.trx", "coverage\test-results\mstest-coverage-run.summary.txt", "coverage\STAGE-968.cobertura.xml", "coverage\STAGE-968.trx")) { if (Test-Path -LiteralPath $f) { Remove-Item -LiteralPath $f -Force } } + $script = Join-Path (Get-Location).Path "scripts\vscode\Invoke-MSTestWithCoverage.ps1" + $global:LASTEXITCODE = 0 + & pwsh -NoProfile -File $script 2>&1 | Tee-Object -FilePath "coverage\logs\STAGE-968.runner.log" | Out-Null + Write-Output ("RUNNER_EXIT_CODE: " + $LASTEXITCODE) + $log = Get-Content -LiteralPath "coverage\logs\STAGE-968.runner.log" -Raw -Encoding UTF8 + Write-Output ("DISCOVERED_LINE: " + [regex]::Match($log, "Discovered \d+ test assemblies\.").Value) + Write-Output ("FIRST_PARTY_LINE: " + [regex]::Match($log, "First-party coverage: [^\r\n]*").Value) + Write-Output ("THRESHOLD_MESSAGE: " + [regex]::Match($log, "Cobertura (line|branch) coverage [^\r\n]*threshold\.").Value) + Write-Output ("COLLECT_FAILURE_MESSAGE: " + [regex]::Match($log, "MSTest with coverage failed with exit code \d+").Value) + Write-Output ("DOCUMENT_PRESENT: " + (Test-Path -LiteralPath "coverage\coverage.cobertura.xml")) + Write-Output ("TRX_PRESENT: " + (Test-Path -LiteralPath "coverage\test-results\mstest-coverage-run.trx")) + if (Test-Path -LiteralPath "coverage\coverage.cobertura.xml") { Copy-Item -LiteralPath "coverage\coverage.cobertura.xml" -Destination "coverage\STAGE-968.cobertura.xml" -Force } + if (Test-Path -LiteralPath "coverage\test-results\mstest-coverage-run.trx") { Copy-Item -LiteralPath "coverage\test-results\mstest-coverage-run.trx" -Destination "coverage\STAGE-968.trx" -Force } + Write-Output "PAYLOAD-COMPLETE" + +**CMD-COVERAGE-DIRECT** (the runner's inner invocation issued directly with the four-class exclusion; `STAGE` substituted; `EXIT_CODE:` is `COLLECT_EXIT_CODE:`; canonical command `dotnet-coverage collect --output coverage\STAGE-968.cobertura.xml --output-format cobertura --settings coverage\effective-coverage-968.config -- vstest.console.exe /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:" "/ResultsDirectory:coverage\test-results\968\STAGE" "/Logger:trx;LogFileName=STAGE-968.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None"`): + + PREFIX + . (Join-Path (Get-Location).Path "scripts\vscode\Invoke-MSTestWithCoverage.ps1") + $ErrorActionPreference = "Continue" + $repo = (Get-Location).Path + New-Item -ItemType Directory -Path "coverage\logs" -Force | Out-Null + foreach ($f in @("coverage\STAGE-968.cobertura.xml", "coverage\STAGE-968.trx")) { if (Test-Path -LiteralPath $f) { Remove-Item -LiteralPath $f -Force } } + $canonical = Get-Content -LiteralPath "coverage.config" -Raw -Encoding UTF8 + $derived = ConvertTo-DerivedCoverageSettingsXml -CanonicalSettingsXml $canonical + $effective = Join-Path $repo "coverage\effective-coverage-968.config" + Set-Content -LiteralPath $effective -Value $derived -Encoding UTF8 -NoNewline + $vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe" + $vstest = & $vswhere -latest -products * -find "Common7\IDE\Extensions\TestPlatform\vstest.console.exe" | Select-Object -First 1 + $rootLen = $repo.TrimEnd([char]92).Length + $asm = @(Get-ChildItem -Path $repo -Recurse -Filter "*.Test.dll" | Where-Object { $_.FullName -like "*\bin\Debug\*" -and $_.FullName -notlike "*\obj\*" -and $_.FullName -notlike "*\ref\*" -and $_.FullName.Substring($rootLen) -notlike "\.claude\*" } | Select-Object -ExpandProperty FullName) + $filter = "TestCategory!=LiveOutlook&FullyQualifiedName!~HelperClasses.ShellUtilities_Tests&FullyQualifiedName!~HelperClasses.ShellUtilitiesStatic_Tests&FullyQualifiedName!~HelperClasses.SysImageListHelperTests&FullyQualifiedName!~EmailIntelligence.OSBrowser_Tests" + $output = Join-Path $repo "coverage\STAGE-968.cobertura.xml" + $settings = Join-Path $repo "scripts\vscode\TaskMaster.cli.runsettings" + $results = Join-Path $repo "coverage\test-results\968\STAGE" + if (Test-Path -LiteralPath $results) { Remove-Item -LiteralPath $results -Recurse -Force } + $global:LASTEXITCODE = 0 + & dotnet-coverage collect --output $output --output-format cobertura --settings $effective -- $vstest @asm "/Settings:$settings" /InIsolation "/TestCaseFilter:$filter" "/ResultsDirectory:$results" "/Logger:trx;LogFileName=STAGE-968.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" 2>&1 | Tee-Object -FilePath "coverage\logs\STAGE-968.collect.log" | Out-Null + Write-Output ("COLLECT_EXIT_CODE: " + $LASTEXITCODE) + Write-Output ("ASSEMBLY_COUNT: " + $asm.Count) + $asm | ForEach-Object { Write-Output ("ASSEMBLY: " + $_.Substring($rootLen)) } + Write-Output ("SEQUENCE_FILES: " + @(Get-ChildItem -LiteralPath $results -Recurse -Filter "Sequence_*.xml" -ErrorAction SilentlyContinue).Count) + if (Test-Path -LiteralPath (Join-Path $results "STAGE-968.trx")) { Copy-Item -LiteralPath (Join-Path $results "STAGE-968.trx") -Destination "coverage\STAGE-968.trx" -Force } + Write-Output ("TRX_PRESENT: " + (Test-Path -LiteralPath "coverage\STAGE-968.trx")) + Write-Output ("DOCUMENT_PRESENT: " + (Test-Path -LiteralPath $output)) + Write-Output "PAYLOAD-COMPLETE" + +**CMD-COVERAGE-POST** (summarise the trx, post-process if raw, apply the floors, print the committed forms and the figures; `STAGE` substituted; `RAW` is `True` under DIRECT and under a RUNNER run whose `COLLECT_FAILURE_MESSAGE:` is non-empty, otherwise `False`, because a completed runner run has already post-processed the document in place; `EXIT_CODE:` is the payload's own exit status, 0 when every line printed): + + PREFIX + . (Join-Path (Get-Location).Path "scripts\vscode\Invoke-MSTestWithCoverage.Helpers.ps1") + . (Join-Path (Get-Location).Path "scripts\vscode\Invoke-MSTest.TrxSummary.ps1") + $ErrorActionPreference = "Continue" + $repo = (Get-Location).Path + $trxText = Get-Content -LiteralPath "coverage\STAGE-968.trx" -Raw -Encoding UTF8 + $summary = Get-TrxRunSummary -TrxContent $trxText + Write-Output "SUMMARY-BEGIN" + Write-Output (Format-TrxRunSummary -Summary $summary) + Write-Output "SUMMARY-END" + Write-Output ("FAILED-SET: " + ((@($summary.FailedTestName) | Sort-Object -Unique) -join ", ")) + [xml]$trx = $trxText + $ns = New-Object System.Xml.XmlNamespaceManager($trx.NameTable) + $ns.AddNamespace("t", "http://microsoft.com/schemas/VisualStudio/TeamTest/2010") + $names = @(NAMES-TARGETS) + foreach ($r in @($trx.SelectNodes("//t:UnitTestResult", $ns))) { if ($names -contains $r.GetAttribute("testName")) { Write-Output ("RESULT " + $r.GetAttribute("testName") + " = " + $r.GetAttribute("outcome")) } } + foreach ($r in @($trx.SelectNodes("//t:UnitTestResult", $ns))) { $o = $r.GetAttribute("outcome"); if ($o -ne "Passed" -and $o -ne "NotExecuted") { $m = $r.SelectSingleNode("t:Output/t:ErrorInfo/t:Message", $ns); Write-Output ("MESSAGE " + $r.GetAttribute("testName") + " :: " + $(if ($m) { $m.InnerText -replace "\s+", " " } else { "(no message)" })) } } + $doc = Get-Content -LiteralPath "coverage\STAGE-968.cobertura.xml" -Raw -Encoding UTF8 + if ("RAW" -eq "True") { $doc = ConvertTo-KoverageCoberturaXml -XmlContent $doc -RepoRoot $repo; Set-Content -LiteralPath "coverage\STAGE-968.cobertura.xml" -Value $doc -Encoding UTF8 -NoNewline } + try { Assert-CoberturaLineCoverageThreshold -CoberturaXml $doc; Write-Output "LINE-FLOOR: MET" } catch { Write-Output ("LINE-FLOOR: NOT MET " + $_.Exception.Message) } + try { Assert-CoberturaBranchCoverageThreshold -CoberturaXml $doc; Write-Output "BRANCH-FLOOR: MET" } catch { Write-Output ("BRANCH-FLOOR: NOT MET " + $_.Exception.Message) } + Write-Output (Get-CoberturaFirstPartyCoverageReport -CoberturaXml $doc) + [xml]$xml = $doc + $root = $xml.SelectSingleNode("/coverage") + Write-Output ("ROOT line-rate=" + $root.GetAttribute("line-rate") + " branch-rate=" + $root.GetAttribute("branch-rate") + " lines-covered=" + $root.GetAttribute("lines-covered") + " lines-valid=" + $root.GetAttribute("lines-valid") + " branches-covered=" + $root.GetAttribute("branches-covered") + " branches-valid=" + $root.GetAttribute("branches-valid")) + $projection = ConvertTo-JacocoPackageProjection -XmlDocument $xml + Assert-JacocoProjectionReconciliation -XmlDocument $xml -ProjectionXml $projection + Write-Output "PROJECTION-BEGIN" + Write-Output $projection + Write-Output "PROJECTION-END" + Write-Output ("TEST_ASSEMBLY_PACKAGES: " + @($xml.SelectNodes("//package") | Where-Object { $_.GetAttribute("name") -like "*.Test" }).Count) + Write-Output ("QFCDATAMODEL_CLASS_ENTRIES: " + @($xml.SelectNodes("//class") | Where-Object { $_.GetAttribute("name") -like "QuickFiler.Controllers.QfcDatamodel*" }).Count) + +The projection and the summary block are the two committed forms (CLAUDE.md "Committed Test Evidence Format"); the remaining lines are figures. `TEST_ASSEMBLY_PACKAGES:` is the observation behind D-7: 0 means no test assembly was instrumented, so no changed test line of this plan has a coverage figure. `QFCDATAMODEL_CLASS_ENTRIES:` is recorded, not gated (D-7): it shows whether the `[ExcludeFromCodeCoverage]` type appears in the report at all. The `MESSAGE` loop prints every non-passed, executed test's message (round-1 defect 10), so P8-T5 can read the `The UI dispatcher has not been captured` text. + +**CMD-HASH** (`FILES` substituted; hashes only): + + PREFIX + foreach ($p in @(FILES)) { Write-Output ("HASH " + $p + " = " + (Get-FileHash -Algorithm SHA256 -LiteralPath $p).Hash) } + +**CMD-LINECOUNT** (`FILES` substituted): + + PREFIX + foreach ($p in @(FILES)) { Write-Output ("LINES " + $p + " = " + @(Get-Content -LiteralPath $p -Encoding UTF8).Count) } + +**CMD-TOKEN-COUNT** (`FILE` and the `TOKENS` list substituted; ordinal, case-sensitive substring counts per physical line, so a token wrapped across two lines reads 0; no token contains a double quote): + + PREFIX + $src = Get-Content -LiteralPath "FILE" -Encoding UTF8 + foreach ($t in @(TOKENS)) { Write-Output ("TOKEN [" + $t + "] = " + @($src | Where-Object { $_.Contains($t) }).Count) } + +**CMD-SPAN-TOKEN-COUNT** (`FILE`, `START`, `END` and `TOKENS` substituted; the span runs from the first line containing START up to, not including, the next line containing END, or to the end of the file when END is the literal `EOF`; exit 4 when an anchor is missing): + + PREFIX + $src = Get-Content -LiteralPath "FILE" -Encoding UTF8 + $s = -1; for ($i = 0; $i -lt $src.Count; $i++) { if ($src[$i].Contains("START")) { $s = $i; break } } + $e = -1; if ($s -ge 0) { if ("END" -eq "EOF") { $e = $src.Count } else { for ($i = $s + 1; $i -lt $src.Count; $i++) { if ($src[$i].Contains("END")) { $e = $i; break } } } } + Write-Output ("SPAN: " + ($s + 1) + "-" + $e) + if ($s -lt 0 -or $e -lt 0) { exit 4 } + $span = $src[$s..($e - 1)] + foreach ($t in @(TOKENS)) { Write-Output ("SPAN-TOKEN [" + $t + "] = " + @($span | Where-Object { $_.Contains($t) }).Count) } + +Span anchors used by this plan (file, START, END): + +- `R4SPAN`: FT, `public async Task Transaction_SecondCallerCannotInstallUntilTheFirstRestores()`, `public async Task Transaction_DisposedTwice_DoesNotOverReleaseTheGate()`. Baseline `SPAN: 212-284`. +- `R4HEAD`: FT, the R4SPAN START, `Dispatcher original = UiThreadDispatcherFixture.Current;` (the first occurrence after the declaration). Baseline `SPAN: 212-224`. +- `R4TAIL`: FT, `issue #230 lost update`, `QfcItemControllerTestSupport.ShutdownDispatcher(liveA);` (the first occurrence after the START). Baseline `SPAN: 267-273`. +- `R1SPAN`: FT, `public async Task EnsureDispatcher_WhileATransactionHoldsALiveDispatcher_DoesNotReplaceIt()`, `public async Task EnsureDispatcher_WhenTheFieldIsNull_InstallsAndRestoresOnDispose()`. +- `R2SPAN`: FT, the R1SPAN END, `public async Task EnsureDispatcher_ScopeDisposedTwice_IsIdempotent()`. +- `R3SPAN`: FT, the R2SPAN END, the R4SPAN START. +- `ENSURE`: FIX, `internal static IDisposable EnsureDispatcher()`, `internal const int TransactionGateAcquireTimeoutMs = 120000;`. Baseline `SPAN: 122-145`. +- `SCOPE`: FIX, `private sealed class EnsureScope : IDisposable`, `internal sealed class UiThreadDispatcherTransaction : IDisposable`. Baseline `SPAN: 249-283`. +- `T1SPAN`: PC, `public async Task EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease()`, `public async Task EnsureDispatcher_TwoPinsHeld_ReleaseOrderDoesNotChangeTheOutcome()`. +- `T2SPAN`: PC, the T1SPAN END, `public async Task EnsureDispatcher_UnderATransactionHoldingALiveDispatcher_ReleasingAllPinsLeavesTheLiveDispatcher()`. +- `T3SPAN`: PC, the T2SPAN END, `public async Task EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores()`. +- `T4SPAN`: PC, the T3SPAN END, `EOF`. +- `T1-LIVE`: LIV, `public async Task DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle()`, `/// Reads the issue #424 producer-liveness flag by reflection.`. Baseline `SPAN: 110-165`. +- `HELD`: LIV, `private static QfcDatamodel StartHeldOpenLoader(`, `public void RemainingLoadActive_AcrossAsyncVoidFirstAwait_StaysTrueWhileLoaderProduces()`. Baseline `SPAN: 183-217`. +- `T-SIB`: DMT, `public async Task DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive()`, `public async Task TryQueueRemainingMailItemAsync_HighConfidenceEnabled_AddsBelowThresholdCandidate()`. Baseline `SPAN: 96-133`. +- `GATE-LAMBDA`: QQP, `var gate = new QfcStreamingDequeueConfidenceGate(`, `QfcGateBatch batch = await gate.DequeueAsync(quantity, timeOut, _token);`. Baseline `SPAN: 299-310`. + +**CMD-PIN-NESTING** (`FILE`, `START`, `END` substituted; same span rule as CMD-SPAN-TOKEN-COUNT; prints, in file order, every span line that carries one of the six nesting tokens, so the acquisition order can be read from the line numbers): + + PREFIX + $src = Get-Content -LiteralPath "FILE" -Encoding UTF8 + $s = -1; for ($i = 0; $i -lt $src.Count; $i++) { if ($src[$i].Contains("START")) { $s = $i; break } } + $e = -1; if ($s -ge 0) { if ("END" -eq "EOF") { $e = $src.Count } else { for ($i = $s + 1; $i -lt $src.Count; $i++) { if ($src[$i].Contains("END")) { $e = $i; break } } } } + Write-Output ("SPAN: " + ($s + 1) + "-" + $e) + if ($s -lt 0 -or $e -lt 0) { exit 4 } + for ($i = $s; $i -lt $e; $i++) { $t = $src[$i]; foreach ($k in @("BeginTransactionAsync()", "EnsureUiThreadDispatcher()", "Dispose()", ".Install(", "finally", "ShutdownDispatcher(")) { if ($t.Contains($k)) { Write-Output ("NEST " + ($i + 1) + " [" + $k + "] " + $t.Trim()); break } } } + +**CMD-CENSUS** (the two independent search strategies of research section 2.1 over every `*.cs` file under the worktree outside `packages`, `.claude`, `obj` and `bin`): + + PREFIX + $root = (Get-Location).Path + $rootLen = $root.TrimEnd([char]92).Length + $files = @(Get-ChildItem -Path $root -Recurse -Filter "*.cs" | Where-Object { $rel = $_.FullName.Substring($rootLen); $rel -notlike "\packages\*" -and $rel -notlike "\.claude\*" -and $rel -notlike "*\obj\*" -and $rel -notlike "*\bin\*" }) + Write-Output ("CS_FILES: " + $files.Count) + $primary = @($files | Select-String -Pattern "EnsureUiThreadDispatcher\(\)|EnsureDispatcher\(\)" -CaseSensitive) + Write-Output ("PRIMARY_LINES: " + $primary.Count) + foreach ($g in @($primary | Group-Object Path)) { Write-Output ("PRIMARY-FILE " + $g.Name.Substring($rootLen) + " = " + $g.Count) } + foreach ($m in $primary) { Write-Output ("PRIMARY " + $m.Path.Substring($rootLen) + ":" + $m.LineNumber + " :: " + $m.Line.Trim()) } + $cross = @($files | Select-String -Pattern "EnsureUiThreadDispatcher|EnsureDispatcher" -CaseSensitive) + Write-Output ("CROSS_LINES: " + $cross.Count) + foreach ($g in @($cross | Group-Object Path)) { Write-Output ("CROSS-FILE " + $g.Name.Substring($rootLen) + " = " + $g.Count) } + foreach ($m in $cross) { Write-Output ("CROSS " + $m.Path.Substring($rootLen) + ":" + $m.LineNumber + " :: " + $m.Line.Trim()) } + Write-Output ("CONTROL_LINES: " + @($files | Select-String -Pattern "BeginTransactionAsync\(" -CaseSensitive).Count) + +`CONTROL_LINES:` is the positive control (fact 5: 23 before the change, 28 after N1 adds five). + +**CMD-LEGACY-CALLERS** (the zero-caller proof for the four `QfcDatamodel` members, two independent strategies over the same file set as CMD-CENSUS plus an extension-unfiltered sweep; run before any removal): + + PREFIX + $root = (Get-Location).Path + $rootLen = $root.TrimEnd([char]92).Length + $files = @(Get-ChildItem -Path $root -Recurse -Filter "*.cs" | Where-Object { $rel = $_.FullName.Substring($rootLen); $rel -notlike "\packages\*" -and $rel -notlike "\.claude\*" -and $rel -notlike "*\obj\*" -and $rel -notlike "*\bin\*" }) + Write-Output ("CS_FILES: " + $files.Count) + $primary = @($files | Select-String -Pattern "Worker_RunWorkerCompleted|LoadRemainingEmailsToQueue\b|LoadRemainingEmailsToQueueAsync|Linked List Locking" -CaseSensitive) + Write-Output ("PRIMARY_LINES: " + $primary.Count) + foreach ($m in $primary) { Write-Output ("PRIMARY " + $m.Path.Substring($rootLen) + ":" + $m.LineNumber + " :: " + $m.Line.Trim()) } + $logField = @(Get-ChildItem -Path (Join-Path $root "QuickFiler\Controllers") -Filter "QfcDatamodel*.cs" | Select-String -Pattern "\blog\b" -CaseSensitive) + Write-Output ("LOG_LINES: " + $logField.Count) + foreach ($m in $logField) { Write-Output ("LOG " + $m.Path.Substring($rootLen) + ":" + $m.LineNumber + " :: " + $m.Line.Trim()) } + $q = [char]34 + $cross = @($files | Select-String -Pattern ($q + "Worker_RunWorkerCompleted" + $q + "|" + $q + "LoadRemainingEmailsToQueue|" + $q + "log" + $q + "|nameof\(log\)|GetField\(" + $q + "log") -CaseSensitive) + Write-Output ("CROSS_LINES: " + $cross.Count) + foreach ($m in $cross) { Write-Output ("CROSS " + $m.Path.Substring($rootLen) + ":" + $m.LineNumber + " :: " + $m.Line.Trim()) } + $all = @(Get-ChildItem -Path $root -Recurse -File | Where-Object { $rel = $_.FullName.Substring($rootLen); $rel -notlike "\packages\*" -and $rel -notlike "\.claude\*" -and $rel -notlike "*\obj\*" -and $rel -notlike "*\bin\*" -and $rel -notlike "\.git\*" -and $rel -notlike "\coverage\*" -and $rel -notlike "\.dotnet-sdk\*" }) + $sweep = @($all | Select-String -Pattern "Worker_RunWorkerCompleted|LoadRemainingEmailsToQueue" -CaseSensitive -ErrorAction SilentlyContinue) + Write-Output ("SWEEP_FILES: " + @($sweep | Group-Object Path).Count) + foreach ($g in @($sweep | Group-Object Path)) { if ($g.Name.EndsWith(".cs")) { Write-Output ("SWEEP-CS " + $g.Name.Substring($rootLen) + " = " + $g.Count) } } + $iface = @(Get-ChildItem -LiteralPath (Join-Path $root "QuickFiler\Interfaces\IQfcDatamodel.cs") | Select-String -Pattern "Worker_RunWorkerCompleted|LoadRemainingEmailsToQueue|\blog\b" -CaseSensitive) + Write-Output ("INTERFACE_LINES: " + $iface.Count) + $ivt = @(Get-ChildItem -Path (Join-Path $root "QuickFiler") -Recurse -Filter "*.cs" | Select-String -Pattern "InternalsVisibleTo" -CaseSensitive) + foreach ($m in $ivt) { Write-Output ("IVT " + $m.Path.Substring($rootLen) + ":" + $m.LineNumber + " :: " + $m.Line.Trim()) } + Write-Output ("QFCDATAMODEL_LINES: " + @(Get-Content -LiteralPath "QuickFiler\Controllers\QfcDatamodel.cs" -Encoding UTF8).Count) + +**CMD-ADDED-SCAN** (added lines of the anchored diff over the fourteen Write Set code files; run after the P6-T9 commit so the new files are tracked): + + PREFIX + $diff = @(git diff 94287369908cc920b21b0e3256314f988ad7d2f5 -- CODE14-GIT) + Write-Output ("GIT_DIFF_EXIT_CODE: " + $LASTEXITCODE) + $added = @($diff | Where-Object { $_.StartsWith("+") -and -not $_.StartsWith("+++") }) + Write-Output ("ADDED_LINES: " + $added.Count) + foreach ($t in @("Thread.Sleep", "Task.Delay", "DoNotParallelize", "Retry(", "Path.GetTempFileName", "Path.GetTempPath", "Workers", "Timeout(", "[Timeout(GateTimeoutMs)]", "GateTimeoutMs = ", "await Task.Yield();", "for (int i", "using var ", "_pinCount", "ArmingFakeTimeProvider")) { Write-Output ("ADDED-TOKEN [" + $t + "] = " + @($added | Where-Object { $_.Contains($t) }).Count) } + foreach ($l in @($added | Where-Object { $_.Contains("GateTimeoutMs = ") })) { Write-Output ("ADDED-LINE " + $l.Substring(1).Trim()) } + +`_pinCount` and `ArmingFakeTimeProvider` are the positive controls: both are added by this plan, so a scan that cannot see added lines reports 0 for them and the gate fails. + +**CMD-HUNKS** (`FILE-GIT` substituted with one forward-slash path; prints the hunk headers of the anchored diff so a region can be shown untouched): + + PREFIX + $d = @(git diff 94287369908cc920b21b0e3256314f988ad7d2f5 -- "FILE-GIT") + Write-Output ("GIT_DIFF_EXIT_CODE: " + $LASTEXITCODE) + foreach ($l in $d) { if ($l.StartsWith("@@")) { Write-Output ("HUNK " + $l) } } + Write-Output ("HUNK_COUNT: " + @($d | Where-Object { $_.StartsWith("@@") }).Count) + +**CMD-EOL** (`FILE` substituted with PC, SBW or AFTP; normalises the new file to CRLF after the Write tool creates it, then reports the line-ending census): + + PREFIX + $p = "FILE" + $t = [System.IO.File]::ReadAllText($p) + $crlf = [string][char]13 + [string][char]10 + $n = [regex]::Replace($t, "\r?\n", $crlf) + if (-not $n.EndsWith($crlf)) { $n = $n + $crlf } + [System.IO.File]::WriteAllText($p, $n, (New-Object System.Text.UTF8Encoding($false))) + $b = [System.IO.File]::ReadAllText($p) + Write-Output ("BARE_LF: " + [regex]::Matches($b, "(? BuildExecutingViewer", "QfcItemControllerTestSupport.BuildExecutingViewer()", "BuildExecutingViewer", "absorbs the delegate without running it", "shared UiThread static is irrelevant", "absorbs the queued application", "[TestMethod]"`; TS `"Becomes moot", "leaks exactly", "still delegate to a callee", "not reachable from another test file", "remaining legitimate", "QfcItemController_UiThreadDispatcherPinCountTests", "internal static void EnsureSynchronizationContext()", "UiThreadDispatcherFixture.EnsureDispatcher();"`; FT `"no other class may dispose", "removed that pin: the fixture now counts pins", "(W5) must not latch", "[Timeout(GateTimeoutMs)]", "private const int GateTimeoutMs = 60000;", "EnsureUiThreadDispatcher()", "issue #230 lost update", "the waiter cannot observe the pre-restore value", "[TestMethod]"`; `R4SPAN` `"EnsureUiThreadDispatcher()", "using (", "transactionA.Dispose();", "finally", ".BeSameAs(", ".NotBeSameAs(", "issue #230 lost update"`; `R4HEAD` `"EnsureUiThreadDispatcher()", "using (", "try"`; `R4TAIL` `"}", "finally", "transactionA.Dispose();"`; `ENSURE` `"_pinCount++", "_fixtureInstalledParked = true;", "lock (FieldLock)", "return new EnsureScope("`; `SCOPE` `"CompareExchange(", "lock (FieldLock)", "_pinCount--", "_fixtureInstalledParked = false;", "DispatcherField.SetValue(null, null);"`. + - Acceptance (each value is derived in facts 1 to 5 and is a falsifiable pre-change observation): `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; LINES 342, 497, 440, 470 in CS4 order; four HASH values recorded as `BASE-HASH:` lines; CMD-CENSUS `PRIMARY_LINES: 9` with PRIMARY-FILE fixture 1, test support 2, fixture tests 4, focus-and-theme 2, `CROSS_LINES: 20` with CROSS-FILE fixture 5, test support 2, InitializationTests.Part2 1, fixture tests 10, focus-and-theme 2, and `CONTROL_LINES: 23`; FIX tokens 0, 0, 4, 3, 2, 1, 1, 0, 0, 0 (the last, `installed nothing carries`, is 0 because the phrase wraps across lines 245 and 246: a vacuous baseline, recorded as such); FAT tokens 2, 1, 0, 9, 0, 0, 0, 17; TS tokens 1, 1, 1, 1, 0, 0, 1, 1; FT tokens 1, 0, 1, 8, 1, 4, 1, 1, 8; `R4SPAN` 1, 2, 1, 2, 1, 1, 1 with `SPAN: 212-284`; `R4HEAD` 1, 1, 1 with `SPAN: 212-224`; `R4TAIL` 3, 1, 0 with `SPAN: 267-273`; `ENSURE` 0, 0, 1, 2 with `SPAN: 122-145`; `SCOPE` 1, 0, 0, 0, 0 with `SPAN: 249-283`. Any differing value is `CENSUS MISMATCH`: record and stop, because a later gate is defined against these values. The non-zero counts (the two focus-and-theme ensure calls, the private helper, the four stale doc tokens, the R4 pin) are the positive controls for the zero gates of P6-T2 and P7-T1. +- [x] [P0-T13] Record the pre-change census of the six existing folded-scope files and the project file in FEATURE/evidence/baseline/fold-census-baseline.md, using `CMD-LINECOUNT` and `CMD-HASH` on FOLD6, `CMD-TOKEN-COUNT` once per FOLD6 file and on PROJ, and `CMD-SPAN-TOKEN-COUNT` for `T1-LIVE`, `HELD`, `T-SIB` and `GATE-LAMBDA`. + - Token lists: LIV `"class SynchronousBackgroundWorker", "StartSynchronously", "SynchronousBackgroundWorker.StartSynchronously", "new SynchronousBackgroundWorker()", "using (var worker = new SynchronousBackgroundWorker())", "StartHeldOpenLoader(", "Task.Yield", "fake.Advance", "FakeTimeProvider", "using QuickFiler.Test.TestSupport;", "NoSynchronizationContext", "Duplicated per file", "new ArmingFakeTimeProvider()", "[TestMethod]"`; TD `"class SynchronousBackgroundWorker", "StartSynchronously", "SynchronousBackgroundWorker.StartSynchronously", "using (var worker = new SynchronousBackgroundWorker())", "Duplicated per file", "using QuickFiler.Test.TestSupport;", "[TestMethod]"`; ZB `"class SynchronousBackgroundWorker", "StartSynchronously", "SynchronousBackgroundWorker.StartSynchronously", "new SynchronousBackgroundWorker()", "using (var worker = new SynchronousBackgroundWorker())", "InitEmailQueue(0, new", "InitEmailQueue(2, new", "Duplicated per file", "through the nested", "starting a real", "using QuickFiler.Test.TestSupport;", "[TestMethod]"`; DMT `"new BackgroundWorker()", "using (var worker = new BackgroundWorker())", "new ArmingFakeTimeProvider()", "clock.ReArm();", "await Task.WhenAny(clock.Armed, pending)", "must keep polling while the worker can still add candidates", "the gate re-armed instead of returning", "await Task.Yield();", "fake.Advance", "FakeTimeProvider", "using QuickFiler.Test.TestSupport;", "[TestMethod]"`; QDM `"Worker_RunWorkerCompleted", "nameof(LoadRemainingEmailsToQueue)", "nameof(LoadRemainingEmailsToQueueAsync)} Error.", "nameof(LoadRemainingEmailsToQueueAsync)} Task cancelled", "LoadRemainingEmailsToQueueAsync(", "LoadRemainingEmailsToQueue(BackgroundWorker bw", "log4net.ILog log =", "log4net.ILog logger =", "Linked List Locking", "#pragma", "#region", "#endregion", "[ExcludeFromCodeCoverage]", "//e.Result =", "//_blockingQueue = null;", "//worker.RunWorkerCompleted", "ForEachAwaitWithCancellationAsync", ": IQfcDatamodel", "RemainingEmailLoader = LoadRemainingEmailsToQueueAsync;", "WorkerStarter(worker);", "_remainingLoadActive = "`; QQP `"RunWorkerAsync", "written on the worker thread and read", "written on the worker thread", "(:31-66)", "TryUnhookOrReplace", "WorkerStarter", "share no other fence", "honest producer-liveness signal", "() => _remainingLoadActive,", "() => false,", "private volatile bool _remainingLoadActive;"`; PROJ `" result = await pending;"`; `GATE-LAMBDA` `"() => _remainingLoadActive,", "() => false,"`. + - Acceptance (facts 14 and 16 to 20; falsifiable pre-change observations): `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; LINES 312, 244, 232, 371, 495, 413 in FOLD6 order; six HASH values recorded as `BASE-HASH:` lines; LIV tokens 1, 3, 0, 2, 0, 4, 3, 3, 1, 0, 0, 0, 0, 4 (the ninth, `FakeTimeProvider`, is line 114 only: the `using Microsoft.Extensions.Time.Testing;` directive does not contain the token); TD tokens 1, 2, 0, 1, 1, 0, 5; ZB tokens 1, 4, 0, 3, 0, 1, 1, 1, 1, 1, 0, 3; DMT tokens 2, 0, 0, 0, 0, 1, 0, 1, 4, 5, 0, 9 (the tenth, `FakeTimeProvider`, is lines 99, 216, 224, 249 and 258; the directive at line 9 does not contain the token); QDM tokens 2, 4, 0, 1, 4, 1, 1, 1, 2, 2, 7, 7, 1, 2, 1, 1, 2, 1, 2, 2, 3; QQP tokens 1, 1, 2, 1, 3, 0, 0, 1, 1, 0, 1; PROJ tokens recorded as `PROJ-COMPILE-ITEMS-BASE:` for the first and 0, 0, 0, 1, 1 for the rest; `T1-LIVE` 5, 0, 3, 3, 1, 0, 1 with `SPAN: 110-165` (the last value is LIV line 163, `(await pending).Should().BeEmpty();`); `HELD` 1, 0, 0 with `SPAN: 183-217`; `T-SIB` 1, 0, 0, 0, 1 with `SPAN: 96-133`; `GATE-LAMBDA` 1, 0 with `SPAN: 299-310` (each printed end is the END anchor line minus one, as for R4SPAN). Any differing value is `FOLD CENSUS MISMATCH`: record and stop. The non-zero counts (three nested classes, the old-shape `Task.Yield` and retry loop, the four legacy members, the stale comment tokens) are the positive controls for the zero gates of P4-T9, P5-T5, P5-T12 and P6-T2. +- [x] [P0-T14] Capture the pre-change concurrent run of QfcItemController_UiThreadDispatcherFixtureTests and QfcItemController_FocusAndThemeTests from QuickFiler.Test/bin/Debug/QuickFiler.Test.dll with `CMD-VSTEST` (`FILTER-BASELINE-CONCURRENT`, `TASKID` p0-t14, empty `NAMES`) and record it in FEATURE/evidence/baseline/concurrent-set-baseline.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `TRX_PRESENT: True`; `SEQUENCE_FILES: 0`; `RESULT_COUNT: 25`; the `COUNTERS` line recorded as `BASELINE-CONCURRENT-COUNTERS:`; every `RESULT` line transcribed; `BASELINE-CONCURRENT-FAILED:` lists every non-Passed name with its `MESSAGE` line, or `NONE` (the comparison basis for P6-T7). Outcomes are observations and are not gated; `ExpectedExitCode:` carries the observed value when non-zero. A `Timeout` or `Aborted` outcome or a Sequence file is `BASELINE HANG`: stop. +- [x] [P0-T15] Capture the pre-change run of the four datamodel test classes from QuickFiler.Test/bin/Debug/QuickFiler.Test.dll with `CMD-VSTEST` (`FILTER-DATAMODEL`, `TASKID` p0-t15, empty `NAMES`) and record it in FEATURE/evidence/baseline/datamodel-set-baseline.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `TRX_PRESENT: True`; `SEQUENCE_FILES: 0`; `RESULT_COUNT: 21`; the `COUNTERS` line recorded as `BASELINE-DATAMODEL-COUNTERS:`; every `RESULT` line transcribed; `BASELINE-DATAMODEL-FAILED:` lists every non-Passed name with its `MESSAGE` line, or `NONE` (the comparison basis for P4-T11 and P6-T8). Outcomes are observations and are not gated; `ExpectedExitCode:` carries the observed value when non-zero. A `Timeout` or `Aborted` outcome or a Sequence file is `BASELINE HANG`: stop, and the two NAMES-LIVENESS tests are the first suspects (addendum section 5.3). +- [x] [P0-T16] Run the stall probe from UtilitiesCS.Test/bin/Debug/UtilitiesCS.Test.dll with `CMD-VSTEST` (`ASSEMBLY` the UtilitiesCS.Test assembly, `FILTER-STALL`, `TASKID` p0-t16, empty `NAMES`) and record FEATURE/evidence/baseline/stall-probe.md. + - Acceptance: the artifact records `WORKTREE-LEAF:`, `EXIT_CODE:`, `ExpectedExitCode:` equal to the observed value when non-zero (presentational), `TRX_PRESENT:`, `SEQUENCE_FILES:`, the `COUNTERS` line when present and every `MESSAGE` line; then exactly one `STALL-PROBE:` line — `CLEAR` when `EXIT_CODE: 0`, `failed=0` and `SEQUENCE_FILES: 0`, otherwise `REPRODUCES` — and exactly one `COVERAGE-ROUTE:` line — `RUNNER` under CLEAR, `DIRECT` under REPRODUCES. The probe runs once and is never re-run. Both values complete this task. +- [x] [P0-T17] Capture the baseline repository-wide test-and-coverage run by the route P0-T16 fixed and record FEATURE/evidence/baseline/coverage-summary.md and FEATURE/evidence/baseline/coverage-jacoco-projection.md: under RUNNER run `CMD-COVERAGE-RUNNER` with `STAGE` baseline, under DIRECT run `CMD-COVERAGE-DIRECT` with `STAGE` baseline; then, unless branch (d) applies, run `CMD-COVERAGE-POST` with `STAGE` baseline and `RAW` per its rule. + - Artifacts: coverage-summary.md carries `Timestamp:`, `Command:` (both payloads, with the route's canonical command), `EXIT_CODE:` (`RUNNER_EXIT_CODE:` or `COLLECT_EXIT_CODE:`), `ExpectedExitCode:` equal to the observed value when non-zero (a baseline observation), and an `Output Summary:` recording `WORKTREE-LEAF:`, `COVERAGE-ROUTE:`, `RAW:`, `DISCOVERED_LINE:` or `ASSEMBLY_COUNT:` with every `ASSEMBLY:` line, `TRX_PRESENT:`, `SEQUENCE_FILES:` (DIRECT), `THRESHOLD_MESSAGE:` and `COLLECT_FAILURE_MESSAGE:` (RUNNER), `LINE-FLOOR:`, `BRANCH-FLOOR:`, the `First-party coverage:` line (the numeric baseline headline: lines covered over valid with percentage, branches likewise), the `ROOT` line, the five summary lines verbatim between `SUMMARY-BEGIN` and `SUMMARY-END`, `FAILED-SET:` recorded as `BASELINE-FAILED-SET:`, every `MESSAGE` line (names redacted of paths), the `RESULT` lines (twelve at baseline: the pin-count tests do not exist yet), `TEST_ASSEMBLY_PACKAGES:`, `QFCDATAMODEL_CLASS_ENTRIES:` (recorded) and `CHANGED-CODE-COVERAGE: NOT MEASURED (TEST ASSEMBLY EXCLUDED; QFCDATAMODEL EXCLUDED BY ATTRIBUTE)`; coverage-jacoco-projection.md carries `Timestamp:`, a `Source:` line naming coverage-summary.md, and the projection verbatim between `PROJECTION-BEGIN` and `PROJECTION-END`. + - Branches, checked in order: (d) `TRX_PRESENT: False`, `SEQUENCE_FILES:` greater than 0, or a non-zero exit with an empty `FAILED-SET:` and no floor message, is `COVERAGE RUN ABORTED`: stop, report the last lines of the log with paths redacted, do not re-run. (c) `TEST_ASSEMBLY_PACKAGES:` other than 0 is `TEST ASSEMBLY INSTRUMENTED`: stop, because D-7 rests on the derived exclusion. (b) a non-zero exit with a non-empty `FAILED-SET:` or a floor `NOT MET` is recorded as `BASELINE-STATE: PRE-EXISTING FAILURES` (with `BASELINE-FLOOR:` naming any floor not met) and completes this task (D-8). (a) exit 0 with both floors met is `BASELINE-STATE: GREEN` and completes this task. +- [x] [P0-T18] Write the baseline toolchain index FEATURE/evidence/baseline/toolchain-baseline.md from the P0-T9, P0-T10, P0-T11 and P0-T17 artifacts. + - Acceptance: `Timestamp:`; one row per step in order — csharpier check, analyzer rebuild, TreatWarningsAsErrors rebuild, coverage run (route) — each with its canonical command, `EXIT_CODE:` copied from the step artifact, and the step artifact's path; `BASELINE-STATE:` copied from P0-T17; the `First-party coverage:` line copied from P0-T17. This file is an index over the per-step artifacts, not a substitute for them. +- [x] [P0-T19] Commit the Phase 0 evidence (FEATURE only) and record it in FEATURE/evidence/baseline/phase0-commit.md. + - Commands, separate Bash calls: `git -C WORKTREE add -- docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968`; `git -C WORKTREE commit -m "docs(968): record phase 0 baseline evidence" -- docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968`; `git -C WORKTREE rev-parse HEAD`; `git -C WORKTREE show --name-status --format= HEAD`; `git -C WORKTREE status --porcelain --untracked-files=all`. + - Acceptance: both git writes exit 0; `PHASE0-COMMIT:` records the new HEAD as an observation; `PHASE0-COMMIT-PATHS:` lists the `show` output and every path is under FEATURE (the pathspec-limited commit cannot carry anything else; any other path is `COMMIT SWEPT FOREIGN PATH`: stop); no porcelain line names a path under FEATURE other than this plan file (whose check-off mark is written after the commit) and FEATURE/evidence/baseline/phase0-commit.md (written after the commit), and no porcelain line names a path under QuickFiler/ or QuickFiler.Test/. This artifact itself is committed in P6-T9. + +### Phase 1 — Regression Test First (fail-before on the unmodified fixture) + +- [x] [P1-T1] Create QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs with the whole content of Delivered Source N1 (Write tool, absolute path, the Markdown indent removed), then normalise its line endings with `CMD-EOL` (`FILE` PC). + - Acceptance (CMD-EOL output recorded by P1-T3): `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `BARE_LF: 0`; `CRLF_COUNT:` equals `LINES:`; `LINES:` at most 500 and at least 200. +- [x] [P1-T2] Insert Delivered Source T1 into QuickFiler.Test/QuickFiler.Test.csproj immediately after line 203 (``), as an in-place Edit. + - Acceptance (recorded by P1-T3): exactly one line of the project file contains `Controllers\QfcItemController.UiThreadDispatcherPinCountTests.cs`, it begins with four spaces and `` (fact 13: the first-release assertion failed because the first pin's release nulled the field). The artifact states that the fixture is at BASE content (P0-T12 `BASE-HASH:` for FIX is re-derived with `CMD-HASH` on `"FIX"` in this task and must match), that the test ran alone so the baseline was null (fact 7), and that this run is the fail-before half of AC5. A `Passed` result is `REGRESSION DID NOT FAIL`: stop and report, because the root-cause claim rests on it. A `Failed` result whose `MESSAGE` lacks `to refer to` or the because text (for example an exception, or the `NotBeNull` because text `the first pin seeds the parked dispatcher into a null field`) is `FAIL-BEFORE WRONG REASON`: stop and report, because the test is then defective rather than the fixture. +- [x] [P1-T6] Run the three specification tests against the unmodified fixture from QuickFiler.Test/bin/Debug/QuickFiler.Test.dll with `CMD-VSTEST` (`FILTER-PC-T234`, `TASKID` p1-t6, `NAMES` the last three names of `NAMES-PC`) and record FEATURE/evidence/regression-testing/specification-tests-before-fix.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `EXIT_CODE: 0`; `TRX_PRESENT: True`; `SEQUENCE_FILES: 0`; `RESULT_COUNT: 3`; three `RESULT` lines, one per name, each `Passed` with duration recorded (the "passes before and after the fix" half of the labels AC6 requires; test 4's second transaction installs the captured parked instance and its pin installs nothing, so it passes on the unmodified fixture too). Any `Failed` is `SPECIFICATION TEST FAILS BEFORE FIX`: stop and report, because the spec's design trace for that test is then wrong. + +### Phase 2 — Fixture Fix (reference-counted pin and D1 documentation) + +- [x] [P2-T1] Insert Delivered Source F-FIELDS into QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs after line 38 (`private static Dispatcher _parkedDispatcher = null;`), as an in-place Edit. + - Acceptance (recorded by P2-T6): one line contains `private static int _pinCount;` and one contains `private static bool _fixtureInstalledParked;`, both inside the class's static field block above the issue #743 counters; the inserted comment contains `only while FieldLock is held` and does not contain `lock (FieldLock)`. +- [x] [P2-T2] Insert Delivered Source F-CLASSDOC into QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs after pre-edit line 30 (`/// ` closing the design-note paragraph, now shifted by P2-T1 only if P2-T1 is applied first; apply this task by content, locating the `/// ` line that immediately precedes the class `/// `). + - Acceptance (recorded by P2-T6): `install-ownership flag` 1 and `pins for the process lifetime` at least 1 in the file; the class doc ends with the new paragraph followed by `/// `. +- [x] [P2-T3] Replace the `EnsureDispatcher` summary (pre-edit lines 116 to 121, located by content: from the `/// ` immediately above `internal static IDisposable EnsureDispatcher()` to the `/// ` immediately above it) in QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs with Delivered Source F-ENSURE-DOC. + - Acceptance (recorded by P2-T6): `leaks exactly` 0; `pins for the process lifetime` 2 in the file. +- [x] [P2-T4] Replace the `EnsureDispatcher` body (pre-edit lines 128 to 137, located by content: from the `lock (FieldLock)` line after `Dispatcher parked = GetParkedDispatcher();` through `return new EnsureScope(null);`) in QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs with Delivered Source F-ENSURE-BODY. + - Acceptance (recorded by P2-T6): within `ENSURE`, `_pinCount++` 1, `_fixtureInstalledParked = true;` 1, `lock (FieldLock)` 1, `return new EnsureScope(` 1. +- [x] [P2-T5] Replace the `EnsureScope` documentation and class (pre-edit lines 243 to 274, located by content: from the `/// ` immediately above `private sealed class EnsureScope : IDisposable` through the class's closing brace, the `}` immediately before the fixture class's closing `}`) in QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs with Delivered Source F-SCOPE. + - Acceptance (recorded by P2-T6): within `SCOPE`, `CompareExchange(` 0, `lock (FieldLock)` 1, `_pinCount--` 1, `_fixtureInstalledParked = false;` 1, `DispatcherField.SetValue(null, null);` 1; `A scope that installed nothing` 0 in the file. +- [x] [P2-T6] Record the fixture-change census in FEATURE/evidence/qa-gates/fixture-change-census.md with `CMD-LINECOUNT` on `"FIX"`, `CMD-TOKEN-COUNT` on FIX (the P0-T12 FIX token list), `CMD-SPAN-TOKEN-COUNT` on `ENSURE` and `SCOPE` (the P0-T12 token lists), `git -C WORKTREE diff --numstat HEAD -- QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs`, `git -C WORKTREE diff HEAD -- QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs`, paired with `git -C WORKTREE status --porcelain -- QuickFiler QuickFiler.Test`. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; FIX LINES 375; FIX tokens 4, 4, 5, 2, 1, 0, 0, 2, 1, 0 (in the P0-T12 order: `_pinCount`, `_fixtureInstalledParked`, `lock (FieldLock)`, `CompareExchange(`, `return new EnsureScope(`, `leaks exactly`, `A scope that installed nothing`, `pins for the process lifetime`, `install-ownership flag`, `installed nothing carries`); `ENSURE` 1, 1, 1, 1; `SCOPE` 0, 1, 1, 1, 1; the porcelain span lists exactly ` M QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs`, ` M QuickFiler.Test/QuickFiler.Test.csproj` and `?? QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs`; and a `FIELDLOCK-ENCLOSURE:` section that, reading the transcribed diff, names the two `lock (FieldLock)` blocks (in `EnsureDispatcher` and in `EnsureScope.Dispose`) and states that the three non-declaration occurrences of each new field lie inside them and that no `CompareExchange` call appears in the scope class (the AC9 reading). Any other count: correct the edit and re-run this task. +- [x] [P2-T7] Build the fixed fixture with `CMD-BUILD` (`TASKID` p2-t7) against WORKTREE/TaskMaster.sln and record it in FEATURE/evidence/regression-testing/pass-after-build.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`, `EXIT_CODE: 0`, `ERRORS: 0`, `TEST_DLL_ADVANCED: True`, `CSC_OUT_QUICKFILER_TEST:` at least 1. +- [x] [P2-T8] Run the four pin-count tests against the fixed fixture from QuickFiler.Test/bin/Debug/QuickFiler.Test.dll with `CMD-VSTEST` (`FILTER-PC-CLASS`, `TASKID` p2-t8, `NAMES-PC`) and record FEATURE/evidence/regression-testing/pass-after-pin-count.md, together with `git -C WORKTREE status --porcelain -- QuickFiler QuickFiler.Test`. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `EXIT_CODE: 0`; `TRX_PRESENT: True`; `SEQUENCE_FILES: 0`; `RESULT_COUNT: 4`; four `RESULT` lines, one per NAMES-PC name, each `Passed` with duration recorded; the porcelain span (`PHASE2-PORCELAIN:`) equals `PHASE1-PORCELAIN:` from P1-T3 plus exactly one extra line ` M QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs`, recorded as the AC5 statement that the only difference between the P1-T5 run and this run is the fixture file. A `Failed` regression test is `FIX DID NOT TAKE`: correct the fixture within D-1 and re-run from P2-T6. + +### Phase 3 — Theme-Test Deletions, R4 Restructure and D2 to D6 + +- [x] [P3-T1] Apply Delivered Source A1 to QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs: delete lines 99 to 116 (the private `BuildExecutingViewer` method and the blank line after it), located by content as the block from `private static Mock BuildExecutingViewer()` through its closing `}` plus the following blank line. + - Acceptance (recorded by P3-T9): `private static Mock BuildExecutingViewer` 0; the line after the `BuildFocusController` method's closing `}` and one blank line is the `/// ` of `EnableHandlelessThemeInvoke`. +- [x] [P3-T2] Apply Delivered Source A2 to QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs: replace all seven `var viewer = BuildExecutingViewer();` lines with `var viewer = QfcItemControllerTestSupport.BuildExecutingViewer();` and replace the six-line cycle-3 comment block with the seven-line A2 block. + - Acceptance (recorded by P3-T9): `QfcItemControllerTestSupport.BuildExecutingViewer()` 8 and `BuildExecutingViewer` 8 (every remaining mention is prefixed). +- [x] [P3-T3] Apply Delivered Source A3 to `SetThemeDark_FromNormal_SelectsDarkNormalTheme` in QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs: replace the two arrange comment lines and the `QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` line with the five A3 comment lines. + - Acceptance (recorded by P3-T9): `absorbs the delegate without running it` 1 and `shared UiThread static is irrelevant` 1; the first statement of the test is `var controller = new FocusController();`. +- [x] [P3-T4] Apply Delivered Source A4 to `SetThemeLight_FromNormal_SelectsLightNormalTheme` in QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs: replace the `// Arrange` line and the `QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` line with the two A4 comment lines. + - Acceptance (recorded by P3-T9): `absorbs the queued application` 1; `EnsureUiThreadDispatcher` 0 in the file. +- [x] [P3-T5] Replace the `EnsureUiThreadDispatcher` documentation (lines 216 to 237, located by content: from the `/// ` whose next line begins `/// Ensures the static UiThread.Dispatcher` to the `/// ` immediately above `internal static IDisposable EnsureUiThreadDispatcher() =>`) in QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs with Delivered Source S1. + - Acceptance (recorded by P3-T9): `Becomes moot` 0, `leaks exactly` 0, `still delegate to a callee` 0, `remaining legitimate` 1, `QfcItemController_UiThreadDispatcherPinCountTests` 1; the declaration and body lines are unchanged (`UiThreadDispatcherFixture.EnsureDispatcher();` 1). +- [x] [P3-T6] Replace the `BuildExecutingViewer` documentation (pre-edit lines 282 to 288, located by content: from the `/// ` whose next line begins `/// Issue #480 shared arrange helper.` to the `/// ` immediately above `internal static Mock BuildExecutingViewer()`) in QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs with Delivered Source S2. + - Acceptance (recorded by P3-T9): `not reachable from another test file` 0; `Issue #480 shared arrange helper` 1. +- [x] [P3-T7] Replace the R4 `` paragraph (lines 196 to 208, located by content: from the `/// ` whose next line begins `/// Issue #950: the earlier intermittent failure` to the following `/// `) in QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs with Delivered Source R-DOC. + - Acceptance (recorded by P3-T9): `no other class may dispose` 0, `removed that pin: the fixture now counts pins` 1, `(W5) must not latch` 1. +- [x] [P3-T8] Apply Delivered Source R-BODY to `Transaction_SecondCallerCannotInstallUntilTheFirstRestores` in QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs: replace the four-line `using (` header (lines 221 to 224) with `try` and `{`, and replace the sixteen-space `}` at line 270 with the five-line `}` / `finally` / `{` / `transactionA.Dispose();` / `}` block; lines 225 to 269 are unchanged. + - Acceptance (recorded by P3-T9): `R4SPAN` `EnsureUiThreadDispatcher()` 0, `using (` 1, `transactionA.Dispose();` 2, `finally` 3, `.BeSameAs(` 1, `.NotBeSameAs(` 1, `issue #230 lost update` 1; `R4HEAD` 0, 0, 2; `R4TAIL` `}` 4, `finally` 2, `transactionA.Dispose();` 1; `[Timeout(GateTimeoutMs)]` 8 and `private const int GateTimeoutMs = 60000;` 1 unchanged. +- [x] [P3-T9] Record the test-edit census in FEATURE/evidence/qa-gates/test-edit-census.md with `CMD-LINECOUNT` on CS5, `CMD-TOKEN-COUNT` on FAT, TS and FT (the P0-T12 lists, the TS list extended with `"Issue #480 shared arrange helper"`), `CMD-SPAN-TOKEN-COUNT` on `R4SPAN`, `R4HEAD` and `R4TAIL` (the P0-T12 lists), `CMD-HUNKS` on `QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs` and on `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs`, and `git -C WORKTREE diff --numstat HEAD -- QuickFiler.Test` paired with `git -C WORKTREE status --porcelain -- QuickFiler QuickFiler.Test`. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; LINES 375, 482, 442, 472 for FIX, FAT, TS, FT and at most 500 for PC; FAT tokens 0, 0, 8, 8, 1, 1, 1, 17; TS tokens 0, 0, 0, 0, 1, 1, 1, 1 then `Issue #480 shared arrange helper` 1; FT tokens 0, 1, 1, 8, 1, 3, 1, 1, 8; `R4SPAN` 0, 1, 2, 3, 1, 1, 1; `R4HEAD` 0, 0, 2; `R4TAIL` 4, 2, 1; TestSupport `HUNK_COUNT: 2` with every `HUNK` old-range start at or above 200 (the `EnsureSynchronizationContext` region 85 to 96 is untouched, AC17); fixture-tests hunks each with old-range start at or above 190 and old-range start plus old-range length at or below 285 (every hunk lies in R4's doc and body, AC10); the porcelain span lists exactly the five `.cs` #968 Write Set paths (four ` M`, one `??`) and ` M QuickFiler.Test/QuickFiler.Test.csproj`. Any other value: correct the edit and re-run this task. + +### Phase 4 — Folded Scope A: Shared Worker, Caller-Owned Disposal and Dead-Code Removal (#972 items 1, 3 and 4) + +- [x] [P4-T1] Record the zero-caller proof for the four legacy `QfcDatamodel` members against the pre-change tree in FEATURE/evidence/qa-gates/qfc-datamodel-legacy-callers.md with `CMD-LEGACY-CALLERS`, before any Phase 4 edit. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `QFCDATAMODEL_LINES: 495` (recorded as `QFCDATAMODEL-LINES-BEFORE:`, the AC28 before figure); `PRIMARY_LINES: 25`; `LOG_LINES: 3`; `CROSS_LINES: 2`; `INTERFACE_LINES: 0`; `SWEEP-CS` lines name exactly the five `.cs` files of fact 15 (`QuickFiler/Controllers/QfcHomeController.cs`, `QuickFiler/Controllers/QfcDatamodel.cs`, `QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs`, `QuickFiler.Test/Controllers/QfcHomeControllerRunAsyncTests.cs`, `QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs`); every `IVT` line is one of the four grants of fact 15. The artifact then classifies every `PRIMARY`, `LOG` and `CROSS` line as `DECLARATION` (inside `QfcDatamodel.cs`), `COMMENTED-OUT`, `DOC-PROSE`, `CREF-ONE-ARG-OVERLOAD` (`QfcDatamodel.cs` 130), `METHOD-GROUP-ONE-ARG-OVERLOAD` (`QfcDatamodel.cs` 40 and 52, the assignment that binds the surviving one-argument overload; fact 14), `REGION-DIRECTIVE` (`QfcDatamodel.cs` 469 and 472, the `#region` and `#endregion` lines of the empty `Linked List Locking` region that P1 removes), `NAMEOF-RETARGETED` (`QfcDatamodel.cs` 369), `SELF-REFERENCE` (a hit inside a removed member's own body), `OTHER-TYPE-SAME-NAME` (`QfcHomeController.cs` 92, 132, 344, 379 and `QfcHomeControllerRunAsyncTests.cs` 325, 376, whose reflective `GetMethod` is invoked on `_controller`, a `QfcHomeController`) or `INVOCATION`; records `INVOCATIONS: 0`; writes the `## Numeric Derivation Evidence` block of the addendum (Complete Family, Exhaustive Search Scope, Inclusion Rules, Exclusion Rules, Primary Search Strategy, Primary Member Set, Primary Count 4, Cross-check Search Strategy, Cross-check Member Set, Cross-check Count 4, Member-set Comparison identical); and states that this is unreachable dead code with no behaviour to regress, so no failing test precedes its removal and the compile proof is the two rebuilds. Any `INVOCATION` classification, or a count other than stated, is `LEGACY MEMBER HAS A CALLER`: stop and report. +- [x] [P4-T2] Create QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs with the whole content of Delivered Source W1 (Write tool, absolute path, the Markdown indent removed), then normalise its line endings with `CMD-EOL` (`FILE` SBW). + - Acceptance (recorded by P4-T9): `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `BARE_LF: 0`; `CRLF_COUNT:` equals `LINES:`; `LINES:` at most 500 and at least 20. +- [x] [P4-T3] Insert the first T2 line (``, four leading spaces) into QuickFiler.Test/QuickFiler.Test.csproj immediately after the line containing `TestSupport\DedicatedWorkerThread.cs`, as an in-place Edit. + - Acceptance (recorded by P4-T9): exactly one line of the project file contains `TestSupport\SynchronousBackgroundWorker.cs`; the line before it contains `TestSupport\DedicatedWorkerThread.cs` and the line after it contains `TestSupport\WinFormsPumpHostTests.cs`. +- [x] [P4-T4] Apply Delivered Source TD1 to QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs (the `using`, the deletion of the nested worker and starter, the starter retarget). + - Acceptance (recorded by P4-T9): TD tokens `class SynchronousBackgroundWorker` 0, `StartSynchronously` 1, `SynchronousBackgroundWorker.StartSynchronously` 1, `using (var worker = new SynchronousBackgroundWorker())` 1, `Duplicated per file` 0, `using QuickFiler.Test.TestSupport;` 1, `[TestMethod]` 5; LINES 229. +- [x] [P4-T5] Apply Delivered Source Z1, Z2 and Z3 to QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs (the `using`, the two doc rewordings, the deletion of the nested worker and starter, the three `using` blocks and starter retargets). + - Acceptance (recorded by P4-T9): ZB tokens `class SynchronousBackgroundWorker` 0, `StartSynchronously` 3, `SynchronousBackgroundWorker.StartSynchronously` 3, `new SynchronousBackgroundWorker()` 3, `using (var worker = new SynchronousBackgroundWorker())` 3, `InitEmailQueue(0, new` 0, `InitEmailQueue(2, new` 0, `Duplicated per file` 0, `through the nested` 0, `starting a real` 0, `using QuickFiler.Test.TestSupport;` 1, `[TestMethod]` 3; LINES at most 500. +- [x] [P4-T6] Apply Delivered Source L1a, L2, L5, L6 and L7 to QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs (the added `using`, the deletion of the nested worker and starter, the `StartHeldOpenLoader` signature and body, the three callers' `using` blocks), and in test 1, as an interim edit that L3 replaces in P5-T3, change the line `model.WorkerStarter = StartSynchronously;` to `model.WorkerStarter = SynchronousBackgroundWorker.StartSynchronously;` so the file compiles before the test-1 rewrite. + - Acceptance (recorded by P4-T9): LIV tokens `class SynchronousBackgroundWorker` 0, `StartSynchronously` 2, `SynchronousBackgroundWorker.StartSynchronously` 2, `new SynchronousBackgroundWorker()` 4, `using (var worker = new SynchronousBackgroundWorker())` 3 (the three callers; test 1 still carries its pre-rewrite `var worker = new SynchronousBackgroundWorker();`), `StartHeldOpenLoader(` 4, `Task.Yield` 3 and `fake.Advance` 3 and `FakeTimeProvider` 1 (all unchanged until P5-T3), `using QuickFiler.Test.TestSupport;` 1, `Duplicated per file` 0, `NoSynchronizationContext` 0, `new ArmingFakeTimeProvider()` 0, `[TestMethod]` 4; `HELD` 0, 1, 1. +- [x] [P4-T7] Apply Delivered Source M1 and M3 to QuickFiler.Test/Controllers/QfcDatamodelTests.cs (the `using`, and the `using` block around the `WaitForQueue` test's worker). + - Acceptance (recorded by P4-T9): DMT tokens `new BackgroundWorker()` 2, `using (var worker = new BackgroundWorker())` 1 (the sibling test's worker is wrapped by M2 in P5-T4), `using QuickFiler.Test.TestSupport;` 1, `[TestMethod]` 9. +- [x] [P4-T8] Apply Delivered Source P1 to QuickFiler/Controllers/QfcDatamodel.cs (the nine deletions and the one `nameof` retarget, located by the quoted lines). + - Acceptance (recorded by P4-T9): QDM tokens 0, 0, 1, 0, 2, 0, 0, 1, 0, 0, 6, 6, 1, 0, 0, 0, 0, 1, 2, 2, 3 in the P0-T13 order; LINES 367 and at most 400. +- [x] [P4-T9] Record the fold-edit census in FEATURE/evidence/qa-gates/fold-edit-census.md with the P4-T2 CMD-EOL output, `CMD-LINECOUNT` on FOLD6 plus `"SBW"`, `CMD-TOKEN-COUNT` on LIV, TD, ZB, DMT, QDM and PROJ (the P0-T13 lists) and on SBW (TOKENS `"class SynchronousBackgroundWorker", "internal sealed class SynchronousBackgroundWorker : BackgroundWorker", "internal static void StartSynchronously(BackgroundWorker worker)", "Dispose", "namespace QuickFiler.Test.TestSupport"`), `CMD-SPAN-TOKEN-COUNT` on `HELD`, `CMD-HUNKS` on `QuickFiler/Controllers/QfcDatamodel.cs`, and `git -C WORKTREE diff --numstat HEAD -- QuickFiler QuickFiler.Test` paired with `git -C WORKTREE status --porcelain -- QuickFiler QuickFiler.Test`. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; the P4-T2 to P4-T8 token and LINES conditions all hold; SBW tokens 1, 1, 1, 1, 1; PROJ tokens `PROJ-COMPILE-ITEMS-BASE:` plus 2, 1, 1, 0, 1, 1; `HUNK_COUNT:` for QfcDatamodel.cs is recorded, not gated (git merges edits separated by at most six unchanged lines, so the nine P1 edits yield fewer hunks); the `--numstat` row for `QuickFiler/Controllers/QfcDatamodel.cs` reads `1 129` (128 removed lines plus the replaced line at old 369, whose replacement is the only added line); the porcelain span lists exactly the five #968 `.cs` paths, ` M QuickFiler.Test/QuickFiler.Test.csproj`, `?? QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs`, ` M QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs`, ` M QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs`, ` M QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs`, ` M QuickFiler.Test/Controllers/QfcDatamodelTests.cs` and ` M QuickFiler/Controllers/QfcDatamodel.cs` (twelve lines; `QfcDatamodel.QueueProcessing.cs` is untouched until P5-T11). Any other value: correct the edit and re-run this task. +- [x] [P4-T10] Build the tree after the Phase 4 edits with `CMD-BUILD` (`TASKID` p4-t10) against WORKTREE/TaskMaster.sln and record it in FEATURE/evidence/regression-testing/fold-build.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`, `EXIT_CODE: 0`, `ERRORS: 0`, `TEST_DLL_ADVANCED: True`, `PROD_DLL_ADVANCED: True`, `CSC_OUT_QUICKFILER:` and `CSC_OUT_QUICKFILER_TEST:` each at least 1. This build is the first compile proof that no surviving code referenced a removed member (a CS0103 or CS0117 naming one of the four is `LEGACY MEMBER HAS A CALLER`: stop). A compile error in a fold test file is corrected within the delivered design, then P4-T9 and this task are re-run. +- [x] [P4-T11] Run the four datamodel classes after the consolidation from QuickFiler.Test/bin/Debug/QuickFiler.Test.dll with `CMD-VSTEST` (`FILTER-DATAMODEL`, `TASKID` p4-t11, empty `NAMES`) and record FEATURE/evidence/regression-testing/datamodel-set-after-consolidation.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `TRX_PRESENT: True`; `SEQUENCE_FILES: 0`; `RESULT_COUNT: 21`; every `RESULT` line transcribed; `EXIT_CODE: 0` with `passed=21 failed=0`, or every non-Passed name present in P0-T15 `BASELINE-DATAMODEL-FAILED:` (recorded as pre-existing with `ExpectedExitCode:` equal to the observed value). A new failure is `CONSOLIDATION BROKE A DATAMODEL TEST`: correct the fold edit at fault within the delivered design and re-run from P4-T9. + +### Phase 5 — Folded Scope B: Deterministic Liveness Tests (#968 comment) and the Producer-Liveness Comment (#972 item 2) + +- [x] [P5-T1] Write the fail-before exception dossier FEATURE/evidence/regression-testing/fail-before-exception..md (timestamp from `Get-Date -Format yyyy-MM-ddTHH-mm` at write time) BEFORE any Phase 5 edit, recording with `CMD-SPAN-TOKEN-COUNT` on `T1-LIVE` and `T-SIB` (the P0-T13 token lists) that the old shapes are still on disk. + - Acceptance: the artifact carries `Timestamp:`, `Command:` (the two span payloads), `EXIT_CODE: 0`, `Output Summary:` with `T1-LIVE` `Task.Yield` 3, `fake.Advance` 3, `for (int i` 1 and `T-SIB` `await Task.Yield();` 1 (the constructs whose removal AC31 requires), a `WhyFailingRunImpossible:` line stating that the old tests fail only when the thread pool delays a queued continuation past the bounded retry or past the second advance, which no test input can force, and that the production behaviour under test is correct before and after (addendum section 5.5), an `## Alternative proof` section naming (i) the mechanism reading of addendum section 5.3 with the web-verified timer facts of 5.2, and (ii) the labelled sensitivity check of P5-T8 as evidence of the new tests' sensitivity rather than a fail-before of the old ones, and a `SearchScope:` / `SearchPatterns:` / `SearchResult:` triple recording that `FEATURE/evidence/regression-testing/` holds no failing-run artifact for these two tests (`SearchPatterns: liveness-*.md, fail-before-*.md`). Exactly one file matching `fail-before-exception.*.md` exists in that folder after this task. +- [x] [P5-T2] Create QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs with the whole content of Delivered Source W2 (Write tool, absolute path, the Markdown indent removed), normalise its line endings with `CMD-EOL` (`FILE` AFTP), and insert the second T2 line (``, four leading spaces) into QuickFiler.Test/QuickFiler.Test.csproj immediately after the line containing `TestSupport\SynchronousBackgroundWorker.cs`. + - Acceptance (recorded by P5-T5): `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `BARE_LF: 0`; `CRLF_COUNT:` equals `LINES:`; `LINES:` at most 500 and at least 30; exactly one project-file line contains `TestSupport\ArmingFakeTimeProvider.cs`, the line before it contains `TestSupport\SynchronousBackgroundWorker.cs` and the line after it contains `TestSupport\WinFormsPumpHostTests.cs`. +- [x] [P5-T3] Apply Delivered Source L1b, L3 and L4 to QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs (remove the `Microsoft.Extensions.Time.Testing` using, replace test 1 whole with L-T1, insert L-SCOPE after `ReadLivenessFlag`). + - Acceptance (recorded by P5-T5): `T1-LIVE` tokens `await` 3, `using (NoSynchronizationContext())` 2, `Task.Yield` 0, `fake.Advance` 0, `for (int i` 0, `clock.ReArm();` 1, `(await pending)` 1; LIV tokens `Task.Yield` 0, `fake.Advance` 0, `FakeTimeProvider` 2 (the two `ArmingFakeTimeProvider` lines of L-T1), `NoSynchronizationContext` 3, `new ArmingFakeTimeProvider()` 1, `new SynchronousBackgroundWorker()` 4, `using (var worker = new SynchronousBackgroundWorker())` 4, `StartSynchronously` 2, `[TestMethod]` 4; and, reading the T1-LIVE span, each `await` line lies outside both `using (NoSynchronizationContext())` blocks (recorded as `SCOPE-BODIES-AWAIT-FREE: YES`). +- [x] [P5-T4] Apply Delivered Source M2 to QuickFiler.Test/Controllers/QfcDatamodelTests.cs (replace the sibling test whole with M-T). + - Acceptance (recorded by P5-T5): `T-SIB` tokens `await Task.Yield();` 0, `clock.ReArm();` 1, `await Task.WhenAny(clock.Armed, pending)` 1, `using (var worker = new BackgroundWorker())` 1, `IList result = await pending;` 1; DMT tokens `new BackgroundWorker()` 2, `using (var worker = new BackgroundWorker())` 2, `new ArmingFakeTimeProvider()` 1, `must keep polling while the worker can still add candidates` 1, `the gate re-armed instead of returning` 1, `await Task.Yield();` 0, `fake.Advance` 2, `FakeTimeProvider` 6, `[TestMethod]` 9. +- [x] [P5-T5] Record the liveness-edit census in FEATURE/evidence/qa-gates/liveness-edit-census.md with the P5-T2 CMD-EOL output, `CMD-LINECOUNT` on `"LIV", "DMT", "AFTP"`, `CMD-TOKEN-COUNT` on LIV, DMT and PROJ (the P0-T13 lists) and on AFTP (TOKENS `"internal sealed class ArmingFakeTimeProvider : FakeTimeProvider", "internal Task Armed", "internal void ReArm()", "public override ITimer CreateTimer(", "base.CreateTimer(", "RunContinuationsAsynchronously", "_armed.TrySetResult(true);", "namespace QuickFiler.Test.TestSupport"`), `CMD-SPAN-TOKEN-COUNT` on `T1-LIVE`, `HELD` and `T-SIB`, and `git -C WORKTREE diff --numstat HEAD -- QuickFiler QuickFiler.Test` paired with `git -C WORKTREE status --porcelain -- QuickFiler QuickFiler.Test`. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; the P5-T2, P5-T3 and P5-T4 conditions all hold; AFTP tokens 1, 1, 1, 1, 1, 1, 1, 1; PROJ tokens `PROJ-COMPILE-ITEMS-BASE:` plus 3, 1, 1, 1, 1, 1; every LINES value at most 500; the porcelain span equals the P4-T9 span plus `?? QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs` (thirteen lines). Any other value: correct the edit and re-run this task. +- [x] [P5-T6] Build the tree with the rewritten tests using `CMD-BUILD` (`TASKID` p5-t6) against WORKTREE/TaskMaster.sln and record it in FEATURE/evidence/regression-testing/liveness-build.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`, `EXIT_CODE: 0`, `ERRORS: 0`, `TEST_DLL_ADVANCED: True`, `CSC_OUT_QUICKFILER_TEST:` at least 1. A compile error in W2, L-T1, L-SCOPE or M-T is corrected within the delivered design, then P5-T5 and this task are re-run. +- [x] [P5-T7] Run the two rewritten tests by fully qualified name from QuickFiler.Test/bin/Debug/QuickFiler.Test.dll with `CMD-VSTEST` (`FILTER-LIVENESS-PAIR`, `TASKID` p5-t7, `NAMES-LIVENESS`) and record FEATURE/evidence/regression-testing/liveness-pass-after.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `EXIT_CODE: 0`; `TRX_PRESENT: True`; `SEQUENCE_FILES: 0`; `RESULT_COUNT: 2`; two `RESULT` lines, one per NAMES-LIVENESS name, each `Passed` with duration recorded. A `Failed` result whose `MESSAGE` contains `must clear the flag before the next poll` means the inline-continuation assumption of D-18 did not hold on this host: stop and report with the message (do not add a wait or a loop). Any other failure or a `Timeout` outcome is `LIVENESS REWRITE DEFECT`: correct within D-18 and re-run from P5-T5. +- [x] [P5-T8] [expect-fail] Run the labelled sensitivity check and record FEATURE/evidence/regression-testing/liveness-sensitivity-check.md: (1) Edit QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs, replacing the line ` () => _remainingLoadActive,` (sixteen-space indent, inside the `new QfcStreamingDequeueConfidenceGate(` argument list) with ` () => false,`; (2) `CMD-SPAN-TOKEN-COUNT` on `GATE-LAMBDA`; (3) `CMD-BUILD` (`TASKID` p5-t8); (4) `CMD-VSTEST` (`FILTER-LIVENESS-PAIR`, `TASKID` p5-t8, `NAMES-LIVENESS`); (5) Edit the same line back to ` () => _remainingLoadActive,`; (6) `CMD-SPAN-TOKEN-COUNT` on `GATE-LAMBDA` again; (7) `git -C WORKTREE diff --exit-code 94287369908cc920b21b0e3256314f988ad7d2f5 -- QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs`; (8) `git -C WORKTREE status --porcelain -- QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs`. + - Acceptance: the artifact's first `Command:` row is the step (4) payload with `EXIT_CODE: 1` and `ExpectedExitCode: 1`; `WORKTREE-LEAF: agent-a291a7fbabf9d0229` in every payload; step (2) `GATE-LAMBDA` 0, 1 (edit applied); step (3) `EXIT_CODE: 0`, `ERRORS: 0`, `PROD_DLL_ADVANCED: True`, `TEST_DLL_ADVANCED: True`; step (4) `TRX_PRESENT: True`, `SEQUENCE_FILES: 0`, `RESULT_COUNT: 2`, both `RESULT` lines `Failed` (not `Timeout`, not `Aborted`), the Liveness `MESSAGE` containing `to refer to` and `the gate must arm a second wait`, the sibling `MESSAGE` containing `to refer to` and `must keep polling while the worker can still add candidates` (each failed on its re-arm assertion, so the new shape is sensitive to a dishonest liveness signal and fails crisply rather than hanging); step (6) `GATE-LAMBDA` 1, 0 (edit reverted); step (7) exits 0 (the file is byte-identical to BASE; recorded as `SENSITIVITY-EDIT-REVERTED: YES`); step (8) prints nothing for that path. The artifact is headed `## Labelled sensitivity check (not a fail-before of the old tests)` and states that the temporary edit was never staged or committed. A `Passed` result in step (4) is `REWRITTEN TEST NOT SENSITIVE`: revert first (steps 5 to 8), then stop and report. If step (7) exits non-zero after the revert, correct the file to BASE content with the Edit tool and repeat steps (6) to (8) before anything else; no later task starts with that diff non-empty. +- [x] [P5-T9] Rebuild the reverted production assembly with `CMD-BUILD` (`TASKID` p5-t9) against WORKTREE/TaskMaster.sln and record it in FEATURE/evidence/regression-testing/liveness-revert-build.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`, `EXIT_CODE: 0`, `ERRORS: 0`, `PROD_DLL_ADVANCED: True` (the sensitivity binary is replaced), `TEST_DLL_ADVANCED: True`, `CSC_OUT_QUICKFILER:` at least 1. +- [x] [P5-T10] Re-run the two rewritten tests on the reverted tree from QuickFiler.Test/bin/Debug/QuickFiler.Test.dll with `CMD-VSTEST` (`FILTER-LIVENESS-PAIR`, `TASKID` p5-t10, `NAMES-LIVENESS`) and record FEATURE/evidence/regression-testing/liveness-pass-after-revert.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `EXIT_CODE: 0`; `TRX_PRESENT: True`; `SEQUENCE_FILES: 0`; `RESULT_COUNT: 2`; both `RESULT` lines `Passed` with durations recorded (the confirming run that the P5-T7 outcome is reproduced after the sensitivity cycle; the P5-T7 run is the measured one). +- [x] [P5-T11] Apply Delivered Source Q1 and Q2 to QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs (the `_remainingLoadActive` doc replacement and the removal of ` (:31-66)` from the `TryUnhookOrReplace` citation). The `QuiesceLoaderAsync` comment at line 52 is left unchanged (D-20). + - Acceptance (recorded by P5-T12): QQP tokens 0, 0, 1, 0, 3, 1, 1, 0, 1, 0, 1 in the P0-T13 order; LINES 413. +- [x] [P5-T12] Record the comment-edit census in FEATURE/evidence/qa-gates/queue-processing-comment-census.md with `CMD-LINECOUNT` on `"QQP"`, `CMD-TOKEN-COUNT` on QQP (the P0-T13 list), `CMD-HUNKS` on `QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs`, `git -C WORKTREE diff 94287369908cc920b21b0e3256314f988ad7d2f5 -- QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs` paired with `git -C WORKTREE status --porcelain -- QuickFiler`. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; the P5-T11 conditions hold; `HUNK_COUNT: 2` with one hunk whose old range starts at or above 10 and ends at or below 30 and one whose old range starts at or above 280 and ends at or below 292; every changed line in the transcribed diff begins with `///` after its indentation (comment-only: the AC26 and AC20 reading); the porcelain span lists exactly ` M QuickFiler/Controllers/QfcDatamodel.cs` and ` M QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs`; and the artifact records `QUIESCE-COMMENT-DECISION: UNCHANGED` with the D-20 reason (the `_remainingLoadTask` write at `QfcDatamodel.cs` 218 happens on the thread that runs `Worker_DoWork`, which in production is still the `BackgroundWorker` thread, and the comment's snapshot rationale holds for any cross-thread writer). + +### Phase 6 — Scoped Format, Pass-After Runs and Implementation Commit + +- [x] [P6-T1] Format the thirteen Write Set `.cs` files (CS13) with a scoped CSharpier pass and record the before-and-after hashes in FEATURE/evidence/qa-gates/scoped-format.md. + - Command: `CMD-HASH` on CS13; then `pwsh -NoProfile -Command 'PREFIX; dotnet tool run csharpier format QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixture.cs QuickFiler.Test\Controllers\QfcItemController.FocusAndThemeTests.cs QuickFiler.Test\Controllers\QfcItemController.TestSupport.cs QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs QuickFiler.Test\Controllers\QfcItemController.UiThreadDispatcherPinCountTests.cs QuickFiler.Test\Controllers\QfcDatamodelLivenessTests.cs QuickFiler.Test\Controllers\QfcDatamodelTeardownTests.cs QuickFiler.Test\Controllers\QfcInitEmailQueueZeroBatchTests.cs QuickFiler.Test\Controllers\QfcDatamodelTests.cs QuickFiler\Controllers\QfcDatamodel.cs QuickFiler\Controllers\QfcDatamodel.QueueProcessing.cs QuickFiler.Test\TestSupport\SynchronousBackgroundWorker.cs QuickFiler.Test\TestSupport\ArmingFakeTimeProvider.cs; "CSHARPIER_EXIT_CODE: $LASTEXITCODE"'`; then `CMD-HASH` on CS13 again. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `CSHARPIER_EXIT_CODE: 0`; the line beginning `Formatted ` is transcribed and labelled as a processed-file count, not a rewrite count; `REWRITTEN:` lists every CS13 path whose hash differs between the two captures, or `NONE` (the rewrite observation is the hash difference, not the console line). Either value completes this task: the pass exists so the committed text is formatter-stable. On a D-13 restart the restarted artifact also records `PRIOR-PASS-REWRITTEN:` (the union of the `REWRITTEN:` paths of every earlier P6-T1 pass in this run, or `NONE`) and `RESTART-CORRECTED:` (the union of the Write Set paths edited by every correction that triggered a D-13 restart in this run). +- [x] [P6-T2] Record the post-format census in FEATURE/evidence/qa-gates/post-format-census.md by re-running every P2-T6, P3-T9, P1-T3, P4-T9, P5-T5 and P5-T12 command (CMD-LINECOUNT on CS13, every CMD-TOKEN-COUNT list including PC, SBW, AFTP and PROJ, the ENSURE, SCOPE, R4SPAN, R4HEAD, R4TAIL, T1-LIVE, HELD, T-SIB and GATE-LAMBDA spans, CMD-HUNKS on TestSupport, the fixture tests, QfcDatamodel.cs and QfcDatamodel.QueueProcessing.cs, numstat paired with the porcelain span), plus `CMD-PIN-NESTING` on `T3SPAN`. + - Acceptance: every token, span, hunk and numstat value holds after formatting as last recorded for its file (P1-T3 for PC; P2-T6 for FIX; P3-T9 for FAT, TS and FT; P4-T9 for TD, ZB, QDM, SBW and the QfcDatamodel.cs numstat row `1 129`; P5-T5 for LIV, DMT, AFTP and PROJ, superseding the interim P4-T6, P4-T7 and P4-T9 values for those files; P5-T12 for QQP), the project-file numstat row reads `3 0` in place of the P1-T3 value, and the porcelain span below replaces every earlier porcelain expectation (a LINES value, a printed `SPAN:` range and the recorded-not-gated QfcDatamodel.cs `HUNK_COUNT:` may differ from their pre-format values only if `REWRITTEN:`, `PRIOR-PASS-REWRITTEN:` or `RESTART-CORRECTED:` named the file; every CS13 LINES value is at most 500 and the QfcDatamodel.cs value is at most 400); `GATE-LAMBDA` 1, 0 (the sensitivity edit is not present); `T3SPAN` NEST output ends with four lines whose tokens are, in order, `finally`, `Dispose()` (the `transaction.Dispose();` line), `finally`, `ShutdownDispatcher(` (the AC3 reading that the live dispatcher is shut down in a finally block); the FIELDLOCK-ENCLOSURE reading of P2-T6 is restated against the formatted diff; the porcelain span lists exactly the fourteen Write Set code paths (eleven ` M`, three `??`); on a D-13 restart that follows the P6-T9 commit, the ref operand of every HEAD-anchored git command in this task and the porcelain expectation are the ones D-13 states. This artifact is the evidence for AC6, AC7, AC9, AC11, AC12, AC13, AC15, AC16, AC17, AC25, AC26, AC30 and the census half of AC3, AC10, AC14, AC21, AC27, AC29 and AC31. +- [x] [P6-T3] Build the formatted tree with `CMD-BUILD` (`TASKID` p6-t3) against WORKTREE/TaskMaster.sln and record it in FEATURE/evidence/regression-testing/implementation-build.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`, `EXIT_CODE: 0`, `ERRORS: 0`, `TEST_DLL_ADVANCED: True`, `CSC_OUT_QUICKFILER_TEST:` at least 1, and `PROD_DLL_ADVANCED: True` with `CSC_OUT_QUICKFILER:` at least 1 when `REWRITTEN:` named a production file (otherwise both recorded). +- [x] [P6-T4] Run the pin-count class alone from QuickFiler.Test/bin/Debug/QuickFiler.Test.dll with `CMD-VSTEST` (`FILTER-PC-CLASS`, `TASKID` p6-t4, `NAMES-PC`) and record FEATURE/evidence/regression-testing/pin-count-class-pass-after.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `EXIT_CODE: 0`; `TRX_PRESENT: True`; `SEQUENCE_FILES: 0`; `RESULT_COUNT: 4`; four `RESULT` lines, each `Passed`, durations recorded. Any other outcome invokes the D-13 Phase 6 restart rule. +- [x] [P6-T5] Run the fixture test class alone from QuickFiler.Test/bin/Debug/QuickFiler.Test.dll with `CMD-VSTEST` (`FILTER-FT-CLASS`, `TASKID` p6-t5, `NAMES-FT`) and record FEATURE/evidence/regression-testing/fixture-class-pass-after.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `EXIT_CODE: 0`; `TRX_PRESENT: True`; `SEQUENCE_FILES: 0`; `RESULT_COUNT: 8`; eight `RESULT` lines, one per NAMES-FT name, each `Passed`, durations recorded (R1 to R6 and the #743 and #882 tests pass with their assertions unchanged, AC10; R4 passes without its pin and with the `try/finally`, AC14). Any other outcome invokes the D-13 Phase 6 restart rule. +- [x] [P6-T6] Run the focus-and-theme class alone from QuickFiler.Test/bin/Debug/QuickFiler.Test.dll with `CMD-VSTEST` (`FILTER-FAT-CLASS`, `TASKID` p6-t6, `NAMES-THEME`) and record FEATURE/evidence/regression-testing/focus-and-theme-class-pass-after.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `EXIT_CODE: 0`; `TRX_PRESENT: True`; `SEQUENCE_FILES: 0`; `RESULT_COUNT: 17`; the two NAMES-THEME `RESULT` lines each `Passed` with durations recorded; `COUNTERS` shows `passed=17 failed=0` (every test in the class passes after the helper switch, AC15, and the theme tests pass without the deleted calls, AC7). Any other outcome invokes the D-13 Phase 6 restart rule. +- [x] [P6-T7] Run the three #968 classes together in one invocation from QuickFiler.Test/bin/Debug/QuickFiler.Test.dll under the CLI runsettings with `CMD-VSTEST` (`FILTER-CONCURRENT`, `TASKID` p6-t7, empty `NAMES`) and record FEATURE/evidence/regression-testing/concurrent-set-test-summary.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `EXIT_CODE: 0`; `TRX_PRESENT: True`; `SEQUENCE_FILES: 0`; `RESULT_COUNT: 29`; the `COUNTERS` line shows `total=29 executed=29 passed=29 failed=0`; every `RESULT` line transcribed and `Passed`; `CONCURRENT-NOT-PASSED: NONE`. The artifact states that this is a supporting observation under Workers 0 and ClassLevel scope (MSTest cannot be made to interleave classes on demand), not the regression gate. Any non-Passed outcome is compared with P0-T14 `BASELINE-CONCURRENT-FAILED:`: a name present there is recorded as pre-existing and completes the task with `ExpectedExitCode:` equal to the observed value; a new failure invokes the D-13 Phase 6 restart rule. +- [x] [P6-T8] Run the four datamodel classes together in one invocation from QuickFiler.Test/bin/Debug/QuickFiler.Test.dll under the CLI runsettings with `CMD-VSTEST` (`FILTER-DATAMODEL`, `TASKID` p6-t8, empty `NAMES`) and record FEATURE/evidence/regression-testing/datamodel-set-test-summary.md. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `EXIT_CODE: 0`; `TRX_PRESENT: True`; `SEQUENCE_FILES: 0`; `RESULT_COUNT: 21`; the `COUNTERS` line shows `total=21 executed=21 passed=21 failed=0`; every `RESULT` line transcribed and `Passed`, including both NAMES-LIVENESS names; `DATAMODEL-NOT-PASSED: NONE` (AC32; the pass-after half of AC25, AC30 and AC31). Any non-Passed outcome is compared with P0-T15 `BASELINE-DATAMODEL-FAILED:`: a name present there is recorded as pre-existing and completes the task with `ExpectedExitCode:` equal to the observed value; a new failure invokes the D-13 Phase 6 restart rule. +- [x] [P6-T9] Commit the implementation (the fourteen Write Set code files and FEATURE) and record FEATURE/evidence/qa-gates/implementation-commit.md. + - Commands, separate Bash calls: `git -C WORKTREE add -- CODE14-GIT FEATURE-GIT` (both tokens expanded; one `git add` with fifteen pathspecs); `git -C WORKTREE commit -m "fix(968): reference-count the UiThreadDispatcherFixture ensure pin, remove the dead theme-test calls and fold the #972 datamodel residuals" -- CODE14-GIT FEATURE-GIT` (the same fifteen pathspecs); `git -C WORKTREE rev-parse HEAD`; `git -C WORKTREE diff --name-status 94287369908cc920b21b0e3256314f988ad7d2f5 HEAD`; `git -C WORKTREE status --porcelain --untracked-files=all`. + - Acceptance: both git writes exit 0; `IMPLEMENTATION-COMMIT:` records the new HEAD as an observation; the name-status diff lists the eleven modified Write Set code paths with status `M`, the three new files (`QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs`, `QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs`, `QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs`) with status `A`, FEATURE paths, and otherwise only paths from P0-T3 `INHERITED-COMMITTED:`; no porcelain line names a path under QuickFiler/ or QuickFiler.Test/. This artifact is committed in P8-T46. + +### Phase 7 — Call-Site Census and Prohibited-Construct Gate + +- [x] [P7-T1] Record the post-change call-site census (two independent strategies and the member-set comparison) in FEATURE/evidence/qa-gates/call-site-census.md with `CMD-CENSUS`. + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `CS_FILES:` recorded; `PRIMARY_LINES: 16` with PRIMARY-FILE fixture 1, test support 2, fixture tests 3, pin-count tests 10 and no focus-and-theme entry; `CROSS_LINES: 32` with CROSS-FILE fixture 5, test support 3, InitializationTests.Part2 1, fixture tests 8, pin-count tests 15 and no focus-and-theme entry; `CONTROL_LINES: 28`; every `PRIMARY` and `CROSS` line transcribed. The artifact then classifies every PRIMARY line as one of `DECLARATION` (fixture `internal static IDisposable EnsureDispatcher()`, test support `internal static IDisposable EnsureUiThreadDispatcher() =>`), `FORWARDER` (test support `UiThreadDispatcherFixture.EnsureDispatcher();`) or `INVOCATION` (the thirteen test-side lines), and every CROSS line not in the PRIMARY set as `DOC`, `COMMENT` or `TEST-NAME` by reading its transcribed text, and records `MEMBER-SET-COMPARISON: AGREE` when the CROSS set contains every PRIMARY line and every CROSS-only line is non-executable (sixteen such lines: fixture 4, test support 1, Part2 1, fixture tests 5, pin-count tests 5). Any CROSS-only line that is an invocation (for example a call written across two lines) is `CENSUS MISMATCH`: stop and report. A `PRIMARY_LINES` or `CROSS_LINES` value other than stated is likewise `CENSUS MISMATCH`. +- [x] [P7-T2] Append the nesting classification to FEATURE/evidence/qa-gates/call-site-census.md with `CMD-PIN-NESTING` on `R1SPAN`, `R2SPAN`, `R3SPAN`, `R4SPAN`, `T1SPAN`, `T2SPAN`, `T3SPAN` and `T4SPAN`. + - Acceptance: for each of R1SPAN, R2SPAN, R3SPAN, T1SPAN, T2SPAN, T3SPAN and T4SPAN the NEST output shows, for every transaction variable in the span (one per span; two in T4SPAN, transaction then foreignTransaction), by ascending line number: that transaction's BeginTransactionAsync() line, then its single .Install( line, then the pins taken under it, where each pin's EnsureUiThreadDispatcher() line precedes that pin's first Dispose() line (ensureScope, pinA, pinB, freshPin, foreignPin), and every such pin Dispose() line precedes that transaction's first Dispose() line; in T4SPAN the foreignTransaction BeginTransactionAsync() line follows the transaction.Dispose(); line; R4SPAN shows no EnsureUiThreadDispatcher() line and two transactionA.Dispose(); lines, the second inside a finally; the artifact records per method NESTED: YES and INSTALL-BETWEEN-PIN-ACQUIRE-AND-RELEASE: NONE, and records INVOCATIONS-CLASSIFIED: 13 of 13 nested (three in the fixture tests, ten in the pin-count tests). Any other ordering is NESTING VIOLATION: stop and report. +- [x] [P7-T3] Record the prohibited-construct gate in FEATURE/evidence/qa-gates/prohibited-constructs-grep.md with `CMD-ADDED-SCAN`, `git -C WORKTREE diff --exit-code 94287369908cc920b21b0e3256314f988ad7d2f5 HEAD -- scripts/vscode/TaskMaster.cli.runsettings TaskMaster.runsettings`, `git -C WORKTREE status --porcelain -- scripts/vscode/TaskMaster.cli.runsettings TaskMaster.runsettings`, and `CMD-TOKEN-COUNT` on FT (TOKENS `"[Timeout(GateTimeoutMs)]", "private const int GateTimeoutMs = 60000;"`). + - Acceptance: `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `GIT_DIFF_EXIT_CODE: 0`; `ADDED_LINES:` greater than 0; `ADDED-TOKEN` counts 0 for `Thread.Sleep`, `Task.Delay`, `DoNotParallelize`, `Retry(`, `Path.GetTempFileName`, `Path.GetTempPath`, `Workers`, `await Task.Yield();`, `for (int i` and `using var `; `Timeout(` 4 and `[Timeout(GateTimeoutMs)]` 4 (every added timeout attribute is the sibling file's constant convention in the pin-count class; the datamodel tests gain none); `GateTimeoutMs = ` 1 with the single `ADDED-LINE` reading `private const int GateTimeoutMs = 60000;` (no timeout increase: the value equals the sibling constant); `_pinCount` and `ArmingFakeTimeProvider` each at least 1 (positive controls); the runsettings diff exits 0 and the porcelain span prints nothing; the FT counts read 8 and 1, equal to P0-T12. Any non-zero prohibited count is `PROHIBITED CONSTRUCT ADDED`: stop and report. + +### Phase 8 — Final QA Loop, Coverage Comparison, Static Gates, Check-offs and Final Commit + +- [x] [P8-T1] Run the CLAUDE.md formatting step `dotnet tool run csharpier format .` at the worktree root (WORKTREE/.) with a tree observation before and after, and record FEATURE/evidence/qa-gates/csharpier-format-final.md. + - Commands: `git -C WORKTREE status --porcelain --untracked-files=all`; `CMD-HASH` on CS13; `pwsh -NoProfile -Command 'PREFIX; dotnet tool run csharpier format .; "CSHARPIER_EXIT_CODE: $LASTEXITCODE"'`; `CMD-HASH` on CS13; `git -C WORKTREE status --porcelain --untracked-files=all`. + - Acceptance: `ITERATION:` recorded; `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `CSHARPIER_EXIT_CODE: 0`; the `Formatted ` line transcribed and labelled as a processed-file count; `REWRITTEN-WRITESET:` lists every CS13 path whose hash changed, or `NONE`; `REWRITTEN-OTHER:` lists every porcelain path present after and absent before, or `NONE`. Clean pass: both `NONE`. A non-empty `REWRITTEN-WRITESET:` with `REWRITTEN-OTHER: NONE` invokes the D-13 format restart; a non-empty `REWRITTEN-OTHER:` is `FORMAT TOUCHED OUT-OF-SCOPE FILE`: stop. +- [x] [P8-T2] Run the read-only formatter gate `dotnet tool run csharpier check .` at the worktree root (WORKTREE/.) and record FEATURE/evidence/qa-gates/csharpier-check-final.md. + - Command: `pwsh -NoProfile -Command 'PREFIX; dotnet tool run csharpier check .; "CSHARPIER_EXIT_CODE: $LASTEXITCODE"'`. + - Acceptance: `ITERATION:` recorded; `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `EXIT_CODE: 0`; the success-case line beginning `Checked ` and ending `ms.` transcribed verbatim. +- [x] [P8-T3] Run the analyzer rebuild with `CMD-REBUILD` (analyzer GATEARGS, `TASKID` p8-t3) against WORKTREE/TaskMaster.sln and record FEATURE/evidence/qa-gates/msbuild-analyzer-final.md. + - Acceptance: `ITERATION:` recorded; `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `EXIT_CODE: 0`; `ERRORS: 0`; `SKIP_CORECOMPILE_LINES: 0`; both `CSC_OUT_` counts at least 1; `WRITESET_DIAGNOSTIC_LINES:` not greater than `ANALYZER-BASELINE-WRITESET-LINES:` and every code in `WRITESET_DIAGNOSTIC_CODES:` present in `ANALYZER-BASELINE-WRITESET-CODES:` (no new analyzer diagnostic in a Write Set file); `WARNINGS:` recorded beside `ANALYZER-BASELINE-WARNINGS:`. This rebuild is the second compile proof for AC27. +- [x] [P8-T4] Run the type-check rebuild with `CMD-REBUILD` (GATEARGS `/p:TreatWarningsAsErrors=true`, no Nullable override, `TASKID` p8-t4) against WORKTREE/TaskMaster.sln and record FEATURE/evidence/qa-gates/msbuild-nullable-final.md. + - Acceptance: `ITERATION:` recorded; `WORKTREE-LEAF: agent-a291a7fbabf9d0229`; `EXIT_CODE: 0`; `ERRORS: 0`; `SKIP_CORECOMPILE_LINES: 0`; both `CSC_OUT_` counts at least 1; both `_DLL_EXISTS:` values `True`. +- [x] [P8-T5] Run the final repository-wide test-and-coverage run by the P0-T16 route (`CMD-COVERAGE-RUNNER` or `CMD-COVERAGE-DIRECT` with `STAGE` final, then `CMD-COVERAGE-POST` with `STAGE` final) and record FEATURE/evidence/qa-gates/coverage-summary.md and FEATURE/evidence/qa-gates/coverage-jacoco-projection.md. + - Artifacts: the same fields as P0-T17, with `ITERATION:`, `FAILED-SET:` recorded as `FINAL-FAILED-SET:`, every `MESSAGE` line, and sixteen `RESULT` lines (NAMES-TARGETS). + - Acceptance, all required: the route equals P0-T16's; `TRX_PRESENT: True`; `SEQUENCE_FILES:` 0 under DIRECT; `TEST_ASSEMBLY_PACKAGES: 0`; `QFCDATAMODEL_CLASS_ENTRIES:` recorded beside the P0-T17 value; the sixteen `RESULT` lines all `Passed` (the four pin-count tests appear as passed in the coverage route's test-result summary, AC21; the two rewritten liveness tests pass in the full parallel run); `NEW-FAILURES:` (names in `FINAL-FAILED-SET:` absent from `BASELINE-FAILED-SET:`) is `NONE`. Any `NEW-FAILURES:` name whose `MESSAGE` contains `The UI dispatcher has not been captured` is recorded as `LEAK-DEPENDENT TEST EXPOSED:` followed by the name, and stops the run for re-planning under the related-defect directive; it is neither a D-13 restart nor `NEW FAILURE OUTSIDE SCOPE`. `FIGURES-COMPARED:` restates the `Total`, `executed`, `error`, `timeout`, `aborted` and `notExecuted` figures from the P0-T17 summary block and from this run's summary block, and this run holds `Total` equal to the P0-T17 value plus 4 (this plan adds four test methods and removes none; the fold rewrites two and adds none), `executed` not less than the P0-T17 value plus 4, and each of `error`, `timeout`, `aborted` and `notExecuted` not greater than its P0-T17 value; `LINE-FLOOR:` and `BRANCH-FLOOR:` each `MET`, or `NOT MET` only where P0-T17 recorded the same floor as not met; the `First-party coverage:` line is recorded as the numeric post-change headline. `RUNNER-GREEN: YES` is recorded when the route is RUNNER and `RUNNER_EXIT_CODE: 0`, otherwise `RUNNER-GREEN: NO` with the reason (`COVERAGE-ROUTE DIRECT` or `PRE-EXISTING FAILURES`). A failure of any target test or a new failure attributable to the Write Set invokes the D-13 failure restart; any other new failure, and any `FIGURES-COMPARED:` breach, is `NEW FAILURE OUTSIDE SCOPE`: stop and report, without re-running. +- [x] [P8-T6] Compare baseline and post-change coverage and test outcomes and record FEATURE/evidence/qa-gates/coverage-comparison.md from FEATURE/evidence/baseline/coverage-summary.md and FEATURE/evidence/qa-gates/coverage-summary.md. + - Acceptance: the artifact records the baseline and post-change `First-party coverage:` lines (lines and branches, numeric) and the two `ROOT` lines; `FIRST-PARTY-LINE-DELTA:` and `FIRST-PARTY-BRANCH-DELTA:` as post-change percentage minus baseline percentage (two decimals, signed); `AC23-STATUS: MET` when both deltas are at least 0.00, otherwise `AC23-STATUS: NOT MET (COVERAGE-VARIANCE)` with both deltas; the repository-wide comparison in exactly one named branch: `BRANCH A` when the two `lines-valid` figures differ by at most 1 percent of the baseline figure (the post-change line rate must not be lower than baseline by more than 0.5 percentage points), otherwise `BRANCH B` (recorded and not gated, with one sentence stating the denominators are not comparable); `CHANGED-CODE-COVERAGE: NOT MEASURED (TEST ASSEMBLY EXCLUDED; QFCDATAMODEL EXCLUDED BY ATTRIBUTE)` with `TEST_ASSEMBLY_PACKAGES: 0` at both stages and the two `QFCDATAMODEL_CLASS_ENTRIES:` values as its reason (the AC29 reading: the removed lines were in no measured denominator); and `BASELINE-FAILED-SET:`, `FINAL-FAILED-SET:` and `NEW-FAILURES: NONE` restated. A Branch A breach is `COVERAGE REGRESSION`: stop; an `AC23-STATUS: NOT MET` with Branch A satisfied is recorded and does not stop the run (D-7). +- [x] [P8-T7] Record the final toolchain pass in FEATURE/evidence/qa-gates/toolchain-final.md from the P8-T1 to P8-T5 artifacts of the final iteration. + - Acceptance: one row per step, in order — csharpier format (`REWRITTEN-WRITESET: NONE`, `REWRITTEN-OTHER: NONE`), csharpier check (exit 0), analyzer rebuild (exit 0, `SKIP_CORECOMPILE_LINES: 0`), TreatWarningsAsErrors rebuild (exit 0, `SKIP_CORECOMPILE_LINES: 0`), coverage run (route, exit code, `RUNNER-GREEN:`) — each with its exact command, `EXIT_CODE:` and the same `ITERATION:` value; `SINGLE-PASS: YES` when all five rows come from one iteration with no restart after P8-T1; `AC22-STATUS: MET` only when `SINGLE-PASS: YES` and `RUNNER-GREEN: YES`, otherwise `AC22-STATUS: NOT MET` with the reason. +- [x] [P8-T8] Record the file-size gate in FEATURE/evidence/qa-gates/file-line-counts.md with `CMD-LINECOUNT` on CS13 and `CMD-TOKEN-COUNT` on PROJ (TOKENS `"`, FEATURE/evidence/regression-testing/pass-after-pin-count.md records the same test `Passed`, and the `PHASE2-PORCELAIN:` span differs from `PHASE1-PORCELAIN:` (FEATURE/evidence/regression-testing/pin-count-file-census.md) by exactly the fixture file. + - Acceptance: only `- [ ] AC5:` changes; otherwise unchecked with `AC5: NOT MET`. +- [x] [P8-T16] Check off AC6 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows the PC tokens `Regression test: fails before the fix` 1, `Specification test: passes before and after the fix` 3 and `never read the shared static` 1, and FEATURE/evidence/regression-testing/specification-tests-before-fix.md records the three specification tests `Passed` before the fix. + - Acceptance: only `- [ ] AC6:` changes; otherwise unchecked with `AC6: NOT MET`. +- [x] [P8-T17] Check off AC7 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows the FAT token `EnsureUiThreadDispatcher` 0 and FEATURE/evidence/regression-testing/focus-and-theme-class-pass-after.md records `SetThemeDark_FromNormal_SelectsDarkNormalTheme` and `SetThemeLight_FromNormal_SelectsLightNormalTheme` both `Passed`. + - Acceptance: only `- [ ] AC7:` changes; otherwise unchecked with `AC7: NOT MET`. +- [x] [P8-T18] Check off AC8 in FEATURE/spec.md when FEATURE/evidence/qa-gates/call-site-census.md records `MEMBER-SET-COMPARISON: AGREE`, `INVOCATIONS-CLASSIFIED: 13 of 13 nested` and `INSTALL-BETWEEN-PIN-ACQUIRE-AND-RELEASE: NONE` for every pin-bearing method. + - Acceptance: only `- [ ] AC8:` changes; otherwise unchecked with `AC8: NOT MET`. +- [x] [P8-T19] Check off AC9 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows FIX tokens `_pinCount` 4, `_fixtureInstalledParked` 4 and `lock (FieldLock)` 5, SCOPE `CompareExchange(` 0 and `lock (FieldLock)` 1, and its `FIELDLOCK-ENCLOSURE:` reading states both fields are private statics whose every use lies inside a `lock (FieldLock)` block. + - Acceptance: only `- [ ] AC9:` changes; otherwise unchecked with `AC9: NOT MET`. +- [x] [P8-T20] Check off AC10 in FEATURE/spec.md when FEATURE/evidence/regression-testing/fixture-class-pass-after.md records all eight NAMES-FT tests `Passed` and FEATURE/evidence/qa-gates/post-format-census.md shows every fixture-tests hunk inside the R4 doc-and-body range and the R4SPAN tokens `.BeSameAs(` 1, `.NotBeSameAs(` 1, `issue #230 lost update` 1, with the FT token `the waiter cannot observe the pre-restore value` 1. + - Acceptance: only `- [ ] AC10:` changes; otherwise unchecked with `AC10: NOT MET`. +- [x] [P8-T21] Check off AC11 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows FIX tokens `leaks exactly` 0, `installed nothing carries` 0, `A scope that installed nothing` 0, `pins for the process lifetime` 2 and `install-ownership flag` 1. + - Acceptance: only `- [ ] AC11:` changes; otherwise unchecked with `AC11: NOT MET`. +- [x] [P8-T22] Check off AC12 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows TS tokens `Becomes moot` 0, `leaks exactly` 0, `still delegate to a callee` 0, `remaining legitimate` 1 and `QfcItemController_UiThreadDispatcherPinCountTests` 1. + - Acceptance: only `- [ ] AC12:` changes; otherwise unchecked with `AC12: NOT MET`. +- [x] [P8-T23] Check off AC13 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows FT tokens `no other class may dispose` 0, `removed that pin: the fixture now counts pins` 1 and `(W5) must not latch` 1. + - Acceptance: only `- [ ] AC13:` changes; otherwise unchecked with `AC13: NOT MET`. +- [x] [P8-T24] Check off AC14 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows R4SPAN `transactionA.Dispose();` 2 and R4TAIL `finally` 2 with `transactionA.Dispose();` 1, and FEATURE/evidence/regression-testing/fixture-class-pass-after.md records `Transaction_SecondCallerCannotInstallUntilTheFirstRestores` and `Transaction_DisposedTwice_DoesNotOverReleaseTheGate` both `Passed` (this check-off also records `CLOSES-972-ITEM-5: YES`). + - Acceptance: only `- [ ] AC14:` changes; otherwise unchecked with `AC14: NOT MET`. +- [x] [P8-T25] Check off AC15 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows FAT tokens `private static Mock BuildExecutingViewer` 0, `QfcItemControllerTestSupport.BuildExecutingViewer()` 8 and `BuildExecutingViewer` 8, the TS token `not reachable from another test file` 0, and FEATURE/evidence/regression-testing/focus-and-theme-class-pass-after.md records `passed=17 failed=0`. + - Acceptance: only `- [ ] AC15:` changes; otherwise unchecked with `AC15: NOT MET`. +- [x] [P8-T26] Check off AC16 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows FAT tokens `absorbs the delegate without running it` 1, `shared UiThread static is irrelevant` 1 and `absorbs the queued application` 1. + - Acceptance: only `- [ ] AC16:` changes; otherwise unchecked with `AC16: NOT MET`. +- [x] [P8-T27] Check off AC17 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows the TestSupport `HUNK_COUNT: 2` with every hunk old-range start at or above 200 and the TS token `internal static void EnsureSynchronizationContext()` 1. + - Acceptance: only `- [ ] AC17:` changes; otherwise unchecked with `AC17: NOT MET`. +- [x] [P8-T28] Check off AC18 in FEATURE/spec.md when FEATURE/evidence/qa-gates/file-line-counts.md shows every CS13 `LINES` value at most 500. + - Acceptance: only `- [ ] AC18:` changes; otherwise unchecked with `AC18: NOT MET`. +- [x] [P8-T29] Check off AC19 in FEATURE/spec.md when FEATURE/evidence/qa-gates/prohibited-constructs-grep.md holds every P7-T3 condition. + - Acceptance: only `- [ ] AC19:` changes; otherwise unchecked with `AC19: NOT MET`. +- [x] [P8-T30] Check off AC20 in FEATURE/spec.md when FEATURE/evidence/qa-gates/footprint-scope.md shows `THIS-ITEM-FOOTPRINT:` equal to the fourteen listed paths, with exactly `QuickFiler/Controllers/QfcDatamodel.cs` and `QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs` under `QuickFiler/` and every other path under `QuickFiler.Test/`, and every other name-status path under FEATURE or in `INHERITED-AND-EXCLUDED:`. + - Acceptance: only `- [ ] AC20:` changes; otherwise unchecked with `AC20: NOT MET`. +- [x] [P8-T31] Check off AC21 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows the PROJ token `Controllers\QfcItemController.UiThreadDispatcherPinCountTests.cs` 1 and FEATURE/evidence/qa-gates/coverage-summary.md records all four NAMES-PC tests `Passed`. + - Acceptance: only `- [ ] AC21:` changes; otherwise unchecked with `AC21: NOT MET`. +- [x] [P8-T32] Check off AC22 in FEATURE/spec.md when FEATURE/evidence/qa-gates/toolchain-final.md reads `AC22-STATUS: MET`. + - Acceptance: only `- [ ] AC22:` changes; otherwise the box stays unchecked and the recorded reason (`COVERAGE-ROUTE DIRECT`, `PRE-EXISTING FAILURES` or a restart after P8-T1) is carried to P8-T43 as `AC22: NOT MET`. +- [x] [P8-T33] Check off AC23 in FEATURE/spec.md when FEATURE/evidence/qa-gates/coverage-comparison.md reads `AC23-STATUS: MET`. + - Acceptance: only `- [ ] AC23:` changes; otherwise the box stays unchecked and `AC23: NOT MET (COVERAGE-VARIANCE)` with both deltas is carried to P8-T43. +- [x] [P8-T34] Check off AC24 in FEATURE/spec.md when FEATURE/evidence/regression-testing/concurrent-set-test-summary.md records `RESULT_COUNT: 29` and `CONCURRENT-NOT-PASSED: NONE`. + - Acceptance: only `- [ ] AC24:` changes; otherwise unchecked with `AC24: NOT MET`. +- [x] [P8-T35] Check off AC25 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows SBW tokens `class SynchronousBackgroundWorker` 1, `internal sealed class SynchronousBackgroundWorker : BackgroundWorker` 1 and `internal static void StartSynchronously(BackgroundWorker worker)` 1, LIV, TD and ZB tokens `class SynchronousBackgroundWorker` 0 and `Duplicated per file` 0 with ZB `through the nested` 0, every `StartSynchronously` line in LIV, TD and ZB equal to its `SynchronousBackgroundWorker.StartSynchronously` count (2 and 2, 1 and 1, 3 and 3), the PROJ token `TestSupport\SynchronousBackgroundWorker.cs` 1, and FEATURE/evidence/regression-testing/datamodel-set-test-summary.md records `passed=21 failed=0` (a repository-wide `class SynchronousBackgroundWorker` count of exactly 1 follows from the three consumer counts of 0 plus the SBW count of 1, because P0-T13 and P6-T2 enumerate every file that carried the identifier). + - Acceptance: only `- [ ] AC25:` changes; otherwise unchecked with `AC25: NOT MET`. +- [x] [P8-T36] Check off AC26 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows QQP tokens `RunWorkerAsync` 0, `written on the worker thread and read` 0, `(:31-66)` 0, `WorkerStarter` 1 and `share no other fence` 1, and FEATURE/evidence/qa-gates/queue-processing-comment-census.md records `QUIESCE-COMMENT-DECISION: UNCHANGED` with its reason and `HUNK_COUNT: 2` with comment-only changed lines. + - Acceptance: only `- [ ] AC26:` changes; otherwise unchecked with `AC26: NOT MET`. +- [x] [P8-T37] Check off AC27 in FEATURE/spec.md when FEATURE/evidence/qa-gates/qfc-datamodel-legacy-callers.md records `INVOCATIONS: 0`, `Primary Count: 4`, `Cross-check Count: 4` and an identical member-set comparison, FEATURE/evidence/qa-gates/post-format-census.md shows QDM tokens `Worker_RunWorkerCompleted` 0, `nameof(LoadRemainingEmailsToQueue)` 0, `nameof(LoadRemainingEmailsToQueueAsync)} Error.` 1, `LoadRemainingEmailsToQueue(BackgroundWorker bw` 0, `log4net.ILog log =` 0, `Linked List Locking` 0, `//e.Result =` 0, `//worker.RunWorkerCompleted` 0, `ForEachAwaitWithCancellationAsync` 0 and `: IQfcDatamodel` 1, and FEATURE/evidence/qa-gates/msbuild-analyzer-final.md and FEATURE/evidence/qa-gates/msbuild-nullable-final.md both record `EXIT_CODE: 0` (every `IQfcDatamodel` member is still implemented, because the type still declares `: IQfcDatamodel` and the solution compiles). + - Acceptance: only `- [ ] AC27:` changes; otherwise unchecked with `AC27: NOT MET`. +- [x] [P8-T38] Check off AC28 in FEATURE/spec.md when FEATURE/evidence/qa-gates/file-line-counts.md shows the `QuickFiler\Controllers\QfcDatamodel.cs` `LINES` value at most 400 beside `QFCDATAMODEL-LINES-BEFORE: 495`. + - Acceptance: only `- [ ] AC28:` changes; otherwise unchecked with `AC28: NOT MET`. +- [x] [P8-T39] Check off AC29 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows the QDM token `[ExcludeFromCodeCoverage]` 1, FEATURE/evidence/qa-gates/qfc-datamodel-legacy-callers.md records that no test references any of the four members (every test-file hit classified `DOC-PROSE` or `OTHER-TYPE-SAME-NAME`), and FEATURE/evidence/qa-gates/coverage-comparison.md reads `AC23-STATUS: MET`. + - Acceptance: only `- [ ] AC29:` changes; otherwise unchecked with `AC29: NOT MET` (an `AC23-STATUS: NOT MET` carries `COVERAGE-VARIANCE` here too). +- [x] [P8-T40] Check off AC30 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows LIV `new SynchronousBackgroundWorker()` 4 and `using (var worker = new SynchronousBackgroundWorker())` 4, ZB `new SynchronousBackgroundWorker()` 3 and `using (var worker = new SynchronousBackgroundWorker())` 3 with `InitEmailQueue(0, new` 0 and `InitEmailQueue(2, new` 0, DMT `new BackgroundWorker()` 2 and `using (var worker = new BackgroundWorker())` 2, `HELD` `new SynchronousBackgroundWorker()` 0 and `SynchronousBackgroundWorker worker,` 1, and FEATURE/evidence/regression-testing/datamodel-set-test-summary.md records `passed=21 failed=0`. + - Acceptance: only `- [ ] AC30:` changes; otherwise unchecked with `AC30: NOT MET`. +- [x] [P8-T41] Check off AC31 in FEATURE/spec.md when FEATURE/evidence/qa-gates/post-format-census.md shows `T1-LIVE` `Task.Yield` 0, `fake.Advance` 0, `for (int i` 0, `clock.ReArm();` 1 and `(await pending)` 1, `T-SIB` `await Task.Yield();` 0, `clock.ReArm();` 1, `await Task.WhenAny(clock.Armed, pending)` 1 and `IList result = await pending;` 1, AFTP tokens all 1 and the PROJ token `TestSupport\ArmingFakeTimeProvider.cs` 1; exactly one file matches `fail-before-exception.*.md` under FEATURE/evidence/regression-testing/ and carries `WhyFailingRunImpossible:`; FEATURE/evidence/regression-testing/liveness-sensitivity-check.md records both tests `Failed` on their re-arm assertions with `SENSITIVITY-EDIT-REVERTED: YES`; and FEATURE/evidence/regression-testing/liveness-pass-after-revert.md and FEATURE/evidence/regression-testing/datamodel-set-test-summary.md record both NAMES-LIVENESS tests `Passed`. + - Acceptance: only `- [ ] AC31:` changes; otherwise unchecked with `AC31: NOT MET`. +- [x] [P8-T42] Check off AC32 in FEATURE/spec.md when FEATURE/evidence/regression-testing/datamodel-set-test-summary.md records `RESULT_COUNT: 21` and `DATAMODEL-NOT-PASSED: NONE`. + - Acceptance: only `- [ ] AC32:` changes; otherwise unchecked with `AC32: NOT MET`. +- [x] [P8-T43] Write the acceptance-criteria status summary FEATURE/evidence/other/ac-status-summary.md. + - Acceptance: the artifact carries `Timestamp:` and the acceptance-criteria-tracking status block (Source: the spec path; Total AC items: 32; Checked off; Remaining; Items remaining with each `ACn: NOT MET` reason), with the counts read from FEATURE/spec.md after P8-T42 (lines beginning `- [x] AC` and `- [ ] AC`, summing to 32). +- [x] [P8-T44] Re-run the P8-T10 hygiene command over FEATURE/evidence/ after P8-T43 and append the result to FEATURE/evidence/qa-gates/evidence-hygiene.md as a `## Re-run after check-offs` section. + - Acceptance: `RAW_DOCUMENTS=0` and `PROFILE_PATH_LINES=0` for the evidence tree as it will be committed. +- [x] [P8-T45] Verify that FEATURE/spec.md changed only in its checkbox lines by recording `git -C WORKTREE diff --numstat HEAD -- docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md` and `git -C WORKTREE diff HEAD -- docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md`, paired with `git -C WORKTREE status --porcelain -- docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md`, in a `## Spec check-off diff` section appended to FEATURE/evidence/other/ac-status-summary.md. + - Acceptance: added and deleted line counts are equal and equal the checked-off count; every deleted line begins `- [ ] AC` and every added line begins `- [x] AC` with identical remaining text. +- [x] [P8-T46] Commit the remaining feature evidence and check-offs (FEATURE only) and record FEATURE/evidence/qa-gates/final-commit.md. + - Commands, separate Bash calls: `git -C WORKTREE add -- docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968`; `git -C WORKTREE commit -m "docs(968): record final QA evidence and acceptance check-offs" -- docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968`; `git -C WORKTREE rev-parse HEAD`; `git -C WORKTREE diff --name-status 94287369908cc920b21b0e3256314f988ad7d2f5 HEAD`; `git -C WORKTREE status --porcelain --untracked-files=all`. + - Acceptance: both git writes exit 0; `FINAL-COMMIT:` records HEAD as an observation; the name-status paths outside FEATURE are exactly the P8-T9 footprint plus `INHERITED-AND-EXCLUDED:`; the porcelain output names no path under FEATURE other than this plan file (whose final check-off mark follows the commit) and FEATURE/evidence/qa-gates/final-commit.md (written after the commit), and no path under QuickFiler/, QuickFiler.Test/ or scripts/. The final-commit artifact and this plan's check-off marks are committed by the orchestrator with the plan file. No PR is opened and no merge is performed by this plan; the PR body the orchestrator authors carries `Closes #968` and `Closes #972` (spec "Dependencies"). + +## Planner self-review and internal review record + +The round-1 to round-4 forms of the two records below are repeated verbatim in `FEATURE/evidence/other/planner-review.2026-10-02T22-44.md` (the round-4 form in its `## Round-4 delta application (2026-10-03T01-25 deltas)` section; the round-1, round-2 and round-3 records precede that section in the same file). The round-5 form is carried in this plan only: the round-5 revision instruction confined edits to the plan file, and the round-5 re-derivation is returned to the orchestrator in the planner's round-5 message. + +SELF-REVIEW: RE-DERIVED THIS PASS + +Citations re-derived in this pass (file and line, test or identifier): see items 1 to 17 and the sibling-region re-checks in `FEATURE/evidence/other/planner-review.2026-10-02T22-44.md`; in summary — the ten existing Write Set `.cs` files (line totals 342, 470, 497, 440, 312, 244, 232, 371, 495, 413 and CRLF on every line), the project file items 155, 157, 161, 183, 200, 201, 203, 212, 227 to 229, the repository-wide censuses (20/9/23 for the ensure calls; 25, 3 and 2 lines for the legacy-member proof; 13 lines in 3 files for `SynchronousBackgroundWorker`; 20 lines in 7 files for the liveness flag; 0 for `ArmingFakeTimeProvider|NoSynchronizationContext`), the gate's `DequeueAsync` 190 to 301, the two helper precedents, the worktree HEAD metadata, the two promoted records, the spec's 32 acceptance lines and amendment literals, issue.md 12 and 65 to 77, and the round-1 report's ten deltas; and, in the round-2 pass, the eight round-2 deltas — the 43-line F-SCOPE block and the 375 total; the fold span END anchors at LIV 166 and 218, DMT 134 and QQP 311 and LIV 163 `(await pending)`; QDM 431 and 440 `ForEachAwaitWithCancellationAsync`; the `FakeTimeProvider` substring at LIV 114 and DMT 99, 216, 224, 249 and 258, with the `using Microsoft.Extensions.Time.Testing;` directive at line 9 of each file not containing it; the P1 removal arithmetic (128 plus the replaced line 369 as numstat `1 129`) and the plan's own tab-separated numstat convention at P1-T3; the L-T1 and M-T doc lines; D-10 — together with the plan-wide occurrence sweep of every changed numeral and token; and, in the round-3 pass, the four round-3 deltas and advisory A1 — the legacy-member primary pattern over `QuickFiler/Controllers/QfcDatamodel.cs` (17 lines: 40, 52, 130, 194, 209, 210, 246, 335, 363, 369, 378, 404, 410, 418, 462, 469, 472) and over `*.cs` repository-wide (25 lines in 5 files: the 17 plus `QfcHomeController.cs` 4, `QfcHomeControllerRunAsyncTests.cs` 2, `QfcDatamodelLivenessTests.cs` 1, `QfcInitEmailQueueZeroBatchTests.cs` 1); the method-group assignments `RemainingEmailLoader = LoadRemainingEmailsToQueueAsync;` at `QfcDatamodel.cs` 40 and 52; the `QfcDatamodel.cs` lines adjacent to every P1 deletion (98 and 102, 193 and 195, 208 and 211, 362 and 364, and the `logger.Error(` argument at 368 to 370), which show that no deletion leaves a double blank line and that the retargeted line 369 is already a lone argument, so the `1 129` numstat row P6-T2 asserts after formatting does not depend on a CSharpier rewrite; `FEATURE/spec.md` line count 334 (Grep `^`) and the `acquired and released inside a held` lines 10, 105, 266 and 282; the plan's own lines 7 to 9, 22, 131, 138, 1050, 1395, 1500, 1554 to 1555, 1684 and 1688 (pre-edit numbering) and the sibling occurrences the sweep found and left unchanged (130 and 1710 are `QfcDatamodel.cs` line numbers, 143 and 1394 are prose and a token list, 563 and 639 are line arithmetic, 1449 and 1516 are interim values correct at their own tasks); and, in the round-4 pass, the three round-4 deltas — `QfcDatamodel.cs` 464 to 475 (`#endregion Email Queue Initial Setup` 467, `#region Linked List Locking` 469, blanks 470 and 471, `#endregion Linked List Locking` 472, `#region Event Handlers` 474); the Grep reproduction of the three CMD-LEGACY-CALLERS strategies (PRIMARY 25 lines in 5 files, LOG 3, CROSS 2) with every one of the 30 lines classified against the revised P4-T1 list (469 and 472 `REGION-DIRECTIVE`; no further category needed); the BASE anchoring of CMD-HUNKS and CMD-ADDED-SCAN, which leaves P6-T2 as the only task that re-runs a HEAD-anchored diff or a `??` expectation after the P6-T9 commit (P8-T45 runs after the check-offs and before P8-T46, on no restart path); the plan's own lines 7 to 9, 23, 155, 1501, 1556, 1685, 1689 and 1741 (pre-edit numbering) and the sibling occurrences the sweep found and left unchanged (1449, 1470, 1495, 1516 and 1533 are pre-commit census tasks; 149, 1252, 1429, 1431, 1435, 1517 and 1598 are the plan's other recorded-not-gated values, none restated as gated by P8-T6, P8-T8, P8-T20, P8-T27 or P8-T36); and, in the round-5 pass, the two round-5 deltas — the single occurrence of each old text (Grep: the P6-T1 sentence at line 1555 and the P6-T2 clause at line 1557, pre-edit numbering); the absence of `PRIOR-PASS-REWRITTEN` and `RESTART-CORRECTED` from the plan before the edit; the plan's own lines 7 to 9, 24, 1555, 1557, 1686 and 1690 (pre-edit numbering); every other `REWRITTEN` occurrence read and left unchanged (1541 is the P5-T8 `REWRITTEN TEST NOT SENSITIVE` stop label; 1559 is the P6-T3 current-pass condition on the production build; 1587 and 1601 are the P8-T1 and P8-T7 `REWRITTEN-WRITESET:` and `REWRITTEN-OTHER:` labels); and every `restart` occurrence read and left unchanged (156 is D-13, which already defines both restart paths the new labels refer to; 170, 1400, 1561 to 1569, 1587, 1597, 1601 and 1653 name a restart rule without reading the exemption); and, in the delta-application knock-on pass of the same round, the `RESTART-CORRECTED:` union wording — the four `RESTART-CORRECTED` occurrences (25, 1556, 1558 and 1691; no line was added, so the numbering is unchanged by the edit) with the label definition at 25 and 1556 changed to the union of every D-13 correction in this run and the P6-T2 consumer at 1558 and the D-13 text at 157 (156 in the pre-round-5 numbering the previous clause uses) read and left unchanged, because 1558 admits a file by label membership without restating how the label is derived and 157 bounds neither the number of Phase 6 restarts (`P6-RESTART: n`) nor the number of Phase 8 restarts to P6-T1 (`ITERATION`), so a run with two restarts is a state the plan already allows. + +PLANNER-INTERNAL-REVIEW: PASS +CITATION-TO-TREE: PASS +AC-TRACEABILITY: PASS +SCOPE-BOUNDARY: PASS +CITATION: QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs | lines 12-31, 34-46, 92-104, 116-138, 146, 231, 243-274, 284-341 +CITATION: QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs | lines 33, 44-98, 107-149, 157-190, 192-276, 285 +CITATION: QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs | lines 27, 98-117, 181-186, 193, 213, 235, 254, 314, 331, 367, 447-478 +CITATION: QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs | lines 85-96, 161-181, 216-239, 251-280, 282-305 +CITATION: QuickFiler.Test/QuickFiler.Test.csproj | lines 17, 35, 155, 157, 161, 183, 196-215, 226-230 +CITATION: QuickFiler.Test/SetupAssemblyInitializer.cs | lines 14-25 +CITATION: QuickFiler.Test/Controllers/QfcItemController.InitializationTests.Part2.cs | line 124 +CITATION: QuickFiler.Test/Controllers/QfcItemController.MailActionsTests.cs | line 203 +CITATION: QuickFiler.Test/Controllers/QfcItemController.SeamFactoryTests.cs | line 13 +CITATION: QuickFiler.Test/TestSupport/DedicatedWorkerThread.cs | lines 4, 21 +CITATION: QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs | lines 9, 12-13, 18-24, 47-61, 62-87, 100-164, 166-172, 174-209, 211-234, 236-270, 272-310 +CITATION: QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs | lines 12, 18-27, 59-74, 180, 220-222 +CITATION: QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs | lines 12, 23-35, 93-100, 114-128, 136-154, 165-183, 195-230 +CITATION: QuickFiler.Test/Controllers/QfcDatamodelTests.cs | lines 9, 12, 95-131, 201-211, 253-283 +CITATION: QuickFiler.Test/Controllers/QfcFormControllerSeamTests.cs | lines 357-367 +CITATION: QuickFiler.Test/Controllers/QfcHomeControllerRunAsyncTests.cs | lines 325, 370-380 +CITATION: QuickFiler/Controllers/QfcDatamodel.cs | lines 25-26, 34-54, 77-103, 107-112, 128-152, 188-195, 197-241, 242-267, 271-315, 335-376, 377-416, 417-465, 467-474, 476-491 +CITATION: QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs | lines 15-24, 37-43, 48-66, 146, 280-291, 299-311, 364, 404-411 +CITATION: QuickFiler/Controllers/QfcDatamodel.FrameBuilding.cs | line 11 +CITATION: QuickFiler/Controllers/QfcStreamingDequeueConfidenceGate.cs | lines 190-301 +CITATION: QuickFiler/Controllers/QfcHomeController.cs | lines 92, 132, 344, 379 +CITATION: QuickFiler/Interfaces/IQfcDatamodel.cs | lines 103, 117, 131, 138-148, 164, 166 +CITATION: QuickFiler/Properties/AssemblyInfo.cs | line 5 +CITATION: QuickFiler/Legacy/IAcceleratorCallbacks.cs | line 5 +CITATION: QuickFiler/Controllers/QfcHighConfidencePreFilter.cs | line 11 +CITATION: QuickFiler/Controllers/QfcItemController.FocusAndTheme.cs | lines 274-286 +CITATION: UtilitiesCS/HelperClasses/ThemeHelpers/Theme.cs | lines 427-445 +CITATION: UtilitiesCS/Threading/UiThread.cs | lines 266-285 +CITATION: UtilitiesCS.Test/TestHelpers/ArmingBarrierTimeProvider.cs | lines 19-54 +CITATION: scripts/vscode/Invoke-MSTestWithCoverage.ps1 | lines 89-93, 97-134, 262, 297-298, 348-355, 399-423, 430-453, 459-461 +CITATION: scripts/vscode/Invoke-MSTestWithCoverage.FirstParty.ps1 | lines 117-123 +CITATION: scripts/vscode/TaskMaster.cli.runsettings | lines 4-7 +CITATION: scripts/hygiene/Test-RepositoryHygiene.Rules.ps1 | line 21 +CITATION: scripts/vscode/Install-RepoDotNetSdk.ps1 | line 3 +CITATION: scripts/vscode/Invoke-Restore.ps1 | lines 1-10 +CITATION: .gitignore | lines 26, 140, 141, 146, 150, 151 +CITATION: .gitattributes | line 4 +CITATION: .csharpierignore | lines 4, 12 +CITATION: global.json | lines 2-9 +CITATION: dotnet-tools.json | line 6 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md | lines 6-13, 56-79, 94-108, 138-171, 173-186, 237-270, 274-306, 308-318 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/issue.md | lines 12, 65-77 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T05-50-dispatcher-pin-call-sites-research.md | sections 1.1, 2.1, 2.2, 3, 4, 5, 6, 7 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T22-20-qfc-datamodel-972-fold-research.md | sections 1 to 8 and Numeric Derivation Evidence +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round1-report.2026-10-02T08-40.md | defects 1 to 10 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round2-report.2026-10-02T23-56.md | defects 1 to 8 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round3-report.2026-10-03T01-01.md | defects 1 to 4 and advisory A1 +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round4-report.2026-10-03T01-25.md | defects 1 to 3 (the advisory delta declined) +CITATION: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/other/preflight-round5-report.2026-10-03T01-53.md | defect 1 (deltas 1a and 1b) and the advisory +CITATION: docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md | exists (Glob) +CITATION: docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md | exists (Glob) +AC-INVENTORY: AC1, AC2, AC3, AC4, AC5, AC6, AC7, AC8, AC9, AC10, AC11, AC12, AC13, AC14, AC15, AC16, AC17, AC18, AC19, AC20, AC21, AC22, AC23, AC24, AC25, AC26, AC27, AC28, AC29, AC30, AC31, AC32 +AC-MAPPING: AC1 | IMPLEMENTATION: P1-T1, P2-T1 to P2-T5 | TESTS: P1-T5, P2-T8, P8-T5 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC2 | IMPLEMENTATION: P1-T1, P2-T4, P2-T5 | TESTS: P2-T8, P8-T5 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC3 | IMPLEMENTATION: P1-T1, P2-T5 | TESTS: P1-T6, P2-T8, P6-T5, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC4 | IMPLEMENTATION: P1-T1, P2-T5 | TESTS: P1-T6, P2-T8, P8-T5 | EVIDENCE: FEATURE/evidence/regression-testing/pass-after-pin-count.md +AC-MAPPING: AC5 | IMPLEMENTATION: P1-T1, P1-T2, P2-T1 to P2-T5 | TESTS: P1-T5, P2-T8 | EVIDENCE: FEATURE/evidence/regression-testing/fail-before-pin-count.md +AC-MAPPING: AC6 | IMPLEMENTATION: P1-T1 | TESTS: P1-T6, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC7 | IMPLEMENTATION: P3-T3, P3-T4 | TESTS: P6-T6, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/focus-and-theme-class-pass-after.md +AC-MAPPING: AC8 | IMPLEMENTATION: P3-T3, P3-T4, P3-T8 | TESTS: P7-T1, P7-T2 | EVIDENCE: FEATURE/evidence/qa-gates/call-site-census.md +AC-MAPPING: AC9 | IMPLEMENTATION: P2-T1, P2-T4, P2-T5 | TESTS: P2-T6, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC10 | IMPLEMENTATION: P3-T7, P3-T8 | TESTS: P6-T5, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/fixture-class-pass-after.md +AC-MAPPING: AC11 | IMPLEMENTATION: P2-T2, P2-T3, P2-T5 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC12 | IMPLEMENTATION: P3-T5 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC13 | IMPLEMENTATION: P3-T7 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC14 | IMPLEMENTATION: P3-T8 | TESTS: P6-T5, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/fixture-class-pass-after.md +AC-MAPPING: AC15 | IMPLEMENTATION: P3-T1, P3-T2, P3-T6 | TESTS: P6-T6, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC16 | IMPLEMENTATION: P3-T3, P3-T4 | TESTS: P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC17 | IMPLEMENTATION: P3-T5, P3-T6 | TESTS: P3-T9, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC18 | IMPLEMENTATION: P1-T1 to P5-T11, P6-T1 | TESTS: P8-T8 | EVIDENCE: FEATURE/evidence/qa-gates/file-line-counts.md +AC-MAPPING: AC19 | IMPLEMENTATION: P1-T1 to P5-T11 | TESTS: P7-T3 | EVIDENCE: FEATURE/evidence/qa-gates/prohibited-constructs-grep.md +AC-MAPPING: AC20 | IMPLEMENTATION: P6-T9 | TESTS: P8-T9 | EVIDENCE: FEATURE/evidence/qa-gates/footprint-scope.md +AC-MAPPING: AC21 | IMPLEMENTATION: P1-T2 | TESTS: P1-T4, P8-T5 | EVIDENCE: FEATURE/evidence/qa-gates/coverage-summary.md +AC-MAPPING: AC22 | IMPLEMENTATION: P8-T1 to P8-T5 | TESTS: P8-T5 | EVIDENCE: FEATURE/evidence/qa-gates/toolchain-final.md +AC-MAPPING: AC23 | IMPLEMENTATION: P0-T17, P8-T5 | TESTS: P8-T6 | EVIDENCE: FEATURE/evidence/qa-gates/coverage-comparison.md +AC-MAPPING: AC24 | IMPLEMENTATION: P1-T1 to P3-T8 | TESTS: P6-T7 | EVIDENCE: FEATURE/evidence/regression-testing/concurrent-set-test-summary.md +AC-MAPPING: AC25 | IMPLEMENTATION: P4-T2 to P4-T6 | TESTS: P4-T11, P6-T8, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC26 | IMPLEMENTATION: P5-T11 | TESTS: P5-T12, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/queue-processing-comment-census.md +AC-MAPPING: AC27 | IMPLEMENTATION: P4-T1, P4-T8 | TESTS: P4-T10, P8-T3, P8-T4, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/qfc-datamodel-legacy-callers.md +AC-MAPPING: AC28 | IMPLEMENTATION: P4-T8 | TESTS: P8-T8 | EVIDENCE: FEATURE/evidence/qa-gates/file-line-counts.md +AC-MAPPING: AC29 | IMPLEMENTATION: P4-T8 | TESTS: P6-T2, P8-T6 | EVIDENCE: FEATURE/evidence/qa-gates/coverage-comparison.md +AC-MAPPING: AC30 | IMPLEMENTATION: P4-T5, P4-T6, P4-T7, P5-T3, P5-T4 | TESTS: P6-T8, P6-T2 | EVIDENCE: FEATURE/evidence/qa-gates/post-format-census.md +AC-MAPPING: AC31 | IMPLEMENTATION: P5-T2, P5-T3, P5-T4 | TESTS: P5-T1, P5-T7, P5-T8, P5-T10, P6-T2 | EVIDENCE: FEATURE/evidence/regression-testing/liveness-sensitivity-check.md +AC-MAPPING: AC32 | IMPLEMENTATION: P4-T2 to P5-T11 | TESTS: P6-T8 | EVIDENCE: FEATURE/evidence/regression-testing/datamodel-set-test-summary.md +UNRESOLVED-GAPS: NONE + +DIRECTIVE: PREFLIGHT VALIDATION ONLY +Executor preflight for this revision has not yet run; the signal below is the planner's request line for the confirming round, not a self-approval and not a discovered defect. +PREFLIGHT: REVISIONS REQUIRED + diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/policy-audit.2026-10-03T04-00.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/policy-audit.2026-10-03T04-00.md new file mode 100644 index 000000000..a9bc75dfe --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/policy-audit.2026-10-03T04-00.md @@ -0,0 +1,288 @@ +# Policy Audit: focus-and-theme-tests-leak-shared-dispatcher-setup (Issue #968, folding Issue #972) + +- Timestamp: 2026-10-03T04-00 +- Branch: bug/focus-and-theme-tests-leak-shared-dispatcher-setup-968 +- Head: 5570b337cfd11e57579228b36bc919f9673b6195 (worktree reflog, last entry: merge of origin/main 993fdd01566dee82e5f37acb761a600feaaa1454 into the item) +- Base: origin/main 993fdd01566dee82e5f37acb761a600feaaa1454. The executor's Phase 0 anchor was 94287369908cc920b21b0e3256314f988ad7d2f5 (merge-base at that time, evidence/baseline/scope-and-anchor.md); origin/main advanced to 993fdd015 before the final head and was merged into the item, so origin/main is now an ancestor of the head and the item change set is the two-dot diff origin/main..HEAD, which the caller verified by name-status and which agrees with evidence/qa-gates/footprint-scope.md and evidence/qa-gates/final-commit.md. +- Work mode: full-bug (issue.md line 12); acceptance-criteria source: spec.md only (AC1 to AC32, spec.md lines 275-307) +- Reviewer: feature-review, no-Bash mode (caller directive). Every check was performed with Read, Grep and Glob against the item worktree, the committed evidence under evidence/, the session checkout's unchanged copies of the modified files as the pre-change text, and the worktree reflog as the head reference and clock. Where a check needs a shell it is recorded as such with the reason. +- Timestamp derivation: the label above is monotone after the head commit's reflog epoch 1791013176 (2026-10-03T07:39:36Z, 03:39:36 at the recorded -0400 offset) and after every label in the feature folder (latest 2026-10-03T03-37). No shell clock was readable in this session. The executor's labels were cross-checked against the reflog: the implementation commit label 03-23 matches epoch 1791012226 (03:23:46 -0400) and the final-commit label 03-37 matches epoch 1791013062 (03:37:42 -0400), so the evidence labels are clock-derived. + +## Executive Summary + +Overall verdict: AWAITING_CI. 1 Blocking finding (B-1: AC22, the full-toolchain criterion, is checked off only from this pull request's own CI run on the final head under the orchestrator ruling; remediability class awaiting_ci; nothing is remediable locally). 0 autonomous findings. 0 findings of class external_dependency, policy_hold or human_decision_required. Non-blocking findings and observations are detailed in code-review.2026-10-03T04-00.md. AC1 to AC21 and AC23 to AC32 verified PASS against code and evidence; AC22 is PENDING CI and stays unchecked. + +| Area | Verdict | Evidence summary | +|---|---|---| +| General Unit Test Policy | PASS | Four new fixture-level tests and two rewritten liveness tests are deterministic (single-threaded pin cycle under a held transaction; explicit armed-timer and completion signals; no clock-advance-then-yield step, no retry loop); no Thread.Sleep, Task.Delay, [DoNotParallelize] or temporary file added; 7361/7361 at baseline, 7365/7365 after the change | +| General Code Change Policy | PASS | Failing regression test first for the fixture defect (fail-before captured with the fixture at base content, pass-after with the fixture change as the only difference); production edits limited to dead-code removal, one nameof retarget and comment rewrites in an attribute-excluded type; every touched file at or under 500 lines; toolchain evidence single pass | +| C# Code Change Policy | PASS | csharpier check exit 0 (1640 files); analyzer /t:Rebuild 0 errors 0 warnings, SKIP_CORECOMPILE_LINES 0; TreatWarningsAsErrors /t:Rebuild 0 errors 0 warnings, SKIP_CORECOMPILE_LINES 0; no /p:Nullable=enable | +| C# Unit Test Policy | PASS | MSTest, Moq, FluentAssertions throughout; first-party lines 85.36%, branches 79.75% (floors 80/75 per CLAUDE.md, 85/75 per .claude/rules, both met) | +| Coverage (C#) | PASS | Repo-wide not lower than baseline (85.35 to 85.36 lines, 79.73 to 79.75 branches); the two changed production files belong to the attribute-excluded type QfcDatamodel (pre-existing attribute, unchanged) and the production edit removes unreachable members and rewrites comments only | +| Evidence hygiene | PASS | 0 host paths and 0 raw trx or coverage documents in the committed feature folder (reviewer Grep and Glob sweep; evidence/qa-gates/evidence-hygiene.md PROFILE_PATH_LINES 0, RAW_DOCUMENTS 0) | +| Coordinator prohibitions | PASS | No retries, no [DoNotParallelize], no Workers=1, no lengthened timeout, no wall-clock wait, no temporary file, runsettings unchanged (evidence/qa-gates/prohibited-constructs-grep.md over 696 added lines plus direct reads) | +| Toolchain step 4 route | PENDING CI (B-1) | Step 4 ran by the DIRECT route (collector over vstest with the four shell-icon classes excluded) because the stall probe reproduced a deterministic environmental failure that also reproduces on main; AC22 closes from the pull request's CI run per the ruling recorded under AC22 in spec.md | + +## Rejected Scope Narrowing + +No scope narrowing was detected in the caller prompt. The following caller statements were evaluated and accepted as factual or as tooling constraints rather than as narrowing: + +- "DO NOT use the Bash tool at all." A tooling constraint, not a scope constraint. The audit scope remains the full branch diff against origin/main; every changed file was read in full from the worktree and compared against the session checkout's unchanged copy where the file pre-existed. +- "The item's change set versus origin/main is exactly (name-status ...)" with sixteen paths plus the feature folder. Confirmed against evidence/qa-gates/footprint-scope.md and evidence/qa-gates/final-commit.md (both record git diff --name-status BASE HEAD) and against the files on disk. Zero PowerShell, TypeScript or Python files changed; no language was declared not applicable by the caller. +- "Evaluate AC22 as PENDING CI (remediability class awaiting_ci), not FAIL." A ruling on one criterion's evidence source, recorded verbatim under AC22 in spec.md and following the #950 AC17 precedent. It does not narrow the audit scope; this review evaluates every criterion and every policy over the full diff. + +## Evidence Location Compliance + +- Branch diff scan for files under artifacts/baselines/, artifacts/qa/, artifacts/evidence/ or artifacts/coverage/: none. The name-status origin/main..HEAD lists the fourteen code paths, the two promoted records under docs/features/potential/promoted/ and paths under docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/ only. +- All executor evidence lives under docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/{baseline,qa-gates,regression-testing,other}/ (74 Markdown files; Glob listing in this review). +- validate_evidence_locations.py --root .: not run (Bash was forbidden for this review; the script is not present in this checkout). The manual scan above substitutes; no violation observed. +- EVIDENCE_LOCATION_OVERRIDE_REJECTED: none required; the caller supplied no non-canonical evidence path. +- PR context artifacts (artifacts/pr_context.summary.txt, artifacts/pr_context.appendix.txt): absent in both the review worktree and the session checkout (Read of each exact path failed). Regeneration was not possible without a shell or the collection tool. Scope was derived from the caller-supplied name-status, the committed footprint evidence and the files on disk; the scope is verified by three agreeing sources even though the artifact pair itself could not be produced. +- Raw coverage document: coverage/final-968.cobertura.xml exists locally in the worktree (gitignored, not committed; root element read for this review: line-rate 0.853557, branch-rate 0.797517, lines-covered 56211, lines-valid 65855, branches-covered 13620, branches-valid 17078, timestamp 1791012617 = 03:30:17 -0400, consistent with the 03-31 label of evidence/qa-gates/coverage-summary.md). The canonical path artifacts/csharp/coverage.xml is absent in both checkouts; the committed JaCoCo package projection plus one-line summary are the forms CLAUDE.md "Committed Test Evidence Format" requires, and the standing ruling treats executor-committed feature-folder coverage evidence as the present artifact. + +## 1. General Unit Test Policy Compliance + +### 1.1 Core principles + +| Principle | Verdict | Evidence | +|---|---|---| +| Independence | PASS | Every pin-count test acquires its own UiThreadDispatcherFixture transaction first (QfcItemController.UiThreadDispatcherPinCountTests.cs lines 39-41, 87-89, 140-142, 188-190, 224-226), so the pin count is zero and the baseline known for the whole test; every test disposes its transaction in a finally. Every datamodel test builds its own uninitialized QfcDatamodel and owns its worker in a using block. The liveness test restores SynchronizationContext.Current through the SynchronizationContextScope disposer (QfcDatamodelLivenessTests.cs lines 189-199). Concurrent run of the three #968 classes 29/29 and of the four datamodel classes 21/21 | +| Isolation | PASS | Each pin-count test targets one property of the counted pin (non-last release, release order, foreign-value protection, flag reset); each rewritten liveness test targets the gate's re-arm decision; the fail-before run names exactly one failing assertion (afterFirstRelease, found null) | +| Fast execution | PASS | Pin-count tests 0.001 s to 0.055 s (evidence/regression-testing/pass-after-pin-count.md); rewritten liveness tests 0.160 s and 0.162 s alone (liveness-pass-after.md); no bounded wait remains | +| Determinism | PASS | The regression is observed on one thread with no concurrency; the census proves every pin is acquired and released inside a held transaction with no Install between (13 of 13 nested, evidence/qa-gates/call-site-census.md), so the final "last release nulls" assertion cannot be disturbed by another class; the liveness tests use ArmingFakeTimeProvider.Armed, Task.WhenAny and the dequeue task itself as signals, with the production awaits registered under a null SynchronizationContext so the loader's completion clears the flag inline; the sensitivity check shows both rewritten tests fail crisply (not hang) when the liveness lambda is forced false | +| Readability | PASS | Descriptive names; XML doc on every test, helper and fixture member; Arrange / Act / Assert markers; a because-reason on every assertion | + +### 1.2 Coverage + +**Coverage Metrics by Language:** + +| Language | Files Changed | Tests | Test Result | Baseline Coverage | Post-Change Coverage | New Code Coverage | +|---|---|---|---|---|---|---| +| C# | 13 | 7365 | 7365 passed, 0 failed | 85.35% lines / 79.73% branches | 85.36% lines / 79.75% branches | N/A (no instrumented production line added; see the per-language comparison block) | +| TypeScript | 0 | N/A | N/A | N/A | N/A | N/A | +| PowerShell | 0 | N/A | N/A | N/A | N/A | N/A | +| Python | 0 | N/A | N/A | N/A | N/A | N/A | + +Files Changed counts .cs files only (thirteen: ten modified, three added); the project file QuickFiler.Test/QuickFiler.Test.csproj and the two promoted Markdown records are the remaining three changed paths. + +Coverage source statement: the figures above are read from the committed projections and summaries (evidence/baseline/coverage-summary.md and coverage-jacoco-projection.md at P0-T17; evidence/qa-gates/coverage-summary.md and coverage-jacoco-projection.md at P8-T5; evidence/qa-gates/coverage-comparison.md at P8-T6), each carrying the first-party summary line, the root counters and the package-level JaCoCo projection, and were cross-checked against the root element of the local raw document coverage/final-968.cobertura.xml. Both runs used the DIRECT route with the identical four-class shell-icon exclusion, so they are comparable. + +Verdict lines: + +- C# coverage verdict: PASS (repo-wide first-party lines 85.36% and branches 79.75% from the committed post-change projection and the raw root element; above the CLAUDE.md floors of 80% lines and 75% branches and above the 85% / 75% floors in .claude/rules; not lower than the baseline 85.35% / 79.73%). +- C# changed-production-file coverage: PASS on the no-regression limb. Both changed production files, QuickFiler/Controllers/QfcDatamodel.cs and QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs, are partials of the type QfcDatamodel, which carries a pre-existing type-level ExcludeFromCodeCoverage attribute (QfcDatamodel.cs line 25, unchanged by this branch), so neither Cobertura document has a class node for the type (QFCDATAMODEL_CLASS_ENTRIES 0 at both stages) and no measured line can regress. The production edit adds no executable line: it removes four caller-free private members, their commented-out references and an empty region, retargets one nameof, and rewrites two doc comments. The thirteen changed or added test files are outside the denominator by policy. +- C# package-level corroboration: the QuickFiler package counters moved by one covered line (LINE missed 2294 / covered 10460 after; the +5 covered lines and +3 covered branches of the root delta are run-to-run variance across packages this branch does not instrument differently), consistent with a change whose only production lines are in an attribute-excluded type. +- PowerShell coverage gate: PASS by vacuity (zero PowerShell files changed on this branch, so the changed-line no-regression requirement has no line to evaluate; no PoshQC format, analyze or test gate was owed or run; artifacts/pester/powershell-coverage.xml was not consulted). +- TypeScript and Python: zero files changed on this branch; no verdict is owed. + +### Coverage Evidence Checklist + +- C# baseline coverage artifact: `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/coverage-summary.md` with `evidence/baseline/coverage-jacoco-projection.md` (committed one-line first-party summary, root counters and JaCoCo package projection; canonical artifacts/csharp/coverage.xml absent in the worktree) +- C# post-change coverage artifact: `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/coverage-summary.md` with `evidence/qa-gates/coverage-jacoco-projection.md` (same three forms; raw document coverage/final-968.cobertura.xml present locally, gitignored; canonical artifacts/csharp/coverage.xml absent in the worktree) +- TypeScript baseline coverage artifact: none consulted (zero TypeScript files changed on this branch) +- TypeScript post-change coverage artifact: none consulted (zero TypeScript files changed on this branch) +- PowerShell baseline coverage artifact: none consulted (zero PowerShell files changed on this branch) +- PowerShell post-change coverage artifact: none consulted (zero PowerShell files changed on this branch) +- Python baseline coverage artifact: none consulted (zero Python files changed on this branch) +- Python post-change coverage artifact: none consulted (zero Python files changed on this branch) +- Per-language comparison summary: the per-language comparison block of this document + +### 1.2.1 Per-Language Coverage Comparison + +- C#: Baseline: 85.35% lines (56206/65855) / 79.73% branches (13617/17078). Post-change: 85.36% lines (56211/65855) / 79.75% branches (13620/17078). Change: +0.01% lines (+5 covered) / +0.02% branches (+3 covered), with lines-valid and branches-valid identical at both stages. Disposition: PASS. Evidence: evidence/baseline/coverage-summary.md, evidence/qa-gates/coverage-summary.md, evidence/qa-gates/coverage-comparison.md, root element of coverage/final-968.cobertura.xml. +- TypeScript: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero TypeScript files changed on this branch. +- PowerShell: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero PowerShell files changed on this branch. +- Python: Baseline: N/A. Post-change: N/A. Change: N/A. Disposition: N/A. Evidence: N/A - zero Python files changed on this branch. + +### 1.2.2 Coverage Artifact State + +| Language | Artifact consulted | State | Disposition | +|---|---|---|---| +| C# | Committed projections and summaries under evidence/baseline and evidence/qa-gates; raw Cobertura root element read locally | Present; canonical artifacts/csharp/coverage.xml absent in the worktree (recorded as observation O-4, recurring) | PASS | +| TypeScript | none | zero files changed | no verdict owed | +| PowerShell | none | zero files changed | no verdict owed | +| Python | none | zero files changed | no verdict owed | + +Coverage exclusion policy check (.claude/rules/general-unit-test.md): the branch adds no coverage-config exclude entry and no new ExcludeFromCodeCoverage attribute (Grep over the fourteen code paths: the only occurrence is the pre-existing QfcDatamodel.cs line 25; the three new files carry none). Under the standing ruling (the CLAUDE.md UT2 COM/VSTO exemption is the more specific clause; the rules file's Blocking clause enumerates config exclude globs), the pre-existing attribute is Not Blocking for this change. Its consequence (no coverage observation for the removed lines) is immaterial here because the removed members were unreachable (zero callers, evidence/qa-gates/qfc-datamodel-legacy-callers.md) and both rebuilds compiled every remaining reference. + +### 1.3 Scenario completeness + +| Scenario | Verdict | Evidence | +|---|---|---| +| Positive flows | PASS | Two pins, first released, dispatcher kept (AC1); last release reverts (AC2); fresh pin after a full cycle installs and restores (AC4); liveness gate re-arms while the loader produces (AC31) | +| Negative flows | PASS | Pins taken under a transaction's live dispatcher install nothing and null nothing at count zero (AC3, R1); a pin under a transaction that installed the parked instance as its own value leaves it in place (AC4 second block); liveness sensitivity: with the liveness lambda forced false both rewritten tests fail on the re-arm assertion | +| Edge cases | PASS | Release order independence (AC2 specification test); idempotent scope dispose (R3, unchanged); double transaction dispose (R5, unchanged); flag-true-but-field-changed branch documented as a residual and shown unreached by the census (no Install between any pin's acquisition and release) | +| Error handling | PASS | R4 now releases its gate through finally on any throw (AC14); the throwing-loader liveness test still clears the flag through finally (unchanged, passes) | +| Concurrency | PASS | Three-class concurrent run 29/29 and four-class datamodel run 21/21 under Workers=0 / ClassLevel; the R4 two-transaction ordering kept with assertions unchanged; the pin lifetime nests inside the gate hold at every call site | +| State transitions | PASS | Count 0 -> 1 -> 2 -> 1 -> 0 with the field observed at each step (tests 1 and 2); flag set on first seeding and cleared on last release (test 4); _remainingLoadActive true -> false observed through ReadLivenessFlag before the final advance | + +### 1.4 Arrange-Act-Assert + +PASS. Every new and rewritten test carries Arrange / Act / Assert markers (pin-count tests lines 38, 49, 55, 86, 97, 103, 136, 149, 154, 186, 200, 206, 223, 234; liveness test lines 104, 143, 147, 157; sibling test lines 105, 133, 137); every assertion carries a because-reason string. + +### 1.5 External dependencies and temporary files + +PASS. No Outlook COM, file system, network or process is touched by any changed test; the parked and running dispatchers are in-process WPF dispatchers on background threads created by the existing fixture helpers; Grep over the thirteen changed test files for Path.GetTempFileName and Path.GetTempPath: 0 hits (evidence/qa-gates/prohibited-constructs-grep.md ADDED-TOKEN 0 and 0). + +### 1.6 Test file location + +PASS (repository convention). Tests live in QuickFiler.Test/Controllers/ and QuickFiler.Test/TestSupport/, mirroring the per-project *.Test layout used for every C# project in this repository; the two new TestSupport files sit beside the existing WinFormsPumpHost.cs and DedicatedWorkerThread.cs helpers. The rules file names a tests/ tree; the per-project layout is the pre-existing convention and no test file was colocated with production source. + +### 1.7 Determinism infrastructure + +PASS. Time is driven by FakeTimeProvider through the ArmingFakeTimeProvider subclass in both rewritten tests (QfcDatamodelLivenessTests.cs line 106, QfcDatamodelTests.cs line 107); the pin-count tests use no time at all. Banned APIs in test code: Thread.Sleep 0, Task.Delay 0, await Task.Yield() 0 over the 696 added lines; the only timed construct is the sibling file's [Timeout(GateTimeoutMs)] attribute with the same 60000 ms constant (four occurrences in the new class, a failure bound, not a wait). No randomness is used. + +## 2. General Code Change Policy Compliance + +| Item | Verdict | Evidence | +|---|---|---| +| Before making changes (plan, spec) | PASS | spec.md (336 lines, amendments 1.1 and 1.2) and plan.2026-10-02T05-42.md (123 of 123 tasks checked) exist; six preflight rounds and a clearance recorded under evidence/other/ | +| Bugfix workflow step 1 (failing regression test first) | PASS | Fixture defect: EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease failed on the fixture at base content (hash equal to the P0-T12 BASE-HASH) with the predicted message "... a holder that did not take the last pin must not lose the dispatcher, but found " (evidence/regression-testing/fail-before-pin-count.md), then passed with the fixture change as the only working-copy difference (pass-after-pin-count.md, porcelain comparison). Liveness rewrites: a deterministic failing run of the old shape is structurally impossible (its failure depends on thread-pool scheduling); the dossier fail-before-exception.2026-10-03T03-09.md records the reason, and the labelled sensitivity check (liveness-sensitivity-check.md) shows the new shape fails crisply when the liveness signal is dishonest, with the temporary production edit reverted and proven byte-identical to base. Dead-code removal: unreachable code with no behaviour to regress; the zero-caller proof (qfc-datamodel-legacy-callers.md, two strategies, member sets identical, INVOCATIONS 0) plus two rebuilds are the proof | +| Bugfix workflow step 2 (minimal targeted fix) | PASS | Fixture diff 48 added / 15 deleted lines, all in EnsureDispatcher, EnsureScope and docs; the production diff removes 129 lines and adds 1 in QfcDatamodel.cs and changes 8 comment lines in QueueProcessing.cs; no reachable production behaviour changes | +| Bugfix workflow step 3 (verify locally, toolchain in order) | PENDING CI (B-1) | Steps 1-3 single pass, exit 0, SKIP_CORECOMPILE_LINES 0 on both rebuilds (evidence/qa-gates/toolchain-final.md SINGLE-PASS: YES). Step 4 ran the DIRECT route because the stall probe recorded ShellUtilities_Tests.GetFileIcon_WithUseFileType_ShouldReturnIconsForDirectoryAndFileExtension failing deterministically on this workstation (Win32 handle not valid; reproduces on main; evidence/baseline/stall-probe.md). The orchestrator ruling under AC22 in spec.md defers the check-off to this pull request's CI run on the final head | +| Design principles (simplicity, reusability, extensibility, separation) | PASS | One counter and one flag at the single mutation point of the shared static; the dead theme-test calls deleted instead of pinning state the tests never read; one shared SynchronousBackgroundWorker replaces three byte-identical copies; ArmingFakeTimeProvider is a 49-line subclass modelled on the existing UtilitiesCS.Test ArmingBarrierTimeProvider | +| Classes, functions, APIs | PASS | Fixture public shape unchanged (internal static IDisposable EnsureDispatcher()); new helpers internal sealed with documented contracts; StartHeldOpenLoader takes the caller-owned worker | +| Error handling | PASS | No new catch; R4 gains try/finally; the hard cast in StartSynchronously fails fast on misuse | +| Logging | PASS | No new logging; the retargeted nameof corrects the method named in an existing log line | +| File size limit (500 lines) | PASS | Reviewer Read line counts: fixture 375, FocusAndThemeTests 482, TestSupport 442, fixture tests 472, pin-count tests 248, Liveness 352, Teardown 229, ZeroBatch 226, DatamodelTests 394, QfcDatamodel.cs 367 (was 495), QueueProcessing.cs 413, SynchronousBackgroundWorker.cs 27, ArmingFakeTimeProvider.cs 49; all agree with evidence/qa-gates/file-line-counts.md | +| Naming | PASS | PascalCase types and members (ArmingFakeTimeProvider, StartSynchronously, ReArm); camelCase locals (pinA, afterFirstRelease, foreignTransaction); descriptive test names without digits | +| Public APIs and compatibility | PASS | IQfcDatamodel unchanged; every removed member was private with zero callers; the one-argument LoadRemainingEmailsToQueueAsync the constructors bind is kept | +| Dependencies | PASS | None added; Microsoft.Extensions.Time.Testing already referenced by the test project | +| I/O boundaries | PASS | No I/O introduced | + +## 3. Language-Specific Code Change Policy Compliance + +Language in scope: C# only. + +| Item | Verdict | Evidence | +|---|---|---| +| Formatting (csharpier via dotnet tool run) | PASS | evidence/qa-gates/csharpier-format-final.md (REWRITTEN-WRITESET: NONE) and csharpier-check-final.md: Checked 1640 files, CSHARPIER_EXIT_CODE 0 | +| Linting (analyzer rebuild, /t:Rebuild, EnableNETAnalyzers, EnforceCodeStyleInBuild) | PASS | evidence/qa-gates/msbuild-analyzer-final.md: MSBUILD_EXIT_CODE 0, ERRORS 0, WARNINGS 0, SKIP_CORECOMPILE_LINES 0, WRITESET_DIAGNOSTIC_LINES 0 | +| Type checking (TreatWarningsAsErrors rebuild, no /p:Nullable=enable) | PASS | evidence/qa-gates/msbuild-nullable-final.md: exit 0, 0 errors, 0 warnings, SKIP_CORECOMPILE_LINES 0; command text matches CLAUDE.md character for character | +| Nullable annotations | PASS | No changed file carries #nullable enable; no nullable directive was added or removed | +| DI seams | PASS | No new production seam; the liveness rewrite uses the existing TimeProvider property and the existing WorkerStarter and RemainingEmailLoader delegates | +| XML docs on non-obvious contract | PASS | Fixture class doc, EnsureDispatcher doc and EnsureScope doc describe counting, ownership, discard consequence and residual; wrapper doc names the remaining callers and the nesting rule; ArmingFakeTimeProvider remarks state the consecutive-Advance prohibition | +| Internal surface | PASS | Both new helpers internal sealed; the fixture's new fields private static | +| Name resolution | PASS | SynchronousBackgroundWorker.StartSynchronously takes BackgroundWorker in a file that imports no Outlook namespace; the datamodel test files that import Microsoft.Office.Interop.Outlook write System.Action where a bare delegate is needed (QfcDatamodelTeardownTests.cs line 175, QfcInitEmailQueueZeroBatchTests.cs line 135) | +| Analyzer suppressions | PASS | None added; the file's only pragma (CS0618 around the removed two-argument overload) disappears with the dead member | + +## 4. Language-Specific Unit Test Policy Compliance + +| Item | Verdict | Evidence | +|---|---|---| +| MSTest framework | PASS | [TestClass] / [TestMethod] in every changed test file; the new class uses the sibling [Timeout(GateTimeoutMs)] convention | +| Moq for mocks | PASS | Mock, Mock, Mock, Mock, Mock, Mock where mocks are needed; the pin-count class states why Moq is not imported | +| FluentAssertions | PASS | All assertions use Should(); no MSTest Assert introduced | +| Repo-wide coverage floors | PASS | 85.36% lines (floor 80% per CLAUDE.md, 85% per rules), 79.75% branches (floor 75%) | +| New module/class/method >= 90% | PASS on the applicable limb | No new production module, class or method exists; the three new files are test files outside the denominator | +| No regression on changed lines | PASS | No measured changed production line exists (attribute-excluded type); repo-wide and package-level figures are not lower | +| Prohibited behaviors (sleeps, retries, timing hacks, weakened assertions) | PASS | ADDED-TOKEN Thread.Sleep 0, Task.Delay 0, DoNotParallelize 0, Retry( 0, await Task.Yield() 0, for (int i 0 over 696 added lines; GateTimeoutMs = 60000 equals the sibling constant; R4 keeps BeSameAs(original) and NotBeSameAs(liveA) with because texts unchanged; R1 to R3 assertions unchanged (reviewer comparison against the session checkout's copy) | +| Test toolchain route | PENDING CI (B-1) | Step 4 used the DIRECT route for the environmental reason recorded in section 2; the inner vstest invocation used the repository runsettings (Workers=0, Scope=ClassLevel) unchanged | + +## 5. Test Coverage Detail + +| File | Change type | Coverage observation | Disposition | +|---|---|---|---| +| QuickFiler/Controllers/QfcDatamodel.cs | Modified production (+1 / -129) | No class node in either Cobertura document (type-level ExcludeFromCodeCoverage, pre-existing). Removed members had zero callers; the surviving one-argument loader is unchanged except the nameof operand | PASS (no-regression limb; no executable line added) | +| QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs | Modified production (comment-only, 8 lines) | Same excluded type; every changed line begins with /// (evidence/qa-gates/queue-processing-comment-census.md transcribed diff, confirmed by reading) | PASS (no executable line changed) | +| QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs | Modified test support (+48 / -15) | Outside the denominator by policy | Not measured; fixture tests 8/8 and pin-count tests 4/4 Passed | +| QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs | Added test (248 lines) | Outside the denominator by policy | Not measured; 4/4 Passed, 1 fail-before captured | +| QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs | Modified test (+16 / -14) | Outside the denominator by policy | Not measured; 8/8 Passed | +| QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs | Modified test (+20 / -35) | Outside the denominator by policy | Not measured; 17/17 Passed | +| QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs | Modified test support (+20 / -18, doc only) | Outside the denominator by policy | Not measured | +| QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs | Modified test (+137 / -101) | Outside the denominator by policy | Not measured; 4/4 Passed | +| QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs | Modified test (+2 / -17) | Outside the denominator by policy | Not measured; 5/5 Passed | +| QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs | Modified test (+41 / -49) | Outside the denominator by policy | Not measured; 3/3 Passed | +| QuickFiler.Test/Controllers/QfcDatamodelTests.cs | Modified test (+68 / -47) | Outside the denominator by policy | Not measured; 9/9 Passed | +| QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs | Added test support (27 lines) | Outside the denominator by policy | Not measured; exercised by every datamodel test that starts a worker | +| QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs | Added test support (49 lines) | Outside the denominator by policy | Not measured; exercised by the two rewritten liveness tests | +| QuickFiler.Test/QuickFiler.Test.csproj | Modified project file (+3 Compile items) | Not a source file | Discovery proven: all four pin-count tests appear in the final run | + +Package-level projection (QuickFiler package): LINE missed 2294 / covered 10460; BRANCH missed 699 / covered 2518 after the change (evidence/qa-gates/coverage-jacoco-projection.md). + +## 6. Test Execution Metrics + +| Run | Scope | Total | Passed | Failed | Source | +|---|---|---|---|---|---| +| Baseline (P0-T17, DIRECT route) | nine test assemblies | 7361 | 7361 | 0 | evidence/baseline/coverage-summary.md | +| Final (P8-T5, DIRECT route) | nine test assemblies | 7365 | 7365 | 0 | evidence/qa-gates/coverage-summary.md | +| Pin-count regression fail-before (P1-T5, fixture at base) | one test | 1 | 0 | 1 (expected) | evidence/regression-testing/fail-before-pin-count.md | +| Specification tests before the fix (P1-T6) | three tests | 3 | 3 | 0 | evidence/regression-testing/specification-tests-before-fix.md | +| Pin-count class pass-after (P2-T8) | four tests | 4 | 4 | 0 | evidence/regression-testing/pass-after-pin-count.md | +| Liveness sensitivity check (P5-T8, lambda forced false, reverted) | two tests | 2 | 0 | 2 (expected, labelled) | evidence/regression-testing/liveness-sensitivity-check.md | +| Liveness pass-after (P5-T7) | two tests | 2 | 2 | 0 | evidence/regression-testing/liveness-pass-after.md | +| Concurrent set, three #968 classes (P6-T7) | QuickFiler.Test | 29 | 29 | 0 | evidence/regression-testing/concurrent-set-test-summary.md | +| Datamodel set, four classes (P6-T8) | QuickFiler.Test | 21 | 21 | 0 | evidence/regression-testing/datamodel-set-test-summary.md | +| Stall probe (P0-T16, shell-icon classes) | 23 tests | 23 | 22 | 1 (environmental, reproduces on main) | evidence/baseline/stall-probe.md | + +Figures compared (evidence/qa-gates/coverage-summary.md FIGURES-COMPARED): final total equals baseline plus 4 (the four pin-count tests; the fold rewrites two tests and adds none); error, timeout, aborted and notExecuted each 0 at both stages; no Sequence file in any run; FINAL-FAILED-SET empty; MESSAGE lines none (no LEAK-DEPENDENT TEST EXPOSED). + +## 7. Code Quality Checks + +| Check | Command or method | Result | Verdict | +|---|---|---|---| +| Confidentiality masking scan | Grep over the feature folder for drive-letter paths, user-profile paths and account names | 0 hits (agrees with evidence/qa-gates/evidence-hygiene.md PROFILE_PATH_LINES 0 after the recorded redaction of inherited preflight reports) | PASS | +| Raw document scan | Glob over the feature folder for non-Markdown files | 0 files (RAW_DOCUMENTS 0 in the executor gate) | PASS | +| Suppression scan (added lines) | Read of all fourteen code paths | No new #pragma, [SuppressMessage], [ExcludeFromCodeCoverage] or analyzer suppression; one pre-existing pragma removed with its dead member | PASS | +| Workflow change scan | Name-status origin/main..HEAD | No .github/, scripts/ or runsettings path changed; the only project-file change is three Compile items (evidence/qa-gates/file-line-counts.md: 190 = 187 + 3) | PASS | +| Prohibited-construct scan | Grep over the thirteen changed test files and the added-lines scan | Thread.Sleep 0, Task.Delay 0, DoNotParallelize 0, Retry( 0, Workers 0, Task.Yield 0 in the two rewritten tests; four [Timeout(GateTimeoutMs)] at the sibling constant | PASS | +| Call-site census | Grep for EnsureUiThreadDispatcher and EnsureDispatcher across the worktree | Sixteen primary lines: two declarations, one forwarder, thirteen invocations (three in R1 to R3, ten in the pin-count class), each nested inside a held transaction with no Install between acquisition and release; zero in FocusAndThemeTests; zero in R4 (reviewer Grep agrees with evidence/qa-gates/call-site-census.md) | PASS | +| Shared-helper census | Grep for class SynchronousBackgroundWorker over *.cs | Exactly one declaration, QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs line 15 | PASS | +| Tonality scan | Read of spec.md, issue.md, both research records and the committed evidence | Neutral, factual wording; no humor, hyperbole or metaphor | PASS | + +## 8. Gaps and Exceptions + +- B-1 (Blocking, awaiting_ci): AC22 requires the MSTest coverage route (Invoke-MSTestWithCoverage.ps1) to pass locally in the same uninterrupted pass as the three preceding steps. The runner cannot pass on this workstation because one UtilitiesCS.Test shell-icon test fails deterministically for an environmental reason that reproduces on main; the DIRECT route substituted with the same runsettings and the same assembly set minus four shell-icon classes, 7365/7365. The orchestrator ruling under AC22 in spec.md (following the #950 AC17 precedent) closes AC22 only from this pull request's own CI run on the final head. No local action can discharge it. Recorded in remediation-inputs.2026-10-03T04-00.md. +- PR context artifact pair absent: scope verified from three agreeing sources instead (section Evidence Location Compliance). +- Canonical C# coverage artifact path absent: committed projections and the local raw document used, per the standing ruling (observation O-4, recurring across #948, #950, #956). +- Liveness-test fail-before: structurally impossible for the old shape; exception dossier plus labelled sensitivity check recorded, as the spec (decision 7) and the atomic-plan contract permit. +- quality-tiers.yml absent at the repository root (pre-existing; tier gates unevaluable; already promoted by the #956 review). Not attributable to this item. + +## 9. Summary of Changes + +- Fixture (QfcItemController.UiThreadDispatcherFixture.cs): ensure pins are reference counted under FieldLock with an install-ownership flag; the last release writes null only when the fixture seeded the parked dispatcher and the field still holds it; the decrement and the conditional revert are inline in one critical section; class, method and scope docs rewritten (D1). +- Theme tests (QfcItemController.FocusAndThemeTests.cs): the two dead EnsureUiThreadDispatcher() calls deleted with corrected arrange comments (D6); the private BuildExecutingViewer copy deleted in favour of the shared helper (D5). +- Test support (QfcItemController.TestSupport.cs): wrapper doc describes the counted pin and names the remaining callers (D2); shared-helper doc no longer cites an unreachable private copy (D5). EnsureSynchronizationContext untouched (D8). +- Fixture tests (QfcItemController.UiThreadDispatcherFixtureTests.cs): R4 doc rewritten to the counting guarantee and nesting invariant (D3); R4's baseline pin removed and transactionA wrapped in try/finally (D4, closing #972 item 5); assertions and because texts unchanged. +- New pin-count test class (QfcItemController.UiThreadDispatcherPinCountTests.cs): one fail-before regression test and three labelled specification tests; Compile item added. +- Folded #972 items 1 to 4 and the #968 liveness comment: shared SynchronousBackgroundWorker helper with the StartSynchronously starter; _remainingLoadActive comment rewritten and the stale TryUnhookOrReplace line range dropped; four caller-free QfcDatamodel members, their commented-out references and the empty region removed with the nameof retarget (495 -> 367 lines); every test-created worker owned in a using block; the two dequeue-liveness tests rewritten to explicit signals through the new ArmingFakeTimeProvider; two Compile items added. +- Evidence: 74 Markdown files under the feature folder (baseline, regression-testing, qa-gates, other); no raw document committed. + +## 10. Compliance Verdict + +AWAITING_CI. Every policy area evaluates PASS over the full branch diff. One blocking finding remains, B-1, of remediability class awaiting_ci: AC22's test-stage evidence comes from this pull request's CI run on the final head under the recorded ruling. Zero autonomous findings; nothing requires a remediation cycle before the pull request is opened. The pull request body must carry the two closing lines Closes #968 and Closes #972 and no other issue number beside a closing keyword (spec Dependencies and Rollout). + +## Appendix A: Test Inventory + +| Test class | Test | Status after change | AC | +|---|---|---|---| +| QfcItemController_UiThreadDispatcherPinCountTests | EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease | Failed before the fix (fixture at base), Passed after | AC1, AC2, AC5, AC6 | +| QfcItemController_UiThreadDispatcherPinCountTests | EnsureDispatcher_TwoPinsHeld_ReleaseOrderDoesNotChangeTheOutcome | Passed before and after | AC2, AC6 | +| QfcItemController_UiThreadDispatcherPinCountTests | EnsureDispatcher_UnderATransactionHoldingALiveDispatcher_ReleasingAllPinsLeavesTheLiveDispatcher | Passed before and after | AC3, AC6 | +| QfcItemController_UiThreadDispatcherPinCountTests | EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores | Passed before and after | AC4, AC6 | +| QfcItemController_UiThreadDispatcherFixtureTests | Transaction_SecondCallerCannotInstallUntilTheFirstRestores (R4) | Passed, alone and concurrent | AC10, AC13, AC14 | +| QfcItemController_UiThreadDispatcherFixtureTests | R1, R2, R3, R5, R6, #743 counters, #882 zero-bound | Passed, unchanged | AC3, AC10 | +| QfcItemController_FocusAndThemeTests | SetThemeDark_FromNormal_SelectsDarkNormalTheme, SetThemeLight_FromNormal_SelectsLightNormalTheme | Passed (ensure calls removed) | AC7, AC16 | +| QfcItemController_FocusAndThemeTests | fifteen other tests | Passed, unchanged behaviour (shared BuildExecutingViewer) | AC15, AC24 | +| QfcDatamodelLivenessTests | DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle | Passed (rewritten); Failed under the sensitivity check | AC31 | +| QfcDatamodelLivenessTests | RemainingLoadActive_AcrossAsyncVoidFirstAwait_StaysTrueWhileLoaderProduces, RemainingLoadActive_AfterLoaderCompletes_BecomesFalse, RemainingLoadActive_WhenLoaderThrows_IsStillClearedByFinally | Passed (caller-owned worker) | AC25, AC30 | +| QfcDatamodelTests | DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive | Passed (rewritten); Failed under the sensitivity check | AC31 | +| QfcDatamodelTests | eight other tests | Passed (two gain using blocks) | AC30, AC32 | +| QfcDatamodelTeardownTests | five tests | Passed (shared helper) | AC25, AC32 | +| QfcInitEmailQueueZeroBatchTests | three tests | Passed (shared helper, using blocks) | AC25, AC30, AC32 | + +Test method count: 7361 at baseline, 7365 after the change (four added, none removed). + +## Appendix B: Toolchain Commands Reference + +| Step | Command (as recorded in evidence/qa-gates/toolchain-final.md) | Exit | Iteration | +|---|---|---|---| +| 1 | dotnet tool run csharpier format . | 0 | 1 | +| 1b | dotnet tool run csharpier check . | 0 | 1 | +| 2 | msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true | 0 | 1 | +| 3 | msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true | 0 | 1 | +| 4 | dotnet-coverage collect --output coverage\final-968.cobertura.xml --output-format cobertura --settings coverage\effective-coverage-968.config -- vstest.console.exe /Settings:scripts\vscode\TaskMaster.cli.runsettings /InIsolation "/TestCaseFilter:TestCategory!=LiveOutlook&FullyQualifiedName!~HelperClasses.ShellUtilities_Tests&FullyQualifiedName!~HelperClasses.ShellUtilitiesStatic_Tests&FullyQualifiedName!~HelperClasses.SysImageListHelperTests&FullyQualifiedName!~EmailIntelligence.OSBrowser_Tests" "/ResultsDirectory:coverage\test-results\968\final" "/Logger:trx;LogFileName=final-968.trx" "/Blame:CollectHangDump;TestTimeout=4min;HangDumpType=None" (DIRECT route; the runner script Invoke-MSTestWithCoverage.ps1 was not run verbatim, for the environmental reason in section 2; PASS 7365/7365) | 0 | 1 | + +PowerShell gates (PoshQC MCP format / analyze / test): not run; zero PowerShell files changed on this branch. + +Reviewer commands: none (Bash forbidden). All verification by Read, Grep and Glob against the item worktree, the session checkout's pre-change copies and the worktree reflog. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/remediation-inputs.2026-10-03T04-00.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/remediation-inputs.2026-10-03T04-00.md new file mode 100644 index 000000000..7be9795b1 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/remediation-inputs.2026-10-03T04-00.md @@ -0,0 +1,37 @@ +# Remediation Inputs: focus-and-theme-tests-leak-shared-dispatcher-setup (Issue #968, folding Issue #972) + +- Timestamp: 2026-10-03T04-00 +- Branch: bug/focus-and-theme-tests-leak-shared-dispatcher-setup-968, head 5570b337cfd11e57579228b36bc919f9673b6195 +- Source artifacts: policy-audit.2026-10-03T04-00.md, code-review.2026-10-03T04-00.md, feature-audit.2026-10-03T04-00.md (all in docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/) + +Review-Verdict: AWAITING_CI + +## Derivation + +- Blocking findings: 1 (B-1 below). +- Findings of class autonomous: 0, so the verdict is not REMEDIATION_REQUIRED. +- Findings of class external_dependency, policy_hold or human_decision_required: 0, so the verdict is not HALT_NON_REMEDIABLE. +- The remaining blocking finding is of class awaiting_ci, so the verdict is AWAITING_CI. +- No code change, test change or evidence change is required before the pull request is opened. + +## Blocking findings + +### B-1: AC22 (full toolchain pass) is closed only from this pull request's CI run on the final head + +Severity: Blocking +Remediability: awaiting_ci +Remediability-Evidence: the only locally failing step is the verbatim MSTest coverage runner, and it fails for an environmental shell-icon test in UtilitiesCS.Test that reproduces on main on this workstation; the orchestrator ruling recorded under AC22 in spec.md (following the #950 AC17 precedent) designates the pull request's own CI run on the final head as the evidence source, and no local action can produce that run. +File: docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md, line 296 (`- [ ] AC22`) with the ruling note at line 297 +Rule: spec.md acceptance criterion AC22; CLAUDE.md "After Making Changes" step 1 (full toolchain, step 4 is the MSTest coverage route); acceptance-criteria-tracking skill, "CI-Dependent Criteria" +Evidence: evidence/qa-gates/toolchain-final.md (steps 1 to 4 exit 0 in ITERATION 1, SINGLE-PASS: YES, both rebuilds SKIP_CORECOMPILE_LINES 0, coverage step COVERAGE-ROUTE DIRECT, RUNNER-GREEN NO, 7365/7365 passed); evidence/baseline/stall-probe.md (STALL-PROBE: REPRODUCES; ShellUtilities_Tests.GetFileIcon_WithUseFileType_ShouldReturnIconsForDirectoryAndFileExtension fails with "Win32 handle that was passed to Icon is not valid or is the wrong type"); evidence/qa-gates/coverage-comparison.md (lines 85.35% to 85.36%, branches 79.73% to 79.75%, AC23-STATUS MET); evidence/other/ac-status-summary.md (AC22 NOT MET, environmental) +Required action (orchestrator, at the CI green gate): open the pull request from the final head with a body carrying `Closes #968` and `Closes #972`; after the C# test-and-coverage job reports success, record the CI run ID, the head SHA, the job's pass and fail counts and coverage figures, and the local DIRECT result under AC22 in spec.md; change `- [ ] AC22` to `- [x] AC22` in the item's own worktree; push; re-run the CI green gate so that ci_gate.head_sha equals the final pull request head. If CI fails any test, AC22 is not met and the item returns to remediation. + +## Non-blocking findings carried for reference (no remediation owed) + +- CR-1: pre-existing commented-out statements remain in QuickFiler/Controllers/QfcDatamodel.cs (lines 63, 70, 199, 268, 318, 350); the ratified spec confines the production edit, and the research disposition (addendum F2) keeps the diff reviewable. Remove in the next change to the file. +- CR-2: the rewritten sibling test in QuickFiler.Test/Controllers/QfcDatamodelTests.cs (lines 103-152) registers its awaits under the ambient SynchronizationContext; accepted by the spec (addendum section 6.2). Wrap the dequeue call in the null-context scope if a non-pumping context ever appears on an MSTest thread. +- O-4: canonical C# coverage artifact path artifacts/csharp/coverage.xml absent in the worktree; committed projections and summaries used under the standing ruling (recurring). + +## Unrelated defects to file + +- None from this item. quality-tiers.yml is absent at the repository root (pre-existing; already promoted by the #956 review). diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T05-50-dispatcher-pin-call-sites-research.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T05-50-dispatcher-pin-call-sites-research.md new file mode 100644 index 000000000..3cd6f4436 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T05-50-dispatcher-pin-call-sites-research.md @@ -0,0 +1,204 @@ +# Research: dispatcher pin call sites and fix design (issue #968) + +- Issue: #968 `focus-and-theme-tests-leak-shared-dispatcher-setup` (full-bug) +- Branch: `bug/focus-and-theme-tests-leak-shared-dispatcher-setup-968`, base `origin/main` 94287369 +- Date: 2026-10-02 +- Method: every claim below was verified by reading the named file at the named lines in the item worktree or by the grep expressions quoted in section 2. `git log` was not available in this session (the Bash tool is disabled), so no history claim is made; where a comment in the code states history, it is quoted as a comment, not asserted as fact. +- Paths are repository-relative. `FEATURE` = `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968`. + +## 0. Findings that change the framing of the issue + +1. **The fixture pin is not reference counted.** `UiThreadDispatcherFixture.EnsureDispatcher()` (`QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs:122-138`) seeds the parked dispatcher only when the static is `null` and returns a scope that remembers only whether *this call* installed it. A second concurrent caller receives a no-op scope. Whichever caller installed the value nulls it on `Dispose` (`:269-272`, `CompareExchange(_installed, null)`) regardless of how many other callers still hold a scope. This is the shared state the issue asks to "find and own". + +2. **The two theme tests do not read the shared dispatcher at all.** `QfcItemController.SetThemeDark(bool)` / `SetThemeLight(bool)` (`QuickFiler/Controllers/QfcItemController.FocusAndTheme.cs:274-286`, `:322-335`) call `Theme.SetQfcTheme(async: true)`, which goes through the theme's injected `_uiDispatcher` (`UtilitiesCS/HelperClasses/ThemeHelpers/Theme.cs:427-432`), and the test's themes are built by `QfcItemControllerTestSupport.BuildColorTheme`, which injects a `Mock` whose `InvokeAsync` returns `Task.CompletedTask` (`QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs:169-181`). No statement on that path dereferences `UtilitiesCS.UiThread.Dispatcher`. Consequently the issue's "a theme test can observe a null dispatcher" cannot be reproduced through the theme tests' own assertions; the `EnsureUiThreadDispatcher()` calls at `FocusAndThemeTests.cs:452` and `:468` are dead arrangement whose only effect is the gate-free write W1 that #950 identified as the concurrent writer that broke R4. The #950 code review (`.../950/code-review.2026-10-02T14-30.md:29`, O-1) assumed the theme tests "then read the process-wide dispatcher"; the code trace in section 3 shows they do not. + +3. **Consequence for the issue's second validation idea** ("show the theme test fails without the fix"): it is not satisfiable as worded. The deterministic regression must live at the fixture level (section 5), where the missing reference counting is directly observable without concurrency. + +4. **FocusAndThemeTests.cs is 497 content lines.** Any additive fix in that file (class-level `[ClassInitialize]`/`[ClassCleanup]` pin) breaches the 500-line limit and forces a file split. The recommended fix (section 4) removes lines from that file instead. + +## 1. Current state + +### 1.1 `UiThreadDispatcherFixture` (`QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs`, 342 content lines) + +| Member | Lines | Behaviour verified | +|---|---|---| +| Class doc, lock design | 12-31 | `FieldLock` guards straight-line read-modify-write of `UiThread._dispatcher`; `TransactionGate` (`SemaphoreSlim(1,1)`) serialises install-to-restore transactions; ordering TransactionGate then FieldLock. Lines 26-30: `EnsureDispatcher` deliberately never takes `TransactionGate` because its callers carry no `[Timeout]`. | +| Statics | 34-46 | `FieldLock`, `TransactionGate`, `ParkedDispatcherLock`, cached `FieldInfo DispatcherField`, `_parkedDispatcher`, three monotonic counters (#743). **No pin counter exists.** | +| `Current` | 62-71 | Reads the private static `UiThread._dispatcher` under `FieldLock` via reflection. Does not call the throwing public getter. | +| `Exchange` | 77-85 | Atomic swap, returns previous. | +| `CompareExchange` | 92-104 | Writes `restoreTo` only if the field still holds `expected`; returns whether written. | +| `ReleaseTransactionGate` | 110-114 | Increments release counter, releases the semaphore. | +| `EnsureDispatcher` | 122-138 | Obtains the parked dispatcher outside `FieldLock` (:126); under `FieldLock`, if field is `null` writes parked and returns `new EnsureScope(parked)` (:130-134); otherwise returns `new EnsureScope(null)` (:137). Doc at 116-121 states: "Disposing the returned scope is optional: a discarded scope leaks exactly as the pre-fix helper did." | +| `TransactionGateAcquireTimeoutMs` | 146 | 120000 ms (#882). | +| `BeginTransactionAsync()` / `(TimeSpan)` | 155-190 | Bounded gate acquisition; throws `TimeoutException` with token `TRANSACTIONGATE_ACQUIRE_TIMEOUT`. | +| `ResolveDispatcherField` | 197-205 | Reflects `UiThread._dispatcher` (`NonPublic | Static`), asserts non-null. | +| `GetParkedDispatcher` | 213-241 | Lazily starts one STA background thread named `UiThreadDispatcherFixture.ParkedDispatcher` that captures `Dispatcher.CurrentDispatcher` and parks on a `ManualResetEventSlim` forever (never pumps). | +| `EnsureScope` (private sealed) | 249-274 | Fields `_installed` (the dispatcher this scope wrote, or `null`), `_disposed`. `Dispose` is idempotent; if `_installed != null` calls `CompareExchange(_installed, null)`. **Releasing the "last" pin is not a concept here: there is no count. The scope that installed nulls the field; any scope that installed nothing does nothing.** No dispatcher is shut down; the parked thread lives for the process. | +| `UiThreadDispatcherTransaction` | 284-341 | `Install` (one-shot, `Exchange`), `Dispose` (restore via `CompareExchange(_installedValue, _previous)` then release gate; idempotent). | + +What "releasing the pin" does today, precisely: it writes `null` into `UiThread._dispatcher` if and only if (a) this scope was the one that installed the parked instance and (b) the field still holds that exact instance. It never nulls a transaction's value and never touches the parked thread. + +### 1.2 `QfcItemControllerTestSupport.EnsureUiThreadDispatcher` (`QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs`, 440 content lines) + +- `:238-239`: `internal static IDisposable EnsureUiThreadDispatcher() => UiThreadDispatcherFixture.EnsureDispatcher();` (pure forwarder). +- Doc `:216-237`: says the helper is "Needed for members that still delegate to a callee using the static `UiThread.Dispatcher` before the Phase 6 `IUiDispatcher` seam replaces it" and that "Discarding the scope is permitted and leaks exactly as the pre-issue-#493 `void` helper did, no more." +- `:161-168` (remarks on `BuildColorTheme`): "Cycle-3: the parameterless `Theme` constructor leaves `_uiDispatcher` null ... `SetThemeDark`/`SetThemeLight` route through `Theme.SetQfcTheme(async: true)`, which now reads `_uiDispatcher`, so a non-executing dispatcher mock ... is injected here". This comment is the in-code record that the theme path stopped reading the static. +- Other dispatcher-related helpers in the same file: `EnsureSynchronizationContext` (:90-96, installs a plain `SynchronizationContext` on the current thread when none exists, never restores), `StartRunningDispatcher` (:251-271, dedicated running STA dispatcher), `ShutdownDispatcher` (:277-280). + +### 1.3 Every member in the solution that reads or writes the shared static `UiThread._dispatcher` + +Production reader/writer (`UtilitiesCS/Threading/UiThread.cs`): +- `UiThread.Dispatcher` getter `:266-284` reads `_dispatcher` once and **throws `InvalidOperationException(DispatcherNotInitializedMessage)` when null** (:273-280); private setter `:283`. +- `Initialize()` `:82` writes it (W5 in #950's numbering); `ResetForTesting()` `:126` nulls it (W6; called only from `UtilitiesCS.Test`). + +QuickFiler.Test readers/writers (all through the fixture; the fixture is `internal`, and a repo-wide grep for `UiThreadDispatcherFixture|UiThreadDispatcherTransaction` over `*.cs` hits only `QuickFiler.Test` files plus one doc-comment path mention in `UtilitiesCS.Test/TestHelpers/UiThreadDispatcherScope.cs:40`): +- `UiThreadDispatcherFixture.Current` reads: `EmailMoveMonitorTests.cs:52,61`; `UiThreadDispatcherFixtureTests.cs:55,61,63,115,120,122,170,225,241`. +- Writers: `EnsureDispatcher` (:132), `EnsureScope.Dispose` (:271), `Transaction.Install` (:316 via `Exchange`), `Transaction.Dispose` (:336 via `CompareExchange`). +- Transaction holders (gated, correct): `UiThreadDispatcherFixtureTests.cs:50,110,160,218,291,338,380,422`; `WpfUiDispatcherTests.cs:59`; `QfcHomeControllerRunAsyncTests.cs:354`; `QfcFormControllerUndoHandoffTests.cs:230,281,337`; `QfcItemController.InitializationTests.Part2.cs:53` (released by `PumpHarness.Restore` :317-330). + +Other test assemblies own their own, different mechanisms over the same static: `UtilitiesCS.Test/TestHelpers/UiThreadDispatcherScope.cs` (:116-117 reflects `_dispatcher`) and `UiThreadStateScope.cs` (:34, :192). They never reference the QuickFiler fixture. #950's research (`.../950/research/2026-10-01T00-00-wall-clock-waits-research.md:129`, W7) records that each test assembly runs in its own AppDomain under the MSTest adapter on .NET Framework, so those statics are per-assembly; that closure was not re-verified here and is cited, not asserted. + +## 2. Exhaustive call-site enumeration + +### 2.1 Numeric Derivation Evidence: call sites of the pin helper + +- **Complete Family:** every invocation expression of `QfcItemControllerTestSupport.EnsureUiThreadDispatcher()` and of `UiThreadDispatcherFixture.EnsureDispatcher()` in C# source anywhere in the repository (all projects, test and production). +- **Exhaustive Search Scope:** `/**/*.cs` (Grep tool, ripgrep, glob `*.cs`, no path restriction). +- **Inclusion Rules:** a line containing the identifier followed by `()` that is an executable statement or expression (not inside `///` or `//`). +- **Exclusion Rules:** method declarations, ``/doc-comment mentions, test-method names that embed the identifier as a prefix. +- **Primary Search Strategy or Query Expression:** content grep `EnsureUiThreadDispatcher\(\)|EnsureDispatcher\(\)` over `*.cs`. +- **Primary Member Set (9 matching lines; 7 invocations after exclusions):** + - Declarations excluded: `QfcItemController.UiThreadDispatcherFixture.cs:122`, `QfcItemController.TestSupport.cs:238`. + - Invocations: `QfcItemController.TestSupport.cs:239` (forwarder calls `EnsureDispatcher()`); `QfcItemController.UiThreadDispatcherFixtureTests.cs:60`, `:119`, `:166`, `:222`; `QfcItemController.FocusAndThemeTests.cs:452`, `:468`. +- **Primary Count:** 7 invocations (6 of `EnsureUiThreadDispatcher()`, 1 of `EnsureDispatcher()`). +- **Cross-check Search Strategy or Query Expression:** count-mode grep of the bare identifiers `EnsureUiThreadDispatcher|EnsureDispatcher` over `*.cs` (catches calls written across lines or without parentheses), then classify each occurrence by reading the line. +- **Cross-check Member Set (20 occurrences in 5 files):** + - `UiThreadDispatcherFixture.cs` 5: lines 26 (``), 27 (doc), 122 (declaration), 195 (doc), 244 (``) → 0 invocations. + - `TestSupport.cs` 2: 238 (declaration), 239 (invocation) → 1. + - `InitializationTests.Part2.cs` 1: line 124 (comment) → 0. + - `UiThreadDispatcherFixtureTests.cs` 10: 44, 107, 157 (test names), 70, 128, 198 (doc/`because` text), 60, 119, 166, 222 (invocations) → 4. + - `FocusAndThemeTests.cs` 2: 452, 468 (invocations) → 2. +- **Cross-check Count:** 7 invocations. +- **Member-set Comparison:** identical sets ({TestSupport:239, FixtureTests:60,119,166,222, FocusAndThemeTests:452,468}); counts agree (7 = 7). The assertion "six test-side call sites of `EnsureUiThreadDispatcher()` plus the single forwarder" is supported. +- **Non-vacuity control:** the same primary pattern applied to the fixture test file alone returns 4 lines; the pattern `EnsureUiThreadDispatcher\(\)\.Dispose\(\)` returns 0 (no site disposes inline), and the control pattern `BeginTransactionAsync\(` returns 15 lines across 6 files (section 1.3), showing the search tool and glob were live. + +### 2.2 Classification of the six `EnsureUiThreadDispatcher()` test call sites + +| # | Site | Test | Usage | Correct? | +|---|---|---|---|---| +| 1 | `FocusAndThemeTests.cs:452` | `QfcItemController_FocusAndThemeTests.SetThemeDark_FromNormal_SelectsDarkNormalTheme` | (a) return value discarded | **No.** Writes W1 (seeds parked into a null field) and never reverts; the test does not read the static (section 3), so the call is also unnecessary. | +| 2 | `FocusAndThemeTests.cs:468` | `...SetThemeLight_FromNormal_SelectsLightNormalTheme` | (a) discarded | **No.** Same as #1. | +| 3 | `UiThreadDispatcherFixtureTests.cs:60` | R1 `EnsureDispatcher_WhileATransactionHoldsALiveDispatcher_DoesNotReplaceIt` | (b) held in a local, explicitly disposed at :62; the acquire-observe-dispose triple is the act under test, no throwing statement between :60 and :62 | Correct for its purpose (observing the field immediately after disposal is the assertion; a `using` would not allow that). Runs inside a gated transaction with `try/finally`. | +| 4 | `UiThreadDispatcherFixtureTests.cs:119` | R2 `EnsureDispatcher_WhenTheFieldIsNull_InstallsAndRestoresOnDispose` | (b) local, disposed at :121 | Correct, same reasoning. Note R2 **nulls the field** on dispose when the baseline was null: this is a W2 write that today can race any other unpinned holder. Under reference counting it is only reachable when R2 is the sole holder. | +| 5 | `UiThreadDispatcherFixtureTests.cs:166` | R3 `EnsureDispatcher_ScopeDisposedTwice_IsIdempotent` | (b) local, disposed at :169 and :171 | Correct (idempotency is the subject). Same W2 note as R2. | +| 6 | `UiThreadDispatcherFixtureTests.cs:222` | R4 `Transaction_SecondCallerCannotInstallUntilTheFirstRestores` | `using` (:221-223), taken after the gate is acquired | Correct. Its disposal is the W2 write #950's plan named in the stop marker (`.../950/plan.2026-10-01T07-11.md:134`). The surrounding `transactionA` is **not** in a `try/finally` (CR-5 in `.../950/code-review.2026-10-02T14-30.md:24`), see section 6. | + +### 2.3 Other touches of the shared dispatcher or related ambient state without a pin + +| Pattern | Hits | Assessment | +|---|---|---| +| `UiThreadDispatcherFixture.Current` outside the fixture tests | `EmailMoveMonitorTests.cs:52,61` | Read-only snapshot and teardown equality assertion. The class is `[DoNotParallelize]` (:24), so MSTest runs it outside the parallel phase; a pin is not required. Correct. | +| `Dispatcher.CurrentDispatcher` in test projects (grep `Dispatcher\.CurrentDispatcher` over `*.Test/**/*.cs`) | QuickFiler.Test: `ItemViewerBreadcrumbThreadAffinityTests.cs:48,181`; `QfcHomeControllerRunAsyncTests.cs:340`; `TestSupport/WinFormsPumpHostTests.cs:226`; `QfcItemController.TestSupport.cs:258`; `QfcItemController.UiThreadDispatcherFixture.cs:225`. UtilitiesCS.Test / TaskMaster.Test: 15 further hits (listed in the grep output; e.g. `UiThread_Tests.cs:363,473`, `ProgressViewer_Tests.cs:246`, `AppOlObjectsFolderTreeServiceLifecycleTests.cs:375`). | These obtain the *current thread's* WPF dispatcher; none writes `UiThread._dispatcher` except via the two fixture mechanisms already enumerated. Not in scope. | +| `SynchronizationContext.SetSynchronizationContext(` in test projects | 200+ hits across QuickFiler.Test, UtilitiesCS.Test, TaskMaster.Test (persisted grep output) | Per-thread ambient context, not the shared static. Out of scope for this issue; one touched-file nit is recorded in section 6 (`TestSupport.EnsureSynchronizationContext` never restores). | +| Transactions without `using`/`try-finally` | `QfcHomeControllerRunAsyncTests.cs:353-355` with `finally { transaction?.Dispose(); }` at :388 (correct); `InitializationTests.Part2.cs:53-65` with catch-dispose-rethrow and `PumpHarness.Restore` (correct); `WpfUiDispatcherTests.cs:60-96` `try/finally` (correct); `QfcFormControllerUndoHandoffTests.cs:230,281,337` `using` (correct); fixture tests R1,R2,R3,R5(partly),R6,#743,#882 `try/finally` (correct); **R4 (:218-251) no `try/finally`** (defect, section 6). | | + +## 3. How the production code under test consumes the dispatcher + +Trace for `SetThemeDark_FromNormal_SelectsDarkNormalTheme` (`FocusAndThemeTests.cs:447-462`): + +1. `new FocusController()` → `protected QfcItemController() { }` (`QuickFiler/Controllers/QfcItemController.Initialization.cs:27`): empty body, touches nothing. +2. `SetField(controller, "_themes", BuildAllThemes())`: `BuildAllThemes` (`FocusAndThemeTests.cs:41-55`) builds **one** `Theme` via `BuildColorTheme` and maps all four keys to it. `BuildColorTheme` (`TestSupport.cs:169-181`): `new Theme()` (`Theme.cs:141`, empty constructor; the only constructor that defaults `_uiDispatcher` to `new WpfUiDispatcher()` is the large overload at `Theme.cs:67`, not used here), three colours, then reflection-injects a `Mock` with `InvokeAsync(It.IsAny())` returning `Task.CompletedTask`. +3. `controller.SetThemeDark(async: true)` (`FocusAndTheme.cs:274-286`): `_activeTheme is null` → `_themes["DarkNormal"].SetQfcTheme(true)` then `_activeTheme = "DarkNormal"`. +4. `Theme.SetQfcTheme(bool async)` (`Theme.cs:427-445`): `if (async) { _uiDispatcher.InvokeAsync(() => SetQfcTheme()); }` → the mock returns a completed task and never runs the delegate. The commented-out line `:441` `//UiThread.Dispatcher.Invoke(() => SetQfcTheme());` is the former static read. +5. Assertion reads `_activeTheme` by reflection. + +`UiThread.Dispatcher` (`UiThread.cs:266-284`) is never evaluated on this path, so a null or foreign value in `UiThread._dispatcher` cannot throw here. The same holds for `SetThemeLight`. Had the path still read the static, the failure mode would be `InvalidOperationException("The UI dispatcher has not been captured. Call UiThread.Init() ...")` from the getter (`UiThread.cs:240-241`, `:279`), not a `NullReferenceException`. + +Members of `QfcItemController` that *do* reach the static exist (for example `QfcTipsDetails.ToggleAsync` via `UtilitiesCS/HelperClasses/ToolTips/QfcTipsDetails.cs:254,277`, which is why `InitializationTests.Part2.cs:121-132` installs the pump dispatcher under a transaction), but none is on the two theme tests' paths: `ToggleFocus*`/`ToggleTips*` tests in the same file go through `IItemViewer.Invoke` mocks and `EnableHandlelessThemeInvoke`, and `ToggleNavigationAsync` uses a `Mock` (:347-357). + +## 4. Candidate approaches and recommendation + +### Approach A (recommended): reference-count the fixture pin; delete the two dead theme-test calls + +Description: +- `UiThreadDispatcherFixture` gains, under `FieldLock`, `private static int _pinCount` and `private static bool _fixtureInstalledParked`. `EnsureDispatcher`: increment `_pinCount`; if the field is `null`, write parked and set `_fixtureInstalledParked = true`; return a scope. `EnsureScope.Dispose` (idempotent via `_disposed`): under `FieldLock`, decrement `_pinCount`; when it reaches 0 and `_fixtureInstalledParked` is true and the field still references the parked instance, write `null` and clear the flag (do the field operations inline under the same lock rather than via the re-locking `CompareExchange`, so the decrement and the write are one straight-line critical section, consistent with the `FieldLock` contract at `:16-18`). +- Semantics preserved: a scope that found a transaction's value installs nothing and, at count zero, nulls nothing (R1 keeps passing). A single pin on a null baseline still nulls on dispose (R2, R3 keep passing). R4 is unchanged in shape; its baseline disposal nulls only if no other pin is live, which is the property the issue asks for. +- Residual: if a transaction installs a live dispatcher while pins are held and the last pin releases during the transaction, the parked value is restored by the transaction and remains with zero pins (the pre-#968 leak shape, benign: a non-null parked dispatcher). Keeping `_fixtureInstalledParked` true until the null write actually succeeds lets the next pin cycle clean it up. Record in the class doc. +- `FocusAndThemeTests.cs:452` and `:468` are deleted together with the `// Arrange — async:true queues the theme application on the dispatcher ...` comment at :450-451 rewritten to say the theme's injected `IUiDispatcher` mock absorbs the queued delegate. The file shrinks (497 → 495 before other in-scope nits). +- Comment drift fixed in the fixture (`:116-121`, `:243-248`), the wrapper (`TestSupport.cs:216-237`), and R4's doc (`FixtureTests.cs:196-208`, whose W2 invariant is superseded by counting). + +Advantages: owns the shared state at its single mutation point (the fixture's own design goal, `:13-14`); protects every present and future pin holder, not only the theme tests; no new MSTest lifecycle attributes (none exist in `QuickFiler.Test` today: grep `\[ClassInitialize\]|\[ClassCleanup` returns 0); no file split; the regression test is single-threaded and deterministic (section 5); the theme tests stop depending on state they neither own nor need. + +Limitations: changes a shared test fixture used by four classes; requires the fixture tests R1-R3 to be re-read for intent (they pass under the new semantics by the analysis above, to be confirmed by running them). A discarded scope now holds a pin forever (count never returns to zero), which keeps the parked dispatcher installed for the rest of the process; that is the same end state as today's discard, so no caller regresses, but the doc must say "a discarded scope pins for the process lifetime" instead of "leaks exactly as the pre-fix helper did". + +Alignment: General Code Change Policy "Simplicity first" and "Reusability" (one fix at the shared seam instead of per-class ceremony); General Unit Test Policy Independence/Isolation (classes stop writing shared state they do not use). + +### Approach B: class-level pin in `QfcItemController_FocusAndThemeTests` (`[ClassInitialize]` acquire, `[ClassCleanup]` release), fixture unchanged + +Rejected as the primary fix because: (i) the theme tests do not read the static, so the pin protects nothing in that class and merely moves the W1 write to class start and the W2 write to class end, where it still races any other unpinned holder; (ii) without reference counting the class-end disposal nulls the field for every other class still relying on an unpinned ensure (exactly the exposure the issue describes, relocated); (iii) it requires a split of a 497-line file; (iv) `[ClassCleanup]` timing: `QuickFiler.Test` uses MSTest 4.4.1 (`QuickFiler.Test/packages.config:44-45`); the `ClassCleanupBehavior` default for that major version was not verified in this session and would have to be pinned explicitly (`ClassCleanupBehavior.EndOfClass`) to avoid holding the pin to end of assembly. If the orchestrator nevertheless requires the issue's literal proposal, it must be combined with Approach A's counting to be sound. + +### Approach C: per-test `using (QfcItemControllerTestSupport.EnsureUiThreadDispatcher())` in the two theme tests, fixture unchanged + +Rejected: same soundness gap as B (each test's disposal is a W2 write that can null the field under a concurrent unpinned holder), adds two lines to a 497-line file, and pins state the tests do not use. + +### Rejected alternative from #950 (not re-litigated): making `EnsureDispatcher` take the transaction gate + +Rejected by the fixture's design note (`:26-30`) and #950 research (`...research.md:152`): callers without `[Timeout]` would block without bound. + +## 5. Deterministic regression test design (fails before, passes after) + +Location: a **new** file `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs` (the existing fixture test file is 470 content lines and the CR-5 `try/finally` fix adds to it; a ~60-line addition would breach 500). Requires a new `` next to `QuickFiler.Test.csproj:203`. MSTest + FluentAssertions; Moq is not needed for the fixture-level tests (state it in the class doc rather than adding an unused `using`). Same `[Timeout(60000)]` convention as the sibling file (`FixtureTests.cs:33,43`). No `Thread.Sleep`, `Task.Delay`, timers, or files; no concurrency is required because the defect is observable on one thread. + +Test 1 (the regression; AC candidate): `EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease` +- Arrange: `using var transaction = await UiThreadDispatcherFixture.BeginTransactionAsync()`; `transaction.Install(null)` to force a known null baseline (the R2 pattern, `FixtureTests.cs:110-116`). `IDisposable pinA = QfcItemControllerTestSupport.EnsureUiThreadDispatcher(); IDisposable pinB = QfcItemControllerTestSupport.EnsureUiThreadDispatcher();` `Dispatcher afterBothPins = UiThreadDispatcherFixture.Current;` +- Act: `pinA.Dispose(); Dispatcher afterFirstRelease = UiThreadDispatcherFixture.Current; pinB.Dispose(); Dispatcher afterLastRelease = UiThreadDispatcherFixture.Current;` +- Assert: `afterBothPins.Should().NotBeNull()`; `afterFirstRelease.Should().BeSameAs(afterBothPins, because: "a holder that did not take the last pin must not lose the dispatcher")`; `afterLastRelease.Should().BeNull(because: "the last release reverts the fixture's own seeding")`. +- Fail-before (current code): `pinA` installed parked (`_installed = parked`), `pinB` is a no-op scope. `pinA.Dispose()` runs `CompareExchange(parked, null)` → field becomes null. The second assertion fails with FluentAssertions text of the form `Expected afterFirstRelease to refer to Dispatcher { ... Name = "UiThreadDispatcherFixture.ParkedDispatcher" } because a holder that did not take the last pin must not lose the dispatcher, but found .` Deterministic: no scheduling is involved. +- Pass-after: count 2 → 1 on `pinA.Dispose()`, field untouched; count 1 → 0 on `pinB.Dispose()`, field nulled. + +Test 2 (order independence of the count): same arrange, dispose `pinB` first then `pinA`; identical assertions. Before the fix `pinB.Dispose()` is a no-op (passes the middle assertion) but then `pinA.Dispose()` nulls (passes the last) — so this test passes before and after; it is a specification test, not the regression, and must be labelled as such in its doc comment. + +Test 3 (foreign value protection, extends R1): under a transaction that installed a live dispatcher from `StartRunningDispatcher`, take two pins and release both; `Current` must still be the live dispatcher (count reaches 0 but `_fixtureInstalledParked` is false). Passes before and after; keeps the install-ownership rule honest. + +Test 4 (counter hygiene, optional): after test 1's sequence, a fresh single pin on a null baseline still installs and its release still nulls (proves the flag was cleared). Passes before and after. + +Negative control to record in evidence: run test 1 against the unmodified fixture by fully qualified name with `/Settings:scripts/vscode/TaskMaster.cli.runsettings` and capture the failure message above; then run after the fixture change and capture `Passed`. + +On the issue's second idea ("show the theme test fails without the fix"): not achievable, section 3. The spec should state that the regression lives at the fixture level and that the theme tests are made independent of the static by removing the dead calls; the orchestrator should ratify that re-wording explicitly because it departs from the issue's proposed validation. + +## 6. Related defects in the same files (in scope per the maintainer directive) + +| # | File:lines | Defect | Proposed in-scope change | +|---|---|---|---| +| D1 | `UiThreadDispatcherFixture.cs:116-121`, `:243-248` | Comment drift once counting exists ("Disposing the returned scope is optional: a discarded scope leaks exactly as the pre-fix helper did"; "A scope that installed nothing carries null and is a no-op"). | Rewrite to describe pin counting, install ownership, and the discard consequence. Also extend the class doc (`:12-31`) with the counting rule under `FieldLock`. | +| D2 | `TestSupport.cs:216-237` | Same drift in the wrapper doc; also states the helper is "Needed for members that still delegate to a callee using the static" and "Becomes moot once the callee routes through the injectable dispatcher seam" while the theme callers it was written for no longer read the static (`:161-168`). | Rewrite the ``/`` to describe the counted pin and name the remaining legitimate callers (the fixture tests). | +| D3 | `UiThreadDispatcherFixtureTests.cs:196-208` (R4 doc) | "Invariant for future editors: no other class may dispose an ensure scope holding the parked dispatcher (W2)" becomes false/obsolete under counting (another class's release can no longer null while R4 holds its pin). | Replace the W2 sentence with the counting guarantee; keep the W5 (`UiThread.Initialize`) residual. | +| D4 | `UiThreadDispatcherFixtureTests.cs:218-251` (R4) | `transactionA` is disposed only at :251 with no `try/finally`; a throw from `Install`, the pin, or `secondCallerStarted.Wait()` would hold the gate until the 120 s bound or the #743 counter test surfaces it (CR-5 of the #950 review; R1/R5/R6 use `try/finally`). Same file is touched by D3. | Wrap in `try/finally` with an idempotent re-dispose (R5 proves double dispose is safe). Adds ~6 lines (470 → ~476). Check that the already-filed #972 (950 review residuals) does not also carry CR-5, to avoid a duplicate fix; if it does, record that #968 delivers it and #972 should drop it. | +| D5 | `FocusAndThemeTests.cs:99-115` | Private `BuildExecutingViewer()` duplicates `QfcItemControllerTestSupport.BuildExecutingViewer()` (`TestSupport.cs:289-305`), whose own doc at `:282-288` says it "mirrors the private static ... which is not reachable from another test file". Copy-paste in a touched file at 497/500 lines. | Delete the private copy and call the shared helper (17 lines removed; also fix the shared helper's doc sentence). Pure test-quality change; no behaviour difference (both execute `Invoke`/`BeginInvoke` synchronously). | +| D6 | `FocusAndThemeTests.cs:450-451` | Arrange comment says "async:true queues the theme application on the dispatcher without executing it" without naming that the dispatcher is the theme's injected mock, which is why the shared static is irrelevant. | Reword when deleting :452/:468. | +| D7 | File sizes (content lines): `FocusAndThemeTests.cs` 497, `UiThreadDispatcherFixtureTests.cs` 470, `TestSupport.cs` 440, `UiThreadDispatcherFixture.cs` 342. | Two files are within 30 lines of the 500 limit; additive designs (B, C, or new tests in the fixture test file) breach it. | Approach A reduces `FocusAndThemeTests.cs`; new tests go to a new file (section 5); fixture growth (~25 lines) stays well under 500. Record post-change counts in evidence. | +| D8 (observed, recommend no change) | `TestSupport.cs:90-96` `EnsureSynchronizationContext` | Installs a plain `SynchronizationContext` on the MSTest worker thread and never restores it; documented as deliberate and relied on by handler tests. Not the dispatcher static and not this issue's root cause. | Leave; note in spec as observed and intentionally untouched (changing it would alter the premise of unrelated tests, #950 recorded the ambient context as an execution-time observation). | + +## 7. Build and test facts the plan needs + +- Test project: `QuickFiler.Test/QuickFiler.Test.csproj`, legacy (non-SDK) project, `v4.8.1` (:17), explicit `` items. Relevant entries: `Controllers\QfcItemController.TestSupport.cs` (:200), `Controllers\QfcItemController.UiThreadDispatcherFixture.cs` (:201), `Controllers\QfcItemController.UiThreadDispatcherFixtureTests.cs` (:203), `Controllers\QfcItemController.FocusAndThemeTests.cs` (:212). **A new test file needs its own ``**; `*.csproj` is excluded from CSharpier by `.csharpierignore`. +- Packages (`QuickFiler.Test/packages.config`): MSTest.TestAdapter/TestFramework 4.4.1 (:44-45), Moq 4.21.0 (:42), FluentAssertions 8.11.0 (:8). No `[ClassInitialize]`/`[ClassCleanup]` is used anywhere in `QuickFiler.Test` today. +- Namespace/visibility: fixture and support types are `internal` in `QuickFiler.Controllers.Tests`; tests in the same assembly reach them directly. +- Test-run route: `scripts/vscode/Invoke-MSTestWithCoverage.ps1`. Inner vstest arguments (`:82-94`): `/Settings:scripts/vscode/TaskMaster.cli.runsettings`, `/InIsolation`, `/TestCaseFilter:TestCategory!=LiveOutlook`, `/ResultsDirectory:coverage\test-results`, `/Logger:trx;LogFileName=mstest-coverage-run.trx` (fixed names `:297-298`, not overridable). Assemblies discovered as `*.Test.dll` under `bin\Debug` excluding `obj`, `ref`, and `.claude` segments (`:348-355`). Coverage settings from repo-root `coverage.config` with a derived test-assembly exclusion (`:97-134`). The run throws `MSTest with coverage failed with exit code N` on any test failure (`:262`) before post-processing. +- Runsettings: `scripts/vscode/TaskMaster.cli.runsettings` sets `0` and `ClassLevel` (:4-7) and no data collector. The repo-root `TaskMaster.runsettings` (Visual Studio auto-detect) carries the coverage exclusions; not used by the CLI route. +- Assembly init: `QuickFiler.Test/SetupAssemblyInitializer.cs:14-25` installs an assembly resolver and WinForms defaults; it does not write `UiThread._dispatcher`, so a class run alone starts with a null baseline (relevant to the fail-before run of section 5). +- Fully qualified names for targeted runs: `QuickFiler.Controllers.Tests.QfcItemController_FocusAndThemeTests.SetThemeDark_FromNormal_SelectsDarkNormalTheme`, `...SetThemeLight_FromNormal_SelectsLightNormalTheme`, `QuickFiler.Controllers.Tests.QfcItemController_UiThreadDispatcherFixtureTests.*` (R1-R6, #743, #882), and the new `QuickFiler.Controllers.Tests.QfcItemController_UiThreadDispatcherPinCountTests.*`. +- Concurrency check for the plan (mirrors #950 P4-T5): run the fixture test class, the new pin-count class, and `QfcItemController_FocusAndThemeTests` in one invocation under the CLI runsettings and require all `Passed`; this is a supporting observation, not the regression gate (MSTest cannot be made to interleave classes on demand). + +## 8. Unrelated defects observed (for filing only; not in this item's scope) + +None new. Two items already recorded elsewhere were re-observed and need no new issue: `QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs:17` comment drift (CR-2 of the #950 review, promoted with the 950 residuals to #972 per the branch's recent commit `89f0283c1`), and the `.claude/agent-memory/parallel-orchestrator/*` uncommitted modifications shown in the starting `git status` (session hygiene, not code). + +## 9. Testing implications summary + +- Regression gate: section 5 test 1 fails on the unmodified fixture with the quoted message and passes after reference counting; evidence under `FEATURE/evidence/regression-testing/`. +- Existing fixture tests R1-R6, #743 and #882 must still pass unchanged in behaviour (R1-R3 are the semantic guard for "installed nothing" and "single pin on null baseline"). +- `EmailMoveMonitorTests` (`[DoNotParallelize]`, snapshot/equality on `Current`) is unaffected: counting never writes outside pin acquire/release. +- Theme tests pass with the calls removed (section 3 trace); run them alone and in the concurrent set. +- Coverage: all changes are in test assemblies, which the coverage route excludes from instrumentation; first-party line/branch figures should be unchanged within run-to-run noise. Record baseline and post-change figures per the fail-closed evidence rule. +- Prohibited constructs: no `[DoNotParallelize]`, `Workers=1`, retries, `Thread.Sleep`, `Task.Delay`, temporary files, or timeout changes are needed or proposed. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T22-20-qfc-datamodel-972-fold-research.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T22-20-qfc-datamodel-972-fold-research.md new file mode 100644 index 000000000..a6780fda5 --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T22-20-qfc-datamodel-972-fold-research.md @@ -0,0 +1,415 @@ +# Research addendum: #972 fold and liveness-test determinism (issue #968) + +- Issue: #968 `focus-and-theme-tests-leak-shared-dispatcher-setup`, with #972 (five residuals of the #950 review) folded in by the Coordinator Scope Amendment (`issue.md`, section "Coordinator Scope Amendment (2026-10-02T22-15, binding)"). +- Branch: `bug/focus-and-theme-tests-leak-shared-dispatcher-setup-968`, item worktree head `4c6de5e84`, base `94287369`. +- Date: 2026-10-02 +- Scope of this record: research questions (a) to (f) of the fold only. The #968 core design (fixture pin counting, the two dead theme-test calls, D1-D8) is in `research/2026-10-02T05-50-dispatcher-pin-call-sites-research.md` and is cited, not repeated. #972 item 5 (`try`/`finally` around `transactionA` in R4) is already designed there as D4; section 7 of this record only confirms it. +- Method: every file:line below was re-read in the item worktree during this session with the Read and Grep tools. Web-sourced facts are marked `[V-web]`. The Bash tool was not surfaced in this session, so `git -C log --oneline 94287369..HEAD -- QuickFiler QuickFiler.Test` could not be executed here; the orchestrator's statement that main has not touched `QuickFiler/` or `QuickFiler.Test/` since the base is therefore carried as an orchestrator-supplied fact, not re-verified. All citations were taken from the worktree's current files, so they are valid for the worktree regardless. +- Paths are repository-relative. `FEATURE` = `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968`. + +## 1. (a) The three `SynchronousBackgroundWorker` helpers + +### 1.1 Declarations, quoted in full + +`QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs:47-60`: + +```csharp + /// + /// Test-side worker whose raises DoWork synchronously on + /// the calling thread through the protected OnDoWork, so the privately subscribed + /// Worker_DoWork runs to its first incomplete await before InitEmailQueue + /// returns. Issue #950: this replaces the bounded waits on a thread-pool worker. + /// + private sealed class SynchronousBackgroundWorker : BackgroundWorker + { + public void RaiseDoWork() => OnDoWork(new DoWorkEventArgs(null)); + } + + /// The synchronous starter assigned to QfcDatamodel.WorkerStarter. + private static void StartSynchronously(BackgroundWorker worker) => + ((SynchronousBackgroundWorker)worker).RaiseDoWork(); +``` + +`QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs:59-73`: + +```csharp + /// + /// Test-side worker whose raises DoWork synchronously on + /// the calling thread through the protected OnDoWork, so the privately subscribed + /// Worker_DoWork runs to its first incomplete await before InitEmailQueue + /// returns (issue #950). Duplicated per file, following the convention documented on + /// QfcDatamodelLivenessTests. + /// + private sealed class SynchronousBackgroundWorker : BackgroundWorker + { + public void RaiseDoWork() => OnDoWork(new DoWorkEventArgs(null)); + } + + /// The synchronous starter assigned to QfcDatamodel.WorkerStarter. + private static void StartSynchronously(BackgroundWorker worker) => + ((SynchronousBackgroundWorker)worker).RaiseDoWork(); +``` + +`QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs:114-127`: + +```csharp + /// + /// Test-side worker whose raises DoWork synchronously on + /// the calling thread through the protected OnDoWork, so no worker started by + /// InitEmailQueue outlives the test (issue #950). Duplicated per file, following + /// the convention documented on QfcDatamodelLivenessTests. + /// + private sealed class SynchronousBackgroundWorker : BackgroundWorker + { + public void RaiseDoWork() => OnDoWork(new DoWorkEventArgs(null)); + } + + /// The synchronous starter assigned to QfcDatamodel.WorkerStarter. + private static void StartSynchronously(BackgroundWorker worker) => + ((SynchronousBackgroundWorker)worker).RaiseDoWork(); +``` + +### 1.2 Diff + +- Code: the three class bodies (`private sealed class SynchronousBackgroundWorker : BackgroundWorker { public void RaiseDoWork() => OnDoWork(new DoWorkEventArgs(null)); }`) and the three `StartSynchronously` starters are byte-identical. Members: one public method each, no fields, no constructor, no `Dispose` override. +- Doc comments differ in prose only (Liveness: "Issue #950: this replaces the bounded waits on a thread-pool worker"; Teardown and ZeroBatch: "Duplicated per file, following the convention documented on QfcDatamodelLivenessTests"; ZeroBatch additionally phrases the purpose as "so no worker started by InitEmailQueue outlives the test"). +- Behaviour: identical. `OnDoWork` is `protected virtual` on `System.ComponentModel.BackgroundWorker`; raising it directly invokes the `DoWork` subscribers (here `QfcDatamodel.Worker_DoWork`, subscribed at `QuickFiler/Controllers/QfcDatamodel.cs:193`) on the calling thread and never starts a thread-pool work item. + +### 1.3 Every use site (repo-wide grep `SynchronousBackgroundWorker` over `*.cs`: 13 lines in 3 files, all in `QuickFiler.Test/Controllers/`) + +| File | Line | Use | +|---|---|---| +| `QfcDatamodelLivenessTests.cs` | 53 | declaration | +| | 60 | cast inside `StartSynchronously` | +| | 127 | `var worker = new SynchronousBackgroundWorker();` (test 1) | +| | 201 | `var worker = new SynchronousBackgroundWorker();` (`StartHeldOpenLoader`, used by tests 2, 3, 4) | +| `QfcDatamodelTeardownTests.cs` | 66 | declaration | +| | 73 | cast inside `StartSynchronously` | +| | 220 | `using (var worker = new SynchronousBackgroundWorker())` | +| `QfcInitEmailQueueZeroBatchTests.cs` | 33 | doc comment ("the nested SynchronousBackgroundWorker") | +| | 120 | declaration | +| | 127 | cast inside `StartSynchronously` | +| | 148 | `model.InitEmailQueue(0, new SynchronousBackgroundWorker())` inline | +| | 173 | `var worker = new SynchronousBackgroundWorker();` | +| | 221 | `model.InitEmailQueue(2, new SynchronousBackgroundWorker())` inline | + +`StartSynchronously` assignments: Liveness `:128`, `:202`; Teardown `:222`; ZeroBatch `:143`, `:172`, `:202`. + +### 1.4 Recommendation: one shared helper + +- Location: new file `QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs`. The project's existing shared test helpers live there (`TestSupport/WinFormsPumpHost.cs`, `TestSupport/DedicatedWorkerThread.cs`, both `internal` in namespace `QuickFiler.Test.TestSupport`, see `DedicatedWorkerThread.cs:4,21`). No existing test-support file is a natural host: `QfcItemController.TestSupport.cs` is item-controller specific (440 lines) and `BayesianPerformanceController.TestSupport.cs` / `QfcCollectionController.TestSupport.cs` are controller specific. A new file keeps the helper discoverable and avoids growing a near-limit file. +- Namespace and name: `QuickFiler.Test.TestSupport.SynchronousBackgroundWorker`. The three consumer files (namespace `QuickFiler.Controllers.Tests`) add `using QuickFiler.Test.TestSupport;`, the convention already used by six files (for example `Controllers/QfcItemController.SeamFactoryTests.cs`, `Controllers/QfcItemController.ViewerSetupTests.cs`). +- Shape: `internal sealed class SynchronousBackgroundWorker : BackgroundWorker` with `internal void RaiseDoWork()` and the starter moved onto the class as `internal static void StartSynchronously(BackgroundWorker worker) => ((SynchronousBackgroundWorker)worker).RaiseDoWork();`, so each consumer replaces `model.WorkerStarter = StartSynchronously;` with `model.WorkerStarter = SynchronousBackgroundWorker.StartSynchronously;` and deletes both its nested class and its private starter. Carry the Liveness doc comment (the fullest of the three) and drop the "Duplicated per file" sentences. +- `IDisposable`: `BackgroundWorker` derives from `System.ComponentModel.Component`, which implements `IDisposable` through the `Dispose()` / `protected virtual Dispose(bool)` pattern. The helper adds no fields, handles or subscriptions, so it must not override `Dispose(bool)`: there is nothing to release and an empty override would only invite analyzer noise. Disposal is the caller's responsibility (section 4). +- `QuickFiler.Test/QuickFiler.Test.csproj` (legacy non-SDK project, explicit `Compile` items; `*.csproj` is excluded from CSharpier by `.csharpierignore`). Neighbouring lines today: + +```xml +226 +227 +228 +229 +230 +``` + + Add after line 228: ` `. The three consumer entries stay unchanged: `Controllers\QfcDatamodelLivenessTests.cs` (:157), `Controllers\QfcInitEmailQueueZeroBatchTests.cs` (:161), `Controllers\QfcDatamodelTeardownTests.cs` (:183). (The original research already requires a new entry next to :203 for the pin-count test file; both additions go in the same csproj edit.) + +### 1.5 Line counts (physical lines, last `}` line of each file as read) + +| File | Before | Expected after (estimate) | Basis | +|---|---|---|---| +| `QfcDatamodelLivenessTests.cs` | 312 | about 325 | minus 14 (helper + starter + blanks), minus about 10 (the two `Advance`/`Yield` blocks of test 1), plus about 15 (the `ArmingFakeTimeProvider` nested helper, section 5) and about 20 (test 1 rewrite, `using` blocks for the workers, helper-signature change) | +| `QfcDatamodelTeardownTests.cs` | 244 | about 229 | minus 15 (helper + starter + blanks); disposal already present | +| `QfcInitEmailQueueZeroBatchTests.cs` | 232 | about 225 | minus 14 (helper + starter), plus about 7 (three `using` blocks) | +| new `TestSupport/SynchronousBackgroundWorker.cs` | 0 | about 30 | class, starter, doc | + +All stay far below 500. The executor records the exact counts in evidence. + +## 2. (b) The `_remainingLoadActive` comment + +### 2.1 Current comment, `QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs:15-24` + +```csharp + /// + /// Issue #424: honest producer-liveness signal. Set immediately before + /// each RunWorkerAsync() call and cleared in a finally once the awaited + /// RemainingEmailLoader completes. BackgroundWorker.IsBusy cannot serve this + /// role: Worker_DoWork is async void, so it returns at its first yielding await + /// and reports idle while the loader is still producing. Both the dequeue gate's + /// sourceActive signal and consume this flag. Declared + /// volatile because it is written on the worker thread and read by dequeue callers. + /// + private volatile bool _remainingLoadActive; +``` + +The #950 review recorded the drift as CR-2 / F-2: "reword the `_remainingLoadActive` doc comment in QfcDatamodel.QueueProcessing.cs to name `WorkerStarter`" (`docs/features/active/2026-09-30-quickfiler-tests-depend-on-wall-clock-timing-950/code-review.2026-10-02T14-30.md:52`). + +### 2.2 Every read and write across the solution (grep `_remainingLoadActive` over `*.cs`: 12 lines) + +Production (3 writes, 2 reads): +- Write `true`: `QfcDatamodel.cs:284` (zero-batch path of `InitEmailQueue`, immediately before `WorkerStarter(worker)` at :285) and `:311` (positive-batch path, before `WorkerStarter(worker)` at :312). +- Write `false`: `QfcDatamodel.cs:227`, in the `finally` of `Worker_DoWork` (:221-228) that wraps `e.Result = await loaderTask` (:219); runs on success and on throw. +- Read: `QfcDatamodel.QueueProcessing.cs:305` (`() => _remainingLoadActive`, the `sourceActive` lambda handed to `QfcStreamingDequeueConfidenceGate`, consumed at `QfcStreamingDequeueConfidenceGate.cs:246`) and `:406` (`WaitForQueue` loop condition). + +Tests (reflection by name only; no production API exposes the flag): `QfcDatamodelLivenessTests.cs:169` (`GetField("_remainingLoadActive")`), `QfcDatamodelTests.cs:114,126,266,279`, `QfcQueuePurePathsTests.cs:246`, `QfcHomeControllerRunAsyncHighConfidenceTests.Part3.cs:116` (all `SetPrivateField(model, "_remainingLoadActive", ...)`). + +### 2.3 Actual post-#950 behaviour + +- The flag is set to `true` by `InitEmailQueue` on the caller's thread, which in production is the `QfcHomeController.Run()` caller (`QfcHomeController.cs:252`) or a `Task.Run` pool thread via `InitEmailQueueAsync` (`QfcDatamodel.cs:330`), and in tests the MSTest worker thread. It is set immediately before `WorkerStarter(worker)`, not before "each `RunWorkerAsync()` call": since #950 the start goes through the injectable `WorkerStarter` seam (`QfcDatamodel.cs:144-152`), whose production value calls `RunWorkerAsync()` (:41, :53) and whose test value raises `DoWork` synchronously on the calling thread. +- It is cleared on the continuation thread of the awaited loader task, which is whichever thread completes `RemainingEmailLoader`'s task (a pool thread in production; in the liveness tests the test thread or a drained test-owned context). "Written on the worker thread" is therefore no longer accurate in either environment; what remains true is that writer and readers are different threads with no shared lock, which is the reason for `volatile`. +- The two consumers are unchanged. + +### 2.4 Proposed replacement (summary body; the executor wraps to the file's column width) + +```csharp + /// + /// Issue #424 producer-liveness signal, read by the dequeue gate's sourceActive and by + /// . InitEmailQueue sets it just before handing the worker to + /// , and Worker_DoWork clears it in a finally when the + /// awaited task completes, because BackgroundWorker.IsBusy + /// already reads idle at that handler's first incomplete await (issue #950 made the start + /// synchronous in tests, so no particular thread owns either write). Volatile: the writers and + /// the readers share no other fence. + /// +``` + +Three sentences; states why the flag exists, why `IsBusy` cannot replace it, and why it is volatile, without restating the code. + +## 3. (c) Legacy members in `QuickFiler/Controllers/QfcDatamodel.cs` (495 lines) + +### 3.1 Candidates enumerated (every member of the file reviewed against callers) + +Kept, with the proof that they are live: +- `Complete` (:158-163), `MovedItems` (:165-168), `InitEmailQueueAsync` (:317-333), `UndoMove` and `DequeueNextItemGroup(int)` (QueueProcessing :141, :339): all are members of `IQfcDatamodel` (`QuickFiler/Interfaces/IQfcDatamodel.cs:138-148`), so they implement interface members and are not unused regardless of direct call counts. `InitEmailQueueAsync` is also called at `QfcHomeController.cs:282`; `Complete` at `QfcHomeController.Iteration.cs:16` and `QfcFormController.EventHandlers.cs:276`; `DequeueNextItemGroup` at `QfcHomeController.Iteration.cs:78`. +- `Token`, `TokenSource` (:170-182): written by `LoadAsync` (:67-68) and read throughout. +- `SetupWorker` (:188-195), `Worker_DoWork` (:197-241), `InitEmailQueue` (:271-315), `LoadRemainingEmailsToQueueAsync(CancellationToken)` (:335-376, the one-argument overload that the constructors assign to `RemainingEmailLoader` at :40 and :52), `Application_NewMailEx` (:476-491), `Cleanup` (:77-103), `LoadAsync` (:56-75), both constructors: live. +- Commented-out field assignments `//_blockingQueue = null;` etc. (:99-101) are comments, not members. + +Candidates that appear unused ("legacy"): four, examined in section 3.2. + +### 3.2 Caller proof per candidate + +1. `log` (:109-111), a second `private static readonly log4net.ILog` alongside `logger` (:28-30). Private, so reachable only from the three partial files or by reflection. Grep `\blog\b` over `QuickFiler/Controllers/QfcDatamodel*.cs`: three hits, the declaration at `QfcDatamodel.cs:109` and two prose words ("log level", "field log") in doc comments at `QueueProcessing.cs:71,90`. Repo-wide grep `"log"|GetField\("log|nameof\(log\)` over `*.cs`: no matches. Zero readers. +2. `Worker_RunWorkerCompleted` (:243-265, including its three-line comment). Grep over `*.cs`: the only references in `QuickFiler` are the declaration and the commented-out subscription at `:194` (`//worker.RunWorkerCompleted += new ...(Worker_RunWorkerCompleted);`). `QfcHomeController.cs:92,132,344,379` and `QfcHomeControllerRunAsyncTests.cs:325,376` concern a different method of the same name on `QfcHomeController`: the test's `GetMethod("Worker_RunWorkerCompleted", NonPublic | Instance)` is invoked on `_controller` (`QfcHomeControllerRunAsyncTests.cs:373-380`), a `QfcHomeController`. Zero subscribers, zero reflective callers. +3. `LoadRemainingEmailsToQueue(BackgroundWorker, CancellationToken)` (:378-416, synchronous). Grep `LoadRemainingEmailsToQueue\b` over `*.cs`: the declaration, the commented-out call at `:210`, two commented `nameof` uses (`:363`, `:404`), and two live `nameof(LoadRemainingEmailsToQueue)` expressions in log strings at `:369` and `:410`. `:410` is inside the method itself. `:369` is inside the live one-argument `LoadRemainingEmailsToQueueAsync` and is a compile-time symbol reference, not an invocation; it must be retargeted to `nameof(LoadRemainingEmailsToQueueAsync)` in the same edit (which also corrects the log line, which today names the wrong method). Zero invocations. +4. `LoadRemainingEmailsToQueueAsync(BackgroundWorker, CancellationToken)` (:418-465, the two-argument overload with the `#pragma warning disable CS0618` block). Grep `LoadRemainingEmailsToQueueAsync` over `*.cs`: `:40` and `:52` are method-group conversions to `Func>`, which overload resolution binds to the one-argument overload only (the two-argument overload is not applicable to a one-parameter delegate type); `:130` is a `` naming the one-argument overload explicitly; `:209` is a commented-out call to the two-argument overload; `:462` is a commented `nameof` inside the method; the two test-file hits (`QfcInitEmailQueueZeroBatchTests.cs:28`, `QfcDatamodelLivenessTests.cs:104`) are doc prose about the one-argument loader. Zero invocations. + +Cross-cutting checks: `QuickFiler` grants `InternalsVisibleTo("QuickFiler.Test")` (`QuickFiler/Properties/AssemblyInfo.cs:5`, `QuickFiler/Controllers/QfcHomeController.cs:15`) and `DynamicProxyGenAssembly2` (`QfcHighConfidencePreFilter.cs:11`, `Legacy/IAcceleratorCallbacks.cs:5`); all four candidates are `private`, so neither grant exposes them. The reflective sweep of the datamodel test files (`GetMethod("|GetField("|GetProperty("` over `QuickFiler.Test/Controllers/QfcDatamodel*.cs`) names only `ToggleOfflineMode`, `WaitForQueue` and `_remainingLoadActive`. The partial siblings are `QfcDatamodel.FrameBuilding.cs` and `QfcDatamodel.QueueProcessing.cs` (Glob `QuickFiler/Controllers/QfcDatamodel*.cs`); neither references any candidate. No designer file belongs to the type. + +## Numeric Derivation Evidence + +- Complete Family: log, Worker_RunWorkerCompleted, LoadRemainingEmailsToQueue, LoadRemainingEmailsToQueueAsync-BackgroundWorker-overload +- Exhaustive Search Scope: every `*.cs` file in the entire repository (all production, test, designer and notes files in every project), plus an extension-unfiltered sweep of the entire repository for the method names, plus the `IQfcDatamodel` interface, both `QfcDatamodel.*.cs` partial siblings, and the `InternalsVisibleTo` grants of `QuickFiler` +- Inclusion Rules: a member is counted as caller-free when every occurrence of its name outside its own declaration and body is a comment, a commented-out statement, a doc-comment reference, a reference to a different type's member of the same name, or a `nameof` symbol reference that is not an invocation and that the same edit retargets +- Exclusion Rules: members that implement an `IQfcDatamodel` interface member, members with any live invocation, event subscription, reflection-by-string lookup against `QfcDatamodel`, override, or designer wiring are excluded from the caller-free set; the one-argument `LoadRemainingEmailsToQueueAsync(CancellationToken)` overload is excluded because the constructors assign it to `RemainingEmailLoader` +- Primary Search Strategy or Query Expression: content grep with the Grep tool over every `*.cs` file in the entire repository using the alternation `Worker_RunWorkerCompleted|LoadRemainingEmailsToQueue\b|LoadRemainingEmailsToQueueAsync|InitEmailQueueAsync|TryUnhookOrReplace|UndoMove|MovedItems|DequeueNextItemGroup\(` plus `\blog\b` restricted to the three `QfcDatamodel*.cs` partials for the private field log, reading each hit to classify it as declaration, invocation, comment, `nameof`, or same-named member of another type, and separating the LoadRemainingEmailsToQueueAsync-BackgroundWorker-overload from the one-argument overload by each hit's argument list and delegate target type +- Primary Member Set: log, Worker_RunWorkerCompleted, LoadRemainingEmailsToQueue, LoadRemainingEmailsToQueueAsync-BackgroundWorker-overload +- Primary Count: 4 +- Cross-check Search Strategy or Query Expression: files-with-matches grep with no file-type filter over the entire repository for the bare identifiers `Worker_RunWorkerCompleted|LoadRemainingEmailsToQueue` (97 files, of which 5 are `*.cs`: `QfcHomeController.cs`, `QfcDatamodel.cs`, `QfcInitEmailQueueZeroBatchTests.cs`, `QfcHomeControllerRunAsyncTests.cs`, `QfcDatamodelLivenessTests.cs`; the other 92 are Markdown, text evidence and agent-memory files), each `*.cs` file read in full at the hit lines to attribute the LoadRemainingEmailsToQueueAsync-BackgroundWorker-overload and LoadRemainingEmailsToQueue references, combined with the string-literal and reflection sweep `"log"|GetField\("log|nameof\(log\)` over `*.cs` for the private field log, a reflection sweep `GetMethod\("|GetField\("|GetProperty\("` over `QuickFiler.Test/Controllers/QfcDatamodel*.cs`, a read of `IQfcDatamodel.cs:101-167`, a Glob of `QuickFiler/Controllers/QfcDatamodel*.cs`, and a grep of `InternalsVisibleTo` under `QuickFiler/` +- Cross-check Member Set: log, Worker_RunWorkerCompleted, LoadRemainingEmailsToQueue, LoadRemainingEmailsToQueueAsync-BackgroundWorker-overload +- Cross-check Count: 4 +- Member-set Comparison: the normalized primary and cross-check member sets are identical (the same four names in the same sense), and both counts are 4; the assertion that exactly these four members have zero callers is supported + +### 3.3 Consequences of removing the four members + +- Resulting physical line count of `QfcDatamodel.cs`: 495 minus 121 = about 374. Breakdown: `log` field :109-111 (3 lines); `Worker_RunWorkerCompleted` with its comment :243-265 plus the separating blank :242 (24); `LoadRemainingEmailsToQueue` :378-416 plus blank :377 (40); two-argument `LoadRemainingEmailsToQueueAsync` :418-465 plus blank :417 (49); the empty `#region Linked List Locking` / `#endregion` block :469-473 (5). Removing the commented-out references that only make sense with those members (`:194`, `:209-210`, `:363`) brings it to about 370. The executor records the exact figure. +- `using` directives: none becomes unused because of the removal. The two-argument overload's `ToAsyncEnumerable()`/`ForEachAwaitWithCancellationAsync` come from `System.Linq` (System.Linq.Async), which `Enumerable.Range`, `.Select` and `.ToList()` in `InitEmailQueue` still need; `TaskCanceledException`, `String.Format`, `MessageBox` (still used at :339) and `RunWorkerCompletedEventArgs`'s namespace `System.ComponentModel` (still used by `BackgroundWorker`) all remain referenced. Whether `System.Collections`, `System.Collections.Concurrent`, `System.Text`, `System.Xml.Linq` or `ToDoModel` (:2-3, :8, :12, :17) were already unused before this change was not established and is independent of it; leave them unless the analyzer build reports IDE0005 for them. +- `#pragma warning disable/restore CS0618` (:436, :457) disappears with the overload; it is the file's only pragma. +- Test coverage of the removed members: none. No test references any of the four (section 3.2), and the whole type is `[ExcludeFromCodeCoverage]` (`QfcDatamodel.cs:25`; a type-level attribute on one partial declaration applies to the type, so it covers all three partial files). The removed lines are therefore not in the measured denominator today and removing them changes no first-party figure. "Changed-line coverage must not drop" is satisfied vacuously; record the pre- and post-change first-party totals as unchanged within run noise. +- Behaviour: none of the four is reachable, so no production behaviour changes and no concurrency or ordering invariant is affected (section 8). + +## 4. (d) Worker ownership and disposal + +### 4.1 Production ownership (verified) + +- `InitEmailQueue` stores the caller's worker in `_worker` (`QfcDatamodel.cs:273`), subscribes `Worker_DoWork` and registers a cancel callback on `_token` (:192-193; on the uninitialized test instances `_token` is `default`, whose `Register` is a no-op because `CanBeCanceled` is false), and starts it through `WorkerStarter` (:285, :312). +- `Cleanup()` only calls `_worker?.CancelAsync()` and nulls the field (:80, :102). Nothing in `QuickFiler/Controllers/QfcDatamodel*.cs` calls `Dispose` on the worker (grep `_worker\b.*Dispose|worker\.Dispose` under `QuickFiler/`: no matches). +- The production worker is `_formViewer.Worker` (`QfcHomeController.cs:252-255`, `:284`), a component owned by the form viewer, not by the datamodel. The datamodel never takes disposal ownership, so a test that disposes a worker it created cannot double-dispose anything. + +### 4.2 Construction sites and current disposal + +| Site | Current disposal | Proposed shape | +|---|---|---| +| `QfcDatamodelLivenessTests.cs:127` (test 1) | none | `using (var worker = new SynchronousBackgroundWorker()) { ... }` spanning the whole act/assert, so the worker outlives the loader release (section 5) | +| `QfcDatamodelLivenessTests.cs:201` (`StartHeldOpenLoader`, callers at :221, :249, :285) | none; the worker is created inside the helper and not returned | change the helper to accept the worker as a parameter (`StartHeldOpenLoader(SynchronousBackgroundWorker worker, Func<...> loaderBody, out TaskCompletionSource release)`) and have each of the three callers own it in a `using` block around the test body; this keeps ownership in the test method, mirrors `QfcDatamodelTeardownTests.cs:220`, and avoids disposing a worker the helper handed to the model | +| `QfcDatamodelTeardownTests.cs:220` | `using` block already | keep | +| `QfcDatamodelTeardownTests.cs:180` (`new BackgroundWorker {...}`) | `using` block already | keep | +| `QfcInitEmailQueueZeroBatchTests.cs:148` (inline `new` inside a lambda) | none | hoist into `using (var worker = new SynchronousBackgroundWorker())` around act and assert and pass `worker` | +| `QfcInitEmailQueueZeroBatchTests.cs:173` | none | wrap in `using` | +| `QfcInitEmailQueueZeroBatchTests.cs:221` (inline `new`) | none | hoist into `using` | +| `QfcDatamodelTests.cs:108`, `:261` (`new BackgroundWorker()` assigned by reflection) | none | related sibling nit in the same component; wrap in `using` (section 6.2) | + +Use the `using (...) { }` block form, not a `using var` declaration: `QuickFiler.Test.csproj` sets no `LangVersion` (only `QuickFiler/QuickFiler.csproj:14` does, `preview`), a grep for `using var |\?\?=|is not null` finds hits in only five test files, and the block form is what the sibling file already uses (`QfcDatamodelTeardownTests.cs:180-181, 220`), so it is the conservative choice regardless of the effective language version. + +Double-dispose and late continuations: `Component.Dispose()` is idempotent, the datamodel never disposes, and `Worker_DoWork`'s post-await line `bw.CancellationPending` (:232) is a plain property read that `BackgroundWorker` does not guard with a disposed check (framework knowledge, not re-verified against reference source). In test 2 (`RemainingLoadActive_AcrossAsyncVoidFirstAwait_StaysTrueWhileLoaderProduces`, :218-234) the `release` source is created with `RunContinuationsAsynchronously` (:190-192), so the loader's continuation runs on a pool thread after `release.SetResult(true)` and may execute after the `using` block disposes the worker; by the above that read is harmless, and the behaviour is unchanged from today, where the worker is simply never disposed. + +## 5. (e) Liveness test 1: scheduling dependence and a deterministic completion signal + +### 5.1 The test as it stands, `QfcDatamodelLivenessTests.cs:109-164` (the act and cleanup, :127-164) + +```csharp + var worker = new SynchronousBackgroundWorker(); + model.WorkerStarter = StartSynchronously; + + // Act — the issue #244 zero-batch short-circuit is COM-free and starts the worker + // through the issue #950 seam, which raises DoWork on this thread. + model.InitEmailQueue(0, worker); + + loaderEntered + .Task.IsCompleted.Should() + .BeTrue("the synchronous starter must reach the injected RemainingEmailLoader"); + + Task> pending = model.DequeueNextItemGroupAsync(1, 200); + fake.Advance(TimeSpan.FromMilliseconds(200)); + await Task.Yield(); + fake.Advance(TimeSpan.FromMilliseconds(200)); + await Task.Yield(); + + // Assert + pending + .IsCompleted.Should() + .BeFalse( + "the loader is still producing, so the gate must keep polling rather than treat " + + "an empty queue as an exhausted source and return an early partial batch" + ); + + // Cleanup — release the loader and let the dequeue drain on the honest signal. + loaderRelease.SetResult(true); + for (int i = 0; i < 20 && !pending.IsCompleted; i++) + { + fake.Advance(TimeSpan.FromMilliseconds(200)); + await Task.Yield(); + } + + pending + .IsCompleted.Should() + .BeTrue("once the loader completes, the gate exits on genuine exhaustion"); + (await pending).Should().BeEmpty(); +``` + +### 5.2 What the production code does under the test + +- `DequeueNextItemGroupAsync(1, 200)` (QueueProcessing :183-193) chains through the four-argument overload (:195-216; `_globals.QfSettings.HighConfidenceModeEnabled` is true from the strict mock at Liveness :90-98), `DequeueWithHighConfidenceGateAsync` (:264-278) and `DequeueWithHighConfidenceGateWithOutcomeAsync` (:292-315), which constructs the gate with `sourceActive: () => _remainingLoadActive` (:305) and the model's `TimeProvider` (:303), and awaits `gate.DequeueAsync(1, 200, _token)` (:311). None of these three awaits uses `ConfigureAwait(false)`, so each captures the `SynchronizationContext` current on the calling thread at call time. +- `QfcStreamingDequeueConfidenceGate.DequeueAsync` (`QfcStreamingDequeueConfidenceGate.cs:190-301`): with an empty master queue `_tryTakeNext()` returns null (:243); on the first pass `alreadyWaitedForEmptySource` is false, so it sets it and awaits `_timeProvider.Delay(200 ms).ConfigureAwait(false)` (:252-256). On each later pass it returns `SourceExhausted` only when `alreadyWaitedForEmptySource && !sourceCanStillProduce` (:246-250); while the flag is true it arms another 200 ms delay. The production completion point the test waits for is therefore the first poll that follows a delay and observes `_remainingLoadActive == false`, which returns at :249 and lets the three datamodel continuations complete `pending`. +- Timer mechanics `[V-web]`: on net481 `TimeProvider.Delay` is the `Microsoft.Bcl.TimeProvider` extension `TimeProviderTaskExtensions.Delay`, whose `DelayState : TaskCompletionSource` is constructed without `RunContinuationsAsynchronously` and is completed by the timer callback's `TrySetResult(true)`; `FakeTimeProvider` is an unsealed `public class FakeTimeProvider : TimeProvider`, does not override `Delay`, exposes `public override ITimer CreateTimer(...)`, and `Advance(TimeSpan)` invokes due timer callbacks synchronously on the calling thread (`WakeWaiters` -> `InvokeCallback`). + +### 5.3 The scheduling dependence + +1. After `fake.Advance(200)` fires the gate's timer, the gate's `ConfigureAwait(false)` continuation is run inline on the advancing thread only if that thread's `SynchronizationContext.Current` is null or exactly `SynchronizationContext` and `TaskScheduler.Current` is the default (the TPL's inlining rule for context-free await continuations); otherwise it is queued to the thread pool. MSTest worker threads carry whatever context an earlier test class left on them: `QfcItemControllerTestSupport.EnsureSynchronizationContext` installs a plain one and never restores it (`QfcItemController.TestSupport.cs:90-96`, D8 in the original research), and any test that installs a derived context on a pool thread without restoring it makes the next test on that thread take the queued path. The test cannot know which path it is on. +2. On the queued path the second `fake.Advance(200)` can run before the gate has re-armed its timer; a timer created afterwards is due 200 ms after the already-advanced clock, so that advance is lost. `await Task.Yield()` gives no ordering guarantee relative to a queued pool work item; it only moves the test to another pool thread, where the ambient context may differ again. +3. `loaderRelease.SetResult(true)` clears the flag inline (through the loader lambda's continuation and `Worker_DoWork`'s `finally`) only under the same inlining conditions on the thread that happens to run the test after the second `Yield`; otherwise the clear is posted and races the next advance. +4. The `for (i < 20 && !pending.IsCompleted)` loop is a bounded retry whose success depends on the pool scheduling the queued continuations within twenty yields. It passes today in practice; under pool starvation during the parallel run it can exhaust its budget, and the final `IsCompleted.Should().BeTrue()` then fails without any production defect. The sibling test `QfcDatamodelTests.cs:96-131` has the same first two steps (:116-119, :126-128) but no retry loop, so on the queued path its lost advance leaves `await pending` (:128) waiting indefinitely rather than failing (section 6.2). + +### 5.4 Deterministic shape (test-only; no production seam required) + +Signal 1, "the gate armed its next wait": a `FakeTimeProvider` subclass that completes a `TaskCompletionSource` after forwarding `CreateTimer` to the base. Precedents: `UtilitiesCS.Test/TestHelpers/ArmingBarrierTimeProvider.cs:19-54` (a forwarding decorator with `Armed`, `ReArm()`, and `RunContinuationsAsynchronously` signals, with the remark at :14-17 that advancing past a deadline the loop has not yet created hangs a test) and `QuickFiler.Test/Controllers/QfcFormControllerSeamTests.cs:358-367` (`private sealed class CountingTimeProvider : FakeTimeProvider` overriding `CreateTimer`, which proves the override compiles in this project). Recommended: `internal sealed class ArmingFakeTimeProvider : FakeTimeProvider` with `Task Armed`, `void ReArm()`, and the override `CreateTimer` that calls `base.CreateTimer(...)` first and then `TrySetResult(true)`; signals created with `TaskCreationOptions.RunContinuationsAsynchronously` so a test continuation never runs inside the gate's `CreateTimer` call. Place it in `QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs` (second consumer in section 6.2; add `` next to the new worker helper). Subclassing is preferred over copying the UtilitiesCS.Test decorator because it is shorter and the project already subclasses `FakeTimeProvider`; linking the UtilitiesCS.Test file across projects would import a foreign namespace and was rejected. + +Signal 2, "the dequeue completed": `pending` itself, the task the production code already returns. + +Signal 3, "the loader completion cleared the flag": make it synchronous and self-checking. Register every production await of the loader path and the dequeue path while no `SynchronizationContext` is installed, so their continuations run inline on the completing thread instead of being posted to a context the test does not drain, then assert the flag through the existing `ReadLivenessFlag` (:167-172) before advancing the clock. A small `IDisposable` scope helper that sets `SynchronizationContext.Current` to null and restores the previous value on dispose (about 12 lines, nested in the test class) expresses this; `DrainableSynchronizationContext` (:68-87) is not used by test 1 because its `Drain()` must run on the creating thread (:80) and test 1 awaits across threads. + +Proposed body (names and messages indicative): + +```csharp + using (var worker = new SynchronousBackgroundWorker()) + { + model.WorkerStarter = SynchronousBackgroundWorker.StartSynchronously; + Task> pending; + using (NoSynchronizationContext()) + { + model.InitEmailQueue(0, worker); + loaderEntered.Task.IsCompleted.Should().BeTrue("..."); + pending = model.DequeueNextItemGroupAsync(1, 200); + } + clock.Armed.IsCompleted.Should().BeTrue("the gate arms its first empty-queue wait before returning"); + clock.ReArm(); + + // Act — the first wait expires while the loader is still producing. + clock.Advance(TimeSpan.FromMilliseconds(200)); + Task first = await Task.WhenAny(clock.Armed, pending); + + // Assert + first.Should().BeSameAs(clock.Armed, "the gate must arm a second wait rather than return an early partial batch"); + pending.IsCompleted.Should().BeFalse("..."); + + // Cleanup — complete the loader; with no captured context its continuations clear the flag inline. + using (NoSynchronizationContext()) { loaderRelease.SetResult(true); } + ReadLivenessFlag(model).Should().BeFalse("the loader's completion must clear the flag before the next poll"); + clock.Advance(TimeSpan.FromMilliseconds(200)); + (await pending).Should().BeEmpty(); + } +``` + +Why each step is deterministic: +- `pending` runs synchronously down to the gate's first `Delay`, so `Armed` is complete before the call returns; `ReArm()` precedes the advance, and the second arming can only happen after the advance, so the re-arm cannot be missed. +- `Task.WhenAny(clock.Armed, pending)` completes whether the gate's continuation is inlined inside `Advance` or queued to the pool. If the liveness flag regressed to a dishonest `IsBusy`-style signal, the gate would return after its first wait and `pending` would win the race, producing a crisp assertion failure instead of a hang; this is the regression sensitivity the old `IsCompleted.Should().BeFalse()` claimed but could not show. +- The flag checkpoint fails crisply if the inline assumption were ever violated (a different TPL rule or a custom scheduler), rather than flaking. +- The final advance fires the second timer; the gate's poll sees the flag false and returns `SourceExhausted`; the three datamodel continuations captured no context and complete `pending` on whichever thread ran the gate; `await pending` is the completion signal. +- No `Thread.Sleep`, `Task.Delay`, retry loop, timeout change, `[DoNotParallelize]` or `Workers=1`. + +Test-only or production seam: test-only. The gate already takes the `TimeProvider` through the datamodel's `TimeProvider` property (`QfcDatamodel.cs:126`), and `pending` already is the production task. No production file changes for (e). + +### 5.5 Showing the old shape is nondeterministic; fail-before status + +A deterministic fail-before run of the old test is structurally impossible: its failure requires the pool to delay a queued continuation past a bounded retry, which the test cannot force, and the production behaviour under test is correct both before and after. The plan should record a fail-before exception dossier for this test-quality change with this structural reason. Two supporting observations can be recorded instead: (i) a reading of the mechanism in section 5.3 with the `[V-web]` facts in 5.2; (ii) an optional regression-sensitivity check of the new test by temporarily editing `QfcDatamodel.QueueProcessing.cs:305` to `() => false` in the executor's working copy, running only the two liveness-style tests, observing the `BeSameAs(clock.Armed)` assertion fail, and reverting before any gate. (ii) is evidence of the new test's sensitivity, not a fail-before of the old one, and must be labelled as such. + +## 6. (f) Other related defects in the files above + +### 6.1 Related, in scope + +| # | File:lines | Defect | Change | +|---|---|---|---| +| F1 | `QfcDatamodel.cs:369` | The live one-argument `LoadRemainingEmailsToQueueAsync` logs `nameof(LoadRemainingEmailsToQueue)`, naming the wrong method; it also becomes a compile error once the synchronous method is removed. | Retarget to `nameof(LoadRemainingEmailsToQueueAsync)` in the removal edit. | +| F2 | `QfcDatamodel.cs:194`, `:209-210`, `:363` (and `:404`, `:462` inside removed members) | Commented-out code referring to the removed members. | Delete with the members. The remaining commented-out lines (`:63`, `:70`, `:99-101`, `:262-263` go with `Worker_RunWorkerCompleted`, `:301`, `:351`, `:394` goes with the sync method, `:478`) are the same kind of noise in a touched file; recommend deleting `:99-101` (fields that no longer exist) and leaving the rest to keep the diff reviewable, at the planner's discretion. | +| F3 | `QfcDatamodel.cs:469-473` | Empty `#region Linked List Locking` / `#endregion`. | Delete. | +| F4 | `QfcDatamodel.QueueProcessing.cs:285` | Doc comment cites `TryUnhookOrReplace` "(:31-66)"; the method is at `:146-181` of the same file. | Drop the stale line range (line numbers in doc comments do not survive edits). | +| F5 | `QfcDatamodelTeardownTests.cs:63-64`, `QfcInitEmailQueueZeroBatchTests.cs:33`, `:117-118`, `QfcDatamodelLivenessTests.cs:19-24` | Doc text about "duplicated per file" and "the nested SynchronousBackgroundWorker" becomes false after consolidation. | Reword alongside (a); the Liveness header keeps its sentence about the reflection helpers (section 6.3). | +| F6 | `QfcInitEmailQueueZeroBatchTests.cs:97-99` | Doc says assigning the inert loader "before starting a real BackgroundWorker is what makes it safe"; since #950 the worker is the synchronous test worker. | Minor reword while the file is open. | + +### 6.2 Related sibling call sites in `QfcDatamodelTests.cs` (same component, same root cause; in scope under the directive) + +- `DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive` (:96-131) drives the same gate with the same `fake.Advance(200); await Task.Yield();` step (:118-119) and then sets the flag false by reflection and advances once more before `await pending` (:126-128). On the queued path of section 5.3 the second advance can precede the gate's re-arm, after which no timer is due and `await pending` never completes. Apply the section 5.4 shape (`ArmingFakeTimeProvider`, `ReArm`, `WhenAny`, then the flag write, then the advance and `await pending`); because the flag is written directly by reflection here, no loader or context handling is needed. The file is 371 lines, so the edit fits. +- `:108` and `:261`: `new BackgroundWorker()` assigned into `_worker` by reflection and never disposed; wrap in `using` blocks (the `WaitForQueue` test at :253-283 is otherwise deterministic: its only advance follows the flag write, and the loop's first wait is armed synchronously by the reflective invoke). +- The `ToggleOfflineMode` test (:220-245) arms one delay synchronously and advances once; it is deterministic and needs no change. + +### 6.3 Observed, intentionally not changed + +- Duplicated `CreateUninitializedDatamodel` / `SetPrivateField` reflection helpers in the three touched files and in `QfcDatamodelTests.cs`, `QfcQueuePurePathsTests.cs`, `QfcHomeControllerRunAsyncHighConfidenceTests.Part3.cs`. This duplication is documented as a deliberate convention (`QfcDatamodelLivenessTests.cs:19-24`) and #972 item 1 names only the worker helper. Consolidating it would touch three further files outside the fold for no behavioural gain; leave it and keep the header sentence that documents it. +- `QfcDatamodel.QueueProcessing.cs:140-144` `UndoMove()` throws `NotImplementedException` behind a TODO. It implements `IQfcDatamodel.UndoMove` (:139) and is a pre-existing design gap unrelated to this item; no change and no new issue is needed beyond what is already visible in the code. +- Test 2 of the liveness file (:218-234) releases its loader with a `RunContinuationsAsynchronously` source and returns without observing the continuation. This is the #950 design and is unaffected by disposal (section 4.2); no change. +- `QfcItemControllerTestSupport.EnsureSynchronizationContext` never restores the installed context (D8 of the original research). It is the most likely source of the ambient-context variation in section 5.3, but changing it alters the premise of unrelated handler tests; the section 5.4 shape is robust to it by construction. No change; record as observed. + +### 6.4 Completely unrelated (report for filing only) + +None found in the files examined for this addendum. + +## 7. Confirmation of #972 item 5 (D4) + +`QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs` R4 (:218-251) disposes `transactionA` at :251 with no `try`/`finally`, exactly as D4 in `research/2026-10-02T05-50-dispatcher-pin-call-sites-research.md` (section 6) describes, with the same proposed fix (wrap in `try`/`finally` with an idempotent re-dispose, about six lines, 470 to about 476). Nothing in this addendum changes that design. The spec must record it as closing #972 item 5. + +## 8. Paths the folded scope creates or modifies, coverage implications, invariants + +Production (both files are in the `[ExcludeFromCodeCoverage]` type `QfcDatamodel`, `QfcDatamodel.cs:25`; `coverage.config` carries no `QfcDatamodel` entry, so the attribute is the only mechanism and it already removes the whole type from the measured denominator): +- `QuickFiler/Controllers/QfcDatamodel.cs`: remove the four caller-free members, the empty region and the dead commented references; retarget the `nameof` at :369. Coverage: no measured lines change; first-party totals unchanged. Invariants: none; all removed code is unreachable. +- `QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs`: doc-comment rewrite at :15-23 and the stale range at :285. Coverage: comment-only. Invariants: none. + +Test: +- `QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs` (new). +- `QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs` (new). +- `QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs`: helper removal, `using` ownership (including the `StartHeldOpenLoader` signature), test 1 rewrite, doc rewording. +- `QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs`: helper removal, doc rewording. +- `QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs`: helper removal, three `using` blocks, doc rewording. +- `QuickFiler.Test/Controllers/QfcDatamodelTests.cs`: section 6.2 sibling test and two `using` blocks. +- `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs`: D4 (already in the #968 plan). +- `QuickFiler.Test/QuickFiler.Test.csproj`: two new `TestSupport\...` `Compile` items after :228, plus the pin-count test item from the original research next to :203. +- Unchanged from the original research and still in scope: `QfcItemController.UiThreadDispatcherFixture.cs`, `QfcItemController.TestSupport.cs`, `QfcItemController.FocusAndThemeTests.cs`, and the new `QfcItemController.UiThreadDispatcherPinCountTests.cs`. +- `FEATURE/spec.md` and the plan: AC20 amended to name exactly the two production paths above; #972 items 1-5 and the liveness-test item mapped to ACs; the fail-before exception dossier for the test-quality rewrites (section 5.5). + +Coverage of test assemblies is excluded by the coverage route (test-assembly exclusion derived in `scripts/vscode/Invoke-MSTestWithCoverage.ps1`, cited in the original research section 7), so none of the test edits moves a figure. + +Concurrency or ordering invariants: no production invariant changes. On the test side, the only ordering that changes is in the two rewritten dequeue tests, which replace an uncontrolled interleaving (advance, yield, retry) with explicit signals (`Armed`, `pending`, the flag checkpoint); the shared worker helper and the `using` blocks change no ordering because the datamodel never disposes or awaits the worker. + +## 9. Testing implications summary + +- Parallel regime unchanged (Workers=0, ClassLevel); no new `[DoNotParallelize]`, timeouts, sleeps, delays or retries. +- Run the four datamodel test classes (`QfcDatamodelTests`, `QfcDatamodelLivenessTests`, `QfcDatamodelTeardownTests`, `QfcInitEmailQueueZeroBatchTests`) together under `scripts/vscode/TaskMaster.cli.runsettings` before and after; all must pass; record the before/after line counts of the five touched test files and `QfcDatamodel.cs`. +- Evidence under `FEATURE/evidence/regression-testing/` for the two rewritten dequeue tests (pass-after, plus the labelled sensitivity check if performed) and `FEATURE/evidence/qa-gates/` for the unchanged first-party coverage totals; the fail-before exception dossier goes with the plan per the atomic-plan contract. diff --git a/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md new file mode 100644 index 000000000..47fe5accd --- /dev/null +++ b/docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/spec.md @@ -0,0 +1,336 @@ +# 2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup (Spec) + +- **Issue:** #968 +- **Parent (optional):** none +- **Owner:** drmoisan +- **Last Updated:** 2026-10-02T22-40 +- **Status:** Approved for planning (amended 1.2) +- **Version:** 1.2 +- **Amendment 1.2 (2026-10-02T22-40, maintainer-directed scope widening per the Coordinator Scope Amendment in `issue.md`):** issue #972 (the five residuals of the #950 review) and the #968 liveness-test residual (2026-10-02T05:37Z issue comment) are folded into this item. The pull request closes both #968 and #972. Changes: D4 is recorded as delivering #972 item 5 and the "coordinator reconciles #972" language is removed; the scope gains the shared `SynchronousBackgroundWorker` test helper (#972 item 1), the `_remainingLoadActive` comment rewrite (#972 item 2), the removal of four caller-free legacy members of `QfcDatamodel` (#972 item 3), test-owned disposal of the synchronous workers (#972 item 4), and a deterministic completion signal for the two dequeue-liveness tests (the #968 comment, widened to the sibling `QfcDatamodelTests` call site under the related-defect directive); AC20 is amended to name exactly the two production paths the new scope requires (`QuickFiler/Controllers/QfcDatamodel.cs`, `QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs`) and nothing else; new criteria AC25 to AC32 are added. The round-1 preflight delta for pin-count test 4 (two-transaction variant) is reflected in "Functions/classes" item 4 and in the census sentence. No existing acceptance criterion is weakened or deleted. Spec authored by the orchestrator (see `local_execution_overrides` in the run checkpoint: the `prd-feature` stop hooks cannot be satisfied from this session topology). +- **Amendment 1.1 (2026-10-02, orchestrator correction applied by the planner):** the determinism invariant (decision 4) is strengthened from "every invocation occurs while its caller holds a transaction" to "every pin is acquired and released while its caller holds a transaction, so the pin lifetime nests inside the gate hold"; the second-caller transaction test (R4) loses the baseline pin that #950 added, because that pin was released after the gate and installed a transaction value over a pinned parked value; the census acceptance criterion classifies each invocation as acquired and released inside a held transaction with no install between acquisition and release; the acceptance criteria carry `ACn:` labels for mechanical check-off; the no-production-change criterion names the inherited committed set; the evidence list admits the plan's per-task artifacts. No acceptance criterion is weakened. +- **Work Mode:** full-bug (this file is the sole acceptance-criteria source; no `user-story.md` exists for this item) +- **Research record:** docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T05-50-dispatcher-pin-call-sites-research.md (section numbers cited below refer to that document unless prefixed "addendum") +- **Research addendum (amendment 1.2):** docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T22-20-qfc-datamodel-972-fold-research.md (cited below as "addendum section N") + +## Context +Tests in `QuickFiler.Test` `QfcItemController_FocusAndThemeTests` set up the shared UI-thread dispatcher (through `UiThreadDispatcherFixture.EnsureDispatcher()`) and do not release that setup. Under the parallel test regime, another test class that releases or resets the shared dispatcher can leave a theme test running against a null dispatcher. The #950 preparation found the exposure: its transaction-test fix releases a dispatcher pin at the end of the test, and the same exposure already exists through two other tests in that file. + +The research record (sections 0, 1 and 3) narrows the root cause and corrects one premise of the issue: + +- The fixture pin is not reference counted. `UiThreadDispatcherFixture.EnsureDispatcher()` (fixture file lines 122-138) seeds the parked dispatcher only when the static is null and returns a scope that remembers only whether that single call installed it. A second concurrent caller receives a no-op scope. Whichever caller installed the value nulls it on `Dispose` (lines 269-272, `CompareExchange(_installed, null)`) regardless of how many other callers still hold a scope. This is the shared state the issue asks to "find and own". +- The two theme tests never read the shared static. `SetThemeDark(bool)` / `SetThemeLight(bool)` call `Theme.SetQfcTheme(async: true)`, which dispatches through the theme's injected `_uiDispatcher`; the tests build that theme with `QfcItemControllerTestSupport.BuildColorTheme`, which injects a `Mock` whose `InvokeAsync` returns a completed task. The `EnsureUiThreadDispatcher()` calls at focus-and-theme test file lines 452 and 468 are dead arrangement whose only effect is an unpinned write into the shared static (the W1 writer that #950 identified). + +Environment: +- OS/version: Windows 11 (local) and windows-latest (CI) +- Python version: n/a (C# / MSTest, Workers=0, Scope=ClassLevel) +- Command/flags used: standard MSTest coverage route (`scripts/vscode/Invoke-MSTestWithCoverage.ps1`, runsettings `scripts/vscode/TaskMaster.cli.runsettings`) +- Data source or fixture: `UiThreadDispatcherFixture` + +Impact / Severity: +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + + +## Repro & Evidence +Steps to Reproduce (as filed): +1. Run `QuickFiler.Test` in parallel. +2. Have a class that resets the shared dispatcher run concurrently with `QfcItemController_FocusAndThemeTests`. +3. A theme test can observe a null dispatcher. This is intermittent. + +Deterministic reproduction (replaces step 3; research section 5, test 1): on a single thread, under a `UiThreadDispatcherFixture` transaction that installed a null baseline, take two ensure pins, dispose the first, and read `UiThreadDispatcherFixture.Current`. On the unmodified fixture the field is already null after the first release: the first pin installed the parked dispatcher, the second pin was a no-op scope, and the first pin's `Dispose` ran `CompareExchange(parked, null)`. No scheduling is involved, so the failure is reproducible on every run. + +Expected: +Each test class acquires and releases the shared dispatcher through a scoped, reference-counted pin, so no class can null it while another still depends on it. + +Actual: +The theme tests depend on dispatcher state they do not own or pin, and the fixture nulls the shared static on the first installer's release regardless of other live holders. + +Logs / Screenshots: +- [ ] Attached minimal logs or screenshot +- Snippet: #950 preparation report; its plan carries the stop marker `THEME TEST NULL-DISPATCHER EXPOSURE OBSERVED`. +- Fail-before evidence for this item is produced at plan execution time by running the regression test against the unmodified fixture (see Test Strategy); the expected FluentAssertions failure text is of the form `Expected afterFirstRelease to refer to Dispatcher { ... Name = "UiThreadDispatcherFixture.ParkedDispatcher" } because a holder that did not take the last pin must not lose the dispatcher, but found .` + + +## Scope & Non-Goals +- In scope: + - Reference-count the ensure pin inside `UiThreadDispatcherFixture` (orchestrator decision 1; research Approach A). + - Delete the two dead `EnsureUiThreadDispatcher()` calls in `QfcItemController_FocusAndThemeTests` (`SetThemeDark_FromNormal_SelectsDarkNormalTheme`, `SetThemeLight_FromNormal_SelectsLightNormalTheme`) and reword their arrange comment. + - Add the new test class `QfcItemController_UiThreadDispatcherPinCountTests` (one fail-before regression test plus three specification tests) and its `` entry. + - Related defects in the touched files, brought into scope under the maintainer's related-defect remediation directive of 2026-10-02 (research section 6): fixture comment drift (D1), wrapper doc drift (D2), the second-caller transaction test's obsolete doc invariant (D3), the removal of that test's baseline pin, which was acquired inside its gate hold but released after it (R4 restructure, amendment 1.1), that test's missing `try/finally` around its first transaction (D4), the duplicated private `BuildExecutingViewer` helper (D5), the theme-test arrange comment (D6), and file-size accounting for every touched file (D7). + - A call-site census proving the gated-caller invariant (Proposed Fix, "Boundaries and invariants"). + - Baseline and final toolchain and coverage evidence as Markdown projections. + - Folded scope (amendment 1.2; issue #972 and the #968 liveness comment): + - #972 item 1: consolidate the three byte-identical nested `SynchronousBackgroundWorker` classes and their private `StartSynchronously` starters (`QfcDatamodelLivenessTests.cs`, `QfcDatamodelTeardownTests.cs`, `QfcInitEmailQueueZeroBatchTests.cs`) into one `internal sealed class SynchronousBackgroundWorker : BackgroundWorker` in the new file `QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs` (namespace `QuickFiler.Test.TestSupport`), carrying `RaiseDoWork()` and `internal static void StartSynchronously(BackgroundWorker worker)`, plus its `` item (addendum section 1.4). The consumers' doc text about "duplicated per file" and "the nested SynchronousBackgroundWorker" is reworded (addendum F5, F6). + - #972 item 2: rewrite the `_remainingLoadActive` doc comment in `QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs` to name `WorkerStarter` instead of "each `RunWorkerAsync()` call" and to drop "written on the worker thread"; drop the stale line range cited for `TryUnhookOrReplace` in the same file (addendum section 2.4, F4). + - #972 item 3: remove the four caller-free members of `QuickFiler/Controllers/QfcDatamodel.cs` proven in addendum section 3.2 and its `## Numeric Derivation Evidence` (the duplicate private `log` field, `Worker_RunWorkerCompleted`, the synchronous `LoadRemainingEmailsToQueue(BackgroundWorker, CancellationToken)`, and the `LoadRemainingEmailsToQueueAsync(BackgroundWorker, CancellationToken)` overload), the commented-out references to them, and the empty `Linked List Locking` region; retarget the live `nameof(LoadRemainingEmailsToQueue)` in the one-argument `LoadRemainingEmailsToQueueAsync` to `nameof(LoadRemainingEmailsToQueueAsync)` (addendum F1 to F3). The members that implement `IQfcDatamodel` are kept. + - #972 item 4: every `SynchronousBackgroundWorker` and test-created `BackgroundWorker` in `QfcDatamodelLivenessTests.cs`, `QfcInitEmailQueueZeroBatchTests.cs` and `QfcDatamodelTests.cs` is owned and disposed by its test method through a `using (...) { }` block; `StartHeldOpenLoader` takes a caller-owned worker (addendum section 4.2). + - #972 item 5: delivered by D4 (below). + - #968 liveness comment: `QfcDatamodelLivenessTests.DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle` and its sibling `QfcDatamodelTests.DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive` replace the `fake.Advance` plus `Task.Yield` steps and the bounded advance loop with explicit completion signals: a new test helper `internal sealed class ArmingFakeTimeProvider : FakeTimeProvider` in `QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs` (an `Armed` task completed after `CreateTimer`, and `ReArm()`), `Task.WhenAny(clock.Armed, pending)` to prove the gate re-armed rather than returned, and the dequeue task itself as the completion signal (addendum section 5.4). Test-only; no production seam. +- Out of scope / non-goals: + - Any production code change other than the two folded-scope paths named in AC20 (`QuickFiler/Controllers/QfcDatamodel.cs`, `QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs`). Within those two files only the removals, the `nameof` retarget and the comment rewrites above are made; no reachable production behaviour changes. + - Consolidating the duplicated `CreateUninitializedDatamodel` / `SetPrivateField` reflection helpers in the datamodel test files: a documented deliberate convention, not named by #972 (addendum section 6.3). + - `QfcDatamodel.QueueProcessing.cs` `UndoMove()` `NotImplementedException`: pre-existing interface gap, unrelated (addendum section 6.3). + - `QfcItemControllerTestSupport.EnsureSynchronizationContext` (D8, research section 6): it installs a plain `SynchronizationContext` on the MSTest worker thread and never restores it. It is observed and intentionally unchanged because it touches the per-thread ambient context, not the shared dispatcher static; it is documented as deliberate and relied on by handler tests; and changing it would alter the premise of unrelated tests (#950 recorded the ambient context as an execution-time observation only). + - Other test assemblies' own mechanisms over the same static (UtilitiesCS.Test `UiThreadDispatcherScope`, `UiThreadStateScope`): they never reference the QuickFiler fixture and run in their own AppDomain under the MSTest adapter (cited from #950 research, not re-verified). + - Making `EnsureDispatcher` take the transaction gate: rejected by the fixture's design note (callers without `[Timeout]` would block without bound) and not re-litigated. + - Serialising the suite: `[DoNotParallelize]`, `Workers=1`, retries, `Thread.Sleep`, `Task.Delay`, timeout increases and temporary files are prohibited. +- Explicitly excluded systems, integrations, or datasets: none beyond the above; no Outlook, COM, or filesystem dependency is involved. + +## Root Cause Analysis +This is the same family as #950 and #882: raced static test-fixture state. It must not be fixed with `[DoNotParallelize]`, Workers=1 or retries. Find and own the shared state. Sequence it after #950 merges, because #950 introduces the pin. + +Precise mechanism (research sections 1.1 and 2.2): + +1. `EnsureDispatcher()` keeps no count of live pins. Its scope records only `_installed` (the dispatcher this one call wrote, or null). +2. The first pin on a null field installs the parked dispatcher; every later pin while the field is non-null installs nothing and is a no-op on dispose. +3. When the first pin's scope is disposed, `CompareExchange(_installed, null)` nulls the field even though other scopes are still live. Any holder that depended on the non-null value now reads null. +4. The theme tests contribute to this by discarding their scope (unpinned write W1, never reverted) while never reading the static themselves. The holders that do read the static and can be hurt are the fixture tests (second-caller transaction test R4 pins a baseline inside its gate but releases that pin only after its gate is released and the waiter has completed, so the pin outlives the gate hold, and it installs a transaction value over the pinned parked value; this item removes that pin. Single-pin tests R2 and R3 null the field on release when the baseline was null). + +Family note: the W-numbered writers (W1 unpinned ensure, W2 ensure-scope dispose, W3/W4 transaction install/restore, W5 `UiThread.Initialize`, W6 `UiThread.ResetForTesting`) follow #950's numbering and are listed in research section 1.3. + + +## Proposed Fix + +### Design summary (what changes where): + +Ratified orchestrator decisions and their rationale: + +1. **Fix design: Approach A, reference-count the pin in the fixture, delete the dead theme-test calls.** `UiThreadDispatcherFixture` gains two private statics guarded by `FieldLock`: a pin counter and an install-ownership flag. `EnsureDispatcher()` increments the counter under `FieldLock`; if the field is null it writes the parked dispatcher and sets the ownership flag. `EnsureScope.Dispose()` (idempotent through `_disposed`) decrements under `FieldLock`; when the count reaches zero, the ownership flag is set, and the field still references the parked instance, it writes null and clears the flag. The decrement and the conditional null write are performed inline in the same `lock (FieldLock)` block (not through the re-locking `CompareExchange` helper) so they form one straight-line critical section, consistent with the `FieldLock` contract in the class doc. The two `EnsureUiThreadDispatcher()` calls in the theme tests are deleted. Rationale: the fix owns the shared state at its single mutation point (the fixture's stated design goal); it protects every present and future pin holder, not only the theme tests; it adds no MSTest lifecycle attributes (none exist in `QuickFiler.Test` today); it needs no file split; and the regression test is single-threaded. + - Approach B (class-level `[ClassInitialize]`/`[ClassCleanup]` pin in the theme test class, fixture unchanged) is rejected: the theme tests do not read the static, so the pin protects nothing in that class and merely relocates the unpinned writes to class start and class end; without counting the class-end disposal still nulls the field for every other unpinned holder; the file is at 497 lines so the addition forces a split; and the `ClassCleanupBehavior` default for MSTest 4.4.1 was not verified and would have to be pinned explicitly. + - Approach C (per-test `using` around `EnsureUiThreadDispatcher()` in the two theme tests, fixture unchanged) is rejected: each test's disposal is still a W2 write that can null the field under a concurrent unpinned holder, it adds lines to a file at the limit, and it pins state the tests do not use. +2. **Validation mechanism replaced, not weakened.** The issue proposes "show the theme test fails without the fix". Research section 3 traces the theme path: `new FocusController()` (empty protected constructor) -> `SetField(_themes, BuildAllThemes())` (one `Theme` from `BuildColorTheme`, whose `_uiDispatcher` is a `Mock` returning `Task.CompletedTask`) -> `SetThemeDark(async: true)` -> `Theme.SetQfcTheme(true)` -> `_uiDispatcher.InvokeAsync(...)` -> assertion on `_activeTheme`. `UiThread.Dispatcher` is never evaluated on this path, so a null or foreign value in the static cannot make the theme test fail, and the issue's validation idea is unsatisfiable as worded. The replacement is a fixture-level regression test (research section 5, test 1) that observes the missing reference counting directly and deterministically without concurrency. The theme tests are made independent of the static by deleting the dead calls. This spec records the replacement explicitly so a reviewer does not read it as a dropped criterion. +3. **Regression test location.** New file `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs` plus its `` item in `QuickFiler.Test/QuickFiler.Test.csproj`. The existing fixture test file is at 470 lines and D4 adds to it, so the new tests cannot go there without breaching the five-hundred-line limit. The file carries test 1 (the fail-before regression) and tests 2 to 4 (specification tests that pass before and after; each is labelled as such in its doc comment). Test 3 starts a running dispatcher through `QfcItemControllerTestSupport.StartRunningDispatcher` and must shut it down through `QfcItemControllerTestSupport.ShutdownDispatcher` in a `finally`. +4. **Determinism invariant (requirement, strengthened by amendment 1.1).** After the fix, every pin taken through `EnsureUiThreadDispatcher()` / `EnsureDispatcher()` in the repository (other than the forwarder itself) is both acquired and released while its caller holds a `UiThreadDispatcherFixture` transaction (that is, while the caller owns the `TransactionGate` permit): the pin lifetime nests inside the gate hold, and no `Install` call lies between a pin's acquisition and its release. Because the gate admits one holder at a time, the pin count is serialised and is zero whenever a transaction is acquired. That is what makes the regression test's final "last release nulls" assertion deterministic under parallel execution: no other class can hold a pin while the test's transaction is live. Nesting is required, not merely acquisition inside the gate: before this amendment R4 took its pin after acquiring `transactionA` but released it only at the end of its `using` block, after `transactionA.Dispose()` had released the gate and the waiter's `transactionB` had completed, so a pin-count test whose transaction was acquired in that window would have observed a count of one and failed its final assertion nondeterministically. The R4 restructure (option (a), see the R4 bullet under "Functions/classes/CLI commands impacted") removes that pin. The plan must include a census (call-site enumeration with two independent search strategies and a member-set comparison, mirroring research section 2.1) executed against the post-change tree, classifying each invocation as acquired and released inside a held transaction and by the absence of an interleaved `Install`, and recorded as evidence. +5. **Related defects in scope** (research section 6): D1 to D7 are delivered by this item; D8 is observed and unchanged (see Scope). D4 is item 5 of issue #972 (CR-5 of the #950 code review); because #972 is folded into this item (amendment 1.2), D4 delivers and closes #972 item 5 (promoted record `docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md`). #972 items 1 to 4 are delivered by the folded scope listed under "Scope & Non-Goals". +6. **Constraints.** Tests stay parallel (runsettings `Workers=0`, `Scope=ClassLevel`). Prohibited: `[DoNotParallelize]` additions, `Workers=1`, retries, `Thread.Sleep`, `Task.Delay`, temporary files, timeout increases. MSTest + Moq + FluentAssertions. Production code changes are limited to the two folded-scope paths named in AC20, and within them to dead-code removal, one `nameof` retarget and comment rewrites. Toolchain in CLAUDE.md C# order with numeric baseline and final coverage recorded as projections. +7. **Folded-scope validation (amendment 1.2).** The four removed `QfcDatamodel` members are unreachable, so no regression test can observe their removal; the zero-caller proof (two independent search strategies, member sets compared, addendum `## Numeric Derivation Evidence`) is re-run against the pre-change tree at execution time and recorded as qa-gates evidence, and the two rebuilds are the compile-level proof that nothing referenced them. The two dequeue-liveness rewrites are test-quality fixes of correct production behaviour, so a deterministic fail-before run of the old shape is structurally impossible (its failure depends on thread-pool scheduling the test cannot force; addendum section 5.5); a fail-before exception dossier records this, and a labelled regression-sensitivity check (a temporary working-copy edit of the `sourceActive` lambda in `QfcDatamodel.QueueProcessing.cs` to `() => false`, reverted before any gate) shows that each rewritten test fails crisply when the liveness signal is dishonest. `QfcDatamodel` carries `[ExcludeFromCodeCoverage]` at type level (`QfcDatamodel.cs` line 25), so the removed lines are in no measured denominator and the first-party coverage figures are unchanged by them; per-file coverage gates on `QfcDatamodel` are therefore not used, and AC23's whole-assembly "not lower than baseline" comparison is the coverage criterion. + +### Boundaries and invariants to preserve: + +Contract of the counted pin, in one sentence: the fixture writes null into the shared static only when the last live pin releases, only if the fixture itself installed the parked instance, and only if the field still holds that instance. + +Trace of two pins through the new code (the accept path and the two guard paths): + +- Pin A on a null field: count 0 -> 1, field null -> parked, ownership flag false -> true. Scope A returned. +- Pin B while the field holds parked: count 1 -> 2, field untouched, flag untouched. Scope B returned. +- Dispose A: count 2 -> 1; count is not zero, so the field is untouched. (Before the fix this step nulls the field; this is the regression test's middle assertion.) +- Dispose B: count 1 -> 0; flag is true and the field still references parked, so the field is written null and the flag cleared. (The regression test's final assertion.) +- Guard, foreign value: if a transaction installed a live dispatcher before any pin, the first pin finds a non-null field, installs nothing and leaves the flag false; at count zero nothing is written. The existing fixture test `EnsureDispatcher_WhileATransactionHoldsALiveDispatcher_DoesNotReplaceIt` (R1) and new test 3 pin this. +- Guard, field changed under the pin: if the count reaches zero with the flag true but the field no longer references parked (a transaction installed over it), nothing is written; the flag stays true so the next pin cycle can revert the parked value once it is back. Residual recorded in the class doc: when a transaction restores parked after the last pin released, parked remains installed with zero pins. This is the pre-fix leak shape (a non-null parked dispatcher) and is benign. Before this item, R4 reached this branch (it installed `liveA` over the pinned parked value); after the R4 restructure no test in the repository installs over a pinned value, which the census records per invocation (no `Install` call between a pin's acquisition and its release). +- Guard, idempotent dispose: a second `Dispose` on the same scope performs no decrement (existing test R3 `EnsureDispatcher_ScopeDisposedTwice_IsIdempotent`). +- Discarded scope: the count never returns to zero, so the parked dispatcher stays installed for the process lifetime. This is the same end state as today's discard, so no caller regresses, but the doc comments must say "a discarded scope pins for the process lifetime" rather than "leaks exactly as the pre-fix helper did". + +Other invariants preserved: +- Lock ordering stays `TransactionGate` then `FieldLock`; `EnsureDispatcher` still never acquires `TransactionGate`; `FieldLock` regions remain straight-line with no wait, thread creation or await (the parked dispatcher is still obtained before the lock is taken). +- `UiThreadDispatcherTransaction` (`Install`, `Dispose`, `Exchange`, `CompareExchange`) and the three monotonic gate counters are unchanged. +- `EmailMoveMonitorTests` (`[DoNotParallelize]`, read-only snapshot of `Current`) is unaffected: counting never writes outside pin acquire/release. +- Existing fixture tests R1 to R6 and the #743 / #882 counter tests pass without changes to their assertions or `because` texts. R4 loses its baseline pin and gains a `try/finally`; its two assertions and `because` texts are unchanged. + +### Dependencies or blocked work: + +- Depends on #950 having merged (it introduced the pin and the fixture tests this item extends). The research confirms the pin exists on `origin/main` 94287369. +- Issue #972 is folded into this item (amendment 1.2). The pull request for this item closes both issues: its body carries the two closing lines `Closes #968` and `Closes #972`, and no other issue number appears next to a closing keyword (a closing keyword closes an issue even inside a negated sentence). + +### Implementation strategy (what changes, not sequencing): + +#### Files/modules to change: + +Write set (the backticked paths in this list constitute the complete change footprint; everything else cited in this document is read-only context): + +- `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs` (fix: pin counter, ownership flag, counted dispose; D1 docs) +- `QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs` (delete two dead calls; D5 delete private `BuildExecutingViewer` and call the shared helper; D6 arrange comment) +- `QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs` (D2 wrapper doc; D5 shared-helper doc sentence) +- `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs` (D3 doc rewrite; D4 `try/finally`) +- `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs` (new; regression test and three specification tests) +- `QuickFiler.Test/QuickFiler.Test.csproj` (new `` item adjacent to the existing fixture-test item; amendment 1.2 adds the two `TestSupport\SynchronousBackgroundWorker.cs` and `TestSupport\ArmingFakeTimeProvider.cs` items after the existing `TestSupport\DedicatedWorkerThread.cs` item) +- Folded scope (amendment 1.2): + - `QuickFiler/Controllers/QfcDatamodel.cs` (production; remove four caller-free members, their commented-out references and the empty region; retarget one `nameof`) + - `QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs` (production; `_remainingLoadActive` doc comment and the stale `TryUnhookOrReplace` line-range citation; comment-only) + - `QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs` (new; shared worker helper and starter) + - `QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs` (new; armed-timer signal for the liveness tests) + - `QuickFiler.Test/Controllers/QfcDatamodelLivenessTests.cs` (helper removal, worker disposal, `StartHeldOpenLoader` signature, liveness test rewrite, doc rewording) + - `QuickFiler.Test/Controllers/QfcDatamodelTeardownTests.cs` (helper removal, doc rewording) + - `QuickFiler.Test/Controllers/QfcInitEmailQueueZeroBatchTests.cs` (helper removal, worker disposal, doc rewording) + - `QuickFiler.Test/Controllers/QfcDatamodelTests.cs` (sibling liveness test rewrite, worker disposal) +- Evidence (Markdown projections only, per CLAUDE.md "Committed Test Evidence Format"; the paths below are the named deliverables, and the plan's one-artifact-per-task files under the same `evidence/baseline/`, `evidence/regression-testing/`, `evidence/qa-gates/` and `evidence/other/` folders are part of the footprint and are enumerated in the plan's Write Set): + - `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/toolchain-baseline.md` + - `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/coverage-summary.md` + - `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/baseline/coverage-jacoco-projection.md` + - `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/fail-before-pin-count.md` + - `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/pass-after-pin-count.md` + - `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/regression-testing/concurrent-set-test-summary.md` + - `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/call-site-census.md` + - `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/file-line-counts.md` + - `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/prohibited-constructs-grep.md` + - `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/toolchain-final.md` + - `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/coverage-summary.md` + - `docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/evidence/qa-gates/coverage-jacoco-projection.md` + +Read-only context (not backticked on purpose): UtilitiesCS/Threading/UiThread.cs (static getter throws `InvalidOperationException` when null; `Initialize` and `ResetForTesting` writers), QuickFiler/Controllers/QfcItemController.FocusAndTheme.cs (`SetThemeDark`/`SetThemeLight`), UtilitiesCS/HelperClasses/ThemeHelpers/Theme.cs (`SetQfcTheme(bool)` dispatches through `_uiDispatcher`), scripts/vscode/TaskMaster.cli.runsettings, scripts/vscode/Invoke-MSTestWithCoverage.ps1, QuickFiler.Test/Helper Classes/EmailMoveMonitorTests.cs. + +#### Functions/classes/CLI commands impacted: + +- `UiThreadDispatcherFixture.EnsureDispatcher()` and the private `EnsureScope` class: counted acquire and release as traced above. Public shape (`internal static IDisposable EnsureDispatcher()`) unchanged. +- `UiThreadDispatcherFixture` class doc: add the counting rule under `FieldLock`, the install-ownership rule, the discard consequence and the transaction-restores-while-pinned residual (D1). `EnsureDispatcher` doc and `EnsureScope` doc rewritten: remove "a discarded scope leaks exactly as the pre-fix helper did" and "A scope that installed nothing carries null and is a no-op". +- `QfcItemControllerTestSupport.EnsureUiThreadDispatcher()`: body unchanged (pure forwarder). Doc rewritten (D2): describe the counted pin; name the fixture tests as the remaining legitimate callers; remove "Needed for members that still delegate to a callee using the static", "Becomes moot once the callee routes through the injectable dispatcher seam" and "leaks exactly as the pre-issue-#493 void helper did". +- `QfcItemControllerTestSupport.BuildExecutingViewer()`: doc sentence "Mirrors the private static BuildExecutingViewer() in QfcItemController.FocusAndThemeTests.cs, which is not reachable from another test file" removed (D5); it becomes the single shared implementation. +- `QfcItemController_FocusAndThemeTests`: private `BuildExecutingViewer()` deleted and every caller switched to the shared helper (D5; both implementations execute `Invoke`/`BeginInvoke` synchronously, so no behaviour change); the two `EnsureUiThreadDispatcher()` calls deleted; the arrange comment of `SetThemeDark_FromNormal_SelectsDarkNormalTheme` reworded to state that the queued theme application is absorbed by the theme's injected `IUiDispatcher` mock, which is why the shared static is irrelevant (D6), with `SetThemeLight_FromNormal_SelectsLightNormalTheme` carrying a matching or referencing comment. +- `QfcItemController_UiThreadDispatcherFixtureTests.Transaction_SecondCallerCannotInstallUntilTheFirstRestores` (R4): the baseline pin that #950 added (`using (IDisposable baseline = QfcItemControllerTestSupport.EnsureUiThreadDispatcher())`, opened after `transactionA` acquired the gate) is removed (amendment 1.1, option (a)). Rationale: #950 added the pin solely to fence the gate-free writers of the two theme tests, which this item deletes; after the deletion the census proves that every remaining pin nests inside a held transaction, so while `transactionA` holds the gate no other class can write the field, and every other transaction restores before it releases, which is all R4's two assertions need; the pin as placed outlived its gate hold (released after `transactionA.Dispose()` and after `transactionB` completed) and installed `liveA` over a pinned parked value, the one shape that reaches the flag-true-but-field-changed branch; and removing it, rather than releasing it before `transactionA.Dispose()` (option (b)), leaves that branch unreached by any test and keeps the fixture test file smaller. Option (b) was rejected because it keeps the install-over-pinned-parked shape and leaves the parked dispatcher installed with the ownership flag set after every R4 run. The doc paragraph is rewritten (D3) to replace "no other class may dispose an ensure scope holding the parked dispatcher (W2)" with the counting guarantee and the nesting invariant, keeping the `UiThread.Initialize` (W5) residual; `transactionA` is wrapped in `try/finally` with the explicit in-body `transactionA.Dispose()` retained and the `finally` re-dispose relying on the idempotency proved by `Transaction_DisposedTwice_DoesNotOverReleaseTheGate` (D4). R4's two assertions and their `because` texts are unchanged. +- New `QfcItemController_UiThreadDispatcherPinCountTests` (MSTest + FluentAssertions; Moq is not needed and no unused `using` is added; `[Timeout(60000)]` convention of the sibling fixture test file). Proposed names (digit-free so they can be cited in acceptance criteria): + 1. `EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease` (regression; fails before). Arrange: a transaction from `UiThreadDispatcherFixture.BeginTransactionAsync()` disposed in a `finally` (the shape of the sibling fixture tests R2 and R3); `transaction.Install(null)`; `pinA`, `pinB` from `EnsureUiThreadDispatcher()`; `afterBothPins = Current`. Act: dispose `pinA`, read `afterFirstRelease`; dispose `pinB`, read `afterLastRelease`. Assert: `afterBothPins` not null; `afterFirstRelease` same as `afterBothPins` (because "a holder that did not take the last pin must not lose the dispatcher"); `afterLastRelease` null (because "the last release reverts the fixture's own seeding"). + 2. `EnsureDispatcher_TwoPinsHeld_ReleaseOrderDoesNotChangeTheOutcome` (specification; passes before and after). Same arrange; dispose `pinB` first, then `pinA`; identical assertions. + 3. `EnsureDispatcher_UnderATransactionHoldingALiveDispatcher_ReleasingAllPinsLeavesTheLiveDispatcher` (specification; passes before and after; extends R1). Transaction installs a dispatcher from `StartRunningDispatcher`; two pins taken and released; `Current` still the live dispatcher. The live dispatcher is shut down through `ShutdownDispatcher` in a `finally`. + 4. `EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores` (specification; passes before and after). After test 1's sequence within the same transaction, a single fresh pin installs and its release restores null; then, in a second transaction that installs the parked instance captured in the first, one pin taken and released leaves that value in place (the discriminating check for the flag reset: had the last release left the install-ownership flag set, this release would revert a value the fixture did not seed). +- CLI commands: none added or changed. + +#### Data flow and validation changes: + +None at the production level (the folded production edits remove unreachable members and rewrite comments only). Test-side: the fixture's internal state gains two fields; all reads and writes of them occur inside `lock (FieldLock)`. + +#### Error handling and logging updates: + +None. The fixture does not log. The D4 `try/finally` changes failure propagation only: a throw between gate acquisition and the explicit dispose now releases the gate immediately instead of holding it until the 120 s bound or until the #743 counter test surfaces it. + +#### Rollback/feature-flag considerations (if applicable): + +Not applicable; no feature flag. The production edits remove unreachable code and rewrite comments, so no runtime behaviour changes. Rollback is a revert of the branch. + +### Technical specifications (interfaces/contracts): + +#### Inputs/outputs and formats: + +- `UiThreadDispatcherFixture.EnsureDispatcher()` -> `IDisposable` (unchanged signature). Post-condition: the shared static is non-null; the pin count is one greater than before. +- `IDisposable.Dispose()` on the returned scope: first call decrements the pin count and conditionally writes null as traced above; later calls are no-ops. + +#### Required configuration keys and defaults: + +None. The CLI runsettings (`Workers=0`, `Scope=ClassLevel`) are unchanged. + +#### Backward-compatibility expectations: + +All existing callers compile and behave as before from their own point of view: a scope that installed nothing still nulls nothing on dispose; a single pin on a null baseline still installs and still reverts on dispose; a discarded scope still leaves the parked dispatcher installed. + +#### Performance constraints (latency/throughput/memory): + +Not applicable. The additional work per pin is one integer increment or decrement and one boolean read under a lock that is already taken. + +## Assumptions, Constraints, Dependencies +- Assumptions (environment, data, access): + - `QuickFiler.Test` is a legacy (non-SDK) project targeting .NET Framework 4.8.1 with explicit `` items; a new test file is invisible to the build until its item is added. + - The MSTest adapter runs each test assembly in its own AppDomain, so the `UtilitiesCS.Test` mechanisms over the same static cannot interleave with this fixture (cited from #950 research; not re-verified). + - `SetupAssemblyInitializer` does not write the shared static, so a class run alone starts from a null baseline (relevant to the fail-before run). +- Constraints (budget, performance, compatibility): + - Every touched or added C# file stays at or under the five-hundred-line limit measured as total physical lines. Pre-change counts (research D7, content lines): focus-and-theme tests 497, fixture tests 470, test support 440, fixture 342. Expected direction: focus-and-theme tests shrink by roughly 15 lines (two calls and the 18-line private helper with its blank line removed, three comment lines added); fixture tests grow by 2 (the removed four-line pin header and its closing brace are replaced by a two-line `try` header and a five-line `finally` block) with a zero-net D3 doc replacement; fixture grows by roughly 32. + - MSTest + Moq + FluentAssertions only (packages.config: MSTest 4.4.1, Moq 4.21.0, FluentAssertions 8.11.0). No new dependency. + - Prohibited constructs as listed in Scope. + - `*.csproj` is excluded from CSharpier by `.csharpierignore`; the `` edit is hand-authored and must match the existing item style. +- External dependencies (services, libraries, releases): none. + +## Data / API / Config Impact +- User-facing or API changes: none. +- Data or migration considerations: none. +- Logging/telemetry updates (if any): none. +- Compatibility notes (CLI flags, config schemas, versioning): none. + +## Test Strategy +Seeded from issue, with disposition: + +- Issue idea "Have the theme tests acquire and release the #950 dispatcher pin in class initialize and cleanup": superseded by orchestrator decision 1. The theme tests do not read the static, so pinning in that class protects nothing; the dead calls are deleted and the fixture is made sound for every holder instead (Approach B rejection, above). +- Issue idea "Write a deterministic regression test that releases a competing pin mid-test, and show the theme test fails without the fix": the first half is delivered as the fixture-level regression test (two pins, first released mid-test). The second half is unsatisfiable as worded (section 3 trace) and is replaced by the fail-before/pass-after run of that regression test (orchestrator decision 2). + +- Regression tests to add or update: + - Add `QfcItemController_UiThreadDispatcherPinCountTests` (four tests as specified above). Test 1 is the regression gate; tests 2 to 4 are specification tests and say so in their doc comments. + - Negative control (must isolate the fixture change): compile the new test file in with the fixture at its pre-fix state and run test 1 by fully qualified name (`QuickFiler.Controllers.Tests.QfcItemController_UiThreadDispatcherPinCountTests.EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease`) with `/Settings:scripts/vscode/TaskMaster.cli.runsettings`; capture the failure message as a test-result summary in `evidence/regression-testing/fail-before-pin-count.md`. Then apply the fixture change only and re-run; capture `Passed` in `evidence/regression-testing/pass-after-pin-count.md`. The only difference between the two runs is the fixture change. + - Existing fixture tests R1 to R6 and the #743 / #882 counter tests: run and pass with no changes to their assertions. + - Theme tests `SetThemeDark_FromNormal_SelectsDarkNormalTheme` and `SetThemeLight_FromNormal_SelectsLightNormalTheme`: run alone and in the concurrent set after the calls are deleted. + - Folded scope (amendment 1.2): rewrite `QfcDatamodelLivenessTests.DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle` and `QfcDatamodelTests.DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive` to the explicit-signal shape (addendum section 5.4). Fail-before: a fail-before exception dossier (`evidence/regression-testing/fail-before-exception..md`) records why a failing run of the old shape cannot be forced. Sensitivity: with the `sourceActive` lambda temporarily edited to `() => false` in the working copy, each rewritten test is run by fully qualified name and observed failing on its re-arm assertion; the edit is reverted (verified by `git diff` against the plan's base for that file) before any toolchain gate; the result is recorded as a labelled sensitivity check, not as a fail-before of the old test. Pass-after: both tests pass, alone and in the four-class datamodel set. + - Folded scope: all tests in `QfcDatamodelLivenessTests`, `QfcDatamodelTeardownTests`, `QfcInitEmailQueueZeroBatchTests` and `QfcDatamodelTests` pass after the helper consolidation and the disposal changes, with their assertions and `because` texts unchanged except in the two rewritten tests. +- Unit tests (MSTest) for the fixed behavior and boundaries: + - Counted release (test 1), order independence (test 2), foreign-value protection at count zero (test 3), ownership-flag reset (test 4), idempotent dispose (existing R3), no-install when non-null (existing R1), single pin on null baseline (existing R2). +- Edge cases and negative scenarios (invalid inputs, missing data, boundary values): + - Count reaches zero while a transaction has replaced the field: nothing is written (covered by the design trace; test 3 covers the flag-false branch; the flag-true-but-field-changed branch is documented as a residual and is not separately tested because constructing it requires a transaction to install over a pinned parked value; before this item R4 did exactly that, and after the R4 restructure no test in the repository does, which the census records per invocation). + - Discarded scope: documented, not tested (it would leave a permanent pin in the shared fixture for the rest of the run). +- Error handling and logging verification: + - D4: a throw inside the second-caller test's body now releases the gate through `finally`; verified by code review of the diff (no injected fault is added, because injecting one would require a seam in the fixture that does not exist and is not in scope). +- Coverage impact and targets for changed lines/modules: + - The #968 core changes are in a test assembly, which the coverage route excludes from instrumentation. The folded production edits are confined to the `[ExcludeFromCodeCoverage]` type `QfcDatamodel` and remove unreachable members and comments only (decision 7). First-party line and branch figures are expected to be unchanged within run-to-run noise. Baseline figures are captured before any edit on this branch (`evidence/baseline/coverage-summary.md`, `evidence/baseline/coverage-jacoco-projection.md`) and final figures after the last edit (`evidence/qa-gates/coverage-summary.md`, `evidence/qa-gates/coverage-jacoco-projection.md`). The acceptance criterion is "not lower than baseline"; the repository floors (line at least 80 percent, branch at least 75 percent on the testable denominator) continue to apply and are not re-derived here. +- Toolchain commands to run (format -> lint -> type-check -> test), in CLAUDE.md order, as one uninterrupted pass after the last edit: + 1. `dotnet tool run csharpier format .` then `dotnet tool run csharpier check .` + 2. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` + 3. `msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true` + 4. `scripts/vscode/Invoke-MSTestWithCoverage.ps1` (inner `vstest.console.exe` with `/Settings:scripts/vscode/TaskMaster.cli.runsettings`, `/InIsolation`, `/TestCaseFilter:TestCategory!=LiveOutlook`; fixed results directory `coverage\test-results` and trx name `mstest-coverage-run.trx`) + Non-vacuity: the two rebuild logs must contain no `Skipping target "CoreCompile"` line. Commands, exit codes and timestamps are recorded in `evidence/baseline/toolchain-baseline.md` (before edits) and `evidence/qa-gates/toolchain-final.md` (final pass). +- Supporting observations recorded as evidence: + - Call-site census (`evidence/qa-gates/call-site-census.md`): primary strategy content grep `EnsureUiThreadDispatcher\(\)|EnsureDispatcher\(\)` over all `*.cs`; cross-check strategy count-mode grep of the bare identifiers `EnsureUiThreadDispatcher|EnsureDispatcher` with per-line classification; member sets compared; every remaining invocation classified as nested (acquired and released inside a `BeginTransactionAsync` holder, with no `Install` call between acquisition and release) or as the forwarder. Expected post-change result: four invocations in the existing files (the forwarder plus three in the fixture tests R1 to R3) and ten in the new pin-count test class, each test-side invocation acquired and released inside a held transaction; zero in the focus-and-theme test file and zero in the second-caller transaction test. + - File line counts (`evidence/qa-gates/file-line-counts.md`): total physical lines of each file in the write set after the change. + - Prohibited-constructs grep (`evidence/qa-gates/prohibited-constructs-grep.md`): grep of the branch diff for `DoNotParallelize`, `Thread.Sleep`, `Task.Delay`, `Workers`, `Timeout(`, `Path.GetTempFileName`, `Path.GetTempPath` with the expected result (no additions; `Timeout(` only as the sibling file's existing constant convention in the new test class). + - Concurrent set (`evidence/regression-testing/concurrent-set-test-summary.md`): one vstest invocation under the CLI runsettings running `QfcItemController_UiThreadDispatcherFixtureTests`, `QfcItemController_UiThreadDispatcherPinCountTests` and `QfcItemController_FocusAndThemeTests` together; all tests `Passed`. This is a supporting observation (MSTest cannot be made to interleave classes on demand), not the regression gate. + - Folded scope (amendment 1.2): the legacy-member zero-caller proof re-run before the removal (`evidence/qa-gates/qfc-datamodel-legacy-callers.md`); the datamodel test set run (`evidence/regression-testing/datamodel-set-test-summary.md`: one vstest invocation under the CLI runsettings running `QfcDatamodelLivenessTests`, `QfcDatamodelTeardownTests`, `QfcInitEmailQueueZeroBatchTests` and `QfcDatamodelTests` together); the sensitivity check (`evidence/regression-testing/liveness-sensitivity-check.md`); a repository-wide count of `class SynchronousBackgroundWorker` declarations; and the post-change physical line count of `QfcDatamodel.cs` and every touched datamodel test file in `evidence/qa-gates/file-line-counts.md`. +- Manual validation steps (if required): none. + + +## Acceptance Criteria +- [x] AC1: Counted pin, non-last release: with two ensure pins held on a null baseline inside a fixture transaction, disposing the first pin leaves the shared dispatcher field holding the parked dispatcher; proved by the fail-before regression test `EnsureDispatcher_TwoPinsHeld_ReleasingTheFirstKeepsTheDispatcherUntilTheLastRelease` in `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs` passing after the fix. +- [x] AC2: Counted pin, last release reverts only the fixture's own seeding: disposing the final live pin writes null back only because the fixture itself installed the parked dispatcher and the field still holds it; proved by the final assertion of the fail-before regression test and by the specification test `EnsureDispatcher_TwoPinsHeld_ReleaseOrderDoesNotChangeTheOutcome` passing. +- [x] AC3: A foreign transaction value is never nulled by pin release: with a transaction holding a live running dispatcher, taking and releasing all pins leaves that live dispatcher in place; proved by the specification test `EnsureDispatcher_UnderATransactionHoldingALiveDispatcher_ReleasingAllPinsLeavesTheLiveDispatcher` passing, with its running dispatcher shut down through `QfcItemControllerTestSupport.ShutdownDispatcher` in a `finally`, and by the existing test `EnsureDispatcher_WhileATransactionHoldsALiveDispatcher_DoesNotReplaceIt` still passing. +- [x] AC4: Ownership flag is cleared on the last release: after a full two-pin cycle inside the same transaction, a fresh single pin on the null baseline installs the parked dispatcher and its release restores null; proved by the specification test `EnsureDispatcher_AfterAFullPinCycle_AFreshSinglePinStillInstallsAndRestores` passing. +- [x] AC5: Fail-before and pass-after evidence isolates the fixture change: the regression test is observed failing against the unmodified fixture with the pin-count test file compiled in, the captured FluentAssertions message names the first-release assertion and reports `found `, and the same test is observed passing after the fixture change with no other difference between the two runs; both runs are recorded as test-result summaries (no raw trx) in this feature's regression-testing evidence folder. +- [x] AC6: The pin-count test class labels its tests: the regression test's doc comment states that it fails before the fix, each of the three specification tests' doc comments states that it passes before and after the fix, and the class doc states why the regression lives at the fixture level (the theme path dispatches through the theme's injected dispatcher mock and never reads the shared static). +- [x] AC7: The dead theme-test calls are removed: a content grep of `QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs` for `EnsureUiThreadDispatcher` returns zero hits, and `SetThemeDark_FromNormal_SelectsDarkNormalTheme` and `SetThemeLight_FromNormal_SelectsLightNormalTheme` both pass. +- [x] AC8: Gated-caller census invariant holds: a census recorded in this feature's qa-gates evidence folder enumerates every invocation of `EnsureUiThreadDispatcher` and `EnsureDispatcher` across all C# files in the repository using two independently constructed search strategies whose member sets are compared and agree, and classifies every invocation other than the forwarder inside `QfcItemControllerTestSupport` as acquired and released inside a held `UiThreadDispatcherFixture` transaction (the scope's acquisition and its disposal both lie within the caller's transaction hold) with no `Install` call between the acquisition and the release. +- [x] AC9: The pin counter and install-ownership flag are private statics of `UiThreadDispatcherFixture`, every read and write of them occurs inside a `lock (FieldLock)` block, and the last-release null write is performed inline in the same critical section as the decrement rather than through the re-locking compare-exchange helper; verified by reading the fixture diff. +- [x] AC10: All existing fixture tests pass unchanged in behaviour: every test in `QfcItemController_UiThreadDispatcherFixtureTests` passes after the fix, and the diff of that file touches only the second-caller transaction test's doc comment, the removal of its baseline pin and its transaction disposal structure, leaving every assertion and `because` text unchanged. +- [x] AC11: Fixture documentation describes the counted pin: the class doc, the `EnsureDispatcher` doc and the scope class doc in `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixture.cs` describe pin counting under the field lock, install ownership, the discard consequence (a discarded scope pins for the process lifetime) and the transaction-restores-while-pinned residual; a grep of that file for `leaks exactly` and for `installed nothing carries` returns zero hits. +- [x] AC12: Wrapper documentation describes the counted pin: the doc comment of `EnsureUiThreadDispatcher` in `QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs` describes the counted pin and names the fixture tests as the remaining legitimate callers; a grep of that file for `Becomes moot`, for `leaks exactly` and for `still delegate to a callee` returns zero hits. +- [x] AC13: The second-caller transaction test's doc no longer asserts the obsolete invariant: the doc comment of `Transaction_SecondCallerCannotInstallUntilTheFirstRestores` states the counting guarantee and keeps the `UiThread.Initialize` residual; a grep of `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherFixtureTests.cs` for `no other class may dispose` returns zero hits. +- [x] AC14: The second-caller transaction test releases its gate on any throw: in `Transaction_SecondCallerCannotInstallUntilTheFirstRestores` the first transaction is disposed in a `finally` block in addition to the explicit in-body dispose, relying on the idempotency proved by `Transaction_DisposedTwice_DoesNotOverReleaseTheGate`; verified by reading the diff and by the test passing. +- [x] AC15: The duplicated viewer helper is removed: a grep of `QuickFiler.Test/Controllers/QfcItemController.FocusAndThemeTests.cs` for `private static Mock BuildExecutingViewer` returns zero hits, every former caller in that class uses `QfcItemControllerTestSupport.BuildExecutingViewer`, the shared helper's doc in `QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs` no longer says it mirrors a private copy that is unreachable from another test file, and every test in `QfcItemController_FocusAndThemeTests` passes. +- [x] AC16: The theme-test arrange comment is corrected: the arrange comment of `SetThemeDark_FromNormal_SelectsDarkNormalTheme` states that the queued theme application is absorbed by the theme's injected `IUiDispatcher` mock, which is why the shared static is irrelevant, and `SetThemeLight_FromNormal_SelectsLightNormalTheme` carries a matching comment or a reference to it; verified by reading the diff. +- [x] AC17: `EnsureSynchronizationContext` in `QuickFiler.Test/Controllers/QfcItemController.TestSupport.cs` is unchanged; the diff of that file contains no hunk touching it. +- [x] AC18: File-size limit: every touched or added C# file in the write set is at or under the five-hundred-line limit measured as total physical lines, and the post-change counts are recorded in this feature's qa-gates evidence folder. +- [x] AC19: No prohibited constructs: the branch diff adds no `DoNotParallelize` attribute, no `Thread.Sleep`, no `Task.Delay`, no retry loop, no temporary file, and no timeout increase, and the CLI runsettings file is unchanged; verified by the recorded grep of the diff in this feature's qa-gates evidence folder. +- [x] AC20: No production code change outside the folded scope (amended): the diff against the merge base, after excluding the paths already committed on the branch before the plan's first task (recorded at Phase 0 as the inherited committed set), lists only paths under `QuickFiler.Test/`, this feature's documentation folder, and exactly the two production paths `QuickFiler/Controllers/QfcDatamodel.cs` and `QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs`; no other path under `QuickFiler/` or any other production project appears. +- [x] AC21: The new test file is built and discovered: `QuickFiler.Test/QuickFiler.Test.csproj` carries a `Compile Include` item for `QuickFiler.Test/Controllers/QfcItemController.UiThreadDispatcherPinCountTests.cs`, and all four pin-count tests appear as passed in the coverage route's test-result summary. +- [x] AC22: Full toolchain pass: csharpier check, the analyzer rebuild, the warnings-as-errors rebuild and the MSTest coverage route complete in that order with every step passing in one uninterrupted pass after the last edit, the two rebuild logs contain no skipped compile target, and the commands with exit codes are recorded in this feature's qa-gates evidence folder. + - Note (2026-10-03, orchestrator ruling under plan D-6 and the AC22-under-DIRECT clause, following the #950 AC17 precedent): the intent of AC22 is unchanged; only the evidence source for the test stage changes. Locally, the first four steps passed in one uninterrupted pass with no skipped compile target, and the coverage stage ran by the DIRECT route because the P0-T16 probe recorded a deterministic shell-icon failure on this workstation (`Win32 handle that was passed to Icon is not valid`), a known environmental failure that reproduces on main; CI runs the shell-icon classes. The local DIRECT run passed 7365 of 7365 with coverage not below baseline (lines 85.35 to 85.36, branches 79.73 to 79.75). AC22 is checked off only from this pull request's own CI run on the final head, recording the run ID, the head SHA, the C# test-and-coverage job result, and the local DIRECT result. If CI fails any test, AC22 is not met. + - Check-off (2026-10-03T07-39): verified from PR #976 CI run 37120059960 on head 78e24a68ce523d18fba6b68c0905876b46f62ceb, conclusion success; format-check, build-analyzers, build-nullable and mstest-coverage all succeeded (MSTest 7388 of 7388 passed, first-party lines 85.99%, branches 80.12%); local DIRECT result 7365 of 7365 passed. Recorded in `evidence/qa-gates/ci-run-ac22.md`. +- [x] AC23: Coverage not reduced: first-party line and branch coverage after the change are each not lower than the baseline captured before any edit on this branch, with both figures recorded as the one-line summary plus the package-level JaCoCo projection in this feature's baseline and qa-gates evidence folders. +- [x] AC24: Parallel run of the three classes together passes: one vstest invocation under the CLI runsettings (workers zero, class-level scope) running `QfcItemController_UiThreadDispatcherFixtureTests`, `QfcItemController_UiThreadDispatcherPinCountTests` and `QfcItemController_FocusAndThemeTests` reports every test as passed, recorded as a test-result summary in this feature's regression-testing evidence folder. +- [x] AC25: One shared synchronous worker helper: a repository-wide content grep of every C# file for `class SynchronousBackgroundWorker` returns exactly one hit, in `QuickFiler.Test/TestSupport/SynchronousBackgroundWorker.cs`, which declares it `internal sealed` deriving from `BackgroundWorker` and carries the shared `StartSynchronously` starter; `QfcDatamodelLivenessTests.cs`, `QfcDatamodelTeardownTests.cs` and `QfcInitEmailQueueZeroBatchTests.cs` declare no nested worker class and no private `StartSynchronously`, and their doc text no longer says the helper is duplicated per file; `QuickFiler.Test/QuickFiler.Test.csproj` carries a `Compile Include` item for the new file; every test in the three classes passes. +- [x] AC26: The producer-liveness comment matches post-fix behaviour: the doc comment of `_remainingLoadActive` in `QuickFiler/Controllers/QfcDatamodel.QueueProcessing.cs` names `WorkerStarter` as the start point and states why `IsBusy` cannot replace the flag and why it is volatile; a grep of that file for `RunWorkerAsync` and for `written on the worker thread and read` returns zero hits (the separate comment on `_remainingLoadTask` in `QuiesceLoaderAsync` is checked for the same drift and corrected only if inaccurate), and the doc comment that cites `TryUnhookOrReplace` no longer carries a line range. +- [x] AC27: Caller-free legacy members are removed: before the removal, a zero-caller proof for the duplicate private `log` field, `Worker_RunWorkerCompleted`, the synchronous `LoadRemainingEmailsToQueue` and the worker-taking overload of `LoadRemainingEmailsToQueueAsync` is re-run against the tree with two independent search strategies whose member sets are compared and agree, and is recorded in this feature's qa-gates evidence folder; after the change `QuickFiler/Controllers/QfcDatamodel.cs` declares none of the four, contains no commented-out reference to them and no empty `Linked List Locking` region, its remaining `nameof` in the one-argument loader names `LoadRemainingEmailsToQueueAsync`, every `IQfcDatamodel` member is still implemented, and both rebuilds pass. +- [x] AC28: The datamodel file sits well under the size limit: `QuickFiler/Controllers/QfcDatamodel.cs` is at or under four hundred physical lines after the change, recorded with the before figure in this feature's qa-gates evidence folder. +- [x] AC29: Removed production lines carry no coverage loss: the type-level `ExcludeFromCodeCoverage` attribute on `QfcDatamodel` is unchanged, no removed member is referenced by any test, and the first-party line and branch figures satisfy AC23. +- [x] AC30: Test-owned workers are disposed: every `SynchronousBackgroundWorker` and every test-created `BackgroundWorker` constructed in `QfcDatamodelLivenessTests.cs`, `QfcInitEmailQueueZeroBatchTests.cs` and `QfcDatamodelTests.cs` is constructed in the header of a `using` block owned by the test method (no inline construction inside an `InitEmailQueue` argument and no construction inside a helper that does not return it), `StartHeldOpenLoader` receives its worker from the caller, and every test in those classes passes. +- [x] AC31: The dequeue-liveness tests use explicit completion signals: `DequeueNextItemGroupAsync_WhileLoaderStillProducing_KeepsPollingAfterWorkerIdle` and `DequeueNextItemGroupAsync_HighConfidenceMode_WaitsWhileSourceWorkerActive` contain no `Task.Yield` and no loop around a clock advance; each proves the gate re-armed by awaiting `Task.WhenAny` over the armed-timer signal of `ArmingFakeTimeProvider` (declared in `QuickFiler.Test/TestSupport/ArmingFakeTimeProvider.cs` and carried by a `Compile Include` item) and the pending dequeue, and completes by awaiting the dequeue task itself; a fail-before exception dossier in this feature's regression-testing evidence folder records why a failing run of the old shape cannot be forced; a labelled sensitivity check records each rewritten test failing on its re-arm assertion with the liveness lambda temporarily forced false and the production file reverted afterwards; both tests pass after the change. +- [x] AC32: The four datamodel test classes pass together: one vstest invocation under the CLI runsettings (workers zero, class-level scope) running `QfcDatamodelLivenessTests`, `QfcDatamodelTeardownTests`, `QfcInitEmailQueueZeroBatchTests` and `QfcDatamodelTests` reports every test as passed, recorded as a test-result summary in this feature's regression-testing evidence folder. + +### Scope-to-criterion traceability (amendment 1.2) + +| Scope item | Acceptance criteria | +|---|---| +| #968 core (counted pin, dead theme calls, D1 to D7) | AC1 to AC24 | +| #972 item 1 (one shared synchronous worker helper) | AC25 | +| #972 item 2 (`_remainingLoadActive` comment) | AC26 | +| #972 item 3 (`QfcDatamodel.cs` legacy members, file size, coverage) | AC27, AC28, AC29, AC20 | +| #972 item 4 (worker disposal) | AC30 | +| #972 item 5 (`transactionA` `try`/`finally` in R4, D4) | AC14 | +| #968 liveness comment (deterministic completion signal) | AC31, AC32 | + +## Risks & Mitigations +- Technical or operational risks: + - Counting changes a shared fixture used by four test classes. Mitigation: existing fixture tests R1 to R3 are the semantic guard for "installed nothing" and "single pin on null baseline"; they run unchanged and must pass. + - A discarded scope now holds a pin for the process lifetime. Mitigation: the census proves every caller disposes its scope inside a gated transaction; the doc states the consequence. + - The flag-true-but-field-changed branch is not directly tested. Mitigation: after the R4 restructure no test in the repository installs a transaction value over a pinned parked value (the census records, for every invocation, that no `Install` call lies between the pin's acquisition and its release), so the branch is reached by no test; it is documented as a benign residual. + - Removing `QfcDatamodel` members could break an unseen caller. Mitigation: the zero-caller proof is re-run against the pre-change tree at execution time with two independent strategies (AC27), the members are private, and both rebuilds compile every reference in the solution. + - Tests that relied on the parked dispatcher leaked by the deleted theme-test calls may now observe a null dispatcher (message "The UI dispatcher has not been captured"). Mitigation: the plan captures each failing test's message and stops for re-planning under the related-defect directive when that message appears. + - The rewritten liveness tests depend on `FakeTimeProvider` invoking due timer callbacks synchronously inside `Advance` and on `CreateTimer` being overridable (addendum section 5.2, web-verified). Mitigation: the sensitivity check shows each rewritten test fails crisply rather than hanging when the liveness signal is wrong; the existing precedent `QfcFormControllerSeamTests` already subclasses `FakeTimeProvider` and overrides `CreateTimer` in this project. + - `*.csproj` is outside CSharpier; a malformed `` item silently drops the new tests. Mitigation: the discovery acceptance criterion requires the four pin-count tests to appear in the test-result summary. +- Mitigations and rollbacks: revert the branch; no data or configuration is affected. + +## Rollout & Follow-up +- Release/rollout steps: merge through the standard PR route after the final toolchain pass; no deployment step. The PR body carries the two closing lines `Closes #968` and `Closes #972`, so merging closes both issues; no other issue number is placed next to a closing keyword. +- Post-fix monitoring or clean-up tasks: none; no new issue is required (research section 8 and addendum section 6.4 found no unrelated defects). +- Links: issue #968 (https://github.com/drmoisan/TaskMaster/issues/968) and issue #972 (https://github.com/drmoisan/TaskMaster/issues/972), both closed by this item's PR; related #950, #882, #743, #493, #424; research record docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T05-50-dispatcher-pin-call-sites-research.md; research addendum docs/features/active/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup-968/research/2026-10-02T22-20-qfc-datamodel-972-fold-research.md; #972 promoted record docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md. diff --git a/docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md b/docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md new file mode 100644 index 000000000..1b0a3279e --- /dev/null +++ b/docs/features/potential/promoted/2026-10-02-focus-and-theme-tests-leak-shared-dispatcher-setup.md @@ -0,0 +1,61 @@ +# focus-and-theme-tests-leak-shared-dispatcher-setup (Issue #968) + +- Date captured: 2026-10-02 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/focus-and-theme-tests-leak-shared-dispatcher-setup/ (Issue #968) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #968 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/968 +- Last Updated: 2026-10-02 +## Summary + +Tests in `QuickFiler.Test` `QfcItemController_FocusAndThemeTests` set up the shared UI-thread dispatcher (through `UiThreadDispatcherFixture.EnsureDispatcher()`) and do not release that setup. Under the parallel test regime, another test class that releases or resets the shared dispatcher can leave a theme test running against a null dispatcher. The #950 preparation found the exposure: its transaction-test fix releases a dispatcher pin at the end of the test, and the same exposure already exists through two other tests in that file. + +## Environment + +- OS/version: Windows 11 (local) and windows-latest (CI) +- Python version: n/a (C# / MSTest, Workers=0, Scope=ClassLevel) +- Command/flags used: standard MSTest coverage route +- Data source or fixture: `UiThreadDispatcherFixture` + +## Steps to Reproduce + +1. Run `QuickFiler.Test` in parallel. +2. Have a class that resets the shared dispatcher run concurrently with `QfcItemController_FocusAndThemeTests`. +3. A theme test can observe a null dispatcher. This is intermittent. + +## Expected Behavior + +Each test class acquires and releases the shared dispatcher through a scoped, reference-counted pin, so no class can null it while another still depends on it. + +## Actual Behavior + +The theme tests depend on dispatcher state they do not own or pin. + +## Logs / Screenshots + +- [ ] Attached minimal logs or screenshot +- Snippet: #950 preparation report; its plan carries the stop marker `THEME TEST NULL-DISPATCHER EXPOSURE OBSERVED`. + +## Impact / Severity + +- [ ] Blocker +- [ ] High +- [x] Medium +- [ ] Low + +## Suspected Cause / Notes + +This is the same family as #950 and #882: raced static test-fixture state. It must not be fixed with `[DoNotParallelize]`, Workers=1 or retries. Find and own the shared state. Sequence it after #950 merges, because #950 introduces the pin. + +## Proposed Fix / Validation Ideas + +- [ ] Have the theme tests acquire and release the #950 dispatcher pin in class initialize and cleanup. +- [ ] Write a deterministic regression test that releases a competing pin mid-test, and show the theme test fails without the fix. + +## Next Step + +- [x] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch diff --git a/docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md b/docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md new file mode 100644 index 000000000..1e89a9407 --- /dev/null +++ b/docs/features/potential/promoted/2026-10-02-qfc-datamodel-950-review-residuals.md @@ -0,0 +1,73 @@ +# qfc-datamodel-950-review-residuals (Issue #972) + +- Date captured: 2026-10-02 +- Author: Dan Moisan +- Status: Promoted -> docs/features/active/qfc-datamodel-950-review-residuals/ (Issue #972) + +> Automation note: Keep the section headings below unchanged; the promotion tooling maps each of them into the GitHub bug issue template. + +- Issue: #972 +- Issue URL: https://github.com/drmoisan/TaskMaster/issues/972 +- Last Updated: 2026-10-02 +## Summary + +The #950 review (PR #971, merged at 860d67bf4) left five non-blocking residuals in QuickFiler production and test code: +1. Three duplicated synchronous-worker test helpers should be consolidated into one shared test-support helper. +2. The `_remainingLoadActive` comment should be reworded to match the new behavior. +3. `QuickFiler/Controllers/QfcDatamodel.cs` is at 495 of 500 lines and has apparently unused legacy members. Remove or move them before its next change. +4. The `SynchronousBackgroundWorker` instances in the liveness and zero-batch tests are not disposed. +5. `transactionA` in test R4 should be wrapped in `try`/`finally`, so a failing assertion cannot leak the transaction. + +The theme-test ensure-scope residual is tracked in #968. + +## Environment + +- OS/version: n/a +- Python version: n/a (C#, .NET Framework 4.8) +- Command/flags used: review of PR #971 +- Data source or fixture: n/a + +## Steps to Reproduce + +1. Read `QfcDatamodel.cs` and the #950 test files on `main`. +2. Compare them with the items above. + +## Expected Behavior + +- One shared helper. +- Accurate comments. +- `QfcDatamodel.cs` well under the 500-line limit. +- Disposable test objects are disposed. +- Transactions are released on failure. + +## Actual Behavior + +As listed in the summary. + +## Logs / Screenshots + +- [ ] Attached minimal logs or screenshot +- Snippet: PR #971 body, Follow-ups 1 to 5. + +## Impact / Severity + +- [ ] Blocker +- [ ] High +- [ ] Medium +- [x] Low + +## Suspected Cause / Notes + +#950 was scoped to removing the wall-clock waits and the dispatcher race. Coordinate this with #968, which touches the same test fixtures. + +## Proposed Fix / Validation Ideas + +- [ ] Extract the shared synchronous-worker helper, and add `using` disposal in each test. +- [ ] Add `try`/`finally` around `transactionA` in R4. +- [ ] Confirm the `QfcDatamodel` legacy members have no callers, then remove them. Keep changed-line coverage from dropping. +- [ ] Run the tests in parallel. Never use `[DoNotParallelize]` or Workers=1. + +## Next Step + +- [x] Promote to GitHub issue (bug-report template) +- [ ] Move to active fix folder / branch