Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
f125d09
Add PendingInvocationCount accessor + DESIGN/PLAN artifacts
DJGosnell May 6, 2026
57fe92b
Add IInvocationInterceptor hook for invocation dispatch
DJGosnell May 22, 2026
04215e0
Add IPipeFactory hook for pipe wrapping
DJGosnell May 22, 2026
c748820
Add OnAuthenticateOverride for server-side test auth
DJGosnell May 22, 2026
a77e0c8
Add NexNet.Testing project with InProcessTransport
DJGosnell May 22, 2026
4f46204
Suspend: workflow paused at end of Phase 5 (5/13)
DJGosnell May 22, 2026
e0ea804
Add Type.InProcess to integration test matrix
DJGosnell May 22, 2026
6e6b225
Add recorder primitives, Arg matchers, ExpressionParser
DJGosnell May 22, 2026
b5d3938
Add TestInvocationInterceptor + QuiescenceTracker
DJGosnell May 22, 2026
20ae884
Add TappingPipeReader/Writer + TestPipeFactory + PipeRecording
DJGosnell May 22, 2026
39f83c2
Add NexusTestHost/NexusTestClient + auth integration
DJGosnell May 22, 2026
68fdebb
Add ChannelRecording<T> for channel-level observation
DJGosnell May 22, 2026
6d8eca3
Add AssertReceived / AssertNotReceived / WaitFor
DJGosnell May 22, 2026
008d38d
Phase 13 reduced-scope closure; transition to REVIEW
DJGosnell May 22, 2026
731e40b
Suspend at IMPLEMENT->REVIEW boundary; all 13 phases done
DJGosnell May 22, 2026
98f2175
Wire quiescence counters end-to-end (R1)
DJGosnell May 26, 2026
1f23ae7
Detect shared-nexus factory misuse instead of hanging (R2)
DJGosnell May 26, 2026
5944cce
Group introspection + broadcast showcase (R3)
DJGosnell May 26, 2026
14e8358
Streaming helpers + unwrap local pipes (R4)
DJGosnell May 26, 2026
ffaf074
Suspend: workflow paused after R4 (4/12 remediation phases)
DJGosnell May 26, 2026
dd562d4
Per-client assertions on NexusTestClient (R5)
DJGosnell May 26, 2026
1fbe336
MethodIdMap robustness + generator parity tests (R6)
DJGosnell May 26, 2026
150e1a0
ArgumentDeserializer precise filter + diagnostic args (R7)
DJGosnell May 26, 2026
b2e5dfb
Tap recording fixes (R8)
DJGosnell May 26, 2026
3d58941
Security fixes (R9)
DJGosnell May 26, 2026
7b09345
Test matrix expansion (R10)
DJGosnell May 26, 2026
823c8dd
Codebase consistency polish (R11)
DJGosnell May 26, 2026
5f4f32a
API ergonomics polish (R12)
DJGosnell May 26, 2026
74189c2
Record PR #77 artifact + workflow update
DJGosnell May 26, 2026
8635c59
Revert InternalsVisibleTo source attribute; wire CI for NexNet.Testing
DJGosnell May 26, 2026
6f2e76e
CI: switch test runs to minimal verbosity (summary only)
DJGosnell May 26, 2026
ce74510
CI: per-test-project progress reporter via NUnit ITestAction
DJGosnell May 26, 2026
3a3a0d6
CI: bump deprecated Node 20 actions to Node 24 majors
DJGosnell May 26, 2026
9bc260a
CI: bump upload-artifact to v7 (v5 still ran on Node 20)
DJGosnell May 26, 2026
22f3689
Suspend: workflow paused at REMEDIATE step 8 awaiting finalize decision
DJGosnell May 26, 2026
1f60566
[WIP] Phase 14 plan: document-editor showcase rewrite
DJGosnell May 27, 2026
3d6b11f
Phase 14a: build EditorAppNexus.cs (additive)
DJGosnell May 27, 2026
c30d8ab
Phase 14b: rename Demo* to Editor* across testing tests
DJGosnell May 27, 2026
7f24fae
Phase 14c: EditorAppShowcaseTests (10 tests) + 200ms ListActiveEditor…
DJGosnell May 27, 2026
1be1595
Phase 14d: showcase tests 11, 12, 13, 14, 16, 17, 18
DJGosnell May 27, 2026
298cfb8
Phase 14e: update pr-body.md sample to editor-app shape
DJGosnell May 27, 2026
a040eff
REVIEW REMEDIATE: address Phase 14 findings (13 A's + 2 B's)
DJGosnell May 27, 2026
c2ececd
Update session log: rebase complete + force-push
DJGosnell May 27, 2026
c6fa955
Step back from FINALIZE to REVIEW (session 8)
DJGosnell May 28, 2026
e45fc78
REVIEW REMEDIATE (s8): MethodIdMap generator parity + dead-surface cl…
DJGosnell May 28, 2026
75de5f1
REVIEW REMEDIATE (s8): ship a NexNet.Testing-specific NuGet README (#8)
DJGosnell May 28, 2026
ded2a70
REVIEW REMEDIATE (s8): update session artifacts (pr-body, review acti…
DJGosnell May 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: 'recursive'
Expand All @@ -25,7 +25,7 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y libmsquic

- name: Install .NET
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.*'
source-url: https://api.nuget.org/v3/index.json
Expand All @@ -38,12 +38,20 @@ jobs:
- name: Validate AOT compatibility
run: dotnet build src/Samples/NexNetSample.Aot.Client -c Release --no-restore -p:TreatWarningsAsErrors=true -p:NoWarn=IL2104%3BIL3053%3BIL3050

# Test runs use --logger "console;verbosity=minimal" so individual passing tests are
# suppressed (the integration suite alone would dump thousands of lines per CI run).
# Each assembly still emits a single summary line ("Passed! - Failed: 0, Passed: N,
# Total: N, Duration: X") and full failure details when tests fail. Step elapsed time in
# the GitHub Actions UI shows progress.
- name: Execute integration tests
run: dotnet test src/NexNet.IntegrationTests -c Release --no-build -v=normal
run: dotnet test src/NexNet.IntegrationTests -c Release --no-build -v=minimal --logger "console;verbosity=minimal"

- name: Execute generator tests
run: dotnet test src/NexNet.Generator.Tests -c Release --no-build -v=normal

run: dotnet test src/NexNet.Generator.Tests -c Release --no-build -v=minimal --logger "console;verbosity=minimal"

- name: Execute testing-harness tests
run: dotnet test src/NexNet.Testing.Tests -c Release --no-build -v=minimal --logger "console;verbosity=minimal"

- name: Pack NexNet
run: dotnet pack src/NexNet -c Release -o ./artifacts --maxcpucount:1

Expand All @@ -52,12 +60,15 @@ jobs:

- name: Pack NexNet.Asp
run: dotnet pack src/NexNet.Asp -c Release -o ./artifacts --maxcpucount:1


- name: Pack NexNet.Testing
run: dotnet pack src/NexNet.Testing -c Release -o ./artifacts --maxcpucount:1

- name: Pack NexNet.Generator
run: dotnet pack src/NexNet.Generator -c Release -o ./artifacts --maxcpucount:1

- name: Export artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
path: artifacts/*

Expand Down
124 changes: 124 additions & 0 deletions _sessions/add-nexnet-testing/_pr-body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
## 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.

## 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:

```csharp
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 1** — `PendingInvocationCount` accessor on `ISessionInvocationStateManager`.
- **Phase 2** — `IInvocationInterceptor` interface + ConfigBase / NexusSessionConfigurations plumbing + Receiving wire-up.
- **Phase 3** — `IPipeFactory` interface + WrapLocal / WrapRemote hook points in `NexusPipeManager`.
- **Phase 4** — `ServerConfig.OnAuthenticateOverride` + `ServerNexusBase.Authenticate` consult-then-fallback.
- **Phase 5** — `NexNet.Testing` project with `InProcessTransport` (paired Pipes cross-wired), listener with Channel-based accept queue, `InProcessServerConfig`/`ClientConfig`, and `InProcessRendezvous`.
- **Phase 6** — `Type.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 8** — `TestInvocationInterceptor`, `QuiescenceCounters`, `QuiescenceTracker` with observe-zero/yield/re-observe pattern.
- **Phase 9** — `PipeRecording`, `TappingPipeReader`/`TappingPipeWriter`, `TappedNexusDuplexPipe`/`TappedRentedNexusDuplexPipe`, `TestPipeFactory`.
- **Phase 10** — `NexusTestHost.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 #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 #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 values** — `AssertReceived` 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.

## 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 v1** — `NexusTestHost`, `NexusTestClient`, `NexusAssertionException`, `Arg.Any<T>()`, `Arg.Is<T>(...)`, `TestIdentity.Of(...)`, `PipeRecording`, `ChannelRecording<T>`, `GroupView`, `GroupIntrospector`, `StreamingExtensions`. Future iteration may add overloads non-breakingly.

### 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` (80 tests) covers transport, recorder, MethodIdMap parity, 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.
Loading