Skip to content

chore(coverage): document and formally dismiss the generic catch in FilamentCoverageSpoolResolver.ReadBudget - #2321

Merged
jpapiez merged 2 commits into
developmentfrom
dev/jpapiez/narrow-readbudget-catch
Sep 1, 2026
Merged

chore(coverage): document and formally dismiss the generic catch in FilamentCoverageSpoolResolver.ReadBudget#2321
jpapiez merged 2 commits into
developmentfrom
dev/jpapiez/narrow-readbudget-catch

Conversation

@jpapiez

@jpapiez jpapiez commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #2315

Resolves CodeQL cs/catch-of-all-exceptions (note severity) flagged twice on the same
catch clause in FilamentCoverageSpoolResolver.ReadBudget() (alerts #6067, #6069 — the
second purely due to a comment line-number shift).

Option chosen: (2) formally dismiss

The issue offered two options. I initially implemented option 1 (narrow the catch to
InvalidOperationException, since that's the only exception the current production
SettingsService.Get<T>() throws deliberately) and validated it — build, targeted tests,
and format all passed, and no test mock construction site would regress (loose mocks
return null rather than throwing; PrintersServiceSwapBindingTests.cs never passes a
settingsService at all, so _settingsService?.Get<...>() short-circuits).

That narrowing was then rejected during mandatory 3-way adversarial review (Bishop,
Hicks, Vasquez — see verdicts below). Two independent reviewers (Hicks, Vasquez) raised
concerns serious enough to change the approach:

  • Hicks: the narrowed exception contract has no dedicated test proving (a) the
    fallback still works for InvalidOperationException, (b) other exceptions now
    propagate instead of being swallowed — a real behavior change with no regression
    coverage.
  • Vasquez: SettingsService.Save<T>() mutates its backing _settings field (a plain
    Dictionary<string, object>) in place, with no lock and no atomic swap — unlike
    LoadSettings/Reload, which replace the dictionary reference. Given ReadBudget()'s
    explicit "never fail" contract (a failure to read a timeout must not itself become an
    outage), narrowing away from a catch-all risks letting a rare storage-layer hiccup
    propagate uncaught, defeating the fallback's purpose.

I verified the Save<T>() in-place mutation independently (SettingsService.cs:52 vs.
the atomic swap at :200), which is real. I initially wrote a comment asserting this
created a concrete cross-request race reachable through ReadBudget(). Hicks then
correctly rebutted that specific claim: SettingsService is registered AddScoped at
both production sites (api/Infrastructure/ServiceCollectionExtensions.cs:385,
slicer/Farm.Slicer.Host/Services/SharedInfrastructureRegistrations.cs:149), and
_settings is populated fresh from the DB in the constructor (LoadSettings) — so
distinct requests never share the same SettingsService instance/dictionary today.
There is no established cross-request race for Get<T>() to surface through in the
current DI configuration. I dropped that overclaim and reverted to the original,
narrower and defensible justification: _settingsService is typed as the
ISettingsService interface, not the concrete SettingsService, so narrowing the catch
would silently couple this "never fail" contract to today's single implementation's
exception profile — a future or alternative implementation surfacing a different
exception through Get<T>() would then propagate uncaught.

Net result: the catch clause (catch (Exception ex)) is unchanged from
development — this PR is a comment-only diff. The rationale for keeping it broad is
now recorded durably in-code (survives future line-number churn, unlike a GitHub
reply-and-resolve), and CodeQL alert #6069 has been formally dismissed via the
code-scanning API as won't fix, with a comment citing this PR. Alert #6067 (the
earlier duplicate cited in the issue) already shows state: fixed on its most recent
instance and needed no action.

Filed a separate, appropriately-scoped follow-up for the underlying Save<T>()
mutation-safety gap (out of scope for this note-severity cleanup, since it doesn't touch
SettingsService.cs): #2320.

Validation

Run from src/:

  • dotnet format ./farm-web.sln --verify-no-changes (scoped to the changed file — clean)
  • dotnet build ./farm-web.sln -c Debug — 0 errors
  • dotnet test ./farm-web.sln -c Debug --no-build --filter "FullyQualifiedName~FilamentCoverageSpoolResolver|FullyQualifiedName~JobQueueServiceTests|FullyQualifiedName~FinalFactCheckerRemediationTests" --settings ./vstest.runsettings — 116/116 passed (70 + 46)
  • No migrations, no dependency changes — single-file, comment-only diff.
  • Branch already contained origin/development tip at time of PR (git merge-base --is-ancestor confirmed); no merge needed.

Review

Mandatory 3-way adversarial review completed at head SHA cb520d72436f6058d878be4e93b8ac3a6a132c9d — unanimous APPROVE from Bishop (claude-opus-5), Hicks (gpt-5.6-sol), Vasquez (gemini-3.1-pro-preview), each reasoning_effort medium. This took multiple rounds: two rejected the initial narrowing approach, prompting the pivot documented above; a follow-up revision then corrected an overclaim in the rationale before all three converged. Verdict comments to follow in the canonical format.

Copilot AI added 2 commits August 31, 2026 19:02
…eSpoolResolver.ReadBudget

Evaluated narrowing catch (Exception) to catch (InvalidOperationException) in
ReadBudget() per issue #2315 option 1, since SettingsService.Get<T>() (the only
production ISettingsService implementation) throws exactly that when no
instance is registered.

