Skip to content

feat(trogon-decider-nats): add JetStream storage adapter#173

Merged
yordis merged 16 commits into
mainfrom
yordis/docs-unit-tests
May 21, 2026
Merged

feat(trogon-decider-nats): add JetStream storage adapter#173
yordis merged 16 commits into
mainfrom
yordis/docs-unit-tests

Conversation

@yordis

@yordis yordis commented May 21, 2026

Copy link
Copy Markdown
Member
  • Enable decider streams to use durable JetStream-backed persistence.
  • Preserve per-stream concurrency guarantees when multiple subjects share one stream.
  • Make the adapter behavior reviewable with local and live JetStream coverage.

@cursor

cursor Bot commented May 21, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Introduces a new JetStream-backed persistence adapter (stream append/read and snapshot KV ops) with optimistic concurrency semantics; correctness depends on NATS/JetStream edge-case handling. Also changes trogon-decider-runtime snapshot encode/decode error types to be generic, which may be a small breaking API surface for downstream crates.

Overview
Adds new trogon-decider-nats crate implementing a JetStream storage adapter for trogon-decider-runtime, including event stream append/read helpers with subject-based optimistic concurrency and header-based event envelopes, plus snapshot persistence in JetStream KV with optional checkpointing and conflict-aware upserts/deletes.

Updates trogon-decider-runtime snapshot codec errors (SnapshotEncodeError/SnapshotDecodeError and encode_snapshot/decode_snapshot) to carry typed payload errors instead of boxed errors, and tweaks the runtime crate docs accordingly. CI Rust workflow test job now sets FORCE_JAVASCRIPT_ACTIONS_TO_NODE24.

Reviewed by Cursor Bugbot for commit 497ebee. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Introduces a new NATS JetStream storage adapter crate (trogon-decider-nats) implementing stream append/read and snapshot persistence with OCC guards, and extends trogon-nats with new JetStream/kv/raw-message traits plus richer deterministic mocks for testing.

Changes

JetStream Abstractions and Test Infrastructure

Layer / File(s) Summary
JetStream Trait Abstractions & Re-exports
rsworkspace/crates/trogon-nats/src/jetstream/traits.rs, rsworkspace/crates/trogon-nats/src/jetstream/mod.rs
Adds async traits for stream info, raw-message-by-sequence, and KV operations with concrete implementations delegating to async-nats and public re-exports.
Publish Message & Stream Raw-message Mocks
rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs
MockJetStreamPublishMessage records published messages and returns queued ack/error outcomes or auto-incremented sequences; mocks expose stream-info and raw-message-by-sequence behavior.
KV Store Mock Enhancements & Call Recording
rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs
MockJetStreamKvStore gains per-call outcome queues for entry/get/update/create/delete, bucket name configuration, queued-create fallback, keys counter, and call-recording accessors.
Consumer Factory & Stream State Tracking
rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs
MockJetStreamConsumerFactory and MockJetStreamStream support configurable stream info and sequence-keyed raw messages and record raw-message calls.
Time Dependency Promotion
rsworkspace/crates/trogon-nats/Cargo.toml
Moves time from [dev-dependencies] into main [dependencies].

JetStream Storage Adapter for Decider Runtime

Layer / File(s) Summary
Project Setup & Core Type Definitions
rsworkspace/crates/trogon-decider-nats/Cargo.toml, rsworkspace/crates/trogon-decider-nats/src/lib.rs
New crate manifest and core types: StreamSubject, SubjectState, StreamSubjectResolver, JetStreamStoreError, and JetStreamStore constructor/accessors; exports snapshot_store and stream_store.
Event Stream Storage Core & Append
rsworkspace/crates/trogon-decider-nats/src/stream_store.rs
Defines trogon header constants, StreamStoreError, and append_stream publishing atomic batches with batch/sequence/commit headers and expected-last-subject-sequence OCC guards; classifies publish acknowledgements into structured results.
Stream Read, Range Iteration & Message Recording
rsworkspace/crates/trogon-decider-nats/src/stream_store.rs
Implements read_stream/read_stream_range, inclusive range iteration with gap skipping, subject filtering, message decoding (record_stream_message), header extraction, and related tests.
Snapshot Storage & Checkpoint Tracking
rsworkspace/crates/trogon-decider-nats/src/snapshot_store.rs
KV-backed snapshot persistence with deterministic snapshot keys, encode/decode helpers, position-aware upsert/delete idempotence, listing, and checkpoint read/write/conditional advance behavior with tests.
JetStreamStore Trait Implementations & Integration
rsworkspace/crates/trogon-decider-nats/src/lib.rs
Implements StreamRead/StreamAppend and SnapshotRead/SnapshotWrite delegating to stream/snapshot modules, resolves StreamWritePrecondition to JetStream expected sequence guards, exposes subject_current_position, and maps OCC conflicts and snapshot errors.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I nibble bytes and chase a stream,
Headers aligned like a tidy dream,
Snapshots safe in KV's nest,
Mocks queue answers, tests attest,
A rabbit hops — the build's at rest!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main objective: introducing a JetStream storage adapter for the trogon-decider-nats crate, which aligns with the comprehensive additions to enable JetStream-backed persistence.
Description check ✅ Passed The description is directly related to the changeset, explaining the three main goals of enabling JetStream persistence, preserving concurrency guarantees, and ensuring reviewable adapter behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch yordis/docs-unit-tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown

badge

Code Coverage Summary

Details
Filename                                                                              Stmts    Miss  Cover    Missing
----------------------------------------------------------------------------------  -------  ------  -------  ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
crates/acp-nats/src/ext_method_name.rs                                                   68       0  100.00%
crates/acp-nats/src/lib.rs                                                               69       0  100.00%
crates/acp-nats/src/in_flight_slot_guard.rs                                              32       0  100.00%
crates/acp-nats/src/acp_prefix.rs                                                        50       0  100.00%
crates/acp-nats/src/jsonrpc.rs                                                            6       0  100.00%
crates/acp-nats/src/session_id.rs                                                        71       0  100.00%
crates/acp-nats/src/error.rs                                                             82       0  100.00%
crates/acp-nats/src/req_id.rs                                                            39       0  100.00%
crates/acp-nats/src/pending_prompt_waiters.rs                                           134       0  100.00%
crates/acp-nats/src/client_proxy.rs                                                     181       0  100.00%
crates/acp-nats/src/config.rs                                                           203       0  100.00%
crates/mcp-nats/src/config.rs                                                           110       0  100.00%
crates/mcp-nats/src/mcp_peer_id.rs                                                       33       0  100.00%
crates/mcp-nats/src/client.rs                                                            31       0  100.00%
crates/mcp-nats/src/jsonrpc.rs                                                           22       0  100.00%
crates/mcp-nats/src/server.rs                                                            31       0  100.00%
crates/mcp-nats/src/transport.rs                                                        722       0  100.00%
crates/mcp-nats/src/mcp_prefix.rs                                                        36       0  100.00%
crates/acp-nats/src/jetstream/provision.rs                                               53       0  100.00%
crates/acp-nats/src/jetstream/consumers.rs                                               91       0  100.00%
crates/acp-nats/src/jetstream/ext_policy.rs                                              26       0  100.00%
crates/acp-nats/src/jetstream/streams.rs                                                163       4  97.55%   206-208, 218
crates/acp-nats/src/nats/subjects/commands/load.rs                                       15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_model.rs                                  15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/cancel.rs                                     15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/prompt.rs                                     15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/resume.rs                                     15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/close.rs                                      15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_mode.rs                                   15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/fork.rs                                       15       0  100.00%
crates/acp-nats/src/nats/subjects/commands/set_config_option.rs                          15       0  100.00%
crates/trogon-decider-runtime/src/event/event_id.rs                                      32       0  100.00%
crates/trogon-decider-runtime/src/event/event_identity.rs                                 3       0  100.00%
crates/trogon-decider-runtime/src/event/stream_event.rs                                   8       0  100.00%
crates/trogon-decider-runtime/src/event/mod.rs                                          162       0  100.00%
crates/mcp-nats/src/nats/subjects/mod.rs                                                 89       0  100.00%
crates/trogon-gateway/src/source/discord/gateway.rs                                     426       1  99.77%   137
crates/trogon-gateway/src/source/discord/config.rs                                      108       0  100.00%
crates/trogon-nats/src/mocks.rs                                                         317       0  100.00%
crates/trogon-nats/src/messaging.rs                                                     561       2  99.64%   144, 154
crates/trogon-nats/src/token.rs                                                           6       0  100.00%
crates/trogon-nats/src/subject_token_violation.rs                                        17       0  100.00%
crates/trogon-nats/src/client.rs                                                         22      22  0.00%    50-86
crates/trogon-nats/src/nats_token.rs                                                    157       0  100.00%
crates/trogon-nats/src/auth.rs                                                          114       0  100.00%
crates/trogon-nats/src/connect.rs                                                        94       9  90.43%   22-23, 33, 60-65
crates/trogon-gateway/src/source/incidentio/incidentio_signing_secret.rs                 67       0  100.00%
crates/trogon-gateway/src/source/incidentio/config.rs                                    16       0  100.00%
crates/trogon-gateway/src/source/incidentio/signature.rs                                206       0  100.00%
crates/trogon-gateway/src/source/incidentio/server.rs                                   343       0  100.00%
crates/trogon-gateway/src/source/incidentio/incidentio_event_type.rs                     62       0  100.00%
crates/trogon-gateway/src/source/slack/server.rs                                        863       0  100.00%
crates/trogon-gateway/src/source/slack/config.rs                                         17       0  100.00%
crates/trogon-gateway/src/source/slack/signature.rs                                      77       0  100.00%
crates/trogon-decider-nats/src/store.rs                                                 145      65  55.17%   33-187
crates/trogon-decider-nats/src/snapshot_store.rs                                        740     140  81.08%   64-217, 265-267, 305-313, 322-323, 338-344, 458-473, 502-511, 563, 568, 646-648, 654-656, 688-689, 699-700, 718, 742-743
crates/trogon-decider-nats/src/stream_store.rs                                          725      84  88.41%   70-213, 240-258, 301, 329-330, 333, 349-353, 519-520, 561, 574-578
crates/trogon-gateway/src/source/gitlab/server.rs                                       460       0  100.00%
crates/trogon-gateway/src/source/gitlab/gitlab_signing_token.rs                          74       0  100.00%
crates/trogon-gateway/src/source/gitlab/signature.rs                                    165       0  100.00%
crates/trogon-gateway/src/source/sentry/server.rs                                       311       0  100.00%
crates/trogon-gateway/src/source/sentry/signature.rs                                     54       0  100.00%
crates/trogon-gateway/src/source/sentry/sentry_client_secret.rs                          17       0  100.00%
crates/trogon-gateway/src/source/telegram/server.rs                                     339       0  100.00%
crates/trogon-gateway/src/source/telegram/config.rs                                     109       0  100.00%
crates/trogon-gateway/src/source/telegram/signature.rs                                   32       0  100.00%
crates/trogon-gateway/src/source/telegram/registration.rs                               327       0  100.00%
crates/trogon-nats/src/lease/nats_kv_lease_config.rs                                     26       0  100.00%
crates/trogon-nats/src/lease/provision.rs                                               187      10  94.65%   82-92
crates/trogon-nats/src/lease/mod.rs                                                     561      13  97.68%   180-193
crates/trogon-nats/src/lease/lease_bucket.rs                                             19       0  100.00%
crates/trogon-nats/src/lease/lease_timing.rs                                             15       0  100.00%
crates/trogon-nats/src/lease/release.rs                                                   5       5  0.00%    8-12
crates/trogon-nats/src/lease/renew.rs                                                   246      19  92.28%   23-29, 48-59
crates/trogon-nats/src/lease/acquire.rs                                                   5       5  0.00%    9-14
crates/trogon-nats/src/lease/lease_config_error.rs                                       11       0  100.00%
crates/trogon-nats/src/lease/renew_interval.rs                                           61       0  100.00%
crates/trogon-nats/src/lease/ttl.rs                                                      73       0  100.00%
crates/trogon-nats/src/lease/lease_key.rs                                                19       0  100.00%
crates/trogon-std/src/env/in_memory.rs                                                   73       0  100.00%
crates/trogon-std/src/env/system.rs                                                      17       0  100.00%
crates/trogon-std/src/fs/system.rs                                                       92       0  100.00%
crates/trogon-std/src/fs/mem.rs                                                         216      10  95.37%   61-63, 77-79, 132-134, 157
crates/mcp-nats/src/nats/subjects/client/cancelled.rs                                    12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/create_message.rs                               12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/ping.rs                                          9       0  100.00%
crates/mcp-nats/src/nats/subjects/client/roots_list_changed.rs                           12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/create_elicitation.rs                           12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/initialized.rs                                  12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/list_roots.rs                                   12       0  100.00%
crates/mcp-nats/src/nats/subjects/client/progress.rs                                     12       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_client.rs                             9       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_session.rs                           12       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/global_all.rs                             9       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/prompt_wildcard.rs                        9       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_agent.rs                             15       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/one_client.rs                            15       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_agent.rs                              9       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_session.rs                            9       0  100.00%
crates/acp-nats/src/nats/subjects/subscriptions/all_agent_ext.rs                          9       0  100.00%
crates/mcp-nats-stdio/src/config.rs                                                     160       0  100.00%
crates/mcp-nats-stdio/src/main.rs                                                       212       0  100.00%
crates/trogon-nats/src/telemetry/messaging.rs                                            82       0  100.00%
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_decode_error.rs                22       3  86.36%   11-13
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_envelope_encode_error.rs       20       0  100.00%
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_encode_error.rs                22       3  86.36%   11-13
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_payload_decode.rs               3       0  100.00%
crates/trogon-decider-runtime/src/snapshot/codec/encoded_snapshot.rs                     57       0  100.00%
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_envelope_decode_error.rs       38       0  100.00%
crates/acp-nats/src/client/terminal_release.rs                                          347       0  100.00%
crates/acp-nats/src/client/terminal_output.rs                                           206       0  100.00%
crates/acp-nats/src/client/request_permission.rs                                        308       0  100.00%
crates/acp-nats/src/client/ext.rs                                                       308       8  97.40%   163-172, 189-198
crates/acp-nats/src/client/fs_read_text_file.rs                                         356       0  100.00%
crates/acp-nats/src/client/ext_session_prompt_response.rs                               135       0  100.00%
crates/acp-nats/src/client/terminal_create.rs                                           274       0  100.00%
crates/acp-nats/src/client/terminal_wait_for_exit.rs                                    378       0  100.00%
crates/acp-nats/src/client/mod.rs                                                      2851       0  100.00%
crates/acp-nats/src/client/session_update.rs                                             55       0  100.00%
crates/acp-nats/src/client/fs_write_text_file.rs                                        418       0  100.00%
crates/acp-nats/src/client/rpc_reply.rs                                                  64       0  100.00%
crates/acp-nats/src/client/terminal_kill.rs                                             290       0  100.00%
crates/trogon-std/src/time/system.rs                                                     31       0  100.00%
crates/trogon-std/src/time/mock.rs                                                      125       0  100.00%
crates/acp-nats/src/nats/subjects/mod.rs                                                362       0  100.00%
crates/acp-nats/src/nats/subjects/stream.rs                                              56       0  100.00%
crates/mcp-nats/src/nats/mod.rs                                                          99       0  100.00%
crates/mcp-nats/src/nats/parsing.rs                                                     191       0  100.00%
crates/trogon-gateway/src/http.rs                                                       192       1  99.48%   119
crates/trogon-gateway/src/source_status.rs                                               28       0  100.00%
crates/trogon-gateway/src/main.rs                                                       116       0  100.00%
crates/trogon-gateway/src/config.rs                                                    2559      48  98.12%   91, 110, 328-329, 332, 712, 715, 875, 878, 881, 885, 960, 963, 966, 970, 1054-1061, 1138, 1141, 1144, 1149, 1207, 1210, 1213, 1292, 1295, 1298, 1302, 1366, 1369, 1372, 1435, 1438, 1441, 1446, 1521, 1524, 1527, 1532, 1590, 1593, 1596, 1809-1811
crates/trogon-gateway/src/source_integration_id.rs                                       61       3  95.08%   55, 57, 65
crates/trogon-gateway/src/streams.rs                                                    169      10  94.08%   11, 23, 31, 39, 47, 55, 63, 71, 79, 87
crates/trogon-gateway/src/source/github/config.rs                                        17       0  100.00%
crates/trogon-gateway/src/source/github/server.rs                                       328       0  100.00%
crates/trogon-gateway/src/source/github/signature.rs                                     61       0  100.00%
crates/acp-nats/src/nats/subjects/global/initialize.rs                                    6       0  100.00%
crates/acp-nats/src/nats/subjects/global/logout.rs                                        6       0  100.00%
crates/acp-nats/src/nats/subjects/global/session_list.rs                                  6       0  100.00%
crates/acp-nats/src/nats/subjects/global/authenticate.rs                                  6       0  100.00%
crates/acp-nats/src/nats/subjects/global/session_new.rs                                   6       0  100.00%
crates/acp-nats/src/nats/subjects/global/ext_notify.rs                                    9       0  100.00%
crates/acp-nats/src/nats/subjects/global/ext.rs                                           9       0  100.00%
crates/acp-nats/src/nats/subjects/responses/prompt_response.rs                           27       0  100.00%
crates/acp-nats/src/nats/subjects/responses/ext_ready.rs                                 12       0  100.00%
crates/acp-nats/src/nats/subjects/responses/response.rs                                  20       0  100.00%
crates/acp-nats/src/nats/subjects/responses/cancelled.rs                                 15       0  100.00%
crates/acp-nats/src/nats/subjects/responses/update.rs                                    27       0  100.00%
crates/trogon-decider-runtime/src/stream/append_stream.rs                                 5       0  100.00%
crates/trogon-decider-runtime/src/stream/mod.rs                                          38       0  100.00%
crates/trogon-decider-runtime/src/stream/read_stream.rs                                  10       0  100.00%
crates/trogon-decider-runtime/src/stream/stream_position.rs                              29       0  100.00%
crates/trogon-gateway/src/source/linear/config.rs                                        17       0  100.00%
crates/trogon-gateway/src/source/linear/server.rs                                       386       0  100.00%
crates/trogon-gateway/src/source/linear/signature.rs                                     54       1  98.15%   16
crates/acp-nats-server/src/config.rs                                                    137       9  93.43%   41, 50-61
crates/acp-nats-server/src/acp_connection_id.rs                                          45       0  100.00%
crates/acp-nats-server/src/connection.rs                                                171      32  81.29%   76-83, 88-99, 115, 117-118, 123, 132-133, 138, 142, 146, 149, 157, 161, 164, 167-171, 207
crates/acp-nats-server/src/main.rs                                                      896      10  98.88%   100, 231-238, 437
crates/acp-nats-server/src/transport.rs                                                1852     110  94.06%   277, 452, 536, 554, 581, 635, 640, 659, 671, 790, 813-815, 867, 884-887, 982-985, 1059, 1062, 1065, 1074, 1078, 1081, 1084-1087, 1106, 1138-1141, 1149-1154, 1166-1170, 1174-1183, 1195-1196, 1214-1215, 1225, 1241-1245, 1273-1279, 1290, 1293-1300, 1305-1309, 1312-1317, 1334, 1336-1337, 1419-1420, 1432-1433, 1453-1454, 1506-1522, 2218, 2261, 2313, 2368, 2380
crates/acp-nats-stdio/src/main.rs                                                       135      25  81.48%   65, 113-120, 126-128, 145, 174-193
crates/acp-nats-stdio/src/config.rs                                                      66       0  100.00%
crates/acp-nats/src/nats/parsing.rs                                                     278       1  99.64%   151
crates/acp-nats/src/nats/extensions.rs                                                    3       0  100.00%
crates/acp-nats/src/nats/mod.rs                                                          23       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_release.rs                         12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/session_request_permission.rs               12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_kill.rs                            12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_wait_for_exit.rs                   12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/fs_write_text_file.rs                       12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/session_update.rs                           12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_output.rs                          12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/fs_read_text_file.rs                        12       0  100.00%
crates/acp-nats/src/nats/subjects/client_ops/terminal_create.rs                          12       0  100.00%
crates/mcp-nats/src/nats/subjects/subscriptions/all_server.rs                             6       0  100.00%
crates/mcp-nats/src/nats/subjects/subscriptions/one_server.rs                             9       0  100.00%
crates/mcp-nats/src/nats/subjects/subscriptions/all_client.rs                             6       0  100.00%
crates/mcp-nats/src/nats/subjects/subscriptions/one_client.rs                             9       0  100.00%
crates/trogon-decider-runtime/src/headers/header_name.rs                                 33       0  100.00%
crates/trogon-decider-runtime/src/headers/mod.rs                                         74       0  100.00%
crates/trogon-decider-runtime/src/headers/from_entries_error.rs                          11       0  100.00%
crates/trogon-decider-runtime/src/headers/header_value.rs                                37       0  100.00%
crates/trogon-decider-runtime/src/headers/header_map.rs                                  54       3  94.44%   20-22
crates/acp-nats/src/telemetry/metrics.rs                                                 53       0  100.00%
crates/trogon-std/src/telemetry/http.rs                                                 217       0  100.00%
crates/trogon-decider-runtime/src/event/codec/event_decode.rs                             3       0  100.00%
crates/trogon-decider-runtime/src/execution.rs                                         1202       0  100.00%
crates/acp-nats/src/agent/load_session.rs                                                89       0  100.00%
crates/acp-nats/src/agent/bridge.rs                                                     123       4  96.75%   108-111
crates/acp-nats/src/agent/js_request.rs                                                 283       0  100.00%
crates/acp-nats/src/agent/set_session_model.rs                                           67       0  100.00%
crates/acp-nats/src/agent/new_session.rs                                                 82       0  100.00%
crates/acp-nats/src/agent/cancel.rs                                                     101       0  100.00%
crates/acp-nats/src/agent/test_support.rs                                               267       0  100.00%
crates/acp-nats/src/agent/logout.rs                                                      49       0  100.00%
crates/acp-nats/src/agent/fork_session.rs                                                94       0  100.00%
crates/acp-nats/src/agent/authenticate.rs                                                49       0  100.00%
crates/acp-nats/src/agent/mod.rs                                                         65       0  100.00%
crates/acp-nats/src/agent/set_session_config_option.rs                                   67       0  100.00%
crates/acp-nats/src/agent/initialize.rs                                                  79       0  100.00%
crates/acp-nats/src/agent/resume_session.rs                                              90       0  100.00%
crates/acp-nats/src/agent/set_session_mode.rs                                            67       0  100.00%
crates/acp-nats/src/agent/close_session.rs                                               63       0  100.00%
crates/acp-nats/src/agent/prompt.rs                                                     471       0  100.00%
crates/acp-nats/src/agent/ext_method.rs                                                  82       0  100.00%
crates/acp-nats/src/agent/list_sessions.rs                                               47       0  100.00%
crates/acp-nats/src/agent/ext_notification.rs                                            82       0  100.00%
crates/mcp-nats/src/telemetry/transport.rs                                                6       0  100.00%
crates/trogon-service-config/src/lib.rs                                                  92       0  100.00%
crates/trogon-gateway/src/source/microsoft_graph/server.rs                              325       0  100.00%
crates/trogon-gateway/src/source/microsoft_graph/client_state.rs                         30       0  100.00%
crates/trogon-gateway/src/source/notion/notion_event_type.rs                             46       3  93.48%   47-49
crates/trogon-gateway/src/source/notion/server.rs                                       318       8  97.48%   93-97, 130-131, 150-151
crates/trogon-gateway/src/source/notion/notion_verification_token.rs                     17       0  100.00%
crates/trogon-gateway/src/source/notion/signature.rs                                     56       1  98.21%   32
crates/trogon-gateway/src/source/notion/verification_token.rs                           240       0  100.00%
crates/mcp-nats/src/nats/subjects/server/subscribe_resource.rs                           12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/cancelled.rs                                    12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/initialize.rs                                   12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/list_resources.rs                               12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/list_tasks.rs                                   12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/tool_list_changed.rs                            12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/cancel_task.rs                                  12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/get_task_result.rs                              12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/progress.rs                                     12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/logging_message.rs                              12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/list_tools.rs                                   12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/complete.rs                                     12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/prompt_list_changed.rs                          12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/call_tool.rs                                    12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/resource_updated.rs                             12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/unsubscribe_resource.rs                         12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/get_prompt.rs                                   12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/ping.rs                                          9       0  100.00%
crates/mcp-nats/src/nats/subjects/server/resource_list_changed.rs                        12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/list_resource_templates.rs                      12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/set_logging_level.rs                            12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/read_resource.rs                                12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/elicitation_completed.rs                        12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/get_task.rs                                     12       0  100.00%
crates/mcp-nats/src/nats/subjects/server/list_prompts.rs                                 12       0  100.00%
crates/trogon-telemetry/src/service_name.rs                                              44       0  100.00%
crates/trogon-telemetry/src/lib.rs                                                      197      23  88.32%   94, 99, 104, 114-115, 121-139, 175, 178, 181, 187
crates/trogon-telemetry/src/metric.rs                                                    26       1  96.15%   29
crates/trogon-telemetry/src/trace.rs                                                     23       1  95.65%   22
crates/trogon-telemetry/src/log.rs                                                       68       1  98.53%   33
crates/trogon-telemetry/src/resource_attribute.rs                                        23       0  100.00%
crates/trogon-decider-runtime/src/snapshot/read_snapshot.rs                              11       0  100.00%
crates/trogon-decider-runtime/src/snapshot/mod.rs                                         3       0  100.00%
crates/trogon-gateway/src/source/standard_webhooks.rs                                   172       0  100.00%
crates/trogon-gateway/src/source/twitter/signature.rs                                    69       0  100.00%
crates/trogon-gateway/src/source/twitter/server.rs                                      525       0  100.00%
crates/trogon-gateway/src/source/twitter/config.rs                                       17       0  100.00%
crates/acp-nats-agent/src/connection.rs                                                1270       1  99.92%   607
crates/mcp-nats-server/src/allowed_host.rs                                               90       0  100.00%
crates/mcp-nats-server/src/config.rs                                                    276       0  100.00%
crates/mcp-nats-server/src/main.rs                                                      357     127  64.43%   149-166, 202-204, 214, 220-221, 228-231, 255-257, 261-270, 292-305, 310-358, 489, 492, 500-542
crates/trogon-nats/src/jetstream/publish.rs                                              64       0  100.00%
crates/trogon-nats/src/jetstream/stream_max_age.rs                                       18       0  100.00%
crates/trogon-nats/src/jetstream/traits.rs                                               40      40  0.00%    181-251
crates/trogon-nats/src/jetstream/claim_check.rs                                         346       0  100.00%
crates/trogon-nats/src/jetstream/mocks.rs                                              1637       7  99.57%   450, 824-826, 1003-1005
crates/trogon-nats/src/jetstream/create_conflicts.rs                                     24       0  100.00%
crates/trogon-std/src/secret_string.rs                                                   35       0  100.00%
crates/trogon-std/src/signal.rs                                                          26      12  53.85%   6-11, 18-25, 34
crates/trogon-std/src/uuid.rs                                                             7       0  100.00%
crates/trogon-std/src/duration.rs                                                        45       0  100.00%
crates/trogon-std/src/args.rs                                                            19       9  52.63%   11-28
crates/trogon-std/src/http.rs                                                            19       0  100.00%
crates/trogon-std/src/json.rs                                                            30       0  100.00%
crates/trogon-std/src/dirs/fixed.rs                                                      80       0  100.00%
crates/trogon-std/src/dirs/system.rs                                                     71       0  100.00%
crates/trogon-decider/src/testing.rs                                                    660       0  100.00%
crates/trogon-decider/src/events.rs                                                      49       0  100.00%
crates/trogon-decider/src/lib.rs                                                        143       0  100.00%
crates/trogon-decider/src/act.rs                                                         62       0  100.00%
crates/trogon-decider/src/decision.rs                                                    37       0  100.00%
TOTAL                                                                                 38912     894  97.70%

Diff against main

Filename                                                                     Stmts    Miss  Cover
-------------------------------------------------------------------------  -------  ------  --------
crates/trogon-decider-nats/src/store.rs                                       +145     +65  +55.17%
crates/trogon-decider-nats/src/snapshot_store.rs                              +740    +140  +81.08%
crates/trogon-decider-nats/src/stream_store.rs                                +725     +84  +88.41%
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_decode_error.rs       -1      +3  -13.64%
crates/trogon-decider-runtime/src/snapshot/codec/snapshot_encode_error.rs       -1      +3  -13.64%
crates/trogon-decider-runtime/src/snapshot/codec/encoded_snapshot.rs            -2       0  +100.00%
crates/acp-nats-server/src/transport.rs                                          0      +4  -0.22%
TOTAL                                                                        +1606    +299  -0.70%

Results for commit: 497ebee

Minimum allowed coverage is 95%

♻️ This comment has been updated with latest results

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
rsworkspace/crates/trogon-decider-nats/src/stream_store.rs (2)

283-289: 💤 Low value

Confusing error context for timestamp conversion failure.

The error source is message.subject.to_string(), which doesn't describe the timestamp issue. Consider including the actual timestamp value or a descriptive message.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-decider-nats/src/stream_store.rs` around lines 283
- 289, The error created in the recorded_at timestamp conversion
(DateTime::<Utc>::from_timestamp) uses message.subject as the error source which
is misleading; update the StreamStoreError::read_source call in the recorded_at
conversion to include the problematic timestamp (e.g.,
message.time.unix_timestamp() and/or message.time.nanosecond()) or a descriptive
string like "invalid timestamp: <value>" instead of message.subject.to_string(),
so the error context accurately reflects the timestamp conversion failure.

330-334: ⚖️ Poor tradeoff

String-based error construction via std::io::Error::other(format!(...)).

These patterns lose typed error information. Consider adding dedicated error variants (e.g., MissingHeader { name: &'static str }, MultiValueHeader { name: String }) to preserve structure and enable programmatic error handling.

As per coding guidelines: "Errors must be typed—use structs or enums, never String or format!()."

Also applies to: 348-352

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-decider-nats/src/stream_store.rs` around lines 330
- 334, Replace the string-based io::Error construction with new typed
StreamStoreError variants: add enum variants like MultiValueHeader { name:
String } and MissingHeader { name: String } (or similar) to StreamStoreError,
update the constructor helpers (e.g., the read_source usage) to return these
variants instead of std::io::Error::other(format!(...)), and change the failing
branches around values.as_slice() / header_name (and the similar block at the
other occurrence) to produce the new typed errors so callers can match on
MultiValueHeader or MissingHeader instead of parsing strings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rsworkspace/crates/trogon-decider-nats/README.md`:
- Around line 33-34: The README uses the floating image tag "nats:latest" which
can break tests; update the docker run line to pin a concrete, tested NATS
version (e.g., replace "nats:latest" with a specific tag such as "nats:2.12" or
"nats:2.12.0") so integration tests are reproducible; modify the entry in
README.md where the docker run command appears to use the pinned tag.

In `@rsworkspace/crates/trogon-decider-nats/src/lib.rs`:
- Around line 42-47: Replace the raw String used for JetStream subjects with a
domain-validated value object: define a Subject (or ValidatedSubject) type whose
constructor enforces the JetStream subject rules, change SubjectState.subject
from String to that Subject type, and update any other structs that currently
store a subject string (the other subject-bearing type near lines 60–65) to use
the Subject type as well; ensure the GraphQL/HTTP resolver (boundary) converts
incoming subject strings into Subject exactly once (returning an error on
invalid input) so internal domain code only ever sees the validated Subject
type.

In `@rsworkspace/crates/trogon-decider-nats/src/stream_store.rs`:
- Around line 112-129: The optimistic-concurrency
(expected_last_subject_sequence) guard is applied to the first message but its
AckFuture is discarded for multi-event batches; change the loop in which
build_publish_message / js.publish_message / publish.outbound_message are used
so that when the current publish carries the expected_last_subject_sequence
(e.g., index==0 && expected_last_subject_sequence.is_some()) you await its ack
immediately and return/map any error (preserving
StreamStoreError::publish_source), and for other messages continue to publish
and only keep the final ack in batch_ack as before.

---

Nitpick comments:
In `@rsworkspace/crates/trogon-decider-nats/src/stream_store.rs`:
- Around line 283-289: The error created in the recorded_at timestamp conversion
(DateTime::<Utc>::from_timestamp) uses message.subject as the error source which
is misleading; update the StreamStoreError::read_source call in the recorded_at
conversion to include the problematic timestamp (e.g.,
message.time.unix_timestamp() and/or message.time.nanosecond()) or a descriptive
string like "invalid timestamp: <value>" instead of message.subject.to_string(),
so the error context accurately reflects the timestamp conversion failure.
- Around line 330-334: Replace the string-based io::Error construction with new
typed StreamStoreError variants: add enum variants like MultiValueHeader { name:
String } and MissingHeader { name: String } (or similar) to StreamStoreError,
update the constructor helpers (e.g., the read_source usage) to return these
variants instead of std::io::Error::other(format!(...)), and change the failing
branches around values.as_slice() / header_name (and the similar block at the
other occurrence) to produce the new typed errors so callers can match on
MultiValueHeader or MissingHeader instead of parsing strings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6c734416-fbfb-41f5-be9d-71654b274181

📥 Commits

Reviewing files that changed from the base of the PR and between d92aaa2 and ebae9d2.

⛔ Files ignored due to path filters (1)
  • rsworkspace/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • rsworkspace/crates/trogon-decider-nats/Cargo.toml
  • rsworkspace/crates/trogon-decider-nats/README.md
  • rsworkspace/crates/trogon-decider-nats/src/lib.rs
  • rsworkspace/crates/trogon-decider-nats/src/snapshot_store.rs
  • rsworkspace/crates/trogon-decider-nats/src/stream_store.rs
  • rsworkspace/crates/trogon-decider-nats/tests/jetstream.rs

Comment thread rsworkspace/crates/trogon-decider-nats/README.md Outdated
Comment thread rsworkspace/crates/trogon-decider-nats/src/lib.rs Outdated
Comment thread rsworkspace/crates/trogon-decider-nats/src/stream_store.rs
@yordis yordis added the rust:coverage-baseline-reset Relax Rust coverage gate to establish a new baseline label May 21, 2026
Comment thread rsworkspace/crates/trogon-decider-nats/src/stream_store.rs
@yordis yordis force-pushed the yordis/docs-unit-tests branch 2 times, most recently from 3bf7848 to 014cd2b Compare May 21, 2026 02:27

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs`:
- Around line 382-387: enqueue_ack_with_sequence pushes a
MockPublishMessageOutcome::Ack(sequence) but doesn't advance the generator
state, so subsequent auto-assigned publishes can return lower sequences; update
the internal next_sequence after enqueuing an explicit ACK (e.g., set
self.next_sequence to max(self.next_sequence, sequence + 1)) while holding the
same lock used for results so sequence remains monotonic; apply the same pattern
to the other enqueue helpers in the 405-435 range that inject explicit
sequences.
- Around line 556-558: set_bucket_name currently updates the internal
bucket_name lock but status() still returns a hardcoded bucket string, making
the mock inconsistent; modify status() (the method that reads bucket name for
status output) to read and return the current value from the same bucket_name
lock (the same Arc<Mutex/...> used by set_bucket_name) so status() reflects
changes made by set_bucket_name and keeps the mock consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e3a5cad3-6b82-4b12-9407-8c2b7659510c

📥 Commits

Reviewing files that changed from the base of the PR and between ebae9d2 and 7ceafbf.

⛔ Files ignored due to path filters (1)
  • rsworkspace/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • rsworkspace/crates/trogon-decider-nats/Cargo.toml
  • rsworkspace/crates/trogon-decider-nats/src/lib.rs
  • rsworkspace/crates/trogon-decider-nats/src/snapshot_store.rs
  • rsworkspace/crates/trogon-decider-nats/src/stream_store.rs
  • rsworkspace/crates/trogon-decider-nats/tests/jetstream.rs
  • rsworkspace/crates/trogon-nats/Cargo.toml
  • rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs
  • rsworkspace/crates/trogon-nats/src/jetstream/mod.rs
  • rsworkspace/crates/trogon-nats/src/jetstream/traits.rs

Comment thread rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs
Comment thread rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (3)
rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs (1)

384-394: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the explicit ACK enqueue atomic with the sequence bump.

Between Line 386 and Line 391, another task can call publish_message(), see an empty results queue, and auto-assign sequence + 1 before the queued ACK is visible. That still allows 11 to be observed before 10 in concurrent tests.

Proposed fix
     pub fn enqueue_ack_with_sequence(&self, sequence: u64) {
-        {
-            let mut next = self.next_sequence.lock().unwrap();
-            if sequence >= *next {
-                *next = sequence + 1;
-            }
-        }
-        self.results
-            .lock()
-            .unwrap()
-            .push_back(MockPublishMessageOutcome::Ack(sequence));
+        let mut results = self.results.lock().unwrap();
+        let mut next = self.next_sequence.lock().unwrap();
+        if sequence >= *next {
+            *next = sequence + 1;
+        }
+        results.push_back(MockPublishMessageOutcome::Ack(sequence));
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs` around lines 384 -
394, enqueue_ack_with_sequence currently bumps next_sequence under
next_sequence.lock() and then separately pushes the Ack into results under
results.lock(), allowing a concurrent publish_message to observe the bumped
sequence before the Ack is visible; make the bump-and-push atomic by holding a
single lock while updating next_sequence and pushing
MockPublishMessageOutcome::Ack(sequence) so no other task can interleave (either
acquire results.lock() while holding next_sequence.lock(), or refactor to a
single Mutex protecting both fields) and ensure publish_message uses the same
locking scheme.
rsworkspace/crates/trogon-decider-nats/src/snapshot_store.rs (1)

107-135: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Reject invalid snapshot ids before they become KV keys.

snapshot_key::<T>("") produces exactly the key shape that stream_id_from_snapshot_key::<T>(...) rejects as invalid, so this API can construct snapshot identifiers that its own read path considers impossible. Please move stream_id to a validated value object (or another fallible constructor) and use that type throughout SnapshotChange, read_snapshot, and write_snapshot.

As per coding guidelines, "Prefer domain-specific value objects over primitives (e.g., AcpPrefix instead of String). Each type's factory must guarantee correctness at construction—invalid instances should be unrepresentable."

Also applies to: 186-190, 212-224, 291-315

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-decider-nats/src/snapshot_store.rs` around lines
107 - 135, The SnapshotChange API currently accepts raw String stream_ids (via
SnapshotChange::upsert and ::delete) which allows invalid snapshot keys (e.g.,
snapshot_key::<T>("")) that the read path (stream_id_from_snapshot_key::<T>)
rejects; create a validated domain value object (e.g., StreamId or StreamIdRef)
with a fallible constructor/validator that enforces the valid key invariants,
replace all String stream_id fields and constructors in SnapshotChange
(Upsert/Delete) to accept this new type, and update read_snapshot,
write_snapshot and any callers (including places referenced at lines ~186-190,
212-224, 291-315) to use the validated StreamId so invalid keys cannot be
constructed anywhere in the codebase; ensure conversions from &str/String to the
value object are explicit and fallible and adjust signatures accordingly.
rsworkspace/crates/trogon-decider-nats/src/stream_store.rs (1)

219-220: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make event-header encoding fallible before calling PublishMessage::header.

build_publish_message passes event_header_name(...) (currently an infallible format! string) into publish.header(...); in async-nats 0.47.0 PublishMessage::header panics on invalid header names/values, so malformed event.headers keys can crash append_stream instead of returning StreamStoreError. Build a fallible NATS header name (e.g., async_nats::HeaderName::try_from(...)) and return a typed StreamStoreError variant when conversion fails (see build_publish_message ~219-221 and event_header_name ~363-370, which already contains a TODO for this).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-decider-nats/src/stream_store.rs` around lines 219
- 220, build_publish_message currently calls
publish.header(event_header_name(...), value.as_str()) which can panic in
async-nats 0.47.0 on invalid header names/values; change this to perform
fallible conversions before calling PublishMessage::header: use
async_nats::HeaderName::try_from(event_header_name(...)) (and
async_nats::HeaderValue::try_from(value.as_str()) if needed) and propagate
failures as a StreamStoreError variant (add one if missing) so append_stream
returns an Err instead of panicking; update event_header_name to return a &str
or raw string and do the try_from conversion at the call site in
build_publish_message (~219-221) and map conversion errors into
StreamStoreError.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@rsworkspace/crates/trogon-decider-nats/src/snapshot_store.rs`:
- Around line 107-135: The SnapshotChange API currently accepts raw String
stream_ids (via SnapshotChange::upsert and ::delete) which allows invalid
snapshot keys (e.g., snapshot_key::<T>("")) that the read path
(stream_id_from_snapshot_key::<T>) rejects; create a validated domain value
object (e.g., StreamId or StreamIdRef) with a fallible constructor/validator
that enforces the valid key invariants, replace all String stream_id fields and
constructors in SnapshotChange (Upsert/Delete) to accept this new type, and
update read_snapshot, write_snapshot and any callers (including places
referenced at lines ~186-190, 212-224, 291-315) to use the validated StreamId so
invalid keys cannot be constructed anywhere in the codebase; ensure conversions
from &str/String to the value object are explicit and fallible and adjust
signatures accordingly.

In `@rsworkspace/crates/trogon-decider-nats/src/stream_store.rs`:
- Around line 219-220: build_publish_message currently calls
publish.header(event_header_name(...), value.as_str()) which can panic in
async-nats 0.47.0 on invalid header names/values; change this to perform
fallible conversions before calling PublishMessage::header: use
async_nats::HeaderName::try_from(event_header_name(...)) (and
async_nats::HeaderValue::try_from(value.as_str()) if needed) and propagate
failures as a StreamStoreError variant (add one if missing) so append_stream
returns an Err instead of panicking; update event_header_name to return a &str
or raw string and do the try_from conversion at the call site in
build_publish_message (~219-221) and map conversion errors into
StreamStoreError.

In `@rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs`:
- Around line 384-394: enqueue_ack_with_sequence currently bumps next_sequence
under next_sequence.lock() and then separately pushes the Ack into results under
results.lock(), allowing a concurrent publish_message to observe the bumped
sequence before the Ack is visible; make the bump-and-push atomic by holding a
single lock while updating next_sequence and pushing
MockPublishMessageOutcome::Ack(sequence) so no other task can interleave (either
acquire results.lock() while holding next_sequence.lock(), or refactor to a
single Mutex protecting both fields) and ensure publish_message uses the same
locking scheme.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eb7a2eb1-c1a1-4a2d-8c32-2d5b4f89f65c

📥 Commits

Reviewing files that changed from the base of the PR and between 7ceafbf and 967e77b.

📒 Files selected for processing (5)
  • rsworkspace/crates/trogon-decider-nats/src/snapshot_store.rs
  • rsworkspace/crates/trogon-decider-nats/src/stream_store.rs
  • rsworkspace/crates/trogon-decider-nats/tests/jetstream.rs
  • rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs
  • rsworkspace/crates/trogon-nats/src/jetstream/traits.rs
💤 Files with no reviewable changes (1)
  • rsworkspace/crates/trogon-nats/src/jetstream/traits.rs

@yordis yordis changed the title feat(decider-nats): add JetStream storage adapter feat(trogon-decider-nats): add JetStream storage adapter May 21, 2026
@yordis yordis force-pushed the yordis/docs-unit-tests branch from 6685218 to a9e96d9 Compare May 21, 2026 05:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs (1)

544-547: 💤 Low value

Document the intentional EOF error trick.

The empty_atomic_batch_member_ack_error function uses serde_json::from_str::<PublishAck>("") to create an EOF error that simulates JetStream's atomic batch member acknowledgment behavior. While this works, the intent is non-obvious. Consider adding a brief comment explaining why this specific error type is used.

📝 Suggested documentation
 fn empty_atomic_batch_member_ack_error() -> PublishError {
+    // JetStream returns an empty response (no JSON body) for atomic batch members
+    // that are staged but not yet committed. The async-nats client surfaces this
+    // as a serde_json EOF error, which we replicate here for mock fidelity.
     let source = serde_json::from_str::<PublishAck>("").unwrap_err();
     PublishError::with_source(PublishErrorKind::Other, source)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs` around lines 544 -
547, Add a brief comment above the function empty_atomic_batch_member_ack_error
explaining that calling serde_json::from_str::<PublishAck>("") intentionally
triggers a serde EOF/deserialize error to simulate JetStream's atomic batch
member acknowledgment behavior (i.e., an empty payload producing a specific
error shape), so future readers know this is deliberate and not a bug; reference
the function name empty_atomic_batch_member_ack_error and the use of
serde_json::from_str::<PublishAck>("") in the comment.
rsworkspace/crates/trogon-decider-nats/src/stream_store.rs (1)

285-293: 💤 Low value

Minor allocation per iteration in read_subject_range_with_stream_id.

The stream_id.to_string() is called once at line 285, but then stream_id.clone() is called for every message in the range via the closure at line 291. For streams with many messages, consider passing a reference or using Cow to avoid repeated allocations.

♻️ Suggested optimization
 async fn read_subject_range_with_stream_id<S>(
     stream: &S,
     stream_id: &str,
     subject: &str,
     from_sequence: u64,
     to_sequence: u64,
 ) -> Result<Vec<StreamEvent>, StreamStoreError>
 where
     S: JetStreamGetRawMessage,
 {
-    let stream_id = stream_id.to_string();
     read_message_range(
         stream,
         from_sequence,
         to_sequence,
         |message| message.subject.as_str() == subject,
-        |_| stream_id.clone(),
+        |_| stream_id.to_string(),
     )
     .await
 }

This removes the intermediate allocation outside the loop and makes the allocation explicit per-message, though you could also change the callback signature to return &str and have record_stream_message take a reference if you want to avoid all allocations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-decider-nats/src/stream_store.rs` around lines 285
- 293, In read_subject_range_with_stream_id: avoid cloning stream_id per message
by returning a reference or Cow instead of cloning inside the closure passed to
read_message_range; specifically, update the closure passed to
read_message_range (currently |_| stream_id.clone()) to return &stream_id (or
Cow::Borrowed(&stream_id)) and adjust read_message_range/record_stream_message
signatures as needed to accept &str or Cow<'_, str> so you only allocate when
necessary.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@rsworkspace/crates/trogon-decider-nats/src/stream_store.rs`:
- Around line 285-293: In read_subject_range_with_stream_id: avoid cloning
stream_id per message by returning a reference or Cow instead of cloning inside
the closure passed to read_message_range; specifically, update the closure
passed to read_message_range (currently |_| stream_id.clone()) to return
&stream_id (or Cow::Borrowed(&stream_id)) and adjust
read_message_range/record_stream_message signatures as needed to accept &str or
Cow<'_, str> so you only allocate when necessary.

In `@rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs`:
- Around line 544-547: Add a brief comment above the function
empty_atomic_batch_member_ack_error explaining that calling
serde_json::from_str::<PublishAck>("") intentionally triggers a serde
EOF/deserialize error to simulate JetStream's atomic batch member acknowledgment
behavior (i.e., an empty payload producing a specific error shape), so future
readers know this is deliberate and not a bug; reference the function name
empty_atomic_batch_member_ack_error and the use of
serde_json::from_str::<PublishAck>("") in the comment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dab09198-d988-4f20-a4f8-534c2c00ea1a

📥 Commits

Reviewing files that changed from the base of the PR and between 967e77b and 6284c7e.

📒 Files selected for processing (3)
  • rsworkspace/crates/trogon-decider-nats/src/lib.rs
  • rsworkspace/crates/trogon-decider-nats/src/stream_store.rs
  • rsworkspace/crates/trogon-nats/src/jetstream/mocks.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@rsworkspace/crates/trogon-decider-nats/src/lib.rs`:
- Around line 108-117: The OptimisticConcurrencyConflictError currently sets
current_position from the value read before the publish attempt which can be
stale when a WrongExpectedVersion occurs; update the logic that constructs
OptimisticConcurrencyConflictError (the code path that detects
WrongExpectedVersion) to re-fetch the latest stream position/metadata (e.g.,
call the same helper that returns StreamPosition or head position) and populate
the current_position field with that refreshed value before returning/raising
the error so retries/assertions see the up-to-date position; apply the same
change to the other constructor/site that creates
OptimisticConcurrencyConflictError (the other occurrence referenced) so both use
the refreshed position.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 33fc4b82-ee15-4dd0-a7bf-1c5db98e94f8

📥 Commits

Reviewing files that changed from the base of the PR and between 6284c7e and ef33af1.

📒 Files selected for processing (1)
  • rsworkspace/crates/trogon-decider-nats/src/lib.rs

Comment on lines +108 to +117
#[derive(Debug, Clone, PartialEq, Eq)]
/// Optimistic concurrency conflict details for a failed stream append.
pub struct OptimisticConcurrencyConflictError {
/// Domain stream id that was being appended.
pub stream_id: String,
/// Expected stream state supplied by the caller.
pub expected: StreamWritePrecondition,
/// Current stream position observed before publishing.
pub current_position: Option<StreamPosition>,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Refresh the conflict position after WrongExpectedVersion.

current_position here is the value read before the publish attempt. If another writer wins the race, this error can still report the old sequence (or None), so the new OCC details are unreliable for retries and assertions.

💡 Suggested fix
-        let stream_position = append_subject_stream(
-            self.as_jetstream(),
-            subject_state.subject,
-            expected_last_subject_sequence,
-            &events,
-        )
-        .await
-        .map_err(|source| match source {
-            StreamStoreError::WrongExpectedVersion => {
-                JetStreamStoreError::OptimisticConcurrencyConflict(OptimisticConcurrencyConflictError {
-                    stream_id: stream_id.to_string(),
-                    expected: expected_state,
-                    current_position,
-                })
-            }
-            other => JetStreamStoreError::AppendStream(other),
-        })?;
+        let stream_position = match append_subject_stream(
+            self.as_jetstream(),
+            subject_state.subject,
+            expected_last_subject_sequence,
+            &events,
+        )
+        .await
+        {
+            Ok(stream_position) => stream_position,
+            Err(StreamStoreError::WrongExpectedVersion) => {
+                let current_position = self
+                    .subject_resolver
+                    .resolve_subject_state(self.events_stream(), stream_id)
+                    .await
+                    .map_err(JetStreamStoreError::ResolveSubject)?
+                    .current_position;
+
+                return Err(JetStreamStoreError::OptimisticConcurrencyConflict(
+                    OptimisticConcurrencyConflictError {
+                        stream_id: stream_id.to_string(),
+                        expected: expected_state,
+                        current_position,
+                    },
+                ));
+            }
+            Err(other) => return Err(JetStreamStoreError::AppendStream(other)),
+        };

Also applies to: 319-325

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rsworkspace/crates/trogon-decider-nats/src/lib.rs` around lines 108 - 117,
The OptimisticConcurrencyConflictError currently sets current_position from the
value read before the publish attempt which can be stale when a
WrongExpectedVersion occurs; update the logic that constructs
OptimisticConcurrencyConflictError (the code path that detects
WrongExpectedVersion) to re-fetch the latest stream position/metadata (e.g.,
call the same helper that returns StreamPosition or head position) and populate
the current_position field with that refreshed value before returning/raising
the error so retries/assertions see the up-to-date position; apply the same
change to the other constructor/site that creates
OptimisticConcurrencyConflictError (the other occurrence referenced) so both use
the refreshed position.

Comment thread rsworkspace/crates/trogon-decider-nats/src/lib.rs Outdated
yordis added 8 commits May 21, 2026 13:33
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
…Stream

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
… details

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis yordis force-pushed the yordis/docs-unit-tests branch from ef33af1 to 1c95bbc Compare May 21, 2026 17:40
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Comment thread rsworkspace/crates/trogon-decider-nats/src/stream_store.rs
yordis added 4 commits May 21, 2026 14:05
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
yordis added 2 commits May 21, 2026 15:52
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 808dad5. Configure here.

Comment thread rsworkspace/crates/trogon-decider-nats/src/stream_store.rs
Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>
@yordis yordis merged commit bc2c441 into main May 21, 2026
7 checks passed
@yordis yordis deleted the yordis/docs-unit-tests branch May 21, 2026 22:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rust:coverage-baseline-reset Relax Rust coverage gate to establish a new baseline

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant