Adds an instrumented regression test for the SQLCipher cold-start crash fixed in 592a797 (bug #210): a cold process opening an already-encrypted cache with nothing to convert reached Room's keyed nativeOpen with the native .so unloaded and crash-looped with UnsatisfiedLinkError.
Every existing on-device test (DatabaseEncryptionTest, DatabaseProvisionerInstrumentedTest, the just-merged DatabaseModuleInstrumentedTest, AccountDataMigratorTest) runs a plaintext→encrypted conversion first, which calls System.loadLibrary("sqlcipher") in-process — so their keyed opens are never cold, masking the bug exactly as production did. This is the one state no other test constructs.
Why a separate process (the app/src/debug/ harness)
System.loadLibrary is process-global: once any test in a run loads sqlcipher, a later test in the same process can't observe the "not loaded" state. Minting the encrypted fixture itself loads the .so. So a faithful cold open cannot be observed in the instrumentation process — it must happen in a pristine process.
app/src/debug/ (debug source set → never in a release APK) contains:
ColdOpenCacheProbe.kt — a ContentProvider declared in the debug manifest with android:process=":coldopen", so its call() runs in a separate app process. It (1) proves that process is genuinely cold — a keyed open with no preceding load must fail at nativeOpen with UnsatisfiedLinkError — then (2) opens the pre-encrypted cache exactly the way production does on a steady-state encrypted start (DatabaseEncryption.ensureEncrypted no-op → ensureNativeLibraryLoaded → Room open through a DeferredOpenHelperFactory wrapping SupportOpenHelperFactory), and reports whether the seeded row reads back.
AndroidManifest.xml — declares that provider in the :coldopen process (exported=false, inert unless targeted).
ColdOpenEncryptedCacheTest mints the encrypted fixture in the instrumentation process ("a file created by a prior encrypted DB instance") and drives the cold open in :coldopen via ContentResolver.call, which spins that pristine process up on demand. The cold-probe check makes the isolation self-verifying: if the library were already loaded in the harness process, the test fails rather than passing a hollow assertion. No orchestrator / dedicated CI run / managed-device change is required — the fork is fresh regardless of what the shared connectedDebugAndroidTest process loaded.
Fidelity note
The harness mirrors DatabaseProvisioner's encrypted-branch decision + DatabaseModule's open lambda against the realDatabaseEncryption, DeferredOpenHelperFactory and SupportOpenHelperFactory, rather than invoking the DatabaseProvisioner/DatabaseModule objects directly. Those require MockK-substituted collaborators (DatabaseKeyStore/SettingsRepository/AccountDataMigrator are final and read the real Keystore/DataStore), and the test APK — hence MockK — is not on a forked app process's classloader. The object-level "load precedes keyed open" invariant is already pinned non-cold by DatabaseProvisionerInstrumentedTest and DatabaseModuleInstrumentedTest (#220); this test adds the missing cold-process coverage.
Test result
Local connectedDebugAndroidTest (API 36 emulator, manual cold boot), class-filtered: 1 test, 0 failures — cold probe returned UNSATISFIED_LINK (process genuinely cold) and the production-wiring open returned OK (seeded row read back).
Closes #221
## What
Adds an instrumented regression test for the SQLCipher cold-start crash fixed in `592a797` (bug #210): a **cold process opening an already-encrypted cache with nothing to convert** reached Room's keyed `nativeOpen` with the native `.so` unloaded and crash-looped with `UnsatisfiedLinkError`.
Every existing on-device test (`DatabaseEncryptionTest`, `DatabaseProvisionerInstrumentedTest`, the just-merged `DatabaseModuleInstrumentedTest`, `AccountDataMigratorTest`) runs a plaintext→encrypted conversion first, which calls `System.loadLibrary("sqlcipher")` in-process — so their keyed opens are never cold, masking the bug exactly as production did. This is the one state no other test constructs.
## Why a separate process (the `app/src/debug/` harness)
`System.loadLibrary` is **process-global**: once any test in a run loads `sqlcipher`, a later test in the same process can't observe the "not loaded" state. Minting the encrypted fixture itself loads the `.so`. So a faithful cold open **cannot** be observed in the instrumentation process — it must happen in a pristine process.
`app/src/debug/` (debug source set → **never in a release APK**) contains:
- **`ColdOpenCacheProbe.kt`** — a `ContentProvider` declared in the debug manifest with `android:process=":coldopen"`, so its `call()` runs in a **separate app process**. It (1) proves that process is genuinely cold — a keyed open with no preceding load must fail at `nativeOpen` with `UnsatisfiedLinkError` — then (2) opens the pre-encrypted cache exactly the way production does on a steady-state encrypted start (`DatabaseEncryption.ensureEncrypted` no-op → `ensureNativeLibraryLoaded` → Room open through a `DeferredOpenHelperFactory` wrapping `SupportOpenHelperFactory`), and reports whether the seeded row reads back.
- **`AndroidManifest.xml`** — declares that provider in the `:coldopen` process (`exported=false`, inert unless targeted).
`ColdOpenEncryptedCacheTest` mints the encrypted fixture in the instrumentation process (**"a file created by a prior encrypted DB instance"**) and drives the cold open in `:coldopen` via `ContentResolver.call`, which spins that pristine process up on demand. The **cold-probe check makes the isolation self-verifying**: if the library were already loaded in the harness process, the test fails rather than passing a hollow assertion. No orchestrator / dedicated CI run / managed-device change is required — the fork is fresh regardless of what the shared `connectedDebugAndroidTest` process loaded.
### Fidelity note
The harness mirrors `DatabaseProvisioner`'s encrypted-branch decision + `DatabaseModule`'s open lambda against the **real** `DatabaseEncryption`, `DeferredOpenHelperFactory` and `SupportOpenHelperFactory`, rather than invoking the `DatabaseProvisioner`/`DatabaseModule` objects directly. Those require MockK-substituted collaborators (`DatabaseKeyStore`/`SettingsRepository`/`AccountDataMigrator` are final and read the real Keystore/DataStore), and the test APK — hence MockK — is not on a forked app process's classloader. The object-level "load precedes keyed open" invariant is already pinned non-cold by `DatabaseProvisionerInstrumentedTest` and `DatabaseModuleInstrumentedTest` (#220); this test adds the missing **cold-process** coverage.
## Test result
Local `connectedDebugAndroidTest` (API 36 emulator, manual cold boot), class-filtered: **1 test, 0 failures** — cold probe returned `UNSATISFIED_LINK` (process genuinely cold) and the production-wiring open returned `OK` (seeded row read back).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #221
What
Adds an instrumented regression test for the SQLCipher cold-start crash fixed in
592a797(bug #210): a cold process opening an already-encrypted cache with nothing to convert reached Room's keyednativeOpenwith the native.sounloaded and crash-looped withUnsatisfiedLinkError.Every existing on-device test (
DatabaseEncryptionTest,DatabaseProvisionerInstrumentedTest, the just-mergedDatabaseModuleInstrumentedTest,AccountDataMigratorTest) runs a plaintext→encrypted conversion first, which callsSystem.loadLibrary("sqlcipher")in-process — so their keyed opens are never cold, masking the bug exactly as production did. This is the one state no other test constructs.Why a separate process (the
app/src/debug/harness)System.loadLibraryis process-global: once any test in a run loadssqlcipher, a later test in the same process can't observe the "not loaded" state. Minting the encrypted fixture itself loads the.so. So a faithful cold open cannot be observed in the instrumentation process — it must happen in a pristine process.app/src/debug/(debug source set → never in a release APK) contains:ColdOpenCacheProbe.kt— aContentProviderdeclared in the debug manifest withandroid:process=":coldopen", so itscall()runs in a separate app process. It (1) proves that process is genuinely cold — a keyed open with no preceding load must fail atnativeOpenwithUnsatisfiedLinkError— then (2) opens the pre-encrypted cache exactly the way production does on a steady-state encrypted start (DatabaseEncryption.ensureEncryptedno-op →ensureNativeLibraryLoaded→ Room open through aDeferredOpenHelperFactorywrappingSupportOpenHelperFactory), and reports whether the seeded row reads back.AndroidManifest.xml— declares that provider in the:coldopenprocess (exported=false, inert unless targeted).ColdOpenEncryptedCacheTestmints the encrypted fixture in the instrumentation process ("a file created by a prior encrypted DB instance") and drives the cold open in:coldopenviaContentResolver.call, which spins that pristine process up on demand. The cold-probe check makes the isolation self-verifying: if the library were already loaded in the harness process, the test fails rather than passing a hollow assertion. No orchestrator / dedicated CI run / managed-device change is required — the fork is fresh regardless of what the sharedconnectedDebugAndroidTestprocess loaded.Fidelity note
The harness mirrors
DatabaseProvisioner's encrypted-branch decision +DatabaseModule's open lambda against the realDatabaseEncryption,DeferredOpenHelperFactoryandSupportOpenHelperFactory, rather than invoking theDatabaseProvisioner/DatabaseModuleobjects directly. Those require MockK-substituted collaborators (DatabaseKeyStore/SettingsRepository/AccountDataMigratorare final and read the real Keystore/DataStore), and the test APK — hence MockK — is not on a forked app process's classloader. The object-level "load precedes keyed open" invariant is already pinned non-cold byDatabaseProvisionerInstrumentedTestandDatabaseModuleInstrumentedTest(#220); this test adds the missing cold-process coverage.Test result
Local
connectedDebugAndroidTest(API 36 emulator, manual cold boot), class-filtered: 1 test, 0 failures — cold probe returnedUNSATISFIED_LINK(process genuinely cold) and the production-wiring open returnedOK(seeded row read back).🤖 Generated with Claude Code