Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ gave you no synchronous overload. Flag it and name the boundary it should have c
- **Test projects** (`test/**`). 🚨 `.ToTask()` is FORBIDDEN here too (2026-08-30): Rx completes its
`TaskCompletionSource` without `RunContinuationsAsynchronously`, so the awaiter resumes INLINE on the
signalling thread inside Rx's trampoline and every later `await` inherits that scheduler. Tests await
through `ObservableAwait.Await(ct)` (MeshWeaver.Fixture) or `ReactiveCompletion.ObserveCompletion`.
through `ObservableAwait.Await(ct)` (MeshWeaver.Messaging, in MeshWeaver.Messaging.Hub — product and test code alike) or `ReactiveCompletion.ObserveCompletion`.
- **A one-line SDK/MCP surface adapter** whose body is reactive, e.g.
`public Task<string> Patch(...) => ops.Patch(...).FirstAsync().ObserveCompletion(Report, ct);` — an external
signature may demand a `Task`; it is still never produced with `.ToTask()`.
Expand Down
11 changes: 11 additions & 0 deletions scripts/type-forwards.allow
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,14 @@
# $(MeshWeaverRoot) (core, MeshWeaver.Plugins, MeshWeaver.SocialMedia — the latter pinned
# pre-split). A forwarder is impossible here: the base would have to reference the AI assembly,
# which references the base (the documented reference-cycle case).

# #2771 — ObservableAwait leaves MeshWeaver.Fixture for MeshWeaver.Messaging.Hub (namespace
# MeshWeaver.Fixture → MeshWeaver.Messaging), so product code gets the deadlock-safe bridge. No
# shipped package can hold the TypeRef: the type was added 2026-08-30 by #2750, one day AFTER the
# last release (v3.0.0-rc8, 2026-08-29), and the shipped MeshWeaver.Fixture 3.0.0-rc8 nupkg was
# pulled from nuget.org and inspected — it contains no ObservableAwait (control: HubTestBase,
# TestBase, ConfigureMesh all present in the same DLL). Fixture is in no module bundle or bake
# surface; its only consumers are TestBase assemblies and test projects that compile from source.
# A forwarder is impossible without keeping `namespace MeshWeaver.Fixture` inside a product
# assembly, which would tell product code to `using MeshWeaver.Fixture` — the wrong direction.
MeshWeaver.Fixture:MeshWeaver.Fixture.ObservableAwait #2771 — added by #2750 after v3.0.0-rc8; never shipped
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
using System.Reactive.Linq;

namespace MeshWeaver.Fixture;
namespace MeshWeaver.Messaging;

/// <summary>
/// The ONE way a test waits for an observable — and the reason <c>.ToTask()</c> does not appear in
/// this repository (maintainer, 2026-08-30: <i>"no ToTask ever"</i>).
/// The ONE way anything in this codebase waits for an observable — and the reason <c>.ToTask()</c>
/// does not appear in it (maintainer, 2026-08-30: <i>"no ToTask ever"</i>, <i>"strictly"</i>).
///
/// <para>🚨 It lives in a PRODUCT assembly, not the test fixture, because the rule is not a test
/// convention. The bridges that actually deadlock a hub are the ones in product code — an SDK
/// boundary we must implement (a Microsoft.Agent.Framework <c>…Async</c> override, an
/// <c>IHostedService</c>, ASP.NET middleware) whose signature is not ours to change. Those had no
/// sanctioned bridge to move to while this helper was test-only, which is why
/// MeshWeaver.Plugins still carries 80 of them in <c>src/</c> while core carries none.</para>
Comment on lines +13 to +14
///
/// <para>🚨 Rx's own <c>ToTask()</c> completes its <c>TaskCompletionSource</c> WITHOUT
/// <c>RunContinuationsAsynchronously</c>, so the awaiting test resumes INLINE on whichever thread
Expand Down
16 changes: 16 additions & 0 deletions test/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,22 @@
covers Should()/asserts + the Seconds()/Milliseconds()/… time helpers, so
test files need no per-file assertion import. -->
<Using Include="MeshWeaver.Reactive.Assertions" />
<!-- ObservableAwait.Await — the ONE sanctioned observable→Task bridge — moved from
MeshWeaver.Fixture (a TEST assembly) into MeshWeaver.Messaging.Hub, because the bridges
that actually deadlock a hub are in PRODUCT code: an SDK boundary we must implement
(a Microsoft.Agent.Framework ...Async override, IHostedService, ASP.NET middleware) cannot
reference the test fixture, so those had nowhere to go and stayed on Rx's .ToTask().
Imported globally here so the ~330 existing call sites need no churn, and so exactly ONE
implementation exists — a forward in the old namespace made every .Await(ct) ambiguous
(CS0121) in the files that import both. -->
<!-- Excluded: the three test projects with no messaging dependency at all (they assert JSON,
image packing and browser E2E). A global using cannot resolve a namespace the project does
not reference, so naming them is honest; adding a ProjectReference just to satisfy a using
would be the wrong direction. -->
<Using Include="MeshWeaver.Messaging"
Condition="'$(MSBuildProjectName)' != 'MeshWeaver.Json.Test'
and '$(MSBuildProjectName)' != 'MeshWeaver.PluginImage.Test'
and '$(MSBuildProjectName)' != 'MeshWeaver.Portal.E2E.Test'" />
</ItemGroup>

<ItemGroup>
Expand Down
14 changes: 10 additions & 4 deletions test/MeshWeaver.Documentation.Test/ObservableToTaskBridgeGuard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,17 @@ private static readonly (string File, string Why)[] SanctionedBridges =
+ "merge into a path-rename conflict. When the move lands, this entry goes stale and "
+ "the register test above will say so."),

("src/MeshWeaver.Fixture/ObservableAwait.cs",
"The test tree's ONE wait, introduced by #2750 when 1,538 `.ToTask(` call sites were "
("src/MeshWeaver.Messaging.Hub/ObservableAwait.cs",
"The ONE wait, introduced by #2750 for the test tree when 1,538 `.ToTask(` call sites "
+ "were swept, and moved by #2771 from MeshWeaver.Fixture into the messaging assembly so "
+ "PRODUCT code has a deadlock-safe bridge too: the sibling repos carried 65 raw "
+ "`.ToTask(` sites in product source that could not be swept while the only safe "
+ "wrapper lived in a test assembly. Same category as ReactiveWait: infrastructure whose "
+ "entire PURPOSE is to hand a caller a Task to await, so it cannot be bridge-free. "
+ "(The type-forwards gate was consulted: the type is one day younger than the last "
+ "release, so no shipped package binds the old name — see scripts/type-forwards.allow.) "
+ "swept. Same category as ReactiveWait: infrastructure whose entire PURPOSE is to hand "
+ "a test a Task to await, so it cannot be bridge-free, and it ships from a production "
+ "root because MeshWeaver.Fixture is where the test bases live. It is a FAITHFUL "
+ "It is a FAITHFUL "
+ "ToTask (last value, faults on an empty sequence) — deliberately, because settling on "
+ "the first notification would have silently changed 462 call sites that do not reduce "
+ "to a single element; only the continuation scheduling differs. Safe form. "
Expand Down
Loading