Skip to content

Publish in-process server-client pair test harness (12.3.2)#515

Merged
leynos merged 12 commits intomainfrom
feat-inprocess-server-client-test-ag7dz7
Apr 5, 2026
Merged

Publish in-process server-client pair test harness (12.3.2)#515
leynos merged 12 commits intomainfrom
feat-inprocess-server-client-test-ag7dz7

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Mar 29, 2026

Summary

  • Implement a reusable in-process server-client pair harness for testing (12.3.2).
  • Adds a public harness surface in wireframe_testing that starts a bound WireframeServer and connects a WireframeClient over a real loopback TCP socket within a single test process.
  • Includes integration tests, BDD scenarios, and test fixtures to validate the public API from end to end.
  • Updates docs and roadmap to reflect the harness design and its public surface.

Changes

  • New: wireframe_testing/src/client_pair.rs
    • Public harness module that exposes a WireframePair, spawn_wireframe_pair, and spawn_wireframe_pair_default.
    • Uses unused_listener() and WireframeServer::bind_existing_listener(...) to deterministically bind and obtain local_addr.
    • Provides a minimal, ergonomic API surface: client_mut(), local_addr(), shutdown(), and Drop semantics.
  • Updated: wireframe_testing/src/lib.rs
    • Exposes the new client_pair module and re-exports WireframePair, spawn_wireframe_pair, and spawn_wireframe_pair_default.
  • New: tests/client_pair_harness.rs
    • rstest-based integration tests exercising default and custom client configurations, round trips, and shutdown behavior.
  • New: tests/features/client_pair_harness.feature
    • BDD feature with scenarios for default pairing and custom configuration.
  • New: tests/fixtures/client_pair_harness.rs
    • BDD world fixture implementing setup, start, request/response cycles, and drop behavior.
  • New: tests/scenarios/client_pair_harness_scenarios.rs
    • BDD scenario bindings for the harness scenarios.
  • New: tests/scenarios/client_pair_harness_scenarios.rs
    • Scenario bindings wiring for the harness world.
  • New: tests/steps/client_pair_harness_steps.rs
    • Step definitions for given/when/then that drive the BDD scenarios.
  • Updated: tests/fixtures/mod.rs, tests/steps/mod.rs, tests/scenarios/mod.rs
    • Registers new harness-related modules for integration test layout.
  • Docs updates:
    • docs/wireframe-testing-crate.md: describes the new client_pair harness module, its public API, and usage.
    • docs/wireframe-client-design.md: documents the in-process harness rationale and API boundary, including why it lives in wireframe_testing.
    • docs/users-guide.md: consumer-facing example showing how to use spawn_wireframe_pair and the public surface.
    • docs/roadmap.md: marks 12.3.2 as done after validation.
  • Roadmap alignment:
    • 12.3.2 now implemented with a concrete, testable harness including Stage A–F style milestones reflected in code and tests.

What this change covers

  • Objective: Provide a reusable in-process server and client test harness that runs both components in the same test process over a real loopback TCP connection to validate client/server compatibility while remaining fast and deterministic.
  • Public API surface (initial direction): a small, ergonomic surface (spawn_wireframe_pair(...) with optional client-builder configuration) yielding a pair handle exposing minimal lifecycle and client access with explicit shutdown.
  • Implementation strategy: reuse existing helpers (unused_listener, bind_existing_listener, TestResult/TestError) and keep new code focused inside wireframe_testing. Public API introduced first as a spike to validate ergonomics, then refined if necessary.

Implementation stages (high level)

  • Stage A: API spike and acceptance test design
    • Proved smallest public API can support both default round-trip and non-default client-builder config.
  • Stage B: Implement the public harness in wireframe_testing
    • Create new module wireframe_testing/src/client_pair.rs with WireframePair and spawn helpers.
    • Reserve a loopback listener with unused_listener(); bind with WireframeServer::bind_existing_listener(...).
    • Expose a minimal surface: client access, local_addr, shutdown, and Drop semantics.
  • Stage C: Add rstest integration coverage
    • Integration tests exercising default round-trip and custom client configuration.
  • Stage D: Add rstest-bdd behavioural coverage
    • Downstream-crate-style scenarios exercising the public API from the consumer perspective.
  • Stage E: Update design/docs/usability docs
    • Document public API decisions, usage patterns, and rationale for in-process harness placement.
  • Stage F: Validation and evidence capture
    • Run targeted tests, lint, docs checks, and integration tests; provide gate checks before proceeding.

Risks and considerations

  • API rigidity vs. flexibility: aim for a small, readable surface; ready to introduce a two-layer API if needed.
  • Production surface impact: harness remains in wireframe_testing to avoid widening production surface.
  • Lifecycle safety: ensure explicit shutdown is always possible and Drop does not hide leaks.
  • Testing scope: harness uses real loopback TCP to exercise end-to-end client/server flow rather than in-memory fixtures.
  • Workspace boundaries: reuse existing helpers; wireframe_testing is not a workspace member; integration tests live under tests/.

Progress status

  • ExecPlan updated to reflect implemented Stage A–F milestones; concrete harness and tests added as described above.

Documentation impact

  • Documentation updated to describe the new harness surface, usage patterns, and rationale.
  • Roadmap item 12.3.2 updated to reflect completion following validation gates.
    +- New: docs/execplans/12-3-2-in-process-server-and-client-pair-test-harness.md describing the ExecPlan for this milestone.

Review guidance

  • Assess clarity and completeness of the public surface, lifecycle guarantees, and staged rollout reflected by the implementation.
  • Verify alignment with roadmap item 12.3.2 and consistency with existing test infrastructure (wireframe_testing, rstest, rstest-bdd).
  • Flag any gaps where additional constraints or documentation would be beneficial before proceeding to Stage A implementation.

📎 Task: https://www.devboxer.com/task/1ee16d32-1205-4834-b001-ca47396977ba

Summary by Sourcery

Introduce a reusable in-process Wireframe server/client pair harness in the wireframe_testing crate and validate it with integration and BDD tests while updating documentation and roadmap for roadmap item 12.3.2.

New Features:

  • Add wireframe_testing::client_pair module exposing WireframePair plus spawn_wireframe_pair and spawn_wireframe_pair_default for loopback integration testing.
  • Expose the new client pair harness types and functions from wireframe_testing::lib for downstream consumers.

Enhancements:

  • Extend wireframe_testing dependencies to enable TCP networking and synchronization support required by the new harness.
  • Add high-level docs describing the in-process client pair harness API, lifecycle, rationale, and usage patterns in the testing crate docs, user guide, and client design documents.
  • Add an ExecPlan documenting constraints, decisions, progress, and validation for roadmap item 12.3.2.
  • Mark roadmap item 12.3.2 as completed after implementation and validation.

Tests:

  • Add rstest-based integration tests covering default and customized client configuration, round-trip behaviour, explicit shutdown, and drop safety of the client pair harness.
  • Add rstest-bdd feature, world fixture, steps, and scenarios that exercise the client pair harness from the downstream crate perspective.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 29, 2026

In-process server–client pair test harness

This PR adds a reusable in‑process Wireframe server–client pair test harness to the wireframe_testing crate, completes roadmap item 12.3.2, and publishes a living execution‑plan at docs/execplans/12-3-2-in-process-server-and-client-pair-test-harness.md.

Core functionality

  • New module: wireframe_testing::client_pair
    • WireframePair — owns a real WireframeServer bound to a reserved loopback listener and a connected WireframeClient (real TCP) within the same test process.
    • spawn_wireframe_pair(...) and spawn_wireframe_pair_default(...) — reserve an unused listener (unused_listener()), bind the server via WireframeServer::bind_existing_listener(...), spawn the server with a readiness handshake, apply an optional client‑builder closure, then connect the client to the server address.
    • Startup is protected by a PendingServer RAII guard so server tasks are torn down if setup fails; the guard is removed on successful transfer into WireframePair.

