fix(TaskMaster.Test): add synchronization barrier to terminal-notification-hook test (#751) - #758
Merged
drmoisan merged 6 commits intoSep 3, 2026
Conversation
Preparation-mode delivery for issue #751 (terminal-notification-hook test lacks a synchronization barrier). Research established the race mechanistically (test-only, production untouched) and recommends awaiting the fixture's existing run.Terminal signal. spec.md documents the fix with a 10-item acceptance-criteria set. The atomic plan cleared atomic-executor preflight after 5 rounds (PREFLIGHT: ALL CLEAR, CONVERGENCE: NO FURTHER ROUNDS EXPECTED), covering the minimal fix, a hardened counter, a route-2 fail-before dossier, and a detached long-running-command execution convention for the full-suite vstest runs. Atomic execution, PR authoring, and CI monitoring are out of scope for this preparation-mode run.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Removes the race that made
TerminalNotificationHookFailure_DoesNotReplaceDispatchFaulta flaky required check. The test asserted on a counter that a terminal hook increments on a thread-pool continuation, with no barrier guaranteeing the hook had run by the assertion point. It failed once on CI withExpected sut.InvokedTerminalHookCount to be 1, but found 0, then passed on an identical-code re-run.This is a test-only fix. No production file is modified.
Closes #751
Root cause
ReleaseAsync()unblocks the production worker before the fixture's terminal hook has necessarily executed. The hook runs on a continuation, so the assertion and the increment were unordered with respect to each other. Research confirmed the race is confined to the test double: no production code path shares it, and there are no production overrides or subclasses of the affected member.The change
Three lines across two files, both under
TaskMaster.Test:TaskMaster.Test/AppGlobals/AppOlObjectsFolderTreeServiceTests.cs— inTerminalNotificationHookFailure_DoesNotReplaceDispatchFault:await run.Operation.ReleaseAsync();andsut.LoadCount.Should().Be(0);:(await GetExceptionAsync(await run.Terminal)).Should().BeSameAs(fault);sut.InvokedTerminalHookCount.Should().Be(1);becomesVolatile.Read(ref sut.InvokedTerminalHookCount).Should().Be(1);TaskMaster.Test/AppGlobals/AppOlObjectsFolderTreeServiceLifecycleTests.cs— inOnFolderTreeServiceInitializationTerminal:InvokedTerminalHookCount++;becomesInterlocked.Increment(ref InvokedTerminalHookCount);The barrier awaits the captured tuple member
run.Terminal, not a freshsut.NextTerminalread. This is load-bearing: the fixture swaps in a new, never-completed signal viaInterlocked.Exchangebefore completing the captured one, so a fresh read would never complete and the test would hang.The expected value stays
1. It is not relaxed, widened to a range, or deleted. No sleep, no polling, no retry, no[Ignore], no[DoNotParallelize], and no narrowed test filter was introduced.Note that the "Changed files overview" section of the generated PR context reports "Core logic changes: 0 files" and its churn-ranked list omits both files above, because the evidence artifacts in this branch outrank them by line count. The two files named here are the complete set of source changes.
Why the barrier closes the race rather than narrowing it
The hook increments, then
Interlocked.Exchangees in a fresh signal, thenTrySetResults the previous one — the exact instance the captured tuple holds. The increment therefore precedes the completion in program order with two full fences between them, so the awaiting continuation cannot observe a pre-increment state. Deadlock-freedom holds because the fixture signals before it throws.Verification
TaskMaster.Teston every run, target testPassedon every post-change run.csharpier format,csharpier check .(exit 0 across 1574 files), analyzer build, nullable build, full MSTest run. No step rewrote a tracked file and no restart was required.Fail-before evidence uses the dossier-plus-stress-record route rather than temporary instrumentation, because instrumenting the fixture to force a red would restate the race rather than reproduce it, and the instrumentation would not survive into the branch. The pre-change three-run series came out green, which is recorded as observed rather than asserted; the one genuine natural red remains the original CI failure.
Coverage
First-party production line coverage: 85.081% before, 85.059% after (56000/65820 and 55986/65820). Both clear the 85% floor. The 0.022pp movement is confined to a package with zero changed lines and is run-to-run drift.
The branch changes zero production lines, so the no-regression-on-changed-lines requirement has an empty subject. The changed test lines are themselves covered.
One procedural gap is recorded rather than papered over: the plan's own numeric coverage capture returned no figures, because it required the
.coveragesearch under each results directory to match exactly one file and vstest emits two under/InIsolation(the published attachment plus an in-run copy). That precondition was unsatisfiable as written. The figures above were recovered independently from the retained attachments during review. The canonicalartifacts/csharp/coverage.xmlis absent, which the policy audit records as a FAIL row on artifact presence; it was deliberately not generated here because doing so would have added a path outside the branch's asserted footprint.Review outcome
Feature review returned 0 blocking findings and 7 non-blocking observations. Audit artifacts are committed under the feature folder:
policy-audit.2026-09-03T17-30.md,code-review.2026-09-03T17-30.md,feature-audit.2026-09-03T17-30.md.All 10 acceptance criteria in
spec.mdare satisfied and checked, each verified independently during review rather than accepted from the executor's report.Follow-ups, not addressed here
Deliberately out of scope to keep the fix minimal, and recorded for separate triage:
[Timeout], and CI invokes vstest without/Settings, so the repository runsettings do not apply. Adding a method-level timeout is the suggested remedy..coveragelocate step needs a deterministic disambiguation rule instead of an exactly-one cardinality assertion.Footprint
Two source files, three added lines, net +1 line, zero production files. Everything else on the branch is documentation and evidence under the feature folder. The production file
TaskMaster/AppGlobals/AppOlObjects.FolderTreeService.csis byte-identical to its state at the branch point, confirmed by blob hash on both sides.Related context, referenced without closing: the test-determinism effort tracked in issue 729, and the CI failure observed on PR 746.