Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package dev.slop.duckweed.companion

import android.content.ContentValues
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

/** Checks repeated sync writes and encrypted reads against real Android storage. */
@RunWith(AndroidJUnit4::class)
class MobileStoragePerformanceTest {
private val context get() = InstrumentationRegistry.getInstrumentation().targetContext
private val pairId = "storage-regression"
private val terminalId = "terminal"
private val workspaceStore get() = WorkspaceStore(context)

@Before
fun reset() {
context.deleteDatabase("duckweed-messages.db")
workspaceStore.remove(pairId)
workspaceStore.remove("other-storage-regression")
}

private fun snapshot() = WorkspaceSnapshot(
pairId, 500L, listOf(RemoteProject(
"project", "Storage regression", "H:/project", null,
terminals = listOf(RemoteTerminal(
terminalId, "Codex", "PowerShell", "Codex", null, "idle",
mode = "conversation", completionSeq = 7L, unreadOnDesktop = true,
conversation = listOf(
RemoteConversationMessage("user", 100L, "user", "Prompt"),
RemoteConversationMessage("older", 200L, "assistant", "Older response"),
RemoteConversationMessage("latest", 300L, "assistant", "Latest response"),
RemoteConversationMessage("stream", 400L, "assistant", "Still streaming", streaming = true),
),
)),
)),
)

@Test
fun repeatedSnapshotsDoNotRewriteHistoryAndStillRepairLegacyRouting() {
val value = snapshot()
val latestId = "workspace:$pairId:$terminalId:latest"
MessageStore(context).use { store ->
assertEquals(emptyList<String>(), store.putSyncedConversation(value))
assertEquals(3, store.conversation(pairId, terminalId).size)
assertNull(store.message(latestId)?.readAt)
assertEquals(200L, store.message("workspace:$pairId:$terminalId:older")?.readAt)
val database = store.writableDatabase
val encrypted = database.rawQuery("SELECT encrypted_payload FROM messages WHERE id = ?", arrayOf(latestId))
.use { it.moveToFirst(); it.getString(0) }
database.execSQL("CREATE TABLE routing_writes (count INTEGER NOT NULL)")
database.execSQL("INSERT INTO routing_writes VALUES (0)")
database.execSQL("CREATE TRIGGER count_routing_writes AFTER UPDATE OF pair_id, terminal_id, kind ON messages BEGIN UPDATE routing_writes SET count = count + 1; END")
repeat(4) { store.putSyncedConversation(value) }
val writes = database.rawQuery("SELECT count FROM routing_writes", null)
.use { it.moveToFirst(); it.getInt(0) }
assertEquals("Unchanged snapshot history should require no routing writes", 0, writes)
val repeated = database.rawQuery("SELECT encrypted_payload FROM messages WHERE id = ?", arrayOf(latestId))
.use { it.moveToFirst(); it.getString(0) }
assertEquals(encrypted, repeated)

store.markRead(latestId, 600L)
database.update("messages", ContentValues().apply {
putNull("pair_id"); putNull("terminal_id"); putNull("kind")
}, "id = ?", arrayOf(latestId))
database.execSQL("UPDATE routing_writes SET count = 0")
store.putSyncedConversation(value)
val repaired = store.message(latestId)!!
assertEquals(600L, repaired.readAt)
assertEquals(pairId, store.conversation(pairId, terminalId).last().pairId)
assertEquals(1, database.rawQuery("SELECT count FROM routing_writes", null)
.use { it.moveToFirst(); it.getInt(0) })

val desktopRead = value.copy(projects = value.projects.map { project ->
project.copy(terminals = project.terminals.map { it.copy(unreadOnDesktop = false, readCompletionSeq = 7L) })
})
store.putSyncedConversation(desktopRead)
assertTrue(store.unreadConversationKeys().isEmpty())
}
}

@Test
fun approvalRefreshSelectsOnlyActiveAttentionMessagesIncludingLegacyRows() {
MessageStore(context).use { store ->
fun put(id: String, kind: String) = store.put(CompletionRecord(
id, 100L, "Codex", "Storage regression", kind, "Response", null,
pairId = pairId, terminalId = terminalId,
))
put("active-attention", "attention")
put("inactive-attention", "attention")
put("active-completion", "completed")
put("legacy-attention", "attention")
store.writableDatabase.update("messages", ContentValues().apply { putNull("kind") }, "id = ?", arrayOf("legacy-attention"))
val active = setOf("active-attention".hashCode(), "active-completion".hashCode(), "legacy-attention".hashCode())
assertEquals(setOf("active-attention", "legacy-attention"), store.activeAttentionMessages(active).map { it.id }.toSet())
assertTrue(store.activeAttentionMessages(emptySet()).isEmpty())
}
}

@Test
fun singleWorkspaceLookupPreservesHeartbeatAndDoesNotReadOtherPairings() {
val value = snapshot()
workspaceStore.put(value, 700L)
workspaceStore.put(value.copy(pairId = "other-storage-regression", updatedAt = 900L), 900L)
workspaceStore.markPresence(pairId, 1_000L)
assertEquals(workspaceStore.all().first { it.pairId == pairId }, workspaceStore.get(pairId))
assertEquals(1_000L, workspaceStore.get(pairId)?.lastSeenAt)
assertNull(workspaceStore.get("missing-storage-regression"))
workspaceStore.remove(pairId)
assertNull(workspaceStore.get(pairId))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ class ApprovalActionReceiver : BroadcastReceiver() {
val result = runCatching {
val credentials = SecretStore.load(context, pairId)
?: error("This desktop is no longer paired.")
val permission = WorkspaceStore(context).all()
.firstOrNull { it.pairId == pairId }
val permission = WorkspaceStore(context).get(pairId)
?.projects
?.asSequence()
?.flatMap { it.terminals.asSequence() }
Expand All @@ -42,12 +41,12 @@ class ApprovalActionReceiver : BroadcastReceiver() {
}
Handler(Looper.getMainLooper()).post {
result.onSuccess {
MessageStore(context).markRead(messageId)
MessageStore(context).use { it.markRead(messageId) }
NotificationManagerCompat.from(context).cancel(messageId.hashCode())
Toast.makeText(context, "Decision sent securely", Toast.LENGTH_SHORT).show()
NotificationTools.announceChanged(context)
}.onFailure { error ->
MessageStore(context).response(messageId)?.let { NotificationTools.show(context, it) }
MessageStore(context).use { it.response(messageId) }?.let { NotificationTools.show(context, it) }
Toast.makeText(
context,
error.message ?: "Could not send this decision. Try again.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ class MainActivity : AppCompatActivity() {
if (granted) {
showPendingNotifications()
} else {
MessageStore(this).dismissPendingNotifications()
MessageStore(this).use { it.dismissPendingNotifications() }
}
}

Expand Down Expand Up @@ -313,7 +313,7 @@ class MainActivity : AppCompatActivity() {
draftStore = DraftStore(this)
pendingActionStore = PendingMobileActionStore(this)
pendingMobileActions = pendingActionStore.all()
storageExecutor.execute { MessageStore(this).recoverInterruptedSends() }
storageExecutor.execute { MessageStore(this).use { it.recoverInterruptedSends() } }
ReadSyncScheduler.enqueue(this)

pairingStatus = findViewById(R.id.pairing_status)
Expand Down Expand Up @@ -880,9 +880,10 @@ class MainActivity : AppCompatActivity() {
if (enabled == notificationsAreActive()) return@setOnCheckedChangeListener
if (!enabled) {
NotificationPreference.setEnabled(this, false)
val store = MessageStore(this)
store.dismissPendingNotifications()
NotificationTools.cancel(this, store.latest())
MessageStore(this).use { store ->
store.dismissPendingNotifications()
NotificationTools.cancel(this, store.latest())
}
return@setOnCheckedChangeListener
}
if (needsNotificationPermission()) {
Expand Down Expand Up @@ -1004,7 +1005,7 @@ class MainActivity : AppCompatActivity() {

private fun requestNotificationPermissionIfEnabled() {
if (!NotificationPreference.isEnabled(this)) {
MessageStore(this).dismissPendingNotifications()
MessageStore(this).use { it.dismissPendingNotifications() }
return
}
if (needsNotificationPermission()) {
Expand Down Expand Up @@ -1217,7 +1218,7 @@ class MainActivity : AppCompatActivity() {
.onSuccess {
SecretStore.remove(this, pairing.pairId)
WorkspaceStore(this).remove(pairing.pairId)
MessageStore(this).discardReadSyncs(pairing.pairId)
MessageStore(this).use { it.discardReadSyncs(pairing.pairId) }
PendingMobileActionStore(this).removePair(pairing.pairId)
}
.onFailure { failures += it.message ?: pairing.pairId }
Expand Down Expand Up @@ -1342,12 +1343,13 @@ class MainActivity : AppCompatActivity() {
.map { Pair(snapshot.pairId, it.id) }
}
}.toSet()
val store = MessageStore(this)
Triple(
snapshots,
store.latestForOpenAgents(openAgentTerminals, 50),
store.unreadConversationKeys(),
)
MessageStore(this).use { store ->
Triple(
snapshots,
store.latestForOpenAgents(openAgentTerminals, 50),
store.unreadConversationKeys(),
)
}
}
runOnUiThread {
remoteStateLoading = false
Expand Down Expand Up @@ -1554,16 +1556,14 @@ class MainActivity : AppCompatActivity() {
val pairId = message.pairId
val terminalId = message.terminalId
if (pairId != null && terminalId != null) {
val cleared = MessageStore(this).markConversationRead(
pairId,
terminalId,
message.completionSeq,
)
val cleared = MessageStore(this).use {
it.markConversationRead(pairId, terminalId, message.completionSeq)
}
NotificationTools.cancelIds(this, cleared)
ReadSyncScheduler.enqueue(this)
messageAdapter.markConversationRead(pairId, terminalId)
} else {
MessageStore(this).markRead(message.id)
MessageStore(this).use { it.markRead(message.id) }
messageAdapter.markRead(message.id)
}
selectedTarget = null
Expand Down Expand Up @@ -3224,7 +3224,7 @@ class MainActivity : AppCompatActivity() {
sentAt,
)
}.onSuccess {
MessageStore(this).updateOutgoingState(commandId, "sent")
MessageStore(this).use { it.updateOutgoingState(commandId, "sent") }
runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
outgoingMessages[commandId] = outgoing.copy(deliveryState = "sent")
Expand All @@ -3241,11 +3241,9 @@ class MainActivity : AppCompatActivity() {
)
}
}.onFailure { error ->
MessageStore(this).updateOutgoingState(
commandId,
"failed",
error.message ?: "Could not send this message.",
)
MessageStore(this).use {
it.updateOutgoingState(commandId, "failed", error.message ?: "Could not send this message.")
}
runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
outgoingMessages[commandId] = outgoing.copy(
Expand Down Expand Up @@ -3276,7 +3274,7 @@ class MainActivity : AppCompatActivity() {
refreshConversation(reloadHistory = false)
commandExecutor.execute {
runCatching {
MessageStore(this).updateOutgoingState(message.id, "sending")
MessageStore(this).use { it.updateOutgoingState(message.id, "sending") }
val credentials = SecretStore.load(this, pairId)
?: error("Desktop pairing is no longer available. Pair this desktop again.")
RelayClient.sendCommand(
Expand All @@ -3289,7 +3287,7 @@ class MainActivity : AppCompatActivity() {
System.currentTimeMillis(),
)
}.onSuccess {
MessageStore(this).updateOutgoingState(message.id, "sent")
MessageStore(this).use { it.updateOutgoingState(message.id, "sent") }
runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
outgoingMessages[message.id] = message.copy(deliveryState = "sent", deliveryError = null)
Expand All @@ -3303,11 +3301,9 @@ class MainActivity : AppCompatActivity() {
)
}
}.onFailure { error ->
MessageStore(this).updateOutgoingState(
message.id,
"failed",
error.message ?: "Could not send this message.",
)
MessageStore(this).use {
it.updateOutgoingState(message.id, "failed", error.message ?: "Could not send this message.")
}
runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
outgoingMessages[message.id] = message.copy(deliveryState = "failed", deliveryError = error.message)
Expand Down Expand Up @@ -3419,7 +3415,7 @@ class MainActivity : AppCompatActivity() {
private fun openIntentResponse() {
if (isAppLockEnabled() && !appUnlocked) return
val messageId = intent.getStringExtra("message_id") ?: return
val message = MessageStore(this).response(messageId) ?: return
val message = MessageStore(this).use { it.response(messageId) } ?: return
intent.removeExtra("message_id")
navigateToPage(Page.ACTIVITY)
openResponse(message)
Expand Down
Loading
Loading