Repository navigation
Hosting.Test: end negative waits on a positive settled signal (#6817) - #6823
Conversation
Eight MeshWeaver.Hosting.Test tests each spent a full NotEmit(TestTimeouts.Quick) window - 36 s on CI - to pass: ~290 s of the project's ~580 s. A passing NotEmit always spends its whole window, so the cost cannot be removed by splitting. - MeshWeaver.Reactive.Assertions: NotEmitBefore(settled) - nothing may emit before a positive settled signal; fails (never passes) when the signal does not arrive. - BuildTriggeredSyncPinsTheBuiltCommitTest (5 tests): a sentinel push through the same processor; nothing but the sentinel may be fetched before its own fetch. The two tests that ran without a wired source now arrange one, so a wrong fetch had something to fetch. - GreenBuildSkipsASettledSourceTest: the next commit's clone ends "no second clone". - SystemOwnedSweepRetractsTheLastAdminTest.ABijectiveSync_RetractsNothing: the create is answered after its post-creation handlers, then one STORE read compared with the set before. The old "set did not empty" passed while a wrongly-run sweep retracted one of two grants (the last admin's delete is refused). - SaveMeshNodeRequestIsACheckedWriteTest.AParticipantPosingAsTheHubItself: a follower save to the same hub settles that the spoofed one was judged; the raw storage write it would start has no observable completion, so a Quick/10 window opens only after the settle. - WritingTests.md, ReactiveTestAssertions.md, testing skill: the rule and shapes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Independently scheduled imports can outlive the sentinel signal, allowing negative assertions to pass before forbidden fetches occur.
4 open findings
What changed in this PR
Reduces Hosting test runtime by replacing long negative waits with positive completion signals, addressing the cost that CI splitting only redistributed.
Changes:
- Adds
NotEmitBefore(settled)to reactive assertions. - Updates eight tests and strengthens grant-preservation checks.
- Documents signal-bounded negative assertions.
| File | Description |
|---|---|
test/MeshWeaver.Hosting.Test/SystemOwnedSweepRetractsTheLastAdminTest.cs |
Compares stored grants after sync creation completes. |
test/MeshWeaver.Hosting.Test/SaveMeshNodeRequestIsACheckedWriteTest.cs |
Adds a follower response before a shorter observation window. |
test/MeshWeaver.Hosting.Test/GreenBuildSkipsASettledSourceTest.cs |
Uses the next commit’s fetch to end the negative check. |
test/MeshWeaver.Hosting.Test/BuildTriggeredSyncPinsTheBuiltCommitTest.cs |
Adds sentinel-fetch checks and wired-source fixtures. |
src/MeshWeaver.Reactive.Assertions/README.md |
Lists the new assertion. |
src/MeshWeaver.Reactive.Assertions/ObservableAssertions.cs |
Implements NotEmitBefore. |
src/MeshWeaver.Documentation/Data/Architecture/WritingTests.md |
Explains completion-signal patterns. |
src/MeshWeaver.Documentation/Data/Architecture/ReactiveTestAssertions.md |
Documents the assertion contract and usage. |
.claude/skills/testing/SKILL.md |
Updates negative-assertion guidance. |
🧠 Review effort: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| public async Task<ObservableAssertions<T>> NotEmitBefore<TSettled>(IObservable<TSettled> settled, | ||
| string because = "", CancellationToken cancellationToken = default) |
There was a problem hiding this comment.
Agreed — added in 4467933: test/MeshWeaver.Testing.Xunit.Test/NotEmitBeforeContractTest.cs pins every failing branch (an emission before the signal, a signal that never arrives, one that completes empty, one that faults), the subject ending first (silence; the signal still decides), synchronous arming before the triggering action, and cancellation (propagates as OperationCanceledException). Both subscriptions are asserted released on success, failure, timeout and cancellation. All 7 pass in the project's full run.
Test Results 12 files - 14 12 suites - 14 28m 37s ⏱️ - 32m 48s Results for commit 330ede8. ± Comparison against base commit 22eb9e4. This pull request removes 7328 and adds 12 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
…#6817) The red-build test moved to a previous commit's build so the push's own fetch could settle the negative; that dropped the case where the pushed commit's OWN build failed. Restore it separately, with the red build's silence ended by the sentinel push rather than by a fetch of the same sha. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…6817) Forbidden emission, a signal that never arrives / completes empty / faults, a subject that ends first, and cancellation; both subscriptions released on success, failure, timeout and cancellation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ait — no 30 s literal The TestTimeoutLiteralRatchetGuard counted the new NotEmitBeforeContractTest bound (381 > 380). Every case there decides synchronously; the bound only caps a hung regression. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Closes #6817
Root cause
Eight
MeshWeaver.Hosting.Testtests each ended onNotEmit(TestTimeouts.Quick).Quickis 36 s on CI (Convergence / 3, CI factor 3), and a passingNotEmitalways spends its whole window. So those eight cost ~290 s of the project's ~580 s on every run. Splitting the project (#6798) only moved that cost between parts. The measured list (CI timing,CI=true, local trx) has 8 tests, not the ~16 the issue estimated:CI=true)Fix: each negative ends on a POSITIVE settled signal
ObservableAssertions.NotEmitBefore(settled)is new inMeshWeaver.Reactive.Assertions. It asserts that nothing is emitted before the settled signal emits, and it fails if the signal never arrives (silence without a settle proves nothing). A public member is added to a class, and no interface changes.BuildTriggeredSync…): a push at a sentinel sha goes through the same processor into the same Space, and nothing but the sentinel may be fetched before the sentinel's own fetch. Two tests used to run without a wired source, so a wrong fetch had nothing to fetch; they now arrange one.GreenBuildSkips…): the clone of the next commit ends "no second clone of this one".ABijectiveSync_RetractsNothing): the create is answered only after its post-creation handlers have run, and that includes the sweep. The STORE is then compared with the set read before.AParticipantPosingAsTheHubItself): a follower save to the same hub proves the spoofed save was judged. The raw storage write that save would start has no observable completion, soNotEmit(Quick / 10)opens only after the settle.Negative controls (product broken locally, then reverted)
ProcessWorkflowRun, which firesTriggerSyncAtCommitwhile still answering 1. Result:AGreenBuild_RecordsTheBuild_ButNoLongerImportsFAILED withExpected the observable not to emit before its settled signal because a green build records the build and imports nothing …, but it emitted 1111111111111111111111111111111111111111.ABijectiveSync_RetractsNothingFAILED withExpected collection {"…/_Access/Roland_Access"} to equal {"…/_Access/Roland_Access", "…/_Access/space-owner_Access"}.IsTwoWay) was inert, becauseIsSystemOwnedalready excludes two-way syncs.AParticipantPosingAsTheHubItself_WritesNothingFAILED withExpected the observable not to emit within 3.6s …, but it emitted True.After the reverts: full
MeshWeaver.Hosting.Test(withCI=true) is 1328 passed, 0 failed, 1 skipped (pre-existing).dotnet build -c Release -warnaserrorgives 0 warnings and 0 errors forMeshWeaver.Reactive.AssertionsandMeshWeaver.Hosting.Test.Docs
Doc/Architecture/WritingTestsgets a new section, "A negative assertion that spends a fixed window when a settled signal exists", plus a new row in the assertion table.Doc/Architecture/ReactiveTestAssertions, the assertions README and the testing skill are updated to match.Pairs-with: none — no public type or member removed; one public method added to a class.
🤖 Generated with Claude Code