Lifecycle guarantees and behaviour fixes

  • client_mut() — mutable access to the live client; returns TestError if called after shutdown.
  • local_addr() — returns the bound loopback socket address.
  • shutdown().await — orderly teardown that closes the client, sends the server shutdown one‑shot, awaits the server task, maps join/server errors into TestError, and clears internal Running state only after awaited cleanup completes.
  • Cancellation‑safety and task‑leak handling:
    • Implementation preserves Running in self until the awaited server‑join completes to avoid partial teardown when shutdown is interrupted.
    • spawn_wireframe_pair preserves a PendingServer guard (and its shutdown channel/handle) through client.connect success to prevent server‑task leaks when connection fails during startup.
    • Drop safety net — Drop signals shutdown and uses a bounded shutdown helper that races server join against a timeout (uses Tokio Handle::try_current(); aborts immediately when no runtime is available) to avoid leaked server tasks.

Code‑quality note & reviewer discussion

  • Automated finding: CodeScene flagged nested conditional complexity in WireframePair::shutdown.
  • Reviewer/requested fix: A proposed flattening (using self.running.take() and let‑else early returns) was suggested to reduce nesting and satisfy the threshold.
  • Implementation caution: A later commit/inspection restored cancellation‑safety and lifecycle behaviour by using self.running.as_mut() rather than take(), and ensuring PendingServer remains until client.connect succeeds. Any flattening must preserve these cancellation‑safety and task‑leak invariants; reviewers should validate that changes to shutdown keep the running state until server join completes and do not reintroduce startup task leaks.

Supporting test helpers

  • wireframe_testing::integration_helpers extended with echo‑oriented primitives:
    • Echo(u8) payload type
    • CommonTestApp and CommonHandler aliases
    • echo_handler(counter) and echo_app_factory(counter) used by the harness tests

Tests & BDD coverage

  • Integration tests: tests/client_pair_harness.rs — exercises request/response round trips, custom client configuration (max_frame_length), explicit shutdown, and Drop safety.
  • BDD artifacts:
    • Feature: tests/features/client_pair_harness.feature
    • Fixture: tests/fixtures/client_pair_harness.rs — ClientPairHarnessWorld with start_default_pair, start_pair_with_max_frame_length, send_request, assert_response
    • Steps: tests/steps/client_pair_harness_steps.rs
    • Scenarios: tests/scenarios/client_pair_harness_scenarios.rs
    • Module registrations updated in tests/fixtures/mod.rs, tests/scenarios/mod.rs, tests/steps/mod.rs

Documentation & execplan

  • ExecPlan: docs/execplans/12-3-2-in-process-server-and-client-pair-test-harness.md — scope, constraints/tolerances, staged A–F plan, decision log, validation commands and expected artefacts; reference this document when reviewing design/acceptance.
  • docs/roadmap.md — marks 12.3.2 completed.
  • docs/wireframe-testing-crate.md, docs/wireframe-client-design.md, docs/users-guide.md — document the harness API, control flow, lifecycle semantics, Drop behaviour, and usage examples (including spawn_wireframe_pair_default and client‑builder customization).

Cargo / dependency change

  • wireframe_testing/Cargo.toml — tokio features expanded to include net and sync to support loopback networking and sync primitives.

Public API exports

  • Re‑exported at wireframe_testing crate root: WireframePair, spawn_wireframe_pair, spawn_wireframe_pair_default
  • Exposed test helpers/types: Echo, CommonHandler, CommonTestApp, echo_app_factory, echo_handler

Notable implementation details for reviewers

  • PendingServer RAII guard ensures server task is cleaned up on early returns or panics during startup; recent fixes ensure the guard remains until client.connect completes to avoid leaks.
  • shutdown preserves Running in self until awaited cleanup finishes to maintain cancellation‑safety; Drop uses a spawn_bounded_shutdown helper to attempt orderly teardown when a Tokio runtime is present, otherwise falls back to immediate abort.
  • A suggested refactor to reduce nested conditionals in shutdown exists; reviewers must ensure any refactoring preserves the cancellation‑safety and task‑leak protections described above.

Review guidance

Please focus review on:

  • Public surface and documentation alignment (re‑exports and examples) and cross‑reference the new execplan for design/acceptance criteria.
  • Lifecycle correctness: shutdown().await semantics, cancellation‑safety, client_mut() post‑shutdown behaviour, and server teardown on client‑connection failure.
  • Correctness and safety of RAII/guarding behaviour at startup (PendingServer) and Drop/bounded‑shutdown handling with respect to runtime presence.
  • The CodeScene nested‑conditional finding in WireframePair::shutdown and any proposed flattening — ensure behaviour, error messages and cancellation semantics remain unchanged.

Walkthrough

Summarise a new in-process test harness that spawns a loopback-TCP WireframeServer and connected WireframeClient inside the same test process, plus supporting docs, BDD fixtures, integration tests, helpers and crate re‑exports for lifecycle and shutdown control.

Changes

Cohort / File(s) Summary
Test Harness Implementation
wireframe_testing/src/client_pair.rs, wireframe_testing/src/lib.rs, wireframe_testing/Cargo.toml
Add client_pair module and re-exports. Implement WireframePair, spawn_wireframe_pair, spawn_wireframe_pair_default. Manage listener reservation, server readiness handshake, shutdown signalling, RAII guards and defensive Drop. Enable Tokio net and sync features.
Integration Helpers
wireframe_testing/src/integration_helpers.rs
Add Echo(pub u8), CommonTestApp/CommonHandler aliases, echo_handler and echo_app_factory helpers; export these for tests and examples.
Integration Tests
tests/client_pair_harness.rs
Add Tokio integration tests for echo round-trip, custom client-builder behaviour (max_frame_length), explicit shutdown semantics and drop safety using echo_app_factory and an invocation counter.
BDD Feature & Fixtures
tests/features/client_pair_harness.feature, tests/fixtures/client_pair_harness.rs, tests/fixtures/mod.rs
Add Cucumber feature file and ClientPairHarnessWorld fixture (runtime, counter, optional pair/response) with helpers to start pair, send requests and assert responses; register fixture module.
BDD Steps & Scenarios
tests/steps/client_pair_harness_steps.rs, tests/steps/mod.rs, tests/scenarios/client_pair_harness_scenarios.rs, tests/scenarios/mod.rs
Add Given/When/Then step definitions and scenario bindings that exercise the harness via the fixture and feature file.
Documentation & Roadmap
docs/execplans/...12-3-2-in-process-server-and-client-pair-test-harness.md, docs/wireframe-client-design.md, docs/wireframe-testing-crate.md, docs/users-guide.md, docs/roadmap.md
Add execution plan, design notes, user guide and examples for client-pair harness; document lifecycle, observable behaviours, validation commands and mark roadmap task 12.3.2 complete.

Sequence Diagram

sequenceDiagram
    participant Test as Test Code
    participant Harness as spawn_wireframe_pair()
    participant Listener as unused_listener()
    participant Server as WireframeServer
    participant TCP as Loopback TCP
    participant Client as WireframeClient

    Test->>Harness: Call spawn_wireframe_pair(app_factory, config)
    Harness->>Listener: Reserve loopback address
    Harness->>Server: bind_existing_listener(listener)
    Server->>Server: Spawn server task
    Server-->>Harness: Signal readiness via oneshot (rgba(0,128,0,0.5))
    Harness->>Harness: Build client with config
    Harness->>Client: Connect to server addr
    Client->>TCP: Open loopback connection
    TCP->>Server: Deliver connection
    Harness->>Test: Return WireframePair

    Test->>Client: client_mut().call(request)
    Client->>TCP: Send request
    TCP->>Server: Receive request
    Server->>Server: Invoke app handler (echo)
    Server->>TCP: Send response
    TCP->>Client: Deliver response
    Client->>Test: Return response

    Test->>Harness: Call pair.shutdown()
    Harness->>Server: Send shutdown signal
    Server->>Server: Drain and stop task
    Harness->>Test: Shutdown complete
