fix(server): stop replaying a command receipt for a different aggregate - #5246
fix(server): stop replaying a command receipt for a different aggregate#5246ostapondo wants to merge 2 commits into
Conversation
📝 WalkthroughWalkthroughThe orchestration engine now rejects reuse of an accepted command ID for a different aggregate. It returns aggregate details in a typed error, preserves identical-command receipt replay, skips reconciliation for conflicts, and adds integration coverage for both cases. ChangesOrchestration command receipt validation
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ 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 |
ApprovabilityVerdict: Approved 185a542 Straightforward bug fix that adds a validation check to prevent command receipts from being incorrectly replayed for a different aggregate. Limited scope, clear intent, and comprehensive test coverage for both success and failure paths. You can customize Macroscope's approvability policy. Learn more. |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
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 `@apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts`:
- Around line 1345-1362: Extend the conflict case in the orchestration test
after the rejected system.run call to read the state for thread-conflict-b and
assert that it remains unchanged, specifically with no user message created.
Reuse the existing read-model/query helpers and fixtures used by the surrounding
tests rather than adding new test infrastructure.
🪄 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: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 8d03f288-021a-4d25-8b8e-e0c90e913b66
📒 Files selected for processing (3)
apps/server/src/orchestration/Errors.tsapps/server/src/orchestration/Layers/OrchestrationEngine.test.tsapps/server/src/orchestration/Layers/OrchestrationEngine.ts
Dismissing prior approval to re-evaluate 185a542
The orchestration engine deduplicates commands by command id alone: when a receipt exists, dispatch returns the stored result sequence without checking which aggregate the receipt belongs to. Reusing an accepted command id against another thread therefore reports success while creating nothing on the target thread. Compare the receipt's stored aggregate with the incoming command's aggregate and fail dispatch with a typed conflict error on mismatch. A genuine retry (same command id, same aggregate) still replays the stored receipt.
185a542 to
4c0aa1e
Compare
What Changed
Command dispatch now checks that an existing receipt belongs to the same aggregate as the incoming command before replaying it. On a mismatch it fails with a new typed
OrchestrationCommandIdConflictErrorinstead of returning the original command's accepted sequence. A genuine retry (same command id, same aggregate) still replays the stored receipt unchanged.No schema or migration changes: receipts already store
aggregateKind/aggregateId, the engine just never compared them.Why
Fixes #5231. The engine deduplicates commands by command id alone. If an accepted command id is reused for a different thread (client retry bug, id collision), dispatch returns HTTP 200 with the first command's sequence while creating nothing on the target thread — the caller reports delivery that never happened.
Verified with two new engine tests running the production persistence layers (real SQLite event store + receipt repository):
main(dispatch resolves with the first command's sequence)Both were mutation-tested: removing the aggregate comparison flips the first test red, forcing the conflict path flips the second red.
Not covered (called out in the issue as the fuller fix): reusing a command id with the same aggregate but a different payload or command type still replays the receipt. Receipts store no command fingerprint, so detecting that needs a new column; happy to follow up if you want it, but it seemed wrong to grow the receipt schema in this PR.
Checklist
Note
Medium Risk
Changes orchestration command deduplication in the engine, which affects whether clients see success vs failure for retried or colliding command ids; scope is focused and covered by integration tests.
Overview
Command dispatch no longer treats a stored receipt as proof of success when the incoming command targets a different aggregate than the receipt. Before replaying an accepted receipt, the engine now compares
aggregateKindandaggregateIdon the receipt with the incoming command; a mismatch yields a newOrchestrationCommandIdConflictErrorinstead of returning the original sequence.Genuine retries (same command id and same aggregate) still replay the accepted receipt and keep a single user message. Read-model reconciliation after dispatch failure is skipped for this conflict error, matching behavior for previously rejected commands.
Tests cover both the conflict path (second thread stays untouched) and the retry path (same sequence, no duplicate message).
Reviewed by Cursor Bugbot for commit 4c0aa1e. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Reject command dispatch when a command ID is reused for a different aggregate
OrchestrationCommandIdConflictErrorin Errors.ts to represent a command ID being reused across different aggregates.OrchestrationCommandIdConflictError, matching existing behavior forOrchestrationCommandPreviouslyRejectedError.Macroscope summarized 4c0aa1e.
Summary by CodeRabbit