fix(#507): guard Globals in RibbonController.Engines - #519
Merged
Conversation
4 tasks
`RibbonController.Engines` was declared `internal IAppItemEngines Engines => Globals.Engines;` with no null guard on `Globals`, unlike its sibling properties in the same file which use `Globals?.`. Any ribbon callback reaching `Engines` before `SetGlobals` had run threw `NullReferenceException` instead of returning `null`. Apply the null-conditional operator so `Engines` matches the sibling precedent already present in `RibbonController.Intelligence.cs`. That one line is the entire production change. Tests ----- Two MSTest regression tests, placed in a new partial-class file `TaskMaster.Test/Ribbon/RibbonControllerTests.Engines.cs` and registered in the legacy non-SDK csproj (which does not glob `*.cs`): - `Engines_WhenGlobalsNotAssigned_ReturnsNullInsteadOfThrowing` reproduces the defect against a bare `new RibbonController()`; verified failing pre-fix with `NullReferenceException`. - `Engines_WhenGlobalsAssigned_ReturnsGlobalsEngines` pins the assigned path by reference identity against a mock, so it proves forwarding rather than a null-to-null coincidence. The tests live in their own partial-class part because appending them to `RibbonControllerTests.cs` pushed that file to 513 lines, past the repository's 500-line cap (CLAUDE.md section 4 item 1). Feature review raised this as blocking; the split restores `RibbonControllerTests.cs` to 452 lines and adds a 73-line sibling, both under the cap. `CreateController()` stays in the primary part and remains reachable. `RibbonController` carries `[ExcludeFromCodeCoverage]` under the ratified VSTO/COM ribbon-handler exemption, so this adds no coverage surface; the exemption is neither removed nor widened. Verification ------------ Full toolchain, single clean pass: - `csharpier check .` - exit 0, 1489 files, 0 reformatted - msbuild `/p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true` - exit 0, 0 errors - msbuild `/t:Rebuild /p:TreatWarningsAsErrors=true` (the gate ci.yml enforces) - exit 0, 0 errors - vstest, 9 assemblies, `/EnableCodeCoverage` - exit 0, 6295/6295 passed Note: CLAUDE.md documents the nullable stage with `/p:Nullable=enable`, but ci.yml deliberately omits that flag and relies on per-file `#nullable enable` pragmas. The changed file has no such pragma. The divergence is recorded in `evidence/qa-gates/phase2-orchestrator-ci-gate-reconciliation.md` and reported separately; it is not a defect in this change. Out of scope ------------ - `RibbonViewer.cs` is untouched. Issues #505 and #506 affect it and are deferred to a feature landing after `bug/ribbon-engine-readiness-guard-503`. - Review finding that the null return relocates rather than eliminates the NRE (all 11 `Controller.Engines` call sites dereference unguarded, all in `RibbonViewer.cs`) is promoted to issue #518, to land after #503. Raw Cobertura dumps (81 MB, ~1.42M lines) were intentionally not committed, following commit d0955dc; numeric coverage headlines are retained in the markdown evidence artifacts. Closes #507 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011vjbBgWNjMZLxBexMQMwLd
drmoisan
force-pushed
the
bug/ribbon-controller-engines-null-unsafe-507
branch
from
August 8, 2026 21:00
f255a3f to
4be4106
Compare
- CLAUDE.md's nullable command adds /p:Nullable=enable but ci.yml does not; forced-flag CS86xx in a file with no #nullable pragma is a false blocker. - The .claude test-discovery exclusion discards every assembly when the agent is itself rooted under .claude/worktrees; filter on the relative path. - Never commit raw Cobertura dumps as evidence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011vjbBgWNjMZLxBexMQMwLd
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.
fix(#507): guard
GlobalsinRibbonController.EnginesSummary
RibbonController.EnginesdereferencedGlobalswith no null guard, so any ribbon callback reaching it beforeSetGlobalshad run threwNullReferenceException. One-line fix:Globals.EnginesbecomesGlobals?.Engines, matching the sibling precedent already in the same file.Globalspath by reference identity so it proves forwarding rather than a null-to-null coincidence.RibbonControllerTests.Engines.csbecause appending them inline pushedRibbonControllerTests.csto 513 lines, past the repository's 500-line cap. Feature review flagged this as blocking; the split restores 452 lines and adds a 73-line sibling.RibbonController.EngineCommands.csthat this fix falsifies. That file arrived with Bug: ribbon-engine-readiness-guard #503 and asserts theEnginesproperty "is not null-safe onGlobals" — true before this PR, false after. Comment-only; the readiness gate's behavior is unchanged.Why
TaskMaster/Ribbon/RibbonController.Intelligence.csdeclared:Globalsisprotected internal ApplicationGlobals Globals { get; set; }and staysnulluntilSetGlobalsruns. Every sibling accessor in the same file already guards it — theSBproperty usesGlobals?.Engines?.InboxEngines?....Engineswas the lone unguarded outlier, so it threw where its siblings returnednull:Severity is Low: the reachable window requires the callback to run before
SetGlobals, and the affected callbacks are configuration submenu items rather than primary commands. It is nevertheless a real inconsistency and an avoidable throw.What Changed
Core fix —
TaskMaster/Ribbon/RibbonController.Intelligence.cs(+1/-1)Tests —
TaskMaster.Test/Ribbon/RibbonControllerTests.Engines.cs(new, +73)Engines_WhenGlobalsNotAssigned_ReturnsNullInsteadOfThrowing— constructs a barenew RibbonController()without settingGlobals, asserts readingEnginesdoes not throw and yieldsnull.Engines_WhenGlobalsAssigned_ReturnsGlobalsEngines— assigns aMock<IAppItemEngines>instance ontoGlobals.Enginesvia reflection and assertsBeSameAs, so the test fails if the property ever stops forwarding.Supporting —
RibbonControllerTests.cs(+1/-1, class markedpartial),TaskMaster.Test.csproj(+1, registers the new file; this project is legacy non-SDK and does not glob*.cs).Comment correction —
TaskMaster/Ribbon/RibbonController.EngineCommands.cs(+8/-5). This file arrived onmainwith #503 while this PR was open. Its XML remarks explained that the readiness accessor readsGlobals?.Enginesdirectly because "The existingRibbonController.Enginesproperty is deliberately NOT used as the accessor because it is not null-safe onGlobals." That was accurate againstmainat the time and is falsified by this PR. The remark now describes the direct read as a deliberate decoupling and records that the property is null-safe as of #507. No code path changes — the accessor, the gate, and the separateSB/Triagesynchronization-context rationale are all untouched.Documentation — the remaining 38 changed files are the feature folder (issue, plan, remediation plan, both audit rounds, evidence artifacts), two agent-memory notes, and the promoted-issue document for #518.
Architecture / How It Fits Together
RibbonControlleris the VSTO ribbon's controller half;RibbonViewerholds the Office callback surface and calls through to it.Enginesis one of several accessors projectingApplicationGlobalsstate to those callbacks. The fix makes that projection uniform with its siblings: during the pre-SetGlobalswindow every accessor now yieldsnullrather than one of them throwing.Verification
Completed (run against the final head, single clean pass, in policy order):
csharpier check .msbuild TaskMaster.sln /t:Build /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=truemsbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=truevstest.console.exe <9 assemblies> /EnableCodeCoverage /InIsolation /TestCaseFilter:"TestCategory!=LiveOutlook"This run is against the rebased head. After this PR opened,
mainadvanced (#515 mergedbug/ribbon-engine-readiness-guard-503, #514 merged the QuickFiler keystroke fix) and the PR wentCONFLICTING. The branch was rebased onto2fe930f5; the only conflict was the shared.claude/agent-memory/feature-review/MEMORY.mdindex, resolved by union. The toolchain was re-run from scratch on the rebased head rather than carrying the pre-rebase result forward. The test total rose from 6295 to 6397 because #503 and #514 brought their own tests ontomain.Both new tests pass by name. The regression test was verified failing against the pre-fix source with
NullReferenceExceptionbefore the fix was applied.A note on the nullable stage.
CLAUDE.mddocuments this stage with/p:Nullable=enable, but.github/workflows/ci.ymldeliberately omits that flag and relies on each file's own#nullable enablepragma. Adding the flag force-enables nullable analysis across thousands of never-annotated files and yields ~414 errors that are red onmaintoo. The changed file carries no#nullablepragma, so theCS8603that the forced flag surfaces never reaches CI; the command CI actually runs returns 0 errors with this change applied. Full reasoning, including why a!orIAppItemEngines?annotation was rejected, is inevidence/qa-gates/phase2-orchestrator-ci-gate-reconciliation.md. TheCLAUDE.md-vs-ci.ymldivergence is a real documentation defect, reported separately.Recommended for a reviewer reproducing locally: run the four commands above in order from the repository root.
Backward Compatibility / Migration Notes
No breaking changes.
Engineskeeps its signature and its behavior wheneverGlobalsis assigned; only the previously-throwing unassigned path changes, and it now returnsnullin line with its siblings. No public API, no renamed paths, no removals.Risks and Mitigations
Controller.Engines.<member>without a guard, so a click in the pre-SetGlobalswindow still throws — one frame later, at the call site. This is tracked as Bug: Bug: ribbon-engines-callers-unguarded-null-deref #518 and is deliberately not fixed here. Post-rebase the picture improved slightly: Bug: ribbon-engine-readiness-guard #503 relocated these toRibbonViewer.EngineCommands.csand routedTestSpam_Clickthrough its newRunEngineCommandAsyncgate, so that one is now safe; the ten remaining are the Spam Config and Triage config submenu callbacks. Bug: Bug: ribbon-engines-callers-unguarded-null-deref #518 has been updated with the corrected file, count, and line numbers. This is not a regression — the siblingSBproperty already exhibits the identical unguarded-caller pattern onmain, and Bug: ribbon-engine-readiness-guard #503'sEngineGatedCommandRunneris the established mechanism for fixing them.RibbonControlleris[ExcludeFromCodeCoverage]under the ratified VSTO/COM ribbon-handler exemption, so this change adds no coverage surface. The exemption is neither removed nor widened. Test pass counts strictly improved (+2, 0 failures).Review Guide
TaskMaster/Ribbon/RibbonController.Intelligence.cs— the entire behavioral change, one line.TaskMaster.Test/Ribbon/RibbonControllerTests.Engines.cs— the two new tests.TaskMaster/Ribbon/RibbonController.EngineCommands.cs— comment-only correction of a rationale that this PR falsifies.TaskMaster.Test/Ribbon/RibbonControllerTests.csandTaskMaster.Test.csproj— mechanical: onepartialkeyword and one<Compile Include>line.Two housekeeping notes for reviewers. The branch was rewritten before this PR opened to drop two raw Cobertura dumps (37 MB + 44 MB, ~1.42M lines) that had been committed as evidence, following the convention set by
d0955dc4; insertions dropped from 1,419,897 to 2,254 and the numeric coverage headlines are retained in the markdown artifacts. The remediation-cycle narrative therefore lives in the committed audit artifacts rather than in commit granularity.Follow-ups
Controller.Enginescall sites inRibbonViewer.EngineCommands.cs. Its blocking dependency onbug/ribbon-engine-readiness-guard-503is now satisfied (fix(#503): guard engine-backed ribbon commands against the InboxEngines initialization race #515 merged), so it is ready to be worked. Consider bundling with Bug: ribbon-async-getpressed-signature #505 and Bug: ribbon-toggle-engine-fire-and-forget #506, which touch four of the same methods.CLAUDE.mdvsci.ymlnullable-command divergence, reported separately for triage.GitHub Auto-close