Keep off-the-record unread marks out of the vault
`MessengerViewModel.update` persists the unread set through `Vault.saveUnreadThreads` on every change, unfiltered. A thread id for a user conversation is `user:<id>`, so an off-the-record conversation with something unread put the peer's id on disk, and it came back at the next launch. `saveOffTheRecord` says the setting is kept and only the setting. The filter goes in `saveUnreadThreads` rather than at the call site, for the reason the call site gives for being a chokepoint: a later caller cannot forget it. The vault already answers `offTheRecord(threadId)` for exactly that key, so the check was available and unused. Nothing is lost. An off-the-record thread holds no lines at rest, so a restored mark would badge a conversation with nothing in it. iOS had the same fault on `unreadPeers` and is fixed in ios!221. Both are the September pins-and-drafts finding again, on the one dictionary each side had left. Falsified: dropping the filter turns two of the four tests red, and the report XML says `tests="4" skipped="0"` in both directions, since gradle will report success having run nothing. Engine 711 and client 469, both green. The 55 engine failures a worktree shows first are `vectors not found`: the vector tests resolve the published vectors through a sibling `flamenet-e2e` checkout, which a worktree has no path to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
2c22ffed75
commit
2cf20e761a
@@ -1381,9 +1381,23 @@ class Vault(
|
||||
return (0 until array.length()).map { array.optString(it) }.filter { it.isNotEmpty() }.toSet()
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the unread marks, minus any off-the-record thread.
|
||||
*
|
||||
* The filter is here rather than at the call site for the reason the call
|
||||
* site gives for being a chokepoint: a later caller cannot forget it. A
|
||||
* thread id for a user conversation is `user:<id>`, so an unfiltered save
|
||||
* put the peer's id on disk for a conversation §6.28 says is gone when the
|
||||
* app closes, and [saveOffTheRecord] says keeps the setting and nothing
|
||||
* else.
|
||||
*
|
||||
* Nothing is lost by dropping them. An off-the-record thread holds no
|
||||
* lines at rest, so a restored mark would badge a conversation with
|
||||
* nothing in it.
|
||||
*/
|
||||
fun saveUnreadThreads(ids: Set<String>) {
|
||||
val array = org.json.JSONArray()
|
||||
ids.sorted().forEach { array.put(it) }
|
||||
ids.filterNot { offTheRecord(it) }.sorted().forEach { array.put(it) }
|
||||
writeJson(UNREAD_THREADS, JSONObject().put("ids", array))
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package io.flamenet.e2e
|
||||
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* An unread mark for an off-the-record thread never reaches the vault.
|
||||
*
|
||||
* `saveOffTheRecord` says the setting is kept and only the setting. A thread
|
||||
* id for a user conversation is `user:<id>`, so saving the unread set
|
||||
* unfiltered put the peer's id on disk for a conversation §6.28 says is gone
|
||||
* when the app closes, and brought it back at the next launch.
|
||||
*
|
||||
* The same fault the pins had, on the set the view model persists at its one
|
||||
* chokepoint. Being the only write site is what makes it the right place to
|
||||
* filter, not a reason the filter was unnecessary.
|
||||
*/
|
||||
class OffTheRecordUnreadMarksTest {
|
||||
private class MapStorage : Vault.Storage {
|
||||
val records = LinkedHashMap<String, ByteArray>()
|
||||
|
||||
override fun read(name: String) = records[name]
|
||||
|
||||
override fun write(
|
||||
name: String,
|
||||
bytes: ByteArray,
|
||||
) {
|
||||
records[name] = bytes
|
||||
}
|
||||
|
||||
override fun delete(name: String) {
|
||||
records.remove(name)
|
||||
}
|
||||
|
||||
override fun names() = records.keys.toList()
|
||||
}
|
||||
|
||||
private fun key(): SecretKey = KeyGenerator.getInstance("AES").apply { init(256) }.generateKey()
|
||||
|
||||
private val secret = "user:7"
|
||||
private val ordinary = "user:8"
|
||||
|
||||
@Test
|
||||
fun `an off the record thread is left out of the saved marks`() {
|
||||
val storage = MapStorage()
|
||||
val k = key()
|
||||
Vault(storage, k).apply {
|
||||
saveOffTheRecord(secret, true)
|
||||
saveUnreadThreads(setOf(secret, ordinary))
|
||||
}
|
||||
|
||||
val restored = Vault(storage, k).unreadThreads()
|
||||
assertFalse(secret in restored, "the off-the-record thread id was written to the vault")
|
||||
assertTrue(ordinary in restored, "an ordinary conversation stopped being badged")
|
||||
assertEquals(1, restored.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `nothing is dropped when no thread is off the record`() {
|
||||
val storage = MapStorage()
|
||||
val k = key()
|
||||
Vault(storage, k).saveUnreadThreads(setOf(secret, ordinary))
|
||||
assertEquals(setOf(secret, ordinary), Vault(storage, k).unreadThreads())
|
||||
}
|
||||
|
||||
/**
|
||||
* Turning it on does not retroactively remove a mark already written, but
|
||||
* the next save must drop it: the set is rewritten whole every time.
|
||||
*/
|
||||
@Test
|
||||
fun `the next save after turning it on clears the mark`() {
|
||||
val storage = MapStorage()
|
||||
val k = key()
|
||||
val v = Vault(storage, k)
|
||||
v.saveUnreadThreads(setOf(secret, ordinary))
|
||||
assertTrue(secret in Vault(storage, k).unreadThreads())
|
||||
|
||||
v.saveOffTheRecord(secret, true)
|
||||
v.saveUnreadThreads(setOf(secret, ordinary))
|
||||
assertFalse(secret in Vault(storage, k).unreadThreads())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a group thread has no off the record setting and passes through`() {
|
||||
val storage = MapStorage()
|
||||
val k = key()
|
||||
Vault(storage, k).saveUnreadThreads(setOf("group:g-1"))
|
||||
assertTrue("group:g-1" in Vault(storage, k).unreadThreads())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user