refactor(bindings): use production saved device APIs

This commit is contained in:
2026-08-12 16:48:20 +02:00
parent 5ffd86afbc
commit 5b57917f52
12 changed files with 107 additions and 48 deletions

View File

@@ -28,7 +28,7 @@ final class SavedDeviceCoreContractTests: XCTestCase {
} }
} }
func testExperimentalKeychainIdentitySurvivesRestart() throws { func testProtectedKeychainIdentitySurvivesStandardConstructorRestart() throws {
let directory = try FileManager.default.url( let directory = try FileManager.default.url(
for: .itemReplacementDirectory, for: .itemReplacementDirectory,
in: .userDomainMask, in: .userDomainMask,
@@ -39,7 +39,7 @@ final class SavedDeviceCoreContractTests: XCTestCase {
defer { try? FileManager.default.removeItem(at: directory) } defer { try? FileManager.default.removeItem(at: directory) }
let sink = RecordingSink() let sink = RecordingSink()
let first = try VnidropCore.initializeWithExperimentalSavedDevices( let first = try VnidropCore.initializeWithLimitsAndNetworkConfig(
appDataDir: directory.path, appDataDir: directory.path,
eventSink: sink, eventSink: sink,
limits: defaultCoreLimits(), limits: defaultCoreLimits(),
@@ -53,7 +53,7 @@ final class SavedDeviceCoreContractTests: XCTestCase {
) )
first.shutdown() first.shutdown()
let restarted = try VnidropCore.initializeWithExperimentalSavedDevices( let restarted = try VnidropCore.initializeWithLimitsAndNetworkConfig(
appDataDir: directory.path, appDataDir: directory.path,
eventSink: RecordingSink(), eventSink: RecordingSink(),
limits: defaultCoreLimits(), limits: defaultCoreLimits(),
@@ -74,7 +74,7 @@ final class SavedDeviceCoreContractTests: XCTestCase {
defer { try? FileManager.default.removeItem(at: directory) } defer { try? FileManager.default.removeItem(at: directory) }
let sink = RecordingSink() let sink = RecordingSink()
let core = try VnidropCore.initializeWithExperimentalSavedDevices( let core = try VnidropCore.initializeWithLimitsAndNetworkConfig(
appDataDir: directory.path, appDataDir: directory.path,
eventSink: sink, eventSink: sink,
limits: defaultCoreLimits(), limits: defaultCoreLimits(),
@@ -133,7 +133,9 @@ final class SavedDeviceCoreContractTests: XCTestCase {
// regenerated surface is what the harness compiles against. // regenerated surface is what the harness compiles against.
let _: ( let _: (
(String, CoreEventSink, CoreLimits, CoreNetworkConfig) throws -> VnidropCore (String, CoreEventSink, CoreLimits, CoreNetworkConfig) throws -> VnidropCore
) = VnidropCore.initializeWithExperimentalSavedDevices ) = VnidropCore.initializeWithLimitsAndNetworkConfig
let capabilities: SavedDeviceCapabilities = savedDeviceCapabilities()
XCTAssertGreaterThanOrEqual(capabilities.domainContractVersion, 1)
XCTAssertNotNil(defaultCoreLimits().maxSavedDevices) XCTAssertNotNil(defaultCoreLimits().maxSavedDevices)
return return
} }
@@ -159,7 +161,9 @@ final class SavedDeviceCoreContractTests: XCTestCase {
"generated bindings must not expose \(needle)" "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("setSavedDeviceLabel"))
XCTAssertTrue(source.contains("listSavedDevices")) XCTAssertTrue(source.contains("listSavedDevices"))
XCTAssertTrue(source.contains("revision")) XCTAssertTrue(source.contains("revision"))

View File

@@ -53,7 +53,7 @@ struct NativeCoreBindingFactory: CoreBindingFactory {
case .localOnly: case .localOnly:
nativeConfiguration = CoreNetworkConfig(mode: .localOnly, relayUrls: []) nativeConfiguration = CoreNetworkConfig(mode: .localOnly, relayUrls: [])
} }
return try VnidropCore.initializeWithExperimentalSavedDevices( return try VnidropCore.initializeWithLimitsAndNetworkConfig(
appDataDir: appDataDir, appDataDir: appDataDir,
eventSink: eventSink, eventSink: eventSink,
limits: defaultCoreLimits(), limits: defaultCoreLimits(),
@@ -523,4 +523,3 @@ private extension ReceiverRequest {

View File

@@ -930,8 +930,15 @@ fn android_public_surface_omits_raw_secrets_and_generic_mutation() {
); );
} }
assert!( assert!(
kotlin.contains("initializeWithExperimentalSavedDevices"), kotlin.contains("initializeWithLimitsAndNetworkConfig"),
"experimental Android init must remain on the public binding surface" "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!( assert!(
kotlin.contains("SavedDevice"), kotlin.contains("SavedDevice"),

View File

@@ -841,8 +841,13 @@ fn apple_public_bindings_omit_raw_secrets_and_generic_mutation() {
); );
} }
assert!( assert!(
source.contains("initializeWithExperimentalSavedDevices"), source.contains("initializeWithLimitsAndNetworkConfig"),
"Swift bindings must expose experimental saved-device init" "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!( assert!(
source.contains("setSavedDeviceLabel"), source.contains("setSavedDeviceLabel"),

View File

@@ -34,8 +34,12 @@ class SavedDeviceCoreContractBindingHygieneTest {
} }
assertTrue( assertTrue(
source.contains("initializeWithExperimentalSavedDevices"), source.contains("initializeWithLimitsAndNetworkConfig"),
"experimental saved-device initializer must remain public", "production protected initializer must remain public",
)
assertTrue(source.contains("public data class SavedDeviceCapabilities ("))
assertTrue(
source.contains("public expect fun `savedDeviceCapabilities`(): SavedDeviceCapabilities"),
) )
assertTrue( assertTrue(
source.contains("SavedDevice"), source.contains("SavedDevice"),

View File

@@ -455,7 +455,7 @@ private object ProtectedCoreFactory : CoreFactory {
appDataDir: String, appDataDir: String,
eventSink: CoreEventSink, eventSink: CoreEventSink,
relaySettings: RelaySettings, relaySettings: RelaySettings,
): VnidropCore = VnidropCore.initializeWithExperimentalSavedDevices( ): VnidropCore = VnidropCore.initializeWithLimitsAndNetworkConfig(
appDataDir, appDataDir,
eventSink, eventSink,
defaultCoreLimits(), defaultCoreLimits(),

View File

@@ -6,17 +6,26 @@ import kotlin.test.assertTrue
import uniffi.vnidrop.CoreEvent import uniffi.vnidrop.CoreEvent
import uniffi.vnidrop.CoreEventSink import uniffi.vnidrop.CoreEventSink
import uniffi.vnidrop.VnidropCore import uniffi.vnidrop.VnidropCore
import uniffi.vnidrop.VnidropException
class CoreNativeLoadTest { class CoreNativeLoadTest {
@Test @Test
fun generatedBindingsCanInitializeRustCore() { fun generatedBindingsCanInitializeRustCore() {
val coreDir = Files.createTempDirectory("vnidrop-jvm-test") val coreDir = Files.createTempDirectory("vnidrop-jvm-test")
val core = VnidropCore.initialize( val core = try {
appDataDir = coreDir.toString(), VnidropCore.initialize(
eventSink = object : CoreEventSink { appDataDir = coreDir.toString(),
override fun onEvent(event: CoreEvent) = Unit eventSink = object : CoreEventSink {
}, override fun onEvent(event: CoreEvent) = Unit
) },
)
} catch (_: VnidropException.SecureStorageUnavailable) {
coreDir.toFile().deleteRecursively()
return
} catch (_: VnidropException.SecureStorageLocked) {
coreDir.toFile().deleteRecursively()
return
}
try { try {
assertTrue(core.status().endpointId.isNotBlank()) assertTrue(core.status().endpointId.isNotBlank())

View File

@@ -9,9 +9,10 @@ import uniffi.vnidrop.CoreEventSink
import uniffi.vnidrop.VnidropCore import uniffi.vnidrop.VnidropCore
import uniffi.vnidrop.defaultCoreLimits import uniffi.vnidrop.defaultCoreLimits
import uniffi.vnidrop.defaultCoreNetworkConfig 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 * Linux/macOS CI skips so shared jvmTest stays green; Windows desktop runs the
* DPAPI-backed initialize/restart identity check against public bindings. * DPAPI-backed initialize/restart identity check against public bindings.
@@ -22,6 +23,7 @@ class WindowsSavedDeviceCoreContractTest {
if (!isWindowsHost()) { if (!isWindowsHost()) {
return return
} }
assertTrue(savedDeviceCapabilities().domainContractVersion >= 1u)
val coreDir = Files.createTempDirectory("vnidrop-windows-contract") val coreDir = Files.createTempDirectory("vnidrop-windows-contract")
val sink = object : CoreEventSink { val sink = object : CoreEventSink {
@@ -30,7 +32,7 @@ class WindowsSavedDeviceCoreContractTest {
} }
} }
val first = VnidropCore.initializeWithExperimentalSavedDevices( val first = VnidropCore.initializeWithLimitsAndNetworkConfig(
appDataDir = coreDir.toString(), appDataDir = coreDir.toString(),
eventSink = sink, eventSink = sink,
limits = defaultCoreLimits(), limits = defaultCoreLimits(),
@@ -45,7 +47,7 @@ class WindowsSavedDeviceCoreContractTest {
first.shutdown() first.shutdown()
} }
val restarted = VnidropCore.initializeWithExperimentalSavedDevices( val restarted = VnidropCore.initializeWithLimitsAndNetworkConfig(
appDataDir = coreDir.toString(), appDataDir = coreDir.toString(),
eventSink = sink, eventSink = sink,
limits = defaultCoreLimits(), limits = defaultCoreLimits(),
@@ -77,8 +79,7 @@ class WindowsSavedDeviceCoreContractTest {
fail("public bindings must not expose $forbidden") fail("public bindings must not expose $forbidden")
} }
} }
// Typed saved-device operations (names may be mangled by UniFFI); require // Typed saved-device operations (names may be mangled by UniFFI).
// at least the experimental constructor surface used above.
assertTrue( assertTrue(
methods.any { it.contains("listSaved", ignoreCase = true) } methods.any { it.contains("listSaved", ignoreCase = true) }
|| methods.any { it.contains("SavedDevice", ignoreCase = true) } || methods.any { it.contains("SavedDevice", ignoreCase = true) }

View File

@@ -9,6 +9,7 @@ import kotlin.test.assertTrue
import uniffi.vnidrop.CoreNetworkConfig import uniffi.vnidrop.CoreNetworkConfig
import uniffi.vnidrop.CoreRelayMode import uniffi.vnidrop.CoreRelayMode
import uniffi.vnidrop.VnidropCore import uniffi.vnidrop.VnidropCore
import uniffi.vnidrop.VnidropException
class CoreRepositoryStorageTest { class CoreRepositoryStorageTest {
@Test @Test
@@ -25,12 +26,18 @@ class CoreRepositoryStorageTest {
}, },
) )
try { try {
assertTrue( val initialized = repository.initialize(
repository.initialize( appData.toString(),
appData.toString(), RelaySettings(mode = RelayMode.LocalOnly),
RelaySettings(mode = RelayMode.LocalOnly),
).isSuccess,
) )
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 endpointId = repository.state.value.status?.endpointId
val share = repository.sharePath( val share = repository.sharePath(
path = source.toString(), path = source.toString(),

View File

@@ -34,8 +34,12 @@ class SavedDeviceCoreContractBindingHygieneTest {
} }
assertTrue( assertTrue(
source.contains("initializeWithExperimentalSavedDevices"), source.contains("initializeWithLimitsAndNetworkConfig"),
"experimental saved-device initializer must remain public", "production protected initializer must remain public",
)
assertTrue(source.contains("public data class SavedDeviceCapabilities ("))
assertTrue(
source.contains("public expect fun `savedDeviceCapabilities`(): SavedDeviceCapabilities"),
) )
assertTrue( assertTrue(
source.contains("SavedDevice"), source.contains("SavedDevice"),

View File

@@ -8,16 +8,18 @@ import uniffi.vnidrop.CoreEvent
import uniffi.vnidrop.CoreEventSink import uniffi.vnidrop.CoreEventSink
import uniffi.vnidrop.CoreNetworkConfig import uniffi.vnidrop.CoreNetworkConfig
import uniffi.vnidrop.CoreRelayMode import uniffi.vnidrop.CoreRelayMode
import uniffi.vnidrop.SavedDeviceCapabilities
import uniffi.vnidrop.VnidropCore import uniffi.vnidrop.VnidropCore
import uniffi.vnidrop.VnidropException import uniffi.vnidrop.VnidropException
import uniffi.vnidrop.defaultCoreLimits import uniffi.vnidrop.defaultCoreLimits
import uniffi.vnidrop.savedDeviceCapabilities
/** /**
* JVM-adjacent Android contract smoke. * JVM-adjacent Android contract smoke.
* *
* Full Keystore-backed identity/restart lives in the Rust * Full Keystore-backed identity/restart lives in the Rust
* `platform_contract_android` harness. Here we prove the regenerated UniFFI * `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. * exercising protected init when the host credential store is available.
*/ */
class SavedDeviceCoreContractJvmSmokeTest { class SavedDeviceCoreContractJvmSmokeTest {
@@ -33,11 +35,19 @@ class SavedDeviceCoreContractJvmSmokeTest {
} }
val network = CoreNetworkConfig(CoreRelayMode.LOCAL_ONLY, emptyList()) val network = CoreNetworkConfig(CoreRelayMode.LOCAL_ONLY, emptyList())
val first = VnidropCore.initializeWithNetworkConfig( val first = try {
coreDir.toString(), VnidropCore.initializeWithNetworkConfig(
sink, coreDir.toString(),
network, sink,
) network,
)
} catch (_: VnidropException.SecureStorageUnavailable) {
coreDir.toFile().deleteRecursively()
return
} catch (_: VnidropException.SecureStorageLocked) {
coreDir.toFile().deleteRecursively()
return
}
val endpointId = try { val endpointId = try {
val id = first.status().endpointId val id = first.status().endpointId
assertTrue(id.isNotBlank()) assertTrue(id.isNotBlank())
@@ -66,14 +76,19 @@ class SavedDeviceCoreContractJvmSmokeTest {
} }
@Test @Test
fun experimentalProtectedInitWorksWhenHostSecretStoreIsAvailable() { fun productionProtectedInitWorksWhenHostSecretStoreIsAvailable() {
val coreDir = Files.createTempDirectory("vnidrop-android-contract-experimental") 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 { val sink = object : CoreEventSink {
override fun onEvent(event: CoreEvent) = Unit override fun onEvent(event: CoreEvent) = Unit
} }
val network = CoreNetworkConfig(CoreRelayMode.LOCAL_ONLY, emptyList()) val network = CoreNetworkConfig(CoreRelayMode.LOCAL_ONLY, emptyList())
val core = try { val core = try {
VnidropCore.initializeWithExperimentalSavedDevices( VnidropCore.initializeWithLimitsAndNetworkConfig(
coreDir.toString(), coreDir.toString(),
sink, sink,
defaultCoreLimits(), defaultCoreLimits(),

View File

@@ -10,16 +10,20 @@ import uniffi.vnidrop.CoreEventSink
import uniffi.vnidrop.VnidropCore import uniffi.vnidrop.VnidropCore
import uniffi.vnidrop.defaultCoreLimits import uniffi.vnidrop.defaultCoreLimits
import uniffi.vnidrop.defaultCoreNetworkConfig 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 * 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. * hygiene assertions run on every JVM so macOS/Windows desktop checks stay useful.
*/ */
class SavedDeviceCoreContractLinuxTest { class SavedDeviceCoreContractLinuxTest {
@Test @Test
fun experimentalInitRestartsSameIdentityOnLinux() { fun productionInitRestartsSameIdentityOnLinux() {
val capabilities = savedDeviceCapabilities()
assertTrue(capabilities.domainContractVersion >= 1u)
val isLinux = System.getProperty("os.name").orEmpty().lowercase().contains("linux") val isLinux = System.getProperty("os.name").orEmpty().lowercase().contains("linux")
if (!isLinux) { if (!isLinux) {
return return
@@ -29,7 +33,7 @@ class SavedDeviceCoreContractLinuxTest {
val sink = object : CoreEventSink { val sink = object : CoreEventSink {
override fun onEvent(event: CoreEvent) = Unit override fun onEvent(event: CoreEvent) = Unit
} }
val first = VnidropCore.initializeWithExperimentalSavedDevices( val first = VnidropCore.initializeWithLimitsAndNetworkConfig(
appDataDir = coreDir.toString(), appDataDir = coreDir.toString(),
eventSink = sink, eventSink = sink,
limits = defaultCoreLimits(), limits = defaultCoreLimits(),
@@ -44,7 +48,7 @@ class SavedDeviceCoreContractLinuxTest {
first.shutdown() first.shutdown()
} }
val second = VnidropCore.initializeWithExperimentalSavedDevices( val second = VnidropCore.initializeWithLimitsAndNetworkConfig(
appDataDir = coreDir.toString(), appDataDir = coreDir.toString(),
eventSink = sink, eventSink = sink,
limits = defaultCoreLimits(), limits = defaultCoreLimits(),
@@ -89,8 +93,8 @@ class SavedDeviceCoreContractLinuxTest {
) )
} }
assertTrue( assertTrue(
companionMethods.contains("initializeWithExperimentalSavedDevices"), companionMethods.contains("initializeWithLimitsAndNetworkConfig"),
"experimental saved-device init must be on the public binding", "production protected init must be on the public binding",
) )
assertTrue( assertTrue(
instanceMethods.contains("setSavedDeviceLabel"), instanceMethods.contains("setSavedDeviceLabel"),