Skip to content

feat: add ExtendHold to reservation-core#139

Open
skel84 wants to merge 3 commits intomainfrom
feat/reservation-extend-hold
Open

feat: add ExtendHold to reservation-core#139
skel84 wants to merge 3 commits intomainfrom
feat/reservation-extend-hold

Conversation

@skel84
Copy link
Copy Markdown
Owner

@skel84 skel84 commented Mar 26, 2026

Summary

  • add ExtendHold to reservation-core commands, codecs, snapshot encoding, and live state transitions
  • preserve correct expiry behavior by ignoring stale queued expiry entries after a hold deadline is extended
  • add recovery coverage proving an extended hold survives restart and later logical-slot progress

Verification

  • cargo test -p reservation-core
  • cargo clippy -p reservation-core --all-targets --all-features -- -D warnings
  • cargo fmt --all --check
  • cargo test
  • scripts/check_repo.sh

Refs #128
Closes #129

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

Summary by CodeRabbit

  • New Features
    • Add ability to extend hold deadlines; successful extensions update deadlines and reschedule auto-expiry without changing held/consumed capacity.
  • Bug Fixes
    • Prevents holds from auto-expiring at prior deadlines after an extension; rejects invalid or elapsed extension requests.
  • Tests
    • Recovery, snapshot, and unit tests verify deadline preservation, validation rules, queue re-scheduling, and expiry behavior.

Walkthrough

Adds an ExtendHold command (tag 6) with codec and snapshot support, state-machine handling that validates and updates hold deadlines (including queue rescheduling), and recovery/test additions ensuring extended deadlines persist through WAL replay.

Changes

Cohort / File(s) Summary
Command Definition
crates/reservation-core/src/command.rs
Added TAG_EXTEND_HOLD: u8 = 6 and Command::ExtendHold { hold_id: HoldId, deadline_slot: Slot }.
Binary Codec
crates/reservation-core/src/command_codec.rs
Encode/decode TAG_EXTEND_HOLD emitting/reading hold_id (u128 LE) then deadline_slot (u64 LE). Updated round-trip test to include ExtendHold.
Snapshot Serialization
crates/reservation-core/src/snapshot.rs
Snapshot encode/decode recognizes ExtendHold (tag 6) and fixture data updated to reflect extended deadline.
State Machine & Hold Queue
crates/reservation-core/src/state_machine.rs
Added apply_extend_hold with validation (missing/invalid state, expiry, non-increasing deadlines), updates/persists deadline, rebuilds hold_retire_queue, and adjusted retire_state to check current deadline before expiring. Added multiple tests covering success, rejection, and rescheduling.
Recovery Tests
crates/reservation-core/src/recovery.rs
New test helpers and recovery_preserves_extended_deadline_before_later_request asserting WAL replay preserves extended deadlines and recovered DB matches live DB.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant WAL as WAL
  participant SM as StateMachine
  participant DB as ReservationDb
  participant Queue as HoldRetireQueue
  participant REC as Recovery

  Client->>WAL: append(ClientCommand::ExtendHold)
  WAL-->>SM: deliver(ClientCommand::ExtendHold)
  SM->>DB: load hold by hold_id
  alt hold missing or invalid state
    DB-->>SM: not found / invalid state
    SM-->>Client: reject (HoldNotFound / InvalidState / HoldExpired)
  else valid held and deadline increase
    SM->>DB: update hold.deadline_slot
    SM->>Queue: rebuild_live_hold_retire_queue()
    DB-->>SM: persist OK
    SM-->>Client: OK
  end

  Note over REC, WAL: Recovery path (WAL replay)
  REC->>WAL: read frames
  WAL-->>REC: ClientCommand::ExtendHold frames
  REC->>DB: apply commands (including ExtendHold)
  REC->>Queue: rebuild_live_hold_retire_queue()
  REC-->>DB: recovered state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.00% 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 'feat: add ExtendHold to reservation-core' clearly and concisely summarizes the main feature addition across multiple files in the changeset.
Description check ✅ Passed The PR description covers all major sections including Summary, Verification steps, and linked issue references, though the optional Linked Issue, Changes, Validation checklist, Docs, and CodeRabbit Triage sections could be more detailed.
Linked Issues check ✅ Passed The PR implements ExtendHold across commands, codecs, snapshot encoding, and state transitions using extracted substrate, adding recovery tests that verify deadline preservation and correct expiry behavior as required by issue #129.
Out of Scope Changes check ✅ Passed All changes are focused on implementing ExtendHold command support and its integration across the reservation-core system with corresponding recovery tests.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/reservation-extend-hold

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

@skel84
Copy link
Copy Markdown
Owner Author

skel84 commented Mar 26, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
crates/reservation-core/src/snapshot.rs (1)

546-558: ⚠️ Potential issue | 🟡 Minor

Make the fixture reflect the post-extension state.

Line 550 still leaves the hold at Slot(5), while Lines 556-558 record a successful ExtendHold to Slot(7). That fixture cannot come from the live state machine, so the restore test no longer validates the extended-deadline state this PR is adding.

🧪 Minimal fix
             holds: vec![HoldRecord {
                 hold_id: HoldId(21),
                 pool_id: PoolId(11),
                 quantity: 2,
-                deadline_slot: Slot(5),
+                deadline_slot: Slot(7),
                 state: HoldState::Held,
             }],
