Skip to content

test(ComboGate): wait until a recorded verdict is READABLE — the gate read before the write was visible - #3324

Merged
rbuergi merged 1 commit into
mainfrom
fix/combo-gate-read-after-write
Sep 4, 2026
Merged

rbuergi merged 1 commit into
mainfrom
fix/combo-gate-read-after-write

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Why

ComboGateRollTest failed twice on 2026-09-04, on two different methods:

run method assertion
33863349195 ANotVerifiableVerdict_NeitherClearsNorRefuses_AndTheUnverifiedRollIsRecorded verdict text lacked could NOT answer
shard 5 on #3319 ARedVerdict_BlocksTheRoll_AndNamesTheModuleThatWouldBreak "a refusal that leaves no trace is the silent freeze this gate must never become … found False"

#3319's entire diff is two YAML files and two Markdown docs — deploy/aks/scripts/values.observability.yaml, deploy/helm/templates/memex-portal/deployment.yaml, and two files under src/MeshWeaver.Documentation/Data/Architecture/. It cannot reach Memex.Portal.Shared.Test at all. That settles attribution without argument: this is the test's own race, and both failures are the same one wearing different assertions.

The mechanism

private Task Record(ComboVerification verdict) =>
    UpdatePolicyNodeType.RecordVerification(Mesh, verdict).FirstAsync()…

A completing stream.Update says the patch was accepted — not that every reader can see it. The gate under test reads the policy through its own stream. When the check ran before the verdict was visible, the gate honestly reported the state it could see —

no combo verification has been recorded for '9999.0.0-ci.1' on this instance

— and the assertion failed on text that was never about a defect. Both observed failures are that same read-after-write gap, surfacing through whichever assertion happened to inspect the verdict text.

What changes

Record now waits until the verdict is readable, on the same predicate a real reader uses — the verification for this candidate tag is present in the content.

No bound is widened, no retry is added, and the assertions are untouched. The test simply waits for the condition it actually depends on, which is the same cure CascadeTest and DisposalRaceNackTest got today.

A correction to the record

When the first of these failed on #3291, I attributed it to that PR's late-verdict change. That attribution was wrong — the class flakes on a diff that cannot reach it. #3291's revert stands on its own evidence (SubscribeDuringRecycleTest's documented contract, plus the measurement that the gate cleared none of #3261's reds), not on this failure.

Verification

Memex.Portal.Shared.Test → ComboGateRollTest 7/7 in Release.

Pairs-with: none — test-only.

🤖 Generated with Claude Code

…just written

