From efba32a66251339840b7969b6e8f8885cff7f702 Mon Sep 17 00:00:00 2001 From: thinkinginmath Date: Wed, 16 Sep 2026 16:54:51 +0000 Subject: [PATCH] Fix swapped labels in the command type mismatch message CommandTypeMismatchError printed the handler's command as what the previous execution recorded, and the recorded command as what the current execution attempts. Its callers construct it with the recorded command as `actual` and the handler's command as `expected` (RawMessage::decode_to during replay, and the lazy/eager state replay paths), so swap the two arguments in Display and document the fields. The regression test replays a recorded Run at index 1 while the handler writes output, and checks both labels in the ErrorMessage. Co-Authored-By: Claude Opus 5 (1M context) --- src/tests/failures.rs | 45 +++++++++++++++++++++++++++++++++++++++++++ src/vm/errors.rs | 6 ++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/tests/failures.rs b/src/tests/failures.rs index cc5055f..c60dcce 100644 --- a/src/tests/failures.rs +++ b/src/tests/failures.rs @@ -217,6 +217,51 @@ mod journal_mismatch { ); } + /// Replays a journal whose next recorded command has a different type than the one the + /// handler issues. The message must name the recorded command as what the previous + /// execution recorded, and the handler's command as what this execution attempts. + #[test] + fn command_type_mismatch_names_recorded_and_attempted_commands() { + let mut output = VMTestCase::new() + .input(start_message(2)) + .input(input_entry_message(b"my-data")) + .input(RunCommandMessage { + result_completion_id: 1, + name: "my-side-effect".to_owned(), + }) + .run(|vm| { + vm.sys_input().unwrap(); + + // The journal recorded a run at index 1, but the handler returns instead + let error = vm + .sys_write_output( + NonEmptyValue::Success(Bytes::from_static(b"done")), + PayloadOptions::default(), + ) + .unwrap_err(); + assert_eq!(error.code(), vm::errors::codes::JOURNAL_MISMATCH.code()); + }); + + let error_message = output.next_decoded::().unwrap(); + assert_eq!( + error_message.code, + vm::errors::codes::JOURNAL_MISMATCH.code() as u32 + ); + assert_that!( + error_message.message, + contains_substring( + "The previous execution ran and recorded the following: 'run' (index '1')" + ) + ); + assert_that!( + error_message.message, + contains_substring( + "The current execution attempts to perform the following: 'handler return'" + ) + ); + assert_eq!(output.next(), None); + } + fn expect_mismatch_on_replay< M: RestateMessage + RestateEncodableMessage + CommandMessageHeaderDiff + Clone, T: fmt::Debug, diff --git a/src/vm/errors.rs b/src/vm/errors.rs index 924411e..a9ab31a 100644 --- a/src/vm/errors.rs +++ b/src/vm/errors.rs @@ -179,8 +179,10 @@ impl std::error::Error for ClosedError {} #[derive(Debug)] pub struct CommandTypeMismatchError { + /// Type of the command recorded in the journal at `command_index`. actual: MessageType, command_index: i64, + /// Type of the command the handler issues now, during replay. expected: MessageType, } @@ -205,9 +207,9 @@ impl fmt::Display for CommandTypeMismatchError { This typically happens when some parts of the code are non-deterministic. - The previous execution ran and recorded the following: '{}' (index '{}') - The current execution attempts to perform the following: '{}'", - self.expected, - self.command_index, self.actual, + self.command_index, + self.expected, ) } }