Skip to content

A zero budget is not an unmeetable one — the identity-read timeout test raced its own query - #976

Merged
rbuergi merged 1 commit into
mainfrom
fix/unmeetable-budget-race
Aug 8, 2026
Merged

rbuergi merged 1 commit into
mainfrom
fix/unmeetable-budget-race

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Fixes a race in a test I merged an hour ago as part of #970.

What happened

LoadDbRoles_WithUnmeetableBudget_IsUnavailable_NotEmptyRoles went green on its own PR and then red on main (b96d20aca, shard 2) — and green again on the next commit. Classic timing, not a regression.

Why the budget was not unmeetable

IdentityRead.Bounded classifies via .Timeout(budget, …). That timer is scheduled; a warm cached synced query emits synchronously during Subscribe. So with TimeSpan.Zero:

  • warm cache → the value wins the race → Resolved → assertion fails
  • cold cache → the timer wins → Unavailable → assertion passes

Same test, same product code, opposite outcomes depending on cache state. "Zero" bounds the wait, it does not prevent the answer.

The fix

Drive the seam with Observable.Never<T>() — a source that cannot emit, so no verdict is reachable by construction and the classification under test is the only possible outcome.

This removes the race rather than widening a bound. No sleep, no retry, no relaxed assertion: the assertion text and its reasoning are byte-identical, and the 50 ms budget is now arbitrary because nothing can beat it.

Renamed to ReadThatReachesNoVerdict_IsUnavailable_NotEmptyRoles — the budget was never the interesting part.

What is deliberately not changed

The product code. Bounded's behaviour is correct: a read that answers inside its budget should resolve. The defect was in how the test tried to reach the unavailable branch.

The end-to-end companion (LoadDbRoles_WithRealBudget_ResolvesEmptyForUserWithNoGrants) keeps exercising the real workspace for the resolved path, so coverage of the live seam is not lost — only the branch that cannot be reached deterministically through a budget moved to the seam that can.

Verification

MeshWeaver.Auth.Test 143/143 in Release, -c Release -warnaserror clean.

Note the trap generalizes: a zero or near-zero .Timeout(...) is never a reliable way to force the timeout branch when the source can complete synchronously. Force it with a source that cannot produce a value.

…rce that cannot emit

LoadDbRoles_WithUnmeetableBudget_IsUnavailable_NotEmptyRoles passed CI on
its own PR and then failed on main. It was a race, not a regression.

IdentityRead.Bounded applies .Timeout(budget, …), and that timer is
SCHEDULED — while a warm cached synced query emits SYNCHRONOUSLY during
Subscribe. With TimeSpan.Zero the value therefore beats the timer whenever
the query is warm, and loses to it when it is cold. Same test, same code,
opposite results depending on cache state.

Driving the seam with Observable.Never removes the race rather than
widening a bound: a source that cannot emit reaches no verdict by
construction, so the classification under test is the only outcome
possible. The assertion and its reasoning are unchanged.

Renamed to say what it now pins — the budget was never the interesting
part, reaching no verdict is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 8, 2026 23:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a nondeterministic timing race in an auth test that tried to force IdentityRead.Bounded’s timeout path with a zero timeout, which can be beaten by synchronous emissions from a warm cached source. The test is updated to deterministically exercise the “no verdict within budget” classification by using a non-emitting observable.

Changes:

  • Replace the flaky “zero budget over real query” approach with Observable.Never<T>() to guarantee the Unavailable path is reached.
  • Rename the test to reflect the actual behavior under test (no-verdict classification, not “unmeetable budget”).
  • Expand the XML doc comment to capture the root cause and why TimeSpan.Zero was inherently racy here.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

685 tests  ±0   685 ✅ ±0   4m 45s ⏱️ -19s
 10 suites ±0     0 💤 ±0 
 10 files   ±0     0 ❌ ±0 

Results for commit 9f296bd. ± Comparison against base commit d058db2.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 877 tests  ±0   1 875 ✅ ±0   5m 52s ⏱️ -4s
   11 suites ±0       2 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 9f296bd. ± Comparison against base commit d058db2.

This pull request removes 1 and adds 1 tests. Note that renamed tests count towards both.
MeshWeaver.Auth.Test.ApiTokenRoleResolutionUnavailableTests ‑ LoadDbRoles_WithUnmeetableBudget_IsUnavailable_NotEmptyRoles
MeshWeaver.Auth.Test.ApiTokenRoleResolutionUnavailableTests ‑ ReadThatReachesNoVerdict_IsUnavailable_NotEmptyRoles

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files  ±0     11 suites  ±0   5m 4s ⏱️ +5s
1 878 tests ±0  1 772 ✅ ±0  106 💤 ±0  0 ❌ ±0 
2 000 runs  ±0  1 894 ✅ ±0  106 💤 ±0  0 ❌ ±0 

Results for commit 9f296bd. ± Comparison against base commit d058db2.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 710 tests  ±0   1 519 ✅ ±0   5m 53s ⏱️ -20s
   11 suites ±0     191 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 9f296bd. ± Comparison against base commit d058db2.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 216 tests  ±0   1 209 ✅ ±0   6m 35s ⏱️ -36s
   11 suites ±0       7 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 9f296bd. ± Comparison against base commit d058db2.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 193 tests  ±0   1 193 ✅ ±0   7m 37s ⏱️ -33s
   11 suites ±0       0 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 9f296bd. ± Comparison against base commit d058db2.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Test Results

   65 files  ±0     65 suites  ±0   35m 49s ⏱️ - 1m 47s
8 559 tests ±0  8 253 ✅ ±0  306 💤 ±0  0 ❌ ±0 
8 681 runs  ±0  8 375 ✅ ±0  306 💤 ±0  0 ❌ ±0 

Results for commit 9f296bd. ± Comparison against base commit d058db2.

This pull request removes 1 and adds 1 tests. Note that renamed tests count towards both.
MeshWeaver.Auth.Test.ApiTokenRoleResolutionUnavailableTests ‑ LoadDbRoles_WithUnmeetableBudget_IsUnavailable_NotEmptyRoles
MeshWeaver.Auth.Test.ApiTokenRoleResolutionUnavailableTests ‑ ReadThatReachesNoVerdict_IsUnavailable_NotEmptyRoles

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.

2 participants