diff --git a/apple/Tests/SavedDeviceCoreContractTests.swift b/apple/Tests/SavedDeviceCoreContractTests.swift index 0819891..5511b31 100644 --- a/apple/Tests/SavedDeviceCoreContractTests.swift +++ b/apple/Tests/SavedDeviceCoreContractTests.swift @@ -28,7 +28,7 @@ final class SavedDeviceCoreContractTests: XCTestCase { } } - func testExperimentalKeychainIdentitySurvivesRestart() throws { + func testProtectedKeychainIdentitySurvivesStandardConstructorRestart() throws { let directory = try FileManager.default.url( for: .itemReplacementDirectory, in: .userDomainMask, @@ -39,7 +39,7 @@ final class SavedDeviceCoreContractTests: XCTestCase { defer { try? FileManager.default.removeItem(at: directory) } let sink = RecordingSink() - let first = try VnidropCore.initializeWithExperimentalSavedDevices( + let first = try VnidropCore.initializeWithLimitsAndNetworkConfig( appDataDir: directory.path, eventSink: sink, limits: defaultCoreLimits(), @@ -53,7 +53,7 @@ final class SavedDeviceCoreContractTests: XCTestCase { ) first.shutdown() - let restarted = try VnidropCore.initializeWithExperimentalSavedDevices( + let restarted = try VnidropCore.initializeWithLimitsAndNetworkConfig( appDataDir: directory.path, eventSink: RecordingSink(), limits: defaultCoreLimits(), @@ -74,7 +74,7 @@ final class SavedDeviceCoreContractTests: XCTestCase { defer { try? FileManager.default.removeItem(at: directory) } let sink = RecordingSink() - let core = try VnidropCore.initializeWithExperimentalSavedDevices( + let core = try VnidropCore.initializeWithLimitsAndNetworkConfig( appDataDir: directory.path, eventSink: sink, limits: defaultCoreLimits(), @@ -133,7 +133,9 @@ final class SavedDeviceCoreContractTests: XCTestCase { // regenerated surface is what the harness compiles against. let _: ( (String, CoreEventSink, CoreLimits, CoreNetworkConfig) throws -> VnidropCore - ) = VnidropCore.initializeWithExperimentalSavedDevices + ) = VnidropCore.initializeWithLimitsAndNetworkConfig + let capabilities: SavedDeviceCapabilities = savedDeviceCapabilities() + XCTAssertGreaterThanOrEqual(capabilities.domainContractVersion, 1) XCTAssertNotNil(defaultCoreLimits().maxSavedDevices) return } @@ -159,7 +161,9 @@ final class SavedDeviceCoreContractTests: XCTestCase { "generated bindings must not expose \(needle)" ) } - XCTAssertTrue(source.contains("initializeWithExperimentalSavedDevices")) + XCTAssertTrue(source.contains("initializeWithLimitsAndNetworkConfig")) + XCTAssertTrue(source.contains("public struct SavedDeviceCapabilities")) + XCTAssertTrue(source.contains("public func savedDeviceCapabilities()")) XCTAssertTrue(source.contains("setSavedDeviceLabel")) XCTAssertTrue(source.contains("listSavedDevices")) XCTAssertTrue(source.contains("revision")) diff --git a/apple/VniDrop/Core/CoreRepository.swift b/apple/VniDrop/Core/CoreRepository.swift index 2093b45..27678ae 100644 --- a/apple/VniDrop/Core/CoreRepository.swift +++ b/apple/VniDrop/Core/CoreRepository.swift @@ -53,7 +53,7 @@ struct NativeCoreBindingFactory: CoreBindingFactory { case .localOnly: nativeConfiguration = CoreNetworkConfig(mode: .localOnly, relayUrls: []) } - return try VnidropCore.initializeWithExperimentalSavedDevices( + return try VnidropCore.initializeWithLimitsAndNetworkConfig( appDataDir: appDataDir, eventSink: eventSink, limits: defaultCoreLimits(), @@ -523,4 +523,3 @@ private extension ReceiverRequest { - diff --git a/crates/vnidrop/src/tests/platform_contract_android.rs b/crates/vnidrop/src/tests/platform_contract_android.rs index 271e57f..25e2afd 100644 --- a/crates/vnidrop/src/tests/platform_contract_android.rs +++ b/crates/vnidrop/src/tests/platform_contract_android.rs @@ -930,8 +930,15 @@ fn android_public_surface_omits_raw_secrets_and_generic_mutation() { ); } assert!( - kotlin.contains("initializeWithExperimentalSavedDevices"), - "experimental Android init must remain on the public binding surface" + kotlin.contains("initializeWithLimitsAndNetworkConfig"), + "production protected Android init must remain on the public binding surface" + ); + assert!( + kotlin.contains("public data class SavedDeviceCapabilities (") + && kotlin.contains( + "public expect fun `savedDeviceCapabilities`(): SavedDeviceCapabilities", + ), + "production saved-device capabilities must remain on the public binding surface" ); assert!( kotlin.contains("SavedDevice"), diff --git a/crates/vnidrop/src/tests/platform_contract_apple.rs b/crates/vnidrop/src/tests/platform_contract_apple.rs index 3f0f32c..c9442a8 100644 --- a/crates/vnidrop/src/tests/platform_contract_apple.rs +++ b/crates/vnidrop/src/tests/platform_contract_apple.rs @@ -841,8 +841,13 @@ fn apple_public_bindings_omit_raw_secrets_and_generic_mutation() { ); } assert!( - source.contains("initializeWithExperimentalSavedDevices"), - "Swift bindings must expose experimental saved-device init" + source.contains("initializeWithLimitsAndNetworkConfig"), + "Swift bindings must expose standard protected initialization" + ); + assert!( + source.contains("public struct SavedDeviceCapabilities") + && source.contains("public func savedDeviceCapabilities()"), + "Swift bindings must expose production saved-device capabilities" ); assert!( source.contains("setSavedDeviceLabel"), diff --git a/shared/src/androidHostTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractBindingHygieneTest.kt b/shared/src/androidHostTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractBindingHygieneTest.kt index 459a4dc..68ca47e 100644 --- a/shared/src/androidHostTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractBindingHygieneTest.kt +++ b/shared/src/androidHostTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractBindingHygieneTest.kt @@ -34,8 +34,12 @@ class SavedDeviceCoreContractBindingHygieneTest { } assertTrue( - source.contains("initializeWithExperimentalSavedDevices"), - "experimental saved-device initializer must remain public", + source.contains("initializeWithLimitsAndNetworkConfig"), + "production protected initializer must remain public", + ) + assertTrue(source.contains("public data class SavedDeviceCapabilities (")) + assertTrue( + source.contains("public expect fun `savedDeviceCapabilities`(): SavedDeviceCapabilities"), ) assertTrue( source.contains("SavedDevice"), diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/core/CoreRepository.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/core/CoreRepository.kt index d76172b..7f50cda 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/core/CoreRepository.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/core/CoreRepository.kt @@ -455,7 +455,7 @@ private object ProtectedCoreFactory : CoreFactory { appDataDir: String, eventSink: CoreEventSink, relaySettings: RelaySettings, - ): VnidropCore = VnidropCore.initializeWithExperimentalSavedDevices( + ): VnidropCore = VnidropCore.initializeWithLimitsAndNetworkConfig( appDataDir, eventSink, defaultCoreLimits(), diff --git a/shared/src/jvmTest/kotlin/com/vnidrop/app/CoreNativeLoadTest.kt b/shared/src/jvmTest/kotlin/com/vnidrop/app/CoreNativeLoadTest.kt index 1ce31d6..1df3f93 100644 --- a/shared/src/jvmTest/kotlin/com/vnidrop/app/CoreNativeLoadTest.kt +++ b/shared/src/jvmTest/kotlin/com/vnidrop/app/CoreNativeLoadTest.kt @@ -6,17 +6,26 @@ import kotlin.test.assertTrue import uniffi.vnidrop.CoreEvent import uniffi.vnidrop.CoreEventSink import uniffi.vnidrop.VnidropCore +import uniffi.vnidrop.VnidropException class CoreNativeLoadTest { @Test fun generatedBindingsCanInitializeRustCore() { val coreDir = Files.createTempDirectory("vnidrop-jvm-test") - val core = VnidropCore.initialize( - appDataDir = coreDir.toString(), - eventSink = object : CoreEventSink { - override fun onEvent(event: CoreEvent) = Unit - }, - ) + val core = try { + VnidropCore.initialize( + appDataDir = coreDir.toString(), + eventSink = object : CoreEventSink { + override fun onEvent(event: CoreEvent) = Unit + }, + ) + } catch (_: VnidropException.SecureStorageUnavailable) { + coreDir.toFile().deleteRecursively() + return + } catch (_: VnidropException.SecureStorageLocked) { + coreDir.toFile().deleteRecursively() + return + } try { assertTrue(core.status().endpointId.isNotBlank()) diff --git a/shared/src/jvmTest/kotlin/com/vnidrop/app/WindowsSavedDeviceCoreContractTest.kt b/shared/src/jvmTest/kotlin/com/vnidrop/app/WindowsSavedDeviceCoreContractTest.kt index 4201684..fc531a9 100644 --- a/shared/src/jvmTest/kotlin/com/vnidrop/app/WindowsSavedDeviceCoreContractTest.kt +++ b/shared/src/jvmTest/kotlin/com/vnidrop/app/WindowsSavedDeviceCoreContractTest.kt @@ -9,9 +9,10 @@ import uniffi.vnidrop.CoreEventSink import uniffi.vnidrop.VnidropCore import uniffi.vnidrop.defaultCoreLimits import uniffi.vnidrop.defaultCoreNetworkConfig +import uniffi.vnidrop.savedDeviceCapabilities /** - * Optional Windows host harness for the experimental saved-device core. + * Optional Windows host harness for the saved-device core. * * Linux/macOS CI skips so shared jvmTest stays green; Windows desktop runs the * DPAPI-backed initialize/restart identity check against public bindings. @@ -22,6 +23,7 @@ class WindowsSavedDeviceCoreContractTest { if (!isWindowsHost()) { return } + assertTrue(savedDeviceCapabilities().domainContractVersion >= 1u) val coreDir = Files.createTempDirectory("vnidrop-windows-contract") val sink = object : CoreEventSink { @@ -30,7 +32,7 @@ class WindowsSavedDeviceCoreContractTest { } } - val first = VnidropCore.initializeWithExperimentalSavedDevices( + val first = VnidropCore.initializeWithLimitsAndNetworkConfig( appDataDir = coreDir.toString(), eventSink = sink, limits = defaultCoreLimits(), @@ -45,7 +47,7 @@ class WindowsSavedDeviceCoreContractTest { first.shutdown() } - val restarted = VnidropCore.initializeWithExperimentalSavedDevices( + val restarted = VnidropCore.initializeWithLimitsAndNetworkConfig( appDataDir = coreDir.toString(), eventSink = sink, limits = defaultCoreLimits(), @@ -77,8 +79,7 @@ class WindowsSavedDeviceCoreContractTest { fail("public bindings must not expose $forbidden") } } - // Typed saved-device operations (names may be mangled by UniFFI); require - // at least the experimental constructor surface used above. + // Typed saved-device operations (names may be mangled by UniFFI). assertTrue( methods.any { it.contains("listSaved", ignoreCase = true) } || methods.any { it.contains("SavedDevice", ignoreCase = true) } diff --git a/shared/src/jvmTest/kotlin/com/vnidrop/app/core/CoreRepositoryStorageTest.kt b/shared/src/jvmTest/kotlin/com/vnidrop/app/core/CoreRepositoryStorageTest.kt index 608c439..1e59f8a 100644 --- a/shared/src/jvmTest/kotlin/com/vnidrop/app/core/CoreRepositoryStorageTest.kt +++ b/shared/src/jvmTest/kotlin/com/vnidrop/app/core/CoreRepositoryStorageTest.kt @@ -9,6 +9,7 @@ import kotlin.test.assertTrue import uniffi.vnidrop.CoreNetworkConfig import uniffi.vnidrop.CoreRelayMode import uniffi.vnidrop.VnidropCore +import uniffi.vnidrop.VnidropException class CoreRepositoryStorageTest { @Test @@ -25,12 +26,18 @@ class CoreRepositoryStorageTest { }, ) try { - assertTrue( - repository.initialize( - appData.toString(), - RelaySettings(mode = RelayMode.LocalOnly), - ).isSuccess, + val initialized = repository.initialize( + appData.toString(), + RelaySettings(mode = RelayMode.LocalOnly), ) + val initializationError = initialized.exceptionOrNull() + if ( + initializationError is VnidropException.SecureStorageUnavailable || + initializationError is VnidropException.SecureStorageLocked + ) { + return@runTest + } + initialized.getOrThrow() val endpointId = repository.state.value.status?.endpointId val share = repository.sharePath( path = source.toString(), diff --git a/shared/src/jvmTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractBindingHygieneTest.kt b/shared/src/jvmTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractBindingHygieneTest.kt index 459a4dc..68ca47e 100644 --- a/shared/src/jvmTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractBindingHygieneTest.kt +++ b/shared/src/jvmTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractBindingHygieneTest.kt @@ -34,8 +34,12 @@ class SavedDeviceCoreContractBindingHygieneTest { } assertTrue( - source.contains("initializeWithExperimentalSavedDevices"), - "experimental saved-device initializer must remain public", + source.contains("initializeWithLimitsAndNetworkConfig"), + "production protected initializer must remain public", + ) + assertTrue(source.contains("public data class SavedDeviceCapabilities (")) + assertTrue( + source.contains("public expect fun `savedDeviceCapabilities`(): SavedDeviceCapabilities"), ) assertTrue( source.contains("SavedDevice"), diff --git a/shared/src/jvmTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractJvmSmokeTest.kt b/shared/src/jvmTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractJvmSmokeTest.kt index caf73f6..3d445f2 100644 --- a/shared/src/jvmTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractJvmSmokeTest.kt +++ b/shared/src/jvmTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractJvmSmokeTest.kt @@ -8,16 +8,18 @@ import uniffi.vnidrop.CoreEvent import uniffi.vnidrop.CoreEventSink import uniffi.vnidrop.CoreNetworkConfig import uniffi.vnidrop.CoreRelayMode +import uniffi.vnidrop.SavedDeviceCapabilities import uniffi.vnidrop.VnidropCore import uniffi.vnidrop.VnidropException import uniffi.vnidrop.defaultCoreLimits +import uniffi.vnidrop.savedDeviceCapabilities /** * JVM-adjacent Android contract smoke. * * Full Keystore-backed identity/restart lives in the Rust * `platform_contract_android` harness. Here we prove the regenerated UniFFI - * surface exposes revision-bearing events and the experimental initializer, + * surface exposes revision-bearing events and the production initializer, * exercising protected init when the host credential store is available. */ class SavedDeviceCoreContractJvmSmokeTest { @@ -33,11 +35,19 @@ class SavedDeviceCoreContractJvmSmokeTest { } val network = CoreNetworkConfig(CoreRelayMode.LOCAL_ONLY, emptyList()) - val first = VnidropCore.initializeWithNetworkConfig( - coreDir.toString(), - sink, - network, - ) + val first = try { + VnidropCore.initializeWithNetworkConfig( + coreDir.toString(), + sink, + network, + ) + } catch (_: VnidropException.SecureStorageUnavailable) { + coreDir.toFile().deleteRecursively() + return + } catch (_: VnidropException.SecureStorageLocked) { + coreDir.toFile().deleteRecursively() + return + } val endpointId = try { val id = first.status().endpointId assertTrue(id.isNotBlank()) @@ -66,14 +76,19 @@ class SavedDeviceCoreContractJvmSmokeTest { } @Test - fun experimentalProtectedInitWorksWhenHostSecretStoreIsAvailable() { - val coreDir = Files.createTempDirectory("vnidrop-android-contract-experimental") + fun productionProtectedInitWorksWhenHostSecretStoreIsAvailable() { + val capabilities: SavedDeviceCapabilities = savedDeviceCapabilities() + assertTrue(capabilities.domainContractVersion >= 1u) + assertTrue(capabilities.relationshipProtocolVersion >= 1u) + assertTrue(capabilities.targetedTransferProtocolVersion >= 1u) + + val coreDir = Files.createTempDirectory("vnidrop-android-contract-production") val sink = object : CoreEventSink { override fun onEvent(event: CoreEvent) = Unit } val network = CoreNetworkConfig(CoreRelayMode.LOCAL_ONLY, emptyList()) val core = try { - VnidropCore.initializeWithExperimentalSavedDevices( + VnidropCore.initializeWithLimitsAndNetworkConfig( coreDir.toString(), sink, defaultCoreLimits(), diff --git a/shared/src/jvmTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractLinuxTest.kt b/shared/src/jvmTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractLinuxTest.kt index 71f817f..9b3d62a 100644 --- a/shared/src/jvmTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractLinuxTest.kt +++ b/shared/src/jvmTest/kotlin/com/vnidrop/app/core/SavedDeviceCoreContractLinuxTest.kt @@ -10,16 +10,20 @@ import uniffi.vnidrop.CoreEventSink import uniffi.vnidrop.VnidropCore import uniffi.vnidrop.defaultCoreLimits import uniffi.vnidrop.defaultCoreNetworkConfig +import uniffi.vnidrop.savedDeviceCapabilities /** - * Linux host harness for the experimental saved-device core contract (ticket 17). + * Linux host harness for the saved-device core contract. * * Identity restart against Secret Service runs only on Linux (CI host). Binding * hygiene assertions run on every JVM so macOS/Windows desktop checks stay useful. */ class SavedDeviceCoreContractLinuxTest { @Test - fun experimentalInitRestartsSameIdentityOnLinux() { + fun productionInitRestartsSameIdentityOnLinux() { + val capabilities = savedDeviceCapabilities() + assertTrue(capabilities.domainContractVersion >= 1u) + val isLinux = System.getProperty("os.name").orEmpty().lowercase().contains("linux") if (!isLinux) { return @@ -29,7 +33,7 @@ class SavedDeviceCoreContractLinuxTest { val sink = object : CoreEventSink { override fun onEvent(event: CoreEvent) = Unit } - val first = VnidropCore.initializeWithExperimentalSavedDevices( + val first = VnidropCore.initializeWithLimitsAndNetworkConfig( appDataDir = coreDir.toString(), eventSink = sink, limits = defaultCoreLimits(), @@ -44,7 +48,7 @@ class SavedDeviceCoreContractLinuxTest { first.shutdown() } - val second = VnidropCore.initializeWithExperimentalSavedDevices( + val second = VnidropCore.initializeWithLimitsAndNetworkConfig( appDataDir = coreDir.toString(), eventSink = sink, limits = defaultCoreLimits(), @@ -89,8 +93,8 @@ class SavedDeviceCoreContractLinuxTest { ) } assertTrue( - companionMethods.contains("initializeWithExperimentalSavedDevices"), - "experimental saved-device init must be on the public binding", + companionMethods.contains("initializeWithLimitsAndNetworkConfig"), + "production protected init must be on the public binding", ) assertTrue( instanceMethods.contains("setSavedDeviceLabel"),