Loading

Poem

✨ Spawn the pair on loopback's line,
Server readies, client threads align,
Echoes bob across the socket tide,
Send shutdown; RAII keeps tasks justified,
Tests finish tidy, harness verified.

🚥 Pre-merge checks | ✅ 4 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Developer Documentation ⚠️ Warning New internal APIs (WireframePair, spawn_wireframe_pair) and architectural additions lack rustdoc comments in client_pair.rs module, and no developer's guide documents this new testing pattern for maintainers. Add rustdoc comments to client_pair.rs module using //! and /// for all public items. Create docs/development-guide.md documenting the new architectural pattern, lifecycle guarantees, and cancellation-safety semantics.
Module-Level Documentation ⚠️ Warning Five new Rust modules lack module-level docstrings explaining their purpose, utility, and function. Add //! docstrings to wireframe_testing/src/client_pair.rs and all test modules, documenting their respective purposes and utilities before any use statements or code.
Testing ❓ Inconclusive Unable to execute shell commands to examine test files and code coverage for the PR. Provide direct access to test files, implementation code, or use a system that allows file inspection to assess test coverage properly.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and directly describes the main change: publishing an in-process server-client pair test harness for roadmap item 12.3.2.
Description check ✅ Passed The PR description is comprehensive and thoroughly related to the changeset, documenting the objectives, implementation stages, changes, and validation approach for the harness.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
User-Facing Documentation ✅ Passed All new public APIs are properly exported from wireframe_testing/src/lib.rs and comprehensively documented in docs/users-guide.md with clear guidance on purpose, usage patterns, dependency requirements, and working examples.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-inprocess-server-client-test-ag7dz7

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Mar 29, 2026

Reviewer's Guide

Implements a new in-process Wireframe server/client pair harness in the wireframe_testing crate, exposes a minimal public API (WireframePair and spawn helpers), and validates it via rstest integration tests, rstest-bdd scenarios, and documentation updates tied to roadmap item 12.3.2.

Sequence diagram for spawn_wireframe_pair server-client startup lifecycle

sequenceDiagram
    actor TestAuthor
    participant TestCode
    participant client_pair_module
    participant unused_listener
    participant WireframeServer
    participant TokioRuntime
    participant WireframeClientBuilder
    participant WireframeClient

    TestAuthor->>TestCode: write async test
    TestCode->>client_pair_module: spawn_wireframe_pair(app_factory, configure_client)

    client_pair_module->>unused_listener: unused_listener()
    unused_listener-->>client_pair_module: TcpListener

    client_pair_module->>WireframeServer: new(app_factory)
    WireframeServer-->>client_pair_module: WireframeServer

    client_pair_module->>WireframeServer: workers(1)
    WireframeServer-->>client_pair_module: WireframeServer

    client_pair_module->>WireframeServer: bind_existing_listener(TcpListener)
    WireframeServer-->>client_pair_module: WireframeServer

    client_pair_module->>WireframeServer: local_addr()
    WireframeServer-->>client_pair_module: SocketAddr

    client_pair_module->>TokioRuntime: spawn(server.run_with_shutdown(...))
    TokioRuntime-->>client_pair_module: JoinHandle

    client_pair_module->>WireframeServer: ready_signal(ready_tx)
    WireframeServer-->>TokioRuntime: signal ready when bound

    client_pair_module->>TokioRuntime: await ready_rx
    TokioRuntime-->>client_pair_module: ready

    client_pair_module->>WireframeClientBuilder: new()
    WireframeClientBuilder-->>client_pair_module: WireframeClientBuilder

    client_pair_module->>WireframeClientBuilder: configure_client(builder)
    WireframeClientBuilder-->>client_pair_module: WireframeClientBuilder

    client_pair_module->>WireframeClientBuilder: connect(SocketAddr)
    WireframeClientBuilder-->>client_pair_module: WireframeClient

    client_pair_module-->>TestCode: WireframePair

    TestCode->>WireframePair: client_mut()
    WireframePair-->>TestCode: &mut WireframeClient

    TestCode->>WireframePair: shutdown().await
    WireframePair->>WireframeClient: drop
    WireframePair->>TokioRuntime: send shutdown signal
    TokioRuntime-->>WireframePair: server task completes
    WireframePair-->>TestCode: Ok
Loading

Class diagram for the new WireframePair harness API

classDiagram
    class WireframePair {
        - WireframeClient client
        - SocketAddr addr
        - Sender shutdown_tx
        - JoinHandle handle
        + client_mut() WireframeClient
        + local_addr() SocketAddr
        + shutdown() TestResult
    }

    class WireframeClient {
    }

    class WireframeClientBuilder {
        + new() WireframeClientBuilder
        + connect(addr SocketAddr) WireframeClient
    }

    class WireframeServer {
        + new(app_factory AppFactory) WireframeServer
        + workers(count int) WireframeServer
        + bind_existing_listener(listener TcpListener) WireframeServer
        + local_addr() SocketAddr
        + ready_signal(sender Sender) WireframeServer
        + run_with_shutdown(shutdown_future Future) Result
    }

    class AppFactory {
    }

    class TestResult {
    }

    class TestError {
    }

    class Sender {
        + send(value void) void
    }

    class JoinHandle {
        + await() Result
        + abort_handle() AbortHandle
    }

    class AbortHandle {
        + abort() void
    }

    class TcpListener {
    }

    class TcpStream {
    }

    class BincodeSerializer {
    }

    class RewindStream {
    }

    class FrameCodec {
    }

    WireframePair "1" o-- "1" WireframeClient : holds
    WireframePair "1" o-- "1" Sender : shutdown_tx
    WireframePair "1" o-- "1" JoinHandle : handle

    WireframeClientBuilder ..> WireframeClient : builds
    WireframeServer ..> AppFactory : uses
    WireframeServer ..> TcpListener : binds

    WireframeClient ..> TcpStream : uses
    WireframeClient ..> BincodeSerializer : serializes
    WireframeClient ..> RewindStream : wraps

    RewindStream ..> TcpStream : wraps

    WireframeServer ..> FrameCodec : uses

    TestResult ..> TestError : wraps

    class client_pair_module {
        + spawn_wireframe_pair(app_factory AppFactory, configure_client ConfigureClientFn) TestResult
        + spawn_wireframe_pair_default(app_factory AppFactory) TestResult
    }

    client_pair_module ..> WireframePair : returns
    client_pair_module ..> WireframeServer : starts
    client_pair_module ..> WireframeClientBuilder : configures
    client_pair_module ..> Sender : creates_channels
    client_pair_module ..> JoinHandle : spawns_task
Loading

File-Level Changes

Change Details Files
Introduce wireframe_testing::client_pair harness with WireframePair type and spawn helpers using real loopback TCP and coordinated shutdown.
  • Add WireframePair struct encapsulating a WireframeServer task, connected WireframeClient, loopback address, and shutdown coordination primitives.
  • Implement client_mut(), local_addr(), and async shutdown() with explicit join and error propagation via TestError/TestResult.
  • Implement Drop for WireframePair to send shutdown and abort the server task after a timeout when shutdown() is not called.
  • Add spawn_wireframe_pair(...) that reserves a loopback listener via unused_listener(), binds WireframeServer::bind_existing_listener(...), uses oneshot channels for readiness/shutdown, and connects a configured WireframeClientBuilder.
  • Add spawn_wireframe_pair_default(...) as a thin wrapper over spawn_wireframe_pair using default client settings.
  • Constrain generics to a concrete BincodeSerializer/() client while keeping server-side AppFactory generic over Packet and FrameCodec types.
