Skip to content

fix(quickfiler): close five QfcItemController lifecycle and error-handling defects - #619

Merged
drmoisan merged 12 commits into
epic/quickfiler-bug-family-integrationfrom
bug/qfc-item-controller-defects-484
Aug 26, 2026
Merged

drmoisan merged 12 commits into
epic/quickfiler-bug-family-integrationfrom
bug/qfc-item-controller-defects-484

Conversation

@drmoisan

Copy link
Copy Markdown
Owner

fix(quickfiler): close five QfcItemController lifecycle and error-handling defects

Summary

  • Repairs five defects in the QfcItemController partial classes: a double toggle in ToggleNavigation, a missing event unwiring path, swallowed failures and unobserved cancellation in MoveMailAsync, an undisposed read timer with stale collaborators in Cleanup(), and unguarded inputs in the WebResourceRequested handler.
  • Adds a real teardown contract: UnwireEvents() now detaches all 22 subscriptions that the wiring methods attach (16 intent, 6 control-tree) plus the WebResourceRequested delegate, closing an event-handler leak that kept controllers alive after pooling.
  • MoveMailAsync now propagates failures instead of swallowing them, preserves InnerException, and routes the user-facing notification through an injectable seam so the path is testable headlessly.
  • Adds 21 regression tests (938 to 959 total, all passing) and raises repository line coverage from 84.775% to 84.8323%.
  • No public API member is added or removed on any of the four production partials, and the QuickFiler/Interfaces files are untouched.

Why

QfcItemController is pooled and reused. Three of these defects only surface on the second and later use of a recycled controller, which is why they escaped earlier testing:

What Changed

Core fixes (4 production partials, +219/-16)

  • QfcItemController.EventWiring.cs — adds UnwireEvents(), UnwireIntentEvents(), UnwireControlTreeEvents(), and DetachWebResourceRequestedHandler(), with early-return guards for a null viewer or keyboard handler.
  • QfcItemController.ViewerSetup.csCleanup() disposes the read timer before nulling it, unwires events before releasing collaborators, and nulls _mailActions; TryResolveCidResource is extracted as an internal static seam with Uri.TryCreate and map-lookup guards.
  • QfcItemController.MailActions.cs — the swallowing catch now rethrows with the inner exception preserved and notifies through the MoveFailureNotifier seam; Token.ThrowIfCancellationRequested() is the first statement of the three async mail actions, outside the try, so cancellation cannot be re-wrapped as a move failure.
  • QfcItemController.FocusAndTheme.cs — removes the unconditional toggle; ApplyReadEmailFormat returns early once torn down.

Tests (5 files, +591/-2) — 21 new MSTest cases using Moq and FluentAssertions, covering wire/unwire symmetry by count, teardown statement order, cancellation, error propagation, and CID guard cases.

Documentation — the atomic plan, 50 acceptance criteria, 68 evidence artifacts, and the three review audits.

Architecture / How It Fits Together

Cleanup() is the single teardown entry point and now runs in a fixed order: unwire events, then dispose the timer, then null it, then release collaborators. The order is load-bearing — unwiring after releasing _itemViewer would lose the handler targets, and nulling the timer before disposing it would orphan an armed callback. Two seams were introduced purely to make host-bound paths testable without a live Outlook or WebView2 runtime: MoveFailureNotifier (defaulting to the modal dialog) and the internal static TryResolveCidResource.

Verification

Completed — full CLAUDE.md toolchain, one consecutive clean pass, zero restarts:

Stage Result
dotnet tool run csharpier format (9 owned files) exit 0, 0 rewritten
dotnet tool run csharpier check . exit 0, 1520 checked, 0 unformatted
msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true exit 0, 0 errors
msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true exit 0, 0 errors
vstest.console.exe QuickFiler.Test.dll /EnableCodeCoverage /InIsolation exit 0, 959/959 passed

The 5 remaining build warnings are pre-existing System.Reactive packages.config notices, unrelated to this change.

Coverage: repository line rate 84.775% to 84.8323%; changed-line coverage 126/132 added executable lines (95.5%), the 6 uncovered lines being the two authorized WebView2-runtime carve-outs; all five new production members at 100%.

Recommended — re-run the four toolchain commands above.

Backward Compatibility / Migration Notes

No breaking change. Public surface is unchanged; the new members are internal. Behavioral changes callers should know about:

  • MoveMailAsync now throws where it previously returned normally on failure. This is the intended fix, but any caller relying on silent failure will now observe an exception.
  • ToggleNavigation(async: true) now toggles once rather than twice, so its net effect changes for that path.

Risks and Mitigations

  • Teardown order regression — order is pinned by explicit statement-order tests and a dedicated evidence artifact.
  • Missed detach — wire and unwire sets are asserted by count (16/16 and 6/6) rather than by inspection.
  • Rollback — the change is confined to four QuickFiler/Controllers partials and their tests; reverting the commits restores prior behavior with no schema or config migration.

Review Guide

  1. QfcItemController.ViewerSetup.csCleanup() statement order (highest risk).
  2. QfcItemController.EventWiring.cs — wire/unwire symmetry.
  3. QfcItemController.MailActions.cs — cancellation placement and rethrow.
  4. Tests, then evidence and audits (mechanical, high volume).

The 68 evidence files and 3 audits account for most of the line count and can be skimmed.

Follow-ups

Two non-blocking residuals identified in review, recommended for promotion through the potential-to-issue lifecycle:

  • A residual TOCTOU window in ApplyReadEmailFormat: the guard re-reads fields after checking them, and parameterless Timer.Dispose() does not wait for an in-flight callback. A local capture would close it.
  • A silent skip in MoveMailAsync when OneDrive is absent.

Also surfaced for maintainer resolution: the repository publishes two different line-coverage floors — 80% in CLAUDE.md and 85% in .claude/rules/general-unit-test.md. This branch measures 84.8323%, which clears the first and not the second. The shortfall is pre-existing and repository-wide, and this branch improves it.

GitHub Auto-close

None. The base of this pull request is the epic integration branch rather than the default branch, so no auto-close keyword is emitted here. Issue references in scope: #484, #480, #481, #483, #485.

drmoisan and others added 12 commits August 26, 2026 10:29
…troller defects

Bootstraps the fresh agent worktree (repo-local .NET SDK, NuGet restore,
analyzer version back-fill, dotnet tool restore) and records the baseline
formatting, analyzer, nullable, test, coverage, file-size, exemption and
capacity-budget state for the five QfcItemController defects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mic58ikwEhpXsTnhz9FShE
Deletes the unconditional _itemPositionTips.Toggle(false) dispatch that
preceded the if (async) branch in ToggleNavigation(bool async), so each
branch produces exactly one flip instead of two. Tightens the existing
synchronous assertion to Times.Once() and adds an exact-count test for
the previously-untested async branch, with a shared executing-viewer
arrange helper appended to the test support file.

Addresses issue 480.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mic58ikwEhpXsTnhz9FShE
Extracts the decision half of the WebResourceRequested handler into
internal static TryResolveCidResource, which now guards an unparsable or
non-absolute URI, an empty final segment, a null map, a null match, and a
null AttachmentData, logging the two diagnosable cases at debug level.
The lambda is reduced to a two-statement adapter that reads the
attachments through a null-conditional ItemHelper, and its delegate and
CoreWebView2 source are captured into private fields for later detach.

Addresses issue 485.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mic58ikwEhpXsTnhz9FShE
…tion

Replaces the swallowing catch in MoveMailAsync with an error log, a
notification through the new MoveFailureNotifier seam marshalled on the
UI dispatcher, and a wrapped rethrow carrying the subject and destination
folder with the original as InnerException. Adds
Token.ThrowIfCancellationRequested() as the first body statement of
MoveMailAsync (outside the try), FlagAsTaskAsync and
EnumerateConversationAsync.

Addresses issue 483.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mic58ikwEhpXsTnhz9FShE
Preserves the formatting pass the interrupted run produced but did not
commit. No semantic change: all three hunks are CSharpier line-break
normalizations over the issue #483 MoveMailAsync work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mic58ikwEhpXsTnhz9FShE
…n cleanup

Issue #484. Cleanup() now disposes _emailIsReadTimer before nulling it and
releases _mailActions, and ApplyReadEmailFormat returns early when the state
it dereferences has been torn down, because a thread-pool timer callback
already in flight when the timer is disposed still executes.

Adds three regression tests (timer disposal, callback inertness, adapter
rebind) plus two shared arrange helpers. The adapter-rebind test is placed in
QfcItemController.ViewerSetupTests.cs under constraint C2 capacity rule 3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mic58ikwEhpXsTnhz9FShE
Issue #481. Adds UnwireEvents(), UnwireControlTreeEvents(), UnwireIntentEvents()
as exact mirrors of the wiring methods, plus DetachWebResourceRequestedHandler()
for the subscription made outside them, and calls UnwireEvents() from Cleanup()
before _itemViewer and _kbdHandler are released. A pooled viewer handed back
after Cleanup() therefore carries no subscription into the released controller.

