diff --git a/CHANGELOG.md b/CHANGELOG.md index fe2304f6..137d360b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,10 @@ `wsc_retained_evidence_export_modes` witness, keeping retained-evidence WSC export coverage in the release gate without using Cargo's slow package-level name filter. +- `cargo xtask test-slice durability-release` now includes the exact + `retained_reading_missing_payload_is_not_empty_success` witness, locking the + app-safe missing-retention posture for reading payloads, reading envelopes, + and retained receipt support. - `warp-core` trusted runtime hosts now configure runtime WAL through `TrustedRuntimeWalConfig`, including in-memory and filesystem-backed adapters. `TrustedRuntimeWalStoreKind` exposes the configured adapter kind as diff --git a/crates/warp-core/tests/retained_evidence_ref_tests.rs b/crates/warp-core/tests/retained_evidence_ref_tests.rs index 97f9e135..5b8f3dfb 100644 --- a/crates/warp-core/tests/retained_evidence_ref_tests.rs +++ b/crates/warp-core/tests/retained_evidence_ref_tests.rs @@ -8,9 +8,11 @@ use warp_core::causal_wal::{ RecoveredRetentionIndexError, RetainedMaterialKind, RetainedMaterialRecord, }; use warp_core::{ - ContractEvidenceIdentity, ContractObstructionKind, ContractObstructionSubject, - ContractOperationKind, InstalledContractPackageId, RetainedEvidenceCoordinate, - RetainedEvidencePosture, RetainedEvidenceRef, RetainedEvidenceRole, + make_head_id, ContractEvidenceIdentity, ContractObstructionKind, ContractObstructionSubject, + ContractOperationKind, GlobalTick, InstalledContractPackageId, IntentOutcome, + IntentOutcomeDecision, IntentOutcomeObservation, ReceiptCorrelationRecord, + RetainedEvidenceCoordinate, RetainedEvidencePosture, RetainedEvidenceRef, RetainedEvidenceRole, + TickReceiptRejection, WorldlineId, WorldlineTick, WriterHeadKey, }; fn hash(seed: u8) -> [u8; 32] { @@ -58,6 +60,41 @@ fn semantic_blob_coordinate(role: RetainedBlobRole, semantic_seed: u8) -> Semant } } +fn writer_head(seed: u8) -> WriterHeadKey { + WriterHeadKey { + worldline_id: WorldlineId::from_bytes(hash(seed)), + head_id: make_head_id(&format!("retained-evidence-head-{seed}")), + } +} + +fn receipt_correlation(contract: ContractEvidenceIdentity) -> ReceiptCorrelationRecord { + ReceiptCorrelationRecord { + ticketed_ingress_id: hash(21), + submission_id: hash(22), + ticket_digest: hash(23), + ingress_id: hash(24), + head_key: writer_head(25), + contract: Some(contract), + commit_global_tick: GlobalTick::from_raw(26), + worldline_tick_after: WorldlineTick::from_raw(27), + tick_receipt_digest: hash(28), + commit_hash: hash(29), + } +} + +fn assert_missing_retention(posture: &RetainedEvidencePosture) { + assert_eq!( + posture.obstruction().map(|obstruction| obstruction.kind), + Some(ContractObstructionKind::MissingRetention) + ); + assert!(matches!( + posture + .obstruction() + .map(|obstruction| &obstruction.subject), + Some(ContractObstructionSubject::Retention { .. }) + )); +} + #[test] fn retained_evidence_ref_id_binds_semantic_coordinate() { let content_hash = content_hash(b"same bytes"); @@ -158,6 +195,146 @@ fn missing_content_returns_missing_retention_obstruction_with_ref_id() { ); } +#[test] +fn retained_reading_missing_payload_is_not_empty_success() { + let query_contract = contract(14, 15, ContractOperationKind::Query); + let reading_id = hash(16); + let payload_bytes = b"retained query payload bytes"; + let envelope_coordinate = RetainedEvidenceCoordinate::new( + query_contract.clone(), + RetainedEvidenceRole::ReadingEnvelope, + reading_id, + ); + let payload_ref = RetainedEvidenceRef::new( + RetainedEvidenceCoordinate::new( + query_contract, + RetainedEvidenceRole::ReadingPayload, + reading_id, + ), + content_hash(payload_bytes), + payload_bytes.len() as u64, + ); + let reading_postures = [ + RetainedEvidencePosture::missing_coordinate(&envelope_coordinate), + RetainedEvidencePosture::missing_content(&payload_ref), + ]; + + assert_eq!(reading_postures.len(), 2); + assert!(matches!( + &reading_postures[0], + RetainedEvidencePosture::MissingCoordinate { .. } + )); + if let RetainedEvidencePosture::MissingCoordinate { + coordinate, + obstruction, + } = &reading_postures[0] + { + assert_eq!(coordinate.role, RetainedEvidenceRole::ReadingEnvelope); + assert_eq!(coordinate.semantic_digest, reading_id); + assert_eq!(obstruction.kind, ContractObstructionKind::MissingRetention); + assert_eq!( + obstruction.subject, + ContractObstructionSubject::Retention { + retention_id: coordinate.coordinate_id() + } + ); + } + assert!(matches!( + &reading_postures[1], + RetainedEvidencePosture::MissingContent { .. } + )); + if let RetainedEvidencePosture::MissingContent { + reference, + obstruction, + } = &reading_postures[1] + { + assert_eq!( + reference.coordinate.role, + RetainedEvidenceRole::ReadingPayload + ); + assert_eq!(reference.coordinate.semantic_digest, reading_id); + assert_eq!(reference.content_hash, content_hash(payload_bytes)); + assert_eq!(reference.byte_len, payload_bytes.len() as u64); + assert_eq!(obstruction.kind, ContractObstructionKind::MissingRetention); + assert_eq!( + obstruction.subject, + ContractObstructionSubject::Retention { + retention_id: reference.evidence_ref_id() + } + ); + } + for posture in &reading_postures { + assert_missing_retention(posture); + } + + let receipt_contract = contract(17, 18, ContractOperationKind::Mutation); + let correlation = receipt_correlation(receipt_contract.clone()); + let applied = IntentOutcome::from_observation(IntentOutcomeObservation::Decided { + correlation: Box::new(correlation.clone()), + decision: IntentOutcomeDecision::Applied { + receipt_entry_index: 3, + rule_id: hash(30), + }, + }); + let rejected = IntentOutcome::from_observation(IntentOutcomeObservation::Decided { + correlation: Box::new(correlation.clone()), + decision: IntentOutcomeDecision::Rejected { + receipt_entry_index: 4, + rule_id: hash(31), + reason: TickReceiptRejection::FootprintConflict, + blocked_by: vec![3], + }, + }); + + for outcome in [&applied, &rejected] { + assert!(matches!( + outcome, + IntentOutcome::Applied { .. } | IntentOutcome::Rejected { .. } + )); + if let IntentOutcome::Applied { receipt, .. } | IntentOutcome::Rejected { receipt, .. } = + outcome + { + assert_eq!(receipt.contract, Some(receipt_contract.clone())); + assert!(matches!( + receipt.retained_evidence.as_slice(), + [RetainedEvidencePosture::MissingCoordinate { .. }] + )); + if let [RetainedEvidencePosture::MissingCoordinate { + coordinate, + obstruction, + }] = receipt.retained_evidence.as_slice() + { + assert_eq!(coordinate.contract, receipt_contract); + assert_eq!(coordinate.role, RetainedEvidenceRole::ContractReceipt); + assert_eq!(coordinate.semantic_digest, correlation.tick_receipt_digest); + assert_eq!(obstruction.kind, ContractObstructionKind::MissingRetention); + } + } + } + assert!(matches!( + rejected, + IntentOutcome::Rejected { + reason: TickReceiptRejection::FootprintConflict, + ref blocked_by, + .. + } if blocked_by == &[3] + )); + + let no_matching_receipt = IntentOutcome::from_observation(IntentOutcomeObservation::Decided { + correlation: Box::new(correlation), + decision: IntentOutcomeDecision::NoMatchingReceiptEntry { + tick_receipt_digest: hash(28), + }, + }); + assert!(matches!( + no_matching_receipt, + IntentOutcome::Obstructed { + obstruction, + .. + } if obstruction.kind == ContractObstructionKind::MissingRetention + )); +} + #[test] fn available_retained_evidence_is_not_an_obstruction() { let reference = RetainedEvidenceRef::new( diff --git a/docs/workflows.md b/docs/workflows.md index 31d76c77..2b6cd8f4 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -164,7 +164,7 @@ The repo also exposes maintenance commands via `cargo xtask …`: - `cargo xtask test-slice contract-path-release` runs the v0.1 local contract-host release witness: installed contract pipeline replay, reference trusted host loop, and the serious external consumer fixture. - `cargo xtask test-slice runtime-wal-ack` runs the fast runtime WAL-backed ACK witness: app-facing acceptance rollback, scheduler tick receipt invariant checks, scheduler tick commit-before-publish, recovered indexes, CLI submission posture JSON, stale-claim guard, and generated man-page check. - `cargo xtask test-slice durable-runtime-wal` runs the release-grade filesystem runtime WAL durability witness: filesystem ACK recovery, filesystem failure atomicity, CLI submission posture JSON, stale-claim guard, and generated man-page check. -- `cargo xtask test-slice durability-release` runs the joined WAL/WSC release witness: filesystem runtime WAL durability, WSC retained evidence recovery, WSC topology recovery, topology WAL recovery, typed missing-material obstruction, stale-claim guards, doctrine checks, and generated man-page freshness. This is a release-gate slice, not the fastest local edit loop. +- `cargo xtask test-slice durability-release` runs the joined WAL/WSC release witness: filesystem runtime WAL durability, WSC retained evidence recovery, app-safe missing-retention posture, WSC topology recovery, topology WAL recovery, typed missing-material obstruction, stale-claim guards, doctrine checks, and generated man-page freshness. This is a release-gate slice, not the fastest local edit loop. - `cargo xtask pr-preflight` runs the default changed-scope pre-PR gate against `origin/main`. - `cargo xtask pr-preflight --full` runs the broader explicit full pre-PR gate. - `cargo xtask dind` runs the DIND (Deterministic Ironclad Nightmare Drills) harness locally. diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 12bccbff..fabe7286 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -793,6 +793,14 @@ fn build_test_slice_commands(slice: TestSlice) -> Vec { "causal_wal_tests", "wsc_retained_evidence_export_modes", ]), + cargo_command([ + "test", + "-p", + "warp-core", + "--test", + "retained_evidence_ref_tests", + "retained_reading_missing_payload_is_not_empty_success", + ]), cargo_command([ "test", "-p", @@ -6711,7 +6719,7 @@ mod tests { #[test] fn test_slice_durability_release_stays_explicit() { let commands = build_test_slice_commands(TestSlice::DurabilityRelease); - assert_eq!(commands.len(), 12); + assert_eq!(commands.len(), 13); let expected = [ ( @@ -6806,6 +6814,17 @@ mod tests { "wsc_retained_evidence_export_modes", ], ), + ( + "cargo", + vec![ + "test", + "-p", + "warp-core", + "--test", + "retained_evidence_ref_tests", + "retained_reading_missing_payload_is_not_empty_success", + ], + ), ( "cargo", vec![