Dear Alex Chen, your reservation for Android Dev Summit '26 is confirmed.
+| Confirmation Number: | RG-90421-MV |
| Room Type: | Room 412 — Deluxe King Suite (Redwood Courtyard View) |
| Check-in: | Tuesday, October 27, 2026 after 3:00 PM (Digital Key enabled) |
| Check-out: | Sunday, November 1, 2026 at 11:00 AM (Late check-out 1:00 PM approved) |
| Guest Wi-Fi: | Network: RedwoodGuest / Passcode: SEQUOIA2026 |
| Breakfast & Amenities: | Complimentary breakfast 6:30 AM - 10:00 AM at Canopy Bistro; EV Level-2 chargers in Garage B2. |
| Conference Shuttle: | Direct shuttle to Shoreline Amphitheatre departs every 20 minutes from Main Lobby. |
Booking Reference: EF-44210-PAR — 2 Adult Summit Elevator Tickets
+SEINE-9921Car 7 Seat 12E
QR Pickup Code: JR994021
+ """ + .trimIndent() + ) + + viewModel.importDocument(uri = Uri.fromFile(emlFile), tripId = TRIP_ID) + settleUntil { documentDao.snapshot().any { it.title == "Shinkansen Nozomi 204 E-Ticket" } } + val imported = documentDao.snapshot().single { it.title == "Shinkansen Nozomi 204 E-Ticket" } + settleUntil { appSearchManager.getIndexedDocumentIds().contains(imported.id) } + + viewModel.onVaultQueryChange("JR994021") + viewModel.searchVault(TRIP_ID) + settleUntil { viewModel.vaultResults.value != null } + + assertThat(viewModel.vaultResults.value?.map { it.title }) + .contains("Shinkansen Nozomi 204 E-Ticket") + } + + /** Creates the demo note and waits until the write-through hook has indexed it. */ + private suspend fun TestScope.createLumiereNote(): TravelDocument { + settleUntil { documentDao.snapshot().isNotEmpty() } + viewModel.createTextDocument( + title = "Hotel Lumiere Booking", + content = "Wifi network LUMIERE-GUEST, password bonjour2026", + documentType = "note", + tripId = TRIP_ID, + ) + settleUntil { documentDao.snapshot().any { it.title == "Hotel Lumiere Booking" } } + val created = documentDao.snapshot().single { it.title == "Hotel Lumiere Booking" } + settleUntil { appSearchManager.getIndexedDocumentIds().contains(created.id) } + return created + } + + /** + * Drains both clocks until [condition] holds. + * + * The ViewModel runs on the virtual test scheduler but the indexing and search paths hop to the + * IO dispatcher, which that scheduler does not own. `advanceUntilIdle()` alone therefore returns + * while real work is still in flight, so each pass advances the scheduler and then parks this + * thread to give the IO side real wall-clock time to catch up. + */ + private suspend fun TestScope.settleUntil(condition: suspend () -> Boolean) { + repeat(SETTLE_ATTEMPTS) { + advanceUntilIdle() + if (condition()) return + Thread.sleep(SETTLE_STEP_MS) + } + advanceUntilIdle() + } + + private companion object { + const val TRIP_ID = "2026-3" + const val SETTLE_ATTEMPTS = 100 + const val SETTLE_STEP_MS = 50L + } +} + +/** + * Minimal in-memory [TravelDocumentDao] so the tests never touch Room. + * + * Every access is synchronized: the ViewModel writes from IO threads while the test thread reads, + * and an unguarded [ArrayList] throws [java.util.ConcurrentModificationException] under that race. + */ +private class FakeVaultDocumentDao : TravelDocumentDao { + private val backing = mutableListOf