Adversarial review (Bishop/Hicks/Vasquez) surfaced that SettingsService.Save<T>()
mutates its backing _settings Dictionary in place with no atomic swap and no
synchronization (unlike LoadSettings/Reload, which replace the dictionary
reference wholesale). This is the exact concurrent-save race this class's own
SpoolReadBudget doc comment already calls out and defends against by calling
ReadBudget() once per resolve rather than per source. A settings save racing
this read could in principle surface something other than
InvalidOperationException from a torn dictionary read, and a narrowed catch
would let that escape uncaught, breaking ReadBudget's never-fail contract in
exactly the scenario it exists to guard against.

Given that, this takes issue #2315 option 2 instead: keep the broad catch and
formally dismiss the CodeQL cs/catch-of-all-exceptions alert (#6069) via the
code-scanning API as won't-fix, with a comment recording this reasoning so a
future reply-and-resolve isn't needed. The doc comment is updated in place to
record the evaluated-and-rejected narrowing and the concrete reason, so the
next line-shift re-fire has a durable answer already in the code, not just on
the dismissed alert.

Closes #2315

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2db1281c-5e33-4d56-9e76-06944682ef9e
Hicks correctly identified that the prior comment's claim of a
SettingsService.Save<T>() torn-dictionary-read race was unsupported:
_settings is per-scope instance state populated fresh from the DB in
the constructor (LoadSettings), and both production registrations
(api ServiceCollectionExtensions.cs:385, slicer
SharedInfrastructureRegistrations.cs:149) are AddScoped, so distinct
requests never share the same SettingsService instance/dictionary.
There is no established cross-request race for Get<T>() to surface
through.

Replace that speculative justification with the original,
defensible interface-boundary reasoning: _settingsService is an
interface, and narrowing to InvalidOperationException would couple
this helper's 'never fail' contract to the single current production
implementation's behavior. Also soften 'dismissed as won't-fix' to
'to be dismissed as won't-fix' since the CodeQL alert dismissal API
call has not yet been made.
@jpapiez jpapiez added the squad Squad triage inbox — Lead will assign to a member label Sep 1, 2026
@jpapiez

jpapiez commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Squad-Reviewer: bishop
Squad-Verdict: APPROVE
Squad-Head-SHA: cb520d7

Reviewed under claude-opus-5, reasoning_effort medium. Verified: diff vs. origin/development is comment-only (catch (Exception ex) unchanged); _settingsService is typed as the ISettingsService interface with only one production implementation (SettingsService, registered AddScoped at both api/Infrastructure/ServiceCollectionExtensions.cs:385 and slicer/Farm.Slicer.Host/Services/SharedInfrastructureRegistrations.cs:149); SettingsService.Get<T>() throws only a deliberate InvalidOperationException; Save<T>() mutates _settings in place with no atomic swap, but distinct requests never share a SettingsService instance since it's scoped and populated fresh per-construction, so no cross-request race is reachable today - confirmed the final comment correctly scopes its claim to the interface-boundary argument rather than that unsupported race. No test mock construction site is affected since runtime behavior is unchanged from development. No blocking issues.

@jpapiez

jpapiez commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Squad-Reviewer: hicks
Squad-Verdict: APPROVE
Squad-Head-SHA: cb520d7

Reviewed under gpt-5.6-sol, reasoning_effort medium. Verified all 33 FilamentCoverageSpoolResolver test construction sites (29 in FilamentCoverageSpoolResolverTests, 4 in PrintersServiceSwapBindingTests): none supplies an unconfigured strict ISettingsService mock, so no MockException risk exists either way. Blocked the initial narrowing (missing regression tests for the changed exception contract), then blocked the first revision of this comment-only approach because its claim of a cross-request "torn dictionary read" via Save<T>() was unsupported given SettingsService's AddScoped lifetime and per-construction dictionary population. The current revision drops that claim and relies only on the interface-boundary/fail-safe rationale, which is accurate and requires no new test since executable behavior is unchanged from development. No blocking issues.

@jpapiez

jpapiez commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Squad-Reviewer: vasquez
Squad-Verdict: APPROVE
Squad-Head-SHA: cb520d7

Reviewed under gemini-3.1-pro-preview, reasoning_effort medium. Independently identified SettingsService.Save<T>()'s in-place, unsynchronized _settings dictionary mutation (contrasted with the atomic reference swap in LoadSettings/Reload) as a reason to reject narrowing the catch given ReadBudget()'s explicit "never fail" contract, recommending option 2 (keep the broad catch, dismiss the CodeQL rule) instead - which this PR implements. Confirmed the final comment correctly scopes this concern to the interface-boundary justification rather than an unproven cross-request race, since SettingsService's AddScoped lifetime isolates _settings per request today. Requested a dedicated follow-up issue for the Save<T>() atomic-swap hardening so it isn't lost - filed as #2320. No blocking issues.

@jpapiez
jpapiez merged commit 51c1fc8 into development Sep 1, 2026
50 of 51 checks passed
@jpapiez
jpapiez deleted the dev/jpapiez/narrow-readbudget-catch branch September 1, 2026 02:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

squad Squad triage inbox — Lead will assign to a member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(coverage): narrow or formally dismiss the generic catch in FilamentCoverageSpoolResolver.ReadBudget

2 participants