diff --git a/crates/tinysecurity-approval/src/engine_tests.rs b/crates/tinysecurity-approval/src/engine_tests.rs index e5a00092..f565fe22 100644 --- a/crates/tinysecurity-approval/src/engine_tests.rs +++ b/crates/tinysecurity-approval/src/engine_tests.rs @@ -429,6 +429,35 @@ fn action(row: &ApprovalRow2, phase: u64) -> ApprovalActionRequest2 { } } #[tokio::test] +async fn abandoned_execution_is_persisted_as_an_immutable_unknown_effect() -> Result<()> { + let f = Arc::new(Fixture::default()); + let owner = engine(f, Arc::new(TestClock::default()))?; + let row = owner.begin(request()).await?; + owner.decide(decision(&row)).await?; + owner.consume(action(&row, 3)).await?; + + let terminal = RecordExecutionRequest2 { + action: action(&row, 4), + outcome: ExecutionOutcome::Abandoned, + }; + let finished = owner.record_execution(terminal.clone()).await?; + assert_eq!(finished.state, ApprovalLifecycle2::Executed); + assert_eq!(finished.outcome, Some(ExecutionOutcome::Abandoned)); + assert_eq!( + owner.list(&request().scope_id).await?, + vec![finished.clone()] + ); + assert_eq!(owner.record_execution(terminal).await?, finished); + + let mut cancelled = RecordExecutionRequest2 { + action: action(&row, 4), + outcome: ExecutionOutcome::Cancelled, + }; + cancelled.action.audit = audit(5); + assert!(owner.record_execution(cancelled).await.is_err()); + Ok(()) +} +#[tokio::test] async fn exact_human_ticket_first_winner_once_consumption_and_terminal_outcome() -> Result<()> { let f = Arc::new(Fixture::default()); let owner = engine(f, Arc::new(TestClock::default()))?; diff --git a/crates/tinysecurity-bus/src/approval.rs b/crates/tinysecurity-bus/src/approval.rs index d75e7c83..79e24c34 100644 --- a/crates/tinysecurity-bus/src/approval.rs +++ b/crates/tinysecurity-bus/src/approval.rs @@ -300,7 +300,7 @@ impl ExecutionOutcome { match value { "success" => Ok(Self::Succeeded), "failure" => Ok(Self::Failed), - "aborted" => Ok(Self::Cancelled), + "aborted" => Ok(Self::Abandoned), _ => Err(crate::Error::InvalidArguments), } } diff --git a/crates/tinysecurity-bus/src/approval_tests.rs b/crates/tinysecurity-bus/src/approval_tests.rs index f4256b04..c230797b 100644 --- a/crates/tinysecurity-bus/src/approval_tests.rs +++ b/crates/tinysecurity-bus/src/approval_tests.rs @@ -11,7 +11,7 @@ fn legacy_execution_outcomes_are_explicit_and_unknown_values_reject() { ); assert_eq!( ExecutionOutcome::from_legacy("aborted"), - Ok(ExecutionOutcome::Cancelled) + Ok(ExecutionOutcome::Abandoned) ); assert!(ExecutionOutcome::from_legacy("approved").is_err()); } diff --git a/crates/tinysecurity-bus/src/device_frame_tests.rs b/crates/tinysecurity-bus/src/device_frame_tests.rs index c3ed01f8..1d5dedb9 100644 --- a/crates/tinysecurity-bus/src/device_frame_tests.rs +++ b/crates/tinysecurity-bus/src/device_frame_tests.rs @@ -70,5 +70,5 @@ fn frame_operations_redact_both_payloads_without_changing_open_wire_shape() #[test] fn frame_member_name_is_stable_and_contract_version_is_bumped() { assert_eq!(crate::names::methods::DEVICE_FRAME2, "DeviceFrame2"); - assert_eq!(crate::CONTRACT_VERSION, (1, 27)); + assert_eq!(crate::CONTRACT_VERSION, (1, 28)); } diff --git a/crates/tinysecurity-bus/src/device_pairing_tests.rs b/crates/tinysecurity-bus/src/device_pairing_tests.rs index bde96c1f..c417a301 100644 --- a/crates/tinysecurity-bus/src/device_pairing_tests.rs +++ b/crates/tinysecurity-bus/src/device_pairing_tests.rs @@ -85,7 +85,7 @@ fn refresh_and_close_are_distinct_transient_session_operations() { #[test] fn pairing_contract_is_versioned_after_the_frame_contract() { - assert_eq!(CONTRACT_VERSION, (1, 27)); + assert_eq!(CONTRACT_VERSION, (1, 28)); } #[test] diff --git a/crates/tinysecurity-bus/src/future.rs b/crates/tinysecurity-bus/src/future.rs index 6de58fad..b42dda0d 100644 --- a/crates/tinysecurity-bus/src/future.rs +++ b/crates/tinysecurity-bus/src/future.rs @@ -20,7 +20,7 @@ pub enum ApprovalState { /// Executed state. Executed, } -/// `ExecutionOutcome` vocabulary, reserved until its engine is implemented. +/// Terminal execution result, including runs abandoned before their effects were known. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ExecutionOutcome { @@ -30,6 +30,8 @@ pub enum ExecutionOutcome { Failed, /// Cancelled state. Cancelled, + /// The in-flight execution was abandoned before its effects could be determined. + Abandoned, } /// `ReplyVerdict` vocabulary, reserved until its engine is implemented. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] diff --git a/crates/tinysecurity-bus/src/lib.rs b/crates/tinysecurity-bus/src/lib.rs index 0933b5eb..0c13ca7f 100644 --- a/crates/tinysecurity-bus/src/lib.rs +++ b/crates/tinysecurity-bus/src/lib.rs @@ -65,7 +65,7 @@ pub use scoped_policy::*; pub use tool_trust::*; pub mod names; /// Current wire vocabulary version, independent of the artifact version. -pub const CONTRACT_VERSION: (u32, u32) = (1, 27); +pub const CONTRACT_VERSION: (u32, u32) = (1, 28); /// Whether a remote module's advertised version supplies this host's vocabulary. /// /// `version` is the provider version received by the host, not a host requirement diff --git a/crates/tinysecurity-bus/tests/fixtures/v1.json b/crates/tinysecurity-bus/tests/fixtures/v1.json index 17b1df30..159334a3 100644 --- a/crates/tinysecurity-bus/tests/fixtures/v1.json +++ b/crates/tinysecurity-bus/tests/fixtures/v1.json @@ -590,7 +590,8 @@ "ExecutionOutcome": [ "succeeded", "failed", - "cancelled" + "cancelled", + "abandoned" ], "ReplyVerdict": [ "allow_once", diff --git a/crates/tinysecurity-bus/tests/scoped_wire.rs b/crates/tinysecurity-bus/tests/scoped_wire.rs index 3b9bebe3..8f03e9bf 100644 --- a/crates/tinysecurity-bus/tests/scoped_wire.rs +++ b/crates/tinysecurity-bus/tests/scoped_wire.rs @@ -84,9 +84,9 @@ fn new_members_are_advertised_and_named_by_contract_constants() { #[test] fn contract_bump_is_additive_for_older_hosts_and_modules() { - assert_eq!(CONTRACT_VERSION, (1, 27)); - // A host built against 1.27 accepts this module and newer minors, not a 1.26 module. - assert!(is_compatible((1, 27))); + assert_eq!(CONTRACT_VERSION, (1, 28)); + // A host built against 1.28 accepts this module and newer minors, not a 1.27 module. + assert!(is_compatible((1, 28))); assert!(!is_compatible((1, 26))); assert!(!is_compatible((1, 25))); assert!(!is_compatible((1, 24))); diff --git a/crates/tinysecurity-bus/tests/wire.rs b/crates/tinysecurity-bus/tests/wire.rs index 4f749965..18bc777b 100644 --- a/crates/tinysecurity-bus/tests/wire.rs +++ b/crates/tinysecurity-bus/tests/wire.rs @@ -114,6 +114,8 @@ fn all_execution_outcome_variants_have_stable_wire_names() assert_eq!(serde_json::to_value(decoded)?, *v); } assert!(serde_json::from_str::("\"future_unknown\"").is_err()); + let abandoned: ExecutionOutcome = serde_json::from_str("\"abandoned\"")?; + assert_eq!(serde_json::to_string(&abandoned)?, "\"abandoned\""); Ok(()) } #[test] @@ -343,7 +345,7 @@ fn redactor_init_is_additive_and_secrets_never_enter_public_info() serde_json::to_value(&config)?["redactor"]["secrets"][0], "literal-private" ); - assert_eq!(CONTRACT_VERSION, (1, 27)); + assert_eq!(CONTRACT_VERSION, (1, 28)); for (mode, wire) in [ (RedactMode::Text, "text"), (RedactMode::StructuredArguments, "structured_arguments"),