Skip to content

Refactor message assembly test API; multiplexing tests#418

Merged
leynos merged 10 commits intomainfrom
terragon/feature/8-2-3-message-key-support-7qvxt8
Jan 16, 2026
Merged

Refactor message assembly test API; multiplexing tests#418
leynos merged 10 commits intomainfrom
terragon/feature/8-2-3-message-key-support-7qvxt8

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Jan 7, 2026

Summary

  • Refactor the message assembly test API and expand multiplexing groundwork, including new test scaffolding, public API re-exports for testing ergonomics, and comprehensive unit/integration tests.
  • Public API surface is re-exported at crate root for ergonomic usage; tests gain richer helpers and clearer separation between series tracking and state management.

Changes

  • Core data types and test scaffolding
    • New modules and public API surface for testing: src/message_assembler/error.rs, src/message_assembler/series.rs, src/message_assembler/state.rs, and src/message_assembler/types.rs.
    • src/message_assembler/header.rs gains FrameSequence::checked_increment() to support overflow handling in tests.
    • src/message_assembler/mod.rs updated to declare new submodules and re-export key types.
    • src/lib.rs re-exports: AssembledMessage, MessageAssemblyError, MessageAssemblyState, MessageSeries, MessageSeriesError, MessageSeriesStatus.
  • Public API surface for testing
    • Public exposure of: MessageSeries, MessageSeriesError, MessageSeriesStatus, MessageAssemblyState, and AssembledMessage for ergonomic test usage.
    • New helper types in src/message_assembler/types.rs: FirstFrameInput, FirstFrameInputError, and AssembledMessage to simplify test interactions.
  • Tests
    • Unit tests extended/added for MessageSeries and MessageAssemblyState in new files:
      • src/message_assembler/series_tests.rs
      • src/message_assembler/state_tests.rs
    • Existing tests wired to reference new test API surface and helpers.
    • Comprehensive cucumber scaffolding for message assembly multiplexing:
      • Features: tests/features/message_assembly.feature
      • World: tests/worlds/message_assembly.rs
      • Steps: tests/steps/message_assembly_steps.rs
    • Registration of new cucumber tests in runner and test worlds/steps.
  • Documentation
    • New exec plan doc: docs/execplans/8-2-3-message-key-support.md.
    • Updated user guide: docs/users-guide.md describing MessageAssemblyState, MessageSeries, and error handling.
    • Roadmap updates: docs/roadmap.md reflecting 8-2-3 and 8-2-4 progress.
  • Tests & Validation
    • Expanded unit tests to cover sequential, out-of-order, duplicates, interleaving, and overflow scenarios (new series_tests.rs and state_tests.rs).
    • Cucumber tests validate multiplexing behavior and continuity scenarios across interleaved streams.
  • Compatibility & Migration
    • Public API now includes MessageSeries, MessageAssemblyState, MessageSeriesError, MessageSeriesStatus, AssembledMessage, and MessageAssemblyError re-exported at crate root for ergonomic usage.
    • No changes to transport-layer fragmentation; this work operates at the protocol-level multiplexing layer.
  • Release Notes
    • Enables multiplexed, per-key frame assemblies with ordering, duplication, and overflow validation; adds lifecycle management for partial assemblies with timeout purging; public API surface re-exported and documented for ease of integration.

Testing & Validation

  • Unit tests cover:
    • series_accepts_sequential_continuation_frames
    • series_rejects_wrong_key
    • series_rejects_out_of_order_sequence
    • series_rejects_duplicate_sequence
    • series_rejects_after_completion
    • series_detects_sequence_overflow
    • state_tracks_multiple_interleaved_messages
    • state_rejects_continuation_without_first_frame
    • state_rejects_duplicate_first_frame
    • state_enforces_size_limit
    • state_purges_expired_assemblies
  • Cucumber tests validate multiplexing behavior and continuity scenarios across interleaved streams.
  • Gates: formatting, linting, and tests via make fmt, make lint, and cargo test.

Compatibility & Migration

  • Public API now includes: MessageSeries, MessageAssemblyState, MessageSeriesError, MessageSeriesStatus, AssembledMessage, and MessageAssemblyError; these are re-exported at the crate root for ergonomic usage.
  • Existing transport-layer fragmentation remains unchanged; this work operates at the protocol-level multiplexing layer.

Release Notes

  • Enables multiplexed, per-key frame assemblies with ordering, duplication, and overflow validation.
  • Adds lifecycle management for partial assemblies with timeout-based purging.
  • Public API surface is re-exported for ergonomic usage and documented for integration.

How to test locally

  • Run: make fmt, make lint, make test
  • Validate with unit tests and cucumber feature tests:
    • cargo test --workspace
    • cargo test --workspace --tests
    • cargo run -p wireframe -- test-helpers (if applicable)

If you’d like, I can walk through the gates locally or adjust docs/examples to align with preferred protocol semantics.

🌿 Generated by Terry


ℹ️ Tag @terragon-labs to ask questions and address PR feedback

📎 Task: https://www.terragonlabs.com/task/4c74a299-56aa-4d2f-af6b-5f00d6b95539

📎 Task: https://www.terragonlabs.com/task/ac7c17c6-37de-4f27-90df-ab49f5dfe935

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 7, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Summary by CodeRabbit

  • New Features

    • Multiplexed message assembly with continuity validation, size limits and timeout-based expiry.
    • Assembled-message APIs and explicit error/status reporting exposed for consumers.
  • Documentation

    • New user‑guide sections and executive plan detailing multi‑key assembly, continuity rules and acceptance criteria.
    • Roadmap updated to mark related items complete.
  • Tests

    • Extensive unit and BDD suites added covering sequencing, error cases, size limits and eviction.

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

Add keyed, multiplexed message assembly and continuity validation: new error/status enums, FrameSequence checked increment, MessageSeries for per-message sequencing, MessageAssemblyState for keyed multi-message assembly, types for first-frame/assembled messages, public re‑exports, documentation, unit tests and BDD feature/steps.

Changes

Cohort / File(s) Summary
Documentation & Roadmap
docs/execplans/8-2-3-message-key-support.md, docs/roadmap.md, docs/users-guide.md
Add exec plan and user‑guide sections describing message-key multiplexing and continuity validation; mark roadmap items 8.2.3/8.2.4 complete.
Top-level re-exports & module wiring
src/lib.rs, src/message_assembler/mod.rs
Re-export new public types; add submodules error, series, state, types; extend header re‑exports and add test helper module stub.
Error & Status types
src/message_assembler/error.rs
Introduce MessageSeriesStatus, MessageSeriesError (KeyMismatch, SequenceMismatch, SeriesComplete, SequenceOverflow, MissingFirstFrame, DuplicateFrame, MissingSequence, ContinuationBodyLengthMismatch) and MessageAssemblyError (Series, DuplicateFirstFrame, MessageTooLarge) with Display/Error impls and From conversion.
Header helper
src/message_assembler/header.rs
Add FrameSequence::checked_increment(self) -> Option<Self> for overflow-safe incrementing.
Per-message sequencing logic
src/message_assembler/series.rs
Implement MessageSeries: construct from first frame, accept continuations, validate/advance sequence, detect duplicates/out‑of‑order/overflow, expose completion and expected total.
Stateful, keyed assembly
src/message_assembler/state.rs
Implement MessageAssemblyState managing keyed partials: accept first/continuation frames (with size checks), timeouts/purge, assemble and emit AssembledMessage, expose public API (accept_*, purge_expired, buffered_count).
Types & validation
src/message_assembler/types.rs
Add FirstFrameInput with validation and FirstFrameInputError, and AssembledMessage with accessors and constructors.
Unit tests
src/message_assembler/series_tests.rs, src/message_assembler/state_tests.rs, src/message_assembler/tests.rs
Add comprehensive unit tests for MessageSeries and MessageAssemblyState; wire new test modules.
BDD tests & steps
tests/cucumber.rs, tests/features/message_assembly.feature, tests/steps/message_assembly_steps.rs, tests/steps/mod.rs
Add MessageAssemblyWorld, feature file for multiplexed scenarios, and step definitions for frame acceptance, time advancement, purging and assertions.
Test worlds & params
tests/worlds/mod.rs, tests/worlds/message_assembly.rs, tests/worlds/message_assembly_params.rs
Add MessageAssemblyWorld, PendingFirstFrame, FirstFrameParams, ContinuationFrameParams helpers and re-exports for BDD tests.
Test helpers
src/message_assembler/test_helpers.rs
Add factory helpers to construct first/continuation headers for tests.
Minor docs formatting
docs/execplans/9-1-2-introduce-a-codec-error-taxonomy.md
Small reflow/format changes in Progress section only.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant MessageAssemblyState
    participant MessageSeries
    participant ErrorHandler

    Client->>MessageAssemblyState: accept_first_frame(FirstFrameInput)
    activate MessageAssemblyState
    MessageAssemblyState->>MessageAssemblyState: validate header, metadata, body sizes
    alt Single-frame final
        MessageAssemblyState-->>Client: return AssembledMessage
    else Multi-frame start
        MessageAssemblyState->>MessageSeries: MessageSeries::from_first_frame(header)
        MessageAssemblyState->>MessageAssemblyState: store partial keyed by MessageKey
        MessageAssemblyState-->>Client: return None
    end
    deactivate MessageAssemblyState

    Client->>MessageAssemblyState: accept_continuation_frame(ContinuationFrameHeader, body)
    activate MessageAssemblyState
    MessageAssemblyState->>MessageSeries: lookup by MessageKey -> accept_continuation(header)
    activate MessageSeries
    alt Key mismatch
        MessageSeries-->>ErrorHandler: KeyMismatch
        MessageSeries-->>MessageAssemblyState: Err(MessageSeriesError)
    else Sequence issue (duplicate/out-of-order/overflow)
        MessageSeries-->>ErrorHandler: SequenceMismatch/DuplicateFrame/SequenceOverflow
        MessageSeries-->>MessageAssemblyState: Err(MessageSeriesError)
    else Valid continuation
        MessageSeries->>MessageSeries: validate_and_advance_sequence()
        MessageSeries-->>MessageAssemblyState: Ok(MessageSeriesStatus)
    end
    deactivate MessageSeries

    alt Continuation completed series
        MessageAssemblyState->>MessageAssemblyState: assemble final body
        MessageAssemblyState-->>Client: return AssembledMessage
    else Still incomplete
        MessageAssemblyState->>MessageAssemblyState: accumulate body
        MessageAssemblyState-->>Client: return None
    end
    deactivate MessageAssemblyState
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

Keys hum in queues of ordered light, 🔑
Frames entwine and find their right, ✨
Series guard each numbered thread,
State gathers pieces, stitched and fed,
Tests sing proof of paths well-led.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main focus: refactoring the test API and adding multiplexing test support via new modules, helpers, and comprehensive tests.
Description check ✅ Passed The description is comprehensive, clearly related to the changeset, covering core data types, public API surface, tests, documentation, and validation in detail.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch terragon/feature/8-2-3-message-key-support-7qvxt8


📜 Recent review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 192445e and c045350.

📒 Files selected for processing (1)
  • src/message_assembler/series_tests.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Run make check-fmt (cargo fmt --workspace -- --check) before committing to validate Rust formatting across the entire workspace without modifying files.
Run make lint (cargo clippy --workspace --all-targets --all-features -- -D warnings) before committing to lint every target with all features enabled and deny all Clippy warnings.
Run make test (cargo test --workspace) before committing to run the full workspace test suite.
Clippy warnings MUST be disallowed. Fix any warnings emitted during tests in the code itself rather than silencing them.
Use make fmt (cargo fmt --workspace) to apply formatting fixes reported by the formatter check.
Where a Rust function is too long, extract meaningfully named helper functions adhering to separation of concerns and CQRS.
Where a Rust function has too many parameters, group related parameters in meaningfully named structs.
Where a Rust function is returning a large error, consider using Arc to reduce the amount of data returned.
Write unit and behavioural tests for new Rust functionality. Run both before and after making any change.
Every Rust module must begin with a module level (//!) comment explaining the module's purpose and utility.
Document public Rust APIs using Rustdoc comments (///) so documentation can be generated with cargo doc.
Prefer immutable data and avoid unnecessary mut bindings in Rust code.
Avoid unsafe code in Rust unless absolutely necessary, and document any usage clearly with a 'SAFETY' comment.
Place function attributes after doc comments in Rust code.
Do not use return in single-line Rust functions.
Use predicate functions for Rust conditional criteria with more than two branches.
Lints must not be silenced in Rust except as a last resort. Lint rule suppressions must be tightly scoped and include a clear reason.
Use concat!() to combine long string literals in Rust rather than escaping newlines with a backslash.
Prefer single-line versions of Rust functions where...

Files:

  • src/message_assembler/series_tests.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • src/message_assembler/series_tests.rs
🧠 Learnings (3)
📚 Learning: 2026-01-03T17:44:28.792Z
Learnt from: leynos
Repo: leynos/wireframe PR: 412
File: tests/streaming_request.rs:85-107
Timestamp: 2026-01-03T17:44:28.792Z
Learning: In Rust code (e.g., the wireframe project), avoid direct indexing like vec[0] because it can panic. Prefer safe accessors that return Option, such as vec.get(0), vec.first(), or patterns that handle Option safely (e.g., match or if let Some). This should be applied across Rust files, including tests (e.g., tests/streaming_request.rs), regardless of prior length checks, to ensure safety and avoid panics from out-of-bounds access.

Applied to files:

  • src/message_assembler/series_tests.rs
📚 Learning: 2026-01-08T08:16:31.989Z
Learnt from: leynos
Repo: leynos/wireframe PR: 411
File: src/client/tests/lifecycle.rs:0-0
Timestamp: 2026-01-08T08:16:31.989Z
Learning: In Rust, apply #[expect(lint, reason = "...")] only to code that actually triggers the specified lint. If the code does not trigger the lint, Clippy will fail with 'unfulfilled lint expectation' errors, which is intentional to prevent stale suppressions. Do not add #[expect] attributes proactively or to code that doesn't trigger the lint, even if similar code elsewhere requires suppression. This guideline should be applied broadly to Rust source files across the repository.

Applied to files:

  • src/message_assembler/series_tests.rs
📚 Learning: 2026-01-08T20:33:24.570Z
Learnt from: leynos
Repo: leynos/wireframe PR: 417
File: src/codec/error_tests.rs:152-152
Timestamp: 2026-01-08T20:33:24.570Z
Learning: Guideline: In Rust, when using contains with a single-character pattern, prefer a char literal (e.g., 'a') over a string literal (e.g., "a") to satisfy the Clippy lint single_char_pattern, which flags single-character strings as less efficient. Apply this across Rust source files (all .rs) to improve efficiency and lint compliance. Example: use s.contains('a') instead of s.contains("a"); note that str::contains can take a char, making the char literal both correct and more efficient.

Applied to files:

  • src/message_assembler/series_tests.rs
🔍 Remote MCP Deepwiki

Summary of additional repository context relevant to this PR review

  • Project patterns & constraints

    • Prefer small focused functions, group many args into parameter objects; prefer .expect() over .unwrap(); use Result types rather than panics — relevant to recommendations in PR objectives (primitive-objects, arg grouping, unwrap->expect).
    • DoS protections expected: per-message max_message_size and reassembly timeouts are established patterns and required protections for reassembly logic.
  • Error & type patterns

    • Typed, thiserror-based error enums and From conversions are standard; exporting protocol/domain error enums is common practice in the codebase (aligns with introduced MessageSeriesError/MessageAssemblyError).
  • Sequence / overflow handling guidance

    • Sequence management and overflow behavior matter for fragmentation/reassembly; repository emphasizes strict validation and reassembly timeouts (so change to treat u32::MAX specially should be validated against established reassembly/overflow patterns).
  • Size validation guidance

    • Existing fragmentation/reassembly code enforces max_message_size and checks total lengths; reviewers should ensure metadata length and header-declared totals are included when validating message size (PR notes this).
  • Concurrency & multiplexing patterns

    • Multiplexed per-key state and concurrent maps (dashmap) are used for interleaved fragment handling; MessageAssemblyState design should align with existing concurrency patterns (non-blocking maps, timeout-based purging).
  • Testing & CI expectations

    • Tests use rstest for parameterised cases, cucumber for BDD; the repo expects unit tests + behavioral feature wiring (cucumber) to be present and passing — PR's added unit tests and cucumber wiring must be integrated correctly into test runner.
    • CI enforces zero Clippy warnings, formatting checks, and full test runs (make check-fmt, make lint, cargo test) — ensure new public exports and API changes have docs/format and lint-clean code.
  • Observability / production practices

    • Production readiness includes metrics and panic-isolation (connection tasks use catch_unwind and increment connection panic metric). New behavior that can cause rejected frames or panics should preserve panic isolation and update metrics where appropriate.

Sources

  • Repository documentation pages retrieved via Deepwiki: core architecture, message processing pipeline, testing, production guidance, roadmap and testing utilities
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Sourcery review
🔇 Additional comments (5)
src/message_assembler/series_tests.rs (5)

1-29: LGTM!

The module structure is well-organised: shared test helpers are imported with clear aliases, and local wrappers provide sensible defaults for body_len. The single-line function style at line 21 aligns with guidelines.


31-38: LGTM!

The SequenceOverflowExpectation enum clearly models the two possible outcomes for the parameterised overflow test. Documentation on each variant aids comprehension.


40-136: LGTM!

Thorough coverage of the happy path and error variants (KeyMismatch, SequenceMismatch, DuplicateFrame). The use of .expect() with descriptive messages will yield clear diagnostics on failure.


158-196: LGTM!

The rstest parameterisation elegantly captures both outcomes (overflow vs completion) for the u32::MAX edge case without duplication. Direct header construction at lines 172–177 is appropriate here for fine-grained control over the test parameters.


198-217: LGTM!

Good coverage of the expected_total accessor and the single-frame completion edge case. Direct header construction is appropriate for verifying that total_body_len propagates correctly.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Jan 7, 2026

Reviewer's Guide

Refactors the message assembly test and public API to support protocol-level multiplexing with new MessageSeries and MessageAssemblyState types, associated error handling, and comprehensive unit/Cucumber tests and docs for interleaved, validated message frame assembly.

Sequence diagram for multiplexed message assembly with MessageAssemblyState

sequenceDiagram
    actor Protocol
    participant MessageAssemblyState
    participant MessageSeries_key1 as MessageSeries_key1
    participant MessageSeries_key2 as MessageSeries_key2
    participant AssembledMessage

    Note over Protocol,MessageAssemblyState: Start first frame for message key 1
    Protocol->>MessageAssemblyState: accept_first_frame(FirstFrameInput{key=1, is_last=false})
    activate MessageAssemblyState
    MessageAssemblyState->>MessageSeries_key1: from_first_frame(FirstFrameHeader{key=1})
    MessageAssemblyState-->>Protocol: Ok(None)
    deactivate MessageAssemblyState

    Note over Protocol,MessageAssemblyState: Interleaved first frame for message key 2
    Protocol->>MessageAssemblyState: accept_first_frame(FirstFrameInput{key=2, is_last=false})
    activate MessageAssemblyState
    MessageAssemblyState->>MessageSeries_key2: from_first_frame(FirstFrameHeader{key=2})
    MessageAssemblyState-->>Protocol: Ok(None)
    deactivate MessageAssemblyState

    Note over Protocol,MessageAssemblyState: Continuation for key 1 completes message 1
    Protocol->>MessageAssemblyState: accept_continuation_frame(ContinuationFrameHeader{key=1, seq=1, is_last=true}, body)
    activate MessageAssemblyState
    MessageAssemblyState->>MessageSeries_key1: accept_continuation(header)
    MessageSeries_key1-->>MessageAssemblyState: Ok(Complete)
    MessageAssemblyState->>AssembledMessage: new(key=1, metadata, body)
    MessageAssemblyState-->>Protocol: Ok(Some(AssembledMessage))
    deactivate MessageAssemblyState

    Note over Protocol,MessageAssemblyState: Continuation for key 2 keeps assembly in progress
    Protocol->>MessageAssemblyState: accept_continuation_frame(ContinuationFrameHeader{key=2, seq=1, is_last=false}, body)
    activate MessageAssemblyState
    MessageAssemblyState->>MessageSeries_key2: accept_continuation(header)
    MessageSeries_key2-->>MessageAssemblyState: Ok(Incomplete)
    MessageAssemblyState-->>Protocol: Ok(None)
    deactivate MessageAssemblyState

    Note over Protocol,MessageAssemblyState: Optional cleanup of expired assemblies
    Protocol->>MessageAssemblyState: purge_expired()
    activate MessageAssemblyState
    MessageAssemblyState-->>Protocol: Vec<MessageKey> (evicted keys)
    deactivate MessageAssemblyState
Loading

Class diagram for MessageSeries and MessageAssemblyState multiplexing API

classDiagram
    class MessageKey {
        +u64 0
    }

    class FrameSequence {
        +u32 0
        +checked_increment() Option~FrameSequence~
    }

    class FirstFrameHeader {
        +MessageKey message_key
        +usize metadata_len
        +usize body_len
        +Option~usize~ total_body_len
        +bool is_last
    }

    class ContinuationFrameHeader {
        +MessageKey message_key
        +Option~FrameSequence~ sequence
        +usize body_len
        +bool is_last
    }

    class MessageSeries {
        -MessageKey message_key
        -Option~FrameSequence~ next_sequence
        -SequenceTracking sequence_tracking
        -bool complete
        -Option~usize~ expected_total
        +from_first_frame(header FirstFrameHeader) MessageSeries
        +message_key() MessageKey
        +is_complete() bool
        +expected_total() Option~usize~
        +accept_continuation(header ContinuationFrameHeader) Result~MessageSeriesStatus, MessageSeriesError~
        +force_next_sequence_for_tests(next FrameSequence) void
    }

    class SequenceTracking {
        <<enumeration>>
        Untracked
        Tracked
    }

    class MessageSeriesStatus {
        <<enumeration>>
        Incomplete
        Complete
    }

    class MessageSeriesError {
        <<enumeration>>
        KeyMismatch
        SequenceMismatch
        SeriesComplete
        SequenceOverflow
        MissingFirstFrame
        DuplicateFrame
    }

    class MessageAssemblyError {
        <<enumeration>>
        Series
        DuplicateFirstFrame
        MessageTooLarge
    }

    class FirstFrameInput~'a~ {
        +&'a FirstFrameHeader header
        +Vec~u8~ metadata
        +&'a [u8] body
        +new(header &'a FirstFrameHeader, metadata Vec~u8~, body &'a [u8]) FirstFrameInput~'a~
    }

    class AssembledMessage {
        -MessageKey message_key
        -Vec~u8~ metadata
        -Vec~u8~ body
        +new(message_key MessageKey, metadata Vec~u8~, body Vec~u8~) AssembledMessage
        +message_key() MessageKey
        +metadata() &[u8]
        +body() &[u8]
        +into_body() Vec~u8~
    }

    class PartialAssembly {
        <<internal>>
        -MessageSeries series
        -Vec~u8~ metadata
        -Vec~u8~ body_buffer
        -Instant started_at
        +new(series MessageSeries, started_at Instant) PartialAssembly
        +push_body(data &[u8]) void
        +set_metadata(data Vec~u8~) void
        +accumulated_len() usize
    }

    class MessageAssemblyState {
        -NonZeroUsize max_message_size
        -Duration timeout
        -HashMap~MessageKey, PartialAssembly~ assemblies
        +new(max_message_size NonZeroUsize, timeout Duration) MessageAssemblyState
        +accept_first_frame(input FirstFrameInput) Result~Option~AssembledMessage~, MessageAssemblyError~
        +accept_first_frame_at(input FirstFrameInput, now Instant) Result~Option~AssembledMessage~, MessageAssemblyError~
        +accept_continuation_frame(header ContinuationFrameHeader, body &[u8]) Result~Option~AssembledMessage~, MessageAssemblyError~
        +accept_continuation_frame_at(header ContinuationFrameHeader, body &[u8], now Instant) Result~Option~AssembledMessage~, MessageAssemblyError~
        +purge_expired() Vec~MessageKey~
        +purge_expired_at(now Instant) Vec~MessageKey~
        +buffered_count() usize
    }

    MessageSeries --> MessageKey
    MessageSeries --> FrameSequence
    MessageSeries --> SequenceTracking
    MessageSeries ..> MessageSeriesStatus
    MessageSeries ..> MessageSeriesError

    MessageSeriesStatus <.. MessageAssemblyState
    MessageSeriesError <.. MessageAssemblyError

    MessageAssemblyError --> MessageKey
    MessageAssemblyError --> NonZeroUsize

    FirstFrameInput --> FirstFrameHeader
    FirstFrameInput --> MessageKey

    AssembledMessage --> MessageKey

    PartialAssembly --> MessageSeries
    PartialAssembly --> AssembledMessage

    MessageAssemblyState --> PartialAssembly
    MessageAssemblyState --> MessageKey
    MessageAssemblyState --> MessageAssemblyError
    MessageAssemblyState --> MessageSeries
    MessageAssemblyState --> AssembledMessage

    FirstFrameHeader --> MessageKey
    ContinuationFrameHeader --> MessageKey
    ContinuationFrameHeader --> FrameSequence
Loading

File-Level Changes

Change Details Files
Introduce MessageSeries type and supporting error/status enums to track per-key frame ordering and completion, with optional sequence validation and overflow handling.
  • Add MessageSeriesStatus, MessageSeriesError, and MessageAssemblyError enums to represent series status and assembly failures using thiserror.
  • Implement MessageSeries in a new series module to manage per-message key state, including sequence tracking modes, completion flag, and expected total length.
  • Add FrameSequence::checked_increment helper to safely advance sequence numbers and detect overflow.
  • Expose MessageSeries and related types via the message_assembler module and crate root.
src/message_assembler/error.rs
src/message_assembler/series.rs
src/message_assembler/header.rs
src/message_assembler/mod.rs
src/lib.rs
Add MessageAssemblyState as a stateful multiplexing manager for concurrent keyed message assemblies with size and timeout limits, producing AssembledMessage outputs.
  • Implement FirstFrameInput helper struct to bundle first-frame header, metadata, and body.
  • Implement PartialAssembly internal struct to hold per-key MessageSeries, metadata, accumulated body, and start time.
  • Implement MessageAssemblyState with accept_first_frame(_at), accept_continuation_frame(_at), purge_expired(_at), and buffered_count, enforcing duplicate-key, missing-first-frame, and size-limit constraints.
  • Add AssembledMessage as the public representation of a completed message, with accessors and into_body, and re-export it from module and crate root.
src/message_assembler/state.rs
src/message_assembler/mod.rs
src/lib.rs
Extend unit tests to cover MessageSeries and MessageAssemblyState behavior for sequencing, errors, multiplexing, and limits.
  • Add series_tests module with helpers and tests for sequential acceptance, untracked sequences, wrong-key, out-of-order, duplicate, post-completion, overflow, expected_total, and single-frame completion behavior.
  • Add state_tests module verifying single and interleaved assemblies, missing/duplicate first frames, size limits on first and continuation frames, timeout-based purging, single-frame immediate completion, and AssembledMessage::into_body.
src/message_assembler/tests.rs
Add Cucumber test world, steps, and feature to exercise message assembly multiplexing and continuity validation end-to-end.
  • Introduce MessageAssemblyWorld with helpers to construct first/continuation frame params, manage a MessageAssemblyState instance under a simulated clock, track last result/errors, completed messages, and evicted keys.
  • Add step definitions mapping Given/When/Then clauses to MessageAssemblyWorld operations and assertions on bodies, buffered counts, and specific error variants.
  • Create message_assembly.feature scenarios for single and interleaved assemblies, out-of-order and duplicate frames, missing first frames, size-limit violations, and timeout eviction.
  • Register the new world and feature in the cucumber test runner, steps mod, and worlds mod so it runs as the thirteenth test suite.
tests/features/message_assembly.feature
tests/worlds/message_assembly.rs
tests/steps/message_assembly_steps.rs
tests/cucumber.rs
tests/worlds/mod.rs
tests/steps/mod.rs
tests/world.rs
Update user and project documentation to describe message key multiplexing, continuity validation, and roadmap status for message assembly.
  • Extend users-guide with sections and examples for MessageAssemblyState-based multiplexing and MessageSeries-based continuity validation, showing API usage and error behavior.
  • Add an exec plan document describing goals, constraints, plan-of-work, and interfaces for message key support and continuity validation (8.2.3/8.2.4).
  • Mark 8.2.3 and 8.2.4 as completed in the roadmap and mention the new MessageAssemblyWorld feature in cucumber runner docs.
docs/users-guide.md
docs/execplans/8-2-3-message-key-support.md
docs/roadmap.md
tests/cucumber.rs

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.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 8, 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:

Code Duplication

What lead to degradation?

src/message_assembler/tests.rs:
The module contains 3 functions with similar structure: series_tests.series_accepts_sequential_continuation_frames,series_tests.series_rejects_after_completion,series_tests.series_rejects_out_of_order_sequence

tests/steps/message_assembly_steps.rs:
The module contains 7 functions with similar structure: then_completes_with_body,then_error_duplicate_first_frame,then_error_duplicate_frame,then_error_message_too_large and 3 more functions

Why does this problem occur?

Duplicated code often leads to code that's harder to change since the same logical change has to be done in multiple functions. More duplication gives lower code health.

How to fix it?

A certain degree of duplicated code might be acceptable. The problems start when it is the same behavior that is duplicated across the functions in the module, ie. a violation of the Don't Repeat Yourself (DRY) principle. DRY violations lead to code that is changed together in predictable patterns, which is both expensive and risky. DRY violations can be identified using CodeScene's X-Ray analysis to detect clusters of change coupled functions with high code similarity. Read More
Once you have identified the similarities across functions, look to extract and encapsulate the concept that varies into its own function(s). These shared abstractions can then be re-used, which minimizes the amount of duplication and simplifies change.

@coderabbitai

This comment was marked as resolved.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 8, 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:

tests/steps/message_assembly_steps.rs

Comment on file

//! Step definitions for message assembly multiplexing and continuity validation.

❌ New issue: Primitive Obsession
In this module, 37.5% of all function arguments are primitive types, threshold = 30.0%

@coderabbitai

This comment was marked as resolved.

leynos and others added 2 commits January 8, 2026 20:48
…or message assembly

- Introduced MessageSeries type to track sequence ordering per message key
- Created MessageAssemblyState to manage concurrent assemblies keyed by MessageKey
- Added error types for key mismatch, sequence mismatch, duplicates, and overflow
- Supported optional sequence validation when protocol sequence numbers are present
- Implemented unit and cucumber BDD tests covering multiplexing, ordering, duplicates, size limits, and timeouts
- Updated message assembler module exports and lib.rs for new public API
- Documented new functionality in users-guide.md and marked roadmap items 8.2.3 and 8.2.4 as complete

Enables protocol-level multiplexed message assembly with strict continuity checks on interleaved frame streams, providing robust frame ordering, duplicate detection, and expiry handling for streamed requests.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
…rtions

Refactor tests in message_assembler and message_assembly_steps to reduce duplication by introducing helper functions for frame headers and assertion checks. Simplify continuation frame creation in message_assembler tests by using helper functions. Improve step assertion readability in message_assembly_steps by introducing a common assert_condition helper function, reducing boilerplate error handling.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
@leynos leynos force-pushed the terragon/feature/8-2-3-message-key-support-7qvxt8 branch from 278c62f to 6b49f17 Compare January 8, 2026 20:52
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos leynos changed the title Implement multiplexed message key support with MessageSeries and State Enable per-key multiplexing with MessageSeries and State Jan 8, 2026
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 9, 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:

Excess Number of Function Arguments

What lead to degradation?

tests/worlds/message_assembly.rs: MessageAssemblyWorld.add_first_frame
MessageAssemblyWorld.add_first_frame has 5 arguments, max arguments = 4

tests/worlds/message_assembly.rs: MessageAssemblyWorld.accept_continuation
MessageAssemblyWorld.accept_continuation has 5 arguments, max arguments = 4

Why does this problem occur?

Functions with many arguments indicate either a) low cohesion where the function has too many responsibilities, or b) a missing abstraction that encapsulates those arguments.
The threshold for the Rust language is 4 function arguments.

How to fix it?

Start by investigating the responsibilities of the function. Make sure it doesn't do too many things, in which case it should be split into smaller and more cohesive functions. Consider the refactoring INTRODUCE PARAMETER OBJECT to encapsulate arguments that refer to the same logical concept.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 9, 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:

Primitive Obsession

What lead to degradation?

tests/steps/message_assembly_steps.rs:
In this module, 37.5% of all function arguments are primitive types, threshold = 30.0%

tests/worlds/message_assembly.rs:
In this module, 40.0% of all function arguments are primitive types, threshold = 30.0%

Why does this problem occur?

Code that uses a high degree of built-in, primitives such as integers, strings, floats, lacks a domain language that encapsulates the validation and semantics of function arguments. Primitive Obsession has several consequences: 1) In a statically typed language, the compiler will detect less erroneous assignments. 2) Security impact since the possible value range of a variable/argument isn't retricted.
In this module, 38 % of all functions have primitive types as arguments.

How to fix it?

Primitive Obsession indicates a missing domain language. Introduce data types that encapsulate the details and constraints of your domain. For example, instead of int userId, consider User clicked.

Helpful refactoring examples

To get a general understanding of what this code health issue looks like - and how it might be addressed - we have prepared some diffs for illustrative purposes.

SAMPLE

# primitive-obsession.ts
-// Problem: It's hard to know what this function does, and what values are valid as parameters.
-function getPopularRepositories(String baseURL, String query, Integer pages, Integer pageSize, String sortorder): Json {
-	let pages == null ? 10 : pages
-	let pageSize == null ? 10 : pageSize
-  return httpClient.get(`${baseURL}?q=${query}&pages=${pages}&pageSize=${pageSize}&sortorder=${sortorder}`)
+// Refactoring: extract the pagination & API logic into a class, and it will
+// attract validation and other logic related to the specific query. It's now
+// easier to use and to maintain the getPopularRepositories function!
+function getPopularRepositories(query: PaginatedRepoQuery): Json {
+  return httpClient.get(query.getURL())
     .map(json => json.repositories)
     .filter(repository => repositry.stargazersCount > 1000)
 }

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 9, 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:

Excess Number of Function Arguments

src/message_assembler/state.rs: MessageAssemblyState.accept_first_frame_at

What lead to degradation?

MessageAssemblyState.accept_first_frame_at has 5 arguments, max arguments = 4

Why does this problem occur?

Functions with many arguments indicate either a) low cohesion where the function has too many responsibilities, or b) a missing abstraction that encapsulates those arguments.
The threshold for the Rust language is 4 function arguments.

How to fix it?

Start by investigating the responsibilities of the function. Make sure it doesn't do too many things, in which case it should be split into smaller and more cohesive functions. Consider the refactoring INTRODUCE PARAMETER OBJECT to encapsulate arguments that refer to the same logical concept.

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

Add FirstFrameInput parameter object to production code and
FirstFrameParams/ContinuationFrameParams to test code, reducing
function arguments and eliminating primitive obsession.

- Add FirstFrameInput<'a> struct that borrows header/body, owns metadata
- Refactor accept_first_frame and accept_first_frame_at to use it
- Remove #[expect(clippy::too_many_arguments)] from state.rs
- Add FirstFrameParams with builder methods (.with_metadata(), .final_frame())
- Add ContinuationFrameParams with builder methods (.with_sequence(), .final_frame())
- Push primitive-to-domain-type conversion to Cucumber step boundary
- Update all unit tests and step definitions to use new APIs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@leynos leynos changed the title Enable per-key multiplexing with MessageSeries and State Refactor message assembly test API Jan 11, 2026
codescene-delta-analysis[bot]

This comment was marked as outdated.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 11, 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:

tests/steps/message_assembly_steps.rs

Comment on file

//! Step definitions for message assembly multiplexing and continuity validation.

❌ New issue: Primitive Obsession
In this module, 39.7% of all function arguments are primitive types, threshold = 30.0%

@coderabbitai

This comment was marked as resolved.

@leynos leynos marked this pull request as ready for review January 11, 2026 20: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 8 issues, and left some high level feedback:

  • The exec plan in docs/execplans/8-2-3-message-key-support.md still has Status: IN PROGRESS and many unchecked items in Progress even though this PR appears to complete most of them; consider updating the status and checklist so the plan accurately reflects the implemented work.
  • In MessageAssemblyWorld, pending first frames are stored in a Vec and consumed with pop(), which makes acceptance LIFO; if future scenarios depend on FIFO semantics, it may be clearer and less error-prone to switch to a VecDeque and pop from the front instead.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The exec plan in `docs/execplans/8-2-3-message-key-support.md` still has `Status: IN PROGRESS` and many unchecked items in `Progress` even though this PR appears to complete most of them; consider updating the status and checklist so the plan accurately reflects the implemented work.
- In `MessageAssemblyWorld`, pending first frames are stored in a `Vec` and consumed with `pop()`, which makes acceptance LIFO; if future scenarios depend on FIFO semantics, it may be clearer and less error-prone to switch to a `VecDeque` and pop from the front instead.

## Individual Comments

### Comment 1
<location> `src/message_assembler/series.rs:195-204` </location>
<code_context>
+        Ok(MessageSeriesStatus::Incomplete)
+    }
+
+    fn validate_and_advance_sequence(
+        &mut self,
+        incoming: FrameSequence,
+    ) -> Result<(), MessageSeriesError> {
+        match self.sequence_tracking {
+            SequenceTracking::Untracked => {
+                // First continuation with a sequence number; start tracking
+                self.sequence_tracking = SequenceTracking::Tracked;
+                self.next_sequence = incoming.checked_increment();
+                if self.next_sequence.is_none() && !self.complete {
+                    return Err(MessageSeriesError::SequenceOverflow { last: incoming });
+                }
</code_context>

<issue_to_address>
**issue (bug_risk):** Sequence overflow handling incorrectly fails even for the final frame.

In `validate_and_advance_sequence`, `incoming.checked_increment()` overflow causes `SequenceOverflow` whenever `self.next_sequence` is `None` and `!self.complete`. But `self.complete` is only set after `accept_continuation` calls this helper, so a final frame with `sequence == FrameSequence(u32::MAX)` is incorrectly rejected as overflow.

To fix this, either:
- pass `header.is_last` into `validate_and_advance_sequence` and only treat overflow as an error when another frame is expected, or
- move the overflow check to `accept_continuation`, after deciding completion, and skip the error when the current frame is final.

This preserves `u32::MAX` as a valid final sequence while still detecting real overflow when more frames should follow.
</issue_to_address>

### Comment 2
<location> `src/message_assembler/state.rs:249` </location>
<code_context>
+        }
+
+        // Validate body size
+        if input.body.len() > self.max_message_size.get() {
+            return Err(MessageAssemblyError::MessageTooLarge {
+                key,
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Size checks on first frames ignore declared total length and metadata size.

`accept_first_frame_at` only checks `input.body.len()`. If `header.total_body_len` is present and already exceeds `max_message_size`, we should reject immediately instead of buffering any data. The guard also ignores `metadata.len()`, even though it contributes to the per-message memory footprint. Tightening the check to use `total_body_len` when available and to optionally include metadata in the accounting would make the limit more accurate and avoid starting assemblies that can never succeed.

Suggested implementation:

```rust
        // Validate message size (prefer declared total body length and include metadata)
        let declared_body_len = input
            .header
            .total_body_len
            .unwrap_or_else(|| input.body.len());

        let total_message_size = declared_body_len
            .saturating_add(input.metadata.len());

        if total_message_size > self.max_message_size.get() {
            return Err(MessageAssemblyError::MessageTooLarge {
                key,
                attempted: total_message_size,
                limit: self.max_message_size,
            });
        }

        let series = MessageSeries::from_first_frame(input.header);

```

Depending on the exact types in your codebase, you may need to adjust:

1. `header.total_body_len`:
   - If this is not `Option<usize>`, adapt the unwrap to match the type, e.g.:
     - `total_body_len.map(|n| n as usize).unwrap_or_else(...)` or
     - `total_body_len.get()` if it’s a `NonZeroUsize`.
2. `input.metadata.len()`:
   - If metadata is stored differently (e.g. `Option<Bytes>` or length in the header), replace `input.metadata.len()` with the appropriate expression (e.g. `input.metadata.as_ref().map_or(0, |m| m.len())` or `input.header.metadata_len`).
3. If `MessageTooLarge::attempted` is not `usize`, cast `total_message_size` to the expected type (e.g. `total_message_size as u64`).
</issue_to_address>

### Comment 3
<location> `tests/steps/message_assembly_steps.rs:103-108` </location>
<code_context>
+    world.accept_continuation(ContinuationFrameParams::new(to_key(key), body.into_bytes()))
+}
+
+#[when(expr = "another first frame for key {int} with body {string} arrives")]
+fn when_another_first_frame(world: &mut MessageAssemblyWorld, key: u64, body: String) {
+    world.add_first_frame(FirstFrameParams::new(to_key(key), body.into_bytes()));
+    // Ignore result, we check error in Then
+    let _ = world.accept_first_frame();
+}
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Avoid discarding the result in the "another first frame" step to keep assertions precise

This step currently calls `accept_first_frame()` and discards the `TestResult` with `let _ = ...`. Because `accept_first_frame` mutates `last_result`, Then steps that assert on errors still work, but any failure in `accept_first_frame` itself would not fail the step, and this step’s signature is inconsistent with the others that return `TestResult`. Consider returning and propagating the `TestResult` here (e.g. `fn when_another_first_frame(...) -> TestResult { world.add_first_frame(...); world.accept_first_frame() }`) so infrastructure errors surface and the cucumber steps stay consistent.

```suggestion
+#[when(expr = "another first frame for key {int} with body {string} arrives")]
+fn when_another_first_frame(
+    world: &mut MessageAssemblyWorld,
+    key: u64,
+    body: String,
+) -> TestResult {
+    world.add_first_frame(FirstFrameParams::new(to_key(key), body.into_bytes()));
+    world.accept_first_frame()
+}
```
</issue_to_address>

### Comment 4
<location> `src/message_assembler/state.rs:40` </location>
<code_context>
+/// let input = FirstFrameInput::new(&header, vec![0x01, 0x02], b"hello");
+/// assert_eq!(input.header.message_key, MessageKey(1));
+/// ```
+#[derive(Debug)]
+pub struct FirstFrameInput<'a> {
+    /// The frame header.
</code_context>

<issue_to_address>
**issue (review_instructions):** Add both unit tests and behavioural (Cucumber) tests that exercise MessageAssemblyState, MessageSeries, and FrameSequence::checked_increment; the new functionality is currently untested.

Add unit tests under `src/message_assembler/tests.rs` that cover the new message-key multiplexing and continuity behaviour: e.g. single-message assembly, interleaved assemblies for multiple `MessageKey`s, continuation without a first frame, duplicate first frames, size-limit enforcement, timeout purging, and sequence-order validation (including duplicates, gaps, and overflow via `force_next_sequence_for_tests`).

In addition, add the required behavioural coverage: implement scenarios in `tests/features/message_assembly.feature` along with corresponding worlds and step definitions in `tests/worlds/message_assembly.rs` and `tests/steps/message_assembly_steps.rs`. Ensure these are wired into the Cucumber runner so that the new protocol-level assembly behaviour is exercised end-to-end through the existing harness.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
- For any new feature or change to an existing feature, both behavioural *and* unit tests are required.
- Bug fixes *must* be demonstrated by a test.

</details>
</issue_to_address>

### Comment 5
<location> `src/message_assembler/state.rs:157` </location>
<code_context>
+/// };
+///
+/// let mut state =
+///     MessageAssemblyState::new(NonZeroUsize::new(1024).unwrap(), Duration::from_secs(30));
+///
+/// // Start assembly for key 1
</code_context>

<issue_to_address>
**suggestion (review_instructions):** The example uses `unwrap()` instead of the preferred `.expect()` for `NonZeroUsize::new(1024)`.

To follow the "Prefer `.expect()` over `.unwrap()`" guideline, please change this example to use `.expect()` with a brief message, e.g.:

```rust
let mut state = MessageAssemblyState::new(
    NonZeroUsize::new(1024).expect("non-zero message size"),
    Duration::from_secs(30),
);
```

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.rs`

**Instructions:**
Prefer `.expect()` over `.unwrap()`

</details>
</issue_to_address>

### Comment 6
<location> `src/message_assembler/state.rs:169` </location>
<code_context>
+/// };
+/// let result = state.accept_first_frame(FirstFrameInput::new(&first, vec![0x01, 0x02], b"hello"));
+/// assert!(result.is_ok());
+/// assert!(result.unwrap().is_none()); // Not yet complete
+///
+/// // Complete with continuation
</code_context>

<issue_to_address>
**suggestion (review_instructions):** The example asserts on `result.unwrap()` instead of using `.expect()` as required by the guidelines.

To comply with the "Prefer `.expect()` over `.unwrap()`" rule, consider rewriting this assertion to avoid a bare `unwrap`, for example:

```rust
let msg = result.expect("first frame processing should succeed");
assert!(msg.is_none()); // Not yet complete
```

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.rs`

**Instructions:**
Prefer `.expect()` over `.unwrap()`

</details>
</issue_to_address>

### Comment 7
<location> `docs/users-guide.md:458` </location>
<code_context>
+};
+
+let mut state = MessageAssemblyState::new(
+    NonZeroUsize::new(1_048_576).unwrap(), // 1 MiB max message
+    Duration::from_secs(30),               // 30s timeout for partial assemblies
+);
</code_context>

<issue_to_address>
**suggestion (review_instructions):** The acronym “MiB” is used without being defined on first use.

To follow the documentation guideline about defining uncommon acronyms, consider expanding this comment to something like `// 1 mebibyte (MiB) max message` so the meaning of MiB is clear at first use.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

### Comment 8
<location> `docs/execplans/8-2-3-message-key-support.md:139` </location>
<code_context>
+
+- Unit tests use rstest for parameterised cases
+  (`src/message_assembler/tests.rs`).
+- Cucumber BDD tests in `tests/features/message_assembler.feature`.
+- Test helper `TestAssembler` in `src/test_helpers.rs`.
+- World/steps in `tests/worlds/message_assembler.rs` and
</code_context>

<issue_to_address>
**suggestion (review_instructions):** The acronym “BDD” is introduced without being defined on first use.

To comply with the acronym rule, please expand this to something like “Cucumber Behaviour-Driven Development (BDD) tests” the first time BDD appears, and then use BDD afterwards.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</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 src/message_assembler/series.rs Outdated
Comment thread src/message_assembler/state.rs Outdated
Comment thread tests/steps/message_assembly_steps.rs
Comment thread src/message_assembler/state.rs Outdated
Comment thread src/message_assembler/state.rs Outdated
Comment thread src/message_assembler/state.rs Outdated
Comment thread docs/users-guide.md Outdated
Comment thread docs/execplans/8-2-3-message-key-support.md Outdated
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: 9763e4d45e

ℹ️ 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 src/message_assembler/series.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: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/message_assembler/tests.rs (1)

1-677: Decompose this file to comply with the 400-line limit.

This file contains 677 lines, exceeding the coding guideline maximum of 400 lines. Split the test modules into separate files:

  • Keep header parsing tests in this file
  • Move series_tests to tests/series_tests.rs
  • Move state_tests to tests/state_tests.rs

Update the module structure with:

#[cfg(test)]
mod header_tests;
#[cfg(test)]
mod series_tests;
#[cfg(test)]
mod state_tests;

Or use a subdirectory structure (tests/message_assembler/*.rs) if further organisation is needed.

As per coding guidelines, files exceeding 400 lines must be decomposed.

🤖 Fix all issues with AI agents
In @docs/users-guide.md:
- Around line 494-548: Add two brief examples to the Continuity validation
section showing how MessageSeries handles duplicate sequences and frames after
completion: create a MessageSeries from a FirstFrameHeader, then call
MessageSeries::accept_continuation with a ContinuationFrameHeader having a
previously-seen FrameSequence to demonstrate
Err(MessageSeriesError::DuplicateFrame { .. }), and call accept_continuation
after a continuation with is_last=true to demonstrate
Err(MessageSeriesError::SeriesComplete { .. }); reference the types
ContinuationFrameHeader, FrameSequence, MessageSeries, accept_continuation, and
the error variants MessageSeriesError::DuplicateFrame and
MessageSeriesError::SeriesComplete so readers can locate the examples.

In @src/message_assembler/error.rs:
- Around line 12-90: The MessageTooLarge error string is grammatically awkward
for limits not equal to one; update the #[error(...)] on
MessageAssemblyError::MessageTooLarge to use consistent pluralization (e.g.,
render the limit as "{limit} bytes" or rephrase to "exceeds size limit:
attempted {attempted} bytes > limit {limit} bytes") so the message reads
correctly for any NonZeroUsize limit; change the annotation for MessageTooLarge
(and keep the field names key, attempted, limit) accordingly.

In @src/message_assembler/series.rs:
- Around line 164-239: The overflow check currently uses self.complete which is
set only after handling the header, causing false SequenceOverflow on a final
continuation whose sequence is max; change validate_and_advance_sequence to take
a boolean is_last (or the header.is_last) and drive the "no further increment
required" check from that flag instead of self.complete: update the call in
accept_continuation to pass header.is_last, and inside
validate_and_advance_sequence replace checks of "!self.complete" with "!is_last"
in both the Untracked and Tracked branches so that an incoming sequence equal to
the overflow sentinel is allowed when is_last is true.

In @src/message_assembler/state.rs:
- Around line 21-60: Change FirstFrameInput::new to validate header.metadata_len
and header.body_len against the provided metadata.len() and body.len() and
return Result<Self, MessageAssemblyError> instead of unconditionally
constructing; if they differ return
MessageAssemblyError::FirstFrameLengthMismatch { expected_metadata:
header.metadata_len, actual_metadata: metadata.len(), expected_body:
header.body_len, actual_body: body.len() } (add that variant in
src/message_assembler/error.rs). Keep the function signature as pub fn
new(header: &'a FirstFrameHeader, metadata: Vec<u8>, body: &'a [u8]) ->
Result<Self, MessageAssemblyError> and only build and return Ok(Self { header,
metadata, body }) when lengths match. Apply the same validation pattern to the
analogous constructor/validator handling first-frame inputs in the 218-275
region.
- Around line 286-361: Ensure continuation frames are rejected when the header's
declared body_len doesn't match the actual payload: in
accept_continuation_frame_at, after obtaining the occupied Entry and before
accumulated size checks (and before push_body), compare header.body_len to
body.len(); if they differ, remove the entry and return a new
MessageAssemblyError::ContinuationBodyLenMismatch containing the key, expected
(header.body_len) and actual (body.len()). Add the
MessageAssemblyError::ContinuationBodyLenMismatch variant in
src/message_assembler/error.rs to support this return path.
- Around line 234-275: In accept_first_frame_at, check
input.header.total_body_len (if Some) and reject immediately when it exceeds
self.max_message_size by returning MessageAssemblyError::MessageTooLarge with
attempted set to that total, instead of only validating input.body.len();
implement this before creating MessageSeries or buffering (i.e., right after
computing key or before starting PartialAssembly) and keep the existing
body-length check as a fallback when total_body_len is None.
- Around line 234-275: The continuation-frame handler currently removes the
PartialAssembly unconditionally on any continuity error; update
accept_continuation_frame_at so that on Err(e) you do NOT call entry.remove()
for DuplicateFrame, SequenceMismatch, or SeriesComplete; specifically: if the
series error is DuplicateFrame, treat it as a harmless retransmit (do not remove
state and return Ok(None) or otherwise ignore the frame), and for
SequenceMismatch and SeriesComplete return Err(MessageAssemblyError::Series(e))
but keep the PartialAssembly in self.assemblies so future retransmits or
timeout-based cleanup can recover; retain the existing removal behavior only
when the continuation successfully completes and you assemble the final
AssembledMessage (or for truly fatal/unrelated errors if any).

In @tests/worlds/message_assembly.rs:
- Around line 1-387: The file exceeds the 400-line guideline because helper
structs FirstFrameParams and ContinuationFrameParams are defined inline; extract
these into a new helper module (e.g., message_assembly_helpers.rs or mod
helpers) and re-export or import them into the test module so
MessageAssemblyWorld stays focused on orchestration; move the impls and their
derives (Debug, methods new/with_metadata/final_frame and
new/with_sequence/final_frame) into the new module, update use/imports so the
test still references FirstFrameParams and ContinuationFrameParams, and ensure
no public API names (FirstFrameParams, ContinuationFrameParams) change to avoid
breaking references in MessageAssemblyWorld.
- Around line 220-239: accept_all_first_frames currently drains
pending_first_frames using pop(), which processes frames LIFO; change it to
drain in FIFO order so queued frames are handled in insertion order. Update
accept_all_first_frames to pull items from the front (e.g., if
pending_first_frames remains a Vec use while
!self.pending_first_frames.is_empty() { let pending =
self.pending_first_frames.remove(0); ... } or better change pending_first_frames
to a VecDeque and use pop_front()) while keeping the existing calls to
state.accept_first_frame_at, pushing to completed_messages, and setting
last_result.
- Around line 155-173: Change create_state to return a cucumber TestResult
instead of panicking: update the signature of create_state to ->
cucumber::TestResult, replace the current panic path (the
NonZeroUsize::new(None) branch) with an early return of a failing TestResult
containing "max_size must be non-zero", and keep the successful path to
initialize MessageAssemblyState, current_time, pending_first_frames,
last_result, completed_messages, and evicted_keys and then return a successful
TestResult; reference the create_state function, NonZeroUsize::new,
MessageAssemblyState::new, and cucumber::TestResult to implement the change.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 746bbd0 and 9763e4d.

📒 Files selected for processing (17)
  • docs/execplans/8-2-3-message-key-support.md
  • docs/roadmap.md
  • docs/users-guide.md
  • src/lib.rs
  • src/message_assembler/error.rs
  • src/message_assembler/header.rs
  • src/message_assembler/mod.rs
  • src/message_assembler/series.rs
  • src/message_assembler/state.rs
  • src/message_assembler/tests.rs
  • tests/cucumber.rs
  • tests/features/message_assembly.feature
  • tests/steps/message_assembly_steps.rs
  • tests/steps/mod.rs
  • tests/world.rs
  • tests/worlds/message_assembly.rs
  • tests/worlds/mod.rs
🧰 Additional context used
📓 Path-based instructions (5)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Run make check-fmt (cargo fmt --workspace -- --check) before committing to validate Rust formatting across the entire workspace without modifying files.
Run make lint (cargo clippy --workspace --all-targets --all-features -- -D warnings) before committing to lint every target with all features enabled and deny all Clippy warnings.
Run make test (cargo test --workspace) before committing to run the full workspace test suite.
Clippy warnings MUST be disallowed. Fix any warnings emitted during tests in the code itself rather than silencing them.
Use make fmt (cargo fmt --workspace) to apply formatting fixes reported by the formatter check.
Where a Rust function is too long, extract meaningfully named helper functions adhering to separation of concerns and CQRS.
Where a Rust function has too many parameters, group related parameters in meaningfully named structs.
Where a Rust function is returning a large error, consider using Arc to reduce the amount of data returned.
Write unit and behavioural tests for new Rust functionality. Run both before and after making any change.
Every Rust module must begin with a module level (//!) comment explaining the module's purpose and utility.
Document public Rust APIs using Rustdoc comments (///) so documentation can be generated with cargo doc.
Prefer immutable data and avoid unnecessary mut bindings in Rust code.
Avoid unsafe code in Rust unless absolutely necessary, and document any usage clearly with a 'SAFETY' comment.
Place function attributes after doc comments in Rust code.
Do not use return in single-line Rust functions.
Use predicate functions for Rust conditional criteria with more than two branches.
Lints must not be silenced in Rust except as a last resort. Lint rule suppressions must be tightly scoped and include a clear reason.
Use concat!() to combine long string literals in Rust rather than escaping newlines with a backslash.
Prefer single-line versions of Rust functions where...

Files:

  • tests/steps/mod.rs
  • tests/worlds/mod.rs
  • src/message_assembler/header.rs
  • tests/world.rs
  • tests/steps/message_assembly_steps.rs
  • tests/cucumber.rs
  • src/message_assembler/error.rs
  • src/message_assembler/mod.rs
  • src/message_assembler/tests.rs
  • src/message_assembler/series.rs
  • src/lib.rs
  • src/message_assembler/state.rs
  • tests/worlds/message_assembly.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • tests/steps/mod.rs
  • tests/worlds/mod.rs
  • src/message_assembler/header.rs
  • tests/world.rs
  • tests/steps/message_assembly_steps.rs
  • tests/cucumber.rs
  • src/message_assembler/error.rs
  • src/message_assembler/mod.rs
  • src/message_assembler/tests.rs
  • src/message_assembler/series.rs
  • src/lib.rs
  • src/message_assembler/state.rs
  • tests/worlds/message_assembly.rs
docs/**/*.md

📄 CodeRabbit inference engine (docs/contents.md)

Follow the documentation style guide conventions when writing project documentation

Write official documentation with separate guides for 'Duplex Messaging & Pushes', 'Streaming Responses', and 'Message Fragmentation', each including runnable examples and explaining relevant concepts and APIs

docs/**/*.md: Use markdown files within the docs/ directory as a knowledge base and source of truth for project requirements, dependency choices, and architectural decisions.
When new decisions are made, requirements change, libraries are added/removed, or architectural patterns evolve, proactively update the relevant file(s) in the docs/ directory to reflect the latest state. Ensure documentation remains accurate and current.
Documentation must use en-GB-oxendict spelling and grammar ('-ize' / '-yse' / '-our'), with the exception of the filename LICENSE.
Reference the documentation style guide provided at docs/documentation-style-guide.md for documentation writing standards.

Documentation describing multi-packet streaming MUST reflect the tuple-based API and explain helper constructors that prepare the channel pair

Replace or retire documentation that refers to Cucumber, including guides and examples in docs/

docs/**/*.md: Use British English based on the Oxford English Dictionary (en-GB-oxendict), including: suffix -ize in words like 'realize' and 'organization', suffix -lyse in words like 'analyse' and 'paralyse', suffix -our in words like 'colour' and 'behaviour', suffix -re in words like 'calibre' and 'centre', double 'l' in words like 'cancelled' and 'counsellor', maintain the 'e' in words like 'likeable' and 'liveable', suffix -ogue in words like 'analogue' and 'catalogue'
The word 'outwith' is acceptable in documentation
Keep US spelling when used in an API, for example 'color'
Use the Oxford comma: 'ships, planes, and hovercraft' where it aids comprehension
Treat company names as collective nouns, for example: 'Lille Industries are expanding'
Write hea...

Files:

  • docs/users-guide.md
  • docs/execplans/8-2-3-message-key-support.md
  • docs/roadmap.md
docs/**/*.{rs,md}

📄 CodeRabbit inference engine (docs/rust-doctest-dry-guide.md)

Every doctest should validate the public API of a crate from the perspective of an external user, treating each documentation test as a separate temporary crate that imports the library as an external dependency

Files:

  • docs/users-guide.md
  • docs/execplans/8-2-3-message-key-support.md
  • docs/roadmap.md
**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

**/*.md: Validate Markdown files using make markdownlint.
Run make fmt after any documentation changes to format all Markdown files and fix table markup.
Validate Mermaid diagrams in Markdown files by running make nixie.
Markdown paragraphs and bullet points must be wrapped at 80 columns.
Code blocks in Markdown must be wrapped at 120 columns.
Markdown tables and headings must not be wrapped.
Use dashes (-) for list bullets in Markdown.
Use GitHub-flavoured Markdown footnotes ([^1]) for references and footnotes in Markdown.

Files:

  • docs/users-guide.md
  • docs/execplans/8-2-3-message-key-support.md
  • docs/roadmap.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/users-guide.md
  • docs/execplans/8-2-3-message-key-support.md
  • docs/roadmap.md
docs/**/roadmap*.md

📄 CodeRabbit inference engine (docs/documentation-style-guide.md)

docs/**/roadmap*.md: Roadmap task writing: Define outcomes, not intentions. Phrase tasks in terms of the capability delivered (e.g. 'Implement role-based access control for API endpoints'), not aspirations
Roadmap task writing: Quantify completion criteria with measurable finish lines (e.g. '90% test coverage for new modules', 'response times under 200ms')
Roadmap task writing: Break into atomic increments. Ensure tasks can be completed in weeks, not quarters. Large goals should be decomposed into clear, deliverable units
Roadmap task writing: Tie to dependencies and sequencing. Document prerequisites so tasks can be scheduled realistically
Roadmap task writing: Bound scope explicitly. Note both in-scope and out-of-scope elements (e.g. 'Build analytics dashboard (excluding churn prediction)')
Roadmap formatting: Use dotted numbering for phases (1, 2, 3, …), steps (1.1, 1.2, 1.3, …), and headline tasks (1.1.1, 1.1.2, 1.1.3, …)
Roadmap formatting: Precede task and sub-task items with a GitHub-Flavoured Markdown (GFM) checkbox ([]) to track completion status
Roadmap formatting: Note non-linear dependencies explicitly using dotted notation (e.g. 'Requires 2.3.1')
Roadmap formatting: Include explicit success criteria only where not immediately obvious from the task description
Roadmap formatting: Where applicable, cite the relevant design document section for each task (e.g. 'See design-doc.md §3.2')

Files:

  • docs/roadmap.md
🧠 Learnings (4)
📚 Learning: 2026-01-03T17:44:28.792Z
Learnt from: leynos
Repo: leynos/wireframe PR: 412
File: tests/streaming_request.rs:85-107
Timestamp: 2026-01-03T17:44:28.792Z
Learning: In Rust code (e.g., the wireframe project), avoid direct indexing like vec[0] because it can panic. Prefer safe accessors that return Option, such as vec.get(0), vec.first(), or patterns that handle Option safely (e.g., match or if let Some). This should be applied across Rust files, including tests (e.g., tests/streaming_request.rs), regardless of prior length checks, to ensure safety and avoid panics from out-of-bounds access.

Applied to files:

  • tests/steps/mod.rs
  • tests/worlds/mod.rs
  • src/message_assembler/header.rs
  • tests/world.rs
  • tests/steps/message_assembly_steps.rs
  • tests/cucumber.rs
  • src/message_assembler/error.rs
  • src/message_assembler/mod.rs
  • src/message_assembler/tests.rs
  • src/message_assembler/series.rs
  • src/lib.rs
  • src/message_assembler/state.rs
  • tests/worlds/message_assembly.rs
📚 Learning: 2026-01-08T08:16:31.989Z
Learnt from: leynos
Repo: leynos/wireframe PR: 411
File: src/client/tests/lifecycle.rs:0-0
Timestamp: 2026-01-08T08:16:31.989Z
Learning: In Rust, apply #[expect(lint, reason = "...")] only to code that actually triggers the specified lint. If the code does not trigger the lint, Clippy will fail with 'unfulfilled lint expectation' errors, which is intentional to prevent stale suppressions. Do not add #[expect] attributes proactively or to code that doesn't trigger the lint, even if similar code elsewhere requires suppression. This guideline should be applied broadly to Rust source files across the repository.

Applied to files:

  • tests/steps/mod.rs
  • tests/worlds/mod.rs
  • src/message_assembler/header.rs
  • tests/world.rs
  • tests/steps/message_assembly_steps.rs
  • tests/cucumber.rs
  • src/message_assembler/error.rs
  • src/message_assembler/mod.rs
  • src/message_assembler/tests.rs
  • src/message_assembler/series.rs
  • src/lib.rs
  • src/message_assembler/state.rs
  • tests/worlds/message_assembly.rs
📚 Learning: 2026-01-08T20:33:24.570Z
Learnt from: leynos
Repo: leynos/wireframe PR: 417
File: src/codec/error_tests.rs:152-152
Timestamp: 2026-01-08T20:33:24.570Z
Learning: Guideline: In Rust, when using contains with a single-character pattern, prefer a char literal (e.g., 'a') over a string literal (e.g., "a") to satisfy the Clippy lint single_char_pattern, which flags single-character strings as less efficient. Apply this across Rust source files (all .rs) to improve efficiency and lint compliance. Example: use s.contains('a') instead of s.contains("a"); note that str::contains can take a char, making the char literal both correct and more efficient.

Applied to files:

  • tests/steps/mod.rs
  • tests/worlds/mod.rs
  • src/message_assembler/header.rs
  • tests/world.rs
  • tests/steps/message_assembly_steps.rs
  • tests/cucumber.rs
  • src/message_assembler/error.rs
  • src/message_assembler/mod.rs
  • src/message_assembler/tests.rs
  • src/message_assembler/series.rs
  • src/lib.rs
  • src/message_assembler/state.rs
  • tests/worlds/message_assembly.rs
📚 Learning: 2026-01-08T18:03:59.395Z
Learnt from: leynos
Repo: leynos/wireframe PR: 417
File: tests/steps/codec_error_steps.rs:58-62
Timestamp: 2026-01-08T18:03:59.395Z
Learning: In the wireframe repo, test step functions that always succeed should not return TestResult, because clippy::unnecessary_wraps will fire and cannot be suppressed in tests. Do not use #[allow(...)] or #[expect(clippy::unnecessary_wraps)] to silence it. Refactor such steps to return () or a type that does not trigger the lint, or move these steps to non-test code or a helper that isn’t linted the same way. This guidance applies to all files under tests/steps that contain similar patterns.

Applied to files:

  • tests/steps/mod.rs
  • tests/steps/message_assembly_steps.rs
🧬 Code graph analysis (4)
src/message_assembler/error.rs (1)
src/message_assembler/header.rs (4)
  • from (13-13)
  • from (17-17)
  • from (29-29)
  • from (33-33)
src/message_assembler/tests.rs (2)
src/message_assembler/series.rs (5)
  • from_first_frame (96-104)
  • is_complete (112-112)
  • accept_continuation (164-193)
  • message_key (108-108)
  • expected_total (116-116)
src/message_assembler/header.rs (1)
  • new (127-127)
src/message_assembler/series.rs (4)
src/message_assembler/header.rs (1)
  • header (152-152)
src/message_assembler/state.rs (1)
  • message_key (122-122)
tests/worlds/message_assembly.rs (1)
  • accept_continuation (250-269)
tests/worlds/correlation.rs (1)
  • expected (45-45)
tests/worlds/message_assembly.rs (3)
src/message_assembler/header.rs (4)
  • fmt (21-21)
  • fmt (37-37)
  • new (127-127)
  • header (152-152)
src/message_assembler/state.rs (7)
  • new (53-59)
  • new (72-79)
  • new (112-118)
  • new (198-204)
  • body (130-130)
  • metadata (126-126)
  • accept_first_frame (218-223)
src/message_assembler/series.rs (1)
  • accept_continuation (164-193)
🪛 LanguageTool
docs/users-guide.md

[uncategorized] ~444-~444: Possible missing comma found.
Context: ...ageKey`. This enables interleaved frame streams where frames from different logical mes...

(AI_HYDRA_LEO_MISSING_COMMA)

docs/execplans/8-2-3-message-key-support.md

[uncategorized] ~13-~13: Use a comma before ‘so’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...mbler` module needs multiplexing support so protocol implementations can process in...

(COMMA_COMPOUND_SENTENCE_2)


[style] ~91-~91: Would you like to use the Oxford spelling “recognize”? The spelling ‘recognise’ is also correct.
Context: ...with transport-level fragmentation will recognise the analogous protocol-level structur...

(OXFORD_SPELLING_Z_NOT_S)


[style] ~137-~137: Would you like to use the Oxford spelling “parameterized”? The spelling ‘parameterised’ is also correct.
Context: ... patterns: - Unit tests use rstest for parameterised cases (`src/message_assembler/tests.r...

(OXFORD_SPELLING_Z_NOT_S)


[style] ~216-~216: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...mbly.rswithMessageAssemblyWorld. - Create tests/steps/message_assembly_steps.rs`...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[uncategorized] ~295-~295: Loose punctuation mark.
Context: ...leaved assembly pass. - make check-fmt, make lint, and make test exit with ...

(UNLIKELY_OPENING_PUNCTUATION)

🔍 Remote MCP

The repository structure is not available through Deepwiki at this moment. However, based on the detailed AI-generated summaries and PR objectives provided, I can synthesize the relevant review context:

Summary of Additional Review Context

Core Feature Architecture

New Modules Introduced:

  • src/message_assembler/error.rs: Defines error types (MessageSeriesError, MessageAssemblyError, MessageSeriesStatus) for protocol-level validation failures including key mismatches, sequence violations, duplicates, and size constraints
  • src/message_assembler/series.rs: Per-message ordering tracker (MessageSeries) that handles optional sequence validation with two modes: Untracked (starts on first sequence) and Tracked (strict ordering enforcement)
  • src/message_assembler/state.rs: Multiplexing state manager (MessageAssemblyState) that manages concurrent in-flight assemblies keyed by MessageKey, enforces size limits, handles timeouts, and returns complete AssembledMessage instances

Key Design Aspects:

  • Optional sequence tracking (skipped if sequence numbers absent) enables backward compatibility with unsequenced frames
  • Per-key isolation prevents interleaved messages from interfering with one another
  • Timeout-based purging removes stale assemblies; size enforcement prevents resource exhaustion

Test Coverage

Unit Tests (src/message_assembler/tests.rs):

  • 488 new lines covering: sequential/non-sequential continuations, key mismatches, out-of-order sequences, duplicates, post-completion behavior, overflow detection, and single-frame completion
  • Test helpers for frame header construction and payload building

Cucumber/BDD Tests (tests/features/message_assembly.feature):

  • 78 lines of scenario-driven tests with 8 scenarios covering multiplexing, continuity, and error conditions
  • Test world (MessageAssemblyWorld) manages state transitions and error inspection

Public API Surface Changes

New exports at crate root (src/lib.rs): AssembledMessage, FirstFrameInput, MessageAssemblyState, MessageAssemblyError, MessageSeries, MessageSeriesError, MessageSeriesStatus

Related production type (FirstFrameInput): Groups header, metadata, and body as a cohesive parameter object—addressing the PR review feedback on argument count reduction

Code Review Findings

Duplication in Tests:

  • src/message_assembler/tests.rs: Repeated FirstFrameHeader/ContinuationFrameHeader construction across series tests
  • tests/steps/message_assembly_steps.rs: Repeated assertion patterns in Then steps
  • Mitigation in PR: Commit 2 introduces test helpers and assert_condition to reduce boilerplate

Primitive Obsession in Test Code:

  • tests/steps/message_assembly_steps.rs and tests/worlds/message_assembly.rs use primitives (usize, u64) instead of domain types (MessageKey, FrameSequence)
  • Mitigation in PR: Commit 3 adds conversion helpers (to_key, to_seq) at step boundary and introduces FirstFrameParams, ContinuationFrameParams parameter objects

Function Argument Count:

  • MessageAssemblyState::accept_first_frame_at: 5 parameters
  • Test world methods originally exceeded threshold
  • Mitigation: FirstFrameInput<'a> in production (commit 3) and parameter objects in tests reduce counts and improve cohesion

[::Source: AI-generated summaries from pull request context::]

  • Core feature definitions and error type hierarchies: src/message_assembler/{error,series,state}.rs summaries
  • Test coverage details: src/message_assembler/tests.rs and tests/cucumber.rs summaries
  • Public API and re-exports: src/lib.rs and module structure summaries
  • Code quality recommendations and mitigations: PR comments and commit messages summaries
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Sourcery review
  • GitHub Check: Sourcery review
🔇 Additional comments (20)
src/message_assembler/header.rs (1)

40-56: LGTM! Well-implemented overflow-safe increment.

The implementation correctly uses checked_add for overflow handling, includes comprehensive doctest examples, and follows the single-line function style guideline. The #[must_use] attribute ensures callers handle the Option result.

tests/worlds/mod.rs (1)

39-39: LGTM! Module declaration follows conventions.

The new message_assembly module is declared consistently with the existing test world modules.

tests/steps/mod.rs (1)

14-14: LGTM! Step module declaration is correct.

The new message_assembly_steps module follows the established pattern for Cucumber step definitions.

docs/roadmap.md (1)

284-286: LGTM! Roadmap accurately reflects completed work.

The completion of tasks 8.2.3 (message key support) and 8.2.4 (continuity validation) aligns with the implemented MessageSeries and MessageAssemblyState modules in this PR.

tests/cucumber.rs (3)

4-4: LGTM! Documentation updated correctly.

The module-level documentation accurately reflects the addition of MessageAssemblyWorld as the thirteenth test suite, with proper description and feature file mapping.

Also applies to: 11-11, 29-29


54-54: LGTM! Import follows conventions.

The MessageAssemblyWorld import is correctly positioned and consistent with the existing pattern.


69-69: LGTM! Test world execution wired correctly.

The MessageAssemblyWorld::run call is properly sequenced and references the correct feature file.

tests/features/message_assembly.feature (2)

1-7: Feature and Background are clear; scenarios comprehensively exercise multiplexing, ordering, and lifecycle management.

The feature description correctly frames protocol-level multiplexing above transport fragmentation. The Background establishes shared state (max size 1024, timeout 30 seconds), and Scenario 8 overrides max size to 10 to test bounds. All 8 scenarios map directly to design goals: single-message assembly, interleaved assembly, sequence validation, duplicate detection, missing first frame, size limits, and timeout-based eviction.


35-42: Scenario 4 step phrasing could be clearer: "expecting 2 but found 3" assumes internal state visibility.

The error assertion at line 41 references expected vs. found sequence. Ensure the step definition parses this format and the world provides adequate error inspection. If possible, extract the expected/found values in the step language to avoid brittle string matching (e.g., "the error reports a sequence mismatch expecting 2 but found 3").

docs/users-guide.md (1)

441-492: Section 8.2.3 clearly demonstrates per-key multiplexing with a well-designed example.

The code example shows two interleaved first frames (keys 1 and 2) and continuation handling. The pattern is sound: construct state with size/timeout bounds, feed first and continuation frames, unwrap/inspect results. One minor point: the example uses expect() for error handling; add a note that production code should propagate errors via ? rather than panic on framework errors.

Verify that the API signatures shown (e.g., state.accept_first_frame(&first1, vec![], b"hello") and return type Option<AssembledMessage>) align with the actual production implementation in src/message_assembler/state.rs.

docs/execplans/8-2-3-message-key-support.md (4)

11-41: Purpose and Constraints are clearly defined; success criteria are observable and testable.

The problem statement correctly frames protocol-level multiplexing above transport fragmentation. Success criteria enumerate new types, test coverage, documentation, and roadmap updates. Constraints are practical (400-line file limit, no new dependencies, integration with existing patterns). No issues detected.


144-224: Plan of Work stages are logically sequenced and deliverables are clearly specified.

Stages A–H provide a clear progression from header extension through error types, series/state management, exports, tests, and documentation. Test scenarios (Stage F) cover single-message assembly, interleaving, ordering violations, duplicates, missing frames, size/timeout constraints—comprehensive. Cucumber integration wiring (cucumber.rs, worlds/mod.rs, steps/mod.rs) is mentioned. One note: the Progress checklist shows [x] only for the draft and [ ] for all subsequent tasks; ensure this is updated as work advances to provide visibility into completion status.


323-377: Formal API specifications are comprehensive and include time-variant methods for testability.

The error taxonomy covers all foreseen failure modes (sequence mismatch, duplicates, missing frames, overflow, size limits). MessageSeries and MessageAssemblyState provide factories, lifecycle methods, and time-variant accessors (_at variants) for deterministic testing. AssembledMessage includes ownership-transfer (into_body) and borrowing accessors. The API design is sound and mirrors transport-level fragmentation patterns for consistency. Ensure implementation signatures strictly follow these specs to avoid user breakage.


52-61: Risks section identifies sequence tracking ambiguity and test coverage gaps; mitigations are sound.

Optional sequence tracking (when ContinuationFrameHeader::sequence is None) is correctly identified as a risk, with mitigation to skip validation. Overflow detection and force_next_sequence_for_tests() helper are specified to address underspecified edge cases. Both mitigations are implementable and reduce risk to acceptable levels.

src/message_assembler/tests.rs (1)

199-226: Helper functions successfully reduce test duplication.

The introduction of first_header, continuation_header, and continuation_header_untracked helpers effectively addresses the code duplication concern raised in the PR comments. This improves test maintainability and readability.

src/message_assembler/mod.rs (1)

1-22: Clear module documentation with feature references.

The module-level documentation effectively explains the purpose of MessageAssemblyState (multiplexing) and MessageSeries (continuity validation), with clear references to specific protocol features (8.2.3, 8.2.4). The structure and examples are well-organised.

tests/steps/message_assembly_steps.rs (2)

8-21: Effective helper functions reduce primitive obsession.

The to_key, to_seq, and assert_condition helpers successfully address the concerns raised in PR comments:

  • Primitive-to-domain conversion is pushed to the step boundary (lines 9, 12)
  • Common assertion pattern is extracted (lines 14-21)

This improves maintainability and aligns with the refactoring objectives outlined in commit 2.

Based on PR objectives and commit messages.


129-132: Appropriate use of #[expect] attributes.

The #[expect(clippy::needless_pass_by_value)] attributes are correctly applied and justified. Cucumber hands {string} captures to step functions as owned String values, making the suppression necessary and the reason clear.

As per coding guidelines and learnings regarding #[expect] usage.

Also applies to: 148-151

src/lib.rs (1)

80-95: Public API expansion is well-organised.

The new re-exports (AssembledMessage, FirstFrameInput, MessageAssemblyError, MessageAssemblyState, MessageSeries, MessageSeriesError, MessageSeriesStatus) are appropriately surfaced at the crate root. The alphabetical ordering within the list is maintained, and the additions align with the PR objectives for exposing message assembly functionality.

tests/worlds/message_assembly.rs (1)

246-269: Keep #[expect] truthful; delete it if clippy stops emitting the lint.

Run clippy and ensure the clippy::needless_pass_by_value expectation is fulfilled; otherwise clippy fails with an unfulfilled expectation. Based on learnings, never leave stale #[expect] behind.

#!/bin/bash
# Run the same clippy invocation used by CI (adjust if your Makefile differs).
make lint

Comment thread docs/users-guide.md
Comment thread src/message_assembler/error.rs
Comment thread src/message_assembler/series.rs
Comment thread src/message_assembler/series.rs
Comment thread src/message_assembler/state.rs Outdated
Comment thread src/message_assembler/state.rs
Comment thread src/message_assembler/state.rs
Comment thread tests/worlds/message_assembly.rs
Comment thread tests/worlds/message_assembly.rs
@leynos leynos changed the title Refactor message assembly test API; add multiplexing and public exports Refactor MessageAssembler: multiplexed per-key assemblies & exports Jan 12, 2026
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: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/message_assembler/state.rs (1)

1-471: File exceeds 400-line limit; decompose into smaller modules.

At 471 lines, this file exceeds the 400-line limit per coding guidelines. Extract FirstFrameInput and FirstFrameInputError into a separate input.rs module, or move AssembledMessage to output.rs.

🔧 Suggested decomposition

Create src/message_assembler/input.rs:

Move lines 23-104 (FirstFrameInput, FirstFrameInputError) to a new input.rs module.
Update mod.rs to re-export from input.
Update state.rs to import from super::input.

Alternatively, create src/message_assembler/output.rs:

Move lines 132-179 (AssembledMessage) to a new output.rs module.
Update mod.rs to re-export from output.
Update state.rs to import from super::output.
🤖 Fix all issues with AI agents
In @docs/execplans/8-2-3-message-key-support.md:
- Around line 74-77: Remove or retire the Cucumber-specific checklist entries
and replace them with the preferred testing guidance: remove references to
tests/features/message_assembly.feature, tests/worlds/message_assembly.rs,
tests/steps/message_assembly_steps.rs and the registration note for
tests/cucumber.rs and tests/worlds/mod.rs, and either mark these Cucumber
artifacts as retired or replace the items with instructions to create equivalent
integration tests using the project's standard test harness (e.g., Rust
integration tests or the approved framework) and link to the preferred testing
guide; ensure the checklist wording reflects retirement or the new testing
approach.
- Around line 99-102: The "Decision: Detect duplicates by comparing incoming
sequence against expected" log entry exceeds 80 columns; reflow that decision
sentence and any following rationale into lines wrapped at 80 characters or less
while preserving content and the Date/Author metadata (e.g., the line containing
"Decision: Detect duplicates..." and "Date/Author: 2026-01-07 (Codex)"). Ensure
each prose line breaks at word boundaries to maintain readability and keep the
original wording and intent unchanged.
- Around line 3-5: The introductory paragraph in the ExecPlan document exceeds
the 80-column wrap guideline; reflow that paragraph to maximum 80 characters per
line (soft-wrap) while leaving the surrounding headings and any code blocks
unwrapped, making sure line breaks occur at word boundaries so the paragraph
reads naturally.

In @src/message_assembler/state_tests.rs:
- Around line 164-189: Extract a parameterised rstest to consolidate the two
size-limit tests: replace the tests state_enforces_size_limit_on_first_frame and
state_enforces_size_limit_on_continuation with a single rstest that takes
parameters (case name, initial first-frame header/body sizes, optional
continuation size, expected attempted size, and expected buffered_count after
rejection); in the test instantiate MessageAssemblyState::new with the small
limit, construct inputs using FirstFrameInput::new and ContinuationInput::new as
needed, call accept_first_frame and optionally accept_continuation on the
MessageAssemblyState, assert the returned error matches
MessageAssemblyError::MessageTooLarge with the expected attempted size and that
the state's buffered_count is 0; reference the existing symbols
MessageAssemblyState, accept_first_frame, accept_continuation, FirstFrameInput,
ContinuationInput, and MessageAssemblyError when editing.

In @tests/steps/message_assembly_steps.rs:
- Around line 58-66: The two step functions when_first_frame_accepted and
when_first_frame_accepted_at_time_t are duplicates; consolidate by removing one
and annotating the remaining function with both step attributes (e.g., add a
second #[when("the first frame is accepted at time T")] above fn
when_first_frame_accepted) so a single function (when_first_frame_accepted)
handles both steps and keeps the body calling world.accept_first_frame().
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e286106 and 29d66df.

📒 Files selected for processing (10)
  • docs/execplans/8-2-3-message-key-support.md
  • src/message_assembler/error.rs
  • src/message_assembler/series_tests.rs
  • src/message_assembler/state.rs
  • src/message_assembler/state_tests.rs
  • src/message_assembler/tests.rs
  • tests/features/message_assembly.feature
  • tests/steps/message_assembly_steps.rs
  • tests/worlds/message_assembly.rs
  • tests/worlds/message_assembly_params.rs
🧰 Additional context used
📓 Path-based instructions (4)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Run make check-fmt (cargo fmt --workspace -- --check) before committing to validate Rust formatting across the entire workspace without modifying files.
Run make lint (cargo clippy --workspace --all-targets --all-features -- -D warnings) before committing to lint every target with all features enabled and deny all Clippy warnings.
Run make test (cargo test --workspace) before committing to run the full workspace test suite.
Clippy warnings MUST be disallowed. Fix any warnings emitted during tests in the code itself rather than silencing them.
Use make fmt (cargo fmt --workspace) to apply formatting fixes reported by the formatter check.
Where a Rust function is too long, extract meaningfully named helper functions adhering to separation of concerns and CQRS.
Where a Rust function has too many parameters, group related parameters in meaningfully named structs.
Where a Rust function is returning a large error, consider using Arc to reduce the amount of data returned.
Write unit and behavioural tests for new Rust functionality. Run both before and after making any change.
Every Rust module must begin with a module level (//!) comment explaining the module's purpose and utility.
Document public Rust APIs using Rustdoc comments (///) so documentation can be generated with cargo doc.
Prefer immutable data and avoid unnecessary mut bindings in Rust code.
Avoid unsafe code in Rust unless absolutely necessary, and document any usage clearly with a 'SAFETY' comment.
Place function attributes after doc comments in Rust code.
Do not use return in single-line Rust functions.
Use predicate functions for Rust conditional criteria with more than two branches.
Lints must not be silenced in Rust except as a last resort. Lint rule suppressions must be tightly scoped and include a clear reason.
Use concat!() to combine long string literals in Rust rather than escaping newlines with a backslash.
Prefer single-line versions of Rust functions where...

Files:

  • src/message_assembler/tests.rs
  • src/message_assembler/state_tests.rs
  • tests/steps/message_assembly_steps.rs
  • src/message_assembler/series_tests.rs
  • src/message_assembler/error.rs
  • tests/worlds/message_assembly_params.rs
  • tests/worlds/message_assembly.rs
  • src/message_assembler/state.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • src/message_assembler/tests.rs
  • src/message_assembler/state_tests.rs
  • tests/steps/message_assembly_steps.rs
  • src/message_assembler/series_tests.rs
  • src/message_assembler/error.rs
  • tests/worlds/message_assembly_params.rs
  • tests/worlds/message_assembly.rs
  • src/message_assembler/state.rs
docs/**/*.md

📄 CodeRabbit inference engine (docs/contents.md)

Follow the documentation style guide conventions when writing project documentation

Write official documentation with separate guides for 'Duplex Messaging & Pushes', 'Streaming Responses', and 'Message Fragmentation', each including runnable examples and explaining relevant concepts and APIs

docs/**/*.md: Use markdown files within the docs/ directory as a knowledge base and source of truth for project requirements, dependency choices, and architectural decisions.
When new decisions are made, requirements change, libraries are added/removed, or architectural patterns evolve, proactively update the relevant file(s) in the docs/ directory to reflect the latest state. Ensure documentation remains accurate and current.
Documentation must use en-GB-oxendict spelling and grammar ('-ize' / '-yse' / '-our'), with the exception of the filename LICENSE.
Reference the documentation style guide provided at docs/documentation-style-guide.md for documentation writing standards.

Documentation describing multi-packet streaming MUST reflect the tuple-based API and explain helper constructors that prepare the channel pair

Replace or retire documentation that refers to Cucumber, including guides and examples in docs/

docs/**/*.md: Use British English based on the Oxford English Dictionary (en-GB-oxendict), including: suffix -ize in words like 'realize' and 'organization', suffix -lyse in words like 'analyse' and 'paralyse', suffix -our in words like 'colour' and 'behaviour', suffix -re in words like 'calibre' and 'centre', double 'l' in words like 'cancelled' and 'counsellor', maintain the 'e' in words like 'likeable' and 'liveable', suffix -ogue in words like 'analogue' and 'catalogue'
The word 'outwith' is acceptable in documentation
Keep US spelling when used in an API, for example 'color'
Use the Oxford comma: 'ships, planes, and hovercraft' where it aids comprehension
Treat company names as collective nouns, for example: 'Lille Industries are expanding'
Write hea...

Files:

  • docs/execplans/8-2-3-message-key-support.md
docs/**/*.{rs,md}

📄 CodeRabbit inference engine (docs/rust-doctest-dry-guide.md)

Every doctest should validate the public API of a crate from the perspective of an external user, treating each documentation test as a separate temporary crate that imports the library as an external dependency

Files:

  • docs/execplans/8-2-3-message-key-support.md
**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

**/*.md: Validate Markdown files using make markdownlint.
Run make fmt after any documentation changes to format all Markdown files and fix table markup.
Validate Mermaid diagrams in Markdown files by running make nixie.
Markdown paragraphs and bullet points must be wrapped at 80 columns.
Code blocks in Markdown must be wrapped at 120 columns.
Markdown tables and headings must not be wrapped.
Use dashes (-) for list bullets in Markdown.
Use GitHub-flavoured Markdown footnotes ([^1]) for references and footnotes in Markdown.

Files:

  • docs/execplans/8-2-3-message-key-support.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/execplans/8-2-3-message-key-support.md
🧠 Learnings (4)
📚 Learning: 2026-01-03T17:44:28.792Z
Learnt from: leynos
Repo: leynos/wireframe PR: 412
File: tests/streaming_request.rs:85-107
Timestamp: 2026-01-03T17:44:28.792Z
Learning: In Rust code (e.g., the wireframe project), avoid direct indexing like vec[0] because it can panic. Prefer safe accessors that return Option, such as vec.get(0), vec.first(), or patterns that handle Option safely (e.g., match or if let Some). This should be applied across Rust files, including tests (e.g., tests/streaming_request.rs), regardless of prior length checks, to ensure safety and avoid panics from out-of-bounds access.

Applied to files:

  • src/message_assembler/tests.rs
  • src/message_assembler/state_tests.rs
  • tests/steps/message_assembly_steps.rs
  • src/message_assembler/series_tests.rs
  • src/message_assembler/error.rs
  • tests/worlds/message_assembly_params.rs
  • tests/worlds/message_assembly.rs
  • src/message_assembler/state.rs
📚 Learning: 2026-01-08T08:16:31.989Z
Learnt from: leynos
Repo: leynos/wireframe PR: 411
File: src/client/tests/lifecycle.rs:0-0
Timestamp: 2026-01-08T08:16:31.989Z
Learning: In Rust, apply #[expect(lint, reason = "...")] only to code that actually triggers the specified lint. If the code does not trigger the lint, Clippy will fail with 'unfulfilled lint expectation' errors, which is intentional to prevent stale suppressions. Do not add #[expect] attributes proactively or to code that doesn't trigger the lint, even if similar code elsewhere requires suppression. This guideline should be applied broadly to Rust source files across the repository.

Applied to files:

  • src/message_assembler/tests.rs
  • src/message_assembler/state_tests.rs
  • tests/steps/message_assembly_steps.rs
  • src/message_assembler/series_tests.rs
  • src/message_assembler/error.rs
  • tests/worlds/message_assembly_params.rs
  • tests/worlds/message_assembly.rs
  • src/message_assembler/state.rs
📚 Learning: 2026-01-08T20:33:24.570Z
Learnt from: leynos
Repo: leynos/wireframe PR: 417
File: src/codec/error_tests.rs:152-152
Timestamp: 2026-01-08T20:33:24.570Z
Learning: Guideline: In Rust, when using contains with a single-character pattern, prefer a char literal (e.g., 'a') over a string literal (e.g., "a") to satisfy the Clippy lint single_char_pattern, which flags single-character strings as less efficient. Apply this across Rust source files (all .rs) to improve efficiency and lint compliance. Example: use s.contains('a') instead of s.contains("a"); note that str::contains can take a char, making the char literal both correct and more efficient.

Applied to files:

  • src/message_assembler/tests.rs
  • src/message_assembler/state_tests.rs
  • tests/steps/message_assembly_steps.rs
  • src/message_assembler/series_tests.rs
  • src/message_assembler/error.rs
  • tests/worlds/message_assembly_params.rs
  • tests/worlds/message_assembly.rs
  • src/message_assembler/state.rs
📚 Learning: 2026-01-08T18:03:59.395Z
Learnt from: leynos
Repo: leynos/wireframe PR: 417
File: tests/steps/codec_error_steps.rs:58-62
Timestamp: 2026-01-08T18:03:59.395Z
Learning: In the wireframe repo, test step functions that always succeed should not return TestResult, because clippy::unnecessary_wraps will fire and cannot be suppressed in tests. Do not use #[allow(...)] or #[expect(clippy::unnecessary_wraps)] to silence it. Refactor such steps to return () or a type that does not trigger the lint, or move these steps to non-test code or a helper that isn’t linted the same way. This guidance applies to all files under tests/steps that contain similar patterns.

Applied to files:

  • tests/steps/message_assembly_steps.rs
🔍 Remote MCP

Let me try a web search to understand the broader context of message assembly and multiplexing patterns:Based on the context provided and the PR content analysis, I have gathered sufficient information to provide focused review guidance. The PR introduces message assembly with multiplexing and continuity validation patterns similar to protocol standards like ISO-TP. Let me provide a summary of the key review insights:

Summary of Key Review Findings for PR #418

Core Design & Architecture

The PR introduces a three-layer message assembly system addressing interleaved multi-message scenarios:

  1. MessageAssemblyState (Orchestrator) – Manages per-key partial assemblies with constraints (max size, timeout-based purging)
  2. MessageSeries (Per-Message Tracker) – Handles frame sequencing, ordering validation, and completion detection for individual message streams
  3. Error Taxonomy – Comprehensive error types (MessageSeriesError, MessageAssemblyError, FirstFrameInputError) with detailed context

Protocol Pattern Alignment

The implementation reflects established patterns from ISO-TP (ISO 15765-2), which is an international standard for sending data packets over a CAN bus allowing transport of messages that exceed the maximum payload of CAN frames by segmenting longer messages into multiple frames and adding metadata that allows reassembly. Key parallels:

  • Frame sequencing – Sequence numbers start at 1 and increment with each frame sent, allowing detection of lost or discarded frames
  • Continuation validation – Checks for key consistency, ordering, overflow, and duplicates
  • Timeout-based cleanup – Purges incomplete assemblies after configurable duration

Critical Review Points

API Design Feedback (from comments):

  • Excess parameters: Methods like accept_first_frame_at() and world methods add_first_frame(), accept_continuation() exceed 4-argument thresholds. The PR already introduces FirstFrameInput as a parameter object for production code, which is appropriate for state.rs. Test APIs should follow similar patterns.
  • Primitive obsession: Cucumber step definitions accept primitives directly. While this is a framework artifact, converting primitives at step boundaries (e.g., to_message_key(), to_frame_sequence()) into domain types improves type safety and readability.
  • Test boilerplate: Both unit tests and step definitions have repeated header construction patterns. Extracting helpers (e.g., FirstFrameHeader::default_for_tests()) reduces duplication.

Validation Coverage:

  • The PR validates continuation body length via ContinuationBodyLengthMismatch error, addressing a critical data integrity concern
  • Error handling distinguishes recoverable (assembly retained) vs. unrecoverable (assembly removed) failures
  • Overflow protection on FrameSequence::checked_increment() prevents wrap-around vulnerabilities

Test Completeness:

  • Unit tests (series_tests.rs, state_tests.rs) cover sequential, out-of-order, duplicate, and timeout scenarios
  • Cucumber feature file exercises multiplexing across interleaved streams and error conditions
  • Single-frame completion optimization (immediate assembly) is validated

Public API Exports

Re-exports at crate root (src/lib.rs) appropriately surface MessageAssemblyState, MessageSeries, error types, and AssembledMessage. This reduces client boilerplate and aligns with ergonomic design goals.

Recommended Validation Steps

  1. Verify purge_expired() correctness under concurrent timestamp scenarios
  2. Confirm size limit enforcement applies consistently to both first frames and continuations
  3. Validate that assembly state is fully cleaned on unrecoverable errors (no dangling metadata or partial buffers)
  4. Test interleaved assembly with realistic frame timing variations
🔇 Additional comments (35)
docs/execplans/8-2-3-message-key-support.md (1)

1-382: Structure and content are sound.

The execution plan comprehensively documents the feature scope, constraints, risks, and staged implementation approach. The decision log captures key architectural choices with rationale.

src/message_assembler/error.rs (3)

1-19: Well-structured error module with appropriate derives.

The module doc comment explains the purpose clearly. MessageSeriesStatus is correctly minimal with Clone, Copy, and the equality traits. Using thiserror aligns with project conventions.


21-83: Comprehensive error variants with contextual data.

MessageSeriesError covers the full spectrum of continuity validation failures: key mismatches, sequence issues, completion state, overflow, and the new ContinuationBodyLengthMismatch. Each variant carries sufficient context for diagnostics.


85-110: Assembly error hierarchy is well-designed.

The #[from] attribute on Series enables ergonomic ? propagation from series-level errors. DuplicateFirstFrame and MessageTooLarge correctly surface assembly-specific failures with actionable context.

src/message_assembler/tests.rs (1)

190-203: Clean module organization for expanded test coverage.

Using #[path = "..."] to wire in series_tests and state_tests from dedicated files keeps this module under the 400-line limit whilst maintaining logical grouping. The banner comments aid navigation.

tests/features/message_assembly.feature (3)

1-79: New Cucumber scaffolding conflicts with coding guidelines.

The coding guidelines state: "Replace or retire documentation that refers to Cucumber, including guides and examples in docs/". Adding new Cucumber feature files contradicts this directive. Clarify whether Cucumber is being retained for this feature or migrate these scenarios to Rust-native tests using rstest parameterisation.

If Cucumber is intentionally retained for BDD coverage, update the coding guidelines to reflect the current policy. Otherwise, convert these scenarios to parameterised unit tests.


35-42: Scenario correctly validates recoverable error handling.

The "Out-of-order continuation is rejected but assembly retained" scenario verifies that sequence mismatches do not discard the partial assembly, allowing recovery. This aligns with the error handling strategy described in the PR objectives.


64-70: Size limit scenario correctly overrides background state.

Overriding the background's max size to 10 bytes in this scenario isolates the size-limit behaviour without affecting other scenarios. The assertion on buffered count is 0 confirms the assembly is removed on unrecoverable errors.

src/message_assembler/state_tests.rs (7)

1-25: Module documentation and helper function are appropriate.

The module doc comment identifies the test scope. state_with_defaults() centralises common setup, reducing repetition across tests.


27-57: Single message assembly test is thorough.

Validates the complete lifecycle: first frame acceptance, buffering, continuation acceptance, completion, and buffer cleanup. Assertions cover message key, metadata, body content, and buffered count.


59-116: Interleaved assembly test validates multiplexing.

This test exercises the core multiplexing capability: two concurrent assemblies with interleaved frame acceptance and independent completion. The buffered count assertions at each stage confirm correct state management.


118-162: Error variant matching is clear and correct.

The matches! macro usage for MissingFirstFrame and DuplicateFirstFrame variants is idiomatic and readable.


231-260: Time-based purging test correctly uses accept_first_frame_at and purge_expired_at.

Using explicit timestamps enables deterministic testing of expiry behaviour without relying on wall-clock time. The 31-second advance past a 30-second timeout correctly triggers eviction.


262-284: Single-frame completion test validates immediate return path.

This confirms that messages with is_last: true on the first frame complete immediately without buffering, avoiding unnecessary state allocation.


286-291: Confirm AssembledMessage::new is part of the public API.

AssembledMessage::new is called directly in this test. Establish whether this constructor is exported as public API or is internal-only. If internal, use an alternative construction path or mark it #[doc(hidden)].

tests/steps/message_assembly_steps.rs (4)

1-21: Well-structured boundary adapters and assertion helper.

The to_key and to_seq functions correctly convert primitives to domain types at the step boundary, addressing the primitive obsession concerns. The assert_condition helper effectively reduces boilerplate in Then steps.


27-52: Given steps correctly use parameter objects and domain type conversion.

The steps consistently use FirstFrameParams with its builder methods and convert primitives via to_key at the boundary.


125-178: Then steps for result verification are well-implemented.

The steps correctly use the assert_condition helper with descriptive error messages. The #[expect] attributes include appropriate reason strings for the Cucumber string capture pattern.


180-245: Error condition steps are consistent and provide good diagnostics.

All error-checking steps use assert_condition with informative error messages that include the actual error via world.last_error() for debugging failed assertions.

src/message_assembler/series_tests.rs (3)

1-40: Test helpers effectively reduce header construction boilerplate.

The first_header, continuation_header, and continuation_header_untracked helpers address the code duplication flagged in review comments whilst keeping test setup concise.


42-110: Sequential and ordering tests provide comprehensive coverage.

The tests correctly exercise the happy path, untracked continuations, key mismatch, and out-of-order sequence scenarios. The use of .expect() and matches! aligns with coding guidelines.


112-200: Edge case tests are thorough and well-structured.

The tests cover duplicate sequence detection, post-completion rejection, sequence overflow at u32::MAX, expected total storage, and single-frame message completion. The force_next_sequence_for_tests helper enables controlled overflow testing.

tests/worlds/message_assembly_params.rs (2)

1-44: FirstFrameParams builder is well-designed.

The parameter object addresses the excess arguments concern with a fluent builder pattern. Public fields enable direct access in tests, and #[must_use] correctly annotates builder methods.


46-84: ContinuationFrameParams builder is consistent and correct.

The default sequence of 1 aligns with protocol expectations where the first continuation frame starts at sequence 1. The builder pattern is consistent with FirstFrameParams.

src/message_assembler/state.rs (6)

1-21: Module documentation is comprehensive and imports are well-organised.

The module-level doc comment clearly explains the purpose of MessageAssemblyState and its role in managing in-flight assemblies.


23-104: FirstFrameInput validation and error handling are exemplary.

The constructor validates header lengths against payloads, returning semantic errors via thiserror. The rustdoc example demonstrates correct usage.


106-130: PartialAssembly is a clean internal helper.

The internal struct encapsulates per-key assembly state with minimal surface area. Single-line method bodies follow coding guidelines.


132-179: AssembledMessage provides a clean API for completed messages.

The struct correctly derives common traits, provides const accessors where possible, and offers into_body for zero-copy body extraction.


240-328: First-frame acceptance logic is well-structured.

The accept_first_frame_at method correctly validates size limits using saturating_add, handles single-frame messages with early return, and stores partial assemblies for multi-frame messages.


356-442: Continuation frame handling correctly distinguishes recoverable from unrecoverable errors.

The error categorisation (lines 391-403) retains assemblies for potentially recoverable errors (duplicate frame, sequence mismatch, series complete) whilst removing assemblies for unrecoverable errors. The size limit enforcement uses checked_add to prevent overflow.

tests/worlds/message_assembly.rs (5)

1-66: World struct and custom Debug implementation are well-designed.

The VecDeque for pending frames enables FIFO semantics as documented. The custom Debug implementation avoids verbose state dumps whilst preserving diagnostic utility.


68-114: State initialisation and frame queueing are correct.

The create_state method appropriately panics on zero max_size (documented in the doc comment) and resets all state for test isolation. The add_first_frame method correctly constructs headers from parameter objects.


116-163: Frame acceptance methods handle errors gracefully.

Both methods correctly validate state initialisation and use safe queue access via pop_front(). Error mapping provides context for test diagnostics.


165-222: Continuation acceptance, time advancement, and purge methods are correctly implemented.

The #[expect] attribute provides a clear reason for accepting by value. Time manipulation and purge operations correctly interact with the underlying state.


224-317: Query and error inspection methods use safe access patterns throughout.

The methods consistently use map_or, matches! with guards, and iterator methods like .rev().find() - no direct indexing. Error inspection is precise and diagnostic-friendly.

Comment thread docs/execplans/8-2-3-message-key-support.md
Comment thread docs/execplans/8-2-3-message-key-support.md
Comment thread docs/execplans/8-2-3-message-key-support.md
Comment thread src/message_assembler/state_tests.rs Outdated
Comment thread tests/steps/message_assembly_steps.rs
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 12, 2026

@coderabbitai Have the following now been resolved?

Please address the comments from this code review:

## Overall Comments
- The exec plan in `docs/execplans/8-2-3-message-key-support.md` still has `Status: IN PROGRESS` and many unchecked items in `Progress` even though this PR appears to complete most of them; consider updating the status and checklist so the plan accurately reflects the implemented work.
- In `MessageAssemblyWorld`, pending first frames are stored in a `Vec` and consumed with `pop()`, which makes acceptance LIFO; if future scenarios depend on FIFO semantics, it may be clearer and less error-prone to switch to a `VecDeque` and pop from the front instead.

## Individual Comments

### Comment 1
<location> `src/message_assembler/series.rs:195-204` </location>
<code_context>
+        Ok(MessageSeriesStatus::Incomplete)
+    }
+
+    fn validate_and_advance_sequence(
+        &mut self,
+        incoming: FrameSequence,
+    ) -> Result<(), MessageSeriesError> {
+        match self.sequence_tracking {
+            SequenceTracking::Untracked => {
+                // First continuation with a sequence number; start tracking
+                self.sequence_tracking = SequenceTracking::Tracked;
+                self.next_sequence = incoming.checked_increment();
+                if self.next_sequence.is_none() && !self.complete {
+                    return Err(MessageSeriesError::SequenceOverflow { last: incoming });
+                }
</code_context>

<issue_to_address>
**issue (bug_risk):** Sequence overflow handling incorrectly fails even for the final frame.

In `validate_and_advance_sequence`, `incoming.checked_increment()` overflow causes `SequenceOverflow` whenever `self.next_sequence` is `None` and `!self.complete`. But `self.complete` is only set after `accept_continuation` calls this helper, so a final frame with `sequence == FrameSequence(u32::MAX)` is incorrectly rejected as overflow.

To fix this, either:
- pass `header.is_last` into `validate_and_advance_sequence` and only treat overflow as an error when another frame is expected, or
- move the overflow check to `accept_continuation`, after deciding completion, and skip the error when the current frame is final.

This preserves `u32::MAX` as a valid final sequence while still detecting real overflow when more frames should follow.
</issue_to_address>

### Comment 2
<location> `src/message_assembler/state.rs:249` </location>
<code_context>
+        }
+
+        // Validate body size
+        if input.body.len() > self.max_message_size.get() {
+            return Err(MessageAssemblyError::MessageTooLarge {
+                key,
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Size checks on first frames ignore declared total length and metadata size.

`accept_first_frame_at` only checks `input.body.len()`. If `header.total_body_len` is present and already exceeds `max_message_size`, we should reject immediately instead of buffering any data. The guard also ignores `metadata.len()`, even though it contributes to the per-message memory footprint. Tightening the check to use `total_body_len` when available and to optionally include metadata in the accounting would make the limit more accurate and avoid starting assemblies that can never succeed.

Suggested implementation:

```rust
        // Validate message size (prefer declared total body length and include metadata)
        let declared_body_len = input
            .header
            .total_body_len
            .unwrap_or_else(|| input.body.len());

        let total_message_size = declared_body_len
            .saturating_add(input.metadata.len());

        if total_message_size > self.max_message_size.get() {
            return Err(MessageAssemblyError::MessageTooLarge {
                key,
                attempted: total_message_size,
                limit: self.max_message_size,
            });
        }

        let series = MessageSeries::from_first_frame(input.header);

```

Depending on the exact types in your codebase, you may need to adjust:

1. `header.total_body_len`:
   - If this is not `Option<usize>`, adapt the unwrap to match the type, e.g.:
     - `total_body_len.map(|n| n as usize).unwrap_or_else(...)` or
     - `total_body_len.get()` if it’s a `NonZeroUsize`.
2. `input.metadata.len()`:
   - If metadata is stored differently (e.g. `Option<Bytes>` or length in the header), replace `input.metadata.len()` with the appropriate expression (e.g. `input.metadata.as_ref().map_or(0, |m| m.len())` or `input.header.metadata_len`).
3. If `MessageTooLarge::attempted` is not `usize`, cast `total_message_size` to the expected type (e.g. `total_message_size as u64`).
</issue_to_address>

### Comment 3
<location> `tests/steps/message_assembly_steps.rs:103-108` </location>
<code_context>
+    world.accept_continuation(ContinuationFrameParams::new(to_key(key), body.into_bytes()))
+}
+
+#[when(expr = "another first frame for key {int} with body {string} arrives")]
+fn when_another_first_frame(world: &mut MessageAssemblyWorld, key: u64, body: String) {
+    world.add_first_frame(FirstFrameParams::new(to_key(key), body.into_bytes()));
+    // Ignore result, we check error in Then
+    let _ = world.accept_first_frame();
+}
+
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Avoid discarding the result in the "another first frame" step to keep assertions precise

This step currently calls `accept_first_frame()` and discards the `TestResult` with `let _ = ...`. Because `accept_first_frame` mutates `last_result`, Then steps that assert on errors still work, but any failure in `accept_first_frame` itself would not fail the step, and this step’s signature is inconsistent with the others that return `TestResult`. Consider returning and propagating the `TestResult` here (e.g. `fn when_another_first_frame(...) -> TestResult { world.add_first_frame(...); world.accept_first_frame() }`) so infrastructure errors surface and the cucumber steps stay consistent.

```suggestion
+#[when(expr = "another first frame for key {int} with body {string} arrives")]
+fn when_another_first_frame(
+    world: &mut MessageAssemblyWorld,
+    key: u64,
+    body: String,
+) -> TestResult {
+    world.add_first_frame(FirstFrameParams::new(to_key(key), body.into_bytes()));
+    world.accept_first_frame()
+}
```
</issue_to_address>

### Comment 4
<location> `src/message_assembler/state.rs:40` </location>
<code_context>
+/// let input = FirstFrameInput::new(&header, vec![0x01, 0x02], b"hello");
+/// assert_eq!(input.header.message_key, MessageKey(1));
+/// ```
+#[derive(Debug)]
+pub struct FirstFrameInput<'a> {
+    /// The frame header.
</code_context>

<issue_to_address>
**issue (review_instructions):** Add both unit tests and behavioural (Cucumber) tests that exercise MessageAssemblyState, MessageSeries, and FrameSequence::checked_increment; the new functionality is currently untested.

Add unit tests under `src/message_assembler/tests.rs` that cover the new message-key multiplexing and continuity behaviour: e.g. single-message assembly, interleaved assemblies for multiple `MessageKey`s, continuation without a first frame, duplicate first frames, size-limit enforcement, timeout purging, and sequence-order validation (including duplicates, gaps, and overflow via `force_next_sequence_for_tests`).

In addition, add the required behavioural coverage: implement scenarios in `tests/features/message_assembly.feature` along with corresponding worlds and step definitions in `tests/worlds/message_assembly.rs` and `tests/steps/message_assembly_steps.rs`. Ensure these are wired into the Cucumber runner so that the new protocol-level assembly behaviour is exercised end-to-end through the existing harness.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*`

**Instructions:**
- For any new feature or change to an existing feature, both behavioural *and* unit tests are required.
- Bug fixes *must* be demonstrated by a test.

</details>
</issue_to_address>

### Comment 5
<location> `src/message_assembler/state.rs:157` </location>
<code_context>
+/// };
+///
+/// let mut state =
+///     MessageAssemblyState::new(NonZeroUsize::new(1024).unwrap(), Duration::from_secs(30));
+///
+/// // Start assembly for key 1
</code_context>

<issue_to_address>
**suggestion (review_instructions):** The example uses `unwrap()` instead of the preferred `.expect()` for `NonZeroUsize::new(1024)`.

To follow the "Prefer `.expect()` over `.unwrap()`" guideline, please change this example to use `.expect()` with a brief message, e.g.:

```rust
let mut state = MessageAssemblyState::new(
    NonZeroUsize::new(1024).expect("non-zero message size"),
    Duration::from_secs(30),
);
```

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.rs`

**Instructions:**
Prefer `.expect()` over `.unwrap()`

</details>
</issue_to_address>

### Comment 6
<location> `src/message_assembler/state.rs:169` </location>
<code_context>
+/// };
+/// let result = state.accept_first_frame(FirstFrameInput::new(&first, vec![0x01, 0x02], b"hello"));
+/// assert!(result.is_ok());
+/// assert!(result.unwrap().is_none()); // Not yet complete
+///
+/// // Complete with continuation
</code_context>

<issue_to_address>
**suggestion (review_instructions):** The example asserts on `result.unwrap()` instead of using `.expect()` as required by the guidelines.

To comply with the "Prefer `.expect()` over `.unwrap()`" rule, consider rewriting this assertion to avoid a bare `unwrap`, for example:

```rust
let msg = result.expect("first frame processing should succeed");
assert!(msg.is_none()); // Not yet complete
```

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.rs`

**Instructions:**
Prefer `.expect()` over `.unwrap()`

</details>
</issue_to_address>

### Comment 7
<location> `docs/users-guide.md:458` </location>
<code_context>
+};
+
+let mut state = MessageAssemblyState::new(
+    NonZeroUsize::new(1_048_576).unwrap(), // 1 MiB max message
+    Duration::from_secs(30),               // 30s timeout for partial assemblies
+);
</code_context>

<issue_to_address>
**suggestion (review_instructions):** The acronym “MiB” is used without being defined on first use.

To follow the documentation guideline about defining uncommon acronyms, consider expanding this comment to something like `// 1 mebibyte (MiB) max message` so the meaning of MiB is clear at first use.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

### Comment 8
<location> `docs/execplans/8-2-3-message-key-support.md:139` </location>
<code_context>
+
+- Unit tests use rstest for parameterised cases
+  (`src/message_assembler/tests.rs`).
+- Cucumber BDD tests in `tests/features/message_assembler.feature`.
+- Test helper `TestAssembler` in `src/test_helpers.rs`.
+- World/steps in `tests/worlds/message_assembler.rs` and
</code_context>

<issue_to_address>
**suggestion (review_instructions):** The acronym “BDD” is introduced without being defined on first use.

To comply with the acronym rule, please expand this to something like “Cucumber Behaviour-Driven Development (BDD) tests” the first time BDD appears, and then use BDD afterwards.

<details>
<summary>Review instructions:</summary>

**Path patterns:** `**/*.md`

**Instructions:**
Define uncommon acronyms on first use.

</details>
</issue_to_address>

@coderabbitai

This comment was marked as resolved.

@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 12, 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:

src/message_assembler/state.rs

Comment on lines +356 to +442

    pub fn accept_continuation_frame_at(
        &mut self,
        header: &ContinuationFrameHeader,
        body: &[u8],
        now: Instant,
    ) -> Result<Option<AssembledMessage>, MessageAssemblyError> {
        self.purge_expired_at(now);

        let key = header.message_key;

        // Validate header body_len matches actual payload
        if header.body_len != body.len() {
            return Err(MessageAssemblyError::Series(
                MessageSeriesError::ContinuationBodyLengthMismatch {
                    key,
                    header_len: header.body_len,
                    actual_len: body.len(),
                },
            ));
        }

        let Entry::Occupied(mut entry) = self.assemblies.entry(key) else {
            return Err(MessageAssemblyError::Series(
                MessageSeriesError::MissingFirstFrame { key },
            ));
        };

        // Validate continuity
        let status = match entry.get_mut().series.accept_continuation(header) {
            Ok(s) => s,
            Err(e) => {
                // Only remove assembly for unrecoverable errors.
                // DuplicateFrame, SequenceMismatch, and SeriesComplete are potentially
                // recoverable (retransmits, reordering) - retain assembly for recovery
                // or timeout-based cleanup.
                match &e {
                    MessageSeriesError::KeyMismatch { .. }
                    | MessageSeriesError::SequenceOverflow { .. }
                    | MessageSeriesError::MissingFirstFrame { .. }
                    | MessageSeriesError::MissingSequence { .. }
                    | MessageSeriesError::ContinuationBodyLengthMismatch { .. } => {
                        entry.remove();
                    }
                    MessageSeriesError::DuplicateFrame { .. }
                    | MessageSeriesError::SequenceMismatch { .. }
                    | MessageSeriesError::SeriesComplete => {
                        // Retain assembly for recovery or timeout-based cleanup
                    }
                }
                return Err(MessageAssemblyError::Series(e));
            }
        };

        // Check size limit
        let accumulated = entry.get().accumulated_len();
        let Some(new_len) = accumulated.checked_add(body.len()) else {
            entry.remove();
            return Err(MessageAssemblyError::MessageTooLarge {
                key,
                attempted: usize::MAX,
                limit: self.max_message_size,
            });
        };

        if new_len > self.max_message_size.get() {
            entry.remove();
            return Err(MessageAssemblyError::MessageTooLarge {
                key,
                attempted: new_len,
                limit: self.max_message_size,
            });
        }

        entry.get_mut().push_body(body);

        match status {
            MessageSeriesStatus::Incomplete => Ok(None),
            MessageSeriesStatus::Complete => {
                let partial = entry.remove();
                Ok(Some(AssembledMessage::new(
                    key,
                    partial.metadata,
                    partial.body_buffer,
                )))
            }
        }
    }

❌ New issue: Large Method
MessageAssemblyState.accept_continuation_frame_at has 71 lines, threshold = 70

Repository owner deleted a comment from coderabbitai Bot Jan 12, 2026
@coderabbitai

This comment was marked as resolved.

@leynos leynos changed the title Refactor MessageAssembler: multiplexed per-key assemblies & exports Implement message key support for multiplexing Jan 13, 2026
… size checks

- Introduced a method to identify unrecoverable continuity errors to better handle assembly removal.
- Added size limit checks when appending message continuation frames to prevent oversized messages.
- Improved error handling to remove assemblies only on unrecoverable errors.
- Documented the multiplexing behavior of MessageAssemblyState for concurrent message keys with a detailed mermaid sequence diagram.
- Enhanced message assembly state management to support multiplexed frame streams keyed by message keys, enabling interleaved processing and timeout cleanup.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.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: 5

🤖 Fix all issues with AI agents
In @docs/execplans/8-2-3-message-key-support.md:
- Around line 13-17: Add a comma before "so" in the sentence describing
MessageAssembler: update the sentence referencing Wireframe's `MessageAssembler`
to read "Wireframe's `MessageAssembler` module needs multiplexing support, so
protocol implementations can process..." (edit the text containing
`MessageAssembler` to insert the comma).
- Around line 427-431: The snippet for the FrameSequence impl is using indented
code style instead of a fenced Rust code block; update the documentation so the
block is fenced with ```rust before and ``` after, and ensure the content shows
the exact impl FrameSequence { pub fn checked_increment(self) -> Option<Self>; }
declaration (referenced symbols: FrameSequence, checked_increment) so it renders
correctly.
- Around line 280-333: Replace all indented code blocks in the "Concrete Steps"
section with fenced code blocks that include a language identifier (use "shell"
for command lists and "plaintext" where appropriate); specifically, convert the
indented blocks under steps 1–10 (examples showing cat commands, “# Edit …”, and
the make commands) into fenced triple-backtick blocks (e.g., ```shell ... ```)
so the examples in lines mentioning src/message_assembler/header.rs,
src/fragment/series.rs, the checked_increment note, the created files
(message_assembler/error.rs, series.rs, state.rs,
tests/features/message_assembly.feature, etc.), and the quality gates commands
are all fenced and annotated to satisfy markdownlint MD046.
- Around line 378-425: The indented Rust interface examples should be converted
to fenced code blocks with language identifier "rust": replace each indented
block that defines types like MessageSeriesStatus, MessageSeriesError,
MessageSeries, AssembledMessage, MessageAssemblyError, and MessageAssemblyState
with a fenced block starting with ```rust and ending with ```; ensure each block
contains the exact original public signatures (e.g., pub enum
MessageSeriesStatus { Incomplete, Complete }, pub fn from_first_frame(header:
&FirstFrameHeader) -> Self, pub fn accept_continuation(&mut self, header:
&ContinuationFrameHeader) -> Result<MessageSeriesStatus, MessageSeriesError>,
pub fn into_body(self) -> Vec<u8>, pub enum MessageAssemblyError {
Series(MessageSeriesError), DuplicateFirstFrame { key: MessageKey },
MessageTooLarge { key: MessageKey, attempted: usize, limit: NonZeroUsize } },
and pub struct MessageAssemblyState with its listed methods) and apply this
fenced format to all remaining interface definition blocks in the section.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 29d66df and 6190b49.

📒 Files selected for processing (2)
  • docs/execplans/8-2-3-message-key-support.md
  • src/message_assembler/state.rs
🧰 Additional context used
📓 Path-based instructions (4)
docs/**/*.md

📄 CodeRabbit inference engine (docs/contents.md)

Follow the documentation style guide conventions when writing project documentation

Write official documentation with separate guides for 'Duplex Messaging & Pushes', 'Streaming Responses', and 'Message Fragmentation', each including runnable examples and explaining relevant concepts and APIs

docs/**/*.md: Use markdown files within the docs/ directory as a knowledge base and source of truth for project requirements, dependency choices, and architectural decisions.
When new decisions are made, requirements change, libraries are added/removed, or architectural patterns evolve, proactively update the relevant file(s) in the docs/ directory to reflect the latest state. Ensure documentation remains accurate and current.
Documentation must use en-GB-oxendict spelling and grammar ('-ize' / '-yse' / '-our'), with the exception of the filename LICENSE.
Reference the documentation style guide provided at docs/documentation-style-guide.md for documentation writing standards.

Documentation describing multi-packet streaming MUST reflect the tuple-based API and explain helper constructors that prepare the channel pair

Replace or retire documentation that refers to Cucumber, including guides and examples in docs/

docs/**/*.md: Use British English based on the Oxford English Dictionary (en-GB-oxendict), including: suffix -ize in words like 'realize' and 'organization', suffix -lyse in words like 'analyse' and 'paralyse', suffix -our in words like 'colour' and 'behaviour', suffix -re in words like 'calibre' and 'centre', double 'l' in words like 'cancelled' and 'counsellor', maintain the 'e' in words like 'likeable' and 'liveable', suffix -ogue in words like 'analogue' and 'catalogue'
The word 'outwith' is acceptable in documentation
Keep US spelling when used in an API, for example 'color'
Use the Oxford comma: 'ships, planes, and hovercraft' where it aids comprehension
Treat company names as collective nouns, for example: 'Lille Industries are expanding'
Write hea...

Files:

  • docs/execplans/8-2-3-message-key-support.md
docs/**/*.{rs,md}

📄 CodeRabbit inference engine (docs/rust-doctest-dry-guide.md)

Every doctest should validate the public API of a crate from the perspective of an external user, treating each documentation test as a separate temporary crate that imports the library as an external dependency

Files:

  • docs/execplans/8-2-3-message-key-support.md
**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

**/*.md: Validate Markdown files using make markdownlint.
Run make fmt after any documentation changes to format all Markdown files and fix table markup.
Validate Mermaid diagrams in Markdown files by running make nixie.
Markdown paragraphs and bullet points must be wrapped at 80 columns.
Code blocks in Markdown must be wrapped at 120 columns.
Markdown tables and headings must not be wrapped.
Use dashes (-) for list bullets in Markdown.
Use GitHub-flavoured Markdown footnotes ([^1]) for references and footnotes in Markdown.

Files:

  • docs/execplans/8-2-3-message-key-support.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/execplans/8-2-3-message-key-support.md
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Run make check-fmt (cargo fmt --workspace -- --check) before committing to validate Rust formatting across the entire workspace without modifying files.
Run make lint (cargo clippy --workspace --all-targets --all-features -- -D warnings) before committing to lint every target with all features enabled and deny all Clippy warnings.
Run make test (cargo test --workspace) before committing to run the full workspace test suite.
Clippy warnings MUST be disallowed. Fix any warnings emitted during tests in the code itself rather than silencing them.
Use make fmt (cargo fmt --workspace) to apply formatting fixes reported by the formatter check.
Where a Rust function is too long, extract meaningfully named helper functions adhering to separation of concerns and CQRS.
Where a Rust function has too many parameters, group related parameters in meaningfully named structs.
Where a Rust function is returning a large error, consider using Arc to reduce the amount of data returned.
Write unit and behavioural tests for new Rust functionality. Run both before and after making any change.
Every Rust module must begin with a module level (//!) comment explaining the module's purpose and utility.
Document public Rust APIs using Rustdoc comments (///) so documentation can be generated with cargo doc.
Prefer immutable data and avoid unnecessary mut bindings in Rust code.
Avoid unsafe code in Rust unless absolutely necessary, and document any usage clearly with a 'SAFETY' comment.
Place function attributes after doc comments in Rust code.
Do not use return in single-line Rust functions.
Use predicate functions for Rust conditional criteria with more than two branches.
Lints must not be silenced in Rust except as a last resort. Lint rule suppressions must be tightly scoped and include a clear reason.
Use concat!() to combine long string literals in Rust rather than escaping newlines with a backslash.
Prefer single-line versions of Rust functions where...

Files:

  • src/message_assembler/state.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • src/message_assembler/state.rs
🧠 Learnings (3)
📚 Learning: 2026-01-03T17:44:28.792Z
Learnt from: leynos
Repo: leynos/wireframe PR: 412
File: tests/streaming_request.rs:85-107
Timestamp: 2026-01-03T17:44:28.792Z
Learning: In Rust code (e.g., the wireframe project), avoid direct indexing like vec[0] because it can panic. Prefer safe accessors that return Option, such as vec.get(0), vec.first(), or patterns that handle Option safely (e.g., match or if let Some). This should be applied across Rust files, including tests (e.g., tests/streaming_request.rs), regardless of prior length checks, to ensure safety and avoid panics from out-of-bounds access.

Applied to files:

  • src/message_assembler/state.rs
📚 Learning: 2026-01-08T08:16:31.989Z
Learnt from: leynos
Repo: leynos/wireframe PR: 411
File: src/client/tests/lifecycle.rs:0-0
Timestamp: 2026-01-08T08:16:31.989Z
Learning: In Rust, apply #[expect(lint, reason = "...")] only to code that actually triggers the specified lint. If the code does not trigger the lint, Clippy will fail with 'unfulfilled lint expectation' errors, which is intentional to prevent stale suppressions. Do not add #[expect] attributes proactively or to code that doesn't trigger the lint, even if similar code elsewhere requires suppression. This guideline should be applied broadly to Rust source files across the repository.

Applied to files:

  • src/message_assembler/state.rs
📚 Learning: 2026-01-08T20:33:24.570Z
Learnt from: leynos
Repo: leynos/wireframe PR: 417
File: src/codec/error_tests.rs:152-152
Timestamp: 2026-01-08T20:33:24.570Z
Learning: Guideline: In Rust, when using contains with a single-character pattern, prefer a char literal (e.g., 'a') over a string literal (e.g., "a") to satisfy the Clippy lint single_char_pattern, which flags single-character strings as less efficient. Apply this across Rust source files (all .rs) to improve efficiency and lint compliance. Example: use s.contains('a') instead of s.contains("a"); note that str::contains can take a char, making the char literal both correct and more efficient.

Applied to files:

  • src/message_assembler/state.rs
🧬 Code graph analysis (1)
src/message_assembler/state.rs (1)
src/message_assembler/series.rs (2)
  • message_key (108-108)
  • from_first_frame (96-104)
🪛 LanguageTool
docs/execplans/8-2-3-message-key-support.md

[uncategorized] ~13-~13: Use a comma before ‘so’ if it connects two independent clauses (unless they are closely connected and short).
Context: ...mbler` module needs multiplexing support so protocol implementations can process in...

(COMMA_COMPOUND_SENTENCE_2)


[style] ~266-~266: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...mbly.rswithMessageAssemblyWorld. - Create tests/steps/message_assembly_steps.rs`...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[uncategorized] ~345-~345: Loose punctuation mark.
Context: ...leaved assembly pass. - make check-fmt, make lint, and make test exit with ...

(UNLIKELY_OPENING_PUNCTUATION)

🪛 markdownlint-cli2 (0.18.1)
docs/execplans/8-2-3-message-key-support.md

280-280: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


286-286: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


291-291: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


296-296: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


301-301: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


307-307: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


312-312: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


316-316: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


323-323: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


328-328: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


378-378: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)


429-429: Code block style
Expected: fenced; Actual: indented

(MD046, code-block-style)

🔍 Remote MCP Deepwiki

Additional relevant facts to aid review

  • Repository context: wireframe is a Rust framework with actor-per-connection design, length-prefixed framing, bincode serializer, PushHandle/session registry, and a layered testing infra including rstest and Cucumber BDD.

  • Test harness & expectations:

    • Project uses extensive automated checks (format, clippy, tests) in CI; PR must pass make fmt, make lint, cargo test. CI enforces zero Clippy warnings and formatting.
    • wireframe_testing provides helpers (drive_with_bincode, duplex streams, logger fixture) used by existing tests and new Cucumber world/steps; new tests should follow these patterns.
  • Public API and design surface impacted by this PR:

    • crate exposes WireframeServer/WireframeApp, PushHandle, SessionRegistry, FrameProcessor, Serializer; re-exports in lib.rs are central to consumer usage — new message_assembler types re-exported at crate root will be part of the public surface to review for compatibility.
  • Testing expectations for message-assembly changes:

    • The project already exercises correlation IDs, streaming responses, fragmentation plans, and prioritized push queues in tests and Cucumber features; new MessageSeries/MessageAssemblyState additions must be covered by unit tests and BDD feature scenarios (the PR adds both). Use wireframe_testing helpers and Cucumber world patterns for consistent behavior.
  • Operational constraints and production concerns relevant to review:

    • Wireframe enforces max message size, reassembly timeouts, and per-connection rate limiting (leaky-bucket). Confirm MessageAssemblyState respects and enforces these existing invariants (size/enforced limits, timeout-driven purge semantics) to match project resilience patterns.

Files/areas to focus review on (based on repo docs and PR summary):

  • message_assembler/series.rs and state.rs: correctness of sequence tracking, overflow handling, completion transition, and error variants consistency with documented error taxonomy.
  • message_assembler/error.rs: clarity and data carried by error enums, Display impls, and conversions used by state logic.
  • lib.rs re-exports: ensure intended public API surface and documentation comments.
  • Tests: series_tests.rs, state_tests.rs, and Cucumber feature/world/steps additions — verify they exercise edge cases (overflow, missing first frame, duplicate first frame, size limits, expiry) and integrate with wireframe_testing utilities.

References:

  • Repository docs and testing/architecture overview used: Deepwiki pages for leynos/wireframe (Overview, Getting Started, Core Architecture, Testing, Roadmap).
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Sourcery review
  • GitHub Check: build-test
🔇 Additional comments (11)
docs/execplans/8-2-3-message-key-support.md (1)

1-107: Well-structured exec plan with clear acceptance criteria.

The document follows a sensible living-document format with constraints, tolerances, risks, progress tracking, and decision log. The Mermaid diagram effectively illustrates interleaved message handling.

src/message_assembler/state.rs (10)

23-104: Well-designed parameter object with validation.

FirstFrameInput cleanly groups frame components and validates header consistency. The error variants provide clear diagnostic information.


106-130: Clean private helper struct.

PartialAssembly encapsulates assembly state appropriately. The accumulated_len method correctly tracks body size, which integrates with the upfront metadata-inclusive size validation in accept_first_frame_at.


132-179: Clean public API for assembled messages.

AssembledMessage provides a well-documented interface with appropriate derives and #[must_use] annotations.


181-254: Solid state manager foundation.

The struct design with NonZeroUsize for the size limit prevents misconfiguration. The doctest provides a clear end-to-end usage example.


284-328: Correct early size validation including declared total.

The size check at lines 298–308 correctly prefers total_body_len when available and includes metadata, ensuring oversized assemblies are rejected upfront. The single-frame fast path is a sensible optimization.


364-390: Robust overflow handling in size check.

The use of checked_add with a fallback to usize::MAX for the error is a sound approach to prevent integer overflow exploits.


462-483: Safe timeout handling with saturating_duration_since.

The use of saturating_duration_since guards against clock monotonicity issues. Returning evicted keys enables callers to observe and log timeouts.


485-488: Simple and correct.


347-362: No issues found. Both MissingSequence and ContinuationBodyLengthMismatch exist in the MessageSeriesError enum and are correctly referenced in this function. All unrecoverable variants are matched, with recoverable variants (DuplicateFrame, SequenceMismatch, SeriesComplete) correctly omitted.


401-460: Well-structured continuation handling with proper cleanup.

The use of Entry::Occupied is idiomatic. Assembly removal on unrecoverable errors prevents resource leaks. The body length validation at lines 411–420 adds defensive protection against malformed frames.

Sequence overflow on the final frame is handled correctly: validate_and_advance_sequence treats overflow as an error only when more frames are expected (!is_last), allowing the message to complete without error when overflow occurs on the final frame.

Comment thread docs/execplans/8-2-3-message-key-support.md Outdated
Comment thread docs/execplans/8-2-3-message-key-support.md Outdated
Comment thread docs/execplans/8-2-3-message-key-support.md Outdated
Comment thread docs/execplans/8-2-3-message-key-support.md Outdated
Comment thread src/message_assembler/state.rs
…acking and validations

Introduce multiplexing in the MessageAssembler module to process interleaved frame streams from multiple logical messages.

- Add MessageSeries and related status/error types for tracking sequences and continuity (ordering, duplicates, missing frames).
- Add MessageAssemblyState to manage concurrent assemblies keyed by MessageKey.
- Implement checked_increment() for FrameSequence.
- Extract FirstFrameInput and AssembledMessage types into dedicated module to reduce file size.
- Add thorough unit and integration tests covering error cases and size limits.
- Update documentation to describe the new message key support and assembly process.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.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: 4

🤖 Fix all issues with AI agents
In @src/message_assembler/state_tests.rs:
- Around line 29-164: Tests duplicate FirstFrameHeader and
ContinuationFrameHeader construction; add small helper factories to reduce
boilerplate and replace inline constructors with them. Implement two helpers
named first_frame_header(key: u64, metadata_len: usize, body_len: usize,
is_last: bool) -> FirstFrameHeader and continuation_header(key: u64, sequence:
u32, body_len: usize, is_last: bool) -> ContinuationFrameHeader, then update
tests (e.g., state_tracks_single_message_assembly,
state_tracks_multiple_interleaved_messages,
state_rejects_continuation_without_first_frame,
state_rejects_duplicate_first_frame) to call first_frame_header(...) and
continuation_header(...) instead of manually building the structs.
- Around line 22-27: Convert the helper function state_with_defaults() into an
rstest fixture by annotating it with #[fixture] and returning
MessageAssemblyState so tests can accept it as a parameter; update tests that
call state_with_defaults() to take a parameter named state_with_defaults:
MessageAssemblyState (or rename locally) and use that value instead of calling
the function; ensure you add the rstest import (use rstest::rstest and/or
rstest::fixture as needed) so the fixture compiles and retains the same
initialization (NonZeroUsize::new(1024) and Duration::from_secs(30)) used in
MessageAssemblyState::new.
- Around line 166-241: Add an extra optional total_body_len to SizeLimitCase and
a new rstest case that sets total_body_len: Some(value) larger than the
configured limit so the first frame is rejected early; update the construction
of FirstFrameHeader in state_enforces_size_limit to set total_body_len:
params.total_body_len and adjust expected_attempted for the new case to be the
declared total (or the actual attempted amount you expect), keeping the rest of
the test flow the same so accept_first_frame triggers
MessageAssemblyError::MessageTooLarge when total_body_len exceeds the
NonZeroUsize limit.
- Around line 1-303: Add a new unit test that verifies a final continuation
frame with sequence u32::MAX completes: create a MessageAssemblyState, accept a
FirstFrameInput for MessageKey(1) (use FirstFrameHeader/FirstFrameInput) for a
non-final first frame, then build a ContinuationFrameHeader with sequence
Some(FrameSequence(u32::MAX)) and is_last = true and call
accept_continuation_frame; assert it returns Ok(Some(AssembledMessage)) with the
expected key/body and that buffered_count() drops to 0. Use the same helper
types (MessageAssemblyState, FirstFrameHeader, FirstFrameInput,
ContinuationFrameHeader, FrameSequence, MessageKey, accept_first_frame,
accept_continuation_frame) as in existing tests and follow the pattern of
series_accepts_final_fragment_at_max_index().
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6190b49 and 39861f4.

📒 Files selected for processing (6)
  • docs/execplans/8-2-3-message-key-support.md
  • src/message_assembler/mod.rs
  • src/message_assembler/state.rs
  • src/message_assembler/state_tests.rs
  • src/message_assembler/types.rs
  • tests/steps/message_assembly_steps.rs
🧰 Additional context used
📓 Path-based instructions (4)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Run make check-fmt (cargo fmt --workspace -- --check) before committing to validate Rust formatting across the entire workspace without modifying files.
Run make lint (cargo clippy --workspace --all-targets --all-features -- -D warnings) before committing to lint every target with all features enabled and deny all Clippy warnings.
Run make test (cargo test --workspace) before committing to run the full workspace test suite.
Clippy warnings MUST be disallowed. Fix any warnings emitted during tests in the code itself rather than silencing them.
Use make fmt (cargo fmt --workspace) to apply formatting fixes reported by the formatter check.
Where a Rust function is too long, extract meaningfully named helper functions adhering to separation of concerns and CQRS.
Where a Rust function has too many parameters, group related parameters in meaningfully named structs.
Where a Rust function is returning a large error, consider using Arc to reduce the amount of data returned.
Write unit and behavioural tests for new Rust functionality. Run both before and after making any change.
Every Rust module must begin with a module level (//!) comment explaining the module's purpose and utility.
Document public Rust APIs using Rustdoc comments (///) so documentation can be generated with cargo doc.
Prefer immutable data and avoid unnecessary mut bindings in Rust code.
Avoid unsafe code in Rust unless absolutely necessary, and document any usage clearly with a 'SAFETY' comment.
Place function attributes after doc comments in Rust code.
Do not use return in single-line Rust functions.
Use predicate functions for Rust conditional criteria with more than two branches.
Lints must not be silenced in Rust except as a last resort. Lint rule suppressions must be tightly scoped and include a clear reason.
Use concat!() to combine long string literals in Rust rather than escaping newlines with a backslash.
Prefer single-line versions of Rust functions where...

Files:

  • tests/steps/message_assembly_steps.rs
  • src/message_assembler/state_tests.rs
  • src/message_assembler/types.rs
  • src/message_assembler/state.rs
  • src/message_assembler/mod.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • tests/steps/message_assembly_steps.rs
  • src/message_assembler/state_tests.rs
  • src/message_assembler/types.rs
  • src/message_assembler/state.rs
  • src/message_assembler/mod.rs
docs/**/*.md

📄 CodeRabbit inference engine (docs/contents.md)

Follow the documentation style guide conventions when writing project documentation

Write official documentation with separate guides for 'Duplex Messaging & Pushes', 'Streaming Responses', and 'Message Fragmentation', each including runnable examples and explaining relevant concepts and APIs

docs/**/*.md: Use markdown files within the docs/ directory as a knowledge base and source of truth for project requirements, dependency choices, and architectural decisions.
When new decisions are made, requirements change, libraries are added/removed, or architectural patterns evolve, proactively update the relevant file(s) in the docs/ directory to reflect the latest state. Ensure documentation remains accurate and current.
Documentation must use en-GB-oxendict spelling and grammar ('-ize' / '-yse' / '-our'), with the exception of the filename LICENSE.
Reference the documentation style guide provided at docs/documentation-style-guide.md for documentation writing standards.

Documentation describing multi-packet streaming MUST reflect the tuple-based API and explain helper constructors that prepare the channel pair

Replace or retire documentation that refers to Cucumber, including guides and examples in docs/

docs/**/*.md: Use British English based on the Oxford English Dictionary (en-GB-oxendict), including: suffix -ize in words like 'realize' and 'organization', suffix -lyse in words like 'analyse' and 'paralyse', suffix -our in words like 'colour' and 'behaviour', suffix -re in words like 'calibre' and 'centre', double 'l' in words like 'cancelled' and 'counsellor', maintain the 'e' in words like 'likeable' and 'liveable', suffix -ogue in words like 'analogue' and 'catalogue'
The word 'outwith' is acceptable in documentation
Keep US spelling when used in an API, for example 'color'
Use the Oxford comma: 'ships, planes, and hovercraft' where it aids comprehension
Treat company names as collective nouns, for example: 'Lille Industries are expanding'
Write hea...

Files:

  • docs/execplans/8-2-3-message-key-support.md
docs/**/*.{rs,md}

📄 CodeRabbit inference engine (docs/rust-doctest-dry-guide.md)

Every doctest should validate the public API of a crate from the perspective of an external user, treating each documentation test as a separate temporary crate that imports the library as an external dependency

Files:

  • docs/execplans/8-2-3-message-key-support.md
**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

**/*.md: Validate Markdown files using make markdownlint.
Run make fmt after any documentation changes to format all Markdown files and fix table markup.
Validate Mermaid diagrams in Markdown files by running make nixie.
Markdown paragraphs and bullet points must be wrapped at 80 columns.
Code blocks in Markdown must be wrapped at 120 columns.
Markdown tables and headings must not be wrapped.
Use dashes (-) for list bullets in Markdown.
Use GitHub-flavoured Markdown footnotes ([^1]) for references and footnotes in Markdown.

Files:

  • docs/execplans/8-2-3-message-key-support.md

⚙️ CodeRabbit configuration file

**/*.md: * Avoid 2nd person or 1st person pronouns ("I", "you", "we")

  • Use en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Headings must not be wrapped.
  • Documents must start with a level 1 heading
  • Headings must correctly increase or decrease by no more than one level at a time
  • Use GitHub-flavoured Markdown style for footnotes and endnotes.
  • Numbered footnotes must be numbered by order of appearance in the document.

Files:

  • docs/execplans/8-2-3-message-key-support.md
🧠 Learnings (4)
📚 Learning: 2026-01-03T17:44:28.792Z
Learnt from: leynos
Repo: leynos/wireframe PR: 412
File: tests/streaming_request.rs:85-107
Timestamp: 2026-01-03T17:44:28.792Z
Learning: In Rust code (e.g., the wireframe project), avoid direct indexing like vec[0] because it can panic. Prefer safe accessors that return Option, such as vec.get(0), vec.first(), or patterns that handle Option safely (e.g., match or if let Some). This should be applied across Rust files, including tests (e.g., tests/streaming_request.rs), regardless of prior length checks, to ensure safety and avoid panics from out-of-bounds access.

Applied to files:

  • tests/steps/message_assembly_steps.rs
  • src/message_assembler/state_tests.rs
  • src/message_assembler/types.rs
  • src/message_assembler/state.rs
  • src/message_assembler/mod.rs
📚 Learning: 2026-01-08T08:16:31.989Z
Learnt from: leynos
Repo: leynos/wireframe PR: 411
File: src/client/tests/lifecycle.rs:0-0
Timestamp: 2026-01-08T08:16:31.989Z
Learning: In Rust, apply #[expect(lint, reason = "...")] only to code that actually triggers the specified lint. If the code does not trigger the lint, Clippy will fail with 'unfulfilled lint expectation' errors, which is intentional to prevent stale suppressions. Do not add #[expect] attributes proactively or to code that doesn't trigger the lint, even if similar code elsewhere requires suppression. This guideline should be applied broadly to Rust source files across the repository.

Applied to files:

  • tests/steps/message_assembly_steps.rs
  • src/message_assembler/state_tests.rs
  • src/message_assembler/types.rs
  • src/message_assembler/state.rs
  • src/message_assembler/mod.rs
📚 Learning: 2026-01-08T20:33:24.570Z
Learnt from: leynos
Repo: leynos/wireframe PR: 417
File: src/codec/error_tests.rs:152-152
Timestamp: 2026-01-08T20:33:24.570Z
Learning: Guideline: In Rust, when using contains with a single-character pattern, prefer a char literal (e.g., 'a') over a string literal (e.g., "a") to satisfy the Clippy lint single_char_pattern, which flags single-character strings as less efficient. Apply this across Rust source files (all .rs) to improve efficiency and lint compliance. Example: use s.contains('a') instead of s.contains("a"); note that str::contains can take a char, making the char literal both correct and more efficient.

Applied to files:

  • tests/steps/message_assembly_steps.rs
  • src/message_assembler/state_tests.rs
  • src/message_assembler/types.rs
  • src/message_assembler/state.rs
  • src/message_assembler/mod.rs
📚 Learning: 2026-01-08T18:03:59.395Z
Learnt from: leynos
Repo: leynos/wireframe PR: 417
File: tests/steps/codec_error_steps.rs:58-62
Timestamp: 2026-01-08T18:03:59.395Z
Learning: In the wireframe repo, test step functions that always succeed should not return TestResult, because clippy::unnecessary_wraps will fire and cannot be suppressed in tests. Do not use #[allow(...)] or #[expect(clippy::unnecessary_wraps)] to silence it. Refactor such steps to return () or a type that does not trigger the lint, or move these steps to non-test code or a helper that isn’t linted the same way. This guidance applies to all files under tests/steps that contain similar patterns.

Applied to files:

  • tests/steps/message_assembly_steps.rs
🧬 Code graph analysis (4)
src/message_assembler/state_tests.rs (2)
src/message_assembler/state.rs (3)
  • new (31-38)
  • new (114-120)
  • buffered_count (353-353)
src/message_assembler/types.rs (5)
  • new (69-91)
  • new (118-124)
  • message_key (128-128)
  • metadata (132-132)
  • body (136-136)
src/message_assembler/types.rs (1)
src/message_assembler/state.rs (2)
  • new (31-38)
  • new (114-120)
src/message_assembler/state.rs (3)
src/message_assembler/types.rs (3)
  • new (69-91)
  • new (118-124)
  • body (136-136)
tests/worlds/message_assembly.rs (4)
  • new (69-78)
  • accept_first_frame (121-138)
  • purge_expired (213-222)
  • buffered_count (226-230)
src/message_assembler/series.rs (1)
  • from_first_frame (96-104)
src/message_assembler/mod.rs (3)
src/message_assembler/header.rs (1)
  • header (152-152)
tests/worlds/fragment/mod.rs (1)
  • series (180-184)
src/extractor.rs (1)
  • state (82-90)
🪛 LanguageTool
docs/execplans/8-2-3-message-key-support.md

[style] ~266-~266: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...mbly.rswithMessageAssemblyWorld. - Create tests/steps/message_assembly_steps.rs`...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)


[uncategorized] ~365-~365: Loose punctuation mark.
Context: ...leaved assembly pass. - make check-fmt, make lint, and make test exit with ...

(UNLIKELY_OPENING_PUNCTUATION)

🔍 Remote MCP Deepwiki

Additional relevant facts for reviewing this PR

  • Project architecture & invariants:

    • Wireframe uses an actor-per-connection model; per-connection tasks must be panic-isolated and respect cancellation/timeout semantics ( Overview / Core Architecture).
    • The framework already enforces max-message-size, reassembly timeouts, and per-connection rate limiting (leaky-bucket) as core DoS protections — new MessageAssemblyState must integrate with these (size/timeout) invariants ( Production Deployment / Resilience sections).
  • Public API and compatibility surface:

    • lib.rs re-exports form the crate public surface used by consumers; adding MessageSeries, MessageAssemblyState, AssembledMessage and error enums makes these types part of the public API and requires stable docs/visibility consideration ( 7 API Reference and API Surface).
  • Testing & CI expectations:

    • The repo uses layered testing with rstest unit tests and Cucumber BDD; behavioral features and worlds are executed in CI when the advanced-tests feature is enabled — the PR added both unit tests and Cucumber worlds/features which must follow existing test patterns and use wireframe_testing helpers (drive_with_bincode, duplex streams) ( Testing / Testing Utilities).
    • CI enforces formatting, clippy (zero-warning) and full test runs via make fmt / make lint / cargo test; new public types & docs should pass these gates ( CI/CD and Development Workflow).
  • Observability & production concerns:

    • The project collects operational metrics (connection counts, reassembly errors, frame counts). Message assembly errors (MessageSeriesError / MessageAssemblyError) will be useful to map to existing metrics (e.g., reassembly_errors_total) or logging to preserve operational diagnosability ( Observability and Monitoring / Production Deployment).
  • Testing patterns to follow in review:

    • Tests should use existing fixtures and wireframe_testing helpers, avoid duplicating header/frame builder boilerplate (the repo already prefers helpers / rstest fixtures) and use tokio time-pausing where appropriate for deterministic timeout tests ( Testing / Testing Utilities).

Sources:

  • Repo docs & architecture (pages listed in wiki): and selected content: Overview, Core Architecture, Production Deployment, Testing, API Reference, CI/CD & Development Workflow ().
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Sourcery review
  • GitHub Check: build-test
🔇 Additional comments (29)
docs/execplans/8-2-3-message-key-support.md (3)

1-107: Well-structured ExecPlan document.

The document follows the expected living-document format with clear success criteria, constraints, tolerances, risks, progress tracking, and decision log. The status is marked COMPLETE, which aligns with the PR objectives.


144-192: Mermaid diagram effectively illustrates multiplexing flow.

The sequence diagram clearly shows how MessageAssemblyState manages interleaved frame streams from two concurrent message keys, including the cleanup path via purge_expired(). The preceding description (lines 144-146) aids accessibility.


394-455: Interfaces section accurately documents the public API.

The expected public interfaces match the implementation described in the PR. This serves as a useful reference for validation and acceptance criteria.

tests/steps/message_assembly_steps.rs (4)

8-21: Boundary adapters and assertion helper are well-designed.

The to_key and to_seq functions convert primitives to domain types at the step boundary, addressing the primitive obsession concern raised in PR comments. The assert_condition helper reduces duplication in Then steps whilst maintaining clear error messages.


27-52: Given steps correctly use parameter objects and domain types.

The step definitions use FirstFrameParams builders with fluent API (with_metadata, final_frame), and domain type conversion occurs at the boundary via to_key. These steps appropriately return () since they always succeed, avoiding the clippy::unnecessary_wraps lint.


58-115: When steps correctly propagate TestResult.

All When steps return TestResult, ensuring errors surface immediately. The when_another_first_frame step (lines 99-107) correctly returns the result from world.accept_first_frame(), addressing the concern raised in PR comments about discarded results.


129-165: Lint expectations correctly applied with reasons.

The #[expect(clippy::needless_pass_by_value, reason = "...")] attributes on then_completes_with_body and then_key_completes are correctly scoped with clear reasons explaining the Cucumber framework requirement. This follows the guideline to apply #[expect] only to code that triggers the specific lint.

src/message_assembler/types.rs (4)

1-9: Module documentation clearly explains purpose.

The module-level comment explains the purpose and notes the extraction from state.rs to meet the 400-line file limit, providing useful context for maintainers.


31-92: FirstFrameInput parameter object is well-designed.

The struct groups related first-frame parameters as suggested in PR comments. The validation in new() correctly checks that header-declared lengths match actual payload sizes, returning typed errors on mismatch. The ownership model (borrowed header, owned metadata, borrowed body) is appropriate for the use case.


41-60: Error enum follows established patterns.

FirstFrameInputError uses thiserror derive with clear error messages and carries diagnostic fields (header_len, actual_len) enabling callers to inspect mismatch details.


108-141: AssembledMessage provides clean encapsulation.

Private fields with public accessors follow good encapsulation practices. The #[must_use] attributes on all methods encourage proper handling of return values. The into_body() method enables zero-copy ownership transfer when the caller needs the body bytes.

src/message_assembler/state.rs (7)

1-19: Module documentation and imports are appropriate.

The module-level comment clearly describes the purpose. The imports are well-organised with standard library types first, followed by local module imports.


21-45: PartialAssembly provides clean internal encapsulation.

The private struct appropriately encapsulates the in-progress assembly state with focused helper methods. The separation from public API is well-designed.


106-120: Constructor uses appropriate type constraints.

Using NonZeroUsize for max_message_size prevents misconfiguration with zero limits at compile time rather than runtime.


150-194: First-frame acceptance logic addresses PR feedback.

The implementation prefers header.total_body_len when present (line 165) and includes metadata in size accounting (line 166), addressing the functional correctness suggestions from PR comments. The single-frame message path correctly returns immediately without buffering.


213-256: Helper extraction improves readability.

The is_unrecoverable_continuity_error and check_size_limit helpers address the PR comment about method size. The const fn on the error classifier enables compile-time evaluation. The size check uses checked_add to prevent arithmetic overflow.


267-326: Continuation frame handling is robust.

The method validates body length against header (lines 278-286), uses idiomatic Entry API, properly cleans up assemblies on unrecoverable errors, and correctly handles completion status. The helper extractions have reduced complexity compared to the concern raised in PR comments.


328-349: Expiration purging is efficient and safe.

Using retain for bulk removal is efficient. The saturating_duration_since handles potential clock anomalies gracefully. Returning evicted keys enables operational logging and metrics.

src/message_assembler/mod.rs (3)

7-21: Module documentation clearly describes new features.

The updated module-level comments document message key multiplexing (8.2.3) and continuity validation (8.2.4) with references to the relevant types. The bullet points clearly enumerate what the MessageSeries type detects.


23-42: Module structure and re-exports are well-organised.

The public modules (error, series, state, types) expose the new functionality, whilst header remains private with selected re-exports. The flat re-exports at module level (lines 31-42) provide convenient access without requiring deep imports.


44-121: MessageAssembler trait and examples remain stable.

The trait definition is unchanged, satisfying the ExecPlan constraint that the existing trait signature must not change. The examples demonstrate realistic usage with the bytes crate.

src/message_assembler/state_tests.rs (8)

1-20: Module structure is sound.

The module doc comment and imports are correctly organised. All imported types appear necessary for the tests.


29-59: Test logic is correct.

The test properly validates the single-message assembly flow, checking completion, body concatenation, and buffer cleanup. The use of .expect() with descriptive messages follows guidelines.


61-118: Interleaving test is well-constructed.

This test validates the core multiplexing capability: two independent message streams processed concurrently with correct isolation and completion. The staged buffered_count assertions provide good visibility into state transitions.


120-139: Error case coverage is appropriate.

The test correctly validates rejection of orphan continuation frames and verifies the expected error variant with the correct message key.


141-164: Duplicate detection test is sound.

Correctly validates that the state rejects a second first frame for the same message key, preserving assembly integrity.


243-272: Deterministic timeout test is well-designed.

Using accept_first_frame_at and purge_expired_at with explicit Instant values avoids real-time dependencies. This is the correct approach per project guidelines for timeout testing.


274-296: Single-frame immediate completion is correctly tested.

Validates that is_last: true on a first frame returns the assembled message immediately without buffering.


298-303: into_body conversion test is sufficient.

Simple but necessary coverage for the ownership-taking accessor.

Comment thread src/message_assembler/state_tests.rs
Comment thread src/message_assembler/state_tests.rs
Comment thread src/message_assembler/state_tests.rs Outdated
Comment thread src/message_assembler/state_tests.rs
- Add series_accepts_final_continuation_at_max_sequence test to verify
  that u32::MAX is accepted as a valid final sequence number
- Convert state_with_defaults helper to an rstest fixture for better
  integration with the parameterized test framework
- Extract header factory helpers (first_header, first_header_with_total,
  continuation_header) to reduce test setup duplication
- Add state_enforces_size_limit_on_first_frame test to verify early
  rejection based on declared total_body_len
- Refactor existing tests to use the new helpers and fixture pattern

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

This comment was marked as outdated.

@leynos leynos changed the title Implement message key support for multiplexing Refactor message assembly test API and multiplexing support Jan 13, 2026
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

🤖 Fix all issues with AI agents
In @src/message_assembler/series_tests.rs:
- Around line 13-40: Convert the three helper factory functions (first_header,
continuation_header, continuation_header_untracked) into rstest fixtures to
match state_tests.rs patterns or move them into a shared test utility module if
they’ll be reused across files; replace direct calls to these functions in tests
with the fixture parameters (or import from the shared module), ensure fixture
signatures return the corresponding types (FirstFrameHeader and
ContinuationFrameHeader) and preserve the existing field values (message_key,
metadata_len/body_len/total_body_len, sequence, is_last) so tests continue to
behave identically.

In @src/message_assembler/state_tests.rs:
- Around line 253-276: Add this new scenario into the existing parameterized
test instead of a separate test: update the SizeLimitCase struct to include a
total_body_len field (optionally Option<usize>), extend the existing
state_enforces_size_limit test to build the FirstFrameInput using that
total_body_len when present, and remove the standalone
state_enforces_size_limit_on_first_frame test; use the same helpers
(first_header_with_total, FirstFrameInput::new) and the same assertion pattern
against MessageAssemblyState::accept_first_frame and
MessageAssemblyError::MessageTooLarge to keep behavior identical.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39861f4 and 6641369.

📒 Files selected for processing (2)
  • src/message_assembler/series_tests.rs
  • src/message_assembler/state_tests.rs
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Run make check-fmt (cargo fmt --workspace -- --check) before committing to validate Rust formatting across the entire workspace without modifying files.
Run make lint (cargo clippy --workspace --all-targets --all-features -- -D warnings) before committing to lint every target with all features enabled and deny all Clippy warnings.
Run make test (cargo test --workspace) before committing to run the full workspace test suite.
Clippy warnings MUST be disallowed. Fix any warnings emitted during tests in the code itself rather than silencing them.
Use make fmt (cargo fmt --workspace) to apply formatting fixes reported by the formatter check.
Where a Rust function is too long, extract meaningfully named helper functions adhering to separation of concerns and CQRS.
Where a Rust function has too many parameters, group related parameters in meaningfully named structs.
Where a Rust function is returning a large error, consider using Arc to reduce the amount of data returned.
Write unit and behavioural tests for new Rust functionality. Run both before and after making any change.
Every Rust module must begin with a module level (//!) comment explaining the module's purpose and utility.
Document public Rust APIs using Rustdoc comments (///) so documentation can be generated with cargo doc.
Prefer immutable data and avoid unnecessary mut bindings in Rust code.
Avoid unsafe code in Rust unless absolutely necessary, and document any usage clearly with a 'SAFETY' comment.
Place function attributes after doc comments in Rust code.
Do not use return in single-line Rust functions.
Use predicate functions for Rust conditional criteria with more than two branches.
Lints must not be silenced in Rust except as a last resort. Lint rule suppressions must be tightly scoped and include a clear reason.
Use concat!() to combine long string literals in Rust rather than escaping newlines with a backslash.
Prefer single-line versions of Rust functions where...

Files:

  • src/message_assembler/state_tests.rs
  • src/message_assembler/series_tests.rs

⚙️ CodeRabbit configuration file

**/*.rs: * Seek to keep the cognitive complexity of functions no more than 9.

  • Adhere to single responsibility and CQRS
  • Place function attributes after doc comments.
  • Do not use return in single-line functions.
  • Move conditionals with >2 branches into a predicate function.
  • Avoid unsafe unless absolutely necessary.
  • Every module must begin with a //! doc comment that explains the module's purpose and utility.
  • Comments and docs must follow en-GB-oxendict (-ize / -yse / -our) spelling and grammar
  • Lints must not be silenced except as a last resort.
    • #[allow] is forbidden.
    • Only narrowly scoped #[expect(lint, reason = "...")] is allowed.
    • No lint groups, no blanket or file-wide suppression.
    • Include FIXME: with link if a fix is expected.
  • Where code is only used by specific features, it must be conditionally compiled or a conditional expectation for unused_code applied.
  • Use rstest fixtures for shared setup and to avoid repetition between tests.
  • Replace duplicated tests with #[rstest(...)] parameterised cases.
  • Prefer mockall for mocks/stubs.
  • Prefer .expect() over .unwrap() in tests.
  • .expect() and .unwrap() are forbidden outside of tests. Errors must be propagated.
  • Ensure that any API or behavioural changes are reflected in the documentation in docs/
  • Ensure that any completed roadmap steps are recorded in the appropriate roadmap in docs/
  • Files must not exceed 400 lines in length
    • Large modules must be decomposed
    • Long match statements or dispatch tables should be decomposed by domain and collocated with targets
    • Large blocks of inline data (e.g., test fixtures, constants or templates) must be moved to external files and inlined at compile-time or loaded at run-time.
  • Environment access (env::set_var and env::remove_var) are always unsafe in Rust 2024 and MUST be marked as such
    • For testing of functionality depending upon environment variables, dependency injection and...

Files:

  • src/message_assembler/state_tests.rs
  • src/message_assembler/series_tests.rs
🧠 Learnings (3)
📚 Learning: 2026-01-03T17:44:28.792Z
Learnt from: leynos
Repo: leynos/wireframe PR: 412
File: tests/streaming_request.rs:85-107
Timestamp: 2026-01-03T17:44:28.792Z
Learning: In Rust code (e.g., the wireframe project), avoid direct indexing like vec[0] because it can panic. Prefer safe accessors that return Option, such as vec.get(0), vec.first(), or patterns that handle Option safely (e.g., match or if let Some). This should be applied across Rust files, including tests (e.g., tests/streaming_request.rs), regardless of prior length checks, to ensure safety and avoid panics from out-of-bounds access.

Applied to files:

  • src/message_assembler/state_tests.rs
  • src/message_assembler/series_tests.rs
📚 Learning: 2026-01-08T08:16:31.989Z
Learnt from: leynos
Repo: leynos/wireframe PR: 411
File: src/client/tests/lifecycle.rs:0-0
Timestamp: 2026-01-08T08:16:31.989Z
Learning: In Rust, apply #[expect(lint, reason = "...")] only to code that actually triggers the specified lint. If the code does not trigger the lint, Clippy will fail with 'unfulfilled lint expectation' errors, which is intentional to prevent stale suppressions. Do not add #[expect] attributes proactively or to code that doesn't trigger the lint, even if similar code elsewhere requires suppression. This guideline should be applied broadly to Rust source files across the repository.

Applied to files:

  • src/message_assembler/state_tests.rs
  • src/message_assembler/series_tests.rs
📚 Learning: 2026-01-08T20:33:24.570Z
Learnt from: leynos
Repo: leynos/wireframe PR: 417
File: src/codec/error_tests.rs:152-152
Timestamp: 2026-01-08T20:33:24.570Z
Learning: Guideline: In Rust, when using contains with a single-character pattern, prefer a char literal (e.g., 'a') over a string literal (e.g., "a") to satisfy the Clippy lint single_char_pattern, which flags single-character strings as less efficient. Apply this across Rust source files (all .rs) to improve efficiency and lint compliance. Example: use s.contains('a') instead of s.contains("a"); note that str::contains can take a char, making the char literal both correct and more efficient.

Applied to files:

  • src/message_assembler/state_tests.rs
  • src/message_assembler/series_tests.rs
🧬 Code graph analysis (2)
src/message_assembler/state_tests.rs (2)
src/message_assembler/types.rs (4)
  • new (69-91)
  • new (118-124)
  • metadata (132-132)
  • body (136-136)
src/message_assembler/state.rs (3)
  • new (31-38)
  • new (114-120)
  • buffered_count (353-353)
src/message_assembler/series_tests.rs (2)
src/message_assembler/series.rs (2)
  • from_first_frame (96-104)
  • expected_total (116-116)
tests/worlds/message_assembly.rs (1)
  • accept_continuation (174-193)
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Sourcery review
  • GitHub Check: build-test
🔇 Additional comments (21)
src/message_assembler/series_tests.rs (11)

1-11: LGTM!

Module documentation present and imports are well-organised. The imports cover all necessary types from the message assembler module.


42-60: LGTM!

Good coverage of the happy path for sequential continuation frames. The test correctly verifies both intermediate Incomplete status and final Complete status.


62-73: LGTM!

Correctly validates that continuations without sequence numbers bypass ordering checks whilst still completing the series.


75-88: LGTM!

Correctly validates rejection of continuations with mismatched message keys, including verification of both expected and found values in the error.


90-110: LGTM!

Correctly validates that sequence gaps are detected and reported with accurate expected/found values.


112-138: LGTM!

Good test for duplicate frame detection. The scenario of sending an old sequence number after advancing correctly triggers the DuplicateFrame error.


140-158: LGTM!

Correctly validates that attempts to continue an already-completed series are rejected with SeriesComplete.


160-179: LGTM!

This test addresses the sequence overflow concern raised in PR feedback. Correctly validates that a non-final frame at u32::MAX triggers SequenceOverflow since incrementing would overflow.


181-201: LGTM!

Excellent complementary test to the overflow case. This validates that u32::MAX is accepted as a valid final sequence number, addressing the PR feedback about the sequence overflow bug fix.


203-215: LGTM!

Correctly validates that expected_total() returns the declared total_body_len from the first frame header.


217-222: LGTM!

Concise and correct test for single-frame message completion.

src/message_assembler/state_tests.rs (10)

1-29: LGTM!

Good use of rstest fixtures for shared test setup. The NonZeroUsize::new(1024).expect("non-zero") is acceptable here as 1024 is a compile-time constant that cannot fail the non-zero check.


31-70: LGTM!

Header factory helpers are well-documented and reduce test setup duplication. The first_header_with_total variant specifically supports testing the early size-rejection behaviour mentioned in PR feedback.


76-102: LGTM!

Comprehensive happy-path test covering metadata handling, body assembly, and correct buffered count tracking through the assembly lifecycle.


104-139: LGTM!

Good validation of interleaved message assembly. The test verifies correct buffered count tracking as messages complete independently.

Optionally, add a test case completing messages in reverse order (message 2 before message 1) to ensure ordering independence.


141-155: LGTM!

Correctly validates rejection of orphan continuation frames and verifies the nested error structure.


157-174: LGTM!

Correctly validates rejection of duplicate first frames for the same message key.


176-251: LGTM!

Excellent use of parameterised testing with rstest and a parameter object (SizeLimitCase) to reduce duplication whilst testing both first-frame and continuation rejection paths. The verification of buffered_count == 0 confirms proper cleanup after size-limit violations.


278-301: LGTM!

Correctly validates the expiration and purge mechanism. The test clearly demonstrates time advancement past the timeout threshold and verification of evicted keys.


303-326: LGTM!

Correctly validates that single-frame messages bypass buffering and are returned immediately. Good verification that buffered_count remains zero.


328-333: LGTM!

Concise test for the consuming into_body() accessor on AssembledMessage.

Comment thread src/message_assembler/series_tests.rs Outdated
Comment thread src/message_assembler/state_tests.rs Outdated
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Jan 15, 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:

src/message_assembler/series_tests.rs

Comment on lines +161 to +179

fn series_detects_sequence_overflow() {
    let first = first_header(1, false);
    let mut series = MessageSeries::from_first_frame(&first);

    // Force next_sequence to u32::MAX
    series.force_next_sequence_for_tests(FrameSequence(u32::MAX));

    // Send a non-final frame at MAX
    let cont = ContinuationFrameHeader {
        message_key: MessageKey(1),
        sequence: Some(FrameSequence(u32::MAX)),
        body_len: 5,
        is_last: false,
    };
    assert!(matches!(
        series.accept_continuation(&cont),
        Err(MessageSeriesError::SequenceOverflow { .. })
    ));
}

❌ New issue: Code Duplication
The module contains 2 functions with similar structure: series_accepts_final_continuation_at_max_sequence,series_detects_sequence_overflow

@coderabbitai

This comment was marked as resolved.

…ate tests

- Create test_helpers.rs module with shared header factory functions
  (first_header, first_header_with_total, continuation_header,
  continuation_header_untracked) to reduce duplication across test files
- Update series_tests.rs to use shared helpers via local wrappers
- Update state_tests.rs to use shared helpers directly
- Merge state_enforces_size_limit_on_first_frame test case into the
  existing parameterized state_enforces_size_limit test by adding
  total_body_len field to SizeLimitCase struct

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

This comment was marked as outdated.

Merge series_detects_sequence_overflow and
series_accepts_final_continuation_at_max_sequence into a single
rstest-parameterised test using SequenceOverflowExpectation enum
to express expected outcomes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@leynos leynos changed the title Refactor message assembly test API and multiplexing support Refactor message assembly test API; multiplexing tests Jan 15, 2026
@leynos leynos merged commit 66f6170 into main Jan 16, 2026
6 checks passed
@leynos leynos deleted the terragon/feature/8-2-3-message-key-support-7qvxt8 branch January 16, 2026 01:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant