From 57c88813b2a0c526f09980788b321af8d8f041ab Mon Sep 17 00:00:00 2001 From: wchwawa Date: Thu, 22 Oct 2026 00:21:00 +1100 Subject: [PATCH] test(client): pin the claim term of the inline receipt check records_inline_publication accepts a stored receipt only when its generation follows the request's claim. No test failed with that term removed, because the reused-identity tests change the body or the revision, which the other terms already refuse. The new test publishes create-only, then reuses the operation id, revision and body with ReplaceOnly { expected_generation: 1 }. The stored receipt matches every other input, and its generation 1 does not follow a replace of generation 1, so only the claim term refuses it. The client keeps the server's RequestReplayMismatch, aborts nothing, and the path keeps generation 1 and the first body. With the term removed, the client returns the create-only receipt as a replayed success instead. Signed-off-by: wchwawa --- crates/nokv-client/src/artifact.rs | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/nokv-client/src/artifact.rs b/crates/nokv-client/src/artifact.rs index 7778a5bbc..456a0a92e 100644 --- a/crates/nokv-client/src/artifact.rs +++ b/crates/nokv-client/src/artifact.rs @@ -5222,6 +5222,43 @@ mod tests { } } + #[test] + fn an_inline_identity_reused_with_another_claim_is_refused_without_a_stale_receipt() { + let events = Arc::new(Mutex::new(Vec::new())); + let transport = ScriptedArtifactTransport::new(Arc::clone(&events), false); + let inspector = transport.clone(); + let client = client(transport); + let store = RecordingStore::new(events, None); + client + .publish_artifact( + &store, + publish_options(DEFAULT_ARTIFACT_BLOCK_SIZE), + b"first", + ) + .unwrap(); + + // Only the claim differs: the create-only receipt records this + // operation, target, revision, and body, but its generation 1 does not + // follow a replace of generation 1. + let mut options = publish_options(DEFAULT_ARTIFACT_BLOCK_SIZE); + options.condition = PublishCondition::ReplaceOnly { + expected_generation: 1, + }; + let error = client + .publish_artifact(&store, options, b"first") + .unwrap_err(); + + expect_inline_failure_without_abort(&error, ErrorCode::RequestReplayMismatch); + let state = inspector.state(); + assert_eq!(state.abort_applies, 0); + let path = state.path.as_ref().unwrap(); + assert_eq!( + path.metadata.as_ref().map(|metadata| metadata.generation), + Some(1) + ); + assert_eq!(path.inline_body.as_deref(), Some(&b"first"[..])); + } + #[test] fn an_inline_publication_whose_response_was_lost_returns_its_receipt_as_a_replay() { let events = Arc::new(Mutex::new(Vec::new()));