`ComboGateRollTest` has now failed twice today on two different methods:

  * `ANotVerifiableVerdict_NeitherClearsNorRefuses_AndTheUnverifiedRollIsRecorded` (run 33863349195)
    — the verdict text lacked "could NOT answer";
  * `ARedVerdict_BlocksTheRoll_AndNamesTheModuleThatWouldBreak` (shard 5 on #3319) — "a refusal
    that leaves no trace is the silent freeze this gate must never become … found False".

#3319's entire diff is two YAML files and two Markdown docs, so it cannot reach
`Memex.Portal.Shared.Test` at all. That settles the attribution: this is the test's own race, and
both failures are the same one wearing different assertions.

`Record` awaited `RecordVerification(...).FirstAsync()`. A completing `stream.Update` says the
patch was ACCEPTED — not that every reader can see it. The gate under test reads the policy through
its own stream, so when the check ran first it honestly reported the state it could see ("no combo
verification has been recorded for '<tag>' on this instance") and the assertion failed on text that
was never about a defect.

Now the record is followed by a read-back on the SAME predicate a real reader uses: the verification
for this candidate tag is present in the content. No bound is widened and no retry is added — the
test simply waits for the condition it actually depends on, which is the cure the other timing
assertions in this repo got today (CascadeTest, DisposalRaceNackTest).

🚨 And a correction to the record: when the first of these failed on #3291 I attributed it to that
PR's late-verdict change. That attribution was wrong — the class flakes on a diff that cannot reach
it. #3291's revert stands on its own evidence (SubscribeDuringRecycleTest's documented contract,
and the measurement that the gate cleared none of #3261's reds), not on this.

Verified: Memex.Portal.Shared.Test ComboGateRollTest 7/7 in Release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

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.

🟡 Changes recommended

The new helper comment is now misleading about timeout/bound behavior and the read-back predicate should use the existing VerificationFor(...) helper to avoid duplicated logic and drift.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR updates ComboGateRollTest to eliminate a read-after-write visibility race by waiting until a newly recorded combo-verification verdict is readable through the policy node’s stream (not just accepted by the write observable), improving test determinism.

Changes:

  • Convert Record(ComboVerification) into a two-step operation: record verdict, then wait until the verdict is observable in UpdatePolicyContent.
  • Add/expand inline documentation explaining the underlying race and why a read-back is required.
File summaries
File Description
test/Memex.Portal.Shared.Test/ComboGateRollTest.cs Makes verdict recording wait for read visibility to avoid flakes caused by read-after-write propagation lag.
Review details

Suppressed comments (1)

test/Memex.Portal.Shared.Test/ComboGateRollTest.cs:620

  • The XML comment says this removes the race “without widening any bound”, but Record now performs two sequential waits, each independently bounded by Budget, so the total time this helper can take is up to ~2× Budget. Consider rewording the comment to reflect the actual timeout behavior (or refactor to a single overall timeout if you truly need an unchanged total bound).
    /// <para>So the record is followed by a read-back on the SAME predicate a real reader uses: the
    /// verification for this candidate tag is present in the content. That is the condition the
    /// test actually depends on, and waiting on it removes the race without widening any bound —
    /// the same cure as the other timing assertions fixed in this repo today.</para>
  • Files reviewed: 1/1 changed files
  • Comments generated: 2
  • Review effort level: Lite

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

Comment on lines +613 to +615
/// assertion then failed on text that was never about a defect. Measured twice on 2026-09-04
/// (runs 33863349195 and the shard-5 red on #3319, a PR whose whole diff is two YAML files and
/// two Markdown docs and therefore cannot reach this test at all).</para>
Comment on lines +629 to +630
await WaitForContent(c => c.ComboVerifications.Any(
v => string.Equals(v.CandidateTag, verdict.CandidateTag, StringComparison.OrdinalIgnoreCase)));
@rbuergi

rbuergi commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Confirming from #3319's side, and adding a control arm your attribution does not yet have.

The failure is exactly as you describe — run 33925367758, Run tests (shard 5), job 101193653326:

Memex.Portal.Shared.Test.ComboGateRollTest.ARedVerdict_BlocksTheRoll_AndNamesTheModuleThatWouldBreak [FAIL]
  MeshWeaver.Reactive.Assertions.AssertionException : Expected value to be true because a refusal
  that leaves no trace is the silent freeze this gate must never become, but found False.
  test/Memex.Portal.Shared.Test/ComboGateRollTest.cs(88,0)

Memex.Portal.Shared.Test  Total: 767, Errors: 0, Failed: 1, Skipped: 0, Time: 57.069s

The control arm: #3320 ran the same shard in the same window and passed it. Its build (33925271322) is completed/success and its Consolidate test results is green, on a head created ~10 minutes before #3319's. #3320's diff is also entirely non-C# — shell, Python, YAML fixtures and two Markdown docs under deploy/aks/scripts/ and Doc/Architecture/. So two PRs that both cannot reach Memex.Portal.Shared.Test ran the same shard minutes apart and one failed. That is a property of the test, not of either diff, and it rules out "some non-C# change perturbs the shard" as well as ruling out #3319 specifically.

One more datum from the same shard, since it discriminates between "the shard was unhealthy" and "this one test raced": every other project in shard 5 exited 0 —

[CI] MeshWeaver.Layout.Test          exit=0 elapsed=19s
[CI] MeshWeaver.Cli.Test             exit=0 elapsed=0s
[CI] MeshWeaver.Documentation.Test   exit=0 elapsed=14s
[CI] Memex.Portal.Shared.Test        exit=1 TESTFAIL (1 failing test(s) recorded in trx;
                                     the host completed normally) elapsed=58s

The host completed normally and recorded exactly one failure, so this is not a crashed or starved shard either.

I am not re-running #3319 and have not touched it — a passing re-run would only be the other arm of the race. I will merge main into it once this lands, which is the sanctioned move when CI fails on something the diff cannot reach.

Not in .github/known-flakes.json, for whoever decides whether it belongs there in the interim.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

251 tests  ±0   251 ✅ ±0   2m 7s ⏱️ -3s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 073ac49. ± Comparison against base commit df87a4a.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

475 tests  ±0   475 ✅ ±0   41s ⏱️ ±0s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 073ac49. ± Comparison against base commit df87a4a.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

368 tests  ±0   366 ✅ ±0   37s ⏱️ -25s
  3 suites ±0     2 💤 ±0 
  3 files   ±0     0 ❌ ±0 

Results for commit 073ac49. ± Comparison against base commit df87a4a.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

588 tests  ±0   396 ✅ ±0   2m 56s ⏱️ -1s
  3 suites ±0   192 💤 ±0 
  3 files   ±0     0 ❌ ±0 

Results for commit 073ac49. ± Comparison against base commit df87a4a.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

2 291 tests  ±0   2 291 ✅ ±0   2m 36s ⏱️ +16s
    3 suites ±0       0 💤 ±0 
    3 files   ±0       0 ❌ ±0 

Results for commit 073ac49. ± Comparison against base commit df87a4a.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    4 files  ±0      4 suites  ±0   1m 49s ⏱️ -1s
1 488 tests ±0  1 488 ✅ ±0  0 💤 ±0  0 ❌ ±0 
1 489 runs  ±0  1 489 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 073ac49. ± Comparison against base commit df87a4a.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Test Results

   15 files  ±0     15 suites  ±0   10m 48s ⏱️ -11s
5 461 tests ±0  5 267 ✅ ±0  194 💤 ±0  0 ❌ ±0 
5 462 runs  ±0  5 268 ✅ ±0  194 💤 ±0  0 ❌ ±0 

Results for commit 073ac49. ± Comparison against base commit df87a4a.

@rbuergi
rbuergi added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit abbe512 Sep 4, 2026
28 checks passed
@rbuergi
rbuergi deleted the fix/combo-gate-read-after-write branch October 10, 2026 13:10
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