Skip to content

test(relay): non-sequential socket cases — commands that race the connection, not just sequential ones #1052

Open

Description

@chappie-daemon

Why this exists

#1044 is a dropped-frame bug that a green suite could not see. Measured on 3.12.0: 8 immediate REQs after connect lost 2 EOSE, 10 lost 1, and the same code with a 1 s delay answered 4/4. The existing integration suite is sequential — connect, one subscription, assert, close — so the frames that race the socket's own setup are never in flight when an assertion is watching. #1045 adds the one test that catches it (real socket, subscribe at open, assert EOSE; it times out at base df8d3d6).

The point of this issue is that shape is not unique to that bug. Anything the server does on or just after a connection, or while another command is in flight, is untested for the same reason. These tests should assert the correct outcome, not merely that the server survives — a relay that drops a REQ has survived perfectly well.

Shapes to cover, each expected to work

  1. Subscribe at the open tick. A REQ sent in the same turn as the socket's open, and N sockets doing it at once (the reconnect-storm shape). Assert EOSE per socket. This is fix(relay): buffer messages during WebSocket admission #1045; the rest are its siblings.
  2. Pipelined commands in one write. Two REQs, or an EVENT followed by a REQ, in a single frame. Assert both answers arrive in the order they were sent. A per-connection buffer that loses its first frames fails here as it fails in 1.
  3. REQ followed immediately by CLOSE of the same id. Assert no events after the CLOSE and no error; a cancel that races its own subscribe must not leave a live subscription or a dropped socket.
  4. EVENT sent in the same turn as open. Assert the OK arrives. Same window as 1, different reply path — worth its own test because the OK is what a publisher blocks on.
  5. Fragmented and binary frames. A command split across two frames, and a binary frame carrying the same JSON. Assert the command is honoured and the next command on the same socket still is. Parser state that resets mid-frame loses a command and then keeps going.
  6. Invalid JSON mid-stream. Assert a NOTICE (or the documented refusal) and that the socket stays usable for the next valid command. A drop here looks exactly like the bug in 1 from the client's side.

Acceptance

  • Deterministic: no sleep to "let the server catch up", because a delay is the thing that hides the bug. Where timing is inherent, repeat the case enough times (e.g. 20 iterations of shape 1 in one test) that the race is exercised, and fail if any iteration misses.
  • Each test must fail against the current base for the reason it names — the same bar fix(relay): buffer messages during WebSocket admission #1045 met — or it is not catching anything.
  • The suite must run in CI where the existing integration job runs, so this class cannot silently regress.

Note: 3.x carries the same ordering (#1044, at src/adapters/web-socket-server-adapter.ts:137 on 3.12.0). If the 3.x line is still supported, shapes 1 and 4 belong there too; the rest can land on rewrite/main only.

Activity

  1. tirthraj01 commented on Nov 5, 2026

    @tirthraj01

    Hi @cameri @chappie-daemon,

    I would like to take this up and implement the non-sequential socket test suite outlined in this issue.

    Here is my plan for covering the test cases:

    1. Connection race & admission storm (Shapes 1 & 4): Immediate REQ and EVENT dispatched on the open tick, along with multi-socket concurrent connect storms, asserting deterministic EOSE and OK responses.
    2. Pipelining & Frame boundaries (Shapes 2 & 5): Multiple commands packed into a single write frame (e.g., EVENT + REQ / dual REQ) asserting ordered processing, as well as fragmented payloads split across chunked and binary frames.
    3. Racing cancellation (Shape 3): Immediate CLOSE following a REQ for the same subscription ID, verifying no lingering subscriptions or unhandled errors.
    4. Stream recovery (Shape 6): Malformed JSON sent mid-stream, verifying appropriate NOTICE/rejection and confirming the socket connection remains healthy for subsequent valid commands.

    Implementation constraints I will follow:

    • Zero arbitrary sleep() delays: The suite will be strictly deterministic, using repeated iterations to reliably trigger and assert race conditions without masking timing bugs.
    • Fail-before-pass verification: Ensuring each test fails against base before the fix/ordering assertions pass.
    • Ensuring seamless execution in the existing CI pipeline.

    Could you please assign this issue to me, and confirm whether you would like the PR targeted against rewrite/main or main? I will fork the repository and get started right away.

  2. cameri commented on Nov 5, 2026

    @cameri
    Owner

    @tirthraj01 This targets main. I'll assign it to you. Thanks for your interest in Nostream.

  3. tirthraj01 commented on Nov 6, 2026

    @tirthraj01

    Thanks @cameri! Noted on targeting main. I'm actively working on implementing the test cases across all shapes now and expect to have the branch ready for review within the next 12–15 hours.

  4. tirthraj01 commented on Nov 7, 2026

    @tirthraj01

    Hi @cameri and @chappie-daemon,

    I have completed the test suite for #1052.

    Context & Problem Solved:

    Previously, the adapter's real socket test suite only verified idealized, sequential happy paths (connect → wait → send). Real-world Nostr relays frequently encounter out-of-order and asynchronous race conditions.

    This update introduces real ws loopback tests verifying WebSocketServerAdapter correctly handles:

    1. TCP chunk fragmentation: Split frames arriving while async admission check is pending are reassembled without corruption.
    2. Admission disconnect race: Sockets dropping before the rate limiter resolves do not emit orphan frames.
    3. Post-closure ingestion: Commands sent during/after socket closure do not leak state or crash the adapter.
    4. Concurrent multi-client traffic: Interleaved frames across multiple racing clients remain strictly isolated.
    5. Backpressure handling: Write-queue saturation is managed cleanly under high throughput.
    6. Rate-limiter rejection race: Sockets are cleanly terminated when rapid messages follow a rejection.

    Quality & Reliability:

    • 100% Deterministic: Zero arbitrary sleep() calls; uses event predicate polling (waitFor) and clean teardown.
    • Suite Status: All 7 real socket tests pass in ~150ms.
    • Changeset: Included .changeset/test-non-sequential-socket-cases.md.

    Compare Branch:

    Due to active repository interaction limits, the branch is ready on my fork:
    👉 Compare: main...tirthraj01:nostream:test/1052-non-sequential-socket-cases

    Please let me know if you would like any adjustments or if you can open the PR from this branch!

  5. chappie-daemon commented on Nov 7, 2026

    @chappie-daemon
    CollaboratorAuthor

    Cherry-picked onto current main as #1072, with -x so your commit (1a63392) and authorship stay on the record. Your branch is untouched — the work was taken from the fork rather than pushed to it.

    Verified before picking it up: npx mocha test/unit/adapters/web-socket-server-adapter-real.spec.ts gives 7 passing in 187 ms here, and the commit adds only the test file and its changeset.

  6. chappie-daemon commented on Nov 7, 2026

    @chappie-daemon
    CollaboratorAuthor

    @tirthraj01 I have posted the detailed brief on #1072 — Cameri asked for the two Greptile findings to be closed, and I added a third ask: show at least the admission shapes reddening when the buffering is broken, since a test that cannot fail is not yet evidence. Push to your fork branch as new commits and I will pick them up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions