Skip to content

Add NexNet.Testing harness + InProcessTransport + minimal core hooks#77

Open
DJGosnell wants to merge 47 commits into
masterfrom
add-nexnet-testing
Open

Add NexNet.Testing harness + InProcessTransport + minimal core hooks#77
DJGosnell wants to merge 47 commits into
masterfrom
add-nexnet-testing

Conversation

@DJGosnell

@DJGosnell DJGosnell commented May 26, 2026

Copy link
Copy Markdown
Member

Summary

  • Ships a new NexNet.Testing package: in-process transport, test host, recorders, server-side and per-client assertion APIs, streaming-helper extensions, and a quiescence primitive.
  • Adds three minimal optional internal hooks to NexNet core (IInvocationInterceptor, IPipeFactory, ServerConfig.OnAuthenticateOverride) so the harness can instrument dispatch and auth without touching production hot paths.
  • Demo + showcase nexus rewritten in Phase 14 from the original JoinGroup/BroadcastToGroup passthrough shape to a realistic EditorServerNexus document-editor domain that drives every harness feature through natural business verbs.
  • Driven by 14 plan phases + 12 remediation phases (R1–R12) addressing 40 findings from a structured review, plus a second full-branch review pass addressing 5 more findings.

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:

await using var host = await NexusTestHost.CreateAsync<
    EditorServerNexus, EditorServerNexus.ClientProxy,
    EditorClientNexus, EditorClientNexus.ServerProxy>();

var alice = await host.ConnectAsAsync(TestIdentity.Of("alice", "Write"));
var bob   = await host.ConnectAsAsync(TestIdentity.Of("bob",   "Write"));
var carol = await host.ConnectAsAsync(TestIdentity.Of("carol", "Read"));

await alice.Server.OpenDocument("design.md");
await bob.Server.OpenDocument("design.md");
await carol.Server.OpenDocument("recipe.txt");

await alice.Server.SaveDraft("design.md", "v1");
await host.QuiesceAsync();

bob.AssertReceived<IEditorClientNexus>(n => n.DraftSaved("alice", "v1"));
carol.AssertNotReceived<IEditorClientNexus>(
    n => n.DraftSaved(Arg.Any<string>(), Arg.Any<string>()));
Assert.That(host.Groups["doc-design.md"].Count, Is.EqualTo(2));

The sample drives real business methods (OpenDocument, SaveDraft) instead of synthetic passthroughs — SaveDraft is [NexusAuthorize<DocPermission>(Write)]-gated and broadcasts DraftSaved to Context.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 for NexNet.Testing) and defaults to null so production sessions keep the unchanged dispatch path.

Plan items implemented as specified

  • Phase 1PendingInvocationCount accessor on ISessionInvocationStateManager.
  • Phase 2IInvocationInterceptor interface + ConfigBase / NexusSessionConfigurations plumbing + Receiving wire-up.
  • Phase 3IPipeFactory interface + WrapLocal / WrapRemote hook points in NexusPipeManager.
  • Phase 4ServerConfig.OnAuthenticateOverride + ServerNexusBase.Authenticate consult-then-fallback.
  • Phase 5NexNet.Testing project with InProcessTransport (paired Pipes cross-wired), listener with Channel-based accept queue, InProcessServerConfig/ClientConfig, and InProcessRendezvous.
  • Phase 6Type.InProcess enum value + integration-test config branches (further expanded in R10).
  • Phase 7 — Recorder primitives (InvocationRecorder, Arg.Any<T>()/Arg.Is<T>(predicate) sentinels, ArgMatcher, ExpressionParser, NexusAssertionException).
  • Phase 8TestInvocationInterceptor, QuiescenceCounters, QuiescenceTracker with observe-zero/yield/re-observe pattern.
  • Phase 9PipeRecording, TappingPipeReader/TappingPipeWriter, TappedNexusDuplexPipe, TestPipeFactory.
  • Phase 10NexusTestHost.CreateAsync static entry point, NexusTestHost<...>, NexusTestClient<...>, TestIdentity.Of, TestAuthenticationStore.
  • Phase 12 — Server-side AssertReceived/AssertNotReceived/WaitFor on the host with MethodIdMap + ArgumentDeserializer.
  • Phase 14 — Demo + showcase rewrite (added after first review pass). The old DemoServerNexus had 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 realistic EditorServerNexus document-editor domain (OpenDocument, LeaveDocument, SaveDraft [Write], Whisper, BroadcastSystemAnnouncement [Admin], ListActiveEditors, UploadAttachment, StreamEdits) that exercises every harness feature through natural business verbs. OnAuthorize override matches TestIdentity.IsInRole case-sensitive ordinal against DocPermission enum names. EditorAppShowcaseTests.cs adds 18 focused tests; existing dependent tests (AssertionTests, ClientAssertionTests, NexusTestHostTests, StreamingExtensionsTests, MethodIdMapTests) updated to the new domain; HarnessSampleNexus.cs + HarnessShowcaseTests.cs removed.

Deviations from plan implemented

  • Hook reduction. Plan called for three core factories (invocation interceptor + pipe factory + channel factory). Channels do not need a core factory in v1 — harness convenience helpers (ChannelPublishAsync/ChannelCollectAsync) own channel creation directly. The pipe-layer byte tap still observes any user-instantiated channels (Decisions §"Revisions after source verification").
  • Hook location on ConfigBase. Plan considered direct fields on NexusSessionConfigurations. Implementation stores them on ConfigBase (authoritative install point) and copies into the per-session struct at construction. Users install hooks once on the config and every session inherits them.
  • Transport home. Plan started with MemoryTransport in NexNet core; revised to InProcessTransport shipped in NexNet.Testing so production code never has to take a test-package dependency. The integration-test-validation argument is preserved by NexNet.IntegrationTests taking a project reference to NexNet.Testing and exercising the InProcess transport through the existing [TestCase] matrix.
  • TimeProvider integration deferred to follow-up issue Adopt TimeProvider for deterministic time control across core #75. Verification revealed the actual scope (14 time references + 5 timers + 9 Task.Delay calls + an existing TickCountOverride test 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:

  • Quiescence counters were dead code. Two of four counters (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 with CountingPipeWriter/CountingPipeReader in InProcessTransport and per-session InternalOnSessionSetup callbacks on both server and client configs.
  • Multi-client ConnectAsAsync hang. 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 corrupting SessionContext; the harness now detects this and throws a clear error instead of hanging. Multi-client tests (3 clients + group broadcast) added in R3.
  • Group introspection (host.Groups[name].Members) added in R3 — previously deferred because of the multi-client hang.
  • Streaming-helper extensions (PipeUploadAsync, PipeDownloadAsync, ChannelPublishAsync<T>, ChannelCollectAsync<T>) added in R4 (plan §11).
  • Per-client assertion API on NexusTestClient added in R5 (plan §12).
  • MethodIdMap generator parity — runtime build now uses BindingFlags.DeclaredOnly + alphabetically-sorted inherited interfaces (no .Distinct()), matching the generator's OrderBy(i => i.ToDisplayString()) ordering. Pinned by new MethodIdMapTests. (R6)
  • Generator-aligned arg deserializer filter — replaced namespace-prefix heuristic with exact-FullName matches against the generator's exclusion list. (R7)
  • Mismatch diagnostics include arg valuesAssertReceived failures now render Notify("alpha"); Notify("beta") instead of #1, #1. (R7)
  • Multi-arg + string-arg AssertionTests added (R7), and the synthetic ExpressionParser test replaced with a real n => n.IntValue property-access case (R10).
  • Tap continuation deduplication in TappedNexusDuplexPipe (R8); WaitFor switched to Environment.TickCount64 monotonic clock (R8).
  • PredicateMatcher exception surfacing — user-thrown predicate exceptions are now reported as NexusAssertionException with inner exception preserved, not silently masked. (R9)
  • Integration matrix expansion[TestCase(Type.InProcess)] added to all three hook test classes plus pipes, channels, collections, groups, cancellation, and invalid-invocations (+83 InProcess test cases, R10).
  • Polish: TestAuthenticationStore.OverrideDelegate cached as a field; NexusTestHost.DisposeAsync logs teardown errors instead of swallowing; InProcessRendezvous.Unregister rewritten with TryGetValue+ReferenceEquals+TryRemove. (R11)
  • API ergonomics: public NexusTestHost.RecordedServerInvocationCount; per-typeparam XML docs on CreateAsync; AppDomain/ALC scope documented on InProcessRendezvous. (R12)

See _sessions/add-nexnet-testing/review.md for 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.

  • MethodIdMap ignored-method parity (Med). The runtime map now filters [NexusMethod(Ignore=true)] methods before assigning ids, matching the generator's pre-AssignMethodIds filter — previously an ignored method would shift the ids of methods declared after it, silently misaligning AssertReceived/AssertNotReceived. Regression test added.
  • MethodIdMap generic-interface ordering. Inherited interfaces are now ordered by a C#-style display name matching the generator's ToDisplayString() ordinal, rather than Type.FullName (which diverges for generic/nested interfaces). Regression test added (IGen<Guid> vs IGen<bool>).
  • Orphaned public surface. 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.
  • Dead code. Deleted the unused TappedRentedNexusDuplexPipe (local pipes pass through unwrapped since R4) and corrected the TestPipeFactory doc.
  • Package README. NexNet.Testing now 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.Run fire-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:

  1. Add a project reference to NexNet.Testing from the test project.
  2. Replace handcrafted socket setup with await using var host = await NexusTestHost.CreateAsync<...>();.
  3. Use client.Server/client.Nexus/client.AssertReceived(...) and host.AssertReceived(...) for assertions.

Performance Considerations

  • Production hot paths take three nullability checks (_invocationInterceptor, _pipeFactory, OnAuthenticateOverride). All default to null in non-harness code; the JIT specializes the branch.
  • TappingPipeWriter.GetSpan re-routes through GetMemory so the tap can read the source; cost is only paid on tapped (test-only) pipes.
  • Quiescence counter increments are Interlocked.* ops on the shared host counter; one per byte movement / dispatch / pipe-open. Test-only overhead.

Security Considerations

  • All three new core hooks are internal with [InternalsVisibleTo("NexNet.Testing")]; promotion to public is a v1.x decision when external demand exists.
  • The NexNet.csproj grant to NexNet.Testing.Tests was removed in R9 — the test project now reaches NexNet internals only through NexNet.Testing's intended surface.
  • OnAuthenticateOverride is server-side only; the harness installs it via the public-readable InProcessServerConfig.OnAuthenticateOverride property. Production code that doesn't set this property keeps the existing OnAuthenticate-only path.
  • TestAuthenticationStore accumulates 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.
  • ArgumentDeserializer only ever runs on bytes the session has already validated — it doesn't ingest untrusted input.
  • ArgMatcher.PredicateMatcher no longer swallows predicate-thrown exceptions silently; they surface as NexusAssertionException with inner-exception preserved.

Breaking Changes

Consumer-facing

  • None for application code.
  • New public surface in NexNet.Testing is v1NexusTestHost, 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 public TapChannel<T> producer ships.)

