app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 4182c7e6574dd44e3efe34306049419fb9f1b0a8
parent ec6a68eb76c7df3417c03d770f50621391d0db07
Author: triesap <tyson@radroots.org>
Date:   Mon, 10 Aug 2026 03:05:08 +0000

operations: adopt durable UUIDv7 request identities

- validate canonical UUIDv7 operation identifiers across Rust and Kotlin
- mint operation and native handle identities without process-local counters
- preserve durable replay, conflict, recovery, and correlation behavior
- refresh FFI compatibility metadata and cover identifier invariants

Diffstat:
Mapp/desktop/src/main/kotlin/org/harvestcircle/application/HarvestCircleApplication.kt | 6++----
Mapp/desktop/src/main/kotlin/org/harvestcircle/application/NativeCompatibility.kt | 2+-
Mapp/desktop/src/main/kotlin/org/harvestcircle/application/NativeHarvestCircleRuntime.kt | 10++++------
Mapp/desktop/src/test/kotlin/org/harvestcircle/application/HarvestCircleApplicationTest.kt | 8+++++---
Mapp/desktop/src/test/kotlin/org/harvestcircle/application/NativeRuntimeMappingsTest.kt | 25++++++++++++++-----------
Mapp/desktop/src/test/kotlin/org/harvestcircle/architecture/ProductNamespaceGuardTest.kt | 16++++++++++++++++
Mapp/shared/src/commonMain/kotlin/org/harvestcircle/application/RuntimeIdentifiers.kt | 3++-
Mapp/shared/src/commonTest/kotlin/org/harvestcircle/application/HarvestCirclePresenterTest.kt | 8+++++---
Mapp/shared/src/commonTest/kotlin/org/harvestcircle/application/RuntimeContractsTest.kt | 12+++++++++---
Mcore/Cargo.lock | 1+
Mcore/compatibility/harvestcircle-ffi-v4.properties | 2+-
Mcore/crates/harvestcircle_application/Cargo.toml | 1+
Mcore/crates/harvestcircle_application/src/ports.rs | 47++++++++++++++++++++++++++++++++++++-----------
Mcore/crates/harvestcircle_application/src/recovery.rs | 2+-
Mcore/crates/harvestcircle_ffi/src/commands.rs | 18+++++++++---------
Mcore/crates/harvestcircle_ffi/src/lib.rs | 10++++++++++
Mcore/crates/harvestcircle_ffi/src/observer.rs | 2+-
Mcore/crates/harvestcircle_runtime/src/persistence.rs | 12++++++++----
Mcore/crates/harvestcircle_runtime/src/runtime_actor.rs | 24++++++++++++++++++------
Mcore/crates/harvestcircle_storage/src/journal.rs | 9++++++---
20 files changed, 150 insertions(+), 68 deletions(-)

diff --git a/app/desktop/src/main/kotlin/org/harvestcircle/application/HarvestCircleApplication.kt b/app/desktop/src/main/kotlin/org/harvestcircle/application/HarvestCircleApplication.kt @@ -12,13 +12,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.withTimeoutOrNull +import org.harvestcircle.ffi.generateOperationIdV7 import org.harvestcircle.identities.ui.HarvestCirclePlatformActions import org.harvestcircle.identities.ui.HarvestCircleScreen import org.harvestcircle.identities.ui.HarvestCircleUiActions import org.harvestcircle.identities.ui.ShutdownFailureScreen import org.harvestcircle.identities.ui.StartupFailureScreen import org.harvestcircle.identities.ui.toUiModel -import java.util.concurrent.atomic.AtomicLong internal typealias HarvestCirclePresenterFactory = (CoroutineScope) -> HarvestCirclePresenter @@ -110,7 +110,5 @@ private object DesktopApplicationClock : ApplicationClock { } private object DesktopOperationIdSource : OperationIdSource { - private val next = AtomicLong(1) - - override fun next(): OperationId = OperationId.from("desktop-operation:${next.getAndIncrement()}") + override fun next(): OperationId = OperationId.from(generateOperationIdV7()) } diff --git a/app/desktop/src/main/kotlin/org/harvestcircle/application/NativeCompatibility.kt b/app/desktop/src/main/kotlin/org/harvestcircle/application/NativeCompatibility.kt @@ -10,7 +10,7 @@ internal const val EXPECTED_DISTRIBUTION_PACKAGE_VERSION = "1.0.0" internal const val EXPECTED_PRODUCT_COORDINATE_DIGEST = "f81db525a0228782530799911879fb55cb25e8e631d09605fd5084e9bd88fbbe" internal const val EXPECTED_SOURCE_PROVENANCE_DIGEST = "d4d54ab897e98a93dfbe27a9d9589dbc38c3b7e2163097617d59beaf64e0358c" internal const val EXPECTED_SOURCE_FOUNDATION_BASELINE = "a2038b3e25b9e34f0b8fd001f26a8ed10b5772cb" -internal const val EXPECTED_FFI_CONTRACT_HASH = "638a2d8d18bb4c26a4d68a5c6d34294fc0b76c51c068ea116f26d1b598f62a75" +internal const val EXPECTED_FFI_CONTRACT_HASH = "cfbf28d566b8379904f276500be724a61e549cd04fa2491e9305abc29163c0ef" internal val EXPECTED_FFI_CONTRACT_MAJOR: UShort = 4.toUShort() internal val MINIMUM_FFI_CONTRACT_MINOR: UShort = 0.toUShort() internal const val EXPECTED_SNAPSHOT_SCHEMA: UInt = 1U diff --git a/app/desktop/src/main/kotlin/org/harvestcircle/application/NativeHarvestCircleRuntime.kt b/app/desktop/src/main/kotlin/org/harvestcircle/application/NativeHarvestCircleRuntime.kt @@ -20,11 +20,11 @@ import org.harvestcircle.ffi.RequestContextDto import org.harvestcircle.ffi.ShutdownReceiptDto import org.harvestcircle.ffi.SnapshotChangeDto import org.harvestcircle.ffi.compatibilityDescriptor -import java.util.concurrent.atomic.AtomicLong +import org.harvestcircle.ffi.generateOperationIdV7 class NativeHarvestCircleRuntime internal constructor( private val native: NativeCorePort, - private val handleIds: NativeHandleIdSource = AtomicNativeHandleIdSource(), + private val handleIds: NativeHandleIdSource = GeneratedNativeHandleIdSource, ) : HarvestCircleRuntime { private val recoveryMutex = Mutex() private val recoveryHandles = mutableMapOf<RecoveryRequestId, NativeGeneratedRecoveryHandle>() @@ -260,10 +260,8 @@ internal fun interface NativeHandleIdSource { fun next(kind: String): String } -private class AtomicNativeHandleIdSource : NativeHandleIdSource { - private val next = AtomicLong(1) - - override fun next(kind: String): String = "native-$kind:${next.getAndIncrement()}" +private object GeneratedNativeHandleIdSource : NativeHandleIdSource { + override fun next(kind: String): String = "$kind:${generateOperationIdV7()}" } internal interface NativeCorePort : AutoCloseable { diff --git a/app/desktop/src/test/kotlin/org/harvestcircle/application/HarvestCircleApplicationTest.kt b/app/desktop/src/test/kotlin/org/harvestcircle/application/HarvestCircleApplicationTest.kt @@ -43,7 +43,7 @@ class HarvestCircleApplicationTest { runtime = createdRuntime, scope = scope, clock = ApplicationClock { UnixSeconds(0) }, - operationIds = OperationIdSource { OperationId.from("application-test") }, + operationIds = OperationIdSource { OperationId.from(TEST_OPERATION_ID) }, ) } BasicText( @@ -77,7 +77,7 @@ class HarvestCircleApplicationTest { runtime = ApplicationRuntime(shutdownClosed = false), scope = scope, clock = ApplicationClock { UnixSeconds(0) }, - operationIds = OperationIdSource { OperationId.from("application-test") }, + operationIds = OperationIdSource { OperationId.from(TEST_OPERATION_ID) }, ) } } @@ -103,7 +103,7 @@ class HarvestCircleApplicationTest { runtime = ApplicationRuntime(shutdownGate = CompletableDeferred()), scope = scope, clock = ApplicationClock { UnixSeconds(0) }, - operationIds = OperationIdSource { OperationId.from("application-test") }, + operationIds = OperationIdSource { OperationId.from(TEST_OPERATION_ID) }, ) } } @@ -171,3 +171,5 @@ private fun applicationSnapshot(revision: SnapshotRevision) = activeIdentity = null, recoverableProblem = null, ) + +private const val TEST_OPERATION_ID = "01890f3e-7b1c-7000-8000-000000000009" diff --git a/app/desktop/src/test/kotlin/org/harvestcircle/application/NativeRuntimeMappingsTest.kt b/app/desktop/src/test/kotlin/org/harvestcircle/application/NativeRuntimeMappingsTest.kt @@ -135,17 +135,17 @@ class NativeRuntimeMappingsTest { fun receiptChangeContextAndShutdownMappingsPreserveRevisions() { val snapshot = populatedSnapshot(revision = 2UL) val result = - IdentityCommandReceiptDto("operation-7", 2UL, snapshot) + IdentityCommandReceiptDto(TEST_OPERATION_ID, 2UL, snapshot) .toApplicationResult() - assertEquals(OperationId.from("operation-7"), result.operationId) + assertEquals(OperationId.from(TEST_OPERATION_ID), result.operationId) assertEquals(SnapshotRevision(2UL), result.committedRevision) val change = SnapshotChangeDto(snapshot, 1UL).toApplicationChange() assertEquals(SnapshotRevision(1UL), change.previousRevision) assertEquals(SnapshotRevision(2UL), change.snapshot.revision) - val context = RequestContext(OperationId.from("operation-7"), SnapshotRevision(1UL), 5_000UL).toNative() - assertEquals("operation-7", context.requestId) + val context = RequestContext(OperationId.from(TEST_OPERATION_ID), SnapshotRevision(1UL), 5_000UL).toNative() + assertEquals(TEST_OPERATION_ID, context.requestId) assertEquals(1UL, context.expectedRevision) assertEquals(5_000UL, context.deadlineMillis) @@ -164,17 +164,17 @@ class NativeRuntimeMappingsTest { category = WireErrorCategory.CREDENTIAL, retryable = false, recoveryAction = WireRecoveryAction.REPAIR_CREDENTIAL, - correlationId = "operation-7", + correlationId = TEST_OPERATION_ID, safeMessage = "The local credential is unavailable.", ).toApplicationProblem() assertEquals(ApplicationErrorCode.CredentialMissing, native.code) - assertEquals(OperationId.from("operation-7"), native.operationId) + assertEquals(OperationId.from(TEST_OPERATION_ID), native.operationId) assertEquals(RecoveryAction.RepairCredential, native.recoveryAction) - val unknown = IllegalStateException("sensitive detail").toApplicationProblem(OperationId.from("fallback-1")) + val unknown = IllegalStateException("sensitive detail").toApplicationProblem(OperationId.from(FALLBACK_OPERATION_ID)) assertEquals(ApplicationErrorCode.Internal, unknown.code) assertEquals("The application command failed.", unknown.safeMessage) - assertEquals(OperationId.from("fallback-1"), unknown.operationId) + assertEquals(OperationId.from(FALLBACK_OPERATION_ID), unknown.operationId) assertFalse(unknown.toString().contains("sensitive detail")) } @@ -191,7 +191,7 @@ class NativeHarvestCircleRuntimeTest { runTest { val port = FakeNativeCorePort() val runtime = NativeHarvestCircleRuntime(port, NativeHandleIdSource { kind -> "$kind-1" }) - val context = RequestContext(OperationId.from("operation-7"), SnapshotRevision(2UL), 5_000UL) + val context = RequestContext(OperationId.from(TEST_OPERATION_ID), SnapshotRevision(2UL), 5_000UL) val secret = SecretKeyInput.from("nsec1boundedsecret") val imported = @@ -266,7 +266,7 @@ private class FakeNativeCorePort : NativeCorePort { context: RequestContextDto, request: NativeGeneratedRecoveryHandle, ): AppSnapshotDto { - assertEquals("operation-7", context.requestId) + assertEquals(TEST_OPERATION_ID, context.requestId) assertEquals(generated, request) return snapshot } @@ -295,7 +295,7 @@ private class FakeNativeCorePort : NativeCorePort { context: RequestContextDto, request: NativeRemovalHandle, ): AppSnapshotDto { - assertEquals("operation-7", context.requestId) + assertEquals(TEST_OPERATION_ID, context.requestId) assertEquals(removal, request) return snapshot } @@ -397,3 +397,6 @@ private fun safeError(code: WireErrorCode): SafeErrorDto = recoveryAction = WireRecoveryAction.RETRY, message = "A safe problem occurred.", ) + +private const val TEST_OPERATION_ID = "01890f3e-7b1c-7000-8000-000000000007" +private const val FALLBACK_OPERATION_ID = "01890f3e-7b1c-7000-8000-000000000008" diff --git a/app/desktop/src/test/kotlin/org/harvestcircle/architecture/ProductNamespaceGuardTest.kt b/app/desktop/src/test/kotlin/org/harvestcircle/architecture/ProductNamespaceGuardTest.kt @@ -12,6 +12,22 @@ import kotlin.test.assertTrue class ProductNamespaceGuardTest { @Test + fun productionKotlinDoesNotMintProcessLocalOperationCounters() { + val root = findRepositoryRoot() + val counterType = "Atomic" + "Long" + val legacyPrefix = "desktop" + "-operation:" + val findings = + trackedFiles(root) + .filter { it.startsWith("app/") && it.contains("/src/") && it.contains("/main/") && it.endsWith(".kt") } + .filter { relative -> + val source = root.resolve(relative).readText() + source.contains(counterType) || source.contains(legacyPrefix) + } + + assertEquals(emptyList(), findings.sorted()) + } + + @Test fun trackedSourcesUseTheHarvestCircleNamingContract() { val root = findRepositoryRoot() val contract = root.resolve("AGENTS.md").readText() diff --git a/app/shared/src/commonMain/kotlin/org/harvestcircle/application/RuntimeIdentifiers.kt b/app/shared/src/commonMain/kotlin/org/harvestcircle/application/RuntimeIdentifiers.kt @@ -2,6 +2,7 @@ package org.harvestcircle.application private val lowercaseHex = Regex("[0-9a-f]{64}") private val opaqueIdentifier = Regex("[A-Za-z0-9][A-Za-z0-9._:-]{0,127}") +private val canonicalUuidV7 = Regex("[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}") @JvmInline value class IdentityId private constructor( @@ -21,7 +22,7 @@ value class OperationId private constructor( ) { companion object { fun from(value: String): OperationId { - require(opaqueIdentifier.matches(value)) { "Operation ID is malformed" } + require(canonicalUuidV7.matches(value)) { "Operation ID must be canonical UUIDv7 text" } return OperationId(value) } } diff --git a/app/shared/src/commonTest/kotlin/org/harvestcircle/application/HarvestCirclePresenterTest.kt b/app/shared/src/commonTest/kotlin/org/harvestcircle/application/HarvestCirclePresenterTest.kt @@ -60,13 +60,13 @@ class HarvestCirclePresenterTest { val ids = DeterministicOperationIds() val presenter = presenter(runtime, ids) runCurrent() - runtime.nextFailure = problem(retryable = true, operationId = OperationId.from("operation-1")) + runtime.nextFailure = problem(retryable = true, operationId = OperationId.from(TEST_OPERATION_ID)) presenter.dispatch(HarvestCircleIntent.SignOut) advanceUntilIdle() assertEquals(CommandStatus.FAILED_RETRYABLE, presenter.state.value.commandStatus) - assertEquals(OperationId.from("operation-1"), presenter.state.value.lastCommandOperationId) + assertEquals(OperationId.from(TEST_OPERATION_ID), presenter.state.value.lastCommandOperationId) presenter.dispatch(HarvestCircleIntent.RetryLastCommand) advanceUntilIdle() @@ -192,7 +192,7 @@ private class DeterministicOperationIds : OperationIdSource { override fun next(): OperationId { calls += 1 - return OperationId.from("operation-$calls") + return OperationId.from("01890f3e-7b1c-7000-8000-${calls.toString().padStart(12, '0')}") } } @@ -310,3 +310,5 @@ private fun snapshot(revision: ULong) = activeIdentity = null, recoverableProblem = null, ) + +private const val TEST_OPERATION_ID = "01890f3e-7b1c-7000-8000-000000000001" diff --git a/app/shared/src/commonTest/kotlin/org/harvestcircle/application/RuntimeContractsTest.kt b/app/shared/src/commonTest/kotlin/org/harvestcircle/application/RuntimeContractsTest.kt @@ -12,9 +12,13 @@ class RuntimeContractsTest { val identityId = IdentityId.fromPublicKeyHex("01".repeat(32)) assertEquals("01".repeat(32), identityId.value) assertFailsWith<IllegalArgumentException> { IdentityId.fromPublicKeyHex("AB".repeat(32)) } + assertEquals(TEST_OPERATION_ID, OperationId.from(TEST_OPERATION_ID).value) assertFailsWith<IllegalArgumentException> { OperationId.from("contains space") } + assertFailsWith<IllegalArgumentException> { OperationId.from("01890f3e-7b1c-4000-8000-000000000001") } + assertFailsWith<IllegalArgumentException> { OperationId.from(TEST_OPERATION_ID.uppercase()) } + assertFailsWith<IllegalArgumentException> { OperationId.from("01890f3e-7b1c-6000-8000-000000000001") } assertFailsWith<IllegalArgumentException> { - RequestContext(OperationId.from("operation-1"), SnapshotRevision(0UL), 0UL) + RequestContext(OperationId.from(TEST_OPERATION_ID), SnapshotRevision(0UL), 0UL) } val secret = SecretKeyInput.from("nsec1boundedsecret") @@ -35,7 +39,7 @@ class RuntimeContractsTest { } assertFailsWith<IllegalArgumentException> { ApplicationCommandResult.Committed( - operationId = OperationId.from("operation-1"), + operationId = OperationId.from(TEST_OPERATION_ID), committedRevision = SnapshotRevision(1UL), snapshot = snapshot, ) @@ -105,7 +109,7 @@ class RuntimeContractsTest { @Test fun everyFoundationCommandHasAnExplicitRuntimeKind() { val id = IdentityId.fromPublicKeyHex("01".repeat(32)) - val context = RequestContext(OperationId.from("operation-1"), SnapshotRevision(1UL), 1_000UL) + val context = RequestContext(OperationId.from(TEST_OPERATION_ID), SnapshotRevision(1UL), 1_000UL) val commands = listOf( ApplicationCommand.AcknowledgeGeneratedIdentity(RecoveryRequestId.from("recovery-1"), context), @@ -214,3 +218,5 @@ private fun commandName(value: ApplicationCommand): String = ApplicationCommand.RefreshActiveProfile -> "refresh-profile" is ApplicationCommand.ConfirmIdentityRemoval -> "confirm-removal" } + +private const val TEST_OPERATION_ID = "01890f3e-7b1c-7000-8000-000000000001" diff --git a/core/Cargo.lock b/core/Cargo.lock @@ -1069,6 +1069,7 @@ dependencies = [ "harvestcircle_domain", "secrecy", "tokio", + "uuid", ] [[package]] diff --git a/core/compatibility/harvestcircle-ffi-v4.properties b/core/compatibility/harvestcircle-ffi-v4.properties @@ -2,7 +2,7 @@ schema=harvestcircle.ffi.v4 contract.id=harvestcircle-desktop-ffi-v4 contract.major=4 contract.minor=0 -contract.hash=638a2d8d18bb4c26a4d68a5c6d34294fc0b76c51c068ea116f26d1b598f62a75 +contract.hash=cfbf28d566b8379904f276500be724a61e549cd04fa2491e9305abc29163c0ef product.coordinate_digest=f81db525a0228782530799911879fb55cb25e8e631d09605fd5084e9bd88fbbe snapshot.schema=1 storage.schema.minimum=5 diff --git a/core/crates/harvestcircle_application/Cargo.toml b/core/crates/harvestcircle_application/Cargo.toml @@ -15,6 +15,7 @@ include = ["src/**", "tests/**", "Cargo.toml"] harvestcircle_domain.workspace = true secrecy = "=0.10.3" tokio = { version = "=1.47.1", features = ["macros", "rt-multi-thread", "sync", "time"] } +uuid.workspace = true [lints] workspace = true diff --git a/core/crates/harvestcircle_application/src/ports.rs b/core/crates/harvestcircle_application/src/ports.rs @@ -7,30 +7,30 @@ use harvestcircle_domain::{ SafeErrorCode, SafeMessage, SecretKeyInput, SignerAvailability, UnixTimestamp, }; -const MAX_DURABLE_REQUEST_ID_BYTES: usize = 128; - #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] pub struct DurableRequestId(String); impl DurableRequestId { - /// Validates an opaque caller-generated idempotency key. + /// Validates a canonical caller-generated UUIDv7 idempotency key. /// /// # Errors /// - /// Returns a safe validation error when the value is empty, oversized, or contains anything - /// other than visible ASCII characters. + /// Returns a safe validation error when the value is not canonical UUIDv7 text. pub fn parse(value: impl Into<String>) -> Result<Self, SafeError> { let value = value.into(); - if value.is_empty() - || value.len() > MAX_DURABLE_REQUEST_ID_BYTES - || !value.bytes().all(|byte| byte.is_ascii_graphic()) - { + let parsed = uuid::Uuid::parse_str(&value).map_err(|_| invalid_request_id())?; + if parsed.get_version_num() != 7 || parsed.hyphenated().to_string() != value { return Err(invalid_request_id()); } Ok(Self(value)) } #[must_use] + pub fn new_v7() -> Self { + Self(uuid::Uuid::now_v7().hyphenated().to_string()) + } + + #[must_use] pub fn as_str(&self) -> &str { &self.0 } @@ -720,7 +720,8 @@ mod tests { #[test] fn durable_request_ids_and_terminal_receipts_are_bounded_and_public() { - let request = DurableRequestId::parse("create:desktop:0001").expect("request id"); + let request = + DurableRequestId::parse("01890f3e-7b1c-7000-8000-000000000031").expect("request id"); let receipt = DurableOperationReceipt::new( request.clone(), PublicKey::from_bytes([7; 32]).expect("valid public key"), @@ -729,11 +730,35 @@ mod tests { ); assert_eq!(receipt.request_id(), &request); assert_eq!(receipt.resulting_revision(), Some(42)); - for invalid in ["", "contains space", &"x".repeat(129)] { + for invalid in [ + "", + "contains space", + "01890f3e-7b1c-4000-8000-000000000031", + "01890F3E-7B1C-7000-8000-000000000031", + "01890f3e-7b1c-6000-8000-000000000031", + ] { assert!(DurableRequestId::parse(invalid).is_err()); } } + #[test] + fn durable_request_id_source_is_stateless_canonical_and_unique() { + let first = DurableRequestId::new_v7(); + let second = DurableRequestId::new_v7(); + + assert_eq!(first.as_str().len(), 36); + assert_eq!(first.as_str().as_bytes()[14], b'7'); + assert!(matches!( + first.as_str().as_bytes()[19], + b'8' | b'9' | b'a' | b'b' + )); + assert_eq!( + DurableRequestId::parse(first.as_str()).expect("generated UUIDv7"), + first + ); + assert_ne!(first, second); + } + #[derive(Default)] struct FakePorts { selected: Mutex<Option<PublicKey>>, diff --git a/core/crates/harvestcircle_application/src/recovery.rs b/core/crates/harvestcircle_application/src/recovery.rs @@ -544,7 +544,7 @@ pub(crate) mod tests { prior_availability: Option<SignerAvailability>, ) -> DurableIdentityOperation { DurableIdentityOperation::new( - DurableRequestId::parse(format!("{kind:?}-{phase:?}")).expect("durable request ID"), + DurableRequestId::new_v7(), kind, identity, Some(1), diff --git a/core/crates/harvestcircle_ffi/src/commands.rs b/core/crates/harvestcircle_ffi/src/commands.rs @@ -787,7 +787,7 @@ mod tests { let core = in_memory_core().await; let initial = core.snapshot(); let context = RequestContextDto { - request_id: "ffi-test-import-1".to_owned(), + request_id: "01890f3e-7b1c-7000-8000-000000000041".to_owned(), expected_revision: initial.revision, deadline_millis: 5_000, }; @@ -803,7 +803,7 @@ mod tests { assert_eq!(first, replay); assert_eq!(first.snapshot.identities.len(), 1); - assert_eq!(first.request_id, "ffi-test-import-1"); + assert_eq!(first.request_id, "01890f3e-7b1c-7000-8000-000000000041"); } #[tokio::test] @@ -820,7 +820,7 @@ mod tests { assert!(nsec.starts_with("nsec1")); assert!(recovery.take_recovery_nsec().is_err()); let context = RequestContextDto { - request_id: "ffi-test-generate-1".to_owned(), + request_id: "01890f3e-7b1c-7000-8000-000000000042".to_owned(), expected_revision: initial.revision, deadline_millis: 5_000, }; @@ -847,7 +847,7 @@ mod tests { let imported = core .import_identity( RequestContextDto { - request_id: "ffi-lifecycle-import".to_owned(), + request_id: "01890f3e-7b1c-7000-8000-000000000043".to_owned(), expected_revision: initial.revision, deadline_millis: 5_000, }, @@ -885,7 +885,7 @@ mod tests { let removed = core .confirm_identity_removal( RequestContextDto { - request_id: "ffi-lifecycle-remove".to_owned(), + request_id: "01890f3e-7b1c-7000-8000-000000000044".to_owned(), expected_revision: signed_out.revision, deadline_millis: 5_000, }, @@ -897,7 +897,7 @@ mod tests { assert!( core.confirm_identity_removal( RequestContextDto { - request_id: "ffi-lifecycle-remove-repeated".to_owned(), + request_id: "01890f3e-7b1c-7000-8000-000000000045".to_owned(), expected_revision: removed.revision, deadline_millis: 5_000, }, @@ -941,12 +941,12 @@ mod tests { deadline_millis: 5_000, }, RequestContextDto { - request_id: "ffi-zero-deadline".to_owned(), + request_id: "01890f3e-7b1c-7000-8000-000000000046".to_owned(), expected_revision: 0, deadline_millis: 0, }, RequestContextDto { - request_id: "ffi-long-deadline".to_owned(), + request_id: "01890f3e-7b1c-7000-8000-000000000047".to_owned(), expected_revision: 0, deadline_millis: 30_001, }, @@ -956,7 +956,7 @@ mod tests { assert!( core.import_identity( RequestContextDto { - request_id: "ffi-invalid-secret".to_owned(), + request_id: "01890f3e-7b1c-7000-8000-000000000048".to_owned(), expected_revision: 0, deadline_millis: 5_000, }, diff --git a/core/crates/harvestcircle_ffi/src/lib.rs b/core/crates/harvestcircle_ffi/src/lib.rs @@ -33,6 +33,14 @@ pub fn native_runtime_version() -> String { PRODUCT_VERSION.to_owned() } +#[cfg_attr(not(coverage_nightly), uniffi::export)] +#[must_use] +pub fn generate_operation_id_v7() -> String { + harvestcircle_application::DurableRequestId::new_v7() + .as_str() + .to_owned() +} + #[cfg(test)] #[cfg_attr(coverage_nightly, coverage(off))] mod tests { @@ -52,5 +60,7 @@ mod tests { assert_eq!(super::DISTRIBUTION_PACKAGE_VERSION, "1.0.0"); assert_eq!(super::FFI_CONTRACT_HASH.len(), 64); assert!(!super::contract::NORMALIZED_CONTRACT_METADATA.is_empty()); + let operation_id = super::generate_operation_id_v7(); + assert!(harvestcircle_application::DurableRequestId::parse(operation_id).is_ok()); } } diff --git a/core/crates/harvestcircle_ffi/src/observer.rs b/core/crates/harvestcircle_ffi/src/observer.rs @@ -443,7 +443,7 @@ mod tests { let imported = core .import_identity( crate::RequestContextDto { - request_id: "observer-import".to_owned(), + request_id: "01890f3e-7b1c-7000-8000-000000000049".to_owned(), expected_revision: core.snapshot().revision, deadline_millis: 5_000, }, diff --git a/core/crates/harvestcircle_runtime/src/persistence.rs b/core/crates/harvestcircle_runtime/src/persistence.rs @@ -464,7 +464,8 @@ mod tests { let adapter = PersistentAppCore::in_memory(RelayConfiguration::default()).expect("adapter"); let secrets = InMemorySecretStore::default(); let snapshot = adapter.bootstrap(&secrets, &FixedClock).expect("bootstrap"); - let request = DurableRequestId::parse("import:adapter:1").expect("request"); + let request = + DurableRequestId::parse("01890f3e-7b1c-7000-8000-000000000021").expect("request"); let imported = adapter .import_secret_key_durable( &request, @@ -503,7 +504,8 @@ mod tests { .database() .save_selected_identity(Some(missing.public_key())) .expect("selection"); - let request = DurableRequestId::parse("repair:recovery:1").expect("request"); + let request = + DurableRequestId::parse("01890f3e-7b1c-7000-8000-000000000022").expect("request"); adapter .database() .begin_durable_operation( @@ -571,7 +573,8 @@ mod tests { .database() .insert_identity(&saved) .expect("identity"); - let import = DurableRequestId::parse("import:response-loss:1").expect("request"); + let import = + DurableRequestId::parse("01890f3e-7b1c-7000-8000-000000000023").expect("request"); adapter .database() .begin_durable_operation( @@ -629,7 +632,8 @@ mod tests { .database() .save_selected_identity(Some(saved.public_key())) .expect("remove selection"); - let removal = DurableRequestId::parse("remove:response-loss:1").expect("request"); + let removal = + DurableRequestId::parse("01890f3e-7b1c-7000-8000-000000000024").expect("request"); removal_adapter .database() .begin_durable_operation( diff --git a/core/crates/harvestcircle_runtime/src/runtime_actor.rs b/core/crates/harvestcircle_runtime/src/runtime_actor.rs @@ -713,7 +713,10 @@ impl RuntimeActorHandle { ) -> Result<ImportIdentityReceipt, SafeError> { let request_number = self.next_request.fetch_add(1, Ordering::Relaxed); self.import_secret_key( - DurableRequestId::parse(format!("test:import:{request_number}"))?, + DurableRequestId::parse(format!( + "01890f3e-7b1c-7000-8000-{:012x}", + request_number + 0x1000 + ))?, self.snapshot().revision(), input, DEFAULT_COMMAND_TIMEOUT, @@ -729,7 +732,10 @@ impl RuntimeActorHandle { let request_number = self.next_request.fetch_add(1, Ordering::Relaxed); self.acknowledge_generated_key_stage( id, - DurableRequestId::parse(format!("test:generate:{request_number}"))?, + DurableRequestId::parse(format!( + "01890f3e-7b1c-7000-8000-{:012x}", + request_number + 0x2000 + ))?, self.snapshot().revision(), DEFAULT_COMMAND_TIMEOUT, ) @@ -744,7 +750,10 @@ impl RuntimeActorHandle { let request_number = self.next_request.fetch_add(1, Ordering::Relaxed); self.confirm_identity_removal( token, - DurableRequestId::parse(format!("test:remove:{request_number}"))?, + DurableRequestId::parse(format!( + "01890f3e-7b1c-7000-8000-{:012x}", + request_number + 0x3000 + ))?, self.snapshot().revision(), DEFAULT_COMMAND_TIMEOUT, ) @@ -760,7 +769,10 @@ impl RuntimeActorHandle { let raw_request = self.next_request.fetch_add(1, Ordering::Relaxed); let request_id = RequestId::new(raw_request).ok_or_else(request_space_exhausted)?; let expected_revision = self.adapter.core().snapshot().revision(); - let durable_request = DurableRequestId::parse(format!("test:timeout:{raw_request}"))?; + let durable_request = DurableRequestId::parse(format!( + "01890f3e-7b1c-7000-8000-{:012x}", + raw_request + 0x4000 + ))?; match self .dispatch_with_deadline( RuntimeCommand::ImportSecretKey { @@ -1684,7 +1696,7 @@ mod tests { let generated = actor .generate_identity( - DurableRequestId::parse("test:generate:public-surface").expect("request"), + DurableRequestId::parse("01890f3e-7b1c-7000-8000-000000005001").expect("request"), actor.snapshot().revision(), DEFAULT_COMMAND_TIMEOUT, ) @@ -1720,7 +1732,7 @@ mod tests { let error = actor .acknowledge_generated_key_stage( handle.id(), - DurableRequestId::parse("test:generate:stale-revision").expect("request"), + DurableRequestId::parse("01890f3e-7b1c-7000-8000-000000005002").expect("request"), stale, DEFAULT_COMMAND_TIMEOUT, ) diff --git a/core/crates/harvestcircle_storage/src/journal.rs b/core/crates/harvestcircle_storage/src/journal.rs @@ -589,7 +589,8 @@ mod tests { #[test] fn durable_repository_replays_matching_requests_and_retains_terminal_receipts() { let database = Database::in_memory().expect("database"); - let request = DurableRequestId::parse("import:test:1").expect("request"); + let request = + DurableRequestId::parse("01890f3e-7b1c-7000-8000-000000000011").expect("request"); let identity = public_key(9); let prior = OperationPriorState::new( Some(public_key(8)), @@ -629,7 +630,8 @@ mod tests { ) .is_err() ); - let missing_request = DurableRequestId::parse("import:test:missing").expect("request"); + let missing_request = + DurableRequestId::parse("01890f3e-7b1c-7000-8000-000000000012").expect("request"); assert!( database .finalize_durable_operation( @@ -741,7 +743,8 @@ mod tests { ) .is_err() ); - let overflow_request = DurableRequestId::parse("import:test:overflow").expect("request"); + let overflow_request = + DurableRequestId::parse("01890f3e-7b1c-7000-8000-000000000013").expect("request"); database .begin_durable_operation( &overflow_request,