As per coding guidelines, "Write extensive tests for every meaningful behavior change. Favor invariant tests, negative-path tests, recovery tests, and regression tests over shallow happy-path coverage."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/reservation-core/src/snapshot.rs` around lines 546 - 558, The
fixture's held record does not reflect the successful ExtendHold operation:
update the HoldRecord used in the snapshot (the holds vec containing HoldRecord
with HoldId(21), PoolId(11), etc.) so its deadline_slot is Slot(7) instead of
Slot(5) to match the OperationRecord that records Command::ExtendHold { hold_id:
HoldId(21), deadline_slot: Slot(7) }; ensure HoldState::Held remains unchanged
and no other fields are altered.
🧹 Nitpick comments (1)
crates/reservation-core/src/command_codec.rs (1)

198-205: Keep the old variant coverage when adding the new round-trip case.

Swapping the single test input to ExtendHold drops direct regression coverage for CreatePool. A small table-driven test over all Command variants would keep both existing tags and the new tag pinned.

🧪 Possible expansion
 #[test]
-fn internal_command_round_trips() {
-    let command = Command::ExtendHold {
-        hold_id: HoldId(7),
-        deadline_slot: Slot(9),
-    };
-
-    let decoded = decode_internal_command(&encode_internal_command(command)).unwrap();
-    assert_eq!(decoded, command);
+fn internal_commands_round_trip() {
+    let commands = [
+        Command::CreatePool {
+            pool_id: PoolId(5),
+            total_capacity: 9,
+        },
+        Command::PlaceHold {
+            pool_id: PoolId(5),
+            hold_id: HoldId(6),
+            quantity: 2,
+            deadline_slot: Slot(7),
+        },
+        Command::ConfirmHold { hold_id: HoldId(6) },
+        Command::ReleaseHold { hold_id: HoldId(6) },
+        Command::ExtendHold {
+            hold_id: HoldId(6),
+            deadline_slot: Slot(8),
+        },
+        Command::ExpireHold { hold_id: HoldId(6) },
+    ];
+
+    for command in commands {
+        let decoded = decode_internal_command(&encode_internal_command(command)).unwrap();
+        assert_eq!(decoded, command);
+    }
 }
As per coding guidelines, "Write extensive tests for every meaningful behavior change. Favor invariant tests, negative-path tests, recovery tests, and regression tests over shallow happy-path coverage."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/reservation-core/src/command_codec.rs` around lines 198 - 205, Replace
the single-case test in internal_command_round_trips with a table-driven loop
that iterates over all Command variants (including CreatePool and the new
ExtendHold), calling encode_internal_command and decode_internal_command for
each and asserting equality to preserve regression coverage; locate the test in
internal_command_round_trips and build a Vec or array of Command instances, then
for each element call
decode_internal_command(&encode_internal_command(cmd)).unwrap() and
assert_eq!(decoded, cmd).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/reservation-core/src/state_machine.rs`:
- Around line 477-479: The code currently always calls
self.push_hold_expiry(hold_id, deadline_slot) after updating hold.deadline_slot
and self.replace_hold(hold), which enqueues a duplicate entry into
hold_retire_queue and can hit RetireQueueError::Full (e.g., max_holds = 1) when
PlaceHold → ExtendHold occurs; change push_hold_expiry to first check for an
existing queued entry for hold_id and either update its deadline in-place or
skip adding a new entry (i.e., deduplicate keyed by hold_id), or provide an API
on the retire queue to reschedule an existing entry instead of pushing; update
the implementation around self.push_hold_expiry/self.replace_hold to reschedule
rather than append, and add a regression test (max_holds = 1) that PlaceHold
followed by ExtendHold does not panic and results in only a single retire entry
for the hold_id.

---

Outside diff comments:
In `@crates/reservation-core/src/snapshot.rs`:
- Around line 546-558: The fixture's held record does not reflect the successful
ExtendHold operation: update the HoldRecord used in the snapshot (the holds vec
containing HoldRecord with HoldId(21), PoolId(11), etc.) so its deadline_slot is
Slot(7) instead of Slot(5) to match the OperationRecord that records
Command::ExtendHold { hold_id: HoldId(21), deadline_slot: Slot(7) }; ensure
HoldState::Held remains unchanged and no other fields are altered.

---

Nitpick comments:
In `@crates/reservation-core/src/command_codec.rs`:
- Around line 198-205: Replace the single-case test in
internal_command_round_trips with a table-driven loop that iterates over all
Command variants (including CreatePool and the new ExtendHold), calling
encode_internal_command and decode_internal_command for each and asserting
equality to preserve regression coverage; locate the test in
internal_command_round_trips and build a Vec or array of Command instances, then
for each element call
decode_internal_command(&encode_internal_command(cmd)).unwrap() and
assert_eq!(decoded, cmd).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 912c9dea-3d58-45b9-be0d-ea96c35137e2

📥 Commits

Reviewing files that changed from the base of the PR and between 86ed8da and 480b4cb.

📒 Files selected for processing (5)
  • crates/reservation-core/src/command.rs
  • crates/reservation-core/src/command_codec.rs
  • crates/reservation-core/src/recovery.rs
  • crates/reservation-core/src/snapshot.rs
  • crates/reservation-core/src/state_machine.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: semgrep-cloud-platform/scan
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Write extensive tests for every meaningful behavior change. Favor invariant tests, negative-path tests, recovery tests, and regression tests over shallow happy-path coverage.
Add extensive logging where it materially improves debuggability or operational clarity. Use the right log level: error for invariant breaks, corruption, and failed operations that require intervention; warn for degraded but expected conditions such as overload, lag, or rejected requests; info for meaningful lifecycle and state-transition events; debug for detailed execution traces useful in development; trace only for very high-volume diagnostic detail.
Logging must be structured and purposeful. Do not add noisy logs that obscure signal or hide bugs.

Files:

  • crates/reservation-core/src/command_codec.rs
  • crates/reservation-core/src/command.rs
  • crates/reservation-core/src/snapshot.rs
  • crates/reservation-core/src/recovery.rs
  • crates/reservation-core/src/state_machine.rs
🔇 Additional comments (5)
crates/reservation-core/src/command.rs (1)

8-8: LGTM.

The new tag and explicit Command::ExtendHold variant keep the command surface and wire tags aligned.

Also applies to: 47-50

crates/reservation-core/src/snapshot.rs (1)

369-376: LGTM.

ExtendHold is encoded and decoded with the same field order, so snapshot round-tripping stays symmetric.

Also applies to: 402-405

crates/reservation-core/src/command_codec.rs (1)

82-89: LGTM.

The new wire tag is serialized and parsed consistently, so ExtendHold round-trips cleanly through the codec.

Also applies to: 115-118

crates/reservation-core/src/state_machine.rs (1)

732-734: Good stale-expiry guard.

This check prevents an old queued deadline from expiring a hold before its current deadline_slot.

crates/reservation-core/src/recovery.rs (1)

306-339: Nice recovery regression.

The helpers keep the scenario deterministic, and the new test proves an extended hold survives replay plus later logical-slot advancement.

Also applies to: 521-620

@skel84
Copy link
Copy Markdown
Owner Author

skel84 commented Mar 27, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/reservation-core/src/state_machine.rs (1)

452-459: Factor the expire-and-release transition into a helper.

This block is now another copy of the same held_capacity decrement + HoldState::Expired mutation already present in apply_confirm_hold and retire_state. A small helper would keep those paths aligned the next time expiry side effects change.

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

In `@crates/reservation-core/src/state_machine.rs` around lines 452 - 459, Extract
the repeated logic that decrements pool.held_capacity, sets hold.state =
HoldState::Expired and calls self.replace_hold(hold) into a single helper method
(e.g., expire_and_release_hold(&mut self, hold: &mut Hold) or similar) and call
it from this expiration branch as well as from apply_confirm_hold and
retire_state; ensure the helper locates the pool via
self.pools.get_mut(hold.pool_id) (preserving the expect(...) behavior or
returning a Result) and performs the held_capacity -= hold.quantity, updates
hold.state to HoldState::Expired, and invokes self.replace_hold(hold) so all
paths share the same side-effect sequence.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/reservation-core/src/state_machine.rs`:
- Around line 1207-1421: Add tests that cover the two missing ExtendHold failure
branches: (1) the exact-deadline late-arrival path in ExtendHold (the branch
around lines 452-465) by creating a Hold with deadline_slot == current slot and
asserting apply_client(context(...), Command::ExtendHold{...}) returns
ResultCode::InvalidState and does not change the hold, and (2) the non-Held
guard in ExtendHold (the branch around lines 442-449) by attempting ExtendHold
on a hold whose state is Released or Consumed and asserting
ResultCode::InvalidState and no state/deadline change; use
ReservationDb::apply_client, Command::ExtendHold, HoldId, Slot, and verify via
db.snapshot().holds and db.hold_retire_queue as in existing tests.

---

Nitpick comments:
In `@crates/reservation-core/src/state_machine.rs`:
- Around line 452-459: Extract the repeated logic that decrements
pool.held_capacity, sets hold.state = HoldState::Expired and calls
self.replace_hold(hold) into a single helper method (e.g.,
expire_and_release_hold(&mut self, hold: &mut Hold) or similar) and call it from
this expiration branch as well as from apply_confirm_hold and retire_state;
ensure the helper locates the pool via self.pools.get_mut(hold.pool_id)
(preserving the expect(...) behavior or returning a Result) and performs the
held_capacity -= hold.quantity, updates hold.state to HoldState::Expired, and
invokes self.replace_hold(hold) so all paths share the same side-effect
sequence.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 20c5fc49-c7cd-4394-abd2-3103e76ebec9

📥 Commits

Reviewing files that changed from the base of the PR and between 480b4cb and ca5ed3a.

📒 Files selected for processing (1)
  • crates/reservation-core/src/state_machine.rs
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: semgrep-cloud-platform/scan
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Write extensive tests for every meaningful behavior change. Favor invariant tests, negative-path tests, recovery tests, and regression tests over shallow happy-path coverage.
Add extensive logging where it materially improves debuggability or operational clarity. Use the right log level: error for invariant breaks, corruption, and failed operations that require intervention; warn for degraded but expected conditions such as overload, lag, or rejected requests; info for meaningful lifecycle and state-transition events; debug for detailed execution traces useful in development; trace only for very high-volume diagnostic detail.
Logging must be structured and purposeful. Do not add noisy logs that obscure signal or hide bugs.

Files:

  • crates/reservation-core/src/state_machine.rs
🔇 Additional comments (1)
crates/reservation-core/src/state_machine.rs (1)

551-574: Nice fix for the reschedule edge case.

Rebuilding the live hold-expiry queue from current Held records, plus the stale-entry guard at Lines 757-759, closes the duplicate-entry/full-queue failure mode cleanly. The max_holds = 1 regression also does a good job pinning it down.

Also applies to: 757-759, 1372-1421

Comment on lines +1207 to +1421
#[test]
fn extend_hold_moves_deadline_forward_without_changing_capacity() {
let mut db = ReservationDb::new(config()).unwrap();
db.apply_client(
context(1, 1),
request(
1,
Command::CreatePool {
pool_id: PoolId(11),
total_capacity: 10,
},
),
);
db.apply_client(
context(2, 2),
request(
2,
Command::PlaceHold {
pool_id: PoolId(11),
hold_id: HoldId(21),
quantity: 3,
deadline_slot: Slot(5),
},
),
);

let outcome = db.apply_client(
context(3, 3),
request(
3,
Command::ExtendHold {
hold_id: HoldId(21),
deadline_slot: Slot(9),
},
),
);

assert_eq!(outcome.result_code, ResultCode::Ok);
assert_eq!(db.snapshot().pools[0].held_capacity, 3);
assert_eq!(db.snapshot().pools[0].consumed_capacity, 0);
assert_eq!(db.snapshot().holds[0].deadline_slot, Slot(9));
assert_eq!(db.snapshot().holds[0].state, HoldState::Held);
}

#[test]
fn extend_hold_rejects_elapsed_or_non_increasing_deadline() {
let mut db = ReservationDb::new(config()).unwrap();
db.apply_client(
context(1, 1),
request(
1,
Command::CreatePool {
pool_id: PoolId(11),
total_capacity: 10,
},
),
);
db.apply_client(
context(2, 2),
request(
2,
Command::PlaceHold {
pool_id: PoolId(11),
hold_id: HoldId(21),
quantity: 3,
deadline_slot: Slot(8),
},
),
);

let stale = db.apply_client(
context(3, 3),
request(
3,
Command::ExtendHold {
hold_id: HoldId(21),
deadline_slot: Slot(3),
},
),
);
let non_increasing = db.apply_client(
context(4, 4),
request(
4,
Command::ExtendHold {
hold_id: HoldId(21),
deadline_slot: Slot(8),
},
),
);

assert_eq!(stale.result_code, ResultCode::InvalidState);
assert_eq!(non_increasing.result_code, ResultCode::InvalidState);
assert_eq!(db.snapshot().holds[0].deadline_slot, Slot(8));
assert_eq!(db.snapshot().holds[0].state, HoldState::Held);
}

#[test]
fn extended_hold_does_not_auto_expire_at_old_deadline() {
let mut db = ReservationDb::new(config()).unwrap();
db.apply_client(
context(1, 1),
request(
1,
Command::CreatePool {
pool_id: PoolId(11),
total_capacity: 10,
},
),
);
db.apply_client(
context(2, 2),
request(
2,
Command::PlaceHold {
pool_id: PoolId(11),
hold_id: HoldId(21),
quantity: 3,
deadline_slot: Slot(5),
},
),
);
db.apply_client(
context(3, 3),
request(
3,
Command::ExtendHold {
hold_id: HoldId(21),
deadline_slot: Slot(10),
},
),
);

let outcome = db.apply_client(
context(4, 8),
request(
4,
Command::CreatePool {
pool_id: PoolId(12),
total_capacity: 1,
},
),
);

assert_eq!(outcome.result_code, ResultCode::Ok);
assert_eq!(
db.snapshot()
.holds
.iter()
.find(|record| record.hold_id == HoldId(21))
.unwrap()
.state,
HoldState::Held
);
assert_eq!(
db.snapshot()
.holds
.iter()
.find(|record| record.hold_id == HoldId(21))
.unwrap()
.deadline_slot,
Slot(10)
);
}

#[test]
fn extend_hold_reschedules_without_overfilling_single_hold_queue() {
let mut config = config();
config.max_holds = 1;
let mut db = ReservationDb::new(config).unwrap();
db.apply_client(
context(1, 1),
request(
1,
Command::CreatePool {
pool_id: PoolId(11),
total_capacity: 10,
},
),
);
db.apply_client(
context(2, 2),
request(
2,
Command::PlaceHold {
pool_id: PoolId(11),
hold_id: HoldId(21),
quantity: 3,
deadline_slot: Slot(5),
},
),
);

let outcome = db.apply_client(
context(3, 3),
request(
3,
Command::ExtendHold {
hold_id: HoldId(21),
deadline_slot: Slot(10),
},
),
);

assert_eq!(outcome.result_code, ResultCode::Ok);
assert_eq!(
db.hold_retire_queue.front(),
Some(allocdb_retire_queue::RetireEntry {
key: HoldId(21),
retire_after_slot: Slot(10),
})
);
assert_eq!(db.hold_retire_queue.pop_front().unwrap().key, HoldId(21));
assert_eq!(db.hold_retire_queue.pop_front(), None);
}
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 | 🟡 Minor

Add regressions for the remaining ExtendHold failure branches.

The new tests only pin successful extension, invalid proposed deadlines, and queue rescheduling. The exact-deadline late-arrival path at Lines 452-465 and the non-Held guards at Lines 442-449 are still unpinned, and those are the branches most likely to drift because they encode the new business rules.

🧪 Suggested regression to add at minimum
+    #[test]
+    fn extend_hold_at_deadline_expires_hold() {
+        let mut db = ReservationDb::new(config()).unwrap();
+        db.apply_client(
+            context(1, 1),
+            request(
+                1,
+                Command::CreatePool {
+                    pool_id: PoolId(11),
+                    total_capacity: 10,
+                },
+            ),
+        );
+        db.apply_client(
+            context(2, 2),
+            request(
+                2,
+                Command::PlaceHold {
+                    pool_id: PoolId(11),
+                    hold_id: HoldId(21),
+                    quantity: 3,
+                    deadline_slot: Slot(5),
+                },
+            ),
+        );
+
+        let outcome = db.apply_client(
+            context(3, 5),
+            request(
+                3,
+                Command::ExtendHold {
+                    hold_id: HoldId(21),
+                    deadline_slot: Slot(9),
+                },
+            ),
+        );
+
+        assert_eq!(outcome.result_code, ResultCode::HoldExpired);
+        assert_eq!(db.snapshot().pools[0].held_capacity, 0);
+        assert_eq!(db.snapshot().holds[0].state, HoldState::Expired);
+    }

As per coding guidelines, "Write extensive tests for every meaningful behavior change. Favor invariant tests, negative-path tests, recovery tests, and regression tests over shallow happy-path coverage."

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

In `@crates/reservation-core/src/state_machine.rs` around lines 1207 - 1421, Add
tests that cover the two missing ExtendHold failure branches: (1) the
exact-deadline late-arrival path in ExtendHold (the branch around lines 452-465)
by creating a Hold with deadline_slot == current slot and asserting
apply_client(context(...), Command::ExtendHold{...}) returns
ResultCode::InvalidState and does not change the hold, and (2) the non-Held
guard in ExtendHold (the branch around lines 442-449) by attempting ExtendHold
on a hold whose state is Released or Consumed and asserting
ResultCode::InvalidState and no state/deadline change; use
ReservationDb::apply_client, Command::ExtendHold, HoldId, Slot, and verify via
db.snapshot().holds and db.hold_retire_queue as in existing tests.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/reservation-core/src/command_codec.rs (1)

199-222: Add wire-layout invariant and negative-path tests for ExtendHold.

The loop in Line 199-222 proves round-trip symmetry, but it won’t catch accidental wire-format drift (tag/field order/width) if encode+decode change together. Please add an explicit byte-layout assertion and a truncated TAG_EXTEND_HOLD decode failure test.

♻️ Proposed test additions
 #[test]
 fn internal_command_round_trips() {
@@
     for command in commands {
         let decoded = decode_internal_command(&encode_internal_command(command)).unwrap();
         assert_eq!(decoded, command);
     }
 }
+
+#[test]
+fn extend_hold_internal_command_wire_layout_is_stable() {
+    let encoded = encode_internal_command(Command::ExtendHold {
+        hold_id: HoldId(0x0102_0304_0506_0708_090A_0B0C_0D0E_0F10),
+        deadline_slot: Slot(0x1112_1314_1516_1718),
+    });
+
+    let mut expected = vec![crate::command::TAG_EXTEND_HOLD];
+    expected.extend_from_slice(
+        &0x0102_0304_0506_0708_090A_0B0C_0D0E_0F10_u128.to_le_bytes(),
+    );
+    expected.extend_from_slice(&0x1112_1314_1516_1718_u64.to_le_bytes());
+    assert_eq!(encoded, expected);
+}
+
+#[test]
+fn rejects_truncated_extend_hold_internal_payload() {
+    let bytes = [crate::command::TAG_EXTEND_HOLD, 0_u8; 10];
+    let error = decode_internal_command(&bytes).unwrap_err();
+    assert_eq!(error, CommandCodecError::BufferTooShort);
+}

As per coding guidelines: **/*.rs: Write extensive tests for every meaningful behavior change. Favor invariant tests, negative-path tests, recovery tests, and regression tests over shallow happy-path coverage.

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

In `@crates/reservation-core/src/command_codec.rs` around lines 199 - 222, Add a
targeted invariant and negative-path test for Command::ExtendHold: after
encoding an ExtendHold value via encode_internal_command, assert the produced
bytes exactly equal a hard-coded expected byte array representing the canonical
wire layout (use the same fields/values as the existing test) to lock the
tag/field order/width; then create a truncated slice starting with the
TAG_EXTEND_HOLD bytes (use the TAG_EXTEND_HOLD constant) but missing trailing
bytes and assert decode_internal_command on that truncated input returns an
error (unwrap_err / matches Err) to ensure truncated/tag decode fails. Name the
test to reflect wire-layout and truncated-decode checks and place it alongside
the existing round-trip tests referencing encode_internal_command,
decode_internal_command, Command::ExtendHold, and TAG_EXTEND_HOLD.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/reservation-core/src/command_codec.rs`:
- Around line 199-222: Add a targeted invariant and negative-path test for
Command::ExtendHold: after encoding an ExtendHold value via
encode_internal_command, assert the produced bytes exactly equal a hard-coded
expected byte array representing the canonical wire layout (use the same
fields/values as the existing test) to lock the tag/field order/width; then
create a truncated slice starting with the TAG_EXTEND_HOLD bytes (use the
TAG_EXTEND_HOLD constant) but missing trailing bytes and assert
decode_internal_command on that truncated input returns an error (unwrap_err /
matches Err) to ensure truncated/tag decode fails. Name the test to reflect
wire-layout and truncated-decode checks and place it alongside the existing
round-trip tests referencing encode_internal_command, decode_internal_command,
Command::ExtendHold, and TAG_EXTEND_HOLD.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 03f9d027-216f-4f44-9223-49d186ff34a7

📥 Commits

Reviewing files that changed from the base of the PR and between ca5ed3a and 9029bd2.

📒 Files selected for processing (2)
  • crates/reservation-core/src/command_codec.rs
  • crates/reservation-core/src/snapshot.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/reservation-core/src/snapshot.rs
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (AGENTS.md)

**/*.rs: Write extensive tests for every meaningful behavior change. Favor invariant tests, negative-path tests, recovery tests, and regression tests over shallow happy-path coverage.
Add extensive logging where it materially improves debuggability or operational clarity. Use the right log level: error for invariant breaks, corruption, and failed operations that require intervention; warn for degraded but expected conditions such as overload, lag, or rejected requests; info for meaningful lifecycle and state-transition events; debug for detailed execution traces useful in development; trace only for very high-volume diagnostic detail.
Logging must be structured and purposeful. Do not add noisy logs that obscure signal or hide bugs.

Files:

  • crates/reservation-core/src/command_codec.rs
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: skel84/allocdb PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-12T15:18:53.086Z
Learning: Treat CodeRabbit as part of the required review path when it is enabled on the repository. Wait for its status to complete before merge. If it completes without a visible review comment or review thread, request visible output with `coderabbitai summary`. Address every substantive CodeRabbit comment explicitly before merge by either applying the change or documenting why it is not being applied. Apply correctness, safety, recovery, test, and docs-alignment feedback by default; document why you reject suggestions that would weaken determinism, boundedness, or trusted-core discipline.
🔇 Additional comments (1)
crates/reservation-core/src/command_codec.rs (1)

82-89: ExtendHold codec encode/decode mapping looks correct.

Line 82-89 and Line 115-118 are consistent on tag, field order, and primitive widths (u128 hold id, u64 deadline slot). Nice, deterministic wire mapping.

Also applies to: 115-118

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

M14-T01: Build one new engine or engine slice against the extracted substrate

1 participant