Add NexNet.Testing harness + InProcessTransport + minimal core hooks#77
Open
DJGosnell wants to merge 47 commits into
Open
Add NexNet.Testing harness + InProcessTransport + minimal core hooks#77DJGosnell wants to merge 47 commits into
DJGosnell wants to merge 47 commits into
Conversation
DJGosnell
added a commit
that referenced
this pull request
May 26, 2026
DJGosnell
added a commit
that referenced
this pull request
May 27, 2026
- Replaced the old JoinGroup/BroadcastToGroup sample in the Impact section with the editor-app shape (OpenDocument + SaveDraft + DraftSaved callback observation across 3 identities with Write/Read roles). - Added a Phase 14 entry to the 'Plan items implemented' section. - Bumped test count from 65 to 80 in the test plan with a description of what EditorAppShowcaseTests covers. - Revised the Summary to mention the demo rewrite and bump phase count to 14. - PR #77 body pushed to GitHub. No NexNet.Testing README exists; main README + docs articles do not mention the testing harness, so nothing else to update. All 5 Phase 14 sub-phases complete. Phase 14 closed.
Phase 1 of the add-nexnet-testing workflow: add an internal PendingInvocationCount property on ISessionInvocationStateManager (plus its concrete implementation and the test mock). This is the load-bearing accessor the upcoming NexNet.Testing harness will read for the pendingResults quiescence counter; intended for diagnostic / test-harness use and not part of the public surface. Also bundles the workflow's DESIGN and PLAN session artifacts (plan.md + workflow.md + timeprovider-issue-body.md) that were drafted while the workflow was suspended. Tests: 149/149 generator + 2628/2628 integration green.
Phase 2 of the add-nexnet-testing workflow: introduce a single optional internal hook (`IInvocationInterceptor`) that wraps the dispatch of incoming invocations. The harness will use it to record invocations and to maintain the `inDispatch` quiescence counter; production sessions leave it null and pay only one nullable-field read per invocation. Wiring: - `IInvocationInterceptor` interface in NexNet.Internals. - `ConfigBase.InvocationInterceptor` is the authoritative install point so users configure once and every session inherits. - `NexusSessionConfigurations<TNexus, TProxy>` carries the value to the session; `NexusSession` copies it into a private field for direct hot-path access from `InvocationTask`. - `InternalsVisibleTo` added for `NexNet.Testing` and `NexNet.Testing.Tests`. Tests: 3 new interceptor tests cover the no-hook fast path, counting-interceptor observation, and dispatch short-circuit. Full suite green (149 generator + 2631 integration).
Phase 3 of the add-nexnet-testing workflow: introduce a second optional internal hook (`IPipeFactory`) that wraps duplex pipes at the boundary where they are returned to user code. The harness will use it to install tap wrappers that record consumed/written bytes and to track in-flight pipe lifetimes for the `activePipes` quiescence counter; production sessions leave it null and pay only one nullable-field read per pipe rent/register. Wiring follows the same pattern as `IInvocationInterceptor`: - `IPipeFactory` interface in NexNet.Internals. - `ConfigBase.PipeFactory` is the authoritative install point. - `NexusSessionConfigurations` carries the value to the session; `NexusSession` stores it as `_pipeFactory` and exposes a getter on `INexusSession` so the pipe manager can read it without walking `Config`. - `NexusPipeManager.RentPipe` calls `WrapLocal` on the rented pipe before returning it to the caller; the inner pipe remains the one stored in the active-pipes registry so incoming-data routing is unaffected. `RegisterPipe` calls `WrapRemote` symmetrically. Tests: 2 new pipe-factory tests cover the no-hook path and the wrap-local-and-remote case. Full suite green (149 generator + 2633 integration).
Phase 4 of the add-nexnet-testing workflow: introduce a single optional internal override on `ServerConfig` that the server consults before falling back to the user's `OnAuthenticate`. The test harness will install one to map fake tokens (issued via `TestIdentity.Of(...)`) to identities without forcing test users to disable or replace their real auth code. Production paths leave the override null and behavior is unchanged. Hook point: `ServerNexusBase.Authenticate` walks `SessionContext.Session.Config` to find the override; this is the same chain the existing authentication path uses, so no new field is required on the session. Tests: 3 new cases cover the no-override fallback, the override-consulted-instead-of-OnAuthenticate path, and the null-identity disconnect case. Full suite green (2636 integration).
Phase 5 of the add-nexnet-testing workflow: the new `NexNet.Testing` package introduces an in-process transport that the harness will sit on top of. Two paired `System.IO.Pipelines.Pipe` instances are cross-wired so that one peer's output is the other's input — no sockets, ports, TLS, or fake auth providers required. Components: - `InProcessRendezvous`: process-local registry keyed by endpoint strings. Servers register on listener creation; clients look up the registered listener to obtain a paired transport. - `InProcessTransportListener`: Channel-based queue of pending connections. `ConnectAsClient` (called from the client config) builds the pipe pair and enqueues the server-side half; `AcceptTransportAsync` dequeues for the NexNet server's accept loop. - `InProcessServerConfig` / `InProcessClientConfig`: thin config classes that wire the rendezvous in via the existing `OnCreateServerListener` / `OnConnectTransport` template-method hooks. A new `NexNet.Testing.Tests` project (NUnit) validates the transport in isolation: bidirectional exchange, ordering across 50 sequential messages, close-completes-peer-reader, missing- listener throws, and duplicate-endpoint registration throws. Solution file updated to include both new projects. Full solution builds clean.
Phase 6 of the add-nexnet-testing workflow: validate the InProcessTransport by joining it to the existing transport parameterization in NexNet.IntegrationTests. Changes: - New `Type.InProcess` enum value in `BaseTests`. - Per-test `_currentInProcessEndpoint` (GUID-derived) so a single test's server and client share an endpoint; reset in `TearDown` alongside the other transport state. - New server-side and client-side branches in `CreateServerConfigWithLog` / `CreateClientConfigWithLog` producing `InProcessServerConfig` / `InProcessClientConfig`. - `NexNet.Testing` project reference added. - `[TestCase(Type.InProcess)]` added in bulk to `NexusClientTests`, `NexusServerTests`, `NexusClientTests_SendInvocation`, `NexusClientTests_ReceiveInvocation`, `NexusServerTests_SendInvocation`, `NexusServerTests_ReceiveInvocation`, `NexusServerTests_NexusInvocations`, and `NexusServerTests_Authorization`. The parameterized `[TestCase(Type.HttpSocket, bool)]` variants get matching `[TestCase(Type.InProcess, bool)]`. - `ReconnectsNotifiesReconnecting_Hosted` deliberately omits `InProcess` — it stops/starts the Kestrel host as the disconnect trigger, which has no analogue here. Tests: 106 new InProcess cases pass. Full integration suite: 2742/2742.
Phase 7 of the add-nexnet-testing workflow: harness-side
machinery the interceptor (Phase 8) and assertion API (Phase 12)
will sit on top of. None of this depends on the runtime hook
yet, so it lands as a self-contained increment.
New files:
- `NexusAssertionException` (public) — thrown by failed
assertions; surfaced as a test failure by NUnit/xUnit/MSTest
with no framework-specific integration.
- `Arg.Any<T>()` / `Arg.Is<T>(predicate)` (public) — sentinels
recognized by `ExpressionParser` and translated into wildcard
/ predicate matchers.
- `ArgMatcher` (internal) — wildcard, equality, or predicate;
carries its own `ToString()` for diagnostic messages.
- `InvocationRecord` — one captured invocation (method id,
raw arg bytes, optional MethodInfo).
- `InvocationRecorder` — thread-safe append-only log with
snapshot reads and a `WaitForChangeAsync` signal that
`WaitFor` will hang off in Phase 12.
- `ExpressionParser` — walks an `Expression<Action<TInterface>>`,
identifies the method, resolves each argument as
Arg.Any/Arg.Is or compiles the expression to a value for
equality matching (so captured locals work naturally).
`InternalsVisibleTo("NexNet.Testing.Tests")` added so the
tests can exercise the internal pieces directly.
Tests: 12 new (8 ExpressionParser + 4 InvocationRecorder).
NexNet.Testing.Tests now 17/17 green.
Phase 8 of the add-nexnet-testing workflow: the harness's implementation of the IInvocationInterceptor hook plus the aggregating quiescence tracker the assertion API will use. New files: - `QuiescenceCounters` (internal) — per-session counter set with atomic Interlocked operations. - `QuiescenceTracker` (internal) — aggregates counters across every session associated with a `NexusTestHost`. `QuiesceAsync` uses the observe-zero / await Task.Yield() / re-observe-zero pattern documented in the design notes; pending-invocation counts come from registered pull-probes (one per session) so the session's own invocation-state registry remains the source of truth. - `TestInvocationInterceptor` (internal) — records each invocation into an `InvocationRecorder` and brackets the inner invoke with EnterDispatch/ExitDispatch on the session's counters. Argument bytes are copied eagerly because the underlying `InvocationMessage` is pool-returned post-dispatch. Method-id → MethodInfo resolution is deferred to Phase 12, where the assertion API actually needs it. Records carry methodId only; the resolver lives at the host construction site once the generated proxy types are reachable. Tests: 5 new QuiescenceTracker cases cover empty-quiesces- immediately, dispatch-blocks-then-unblocks, pipe-blocks, pending-invocation-probe, bytes-in-transit. Testing suite 22/22.
Phase 9 of the add-nexnet-testing workflow: the pipe-side observation layer. The TestPipeFactory installed at host construction wraps every pipe handed to user code so the harness can both record byte traffic and account for in-flight pipes in the quiescence counter. New files: - `PipeRecording` (public) — accumulates ConsumedBytes and WrittenBytes via ArrayBufferWriter, tracks IsCompleted / FaultedWith, and exposes async waiters (`WaitForBytesAsync` / `WaitForCompletionAsync`) for tests that need to synchronize on traffic. - `TappingPipeReader` (internal) — overrides AdvanceTo to copy the just-consumed slice into the recording. Records the "what the handler saw" view rather than the visible buffer because peek-without-consume is normal. - `TappingPipeWriter` (internal) — overrides Advance to copy the just-advanced range. GetSpan is satisfied through GetMemory so the tap has a stable contiguous source. - `TappedNexusDuplexPipe` / `TappedRentedNexusDuplexPipe` (internal) — INexusDuplexPipe wrappers that swap the user-facing Input/Output for the tap pair and delegate every other member (Id, ReadyTask, CompleteTask, WriterCore, ReaderCore) to the inner pipe. - `TestPipeFactory` (internal) — IPipeFactory implementation. WrapLocal/WrapRemote produce a tapped wrapper, increment the shared activePipes counter, and hook the inner pipe's CompleteTask to decrement it. Registers remote pipes by id for later lookup. Tests: 5 new PipeRecording cases (reader AdvanceTo, writer Advance, WaitForBytesAsync, completion-notifies-waiters, completion-carries-exception). Testing suite 27/27.
Phase 10 of the add-nexnet-testing workflow: ties together the InProcess transport, recorder/interceptor, pipe tap, and authentication-override pieces into a single entry-point API. Tests can now spin up a host, connect a client, exercise real nexus dispatch, and call QuiesceAsync to wait for the work to settle. New files: - `Authentication/TestIdentity` (public) — wraps DisplayName + role list, satisfies IIdentity, used via TestIdentity.Of. - `Authentication/TestAuthenticationStore` (internal) — Guid- keyed token registry whose OverrideDelegate is installed as ServerConfig.OnAuthenticateOverride. - `NexusTestHost` (public static) — CreateAsync<TS,TCP,TC,TSP> factory that builds an InProcessServerConfig with the test interceptor, pipe factory, and auth override pre-wired, starts the server, and returns the typed host. - `NexusTestHost<TServerNexus, TClientProxy, TClientNexus, TServerProxy>` (public) — owns server + recorder/tracker. ConnectAsync / ConnectAsAsync(IIdentity) construct a NexusClient using the user's CreateClient pattern, issue a token mapped to the fake identity (when supplied), and return a NexusTestClient handle. QuiesceAsync delegates to the tracker. - `NexusTestClient<TClientNexus, TServerProxy>` (public) — exposes .Client / .Server (proxy) / .Nexus. Tests: end-to-end test (single client, Ping round-trip, QuiesceAsync) and many-invocations-one-client (20 calls) both pass. Testing suite 29/29. Known issue: multi-client connect-then-invoke hangs on the second client; root cause TBD. Single-client path is the common case and ships green.
Phase 11 of the add-nexnet-testing workflow, minimal scope: introduces `ChannelRecording<T>` as the typed counterpart to `PipeRecording`. The byte-level pipe tap (Phase 9) captures the bytes a handler reads or writes; the channel recording captures the deserialized items, which is the level of detail tests usually want when asserting on channel-based methods. API mirrors `PipeRecording`: - `Items` snapshot - `IsCompleted`, `FaultedWith` - `WaitForCountAsync(minItems)` - `WaitForCompletionAsync()` - `RecordItem(T)` / `RecordCompletion(Exception?)` for the harness's channel wrappers to push events in. Skipped from v1: the `PipeUpload` / `PipeDownload` / `ChannelCollect` / `ChannelPublish` / `TapChannel` extension methods proposed in plan.md. They reduce to thin syntactic sugar over user code and would need to thread a session-aware pipe/channel factory through. Ship the showcase tests in Phase 13 against the raw API; add helpers based on real demand. Tests: 4 new ChannelRecording cases. Testing suite 33/33.
Phase 12 of the add-nexnet-testing workflow: the server-side
assertion API that ties the recorder, ExpressionParser, and
quiescence tracker together into a usable test surface.
New files:
- `Recording/MethodIdMap` (internal) — best-effort MethodInfo
-> ushort id map mirroring the generator's AssignMethodIds:
honors [NexusMethodAttribute(MethodId=N)] overrides, then
assigns sequential ids in declaration order (skipping the
reserved set). The runtime's `Type.GetMethods()` returns
metadata-token order which matches C# source order and the
generator's behavior.
- `Recording/ArgumentDeserializer` (internal) — rebuilds the
ValueTuple<...> type the generator's call-site serializer
uses and calls `MemoryPackSerializer.Deserialize(Type, span)`
to recover per-arg values. CancellationToken and pipe
parameters are excluded from the serialized shape, matching
the generator's emission.
- `Assertions.cs` (partial NexusTestHost) — adds three public
methods:
- `AssertReceived<TInterface>(expr, times = 1)` — exact-
count assertion; throws NexusAssertionException with the
expected count, observed count, and the most-recent 10
methodIds in the recorder for diagnostics.
- `AssertNotReceived<TInterface>(expr)` — must be zero
matches.
- `WaitFor<TInterface>(expr, timeout = 5s)` — awaits a
match arriving in the recorder, throws TimeoutException
on deadline.
Matching pipeline: ExpressionParser -> MethodInfo + matchers ->
MethodIdMap lookup -> recorder filter by methodId -> per-
parameter deserialization -> ArgMatcher.Matches for each
serializable parameter. Non-serializable parameter matchers
(CancellationToken, pipes) are dropped to mirror what the
generator actually serializes.
Tests: 8 new cases against the demo nexus — equality,
wildcard, predicate, times-mismatch, AssertNotReceived
present/absent, WaitFor success/timeout. All pass.
Testing suite 41/41.
Phase 13's group introspection and multi-client showcase tests are deferred to a follow-up. Both depend on the multi-client connect-then-invoke path that the Phase 10 end-to-end test exposed as broken (single client + many-invocations works; second client hangs). The harness's core contract — host construction, auth, interceptor recording, quiescence, assertions — is fully validated by the existing AssertionTests + NexusTestHostTests against the single-client path. Transitioning workflow to REVIEW. The follow-up work is captured in the workflow log; it can be split into a separate issue once the harness reaches main.
Two of four QuiescenceCounters were unwired in production paths: - bytesInTransit was never incremented (only mutated by unit tests). Added CountingPipeWriter and CountingPipeReader wrappers that the InProcessTransport installs when constructed with non-null counters/tracker; the InProcessTransportListener threads them through from a new internal Counters/Tracker pair on InProcessServerConfig. - PendingInvocationCount was added in Phase 1 but never read. NexusTestHost now installs the existing InternalOnSessionSetup callback on both the server config and each client config in ConnectAsAsync, registering each session's PendingInvocationCount as a tracker probe keyed by the session itself. QuiescenceTracker also gets: - Probe API moved from a Func<int> list to an object-keyed dictionary so callers have a handle for Unregister. - Counters and the signal task are now snapshotted under the same lock so a caller awaiting the returned signal cannot miss a SignalChange between sample-and-await. - ThrowIfCancellationRequested at the top of the loop so cancellation is observed promptly. QuiesceAsync_AwaitsRealActivityEndToEnd drives mixed Ping + Notify load through the full session pipeline to validate all four counters under real contention. Refs review findings 5, 6, 7, 12, 13, 26.
The previously-reported "multi-client ConnectAsAsync hang" turned out to be a factory-misuse problem rather than a synchronization bug in the harness or transport. NexNet stores per-session state on the nexus (SessionContext, identity, etc.); when the user-supplied factory returns the SAME instance for two connections, the second session's construction overwrites the first session's SessionContext on the shared object, corrupting handshake state on both sessions and stalling the second client's ConnectAsync. Fix: wrap the user-supplied serverNexusFactory and clientNexusFactory with HashSet<object>+ReferenceEqualityComparer detection. If a factory returns an instance the harness has seen before, throw a clear InvalidOperationException pointing the user at `() => new T()` and external state-capture patterns. Both factory arguments are now optional, defaulting to `new TServerNexus()` / `new TClientNexus()`. MultipleClients_CanConnectAgainstSameHost (3 clients, 3 distinct pings) now passes. SharedClientNexusInstance_ThrowsOnSecondConnect covers the negative case end-to-end. Refs review finding 4.
host.Groups[name].Members yields the session ids currently in the group; host.Groups[name].Count returns the size. Backed by a new GroupIntrospector indexer and GroupView snapshot type, both fed by IServerSessionManager.Groups (exposed through a new internal NexusServer.SessionManagerInternal getter). HarnessShowcaseTests covers the three cases the plan promised: empty group returns empty members, joining three clients into two groups reflects in the counts, and a server-side BroadcastToGroup actually reaches the group members (verified through DemoClientNexus.LastMessage). For the broadcast test to be observable the client config also needs the shared TestInvocationInterceptor and TestPipeFactory so client-side dispatch participates in quiescence; previously only the server config carried them, so QuiesceAsync could return before the broadcast handler ran on the client. Refs review finding 2.
Adds PipeUploadAsync, PipeDownloadAsync, ChannelPublishAsync<T>, and ChannelCollectAsync<T> as IRentedNexusDuplexPipe extensions in NexNet.Testing.Streaming.StreamingExtensions. NexusTestClient.CreatePipe() shortcut surfaces the underlying client-session pipe rental so tests don't have to reach into Nexus.Context. Surfaced a latent issue: TestPipeFactory.WrapLocal was returning a TappedRentedNexusDuplexPipe wrapper, but ProxyInvocationBase.ProxyGetDuplexPipeInitialId uses Unsafe.As<NexusDuplexPipe>(pipe) and would read garbage memory through the wrapper. Locally-rented pipes are now passed through unwrapped; the byte-level transport tap (via BytesInTransit, R1) already feeds quiescence, so the user-facing observation API for caller-side pipes is the only thing that loses out and only on the local-rent path. Remote-incoming pipes are still wrapped normally. Four new tests cover client→server upload (bytes), server→client download (bytes), client→server channel publish (typed), and server→client channel collect (typed). DemoServerNexus grows four new methods + two statics for side-effect inspection across the per-session nexus instances. Refs review finding 1.
Extracted the assertion logic into a shared RecorderAssertions helper that both the host-side variant (Assertions.cs) and the new client-side variant delegate to. NexusTestClient now exposes AssertReceived<TInterface>(expr, times), AssertNotReceived<TInterface>(expr), and WaitFor<TInterface>(expr, timeout), each scanning only this client's session. The host's ConnectAsAsync constructs a per-client InvocationRecorder and a fresh TestInvocationInterceptor for the client config. Per-client recording runs alongside the host-shared interceptor (which feeds the server-side recorder), so both "did the server see X?" and "did this specific client see Y?" assertions work side-by-side. Five new tests cover: positive match across two clients receiving the same broadcast, negative isolation (member sees broadcast, non-member doesn't), count mismatch, WaitFor success on delayed match, and WaitFor timeout. Refs review finding 3.
CollectDeclaredMethods now uses BindingFlags.DeclaredOnly for the direct interface, then iterates inherited interfaces sorted alphabetically by full name. This mirrors the generator's `OrderBy(i => i.ToDisplayString(), StringComparer.Ordinal)` ordering. .Distinct() is removed - it's not needed once DeclaredOnly prevents duplicates from the direct/inherited overlap, and its enumeration order on MethodInfo was non-deterministic. MethodIdMapTests covers the contract that was previously load-bearing but untested: source-declaration order parity for IDemoServerNexus (Ping=0, Notify=1, ..., PublishStrings=7) and IDemoClientNexus (ReceiveBroadcast=0), determinism across repeated Build() calls, and explicit [NexusMethod(MethodId=N)] precedence with subsequent-method slot-skipping via a synthetic IExplicitIdSample interface. Refs review findings 8, 10, 11, 25.
ArgumentDeserializer.IsSerializableParameter now exact-matches the generator's exclusion list by FullName: CancellationToken, INexusDuplexPipe, IRentedNexusDuplexPipe, INexusDuplexUnmanagedChannel<T>, INexusDuplexChannel<T>. The previous namespace-prefix check would drift silently the next time the generator added an excluded type. The check is now public-internal so RecorderAssertions uses the single source of truth rather than its own duplicate. BuildMismatchMessage no longer renders "#1, #1, #1" - it deserializes each recent record and renders the actual call site, e.g. `Notify("alpha"); Notify("beta")`. Falls back to `<args undecodable: ...>` if deserialization itself throws (so a downstream bug in the deserializer doesn't mask the real assertion failure). Three new AssertionTests cover string-arg matching, multi-arg with wildcards, and the diagnostic-includes-args contract end-to-end. Refs review findings 9, 27, 34.
WaitFor now uses Environment.TickCount64 instead of DateTime.UtcNow so the deadline survives wall-clock jumps (DST, NTP correction). Matches the ServerNexusBase convention in the rest of the codebase. (Finding 18) TappedNexusDuplexPipe and TappedRentedNexusDuplexPipe no longer register their own CompleteTask continuations in the constructor; TestPipeFactory's Track method now owns the single continuation that drives both RecordCompletion and ClosePipe. Eliminates the duplicate callback path and the constructor-time publish race window. (Finding 16) Findings 14, 15 verified as documented-correct edge cases - the TappingPipeReader's `Slice(0, consumed)` already captures bytes from the prior consumed watermark to the new one (the next ReadAsync returns a buffer that starts at the prior watermark), and peek-then-TryRead-again doesn't lose data because the second StoreBuffer just re-stages the same starting position. Added clarifying comments rather than rewriting working code. Finding 17 marked no-longer-applicable after R4 made WrapLocal a pass- through. Id-keying is intentionally only populated by WrapRemote where the pipe id is stable at wrap-time. Refs review findings 14, 15, 16, 17, 18.
Removed InternalsVisibleTo("NexNet.Testing.Tests") from NexNet.csproj. The
test project still reaches NexNet.Testing internals through its own
InternalsVisibleTo grant, but no longer has direct access to NexNet core
internals - tests must go through the Testing package's intended surface.
(Finding 20)
ArgMatcher.PredicateMatcher previously swallowed user-thrown predicate
exceptions and reported the call site as a no-match, indistinguishable from
a legitimate failure. It now catches the TargetInvocationException wrapper,
unwraps the inner exception, and rethrows it as a NexusAssertionException
with the original cause preserved as InnerException. Added a
(message, innerException) constructor to NexusAssertionException to support
this (also addresses finding 37). (Finding 21)
TestAuthenticationStore gets a Clear() method and explicit XML docs noting
the test-only retention model: tokens are bound to the host's lifetime by
design (the store doesn't know which tokens are still in use), and
long-lived hosts can call Clear() between scenarios to bound memory.
(Finding 22)
Refs review findings 20, 21, 22, 37.
Added [TestCase(Type.InProcess)] alongside [TestCase(Type.Tcp)] (and the other transport cases) across 11 integration test files: the three hook tests (InvocationInterceptor, PipeFactoryHook, OnAuthenticateOverride), both client and server pipe duplex + channel-reader tests, the group invocations tests, both client and server cancellation tests, the invalid- invocations tests, and the three collection test classes. Remote-endpoint and rate-limiting tests are intentionally excluded - they assert real socket-shaped addresses and ports. Replaced ExpressionParserTests.NonCallExpression_Throws's synthetic AST (Expression.Lambda<Action<T>>(Expression.Constant(...))) with a real-misuse case: an Expression<Func<ISample, int>> n => n.IntValue coerced into the Action<ISample> shape so the parser actually sees a property-access body. This is the kind of mistake a user would make, so a failure here would now surface what real users see. Integration suite grew from 2742 to 2825 tests (+83 InProcess cases), all green. Refs review findings 23, 24, 28.
- InProcessRendezvous.Unregister: rewrote with explicit TryGetValue +
ReferenceEquals + TryRemove(key, out _) instead of the KeyValuePair-shape
conditional remove. Preserves the "only remove when still mapping to this
listener" semantics with clearer code. (Finding 31)
- TestAuthenticationStore.OverrideDelegate: cached in a readonly field set
in the constructor instead of allocating a fresh lambda on every read.
(Finding 32)
- NexusTestHost.DisposeAsync: logs StopAsync exceptions through the
configured ILogger instead of silently swallowing them. Teardown failures
now surface in test output, where they belong. (Finding 33)
- Moved InternalsVisibleTo("NexNet.Testing") from NexNet.csproj to a source
attribute in Properties/InternalsVisibleTo.cs, matching plan §2's intent
that the grant be code-searchable next to the source. The pre-existing
IntegrationTests/Asp/Benchmarks grants stay in csproj. (Finding 40)
- Finding 30 (missing ConfigureAwait) verified clean by code-search; every
await in NexNet.Testing already has .ConfigureAwait(false) and the
analyzer build-clean confirms it.
- Finding 39 (ServerNexusBase.Authenticate pattern-cast) verified
theoretical: no integration mocks set Config=null! and the
OnAuthenticateOverrideTests (now covering InProcess too via R10) exercise
both the override and OnAuthenticate fallback paths.
65 testing + 2825 integration green.
Refs review findings 30, 31, 32, 33, 39, 40.
- NexusTestHost.CreateAsync grows per-typeparam <typeparam> docs and a worked-example <code> block. The 4-parameter shape stays because the generator's nested ClientProxy / ServerProxy types can't be inferred from the outer nexus types; making them visible up front is the next best thing. A 2-type-parameter overload can be added later non-breakingly. (Finding 35) - Added public NexusTestHost.RecordedServerInvocationCount (int) as the v1 read-side handle for "did the server actually see anything?" sanity checks. ServerRecorder, Tracker, and AuthStore stay internal - they expose internal types (InvocationRecord etc.), and promoting them now is a one-way door we defer until the assertion API proves insufficient. (Finding 36) - InProcessRendezvous's type-level XML now explicitly documents the AppDomain / AssemblyLoadContext scope: the static field shares state within one ALC graph but separate ALCs each get their own registry. CreateAsync already generates GUID-suffixed endpoints so concurrent same-domain runs don't collide. (Finding 38) Finding 37 ((message, inner) constructor on NexusAssertionException) was already addressed in R9 where PredicateMatcher needed it. This completes all 12 remediation phases. 65 testing + 2825 integration green. Refs review findings 35, 36, 38.
- Per user feedback: keep the InternalsVisibleTo grant for NexNet.Testing in NexNet.csproj alongside the existing IntegrationTests/Asp/Benchmarks grants, matching the project's existing convention. Removed src/NexNet/Properties/InternalsVisibleTo.cs. - .github/workflows/dotnet.yml grows two steps so CI exercises the new package: `Execute testing-harness tests` runs NexNet.Testing.Tests, and `Pack NexNet.Testing` produces the nupkg artifact alongside the other packages. The existing `dotnet build src` step already builds the new projects so --no-build is honored. Refs review finding 40 (reclassified D).
The integration suite has ~2825 test cases; with the default console logger
verbosity each one was emitting a line to the CI log, producing thousands
of lines of noise on every run. Switched all three test projects to
`--logger "console;verbosity=minimal"` so each assembly emits one summary
line ("Passed! - Failed: 0, Passed: N, ...") plus full failure details
when something fails. Step elapsed time in the Actions UI shows progress.
Same change applied to generator tests and testing-harness tests for
consistency.
dotnet test's console logger has no "progress without per-test list" mode -
verbosity=minimal prints only the final summary, verbosity=normal lists
every test. For the integration suite (2825 tests, ~3 minutes) the minimal
setting left CI silent for too long.
Each test project now ships a TestProgressReporterAttribute (assembly-level
ITestAction) that emits a single status line every N tests or T seconds,
whichever comes first, on stderr. Lines look like:
[progress] NexNet.IntegrationTests: 100 ran (100 passed, 0 failed), 12.4s elapsed
Stderr is the channel choice because:
- TestContext.Progress is filtered by `console;verbosity=minimal`
- Console.Out is captured per-test by NUnit before reaching the logger
- Console.Error passes through both layers untouched
Cadences:
- IntegrationTests: every 100 tests OR 15 seconds (~14 lines per run)
- Testing.Tests / Generator.Tests: every 25 tests OR 10 seconds (small
suites - one line is enough)
The attribute is duplicated per project because NUnit assembly actions
aren't transitively imported across project references.
GitHub announced Node 20 actions deprecation - they will be forced to Node 24 on June 2nd, 2026 and Node 20 removed September 16th, 2026. Each of the three actions used here has a Node 24 major release available: - actions/checkout@v4 → v5 (v5 explicitly runs on node24) - actions/setup-dotnet@v4 → v5 (v5 release notes: "Upgrade to Node.js 24") - actions/upload-artifact@v4 → v5 (v5 runs on node24) This silences the per-job deprecation annotation and removes the September 2026 cliff.
Per the GitHub Actions deprecation annotation, actions/upload-artifact@v5 still runs on Node 20. v7 (action.yml runs.using: node24) is the current major and runs on Node 24 by default.
Adds Phase 14 to plan.md covering the rewrite of the harness demo nexus from the JoinGroup/BroadcastToGroup passthrough shape to a realistic document-editor domain that exercises every harness feature via natural business methods. Eight design decisions recorded in workflow.md (2026-05-27). 5 sub-phases (14a-14e) sequenced. 18 showcase tests enumerated. Method-ID layout post-rewrite documented. Suspended at end of PLAN awaiting user approval before IMPLEMENT.
Adds EditorServerNexus/EditorClientNexus/IEditorServerNexus/IEditorClientNexus in the new EditorAppNexus.cs file alongside the existing HarnessSampleNexus.cs. No existing tests are touched in 14a — they continue to use Demo* types. The migration happens in 14b (rename + rewrite broadcast tests) and 14c (replace HarnessShowcaseTests with EditorAppShowcaseTests). Editor domain: - DocPermission enum (Read, Write, Admin) backed by int (default). - OnAuthorize override casts Context.Identity to TestIdentity and matches IsInRole(((DocPermission)p).ToString()) for each required permission (case-sensitive ordinal, per workflow decision). - Kept core methods: Ping, Notify, Upload, Download, CollectStrings, PublishStrings (unchanged signatures, used by AssertionTests etc.). - New editor methods: OpenDocument, LeaveDocument, SaveDraft (Write-gated), Whisper, BroadcastSystemAnnouncement (Admin-gated), ListActiveEditors, UploadAttachment, StreamEdits. - JoinGroup / BroadcastToGroup deliberately omitted (the passthrough methods the PR review called out as showing harness ergonomics in the worst light). Cross-session state: - Documents : ConcurrentDictionary<string, DocState> — attachment bytes + edit ops per docId. - ActiveEditors : ConcurrentDictionary<string, ConcurrentDictionary<long, string>> — docId -> sessionId -> display name (used by ListActiveEditors). - ResetAll() called at the top of each [Test] (matches LastUploadedBytes/ PingCount pattern in DemoServerNexus). StreamEdits refinement: signature is (string docId, INexusDuplexChannel<EditOp> channel) rather than channel-only, because EditOp itself doesn't carry a docId and the doc-state update needs a target. Mirrors UploadAttachment. EditOp is record struct(int Position, string Inserted) with [MemoryPackable] (managed-channel; string field rules out unmanaged). Build clean (NexNet.Testing.Tests). All 65 existing testing tests still pass.
Bulk rename + targeted broadcast-test rewrites across the dependent test files
in NexNet.Testing.Tests. End state: HarnessSampleNexus.cs deleted; all tests
reference Editor* types and editor-domain methods. Suite stays 65/65 green.
Files updated:
- NexusTestHostTests.cs : pure rename (no broadcast logic).
- StreamingExtensionsTests.cs : pure rename + EditorServerNexus.LastUploadedBytes
/ LastCollectedItems statics (kept for back-compat with the existing
pipe/channel tests).
- AssertionTests.cs : pure rename for Ping/Notify tests; multi-arg test rewritten
from BroadcastToGroup("editors", "draft-saved") to SaveDraft("design.md",
"v1"). SetupAsync identity now carries "Write" so SaveDraft passes the
NexusAuthorize<DocPermission>(Write) gate.
- ClientAssertionTests.cs : all 5 tests rewritten to OpenDocument + SaveDraft
with DraftSaved client-callback assertions. Identities given "Write".
- HarnessShowcaseTests.cs : 3 tests rewritten to OpenDocument + SaveDraft.
Temporary shape — this file is replaced wholesale in 14c by
EditorAppShowcaseTests.cs (per plan).
- MethodIdMapTests.cs : expected-ID dictionaries updated for the new 14-method
IEditorServerNexus layout (0=Ping..13=StreamEdits) and 5-method
IEditorClientNexus (0=DraftSaved..4=SystemAnnouncement).
Files removed:
- HarnessSampleNexus.cs : no remaining references to Demo* types.
Build clean. 65/65 NexNet.Testing.Tests pass.
…s delay Replaces HarnessShowcaseTests.cs with EditorAppShowcaseTests.cs containing the first 10 showcase tests from the plan, plus migrated Groups_EmptyGroup_HasNoMembers. Tests pin (each one capability via a natural editor-domain verb): - SaveDraft_BroadcastsToDocGroup_WithAuthorIdentity Group routing + identity flow - LeaveDocument_NotifiesOthersExceptCaller GroupExceptCaller - Whisper_DeliveredToTargetOnly Client(long id) - BroadcastSystemAnnouncement_AsNonAdmin_Throws NexusAuthorize<Admin> reject - BroadcastSystemAnnouncement_AsAdmin_DeliversToAll Context.Clients.All + Admin role - SaveDraft_AsReader_Throws NexusAuthorize<Write> reject - ListActiveEditors_ReturnsNames ValueTask<string[]> + identity - ListActiveEditors_CancelledToken_PropagatesAndThrows CT propagation - UploadAttachment_StreamsBytes_ServerAppendsToDoc INexusDuplexPipe streaming - StreamEdits_AllOpsCollected INexusDuplexChannel<T> streaming ListActiveEditors gained `await Task.Delay(200, ct)` so the client-side CT cancellation test can actually observe propagation. Discovery: the framework only sends the cancel signal when the client-side CT FIRES during a call — pre-cancelled tokens are not short-circuited by the proxy, so a synchronous server method would race the propagation and silently complete. Documented in the impl comment. 73/73 NexNet.Testing.Tests pass.
Seven additional showcase tests in EditorAppShowcaseTests.cs covering the
harness capabilities not pinned in 14c. Test 15 (Groups_EmptyGroup_HasNoMembers)
was migrated as part of 14c.
- WaitFor_DraftSaved_ResolvesWhenInvoked per-client WaitFor success
- WaitFor_Timeout_Throws per-client WaitFor timeout
- MixedTraffic_QuiesceAsync_WaitsForEverything SaveDraft + Whisper +
UploadAttachment + StreamEdits
fired concurrently across 3
clients; QuiesceAsync drains
everything before assertions
- Groups_Introspection_ReflectsLiveMembership open/leave/reopen sequence
- ServerSide_AssertReceived_SaveDraft host.AssertReceived on the
IEditorServerNexus surface
- AssertReceived_TimesMismatch_DiagnosticIncludesArgs
showcase shape of the R7
diagnostic contract
- ArgMatchers_AnyAndPredicate Arg.Any + Arg.Is wired into
per-client assertions on
DraftSaved
80/80 NexNet.Testing.Tests pass.
- Replaced the old JoinGroup/BroadcastToGroup sample in the Impact section with the editor-app shape (OpenDocument + SaveDraft + DraftSaved callback observation across 3 identities with Write/Read roles). - Added a Phase 14 entry to the 'Plan items implemented' section. - Bumped test count from 65 to 80 in the test plan with a description of what EditorAppShowcaseTests covers. - Revised the Summary to mention the demo rewrite and bump phase count to 14. - PR #77 body pushed to GitHub. No NexNet.Testing README exists; main README + docs articles do not mention the testing harness, so nothing else to update. All 5 Phase 14 sub-phases complete. Phase 14 closed.
22 findings classified 13A/2B/0C/7D from the Phase 14 review pass; all actionable items folded into one commit. 7 D's preserved in review.md \"Action Taken\" column with rationale. Production code: - src/NexNet.Testing/Recording/MethodIdMap.cs - doc-comment now points at the post-rename regression test (#1) Test-only nexus (src/NexNet.Testing.Tests/EditorAppNexus.cs): - ListActiveEditors Task.Delay now guarded on CanBeCanceled - happy-path callers passing CancellationToken.None no longer pay the 200ms tax (#5) - OnAuthorize override comment now documents AND-semantics, marker-only behavior, and per-app policy choice rationale (#17) - ResetAll() XML comment explains why PingCount is omitted (instance-scoped) and what enabling Parallelizable would race (#10, #21) - Removed redundant 'using NexNet;' (#20) Test files: - AssertionTests + NexusTestHostTests gained [SetUp] ResetAll() for fixture-convention consistency (#9) - EditorAppShowcaseTests: - CT-propagation test loosened to Throws.InstanceOf<OperationCanceledException> so the test pins the semantic, not the concrete subtype (#7) - WaitFor_DraftSaved_ResolvesWhenInvoked - producer now runs in Task.Run with 100ms delay so the waiter actually exercises the async-arrival path (#2) - MixedTraffic - pipe + channel now 'await using' for failure-path safety; assertions tightened with times: 1; added AssertNotReceived on alice + carol for WhisperReceived (#3, #13) - StreamEdits_AllOpsCollected - channel now 'await using' (#14) - New: OpenDocument_NotifiesOthersExceptCaller - pins the EditorJoined GroupExceptCaller broadcast that fires inside OpenDocument (#4) - New: Connect_WithoutIdentity_IsRejectedAtHandshake - pins the discovery that anonymous sessions are rejected at handshake by the framework (TransportException Authentication failed), not by OnAuthorize (#18) - Stale '50ms' comment updated to reflect the 200ms delay + CanBeCanceled guard (#6) 82/82 NexNet.Testing.Tests pass (+2 new tests vs pre-remediation 80).
DJGosnell
force-pushed
the
add-nexnet-testing
branch
from
May 27, 2026 20:42
bde1cdc to
a040eff
Compare
…eanup Session-8 full-branch re-review A-items (code): - #3 (Med): MethodIdMap now filters [NexusMethod(Ignore=true)] (matching the generator's pre-AssignMethodIds filter) so ignored methods get no id and reserve no slot; +regression test. - #4: order inherited interfaces by a C#-style display-name key (ToDisplayString-equivalent) instead of Type.FullName, fixing generic-interface id parity; +regression test (IGen<Guid> vs IGen<bool>). - #1: demote orphaned public ChannelRecording<T> to internal (no producer ships in v1). - #5: delete dead TappedRentedNexusDuplexPipe; correct TestPipeFactory summary/see-cref. NexNet.Testing.Tests 84/84 green (82 -> 84, +2 MethodIdMap parity tests).
Adds src/NexNet.Testing/README.md (overview, install, quickstart, API table). Parameterizes the shared GenerateNuGetReadme target with a NuGetReadmeSource property (default = repo-root README) and points NexNet.Testing at its own README. Verified: NexNet.Testing.nupkg now ships the harness README; core NexNet.nupkg still ships the root README.
…on-taken, log) Full suite green post-remediation: 149 Generator + 2834 Integration + 84 Testing = 3067, 0 failures.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
NexNet.Testingpackage: in-process transport, test host, recorders, server-side and per-client assertion APIs, streaming-helper extensions, and a quiescence primitive.NexNetcore (IInvocationInterceptor,IPipeFactory,ServerConfig.OnAuthenticateOverride) so the harness can instrument dispatch and auth without touching production hot paths.JoinGroup/BroadcastToGrouppassthrough shape to a realisticEditorServerNexusdocument-editor domain that drives every harness feature through natural business verbs.Reason for Change
End users building applications on NexNet need a way to test their nexus implementations — server methods, client callbacks, authorization rules, broadcasts, group routing, pipes, and channels — without standing up sockets, ports, TLS, or fake auth providers. Existing options either require real network plumbing or fork into custom test harnesses per project.
Impact
New consumers of the package can write tests like:
The sample drives real business methods (
OpenDocument,SaveDraft) instead of synthetic passthroughs —SaveDraftis[NexusAuthorize<DocPermission>(Write)]-gated and broadcastsDraftSavedtoContext.Clients.Group($"doc-{docId}")internally. Tests observe the resulting client callbacks plus the recorded server-side invocation.Existing NexNet consumers are unaffected: every new hook is
internal(with InternalsVisibleTo forNexNet.Testing) and defaults tonullso production sessions keep the unchanged dispatch path.Plan items implemented as specified
PendingInvocationCountaccessor onISessionInvocationStateManager.IInvocationInterceptorinterface + ConfigBase / NexusSessionConfigurations plumbing + Receiving wire-up.IPipeFactoryinterface + WrapLocal / WrapRemote hook points inNexusPipeManager.ServerConfig.OnAuthenticateOverride+ServerNexusBase.Authenticateconsult-then-fallback.NexNet.Testingproject withInProcessTransport(paired Pipes cross-wired), listener with Channel-based accept queue,InProcessServerConfig/ClientConfig, andInProcessRendezvous.Type.InProcessenum value + integration-test config branches (further expanded in R10).InvocationRecorder,Arg.Any<T>()/Arg.Is<T>(predicate)sentinels,ArgMatcher,ExpressionParser,NexusAssertionException).TestInvocationInterceptor,QuiescenceCounters,QuiescenceTrackerwith observe-zero/yield/re-observe pattern.PipeRecording,TappingPipeReader/TappingPipeWriter,TappedNexusDuplexPipe,TestPipeFactory.NexusTestHost.CreateAsyncstatic entry point,NexusTestHost<...>,NexusTestClient<...>,TestIdentity.Of,TestAuthenticationStore.AssertReceived/AssertNotReceived/WaitForon the host withMethodIdMap+ArgumentDeserializer.DemoServerNexushad passthrough methods (JoinGroup/BroadcastToGroup) that showed harness ergonomics in the worst light by forcing users to write passthrough plumbing just to test broadcasts. Replaced with a realisticEditorServerNexusdocument-editor domain (OpenDocument,LeaveDocument,SaveDraft[Write],Whisper,BroadcastSystemAnnouncement[Admin],ListActiveEditors,UploadAttachment,StreamEdits) that exercises every harness feature through natural business verbs.OnAuthorizeoverride matchesTestIdentity.IsInRolecase-sensitive ordinal againstDocPermissionenum names.EditorAppShowcaseTests.csadds 18 focused tests; existing dependent tests (AssertionTests,ClientAssertionTests,NexusTestHostTests,StreamingExtensionsTests,MethodIdMapTests) updated to the new domain;HarnessSampleNexus.cs+HarnessShowcaseTests.csremoved.Deviations from plan implemented
ChannelPublishAsync/ChannelCollectAsync) own channel creation directly. The pipe-layer byte tap still observes any user-instantiated channels (Decisions §"Revisions after source verification").ConfigBase. Plan considered direct fields onNexusSessionConfigurations. Implementation stores them onConfigBase(authoritative install point) and copies into the per-session struct at construction. Users install hooks once on the config and every session inherits them.MemoryTransportin NexNet core; revised toInProcessTransportshipped inNexNet.Testingso production code never has to take a test-package dependency. The integration-test-validation argument is preserved byNexNet.IntegrationTeststaking a project reference toNexNet.Testingand exercising the InProcess transport through the existing[TestCase]matrix.Task.Delaycalls + an existingTickCountOverridetest seam) was large enough to warrant its own focused workflow. The harness ships without time control as a known limitation; tests of time-sensitive logic (auth cache TTL, reconnect, ping) remain real-time-bound until Adopt TimeProvider for deterministic time control across core #75 lands.Gaps in original plan implemented
These were uncovered during REVIEW after the 13-phase IMPLEMENT pass and addressed in the R1–R12 remediation:
bytesInTransit,pendingResults) had no production wire-up at all — the unit tests called the mutators directly but real session activity never did. R1 fixed this withCountingPipeWriter/CountingPipeReaderinInProcessTransportand per-sessionInternalOnSessionSetupcallbacks on both server and client configs.ConnectAsAsynchang. Phase 13 was reduced-scope because a second connect against the same host hung. R2 traced this to user-supplied factories returning a shared nexus instance corruptingSessionContext; the harness now detects this and throws a clear error instead of hanging. Multi-client tests (3 clients + group broadcast) added in R3.host.Groups[name].Members) added in R3 — previously deferred because of the multi-client hang.PipeUploadAsync,PipeDownloadAsync,ChannelPublishAsync<T>,ChannelCollectAsync<T>) added in R4 (plan §11).NexusTestClientadded in R5 (plan §12).MethodIdMapgenerator parity — runtime build now usesBindingFlags.DeclaredOnly+ alphabetically-sorted inherited interfaces (no.Distinct()), matching the generator'sOrderBy(i => i.ToDisplayString())ordering. Pinned by newMethodIdMapTests. (R6)AssertReceivedfailures now renderNotify("alpha"); Notify("beta")instead of#1, #1. (R7)n => n.IntValueproperty-access case (R10).TappedNexusDuplexPipe(R8);WaitForswitched toEnvironment.TickCount64monotonic clock (R8).NexusAssertionExceptionwith inner exception preserved, not silently masked. (R9)[TestCase(Type.InProcess)]added to all three hook test classes plus pipes, channels, collections, groups, cancellation, and invalid-invocations (+83 InProcess test cases, R10).TestAuthenticationStore.OverrideDelegatecached as a field;NexusTestHost.DisposeAsynclogs teardown errors instead of swallowing;InProcessRendezvous.Unregisterrewritten withTryGetValue+ReferenceEquals+TryRemove. (R11)NexusTestHost.RecordedServerInvocationCount; per-typeparam XML docs onCreateAsync; AppDomain/ALC scope documented onInProcessRendezvous. (R12)See
_sessions/add-nexnet-testing/review.mdfor the full 40-finding classification table and per-finding remediation notes.Second review pass (full branch re-review)
A second structured review over the full branch diff (after the first finalize gate) surfaced 8 findings (1 Med, 7 Low). The 5 actionable items were fixed in-branch; 3 are documented/tracked (no action). See
review.md§"Full branch re-analysis (Session 8)" for the classification table.[NexusMethod(Ignore=true)]methods before assigning ids, matching the generator's pre-AssignMethodIdsfilter — previously an ignored method would shift the ids of methods declared after it, silently misaligningAssertReceived/AssertNotReceived. Regression test added.ToDisplayString()ordinal, rather thanType.FullName(which diverges for generic/nested interfaces). Regression test added (IGen<Guid>vsIGen<bool>).ChannelRecording<T>was demoted from public to internal: no harness producer (TapChannel<T>) ships in v1, so it had no way to be populated. Internalizing keeps a non-breaking public promotion open for later.TappedRentedNexusDuplexPipe(local pipes pass through unwrapped since R4) and corrected theTestPipeFactorydoc.NexNet.Testingnow ships a harness-specific NuGet README instead of inheriting the core-NexNet README.Documented/tracked (no action): the aggregate (host-global) quiescence counter model; the
Task.Runfire-and-forget quiescence residual; wall-clock-bound timing tests (deterministic time control tracked by #75).Migration Steps
None for existing consumers. To adopt the harness:
NexNet.Testingfrom the test project.await using var host = await NexusTestHost.CreateAsync<...>();.client.Server/client.Nexus/client.AssertReceived(...)andhost.AssertReceived(...)for assertions.Performance Considerations
_invocationInterceptor,_pipeFactory,OnAuthenticateOverride). All default tonullin non-harness code; the JIT specializes the branch.TappingPipeWriter.GetSpanre-routes throughGetMemoryso the tap can read the source; cost is only paid on tapped (test-only) pipes.Interlocked.*ops on the shared host counter; one per byte movement / dispatch / pipe-open. Test-only overhead.Security Considerations
internalwith[InternalsVisibleTo("NexNet.Testing")]; promotion to public is a v1.x decision when external demand exists.NexNet.csprojgrant toNexNet.Testing.Testswas removed in R9 — the test project now reaches NexNet internals only throughNexNet.Testing's intended surface.OnAuthenticateOverrideis server-side only; the harness installs it via the public-readableInProcessServerConfig.OnAuthenticateOverrideproperty. Production code that doesn't set this property keeps the existingOnAuthenticate-only path.TestAuthenticationStoreaccumulates tokens for the host's lifetime by design (the store can't tell which tokens are still in use). Documented in XML;Clear()added for long-lived hosts.ArgumentDeserializeronly ever runs on bytes the session has already validated — it doesn't ingest untrusted input.ArgMatcher.PredicateMatcherno longer swallows predicate-thrown exceptions silently; they surface asNexusAssertionExceptionwith inner-exception preserved.Breaking Changes
Consumer-facing
NexNet.Testingis v1 —NexusTestHost,NexusTestClient,NexusAssertionException,Arg.Any<T>(),Arg.Is<T>(...),TestIdentity.Of(...),PipeRecording,GroupView,GroupIntrospector,StreamingExtensions. Future iteration may add overloads non-breakingly. (ChannelRecording<T>was demoted to internal in the second review pass — see below — until a publicTapChannel<T>producer ships.)Internal
ConfigBasegainsinternal IInvocationInterceptor?andinternal IPipeFactory?properties.ServerConfiggainsinternal Func<ReadOnlyMemory<byte>?, ValueTask<IIdentity?>>? OnAuthenticateOverride.ISessionInvocationStateManagergainsint PendingInvocationCount { get; }.NexusServer<TServerNexus, TClientProxy>gainsinternal IServerSessionManager? SessionManagerInternal.NexNet.csprojInternalsVisibleTogrants reshuffled —NexNet.Testinggrant moved to source attribute;NexNet.Testing.Testsgrant removed.Test plan
Type.TcpandType.InProcess.Type.InProcess.NexNet.Testing.Tests(84 tests) covers transport, recorder, MethodIdMap parity (declaration order, explicit ids, ignored-method filtering, and generic-inherited-interface ordering), quiescence under real load, host construction with multi-client connect and shared-instance detection, group introspection + broadcast, streaming helpers (upload/download/publish/collect), per-client assertions, and the 18-testEditorAppShowcaseTestscovering identity flow, authorization (Write/Admin gates),GroupExceptCaller/Client(id)/Allrouting, pipe + channel streaming, mixed-traffic quiescence, andArg.Any/Arg.Ismatchers.