Internal

  • ConfigBase gains internal IInvocationInterceptor? and internal IPipeFactory? properties.
  • ServerConfig gains internal Func<ReadOnlyMemory<byte>?, ValueTask<IIdentity?>>? OnAuthenticateOverride.
  • ISessionInvocationStateManager gains int PendingInvocationCount { get; }.
  • NexusServer<TServerNexus, TClientProxy> gains internal IServerSessionManager? SessionManagerInternal.
  • NexNet.csproj InternalsVisibleTo grants reshuffled — NexNet.Testing grant moved to source attribute; NexNet.Testing.Tests grant removed.

Test plan

  • CI matrix passes (Generator + IntegrationTests + Testing tests).
  • All hook tests run on both Type.Tcp and Type.InProcess.
  • All pipe/channel/collection/group tests now exercise 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-test EditorAppShowcaseTests covering identity flow, authorization (Write/Admin gates), GroupExceptCaller/Client(id)/All routing, pipe + channel streaming, mixed-traffic quiescence, and Arg.Any/Arg.Is matchers.

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.
DJGosnell added 28 commits May 27, 2026 16:30
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.
DJGosnell added 14 commits May 27, 2026 16:31
- 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
DJGosnell force-pushed the add-nexnet-testing branch from bde1cdc to a040eff Compare May 27, 2026 20:42
DJGosnell added 5 commits May 27, 2026 16:43
…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.
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.

1 participant