The unwire methods carry teardown guards the wiring methods do not need, because
Cleanup() is reachable from an aborted initialization; the unguarded fail-before
run records that those guards are load-bearing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mic58ikwEhpXsTnhz9FShE
…vidence

Records the changed-file set, interface and forbidden-file stability, the
delivered Cleanup() statement order against constraint C4, public-surface
stability of the four owned partials, and the post-Cleanup() lifecycle
invariant, and checks off the five matching acceptance criteria.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mic58ikwEhpXsTnhz9FShE
Records the four-stage final pass: scope-locked csharpier format (0 of 9 files
rewritten), repo-wide csharpier check (0 unformatted of 1520), MSBuild analyzer
and nullable rebuilds (0 errors each), and vstest with coverage (959 of 959
passed, baseline 938 plus 21). Adds the coverage delta, file-size, test-policy,
real-ItemViewer and exemption audits plus the consecutive-pass confirmation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mic58ikwEhpXsTnhz9FShE
…r defects

Checks off the final 17 spec.md acceptance criteria and records the
reconciliation and status summary: 50 of 50 criteria delivered and verified,
with no criterion text modified. Two descriptive sub-clause divergences are
recorded against their cited evidence: the ViewerSetupTests relocation forced
by the constraint C2 capacity rule, and the notifier default delegate whose
measured line rate is 100 percent rather than the predicted zero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mic58ikwEhpXsTnhz9FShE
Records the [P8-T15] evidence: empty git status, a 73-path changed-file set
confined to the nine owned files and the feature folder, and the branch commit
history since BASE_SHA. Closes out the atomic plan checklist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mic58ikwEhpXsTnhz9FShE
…fects

Adds the policy-audit, code-review and feature-audit artifacts for the
issue #484 branch. Zero blocking findings; verdict is ready to merge.

The review records two non-blocking residuals recommended for promotion
through the potential-to-issue lifecycle: a TOCTOU race in
ApplyReadEmailFormat, and a silent skip when OneDrive is missing in
MoveMailAsync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mic58ikwEhpXsTnhz9FShE
@drmoisan
drmoisan merged commit 363bfcd into epic/quickfiler-bug-family-integration Aug 26, 2026
drmoisan added a commit that referenced this pull request Aug 26, 2026
Regenerated projection. Feature 484 (qfc-item-controller-defects) merged into the
integration branch as 363bfcd. Records the parallel-removal-gate misfire that
blocks worktree cleanup on epic runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mic58ikwEhpXsTnhz9FShE
drmoisan added a commit that referenced this pull request Aug 26, 2026
Projects the epic checkpoint after the fan-in-only run: 484 (PR #619), 446
(PR #625) and 498 (PR #626) are merged into the integration branch, whose tip
is now 8c8f769.

Records that 468 is halted rather than merged. Its stale-base merge is pushed
and its CI run is green, but its atomic plan is 120 of 180 tasks complete
(P13 stops at T3; P14, P15 and P16 never ran) and 14 of 29 acceptance criteria
in spec.md are unchecked, so no pull request was opened for it.

Also carries forward the child-PR CI trigger gap, the missing feature-review
artifacts, and the worktree-removal gate defect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
drmoisan added a commit that referenced this pull request Aug 26, 2026
Projects the epic checkpoint after feature 468 merged as PR #636. Four of
twelve features are now on the integration branch: 484 (#619), 446 (#625),
498 (#626) and 468 (#636), whose merge is the current tip 808bf46.

Records that 468 landed on a second pass. It was halted earlier in this
session at 120 of 180 plan tasks with 14 of 29 acceptance criteria unchecked
and no feature review; it was re-delegated to resume at P13-T4 and is now
180 of 180 tasks and 28 of 29 criteria with three audit artifacts carrying
zero blocking findings. AC-28 remains unchecked by design, because an
integration-branch merge cannot close the seven referenced issues.

Also records the repository-wide line-coverage shortfall against the
rules-file floor and the absence of feature-review artifacts for 498.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@drmoisan
drmoisan deleted the bug/qfc-item-controller-defects-484 branch August 28, 2026 11:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant