test: dedup test suites and add failure-path coverage - #110
Conversation
|
Warning Review limit reached
Next review available in: 30 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (24)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…ayer Extends the existing SqlxReadModelBackend / lock/sqlx_common dialect-trait pattern to the event-store/snapshot/outbox/inbox layers. postgres_repo and sqlite_repo shrink from ~1700 lines each to ~460-line dialect shims over a shared SqlxRepository/SqlxOutboxStore in src/sqlx_repo/repo.rs. Outbox `claim` stays per-backend (postgres SKIP LOCKED CTE vs sqlite scan-loop). Squashed from 14 commits for a single rebase reconciliation onto main (after #106/#107/#109 merged). Notable changes rolled up: - fix: surface malformed sqlite timestamps as errors (was silent UNIX_EPOCH); align null-bind error message with postgres (includes field_name). - refactor: share commit-batch validation across all backends (fixes hashmap vs SQL snapshot-identity drift). - feat!: widen postgres integer columns to BIGINT so both backends decode i64; deletes the width-conversion helpers. - perf: batch snapshot loads for get_all hydration (fixes N+1); get_streams is a GetStream default method. - perf: batch the commit_batch concurrency pre-check into one query. - perf!: borrow events in PreparedEventAppend instead of cloning. - feat!: bound outbox status listings (messages_by_status/pending) with a limit. - perf!: store EventRecord.payload_codec as Cow<'static, str>. - feat: run migrations through sqlx's Migrator with a _sqlx_migrations ledger. - fix: chunk postgres event/outbox inserts under the 65535 bind-param cap. - fix: classify postgres 57P01/57P02/57P03 and class-08 SQLSTATEs as transient. Merge-reconciliation notes: - table/ is now the canonical vocabulary (#109); backend shims import the renamed types via local aliases (TableColumn as ColumnDef, TableStoreError as ReadModelError) to keep the collapsed bodies unchanged. - #106's batched OutboxStore::complete_many overrides lived in the old backend files and were collapsed away; SqlxOutboxStore inherits the serial default (correct, conformance-tested). Re-adding a batched override to the shared layer is a follow-up (see tasks/outbox-sqlx-batched-complete-many). NOTE for #110: this flips the 57P01 classification, so the "flip this assertion" fault-injection test on review/tests-dedup-coverage must be updated when both merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DzYSVLas93c7LbgHJWsW7n
Squashed #110 (9 commits) for a single rebase reconciliation onto main. See PR #110 description for the original commit breakdown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DzYSVLas93c7LbgHJWsW7n
a210d71 to
a852fa8
Compare
…ayer Extends the existing SqlxReadModelBackend / lock/sqlx_common dialect-trait pattern to the event-store/snapshot/outbox/inbox layers. postgres_repo and sqlite_repo shrink from ~1700 lines each to ~460-line dialect shims over a shared SqlxRepository/SqlxOutboxStore in src/sqlx_repo/repo.rs. Outbox `claim` stays per-backend (postgres SKIP LOCKED CTE vs sqlite scan-loop). Squashed from 14 commits for a single rebase reconciliation onto main (after - fix: surface malformed sqlite timestamps as errors (was silent UNIX_EPOCH); align null-bind error message with postgres (includes field_name). - refactor: share commit-batch validation across all backends (fixes hashmap vs SQL snapshot-identity drift). - feat!: widen postgres integer columns to BIGINT so both backends decode i64; deletes the width-conversion helpers. - perf: batch snapshot loads for get_all hydration (fixes N+1); get_streams is a GetStream default method. - perf: batch the commit_batch concurrency pre-check into one query. - perf!: borrow events in PreparedEventAppend instead of cloning. - feat!: bound outbox status listings (messages_by_status/pending) with a limit. - perf!: store EventRecord.payload_codec as Cow<'static, str>. - feat: run migrations through sqlx's Migrator with a _sqlx_migrations ledger. - fix: chunk postgres event/outbox inserts under the 65535 bind-param cap. - fix: classify postgres 57P01/57P02/57P03 and class-08 SQLSTATEs as transient. Merge-reconciliation notes: - table/ is now the canonical vocabulary (#109); backend shims import the renamed types via local aliases (TableColumn as ColumnDef, TableStoreError as ReadModelError) to keep the collapsed bodies unchanged. - #106's batched OutboxStore::complete_many overrides lived in the old backend files and were collapsed away; SqlxOutboxStore inherits the serial default (correct, conformance-tested). Re-adding a batched override to the shared layer is a follow-up (see tasks/outbox-sqlx-batched-complete-many). REBASED onto main+#110 (9f34f0a): #110 already merged, so its fault-injection test that pinned the OLD 57P01 (mis)classification is flipped here to `assert!(err.is_retryable())` to match this PR's classification fix; #110's shared outbox test helpers are updated for the bounded messages_by_status(limit) API. Main stays green on merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DzYSVLas93c7LbgHJWsW7n
…ayer (#111) Extends the existing SqlxReadModelBackend / lock/sqlx_common dialect-trait pattern to the event-store/snapshot/outbox/inbox layers. postgres_repo and sqlite_repo shrink from ~1700 lines each to ~460-line dialect shims over a shared SqlxRepository/SqlxOutboxStore in src/sqlx_repo/repo.rs. Outbox `claim` stays per-backend (postgres SKIP LOCKED CTE vs sqlite scan-loop). Squashed from 14 commits for a single rebase reconciliation onto main (after - fix: surface malformed sqlite timestamps as errors (was silent UNIX_EPOCH); align null-bind error message with postgres (includes field_name). - refactor: share commit-batch validation across all backends (fixes hashmap vs SQL snapshot-identity drift). - feat!: widen postgres integer columns to BIGINT so both backends decode i64; deletes the width-conversion helpers. - perf: batch snapshot loads for get_all hydration (fixes N+1); get_streams is a GetStream default method. - perf: batch the commit_batch concurrency pre-check into one query. - perf!: borrow events in PreparedEventAppend instead of cloning. - feat!: bound outbox status listings (messages_by_status/pending) with a limit. - perf!: store EventRecord.payload_codec as Cow<'static, str>. - feat: run migrations through sqlx's Migrator with a _sqlx_migrations ledger. - fix: chunk postgres event/outbox inserts under the 65535 bind-param cap. - fix: classify postgres 57P01/57P02/57P03 and class-08 SQLSTATEs as transient. Merge-reconciliation notes: - table/ is now the canonical vocabulary (#109); backend shims import the renamed types via local aliases (TableColumn as ColumnDef, TableStoreError as ReadModelError) to keep the collapsed bodies unchanged. - #106's batched OutboxStore::complete_many overrides lived in the old backend files and were collapsed away; SqlxOutboxStore inherits the serial default (correct, conformance-tested). Re-adding a batched override to the shared layer is a follow-up (see tasks/outbox-sqlx-batched-complete-many). REBASED onto main+#110 (9f34f0a): #110 already merged, so its fault-injection test that pinned the OLD 57P01 (mis)classification is flipped here to `assert!(err.is_retryable())` to match this PR's classification fix; #110's shared outbox test helpers are updated for the bounded messages_by_status(limit) API. Main stays green on merge. Claude-Session: https://claude.ai/code/session_01DzYSVLas93c7LbgHJWsW7n Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Two halves: remove ~1,100 lines of copy-pasted test scaffolding with zero coverage loss (and one coverage gain), then add the failure-path coverage the suites were missing. No
src/changes.Part A — dedup
1. Repository conformance mains → one macro
The hashmap/sqlite/postgres conformance mains were 17 near-identical
#[tokio::test]shims each. They are now a sharedrepository_conformance_tests!()macro intests/persistent_repository_conformance/mod.rsover a normalizedasync fn repository() -> Option<Backend>factory; each main is ~15 lines. This also fixes a coverage drift: only the hashmap main ran theread_modelsscenarios — sqlite and postgres now run the full 20-case list including read_models (the SQL factories bootstrap the conformance table), and the#![allow(dead_code)]that papered over the drift is gone.2. Bus behaviour scenarios ×5 → transport conformance
The point-to-point / fan-out / named-service consumer-group scenarios were copy-pasted byte-identical-modulo-construction across the five bus transport mains. They are now generic scenario fns in
tests/transport_conformance/taking a bus-factory closure; each main keeps a small factory + shims. Kept local on purpose: Kafka's offset-commit point-to-point proof and RabbitMQ's bind-before-publish fan-out + named-service variants — their transport-specific semantics are the point. The sqlite suite's message/id micro-helpers were promoted alongside.3. Conformance-redundant tests deleted
optimistic_conflict_rolls_back_other_stream_and_snapshotandduplicate_stream_identity_is_rejected_before_sql_writes(postgres main) andsnapshots_persist_by_full_stream_identity(both SQL mains) re-proved scenarios the shared conformance suite already runs against every backend. The remaining raw-SQL dialect assertions were deliberately not merged.4.
tests/support/modulessupport/ids.rs—unique_id/run_token/unique(was copied 6×)support/outbox.rs— the scan-four-statusesfind_outbox_by_idloop (was copied 7×)support/sqlite.rs— the temp-fileTempDbwith WAL/SHM cleanup (was copied 2×)support/env.rs— thebroker_envskip guard (3 broker suites)tests/todosandtests/read_modelsuntouched (owned elsewhere).Part B — new coverage (all env-gated; suites skip cleanly without services)
5. Broker failure paths — the real-broker suites were entirely happy-path; the ack/nack/dead-letter contract was proven only against in-memory fakes and the SQL buses. Per broker (NATS, RabbitMQ, Kafka):
retryable_failure_is_redelivered_then_succeeds— Nak / requeue / seek-back redelivery, then drainMSG_TERMINATEDadvisory (the parking destination) fires and the message is not redelivered to the durable; Kafka: the adapter has no native DLQ (dead_letter= offset-commit skip), so the test proves no-redelivery-to-group + flow-continues insteadundecodable_payload_dead_letters_without_blocking— raw garbage on the subject/queue/topic is dead-lettered and subsequent messages still flow6. Composed at-least-once — publish succeeds, completion write fails (simulated crash), row reclaimed after lease expiry and republished (duplicate delivery, same stable id), both deliveries replayed through
run_sourceinto a consumer whose handler commits its effect atomically with an inbox receipt. Exactly one effect commits; the duplicate is acked, not nacked, and the second attempt's effect row is fenced.7.
QueuedRepositorystale-lease fencing — writer A holds a durableSqliteLockManagerlease, the TTL expires, writer B steals it and commits, then A commits its stale load → A observesConcurrentWrite(the optimistic check is the backstop once the lock can't protect it) and B's write survives.8. SQLite 1000-event batch —
commit_batch_with_1000_events_round_tripsexercises the 999-bind-param multi-row INSERT chunking, asserting count, contiguous sequences, and payloads across chunk seams.9. Postgres mid-commit fault injection — an
ACCESS EXCLUSIVElock parks acommit_batchmid-transaction,pg_blocking_pidspinpoints the blocked backend,pg_terminate_backendkills it. Nothing persists and the commit fails with aStorageerror.src findings (documented, not fixed — src/ is owned by other agents)
SQLSTATE 57P01 classified permanent (
src/sqlx_repo/mod.rs::is_sqlx_transient): a terminated/killed postgres backend surfaces as 57P01, which is classifiedretryable: falsebecause only40001/40P01are whitelisted amongDatabaseerrors. A lost connection is an infrastructure hiccup and should be retryable — as-is, an outbox dispatcher would fail/dead-letter a row because postgres restarted mid-commit. The related connection-failure classes (08***,57P02,57P03) have the same gap. The newbackend_termination_mid_commit_rolls_back_and_nothing_persiststest pins the current behavior with a comment saying which assertion to flip once fixed.Outbox lease deadlines truncate to whole seconds (
src/outbox/message.rs::lease_deadline_secsuses.as_secs()): a lease's expiry floor-truncates, so a sub-second lease (or any lease landing just before a second boundary) can be born up to ~1s shorter than requested — even already expired, at which point the store fences the claiming worker's owncompletewithInvalidState. Found when the composed at-least-once test flaked under load with a 100ms lease; the test now models the retry as a fresh worker with a comfortable lease, and the quirk is called out in the commit.Service::dispatch_messageswallows payload decode failures (src/microsvc/service.rs,message_to_json_inputfallback): a non-JSON payload falls back toValue::Nullinput instead of surfacingHandlerError::DecodeFailed, so an undecodable message is silently dispatched rather than dead-lettered (theDecodeFailed → permanentmapping inmicrosvc/error.rsis unreachable from this path). Discovered while writing theundecodable_payload_*tests — they use a payload-decoding handler as the decode point, which is the framework's effective contract today. Worth deciding whether that fallback is intentional.Test results
cargo fmtclean;cargo clippy --workspace --all-features --all-targetscarries only the two warnings that pre-exist onmain(todosMutexGuard-across-await, libmanual_async_fn).🤖 Generated with Claude Code
https://claude.ai/code/session_01DzYSVLas93c7LbgHJWsW7n