diff --git a/crates/psyche-coven/src/port.rs b/crates/psyche-coven/src/port.rs index 4079e8a..894f4a0 100644 --- a/crates/psyche-coven/src/port.rs +++ b/crates/psyche-coven/src/port.rs @@ -18,6 +18,7 @@ const EXECUTION_REQUEST_SCHEMA: &str = "psyche.execution_request.v1"; const MAX_STRING_BYTES: usize = 255; const MAX_ARTIFACTS: usize = 1024; const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991; +const MAX_CONTENT_SIZE_BYTES: u64 = i64::MAX as u64; /// A capability that a Coven implementation may advertise. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -810,7 +811,8 @@ impl ContentAddressedReference { /// Validates metadata only; this does not attest payload bytes. pub fn validate(&self) -> Result<(), PortError> { validate_media_type(&self.media_type)?; - if self.size_bytes == 0 || self.size_bytes > MAX_SAFE_INTEGER || !utc(self.expires_at) { + if self.size_bytes == 0 || self.size_bytes > MAX_CONTENT_SIZE_BYTES || !utc(self.expires_at) + { return Err(PortError::InvalidRequest); } Ok(()) diff --git a/crates/psyche-coven/tests/bindings.rs b/crates/psyche-coven/tests/bindings.rs index 888df68..3f172fe 100644 --- a/crates/psyche-coven/tests/bindings.rs +++ b/crates/psyche-coven/tests/bindings.rs @@ -137,10 +137,10 @@ fn content_reference_rejects_digest_size_media_type_and_lifetime_mismatch() { zero.size_bytes = 0; assert!(zero.validate().is_err()); let mut oversized = reference.clone(); - oversized.size_bytes = 9_007_199_254_740_992; + oversized.size_bytes = (i64::MAX as u64) + 1; assert!(oversized.validate().is_err()); let mut maximum = reference.clone(); - maximum.size_bytes = 9_007_199_254_740_991; + maximum.size_bytes = i64::MAX as u64; maximum.validate().unwrap(); let maximum: ContentAddressedReference = serde_json::from_value(serde_json::to_value(maximum).unwrap()).unwrap(); diff --git a/crates/psyche-test-support/src/lib.rs b/crates/psyche-test-support/src/lib.rs index 909d0f8..2ce75ea 100644 --- a/crates/psyche-test-support/src/lib.rs +++ b/crates/psyche-test-support/src/lib.rs @@ -19,7 +19,7 @@ pub use suites::{ assert_c_s8_terminal_authority, assert_c_s9_cancellation_acknowledgement, assert_c_s10_result_artifact_binding, assert_c_s11_restart_persistence, assert_c_s12_structured_denial, assert_surface_unknown_delivery, scripted_fixture, - scripted_surface, unsupported_fixture, + scripted_fixture_with_session_id, scripted_surface, unsupported_fixture, }; pub use surface::{ FakeSurface, FakeSurfaceBuilder, SurfaceFakeBuildError, SurfaceFakeCall, SurfaceScriptReturn, diff --git a/crates/psyche-test-support/src/suites/coven.rs b/crates/psyche-test-support/src/suites/coven.rs index 9366db6..c2dd0b7 100644 --- a/crates/psyche-test-support/src/suites/coven.rs +++ b/crates/psyche-test-support/src/suites/coven.rs @@ -30,6 +30,7 @@ use crate::coven::{ const CONTRACT: &str = "coven.daemon.v1"; const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991; +const MAX_CONTENT_SIZE_BYTES: u64 = i64::MAX as u64; const LAUNCH_GOLDEN: &[u8] = include_bytes!("../../../psyche-coven/tests/fixtures/execution-request-launch.json"); const INPUT_GOLDEN: &[u8] = @@ -105,6 +106,7 @@ struct ScriptedRuntimeState { struct ScriptedG2Port { durable: Arc>, runtime: Arc>, + initial_session_id: String, } /// Deterministic, restartable fixture used for the scripted G2 evidence rows. @@ -115,10 +117,16 @@ pub struct ScriptedG2Fixture { /// Builds a clean scripted fixture supporting all twelve G2 cases. pub fn scripted_fixture() -> ScriptedG2Fixture { + scripted_fixture_with_session_id("session-1") +} + +/// Builds a clean scripted fixture with a caller-selected opaque session ID. +pub fn scripted_fixture_with_session_id(session_id: impl Into) -> ScriptedG2Fixture { ScriptedG2Fixture { port: ScriptedG2Port { durable: Arc::new(Mutex::new(ScriptedDurableState::default())), runtime: Arc::new(Mutex::new(ScriptedRuntimeState::default())), + initial_session_id: session_id.into(), }, } } @@ -132,8 +140,16 @@ impl ScriptedG2Port { self.runtime.lock().map_err(|_| PortError::Unavailable) } - fn session_for_launch(state: &ScriptedDurableState) -> String { - format!("session-{}", state.sessions.len().saturating_add(1)) + fn session_for_launch(&self, state: &ScriptedDurableState) -> String { + if state.sessions.is_empty() { + self.initial_session_id.clone() + } else { + format!( + "{}-{}", + self.initial_session_id, + state.sessions.len().saturating_add(1) + ) + } } fn adoption_fault( @@ -324,7 +340,7 @@ impl CovenPort for ScriptedG2Port { let disposition = match request.input() { ExecutionRequestInput::Launch { .. } => AdoptionDisposition::Adopted { - session_id: Self::session_for_launch(&state), + session_id: self.session_for_launch(&state), }, ExecutionRequestInput::Input { session_id, .. } => { if !state.sessions.contains_key(session_id) { @@ -681,6 +697,7 @@ impl CovenConformanceFixture for ScriptedG2Fixture { self.port = ScriptedG2Port { durable: Arc::clone(&self.port.durable), runtime: Arc::new(Mutex::new(ScriptedRuntimeState::default())), + initial_session_id: self.port.initial_session_id.clone(), }; } @@ -980,7 +997,8 @@ async fn expected_unsupported( assert_eq!(fixture.port().result("session-1").await, Err(expected)); } UnsupportedCall::Terminate => { - let requested = termination_requested_binding(&launch_request(), "operator_request"); + let requested = + termination_requested_binding(&launch_request(), "session-1", "operator_request"); let mut persistence = MemoryTerminationPersistence::default(); assert!(matches!( persist_then_terminate(&mut persistence, fixture.port(), requested).await, @@ -1041,12 +1059,24 @@ fn launch_request() -> AdoptionRequest { } } -fn session_input_request() -> AdoptionRequest { +async fn adopt_session( + fixture: &dyn CovenConformanceFixture, + request: AdoptionRequest, + context: &str, +) -> String { + match fixture.port().adopt(request).await { + Ok(AdoptionDisposition::Adopted { session_id }) => session_id, + other => panic!("{context}: {other:?}"), + } +} + +fn session_input_request(session_id: &str) -> AdoptionRequest { let mut value: serde_json::Value = match serde_json::from_slice(INPUT_GOLDEN) { Ok(value) => value, Err(error) => panic!("canonical input fixture must decode: {error}"), }; value["request_id"] = serde_json::json!("req_01J00000000000000000000003"); + value["session_id"] = serde_json::json!(session_id); let input: ExecutionRequestInput = match serde_json::from_value(value) { Ok(value) => value, Err(error) => panic!("session input fixture must remain typed: {error}"), @@ -1238,7 +1268,7 @@ fn stale_digest_mutations(request: &AdoptionRequest) -> Vec<(&'static str, Adopt mutations.push(("/input", other_input)); mutations .into_iter() - .map(|(pointer, replacement)| { + .map(|(pointer, mut replacement)| { let mut value = match serde_json::to_value(request) { Ok(value) => value, Err(error) => panic!("typed adoption request must serialize: {error}"), @@ -1246,6 +1276,12 @@ fn stale_digest_mutations(request: &AdoptionRequest) -> Vec<(&'static str, Adopt let Some(field) = value.pointer_mut(pointer) else { panic!("static request mutation pointer must exist: {pointer}"); }; + if pointer == "/input/session_id" { + let Some(session_id) = field.as_str() else { + panic!("session input mutation requires a string session id"); + }; + replacement = serde_json::json!(distinct_session_id(session_id, "session-changed")); + } *field = replacement; let forged = match serde_json::from_value(value) { Ok(value) => value, @@ -1342,7 +1378,11 @@ impl MemoryTerminationPersistence { } } -fn termination_requested_binding(adoption: &AdoptionRequest, reason: &str) -> ExecutionBinding { +fn termination_requested_binding( + adoption: &AdoptionRequest, + session_id: &str, + reason: &str, +) -> ExecutionBinding { let correlation = adoption.correlation(); ExecutionBinding { schema_version: schema("psyche.execution_binding.v1"), @@ -1357,7 +1397,7 @@ fn termination_requested_binding(adoption: &AdoptionRequest, reason: &str) -> Ex request_created_at: correlation.created_at, request_valid_until: correlation.valid_until, coven_contract_version: CONTRACT.to_owned(), - coven_session_id: Some("session-1".to_owned()), + coven_session_id: Some(session_id.to_owned()), adoption_state: AdoptionState::Adopted, event_cursor: Some("cursor:0".to_owned()), cancellation_state: CancellationState::TerminationRequested, @@ -1396,6 +1436,53 @@ fn digest_for_sequence(sequence: u64) -> Sha256Digest { digest_of(char::from(HEX[index])) } +fn assert_cursor_page_progress(cursor: &EventCursor, page: &EventPage, terminal_high_water: u64) { + assert!( + cursor.after_sequence <= terminal_high_water + && page.next_cursor.after_sequence <= terminal_high_water, + "cursor advanced beyond terminal high-water mark" + ); + assert!( + cursor.after_sequence == terminal_high_water + || page.next_cursor.after_sequence > cursor.after_sequence, + "cursor page did not advance before terminal high-water mark" + ); +} + +fn distinct_session_id(session_id: &str, candidate: &str) -> String { + if session_id == candidate { + format!("{candidate}:distinct") + } else { + candidate.to_owned() + } +} + +fn assert_returned_session_matches_adoption( + returned_session_id: &str, + disposition: &AdoptionDisposition, +) { + match disposition { + AdoptionDisposition::Adopted { session_id } => assert_eq!( + returned_session_id, session_id, + "returned reconciliation session does not match durable adoption" + ), + AdoptionDisposition::ProvenNotAdopted | AdoptionDisposition::Unknown => { + panic!("original adoption is not durably adopted") + } + } +} + +async fn lookup_durable_adoption( + fixture: &mut dyn CovenConformanceFixture, + correlation: &ExecutionCorrelation, +) -> AdoptionDisposition { + fixture + .port() + .lookup(&correlation.request_id) + .await + .unwrap_or_else(|error| panic!("durable adoption lookup must succeed: {error}")) +} + fn schema(value: &str) -> SchemaVersion { match SchemaVersion::parse(value) { Ok(value) => value, @@ -1497,26 +1584,27 @@ pub async fn assert_c_s2_session_lifecycle( fixture.reset().await; let launch = launch_request(); let launch_correlation = launch.correlation(); + let session_id = adopt_session(fixture, launch.clone(), "session launch must be adopted").await; let adopted = AdoptionDisposition::Adopted { - session_id: "session-1".to_owned(), + session_id: session_id.clone(), }; assert_eq!( - fixture.port().adopt(launch.clone()).await, - Ok(adopted.clone()) - ); - assert_eq!( - fixture.port().adopt(session_input_request()).await, + fixture + .port() + .adopt(session_input_request(&session_id)) + .await, Ok(adopted) ); let snapshot = fixture .port() - .inspect("session-1") + .inspect(&session_id) .await .unwrap_or_else(|error| panic!("adopted session must be observable: {error}")); - assert_eq!(snapshot.session_id, "session-1"); + assert_eq!(snapshot.session_id, session_id); assert_eq!(snapshot.correlation, launch_correlation); - let requested = termination_requested_binding(&launch, "operator_request"); + let requested = + termination_requested_binding(&launch, &snapshot.session_id, "operator_request"); let mut persistence = MemoryTerminationPersistence::default(); let disposition = persist_then_terminate(&mut persistence, fixture.port(), requested.clone()) .await @@ -1527,7 +1615,7 @@ pub async fn assert_c_s2_session_lifecycle( )); let closed = fixture .port() - .inspect("session-1") + .inspect(&snapshot.session_id) .await .unwrap_or_else(|error| panic!("closed session must remain observable: {error}")); assert_eq!( @@ -1556,11 +1644,14 @@ pub async fn assert_c_s2_session_lifecycle( fixture.reset().await; assert_eq!( - fixture.port().adopt(session_input_request()).await, + fixture + .port() + .adopt(session_input_request(&snapshot.session_id)) + .await, Err(PortError::NotFound) ); assert_eq!( - fixture.port().inspect("session-1").await, + fixture.port().inspect(&snapshot.session_id).await, Err(PortError::NotFound) ); let mut persistence = MemoryTerminationPersistence::default(); @@ -1568,7 +1659,7 @@ pub async fn assert_c_s2_session_lifecycle( persist_then_terminate( &mut persistence, fixture.port(), - termination_requested_binding(&launch, "operator_request"), + termination_requested_binding(&launch, &snapshot.session_id, "operator_request"), ) .await, Err(TerminationDispatchError::Port(PortError::NotFound)) @@ -1592,17 +1683,14 @@ pub async fn assert_c_s3_snapshot_attempt_binding( fixture.reset().await; let adoption = launch_request(); let correlation = adoption.correlation(); - assert!(matches!( - fixture.port().adopt(adoption).await, - Ok(AdoptionDisposition::Adopted { .. }) - )); + let session_id = adopt_session(fixture, adoption, "snapshot setup must adopt").await; let snapshot = fixture .port() - .inspect("session-1") + .inspect(&session_id) .await .unwrap_or_else(|error| panic!("snapshot must round-trip: {error}")); assert_eq!(snapshot.correlation, correlation); - assert_eq!(snapshot.session_id, "session-1"); + assert_eq!(snapshot.session_id, session_id); let valid_reconciliation = ReconciliationRequest { correlation: correlation.clone(), ambiguity_digest: digest_of('d'), @@ -1616,6 +1704,13 @@ pub async fn assert_c_s3_snapshot_attempt_binding( valid_disposition .validate_for(&valid_reconciliation) .unwrap_or_else(|error| panic!("valid snapshot correlation must echo exactly: {error}")); + assert_eq!( + lookup_durable_adoption(fixture, &correlation).await, + AdoptionDisposition::Adopted { + session_id: session_id.clone(), + }, + "reconciliation must not alter the original adoption" + ); let changed_correlations = changed_correlations(&correlation); let changed_count = u64::try_from(changed_correlations.len()) @@ -1668,19 +1763,42 @@ pub async fn assert_c_s4_stable_adoption( assert_eq!(fixture.observations().await.adoption_calls, 1); fixture.restart().await; require_clear_fault(fixture).await; - let disposition = AdoptionDisposition::Adopted { - session_id: "session-1".to_owned(), + let disposition = fixture + .port() + .adopt(request.clone()) + .await + .unwrap_or_else(|error| panic!("durable adoption must replay: {error}")); + let AdoptionDisposition::Adopted { session_id } = &disposition else { + panic!("durable adoption replay must remain adopted"); }; + assert_eq!(fixture.observations().await.adoption_calls, 0); assert_eq!( fixture.port().adopt(request.clone()).await, Ok(disposition.clone()) ); assert_eq!(fixture.observations().await.adoption_calls, 0); - assert_eq!(fixture.port().adopt(request.clone()).await, Ok(disposition)); - assert_eq!(fixture.observations().await.adoption_calls, 0); - for typed in [request, session_input_request()] { + let input = session_input_request(session_id); + let input_disposition = fixture + .port() + .adopt(input.clone()) + .await + .unwrap_or_else(|error| panic!("valid session input must adopt: {error}")); + for (typed, expected_disposition) in [(request, disposition), (input, input_disposition)] { + let request_id = typed.correlation().request_id; + let durable_before = fixture + .port() + .lookup(&request_id) + .await + .unwrap_or_else(|error| panic!("durable adoption snapshot must succeed: {error}")); + assert_eq!(durable_before, expected_disposition); for (field, forged) in stale_digest_mutations(&typed) { + let forged_request_id = forged.correlation().request_id; + let forged_durable_before = fixture + .port() + .lookup(&forged_request_id) + .await + .unwrap_or_else(|error| panic!("forged adoption snapshot failed: {error}")); let before = fixture.observations().await; assert_eq!( fixture.port().adopt(forged).await, @@ -1688,6 +1806,30 @@ pub async fn assert_c_s4_stable_adoption( "{field}" ); assert_eq!(fixture.observations().await, before, "{field}"); + assert_eq!( + fixture + .port() + .lookup(&request_id) + .await + .unwrap_or_else(|error| panic!("durable adoption lookup failed: {error}")), + durable_before, + "{field}" + ); + assert_eq!( + fixture + .port() + .lookup(&forged_request_id) + .await + .unwrap_or_else(|error| panic!("forged adoption lookup failed: {error}")), + forged_durable_before, + "{field}" + ); + assert_eq!( + fixture.port().adopt(typed.clone()).await, + Ok(expected_disposition.clone()), + "{field}" + ); + assert_eq!(fixture.observations().await, before, "{field}"); } } ConformanceOutcome::Verified @@ -1710,13 +1852,10 @@ pub async fn assert_c_s5_non_adoption_proof( } fixture.reset().await; let launch = launch_request(); + let session_id = adopt_session(fixture, launch.clone(), "lookup setup must adopt").await; let adopted = AdoptionDisposition::Adopted { - session_id: "session-1".to_owned(), + session_id: session_id.clone(), }; - assert_eq!( - fixture.port().adopt(launch.clone()).await, - Ok(adopted.clone()) - ); assert_eq!( fixture .port() @@ -1751,9 +1890,7 @@ pub async fn assert_c_s5_non_adoption_proof( ); assert_eq!(redispatch_decision(&unknown), RedispatchDecision::Blocked); assert_eq!( - redispatch_decision(&AdoptionDisposition::Adopted { - session_id: "session-1".to_owned(), - }), + redispatch_decision(&AdoptionDisposition::Adopted { session_id }), RedispatchDecision::Blocked ); ConformanceOutcome::Verified @@ -1790,6 +1927,7 @@ pub async fn assert_c_s6_ambiguity_fence( ] { fixture.reset().await; let correlation = mark_ambiguous(fixture).await; + let original_adoption = lookup_durable_adoption(fixture, &correlation).await; let request = reconciliation_request(correlation, false); require_fault(fixture, point).await; let expected_error = if point == CovenFaultPoint::ReconcileStall { @@ -1814,53 +1952,86 @@ pub async fn assert_c_s6_ambiguity_fence( assert_eq!(blocked.adoption_calls, 0); assert_eq!(blocked.reconciliation_calls, 0); assert!(blocked.durable_reconciliation.is_none()); + assert_eq!( + fixture + .redispatch_eligibility(&request.correlation) + .await + .unwrap_or_else(|error| panic!("ambiguous eligibility lookup failed: {error}")), + RedispatchEligibility::Blocked + ); + assert_eq!( + fixture.observations().await.adoption_calls, + blocked.adoption_calls, + "ambiguous eligibility observation must not dispatch" + ); let recovered = fixture .port() - .reconcile(request) + .reconcile(request.clone()) .await .unwrap_or_else(|error| panic!("cleared reconciliation must recover: {error}")); - assert!(matches!( - recovered, - ReconciliationDisposition::Returned { .. } - )); + let ReconciliationDisposition::Returned { session_id, .. } = &recovered else { + panic!("cleared reconciliation must return the original session"); + }; + assert_returned_session_matches_adoption(session_id, &original_adoption); + assert_eq!( + lookup_durable_adoption(fixture, &request.correlation).await, + original_adoption, + "recovered reconciliation must not alter the original adoption" + ); let recovered_observations = fixture.observations().await; assert_eq!(recovered_observations.adoption_calls, 0); assert_eq!(recovered_observations.reconciliation_calls, 1); } - fixture.reset().await; - let correlation = mark_ambiguous(fixture).await; - let request = reconciliation_request(correlation, true); - require_fault(fixture, CovenFaultPoint::ReconcileAfterDisposition).await; - assert_eq!( - fixture.port().reconcile(request.clone()).await, - Err(PortError::Unavailable) - ); - let committed = fixture.observations().await; - assert_eq!(committed.adoption_calls, 1); - assert_eq!(committed.reconciliation_calls, 1); - let committed_observation = committed - .durable_reconciliation - .clone() - .unwrap_or_else(|| panic!("after-disposition fault must retain durable fence")); - assert!(matches!( - committed_observation.kind, - DurableDispositionKind::Fenced { .. } - )); - fixture.restart().await; - require_clear_fault(fixture).await; - let replay = fixture - .port() - .reconcile(request) - .await - .unwrap_or_else(|error| panic!("durable fence must replay after restart: {error}")); - assert_eq!( - disposition_observation(&replay), - Some(committed_observation) - ); - let replayed = fixture.observations().await; - assert_eq!(replayed.adoption_calls, 0); - assert_eq!(replayed.reconciliation_calls, 1); + for fenced in [false, true] { + fixture.reset().await; + let correlation = mark_ambiguous(fixture).await; + let original_adoption = lookup_durable_adoption(fixture, &correlation).await; + let request = reconciliation_request(correlation, fenced); + require_fault(fixture, CovenFaultPoint::ReconcileAfterDisposition).await; + assert_eq!( + fixture.port().reconcile(request.clone()).await, + Err(PortError::Unavailable) + ); + let committed = fixture.observations().await; + assert_eq!(committed.adoption_calls, 1); + assert_eq!(committed.reconciliation_calls, 1); + let committed_observation = committed + .durable_reconciliation + .clone() + .unwrap_or_else(|| panic!("after-disposition fault must retain a terminal outcome")); + assert_eq!( + matches!( + committed_observation.kind, + DurableDispositionKind::Fenced { .. } + ), + fenced + ); + fixture.restart().await; + require_clear_fault(fixture).await; + let replay = fixture + .port() + .reconcile(request.clone()) + .await + .unwrap_or_else(|error| { + panic!("durable terminal outcome must replay after restart: {error}") + }); + assert_eq!( + disposition_observation(&replay), + Some(committed_observation) + ); + if let ReconciliationDisposition::Returned { session_id, .. } = &replay { + assert_returned_session_matches_adoption(session_id, &original_adoption); + } + assert_eq!( + lookup_durable_adoption(fixture, &request.correlation).await, + original_adoption, + "replayed reconciliation must not alter the original adoption" + ); + let replayed = fixture.observations().await; + assert_eq!(replayed.adoption_calls, 0); + assert_eq!(replayed.reconciliation_calls, 1); + } ConformanceOutcome::Verified } @@ -1903,6 +2074,7 @@ fn reconciliation_request( async fn assert_reconciliation_terminal(fixture: &mut dyn CovenConformanceFixture, fenced: bool) { fixture.reset().await; let correlation = mark_ambiguous(fixture).await; + let original_adoption = lookup_durable_adoption(fixture, &correlation).await; let request = reconciliation_request(correlation.clone(), fenced); let disposition = fixture .port() @@ -1921,11 +2093,12 @@ async fn assert_reconciliation_terminal(fixture: &mut dyn CovenConformanceFixtur recorded_at, } => { assert!(!fenced); - assert_eq!(session_id, "session-1"); + assert!(!session_id.is_empty()); assert_eq!(echoed, &correlation); assert_eq!(ambiguity_digest, &request.ambiguity_digest); assert!(!disposition_id.is_empty()); assert!(*recorded_at >= correlation.created_at); + assert_returned_session_matches_adoption(session_id, &original_adoption); let resumed = fixture .port() .inspect(session_id) @@ -1965,6 +2138,11 @@ async fn assert_reconciliation_terminal(fixture: &mut dyn CovenConformanceFixtur panic!("terminal script must not return unresolved") } } + assert_eq!( + lookup_durable_adoption(fixture, &correlation).await, + original_adoption, + "terminal reconciliation must not alter the original adoption" + ); let first_observation = fixture .observations() @@ -1976,6 +2154,10 @@ async fn assert_reconciliation_terminal(fixture: &mut dyn CovenConformanceFixtur Some(first_observation.clone()) ); let adoption_calls_before_eligibility = fixture.observations().await.adoption_calls; + assert_eq!( + adoption_calls_before_eligibility, 1, + "reconciliation must not dispatch a second adoption" + ); let eligibility = fixture .redispatch_eligibility(&correlation) .await @@ -2054,12 +2236,14 @@ pub async fn assert_c_s7_ordered_cursor( return outcome; } fixture.reset().await; - assert!(matches!( - fixture.port().adopt(launch_request()).await, - Ok(AdoptionDisposition::Adopted { .. }) - )); + let session_id = adopt_session( + fixture, + launch_request(), + "cursor setup must adopt a session", + ) + .await; let initial = EventCursor { - session_id: "session-1".to_owned(), + session_id: session_id.clone(), after_sequence: 0, }; require_fault(fixture, CovenFaultPoint::CursorBeforePage).await; @@ -2074,6 +2258,10 @@ pub async fn assert_c_s7_ordered_cursor( .events(initial.clone()) .await .unwrap_or_else(|error| panic!("cursor must recover before-page fault: {error}")); + first + .validate_for(&initial) + .unwrap_or_else(|error| panic!("first cursor page must validate: {error}")); + assert_cursor_page_progress(&initial, &first, RAW_LEDGER_STATES.len() as u64); assert_eq!( first .events @@ -2092,7 +2280,9 @@ pub async fn assert_c_s7_ordered_cursor( .events(cursor.clone()) .await .unwrap_or_else(|error| panic!("ordered cursor page must succeed: {error}")); - assert_eq!(page.next_cursor.session_id, cursor.session_id); + page.validate_for(&cursor) + .unwrap_or_else(|error| panic!("ordered cursor page must validate: {error}")); + assert_cursor_page_progress(&cursor, &page, RAW_LEDGER_STATES.len() as u64); all.extend(page.events); cursor = page.next_cursor; } @@ -2119,17 +2309,28 @@ pub async fn assert_c_s7_ordered_cursor( fixture .port() .events(EventCursor { - session_id: "session-1".to_owned(), + session_id: session_id.clone(), + after_sequence: RAW_LEDGER_STATES.len() as u64 + 1, + }) + .await, + Err(PortError::InvalidRequest) + ); + assert_eq!( + fixture + .port() + .events(EventCursor { + session_id: session_id.clone(), after_sequence: 1, }) .await, Err(PortError::IntentConflict) ); + let foreign_session_id = distinct_session_id(&session_id, "foreign-session"); assert_eq!( fixture .port() .events(EventCursor { - session_id: "foreign-session".to_owned(), + session_id: foreign_session_id, after_sequence: 0, }) .await, @@ -2137,24 +2338,29 @@ pub async fn assert_c_s7_ordered_cursor( ); fixture.reset().await; - assert!(fixture.port().adopt(launch_request()).await.is_ok()); + let reset_session_id = + adopt_session(fixture, launch_request(), "cursor fault setup must adopt").await; + let reset_initial = EventCursor { + session_id: reset_session_id, + after_sequence: 0, + }; require_fault(fixture, CovenFaultPoint::CursorAfterPage).await; assert_eq!( - fixture.port().events(initial.clone()).await, + fixture.port().events(reset_initial.clone()).await, Err(PortError::Unavailable) ); fixture.restart().await; require_clear_fault(fixture).await; - assert_eq!( - fixture - .port() - .events(initial) - .await - .unwrap_or_else(|error| panic!("committed page must replay: {error}")) - .next_cursor - .after_sequence, - 3 - ); + let replayed = fixture + .port() + .events(reset_initial.clone()) + .await + .unwrap_or_else(|error| panic!("committed page must replay: {error}")); + replayed + .validate_for(&reset_initial) + .unwrap_or_else(|error| panic!("replayed cursor page must validate: {error}")); + assert_cursor_page_progress(&reset_initial, &replayed, RAW_LEDGER_STATES.len() as u64); + assert_eq!(replayed.next_cursor.after_sequence, 3); ConformanceOutcome::Verified } @@ -2173,17 +2379,22 @@ pub async fn assert_c_s8_terminal_authority( } fixture.reset().await; let launch = launch_request(); - assert!(fixture.port().adopt(launch.clone()).await.is_ok()); + let session_id = adopt_session( + fixture, + launch.clone(), + "terminal setup must adopt a session", + ) + .await; let snapshot = fixture .port() - .inspect("session-1") + .inspect(&session_id) .await .unwrap_or_else(|error| panic!("raw session status must be readable: {error}")); assert_eq!(snapshot.terminal_state.as_deref(), Some("created")); let raw_page = fixture .port() .events(EventCursor { - session_id: "session-1".to_owned(), + session_id: session_id.clone(), after_sequence: 0, }) .await @@ -2198,7 +2409,7 @@ pub async fn assert_c_s8_terminal_authority( assert_ne!(raw, Some("authoritatively_terminated")); } - let requested = termination_requested_binding(&launch, "operator_request"); + let requested = termination_requested_binding(&launch, &session_id, "operator_request"); let mut unproven = requested.clone(); unproven.cancellation_state = CancellationState::AcknowledgedTerminated; unproven.terminal_state = Some("process_exited".to_owned()); @@ -2215,7 +2426,7 @@ pub async fn assert_c_s8_terminal_authority( fixture.restart().await; let still_raw = fixture .port() - .inspect("session-1") + .inspect(&session_id) .await .unwrap_or_else(|error| { panic!("unpersisted terminal must remain observable only as raw: {error}") @@ -2240,7 +2451,7 @@ pub async fn assert_c_s8_terminal_authority( assert_eq!( fixture .port() - .inspect("session-1") + .inspect(&session_id) .await .unwrap_or_else(|error| panic!("durable terminal must survive restart: {error}")) .terminal_state @@ -2265,14 +2476,19 @@ pub async fn assert_c_s9_cancellation_acknowledgement( } fixture.reset().await; let launch = launch_request(); - assert!(fixture.port().adopt(launch.clone()).await.is_ok()); + let session_id = adopt_session( + fixture, + launch.clone(), + "cancellation setup must adopt a session", + ) + .await; let mut snapshot_states = Vec::new(); for _ in 0..RAW_LEDGER_STATES.len() { snapshot_states.push( fixture .port() - .inspect("session-1") + .inspect(&session_id) .await .unwrap_or_else(|error| panic!("raw snapshot must be readable: {error}")) .terminal_state @@ -2284,7 +2500,7 @@ pub async fn assert_c_s9_cancellation_acknowledgement( assert_eq!( fixture .port() - .inspect("session-1") + .inspect(&session_id) .await .unwrap_or_else(|error| panic!("restart snapshot must be readable: {error}")) .terminal_state @@ -2302,27 +2518,25 @@ pub async fn assert_c_s9_cancellation_acknowledgement( let mut event_states = Vec::new(); let mut cursor = EventCursor { - session_id: "session-1".to_owned(), + session_id: session_id.clone(), after_sequence: 0, }; - loop { + while cursor.after_sequence < RAW_LEDGER_STATES.len() as u64 { let page = fixture .port() - .events(cursor) + .events(cursor.clone()) .await .unwrap_or_else(|error| panic!("raw event table must be readable: {error}")); + assert_cursor_page_progress(&cursor, &page, RAW_LEDGER_STATES.len() as u64); event_states.extend(page.events.into_iter().map(|event| { event .terminal_state .unwrap_or_else(|| panic!("scripted raw event must name its state")) })); cursor = page.next_cursor; - if cursor.after_sequence == RAW_LEDGER_STATES.len() as u64 { - break; - } } assert_eq!(event_states, snapshot_states); - let requested = termination_requested_binding(&launch, "operator_request"); + let requested = termination_requested_binding(&launch, &session_id, "operator_request"); for state in &snapshot_states { let mut raw_only = requested.clone(); raw_only.cancellation_state = CancellationState::AcknowledgedTerminated; @@ -2331,8 +2545,10 @@ pub async fn assert_c_s9_cancellation_acknowledgement( } fixture.reset().await; - assert!(fixture.port().adopt(launch.clone()).await.is_ok()); - let unresolved_requested = termination_requested_binding(&launch, "force_unresolved"); + let unresolved_session_id = + adopt_session(fixture, launch.clone(), "unresolved setup must adopt").await; + let unresolved_requested = + termination_requested_binding(&launch, &unresolved_session_id, "force_unresolved"); require_fault(fixture, CovenFaultPoint::CancellationBeforeAcknowledgement).await; let mut unresolved_persistence = MemoryTerminationPersistence::default(); assert!(matches!( @@ -2379,8 +2595,10 @@ pub async fn assert_c_s9_cancellation_acknowledgement( ); fixture.reset().await; - assert!(fixture.port().adopt(launch.clone()).await.is_ok()); - let acknowledged_requested = termination_requested_binding(&launch, "operator_request"); + let acknowledged_session_id = + adopt_session(fixture, launch.clone(), "acknowledgement setup must adopt").await; + let acknowledged_requested = + termination_requested_binding(&launch, &acknowledged_session_id, "operator_request"); require_fault(fixture, CovenFaultPoint::CancellationAfterAcknowledgement).await; let mut acknowledged_persistence = MemoryTerminationPersistence::default(); assert!(matches!( @@ -2465,7 +2683,7 @@ fn assert_invalid_acknowledgements( changed.termination_request_id = request_id(8); mutations.push(("termination_request_id", changed)); let mut changed = valid.clone(); - changed.session_id = "session-other".to_owned(); + changed.session_id = distinct_session_id(&valid.session_id, "session-other"); mutations.push(("session_id", changed)); let mut changed = valid.clone(); changed.execution_request_id = request_id(8); @@ -2511,23 +2729,31 @@ pub async fn assert_c_s10_result_artifact_binding( fixture.reset().await; let launch = launch_request(); let launch_correlation = launch.correlation(); - assert!(fixture.port().adopt(launch).await.is_ok()); - let expected: ResultBundle = match serde_json::from_slice(RESULT_GOLDEN) { + let session_id = adopt_session(fixture, launch, "result setup must adopt a session").await; + let golden: ResultBundle = match serde_json::from_slice(RESULT_GOLDEN) { Ok(bundle) => bundle, Err(error) => panic!("strict result fixture must decode: {error}"), }; - expected + golden .validate() .unwrap_or_else(|error| panic!("strict result fixture must validate: {error}")); - assert_eq!(expected.correlation, launch_correlation); + assert_eq!(golden.correlation, launch_correlation); assert_eq!( - canonical_bytes(&expected) + canonical_bytes(&golden) .unwrap_or_else(|error| panic!("result fixture must canonicalize: {error}")), RESULT_GOLDEN ); + let mut expected = golden; + expected.session_id = session_id.clone(); + for artifact in &mut expected.artifacts { + artifact.session_id = session_id.clone(); + } + expected + .validate() + .unwrap_or_else(|error| panic!("result fixture must accept the opaque session: {error}")); let actual = fixture .port() - .result("session-1") + .result(&session_id) .await .unwrap_or_else(|error| panic!("complete result must be returned: {error}")); assert_eq!(actual, expected); @@ -2544,11 +2770,12 @@ pub async fn assert_c_s10_result_artifact_binding( assert_complete_result_rejected(&artifact_changed, &expected, &format!("artifact_{field}")); } + let wrong_session_id = distinct_session_id(&session_id, "session-other"); let mut wrong_session = expected.clone(); - wrong_session.session_id = "session-other".to_owned(); + wrong_session.session_id = wrong_session_id.clone(); assert_complete_result_rejected(&wrong_session, &expected, "session_id"); let mut wrong_artifact_session = expected.clone(); - wrong_artifact_session.artifacts[0].session_id = "session-other".to_owned(); + wrong_artifact_session.artifacts[0].session_id = wrong_session_id; assert_complete_result_rejected(&wrong_artifact_session, &expected, "artifact_session_id"); for (field, mutate) in [ @@ -2573,13 +2800,13 @@ pub async fn assert_c_s10_result_artifact_binding( zero_result.result.size_bytes = 0; assert!(zero_result.validate().is_err()); let mut oversized_result = expected.clone(); - oversized_result.result.size_bytes = MAX_SAFE_INTEGER + 1; + oversized_result.result.size_bytes = MAX_CONTENT_SIZE_BYTES + 1; assert!(oversized_result.validate().is_err()); let mut safe_result = expected.clone(); - safe_result.result.size_bytes = MAX_SAFE_INTEGER; + safe_result.result.size_bytes = MAX_CONTENT_SIZE_BYTES; safe_result .validate() - .unwrap_or_else(|error| panic!("JSON safe-integer boundary must validate: {error}")); + .unwrap_or_else(|error| panic!("maximum content size must validate: {error}")); let mut malformed_result = expected.clone(); malformed_result.result.media_type = "Application/JSON".to_owned(); assert!(malformed_result.validate().is_err()); @@ -2592,13 +2819,13 @@ pub async fn assert_c_s10_result_artifact_binding( zero_artifact.artifacts[0].content.size_bytes = 0; assert!(zero_artifact.validate().is_err()); let mut oversized_artifact = expected.clone(); - oversized_artifact.artifacts[0].content.size_bytes = MAX_SAFE_INTEGER + 1; + oversized_artifact.artifacts[0].content.size_bytes = MAX_CONTENT_SIZE_BYTES + 1; assert!(oversized_artifact.validate().is_err()); let mut safe_artifact = expected.clone(); - safe_artifact.artifacts[0].content.size_bytes = MAX_SAFE_INTEGER; + safe_artifact.artifacts[0].content.size_bytes = MAX_CONTENT_SIZE_BYTES; safe_artifact .validate() - .unwrap_or_else(|error| panic!("artifact safe-integer boundary must validate: {error}")); + .unwrap_or_else(|error| panic!("maximum artifact size must validate: {error}")); let mut malformed_artifact = expected.clone(); malformed_artifact.artifacts[0].content.media_type = "text/plain; charset=utf-8".to_owned(); assert!(malformed_artifact.validate().is_err()); @@ -2622,7 +2849,7 @@ pub async fn assert_c_s10_result_artifact_binding( assert_eq!( fixture .port() - .result("session-1") + .result(&session_id) .await .unwrap_or_else(|error| panic!("complete result must replay: {error}")), expected @@ -2787,10 +3014,13 @@ async fn assert_restart_resets_runtime(fixture: &mut dyn CovenConformanceFixture .adopt(request) .await .unwrap_or_else(|error| panic!("restart setup must adopt: {error}")); + let AdoptionDisposition::Adopted { session_id } = &adopted else { + panic!("restart setup must return an adopted session"); + }; assert_eq!( fixture .port() - .inspect("session-1") + .inspect(session_id) .await .unwrap_or_else(|error| panic!("restart setup must inspect: {error}")) .terminal_state @@ -2805,13 +3035,13 @@ async fn assert_restart_resets_runtime(fixture: &mut dyn CovenConformanceFixture ); assert_eq!( fixture.port().lookup(&request_id).await, - Ok(adopted), + Ok(adopted.clone()), "durable adoption must survive while the selected fault is cleared" ); assert_eq!( fixture .port() - .inspect("session-1") + .inspect(session_id) .await .unwrap_or_else(|error| panic!("durable session must survive restart: {error}")) .terminal_state @@ -2830,11 +3060,17 @@ async fn assert_adoption_fault_recovery( point, CovenFaultPoint::InputBeforeCommit | CovenFaultPoint::InputAfterCommit ); - if input_fault { - assert!(fixture.port().adopt(launch_request()).await.is_ok()); - } + let session_id = if input_fault { + Some(adopt_session(fixture, launch_request(), "input fault setup must adopt").await) + } else { + None + }; let request = if input_fault { - session_input_request() + session_input_request( + session_id + .as_deref() + .unwrap_or_else(|| panic!("input fault requires an adopted session")), + ) } else { launch_request() }; @@ -2917,9 +3153,10 @@ async fn assert_cursor_fault_recovery( point: CovenFaultPoint, ) { fixture.reset().await; - assert!(fixture.port().adopt(launch_request()).await.is_ok()); - let cursor = EventCursor { - session_id: "session-1".to_owned(), + let session_id = + adopt_session(fixture, launch_request(), "cursor fault setup must adopt").await; + let mut cursor = EventCursor { + session_id: session_id.clone(), after_sequence: 0, }; require_fault(fixture, point).await; @@ -2930,7 +3167,7 @@ async fn assert_cursor_fault_recovery( fixture.restart().await; require_clear_fault(fixture).await; let regression = EventCursor { - session_id: "session-1".to_owned(), + session_id, after_sequence: 1, }; if point == CovenFaultPoint::CursorAfterPage { @@ -2941,9 +3178,13 @@ async fn assert_cursor_fault_recovery( } else { let probe = fixture .port() - .events(regression) + .events(regression.clone()) .await .unwrap_or_else(|error| panic!("before-page fault persisted a cursor: {error}")); + probe + .validate_for(®ression) + .unwrap_or_else(|error| panic!("before-page recovery probe must validate: {error}")); + assert_cursor_page_progress(®ression, &probe, RAW_LEDGER_STATES.len() as u64); assert_eq!( probe .events @@ -2953,22 +3194,34 @@ async fn assert_cursor_fault_recovery( vec![2, 3, 4] ); fixture.reset().await; - assert!(fixture.port().adopt(launch_request()).await.is_ok()); + let reset_session_id = adopt_session( + fixture, + launch_request(), + "cursor recovery setup must adopt", + ) + .await; + cursor.session_id = reset_session_id; } let recovered = fixture .port() .events(cursor.clone()) .await .unwrap_or_else(|error| panic!("{point:?} cursor must recover: {error}")); + recovered + .validate_for(&cursor) + .unwrap_or_else(|error| panic!("{point:?} recovered cursor page must validate: {error}")); + assert_cursor_page_progress(&cursor, &recovered, RAW_LEDGER_STATES.len() as u64); fixture.restart().await; - assert_eq!( - fixture - .port() - .events(cursor) - .await - .unwrap_or_else(|error| panic!("{point:?} cursor must replay: {error}")), - recovered - ); + let replayed = fixture + .port() + .events(cursor.clone()) + .await + .unwrap_or_else(|error| panic!("{point:?} cursor must replay: {error}")); + replayed + .validate_for(&cursor) + .unwrap_or_else(|error| panic!("{point:?} replayed cursor page must validate: {error}")); + assert_cursor_page_progress(&cursor, &replayed, RAW_LEDGER_STATES.len() as u64); + assert_eq!(replayed, recovered); } async fn assert_termination_fault_recovery( @@ -2977,8 +3230,13 @@ async fn assert_termination_fault_recovery( ) { fixture.reset().await; let launch = launch_request(); - assert!(fixture.port().adopt(launch.clone()).await.is_ok()); - let requested = termination_requested_binding(&launch, "operator_request"); + let session_id = adopt_session( + fixture, + launch.clone(), + "termination fault setup must adopt", + ) + .await; + let requested = termination_requested_binding(&launch, &session_id, "operator_request"); let mut persistence = MemoryTerminationPersistence::default(); require_fault(fixture, point).await; assert!(matches!( @@ -3021,29 +3279,30 @@ async fn assert_result_fault_recovery( point: CovenFaultPoint, ) { fixture.reset().await; - assert!(fixture.port().adopt(launch_request()).await.is_ok()); + let session_id = + adopt_session(fixture, launch_request(), "result fault setup must adopt").await; require_fault(fixture, point).await; assert_eq!( - fixture.port().result("session-1").await, + fixture.port().result(&session_id).await, Err(PortError::Unavailable) ); fixture.restart().await; require_fault(fixture, CovenFaultPoint::ResultBeforePersistence).await; let recovered = if point == CovenFaultPoint::ResultBeforePersistence { assert_eq!( - fixture.port().result("session-1").await, + fixture.port().result(&session_id).await, Err(PortError::Unavailable) ); require_clear_fault(fixture).await; fixture .port() - .result("session-1") + .result(&session_id) .await .unwrap_or_else(|error| panic!("{point:?} result must recover: {error}")) } else { let bundle = fixture .port() - .result("session-1") + .result(&session_id) .await .unwrap_or_else(|error| panic!("artifact fault lost primary result: {error}")); require_clear_fault(fixture).await; @@ -3053,7 +3312,7 @@ async fn assert_result_fault_recovery( assert_eq!( fixture .port() - .result("session-1") + .result(&session_id) .await .unwrap_or_else(|error| panic!("{point:?} result must replay: {error}")), recovered @@ -3066,6 +3325,7 @@ async fn assert_reconciliation_fault_recovery( ) { fixture.reset().await; let correlation = mark_ambiguous(fixture).await; + let original_adoption = lookup_durable_adoption(fixture, &correlation).await; let request = reconciliation_request(correlation, true); require_fault(fixture, point).await; let expected = if point == CovenFaultPoint::ReconcileStall { @@ -3100,7 +3360,7 @@ async fn assert_reconciliation_fault_recovery( assert_eq!( fixture .port() - .reconcile(request) + .reconcile(request.clone()) .await .unwrap_or_else(|error| panic!("{point:?} reconciliation must replay: {error}")), recovered @@ -3109,6 +3369,11 @@ async fn assert_reconciliation_fault_recovery( fixture.observations().await.durable_reconciliation, Some(durable) ); + assert_eq!( + lookup_durable_adoption(fixture, &request.correlation).await, + original_adoption, + "fault recovery must not alter the original adoption" + ); assert_eq!(fixture.observations().await.adoption_calls, 0); } @@ -3144,7 +3409,7 @@ pub async fn assert_c_s12_structured_denial( let launch = launch_request(); let correlation = launch.correlation(); - assert!(fixture.port().adopt(launch).await.is_ok()); + let session_id = adopt_session(fixture, launch, "structured denial setup must adopt").await; let mut changed = correlation.clone(); changed.project_id = "project:sha256:other".to_owned(); assert_eq!( @@ -3166,11 +3431,12 @@ pub async fn assert_c_s12_structured_denial( .is_none() ); + let foreign_session_id = distinct_session_id(&session_id, "foreign-session"); assert_eq!( fixture .port() .events(EventCursor { - session_id: "foreign-session".to_owned(), + session_id: foreign_session_id, after_sequence: 0, }) .await, @@ -3180,7 +3446,7 @@ pub async fn assert_c_s12_structured_denial( fixture .port() .events(EventCursor { - session_id: "session-1".to_owned(), + session_id: session_id.clone(), after_sequence: MAX_SAFE_INTEGER + 1, }) .await, @@ -3189,7 +3455,10 @@ pub async fn assert_c_s12_structured_denial( fixture.reset().await; assert_eq!( - fixture.port().adopt(session_input_request()).await, + fixture + .port() + .adopt(session_input_request(&session_id)) + .await, Err(PortError::NotFound) ); assert_eq!( @@ -3208,7 +3477,7 @@ pub async fn assert_c_s12_structured_denial( zero.size_bytes = 0; assert_eq!(zero.validate(), Err(PortError::InvalidRequest)); let mut oversized = base.clone(); - oversized.size_bytes = MAX_SAFE_INTEGER + 1; + oversized.size_bytes = MAX_CONTENT_SIZE_BYTES + 1; assert_eq!(oversized.validate(), Err(PortError::InvalidRequest)); let mut malformed = base; malformed.media_type = "free form error".to_owned(); @@ -3228,3 +3497,49 @@ pub async fn assert_c_s12_structured_denial( } ConformanceOutcome::Verified } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic(expected = "cursor page did not advance before terminal high-water mark")] + fn cursor_page_must_advance_before_terminal_high_water() { + let cursor = EventCursor { + session_id: "opaque-session".to_owned(), + after_sequence: 3, + }; + let page = EventPage { + events: Vec::new(), + next_cursor: cursor.clone(), + }; + + assert_cursor_page_progress(&cursor, &page, 7); + } + + #[test] + #[should_panic(expected = "cursor advanced beyond terminal high-water mark")] + fn cursor_page_must_reject_terminal_high_water_overshoot() { + let cursor = EventCursor { + session_id: "opaque-session".to_owned(), + after_sequence: 8, + }; + let page = EventPage { + events: Vec::new(), + next_cursor: cursor.clone(), + }; + + assert_cursor_page_progress(&cursor, &page, 7); + } + + #[test] + #[should_panic(expected = "returned reconciliation session does not match durable adoption")] + fn returned_reconciliation_must_name_original_session() { + assert_returned_session_matches_adoption( + "returned-session", + &AdoptionDisposition::Adopted { + session_id: "original-session".to_owned(), + }, + ); + } +} diff --git a/crates/psyche-test-support/src/suites/mod.rs b/crates/psyche-test-support/src/suites/mod.rs index 59d8479..8dcb12a 100644 --- a/crates/psyche-test-support/src/suites/mod.rs +++ b/crates/psyche-test-support/src/suites/mod.rs @@ -10,7 +10,7 @@ pub use coven::{ assert_c_s7_ordered_cursor, assert_c_s8_terminal_authority, assert_c_s9_cancellation_acknowledgement, assert_c_s10_result_artifact_binding, assert_c_s11_restart_persistence, assert_c_s12_structured_denial, scripted_fixture, - unsupported_fixture, + scripted_fixture_with_session_id, unsupported_fixture, }; pub use surface::{assert_surface_unknown_delivery, scripted_surface}; diff --git a/crates/psyche-test-support/tests/conformance.rs b/crates/psyche-test-support/tests/conformance.rs index 1bab334..6530d16 100644 --- a/crates/psyche-test-support/tests/conformance.rs +++ b/crates/psyche-test-support/tests/conformance.rs @@ -7,7 +7,7 @@ use psyche_test_support::suites::{ assert_c_s8_terminal_authority, assert_c_s9_cancellation_acknowledgement, assert_c_s10_result_artifact_binding, assert_c_s11_restart_persistence, assert_c_s12_structured_denial, assert_surface_unknown_delivery, scripted_fixture, - scripted_surface, unsupported_fixture, + scripted_fixture_with_session_id, scripted_surface, unsupported_fixture, }; #[tokio::test] @@ -111,6 +111,70 @@ async fn surface_unknown_delivery() { assert_surface_unknown_delivery(&scripted_surface()).await; } +#[tokio::test] +async fn reusable_conformance_accepts_opaque_session_ids() { + const OPAQUE_SESSION_ID: &str = "coven-session:opaque-7f4d2a"; + + assert_eq!( + assert_c_s2_session_lifecycle(&mut scripted_fixture_with_session_id(OPAQUE_SESSION_ID)) + .await, + ConformanceOutcome::Verified + ); + assert_eq!( + assert_c_s3_snapshot_attempt_binding(&mut scripted_fixture_with_session_id( + OPAQUE_SESSION_ID, + )) + .await, + ConformanceOutcome::Verified + ); + assert_eq!( + assert_c_s4_stable_adoption(&mut scripted_fixture_with_session_id(OPAQUE_SESSION_ID)).await, + ConformanceOutcome::Verified + ); + assert_eq!( + assert_c_s5_non_adoption_proof(&mut scripted_fixture_with_session_id(OPAQUE_SESSION_ID)) + .await, + ConformanceOutcome::Verified + ); + assert_eq!( + assert_c_s6_ambiguity_fence(&mut scripted_fixture_with_session_id(OPAQUE_SESSION_ID)).await, + ConformanceOutcome::Verified + ); + assert_eq!( + assert_c_s7_ordered_cursor(&mut scripted_fixture_with_session_id(OPAQUE_SESSION_ID)).await, + ConformanceOutcome::Verified + ); + assert_eq!( + assert_c_s8_terminal_authority(&mut scripted_fixture_with_session_id(OPAQUE_SESSION_ID)) + .await, + ConformanceOutcome::Verified + ); + assert_eq!( + assert_c_s9_cancellation_acknowledgement(&mut scripted_fixture_with_session_id( + OPAQUE_SESSION_ID, + )) + .await, + ConformanceOutcome::Verified + ); + assert_eq!( + assert_c_s10_result_artifact_binding(&mut scripted_fixture_with_session_id( + OPAQUE_SESSION_ID, + )) + .await, + ConformanceOutcome::Verified + ); + assert_eq!( + assert_c_s11_restart_persistence(&mut scripted_fixture_with_session_id(OPAQUE_SESSION_ID)) + .await, + ConformanceOutcome::Verified + ); + assert_eq!( + assert_c_s12_structured_denial(&mut scripted_fixture_with_session_id(OPAQUE_SESSION_ID)) + .await, + ConformanceOutcome::Verified + ); +} + #[tokio::test] async fn expected_unsupported_paths_execute_public_calls_without_mutation() { assert_all_expected_unsupported("CapabilityMissing").await; diff --git a/crates/psyche-test-support/tests/state_machine.rs b/crates/psyche-test-support/tests/state_machine.rs index a9c525d..08ff9d9 100644 --- a/crates/psyche-test-support/tests/state_machine.rs +++ b/crates/psyche-test-support/tests/state_machine.rs @@ -23,7 +23,7 @@ use psyche_store::{ }; use psyche_test_support::{ CovenConformanceFixture, CovenFaultPoint, DurableDispositionKind, RedispatchEligibility, - scripted_fixture, + scripted_fixture_with_session_id, }; use serde_json::{Map, json}; use tempfile::TempDir; @@ -1130,6 +1130,7 @@ enum RecoveryDispatchDecision { struct CovenRecoveryModel { state: RecoveryState, adoption_calls: u64, + adoption: Option, request: Option, disposition: Option, } @@ -1139,6 +1140,7 @@ impl Default for CovenRecoveryModel { Self { state: RecoveryState::Clean, adoption_calls: 0, + adoption: None, request: None, disposition: None, } @@ -1181,7 +1183,7 @@ fn mutate_reconciliation(request: &ReconciliationRequest, mutation: u8) -> Recon async fn compare_c_s6_model_and_fixture( operations: Vec, ) -> Result<(), TestCaseError> { - let mut fixture = scripted_fixture(); + let mut fixture = scripted_fixture_with_session_id("state-machine:opaque-session"); let adoption = launch_adoption(); let correlation = adoption.correlation(); let mut model = CovenRecoveryModel::default(); @@ -1214,9 +1216,15 @@ async fn compare_c_s6_model_and_fixture( .clear_fault() .await .map_err(|error| TestCaseError::fail(error.to_string()))?; + let durable_adoption = fixture + .port() + .lookup(&correlation.request_id) + .await + .map_err(|error| TestCaseError::fail(error.to_string()))?; model = CovenRecoveryModel { state: RecoveryState::Ambiguous, adoption_calls: 1, + adoption: Some(durable_adoption), request: None, disposition: None, }; @@ -1413,10 +1421,28 @@ async fn compare_c_s6_model_and_fixture( let DurableDispositionKind::Returned { session_id } = durable.kind else { return Err(TestCaseError::fail("returned model observed a fence")); }; - prop_assert_eq!(session_id, "session-1"); + let durable_adoption = fixture + .port() + .lookup(&correlation.request_id) + .await + .map_err(|error| TestCaseError::fail(error.to_string()))?; + let original_adoption = model + .adoption + .as_ref() + .ok_or_else(|| TestCaseError::fail("returned model lost adoption baseline"))?; + prop_assert_eq!(&durable_adoption, original_adoption); + let AdoptionDisposition::Adopted { + session_id: adopted_session_id, + } = original_adoption + else { + return Err(TestCaseError::fail( + "returned model lost the original durable adoption", + )); + }; + prop_assert_eq!(session_id.as_str(), adopted_session_id.as_str()); let resumed = fixture .port() - .inspect("session-1") + .inspect(&session_id) .await .map_err(|error| TestCaseError::fail(error.to_string()))?; prop_assert_eq!(resumed.correlation, correlation.clone()); @@ -1469,23 +1495,35 @@ struct RequestDigestModel { async fn compare_request_digest_model_and_fixture( operations: Vec, ) -> Result<(), TestCaseError> { - let mut fixture = scripted_fixture(); + let mut fixture = scripted_fixture_with_session_id("digest-model:opaque-session"); let mut model = RequestDigestModel::default(); for operation in operations { match operation { RequestDigestOperation::ConstructRequest { input } => { fixture.reset().await; model = RequestDigestModel::default(); - if input { - fixture + let input_session_id = if input { + let disposition = fixture .port() .adopt(launch_adoption()) .await .map_err(|error| TestCaseError::fail(error.to_string()))?; model.adoption_calls = 1; - } + let AdoptionDisposition::Adopted { session_id } = disposition else { + return Err(TestCaseError::fail( + "launch setup did not return an adopted session", + )); + }; + Some(session_id) + } else { + None + }; let request = if input { - session_input_adoption() + session_input_adoption( + input_session_id + .as_deref() + .ok_or_else(|| TestCaseError::fail("input setup lost its session"))?, + ) } else { launch_adoption() }; @@ -1522,6 +1560,24 @@ async fn compare_request_digest_model_and_fixture( let mutations = stale_digest_requests(request); let (_, forged) = &mutations[usize::from(field) % mutations.len()]; let before = fixture.observations().await; + let original_id = request.correlation().request_id; + let original_lookup_before = fixture + .port() + .lookup(&original_id) + .await + .map_err(|error| TestCaseError::fail(error.to_string()))?; + let forged_id = forged.correlation().request_id; + let forged_lookup_before = if forged_id == original_id { + None + } else { + Some( + fixture + .port() + .lookup(&forged_id) + .await + .map_err(|error| TestCaseError::fail(error.to_string()))?, + ) + }; prop_assert_eq!( fixture.port().adopt(forged.clone()).await, Err(PortError::RequestDigestMismatch) @@ -1529,13 +1585,14 @@ async fn compare_request_digest_model_and_fixture( let after = fixture.observations().await; prop_assert_eq!(after.adoption_calls, before.adoption_calls); prop_assert_eq!(after.durable_reconciliation, before.durable_reconciliation); - let forged_id = forged.correlation().request_id; - if forged_id != request.correlation().request_id { - prop_assert_ne!( + prop_assert_eq!( + fixture.port().lookup(&original_id).await, + Ok(original_lookup_before) + ); + if let Some(forged_lookup_before) = forged_lookup_before { + prop_assert_eq!( fixture.port().lookup(&forged_id).await, - Ok(AdoptionDisposition::Adopted { - session_id: "session-1".to_owned(), - }) + Ok(forged_lookup_before) ); } prop_assert_eq!( @@ -1562,9 +1619,10 @@ fn launch_adoption() -> AdoptionRequest { AdoptionRequest::new(input).unwrap() } -fn session_input_adoption() -> AdoptionRequest { +fn session_input_adoption(session_id: &str) -> AdoptionRequest { let mut value: serde_json::Value = serde_json::from_slice(INPUT_GOLDEN).unwrap(); value["request_id"] = json!("req_01J00000000000000000000003"); + value["session_id"] = json!(session_id); AdoptionRequest::new(serde_json::from_value(value).unwrap()).unwrap() } @@ -1673,9 +1731,18 @@ fn stale_digest_requests(request: &AdoptionRequest) -> Vec<(&'static str, Adopti mutations.push(("/input", other_input)); mutations .into_iter() - .map(|(pointer, replacement)| { + .map(|(pointer, mut replacement)| { let mut value = serde_json::to_value(request).unwrap(); - *value.pointer_mut(pointer).unwrap() = replacement; + let field = value.pointer_mut(pointer).unwrap(); + if pointer == "/input/session_id" { + let session_id = field.as_str().unwrap(); + replacement = json!(if session_id == "session-changed" { + "session-changed:distinct" + } else { + "session-changed" + }); + } + *field = replacement; (pointer, serde_json::from_value(value).unwrap()) }) .collect() diff --git a/docs/G2-EVIDENCE.md b/docs/G2-EVIDENCE.md index 0b93e91..81bf23a 100644 --- a/docs/G2-EVIDENCE.md +++ b/docs/G2-EVIDENCE.md @@ -1,11 +1,11 @@ # G2 Contract Foundation Evidence -**Status:** passed -**Tested source commit:** 75877d78e00d36030d105db0b04b132081814f67 -**CI attestation:** https://github.com/OpenCoven/psyche/actions/runs/31290123379 -**Coven plan source commit:** 5f22ebef1e23d045a10f2ec0a3c87be029446cf6 -**Coven plan URL:** https://github.com/OpenCoven/coven/blob/5f22ebef1e23d045a10f2ec0a3c87be029446cf6/docs/superpowers/plans/2026-08-05-psyche-w2-g2-foundation.md -**Coven plan SHA-256:** sha256:4fba002ad9f969cd01866ea08f270654f82b53c7d90b73d28643a9abb12cba68 +**Status:** candidate +**Tested source commit:** not recorded before remote review +**CI attestation:** not recorded before remote review +**Coven plan source commit:** not recorded before plan approval +**Coven plan URL:** not recorded before plan approval +**Coven plan SHA-256:** not recorded before plan approval **Coven specification source commit:** `42dcbc43-34cb48ec-af63efb5-50345e3e-ea2fb7ad` | Coven source | Immutable URL | SHA-256 | @@ -18,35 +18,35 @@ | Criterion | Command | Result | Artifact | |---|---|---|---| -| Canonical ID prefixes and execution-binding identity | `cargo test -p psyche-core --test contracts -- --exact delivery_keeps_the_canonical_del_prefix && cargo test -p psyche-core --test contracts -- --exact delegation_uses_the_distinct_dlg_prefix && cargo test -p psyche-core --test contracts -- --exact execution_binding_uses_attempt_as_its_only_record_kind` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Complete canonical error enum | `cargo test -p psyche-core --test contracts -- --exact all_canonical_error_codes_decode` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Canonical delivery v1 shape | `cargo test -p psyche-core --test contracts -- --exact delivery_v1_fixture_round_trips_canonically && cargo test -p psyche-store --test records -- --exact delivery_direct_insert_round_trips_canonically` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Surface and quarantine owned types | `cargo test -p psyche-core --test contracts -- --exact surface_event_and_effect_fixtures_round_trip && cargo test -p psyche-store --test retention -- --exact quarantine_id_constructor_parser_and_serde_round_trip` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Package-local nullable-binding fixtures | `cargo test -p psyche-core --test contracts -- --exact graph_and_node_accept_only_the_two_frozen_nullable_bindings` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Exhaustive registered decode | `cargo test -p psyche-core --test decode -- --exact recognized_error_envelope_decodes_exhaustively` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Unknown kind/version/enum denial and quarantine | `cargo test -p psyche-core --test decode -- --exact unknown_typed_enum_is_a_quarantinable_decode_failure && cargo test -p psyche-store --test retention -- --exact unknown_enum_is_quarantined_without_dispatchable_record` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Quarantine resolution | `cargo test -p psyche-store --test retention -- --exact quarantine_resolution_is_durable_and_idempotent && cargo test -p psyche-store --test retention -- --exact concurrent_quarantine_resolution_has_one_durable_winner` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Direct typed insert validation | `cargo test -p psyche-store --test records -- --exact direct_insert_rejects_wrong_field_id_kind_without_writing && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_acknowledged_cancellation_without_evidence && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_acknowledged_state_without_termination_correlation && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_mismatched_cancellation_evidence && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_wrong_termination_request_id && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_termination_before_execution_request && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_acknowledgement_outside_termination_window && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_acknowledgement_before_termination_window && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_unresolved_outside_termination_window && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_unresolved_before_termination_window && cargo test -p psyche-store --test records -- --exact direct_insert_accepts_acknowledgement_at_termination_window_boundaries && cargo test -p psyche-store --test records -- --exact direct_insert_accepts_unresolved_at_termination_window_boundaries && cargo test -p psyche-store --test records -- --exact direct_insert_accepts_termination_window_after_execution_deadline && cargo test -p psyche-store --test records -- --exact direct_insert_accepts_termination_at_execution_creation_boundary` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Append-only execution-binding revisions | `cargo test -p psyche-store --test records -- --exact execution_binding_revision_appends_termination_outcomes_without_record_conflict && cargo test -p psyche-store --test records -- --exact execution_binding_revision_rejects_forks_gaps_and_changed_correlation && cargo test -p psyche-store --test records -- --exact execution_binding_revision_replay_is_idempotent && cargo test -p psyche-store --test records -- --exact execution_binding_revision_rejects_same_revision_changed_bytes && cargo test -p psyche-store --test records -- --exact execution_binding_revision_rejects_changed_reason_replay && cargo test -p psyche-store --test records -- --exact execution_binding_revision_rejects_every_frozen_execution_field_change && cargo test -p psyche-store --test records -- --exact execution_binding_revision_rejects_session_and_termination_rebinding && cargo test -p psyche-store --test records -- --exact execution_binding_revision_rejects_termination_correlation_removal && cargo test -p psyche-store --test records -- --exact execution_binding_revision_rejects_timestamp_regression && cargo test -p psyche-store --test retention -- --exact pruning_preserves_unresolved_quarantine_binding_revisions_and_transitions` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Transition contract and append-only rules | `cargo test -p psyche-store --test records -- --exact transition_versions_are_monotonic_and_append_only` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Checkpoint-failure shutdown | `cargo test -p psyche-runtime --lib -- --exact tests::checkpoint_failure_stops_and_releases_every_shutdown_waiter` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Migrations | `cargo test -p psyche-store --test migrations -- --exact fresh_store_applies_v1_once_and_reopens` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| State-machine/property | `cargo test -p psyche-test-support --test state_machine -- --exact model_and_store_agree_after_any_foundation_operation_sequence` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Crash/restart | `cargo test -p psyche-store --features test-fault-injection --test crash -- --exact killed_writer_exposes_only_committed_state_after_reopen` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Fake boundaries and durable termination ordering | `cargo test -p psyche-test-support --test fakes -- --exact advertised_adoption_requires_a_scripted_adoption_step && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_requires_durable_session_bound_revision && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_persists_acknowledged_outcome_before_success && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_persists_unresolved_outcome_before_success && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_exact_replay_is_idempotent && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_crash_after_response_leaves_recoverable_request && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_restart_recovers_missing_outcome && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_rejects_conflicting_replay_response && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_rejects_invalid_outcome_evidence && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_rejects_unresolved_outside_termination_window && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_reports_indeterminate_outcome_persistence && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_accepts_concurrent_exact_outcome_replay && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_rejects_concurrent_divergent_outcome && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_rejects_outcome_byte_attestation_mismatch` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Execution request RFC3339 golden bytes | `cargo test -p psyche-coven --test request_digest -- --exact execution_request_launch_matches_golden_bytes_and_digest && cargo test -p psyche-coven --test request_digest -- --exact execution_request_input_matches_golden_bytes_and_digest` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Validated termination dispatch | `cargo test -p psyche-coven --test bindings -- --exact termination_dispatch_rejects_invalid_request_before_persistence` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| G2 cancellation-state vocabulary | `cargo test -p psyche-core --test contracts -- --exact cancellation_state_vocabulary_requires_matching_o5_evidence` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| Full execution-request digest binding | `cargo test -p psyche-test-support --test state_machine -- --exact request_digest_binds_every_typed_field` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| C-S1 scripted contract negotiation | `cargo test -p psyche-test-support --test conformance -- --exact c_s1_contract_negotiation` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| C-S2 scripted session lifecycle | `cargo test -p psyche-test-support --test conformance -- --exact c_s2_session_lifecycle` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| C-S3 scripted snapshot/attempt binding | `cargo test -p psyche-test-support --test conformance -- --exact c_s3_snapshot_attempt_binding` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| C-S4 scripted stable adoption | `cargo test -p psyche-test-support --test conformance -- --exact c_s4_stable_adoption` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| C-S5 scripted non-adoption proof | `cargo test -p psyche-test-support --test conformance -- --exact c_s5_non_adoption_proof` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| C-S6 scripted ambiguity reconciliation/fence | `cargo test -p psyche-test-support --test state_machine -- --exact c_s6_model_never_redispatches_without_fence && cargo test -p psyche-test-support --test conformance -- --exact c_s6_ambiguity_fence` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| C-S7 scripted ordered cursor | `cargo test -p psyche-test-support --test conformance -- --exact c_s7_ordered_cursor` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| C-S8 scripted terminal authority | `cargo test -p psyche-test-support --test conformance -- --exact c_s8_terminal_authority` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| C-S9 scripted O5 cancellation acknowledgement | `cargo test -p psyche-test-support --test conformance -- --exact c_s9_cancellation_acknowledgement` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| C-S10 scripted result/artifact binding | `cargo test -p psyche-coven --test bindings -- --exact result_bundle_fixture_round_trips_complete_content_references && cargo test -p psyche-coven --test bindings -- --exact result_bundle_fixture_uses_launch_request_correlation && cargo test -p psyche-coven --test bindings -- --exact content_reference_rejects_digest_size_media_type_and_lifetime_mismatch && cargo test -p psyche-test-support --test conformance -- --exact c_s10_result_artifact_binding` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| C-S11 scripted restart persistence | `cargo test -p psyche-test-support --test conformance -- --exact c_s11_restart_persistence` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | -| C-S12 scripted structured denial | `cargo test -p psyche-test-support --test conformance -- --exact c_s12_structured_denial` | passed | https://github.com/OpenCoven/psyche/actions/runs/31290123379 | +| Canonical ID prefixes and execution-binding identity | `cargo test -p psyche-core --test contracts -- --exact delivery_keeps_the_canonical_del_prefix && cargo test -p psyche-core --test contracts -- --exact delegation_uses_the_distinct_dlg_prefix && cargo test -p psyche-core --test contracts -- --exact execution_binding_uses_attempt_as_its_only_record_kind` | not run remotely | none | +| Complete canonical error enum | `cargo test -p psyche-core --test contracts -- --exact all_canonical_error_codes_decode` | not run remotely | none | +| Canonical delivery v1 shape | `cargo test -p psyche-core --test contracts -- --exact delivery_v1_fixture_round_trips_canonically && cargo test -p psyche-store --test records -- --exact delivery_direct_insert_round_trips_canonically` | not run remotely | none | +| Surface and quarantine owned types | `cargo test -p psyche-core --test contracts -- --exact surface_event_and_effect_fixtures_round_trip && cargo test -p psyche-store --test retention -- --exact quarantine_id_constructor_parser_and_serde_round_trip` | not run remotely | none | +| Package-local nullable-binding fixtures | `cargo test -p psyche-core --test contracts -- --exact graph_and_node_accept_only_the_two_frozen_nullable_bindings` | not run remotely | none | +| Exhaustive registered decode | `cargo test -p psyche-core --test decode -- --exact recognized_error_envelope_decodes_exhaustively` | not run remotely | none | +| Unknown kind/version/enum denial and quarantine | `cargo test -p psyche-core --test decode -- --exact unknown_typed_enum_is_a_quarantinable_decode_failure && cargo test -p psyche-store --test retention -- --exact unknown_enum_is_quarantined_without_dispatchable_record` | not run remotely | none | +| Quarantine resolution | `cargo test -p psyche-store --test retention -- --exact quarantine_resolution_is_durable_and_idempotent && cargo test -p psyche-store --test retention -- --exact concurrent_quarantine_resolution_has_one_durable_winner` | not run remotely | none | +| Direct typed insert validation | `cargo test -p psyche-store --test records -- --exact direct_insert_rejects_wrong_field_id_kind_without_writing && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_acknowledged_cancellation_without_evidence && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_acknowledged_state_without_termination_correlation && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_mismatched_cancellation_evidence && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_wrong_termination_request_id && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_termination_before_execution_request && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_acknowledgement_outside_termination_window && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_acknowledgement_before_termination_window && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_unresolved_outside_termination_window && cargo test -p psyche-store --test records -- --exact direct_insert_rejects_unresolved_before_termination_window && cargo test -p psyche-store --test records -- --exact direct_insert_accepts_acknowledgement_at_termination_window_boundaries && cargo test -p psyche-store --test records -- --exact direct_insert_accepts_unresolved_at_termination_window_boundaries && cargo test -p psyche-store --test records -- --exact direct_insert_accepts_termination_window_after_execution_deadline && cargo test -p psyche-store --test records -- --exact direct_insert_accepts_termination_at_execution_creation_boundary` | not run remotely | none | +| Append-only execution-binding revisions | `cargo test -p psyche-store --test records -- --exact execution_binding_revision_appends_termination_outcomes_without_record_conflict && cargo test -p psyche-store --test records -- --exact execution_binding_revision_rejects_forks_gaps_and_changed_correlation && cargo test -p psyche-store --test records -- --exact execution_binding_revision_replay_is_idempotent && cargo test -p psyche-store --test records -- --exact execution_binding_revision_rejects_same_revision_changed_bytes && cargo test -p psyche-store --test records -- --exact execution_binding_revision_rejects_changed_reason_replay && cargo test -p psyche-store --test records -- --exact execution_binding_revision_rejects_every_frozen_execution_field_change && cargo test -p psyche-store --test records -- --exact execution_binding_revision_rejects_session_and_termination_rebinding && cargo test -p psyche-store --test records -- --exact execution_binding_revision_rejects_termination_correlation_removal && cargo test -p psyche-store --test records -- --exact execution_binding_revision_rejects_timestamp_regression && cargo test -p psyche-store --test retention -- --exact pruning_preserves_unresolved_quarantine_binding_revisions_and_transitions` | not run remotely | none | +| Transition contract and append-only rules | `cargo test -p psyche-store --test records -- --exact transition_versions_are_monotonic_and_append_only` | not run remotely | none | +| Checkpoint-failure shutdown | `cargo test -p psyche-runtime --lib -- --exact tests::checkpoint_failure_stops_and_releases_every_shutdown_waiter` | not run remotely | none | +| Migrations | `cargo test -p psyche-store --test migrations -- --exact fresh_store_applies_v1_once_and_reopens` | not run remotely | none | +| State-machine/property | `cargo test -p psyche-test-support --test state_machine -- --exact model_and_store_agree_after_any_foundation_operation_sequence` | not run remotely | none | +| Crash/restart | `cargo test -p psyche-store --features test-fault-injection --test crash -- --exact killed_writer_exposes_only_committed_state_after_reopen` | not run remotely | none | +| Fake boundaries and durable termination ordering | `cargo test -p psyche-test-support --test fakes -- --exact advertised_adoption_requires_a_scripted_adoption_step && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_requires_durable_session_bound_revision && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_persists_acknowledged_outcome_before_success && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_persists_unresolved_outcome_before_success && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_exact_replay_is_idempotent && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_crash_after_response_leaves_recoverable_request && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_restart_recovers_missing_outcome && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_rejects_conflicting_replay_response && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_rejects_invalid_outcome_evidence && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_rejects_unresolved_outside_termination_window && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_reports_indeterminate_outcome_persistence && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_accepts_concurrent_exact_outcome_replay && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_rejects_concurrent_divergent_outcome && cargo test -p psyche-test-support --test fakes -- --exact termination_dispatch_rejects_outcome_byte_attestation_mismatch` | not run remotely | none | +| Execution request RFC3339 golden bytes | `cargo test -p psyche-coven --test request_digest -- --exact execution_request_launch_matches_golden_bytes_and_digest && cargo test -p psyche-coven --test request_digest -- --exact execution_request_input_matches_golden_bytes_and_digest` | not run remotely | none | +| Validated termination dispatch | `cargo test -p psyche-coven --test bindings -- --exact termination_dispatch_rejects_invalid_request_before_persistence` | not run remotely | none | +| G2 cancellation-state vocabulary | `cargo test -p psyche-core --test contracts -- --exact cancellation_state_vocabulary_requires_matching_o5_evidence` | not run remotely | none | +| Full execution-request digest binding | `cargo test -p psyche-test-support --test state_machine -- --exact request_digest_binds_every_typed_field` | not run remotely | none | +| C-S1 scripted contract negotiation | `cargo test -p psyche-test-support --test conformance -- --exact c_s1_contract_negotiation` | not run remotely | none | +| C-S2 scripted session lifecycle | `cargo test -p psyche-test-support --test conformance -- --exact c_s2_session_lifecycle` | not run remotely | none | +| C-S3 scripted snapshot/attempt binding | `cargo test -p psyche-test-support --test conformance -- --exact c_s3_snapshot_attempt_binding` | not run remotely | none | +| C-S4 scripted stable adoption | `cargo test -p psyche-test-support --test conformance -- --exact c_s4_stable_adoption` | not run remotely | none | +| C-S5 scripted non-adoption proof | `cargo test -p psyche-test-support --test conformance -- --exact c_s5_non_adoption_proof` | not run remotely | none | +| C-S6 scripted ambiguity reconciliation/fence | `cargo test -p psyche-test-support --test state_machine -- --exact c_s6_model_never_redispatches_without_fence && cargo test -p psyche-test-support --test conformance -- --exact c_s6_ambiguity_fence` | not run remotely | none | +| C-S7 scripted ordered cursor | `cargo test -p psyche-test-support --test conformance -- --exact c_s7_ordered_cursor` | not run remotely | none | +| C-S8 scripted terminal authority | `cargo test -p psyche-test-support --test conformance -- --exact c_s8_terminal_authority` | not run remotely | none | +| C-S9 scripted O5 cancellation acknowledgement | `cargo test -p psyche-test-support --test conformance -- --exact c_s9_cancellation_acknowledgement` | not run remotely | none | +| C-S10 scripted result/artifact binding | `cargo test -p psyche-coven --test bindings -- --exact result_bundle_fixture_round_trips_complete_content_references && cargo test -p psyche-coven --test bindings -- --exact result_bundle_fixture_uses_launch_request_correlation && cargo test -p psyche-coven --test bindings -- --exact content_reference_rejects_digest_size_media_type_and_lifetime_mismatch && cargo test -p psyche-test-support --test conformance -- --exact c_s10_result_artifact_binding` | not run remotely | none | +| C-S11 scripted restart persistence | `cargo test -p psyche-test-support --test conformance -- --exact c_s11_restart_persistence` | not run remotely | none | +| C-S12 scripted structured denial | `cargo test -p psyche-test-support --test conformance -- --exact c_s12_structured_denial` | not run remotely | none |