wireframe_testing/src/client_pair.rs
Expose the new harness from wireframe_testing and extend its Tokio feature set for networking and sync primitives.
  • Export the client_pair module and publicly re-export WireframePair, spawn_wireframe_pair, and spawn_wireframe_pair_default from the crate root.
  • Enable additional Tokio features (net, sync) required by the new harness in wireframe_testing's Cargo.toml.
wireframe_testing/src/lib.rs
wireframe_testing/Cargo.toml
Add rstest-based integration tests that exercise round-trip behaviour, client configuration, and lifecycle semantics of the harness.
  • Define a TestApp and Echo payload plus a small echo handler/factory that routes on a single message id using CommonTestEnvelope.
  • Add round_trip_via_pair_harness test that uses spawn_wireframe_pair_default, performs a call via client_mut(), and asserts correlation id, payload, and handler invocation count.
  • Add custom_max_frame_length_via_pair_harness test that configures max_frame_length via the configure_client closure and verifies behaviour.
  • Add explicit_shutdown_completes_cleanly test that checks local_addr() returns a loopback, non-zero port and that shutdown() completes.
  • Add drop_without_explicit_shutdown_does_not_hang test to validate that dropping WireframePair without shutdown() does not hang the runtime (Drop safety net).
tests/client_pair_harness.rs
Add rstest-bdd world, steps, scenarios, and feature file to exercise the harness from a downstream-consumer perspective.
  • Introduce ClientPairHarnessWorld BDD world encapsulating a Tokio runtime, an invocation counter, an optional WireframePair, and last response storage, plus a Default implementation that constructs the runtime and shared state.
  • Add start_default_pair() and start_pair_with_max_frame_length() methods that spawn pairs using spawn_wireframe_pair_default and spawn_wireframe_pair with builder customization.
  • Add send_request() and assert_response() world methods that construct CommonTestEnvelope messages with an Echo payload, call through pair.client_mut().call(...), and validate correlation id and payload decoding.
  • Define a rstest fixture client_pair_harness_world to provide the world to steps.
  • Implement BDD step functions for Given/When/Then that delegate to the world methods, parameterising ids, correlation ids, and max_frame_length.
  • Wire scenarios in client_pair_harness_scenarios.rs to the new .feature scenarios using rstest_bdd_macros::scenario.
  • Create client_pair_harness.feature with two scenarios: one for default round trip via the harness and one for custom client configuration via max frame length.
tests/fixtures/client_pair_harness.rs
tests/steps/client_pair_harness_steps.rs
tests/scenarios/client_pair_harness_scenarios.rs
tests/features/client_pair_harness.feature
Register new BDD modules in the existing fixtures/steps/scenarios layout so they are compiled and executed.
  • Add client_pair_harness module to fixtures/mod.rs.
  • Add client_pair_harness_steps module to steps/mod.rs.
  • Add client_pair_harness_scenarios module to scenarios/mod.rs.
tests/fixtures/mod.rs
tests/steps/mod.rs
tests/scenarios/mod.rs
Document the new client_pair harness and mark roadmap item 12.3.2 complete, including an execution plan document.
  • Extend wireframe-testing-crate.md with a crate-layout entry for src/client_pair.rs and a dedicated "In-process server/client pair harness" section documenting the public API, lifecycle, example usage, and rationale for using loopback TCP and wireframe_testing placement.
  • Extend users-guide.md with a consumer-facing "In-process server/client pair harness" section, including example code using spawn_wireframe_pair and spawn_wireframe_pair_default, and a note about dependencies and when to use this harness versus in-memory drive_with_* helpers.
  • Extend wireframe-client-design.md with an "In-process server and client pair harness" section that explains the rationale, placement in wireframe_testing, high-level behaviour (listener reservation, bind_existing_listener, readiness signalling, shutdown and Drop semantics), and ownership/borrowing for streaming responses.
  • Update docs/roadmap.md to mark roadmap item 12.3.2 as completed ([x]).
  • Add an ExecPlan document detailing constraints, tolerances, risks, progress, decisions, implementation stages, validation, and outcomes for the 12.3.2 in-process server/client pair harness work.
docs/wireframe-testing-crate.md
docs/users-guide.md
docs/wireframe-client-design.md
docs/roadmap.md
docs/execplans/12-3-2-in-process-server-and-client-pair-test-harness.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

codescene-delta-analysis[bot]

This comment was marked as outdated.

Introduce a reusable public test harness in wireframe_testing that starts a real Wireframe server
and a connected client within the same test process using real loopback TCP sockets. This harness
aims to reduce duplication, lifecycle bugs, and complexity in client-facing tests by providing a
clean, generic API for bounded server/client orchestration. It includes support for client builder
customization, explicit shutdown handling, and integrates with rstest and rstest-bdd for coverage.

This change also updates design docs, user guides, and roadmap to document this new validation
milestone (12.3.2), ensuring downstream crates can leverage the harness for protocol integration
and compatibility tests.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@devboxerhub devboxerhub Bot force-pushed the feat-inprocess-server-client-test-ag7dz7 branch from a35f755 to 20c4f5f Compare April 3, 2026 15:34
codescene-delta-analysis[bot]

This comment was marked as outdated.

Add WireframePair, spawn_wireframe_pair, and spawn_wireframe_pair_default
to wireframe_testing, providing a reusable loopback TCP harness that
starts a real server and connects a client within the same test process.
This eliminates lifecycle boilerplate and address-race bugs in
client-facing integration tests.

- Stage A+B: client_pair module in wireframe_testing with WireframePair
  struct, spawn functions, graceful shutdown, and Drop safety net
- Stage C: four rstest integration tests covering round-trip, custom
  max frame length, explicit shutdown, and drop-without-shutdown
- Stage D: two rstest-bdd scenarios with full fixture/steps/scenario
  wiring for default and custom-configuration round trips
- Stage E: updated wireframe-client-design, wireframe-testing-crate,
  users-guide, and roadmap documentation; exec plan marked COMPLETE
- Stage F: all quality gates pass (check-fmt, lint, test, markdownlint,
  nixie)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@leynos leynos changed the title Add in-process server-client test harness plan (12.3.2) Publish in-process server-client pair test harness (12.3.2) Apr 3, 2026
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos leynos marked this pull request as ready for review April 3, 2026 22:20
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The WireframePair::client_mut API panics after shutdown, which is surprising for a test harness; consider returning a Result or Option here so misuse is surfaced at compile time or via explicit error handling instead of a runtime panic.
  • The Drop implementation for WireframePair creates a new OS thread that sleeps for 5 seconds before aborting, which is quite heavy for a safety net; you might want to use an async timeout on the existing Tokio runtime (or a shorter delay) to avoid unbounded thread accumulation in large test suites.
  • The echo-app setup (Echo type, handler, and factory) is duplicated between tests/client_pair_harness.rs and tests/fixtures/client_pair_harness.rs; consider extracting this into a shared helper to keep the harness behaviour and routing logic in sync across tests.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `WireframePair::client_mut` API panics after shutdown, which is surprising for a test harness; consider returning a `Result` or `Option` here so misuse is surfaced at compile time or via explicit error handling instead of a runtime panic.
- The `Drop` implementation for `WireframePair` creates a new OS thread that sleeps for 5 seconds before aborting, which is quite heavy for a safety net; you might want to use an async timeout on the existing Tokio runtime (or a shorter delay) to avoid unbounded thread accumulation in large test suites.
- The echo-app setup (Echo type, handler, and factory) is duplicated between `tests/client_pair_harness.rs` and `tests/fixtures/client_pair_harness.rs`; consider extracting this into a shared helper to keep the harness behaviour and routing logic in sync across tests.

## Individual Comments

### Comment 1
<location path="wireframe_testing/src/client_pair.rs" line_range="66" />
<code_context>
+///
+/// [`WireframeServer`]: wireframe::server::WireframeServer
+/// [`WireframeClient`]: wireframe::client::WireframeClient
+pub struct WireframePair {
+    client: Option<WireframeClient<BincodeSerializer, RewindStream<tokio::net::TcpStream>, ()>>,
+    addr: SocketAddr,
</code_context>
<issue_to_address>
**issue (complexity):** Consider collapsing the per-field Options into a single Running state and simplifying shutdown/Drop so the lifecycle reads linearly and avoids extra threads or implicit invariants.

You can simplify the lifecycle quite a bit by collapsing the per-field `Option`s into a single “running” state, and by making `Drop` abort immediately after signalling shutdown instead of spawning a second thread.

### 1. Replace per-field `Option`s with a single `Running` state

This removes the need to juggle multiple `Option`s and the implicit invariants between them:

```rust
struct Running {
    client: WireframeClient<BincodeSerializer, RewindStream<tokio::net::TcpStream>, ()>,
    shutdown_tx: oneshot::Sender<()>,
    handle: JoinHandle<Result<(), wireframe::server::ServerError>>,
}

pub struct WireframePair {
    addr: SocketAddr,
    running: Option<Running>,
}
```

Construction becomes simpler:

```rust
Ok(WireframePair {
    addr,
    running: Some(Running {
        client,
        shutdown_tx,
        handle,
    }),
})
```

`client_mut` then enforces the invariant at the type level:

```rust
#[expect(
    clippy::expect_used,
    reason = "intentional panic for post-shutdown misuse"
)]
pub fn client_mut(
    &mut self,
) -> &mut WireframeClient<BincodeSerializer, RewindStream<tokio::net::TcpStream>, ()> {
    self.running
        .as_mut()
        .map(|r| &mut r.client)
        .expect("client_mut called after shutdown")
}
```

### 2. Simplify `shutdown` and error plumbing

With `Running` encapsulating state, `shutdown` reads linearly and separates join errors from server errors:

```rust
pub async fn shutdown(&mut self) -> TestResult<()> {
    if let Some(Running { client, shutdown_tx, handle }) = self.running.take() {
        drop(client); // ensure server sees disconnect

        let _ = shutdown_tx.send(());

        match handle.await {
            Err(join_err) => {
                return Err(TestError::Msg(format!("server task join error: {join_err}")));
            }
            Ok(Err(server_err)) => {
                return Err(TestError::Msg(format!("server error: {server_err}")));
            }
            Ok(Ok(())) => {}
        }
    }

    Ok(())
}
```

This avoids `??` and the stringly-typed error path is now explicit and easier to scan.

### 3. Make `Drop` minimal and avoid extra threads

You can still send a shutdown signal in `Drop` and ensure the task is not leaked, without spawning a dedicated OS thread per pair. For a test harness, an immediate abort after signalling is typically sufficient:

```rust
impl Drop for WireframePair {
    fn drop(&mut self) {
        if let Some(Running { shutdown_tx, handle, .. }) = self.running.take() {
            let _ = shutdown_tx.send(());
            handle.abort(); // immediate safety net to avoid leaked tasks
        }
    }
}
```

This keeps the “safety net” behavior (no orphaned tasks) but removes:
- the extra `std::thread::spawn` per pair,
- the 5s timing wrinkle in `Drop`,
- cross-runtime interaction between `std::thread` and `tokio::JoinHandle`.

If you still want a timeout-based graceful path, you could document that callers should use `shutdown()` for that, and keep `Drop` as the minimal safety net above.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread wireframe_testing/src/client_pair.rs
@coderabbitai coderabbitai Bot added the Roadmap label Apr 3, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e032ce5880

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread wireframe_testing/src/client_pair.rs
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/fixtures/client_pair_harness.rs`:
- Around line 20-21: Duplicate definition of the test payload type `Echo(u8)`
exists; extract the `Echo` struct into a shared test utilities module (e.g.,
tests::util or a new tests/common.rs) and replace the local `struct Echo(u8);`
in both `tests/fixtures/client_pair_harness.rs` and
`tests/client_pair_harness.rs` with a use/import of that shared definition;
ensure the shared module exposes the same traits/derives (bincode::Encode,
bincode::BorrowDecode, PartialEq, Debug) so existing tests compile unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9578c7db-7ddb-4c7e-a464-b6ba14d2a13f

📥 Commits

Reviewing files that changed from the base of the PR and between 2fc5c81 and e032ce5.

📒 Files selected for processing (16)
  • docs/execplans/12-3-2-in-process-server-and-client-pair-test-harness.md
  • docs/roadmap.md
  • docs/users-guide.md
  • docs/wireframe-client-design.md
  • docs/wireframe-testing-crate.md
  • tests/client_pair_harness.rs
  • tests/features/client_pair_harness.feature
  • tests/fixtures/client_pair_harness.rs
  • tests/fixtures/mod.rs
  • tests/scenarios/client_pair_harness_scenarios.rs
  • tests/scenarios/mod.rs
  • tests/steps/client_pair_harness_steps.rs
  • tests/steps/mod.rs
  • wireframe_testing/Cargo.toml
  • wireframe_testing/src/client_pair.rs
  • wireframe_testing/src/lib.rs

Comment thread tests/fixtures/client_pair_harness.rs Outdated
leynos and others added 2 commits April 4, 2026 08:18
Address three review concerns in the in-process server/client pair
test harness:

1. Collapse per-field Options into a single Running struct, simplifying
   shutdown logic and reducing implicit invariants between fields.

2. Replace the OS-thread-based Drop safety net (5-second sleep before
   abort) with an immediate handle.abort() after signalling shutdown.
   Callers wanting graceful teardown should call shutdown() explicitly.

3. Clean up the spawned server task when the client connection fails in
   spawn_wireframe_pair, preventing leaked tasks and bound listeners.

Additionally, extract the duplicated echo app setup into a shared
echo_app_factory and echo_handler in wireframe_testing, keeping the
routing logic in sync across the rstest integration tests and the
BDD fixture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the duplicated `Echo(u8)` struct from `tests/client_pair_harness.rs`
and `tests/fixtures/client_pair_harness.rs` into
`wireframe_testing::integration_helpers` alongside `CommonTestEnvelope`.
Both test files now import the shared definition, keeping the payload
type and its derives in sync across the rstest and BDD suites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/wireframe-client-design.md`:
- Around line 132-134: The doc currently implies spawn_wireframe_pair accepts an
optional client-builder parameter; update the wording to match the actual
signature by stating spawn_wireframe_pair takes a configuration closure (for
customizing the client) and add a note directing callers who want the
no-customization path to use spawn_wireframe_pair_default; reference
spawn_wireframe_pair and spawn_wireframe_pair_default and keep the surrounding
mention of unused_listener unchanged.

In `@wireframe_testing/src/client_pair.rs`:
- Around line 96-107: Change client_mut to return a TestResult<&mut
WireframeClient<BincodeSerializer, RewindStream<tokio::net::TcpStream>, ()>>
instead of panicking: remove the #[expect(...)] suppression and the
.expect("client_mut called after shutdown") call, and replace it with an ok_or /
match that returns an Err(TestResult::Err(...) or the crate's TestError via
TestResult) when self.running is None. Keep the rest of the method body
returning the mutable client reference on Ok, and update any callers to
propagate the TestResult instead of assuming a panic path (follow the pattern
used in WireframePair::shutdown()).

In `@wireframe_testing/src/integration_helpers.rs`:
- Around line 249-261: The helper echo_app_factory currently panics via
.expect("failed to build echo app"); change it to return a fallible result
instead: remove the clippy::expect_used suppression, make echo_app_factory
produce an impl Fn() -> Result<CommonTestApp, TestError> (or the existing
TestResult type) and propagate errors using ? on CommonTestApp::new() and
app.route(...) so the closure returns Err on failure rather than calling
.expect; update callers to handle the Result accordingly so panics are
eliminated from this shared fixture.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6b39c032-6036-4799-85c6-8fab7dfa8599

📥 Commits

Reviewing files that changed from the base of the PR and between e032ce5 and dc474c4.

📒 Files selected for processing (9)
  • docs/execplans/12-3-2-in-process-server-and-client-pair-test-harness.md
  • docs/users-guide.md
  • docs/wireframe-client-design.md
  • docs/wireframe-testing-crate.md
  • tests/client_pair_harness.rs
  • tests/fixtures/client_pair_harness.rs
  • wireframe_testing/src/client_pair.rs
  • wireframe_testing/src/integration_helpers.rs
  • wireframe_testing/src/lib.rs

Comment thread docs/wireframe-client-design.md
Comment thread wireframe_testing/src/client_pair.rs Outdated
Comment thread wireframe_testing/src/integration_helpers.rs Outdated
Replace the two remaining panic paths in the pair harness with
fallible returns:

- client_mut() now returns TestResult<&mut WireframeClient<...>>
  instead of panicking when called after shutdown. Callers propagate
  the error with ?.

- echo_app_factory() returns Fn() -> TestResult<CommonTestApp>
  instead of a closure that calls .expect(). The AppFactory blanket
  impl already accepts Fn() -> Result<App, Err> so callers need no
  changes.

Also fix the wireframe-client-design.md wording that incorrectly
described the configure_client parameter as optional; callers wanting
no customization should use spawn_wireframe_pair_default.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/client_pair_harness.rs`:
- Around line 56-74: The test custom_max_frame_length_via_pair_harness is
ineffective because it uses Echo(99) which fits the default frame limit; change
it to send a payload that exceeds the default client frame limit so the
builder.max_frame_length(2048) override is required to pass: in the
custom_max_frame_length_via_pair_harness function (and where Echo is
constructed/sized) create a larger payload whose serialized length is greater
than the default max frame length (or otherwise exercise a condition that would
be rejected by the default configuration), send that payload via
pair.client_mut()?.call(&request).await? and assert the response succeeds (or
assert the expected failure only when the override is missing) so the test
actually verifies spawn_wireframe_pair + builder.max_frame_length(2048) took
effect.
- Around line 81-97: The test explicit_shutdown_completes_cleanly should verify
the post-shutdown client_mut() contract: after calling pair.shutdown().await
assert that pair.client_mut() returns an Err (i.e. non-panicking error result)
to guard the new non-panicking path; update the test to stop after
pair.shutdown().await and add a short assertion checking pair.client_mut()
yields Err, referencing the explicit_shutdown_completes_cleanly test and the
pair.shutdown().await / pair.client_mut() calls.

In `@wireframe_testing/src/client_pair.rs`:
- Around line 121-131: In shutdown(), instead of immediately dropping the client
field from the Running struct, call the client's async close path
(WireframeClient::close().await) so any teardown hooks configured via
configure_client run, then drop the client and send shutdown_tx, and finally
await the server task handle; ensure you await and handle/propagate any error
returned by WireframeClient::close() before proceeding with shutdown of the
server task (running.take(), client, shutdown_tx, handle).
- Around line 150-160: In both the Drop implementation for WireframePair and the
abort_server function, stop immediately calling handle.abort() after sending
shutdown; instead send the shutdown signal via shutdown_tx.send(()), then await
the task's completion (join on the task handle returned from Running.handle)
with a bounded timeout, and only call handle.abort() if the join times out or
the task panics; update the teardown flow so run_with_shutdown() can run
tracker.close() and await tracker.wait().await (which tracker.spawn()’d
accept/connection tasks rely on) before force-aborting the handle.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 65e3c3ac-4b25-48be-ab5c-99d793312fb3

📥 Commits

Reviewing files that changed from the base of the PR and between dc474c4 and 3497436.

📒 Files selected for processing (8)
  • docs/execplans/12-3-2-in-process-server-and-client-pair-test-harness.md
  • docs/users-guide.md
  • docs/wireframe-client-design.md
  • docs/wireframe-testing-crate.md
  • tests/client_pair_harness.rs
  • tests/fixtures/client_pair_harness.rs
  • wireframe_testing/src/client_pair.rs
  • wireframe_testing/src/integration_helpers.rs

Comment thread tests/client_pair_harness.rs
Comment thread tests/client_pair_harness.rs
Comment thread wireframe_testing/src/client_pair.rs Outdated
Comment thread wireframe_testing/src/client_pair.rs
Improve the WireframePair harness shutdown behaviour and strengthen test
coverage in response to inline code review feedback.

Changes to WireframePair lifecycle:
- Call client.close().await in shutdown() before dropping the client so
  configured teardown hooks (on_disconnect) run during explicit shutdown
- Update Drop and abort_server to await task completion with a bounded
  timeout (100ms) before force-aborting; spawn timeout logic on a
  separate task to avoid blocking from within a runtime
- Use tokio::select! to race task join against timeout, aborting only if
  the task does not complete in time, allowing tracker.close() and
  tracker.wait() to finish

Test improvements:
- Refactor custom_max_frame_length_via_pair_harness to send a 5000-byte
  payload that actually exceeds the default 4096-byte frame limit,
  verifying that both server (via buffer_capacity) and client (via
  max_frame_length) configuration take effect
- Add assertion in explicit_shutdown_completes_cleanly that client_mut()
  returns Err after shutdown, guarding the new non-panicking contract

All tests, lint, and format checks pass.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wireframe_testing/src/client_pair.rs`:
- Around line 150-186: The current Drop for WireframePair calls
tokio::runtime::Handle::try_current() inside a newly spawned OS thread which
always returns Err; instead capture the runtime handle on the current thread
before spawning and move that Option<Handle> into the closure so the spawned
thread can use it. Concretely, call tokio::runtime::Handle::try_current() (or
Handle::current()/clone when appropriate) in the Drop block just after taking
self.running, store the result (Option<Handle>) and move it into the
std::thread::spawn closure; inside the closure check that Option and call
runtime.spawn(...) when Some(handle) and fallback to handle.abort() when None.
This preserves the 100ms grace period while keeping references to shutdown_tx
and handle consistent with the Running struct.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a2e88831-e27a-40b6-a5cd-0d301e050c17

📥 Commits

Reviewing files that changed from the base of the PR and between 3497436 and af16e46.

📒 Files selected for processing (2)
  • tests/client_pair_harness.rs
  • wireframe_testing/src/client_pair.rs

Comment thread wireframe_testing/src/client_pair.rs
Capture the tokio runtime handle on the current thread before spawning
OS threads in WireframePair::Drop and abort_server, since
Handle::try_current() always returns Err when called from within a
newly spawned OS thread that has no tokio context.

Changes:
- In Drop, call Handle::try_current().ok() before std::thread::spawn
  and move the Option<Handle> into the closure
- In abort_server, apply the same pattern for consistency even though
  it is called from async context where try_current() would work
- Update comments to clarify why we capture before spawning

This preserves the 100ms grace period for tracker.close() and
tracker.wait() while ensuring the runtime handle is available inside
the spawned thread.

All tests, lint, and format checks pass.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

The previous Drop implementation wrapped cleanup in std::thread::spawn,
which always failed to find a tokio runtime context (try_current()
returned Err) and immediately aborted the task, defeating the 100ms
grace period design.

Fix by calling tokio::runtime::Handle::try_current() directly on the
dropping thread (where the tokio context is available). When successful,
spawn the timeout task on that runtime; when Err, fall through to
immediate abort. This mirrors the abort_server implementation exactly.

Also update the WireframePair doc comment to reflect the bounded-wait
behaviour instead of stating it "immediately aborts".

All tests, lint, and format checks pass.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wireframe_testing/src/client_pair.rs`:
- Around line 183-215: Both the Drop implementation for the server pair and the
abort_server function duplicate the same bounded-timeout shutdown logic; extract
that logic into a single helper (e.g., spawn_bounded_shutdown or similar) that
accepts the JoinHandle<Result<(), wireframe::server::ServerError>> and a
Duration (or timeout in ms) and performs the current behavior of trying to get
the current tokio runtime handle, spawning an async task that awaits the handle
with a tokio::select! against sleep, and aborts the JoinHandle on timeout (or
immediately aborts if no runtime). Replace the duplicated blocks in abort_server
and the Drop impl to call this new helper so the timeout value and abort
behavior are centralized and consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5a8806b5-a018-4db2-97a7-b620ecbc58a0

📥 Commits

Reviewing files that changed from the base of the PR and between 99cee7e and 95cfcda.

📒 Files selected for processing (1)
  • wireframe_testing/src/client_pair.rs

Comment thread wireframe_testing/src/client_pair.rs Outdated
Both Drop for WireframePair and abort_server duplicated the same
bounded-timeout shutdown logic. Extract into spawn_bounded_shutdown
helper that centralizes the timeout value and abort behavior.

Changes:
- Add spawn_bounded_shutdown(handle, timeout) helper that:
  - Tries to get the current tokio runtime handle
  - Spawns an async task racing join against timeout via tokio::select!
  - Aborts only on timeout or immediately if no runtime
- Update Drop to call spawn_bounded_shutdown with 100ms timeout
- Update abort_server to call spawn_bounded_shutdown with 100ms timeout
- Remove duplicated timeout logic from both call sites

This ensures consistent shutdown behavior and makes the timeout value
easy to adjust in one place if needed.

All tests, lint, and format checks pass.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wireframe_testing/src/client_pair.rs`:
- Around line 121-147: The shutdown() currently does self.running.take() before
any await which drops Running if the future is cancelled; instead keep Running
in self until cleanup finishes: obtain a mutable reference to the Running inside
self (e.g., via self.running.as_mut() or similar) so you can call
running.client.close().await, send on running.shutdown_tx, await running.handle,
and only after all awaits succeed or errors are handled set self.running = None
(or call Option::take() at the final step); ensure you still return appropriate
TestError variants for join/ server errors and preserve the existing
shutdown_tx.send() and handle.await logic using the Running symbols (client,
shutdown_tx, handle).
- Around line 270-303: The spawn leaves shutdown_tx and handle unguarded so
panics or readiness failures leak the server task; wrap them in a RAII guard
(e.g., struct PendingServer(Option<(oneshot::Sender<()>, JoinHandle<Result<(),
wireframe::server::ServerError>>)>)) whose Drop calls abort_server(shutdown_tx,
handle), assign PendingServer(Some((shutdown_tx, handle))) after tokio::spawn
where server.ready_signal(ready_tx).run_with_shutdown(...) is spawned, replace
ready_rx.await.map_err(...) with a match that on Err takes the pending handle
and .awaits it to surface join or ServerError (returning a descriptive
TestError), on Ok take the pending pair out before calling
configure_client(WireframeClientBuilder::new()) so client.connect(addr).await
Err no longer calls abort_server directly (the Drop guard will handle abort on
unwinding).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0540786a-ec10-4e29-abc2-0fcce9ea43c0

📥 Commits

Reviewing files that changed from the base of the PR and between 95cfcda and 1b233aa.

📒 Files selected for processing (1)
  • wireframe_testing/src/client_pair.rs

Comment thread wireframe_testing/src/client_pair.rs
Comment thread wireframe_testing/src/client_pair.rs
Address two correctness issues in WireframePair lifecycle management:

1. shutdown() now keeps Running in self until all awaits complete
   - Changed Running fields to Option so they can be taken individually
   - Use running.as_mut() to borrow, take() fields one by one
   - Only set self.running = None after all awaits succeed/fail
   - If the shutdown future is cancelled mid-flight, Drop can still
     clean up the remaining Running state

2. spawn_wireframe_pair uses PendingServer RAII guard
   - Wrap (shutdown_tx, handle) in PendingServer guard immediately
     after tokio::spawn
   - Guard's Drop calls spawn_bounded_shutdown if not taken
   - Eliminates manual abort_server call in error path
   - Panics or early returns now automatically clean up via Drop
   - Take the pending pair out before client.connect so Drop doesn't
     fire on normal success path

Changes to Running struct:
- All fields now Option<T> instead of T
- Updated client_mut to use .and_then(|r| r.client.as_mut())
- Updated Drop to unwrap Options before cleanup
- Updated spawn_wireframe_pair to construct with Some(...) wrappers

All tests, lint, and format checks pass.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 5, 2026

@coderabbitai Please suggest a fix for this issue and supply a prompt for an AI coding agent to enable it to apply the fix. Include the file and symbol names indicated in the issue at the head of your response.

wireframe_testing/src/client_pair.rs

Comment on file

    pub async fn shutdown(&mut self) -> TestResult<()> {
        if let Some(running) = self.running.as_mut() {
            // Take the client and close it.
            if let Some(client) = running.client.take() {
                client.close().await;
            }

            // Send the shutdown signal.
            if let Some(shutdown_tx) = running.shutdown_tx.take() {
                let _ = shutdown_tx.send(());
            }

            // Await the server task.
            if let Some(handle) = running.handle.take() {
                match handle.await {
                    Err(join_err) => {
                        // Clean up Running before returning error.
                        self.running = None;
                        return Err(TestError::Msg(format!(
                            "server task join error: {join_err}"
                        )));
                    }
                    Ok(Err(server_err)) => {
                        // Clean up Running before returning error.
                        self.running = None;
                        return Err(TestError::Msg(format!("server error: {server_err}")));
                    }
                    Ok(Ok(())) => {
                        // Success - clean up Running.
                        self.running = None;
                    }
                }
            }
        }

        Ok(())
    }

❌ New issue: Bumpy Road Ahead
WireframePair.shutdown has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function

@coderabbitai

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
wireframe_testing/src/client_pair.rs (1)

254-256: ⚠️ Potential issue | 🟠 Major

Keep PendingServer armed until client construction succeeds.

Lines 254-256 promise that connect failures tear the server down before returning, but Line 326 disarms the RAII guard before configure_client and connect. Any error or panic after that point drops plain locals instead of going through the bounded cleanup path, and ready_rx.await.map_err(...) also hides the real start-up failure. Hold the guard until after builder.connect(addr).await succeeds, join the task on readiness failure to surface the actual server error, and replace the .expect(...) handoff with a propagated TestError. As per coding guidelines: ".expect() and .unwrap() are forbidden outside of tests. Errors must be propagated."

🔧 Suggested fix
-    ready_rx
-        .await
-        .map_err(|_| TestError::Msg("server did not signal ready".into()))?;
-
-    // Take the pending server out of the guard before connecting the client.
-    let (shutdown_tx, handle) = pending.take().expect("pending server already taken");
-
-    let builder = configure_client(WireframeClientBuilder::new());
-    let client = builder.connect(addr).await?;
+    match ready_rx.await {
+        Ok(()) => {}
+        Err(_) => {
+            let Some((_, handle)) = pending.take() else {
+                return Err(TestError::Msg("pending server state missing".into()));
+            };
+            return match handle.await {
+                Err(join_err) => Err(TestError::Msg(format!(
+                    "server task failed before readiness: {join_err}"
+                ))),
+                Ok(Err(server_err)) => Err(TestError::Msg(format!(
+                    "server failed before readiness: {server_err}"
+                ))),
+                Ok(Ok(())) => Err(TestError::Msg(
+                    "server exited before signalling readiness".into(),
+                )),
+            };
+        }
+    }
+
+    let builder = configure_client(WireframeClientBuilder::new());
+    let client = builder.connect(addr).await?;
+    let Some((shutdown_tx, handle)) = pending.take() else {
+        return Err(TestError::Msg("pending server state missing".into()));
+    };

Also applies to: 320-329

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wireframe_testing/src/client_pair.rs` around lines 254 - 256, The RAII guard
PendingServer is being disarmed too early: keep the PendingServer guard alive
until client construction completes successfully by moving the guard drop to
after builder.connect(addr).await succeeds; call configure_client and await
builder.connect while the guard is still held, and on ready_rx.await failure
join the server task to surface the real startup error instead of hiding
it—replace the .expect(...) handoff with returning/propagating a TestError so
errors are propagated rather than unwrapped. Ensure references to PendingServer,
configure_client, builder.connect, ready_rx, and TestError are updated
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wireframe_testing/src/client_pair.rs`:
- Around line 116-119: The shutdown() implementation currently takes
running.handle out of self before awaiting the join, which breaks the
cancellation-safe guarantee for Running/Drop; instead, keep the server handle in
self while awaiting by calling join via running.handle.as_mut().await (or create
a temporary guard that borrows the handle) and only clear/set running.handle =
None after the join completes; update the code paths in shutdown() (the block
around where running.handle is taken and the join is awaited, covering the
region corresponding to the existing 137-155 logic) to await through a mutable
borrow and clear the field afterward so Drop still observes the handle if
shutdown is cancelled.

---

Duplicate comments:
In `@wireframe_testing/src/client_pair.rs`:
- Around line 254-256: The RAII guard PendingServer is being disarmed too early:
keep the PendingServer guard alive until client construction completes
successfully by moving the guard drop to after builder.connect(addr).await
succeeds; call configure_client and await builder.connect while the guard is
still held, and on ready_rx.await failure join the server task to surface the
real startup error instead of hiding it—replace the .expect(...) handoff with
returning/propagating a TestError so errors are propagated rather than
unwrapped. Ensure references to PendingServer, configure_client,
builder.connect, ready_rx, and TestError are updated accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6ff28157-bef4-4600-aac5-9c058aba6f88

📥 Commits

Reviewing files that changed from the base of the PR and between 1b233aa and e1d519c.

📒 Files selected for processing (1)
  • wireframe_testing/src/client_pair.rs

Comment thread wireframe_testing/src/client_pair.rs
Restore cancellation safety to shutdown() and fix PendingServer guard
lifetime to prevent server task leaks.

Changes:
- shutdown(): use self.running.as_mut() instead of take() and await
  handle via as_mut() so Running stays in self until all awaits
  complete; only clear self.running after handle.await finishes
- spawn_wireframe_pair: keep PendingServer guard alive until after
  builder.connect succeeds so guard cleans up if connection fails

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
codescene-delta-analysis[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
wireframe_testing/src/client_pair.rs (1)

317-319: ⚠️ Potential issue | 🟠 Major

Propagate the server’s startup failure instead of collapsing it into a generic readiness error.

Join the spawned server task when ready_rx.await returns Err. The current branch drops pending, which tears the task down but discards the actual JoinError or ServerError, so a pre-readiness crash is reported only as “server did not signal ready”.

🩹 Proposed fix
-    ready_rx
-        .await
-        .map_err(|_| TestError::Msg("server did not signal ready".into()))?;
+    match ready_rx.await {
+        Ok(()) => {}
+        Err(_) => {
+            let Some((_, handle)) = pending.take() else {
+                return Err(TestError::Msg("pending server state missing".into()));
+            };
+            return match handle.await {
+                Err(join_err) => Err(TestError::Msg(format!(
+                    "server task failed before readiness: {join_err}"
+                ))),
+                Ok(Err(server_err)) => Err(TestError::Msg(format!(
+                    "server failed before readiness: {server_err}"
+                ))),
+                Ok(Ok(())) => Err(TestError::Msg(
+                    "server exited before signalling readiness".into(),
+                )),
+            };
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wireframe_testing/src/client_pair.rs` around lines 317 - 319, When
ready_rx.await returns Err, don't collapse the failure into TestError::Msg;
instead await the spawned server task (the join handle named pending) to
retrieve its JoinError or the inner ServerError and propagate that as the
TestError (or wrap with a descriptive message). Replace the current map_err
branch that drops pending with logic that .awaits pending (or .join().await) to
capture the actual failure and convert it into a TestError variant (preserving
error details) rather than always returning "server did not signal ready".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wireframe_testing/src/client_pair.rs`:
- Around line 325-326: The code currently calls pending.take().expect("pending
server already taken") which panics; replace this with proper error propagation
by making the surrounding function return a Result (or use the function's
existing Result) and map the None case to an appropriate error type/value
instead of panicking; locate the hand-off where pending.take() is used (the let
(shutdown_tx, handle) = pending.take().expect(...) line) and change it to
destructure via match or .ok_or_else(...) so you can return Err(...) when
pending is None, preserving the shutdown_tx and handle variable names for
downstream logic.

---

Duplicate comments:
In `@wireframe_testing/src/client_pair.rs`:
- Around line 317-319: When ready_rx.await returns Err, don't collapse the
failure into TestError::Msg; instead await the spawned server task (the join
handle named pending) to retrieve its JoinError or the inner ServerError and
propagate that as the TestError (or wrap with a descriptive message). Replace
the current map_err branch that drops pending with logic that .awaits pending
(or .join().await) to capture the actual failure and convert it into a TestError
variant (preserving error details) rather than always returning "server did not
signal ready".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 31d25bed-fd61-4a8b-a68f-9c03a3ac8f6d

📥 Commits

Reviewing files that changed from the base of the PR and between e1d519c and ea55242.

📒 Files selected for processing (1)
  • wireframe_testing/src/client_pair.rs

Comment thread wireframe_testing/src/client_pair.rs Outdated
Improve error handling in spawn_wireframe_pair by eliminating panic paths
and surfacing actual server startup errors to callers.

Changes:
- Replace pending.take().expect(...) with .ok_or_else(...) for proper
  error propagation when pending server is already taken
- When ready_rx.await fails, await the server task handle to capture the
  actual startup error (JoinError or ServerError) instead of returning
  generic "server did not signal ready" message
- Distinguish three failure modes: task panic/cancel, server error, and
  clean exit before ready signal

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wireframe_testing/src/client_pair.rs`:
- Around line 317-333: Update the American spelling "signaling" to British
"signalling" in this block: change the inline comment that reads "Server dropped
ready_tx without signaling - join the task to get the real error." to use
"signalling", update the error message returned when Ok(Ok(())) which currently
says "server exited before signaling ready" to "server exited before signalling
ready", and update any other string literal in this snippet (e.g., any logging
or TestError::Msg text) that uses "signaling" so all occurrences match
en-GB-oxendict spelling; keep the same punctuation and message semantics, only
adjust spelling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ba6c73b0-1610-47b0-93fe-d830b2def76b

📥 Commits

Reviewing files that changed from the base of the PR and between ea55242 and 1d192a0.

📒 Files selected for processing (1)
  • wireframe_testing/src/client_pair.rs

Comment thread wireframe_testing/src/client_pair.rs
@leynos leynos merged commit c2e4f0c into main Apr 5, 2026
6 checks passed
@leynos leynos deleted the feat-inprocess-server-client-test-ag7dz7 branch April 5, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant