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(
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"))

View File

@@ -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 {

View File

@@ -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"),

View File

@@ -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"),

View File

@@ -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"),

View File

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

View File

@@ -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())

View File

@@ -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) }

View File

@@ -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(),

View File

@@ -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"),

View File

@@ -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(),

View File

@@ -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"),