fix: stop FindUsersTest flaking when GC evicts weakly-cached users

DesktopLocalCache stores users in LargeSoftCache, which holds each User
via a WeakReference. FindUsersTest populated the cache but kept no strong
reference to the created users, so a GC between consumeMetadata and
findUsersStartingWith could collect them — LargeSoftCache.forEach then
skips (and evicts) the cleared entries, making the search return fewer
results. multipleUsersWithMetadata allocates the most objects and tripped
this most often (AssertionError at FindUsersTest.kt:115).

Verified with a forced-GC probe: without a strong reference all three
users were evicted (found=0); holding a reference kept all three (found=3).

Pin each test's users in a strong-reference list that stays reachable
through the assertions, mirroring how Notes/Account/follow lists keep
authors alive in the real app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X4BThbs8tz9egmFDYv2RPB
This commit is contained in:
Claude
2026-07-27 23:04:04 +00:00
parent 2beb75b59e
commit 2d0ccb4d70
@@ -20,16 +20,31 @@
*/
package com.vitorpamplona.amethyst.desktop.cache
import com.vitorpamplona.amethyst.commons.model.User
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.metadata.MetadataEvent
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class FindUsersTest {
private fun createCache() = DesktopLocalCache()
/**
* [DesktopLocalCache] backs its user store with [com.vitorpamplona.amethyst.commons.model.cache.LargeSoftCache],
* which holds each [User] via a `WeakReference`. Unlike the production app — where Notes,
* the Account, and follow lists keep authors strongly reachable — these tests populate the
* cache and hold no other reference to the created users, so a GC between `consumeMetadata`
* and `findUsersStartingWith` could collect them and make the search return fewer results
* (flaky failure at line 115). Every test keeps its users in a strong-reference list that
* stays reachable through the assertions to pin them in the cache.
*/
private val retained = mutableListOf<User?>()
private fun DesktopLocalCache.retainUser(pubkey: String): User? = getUserIfExists(pubkey).also { retained.add(it) }
private fun fakeMetadata(
pubKey: String,
name: String,
@@ -49,7 +64,7 @@ class FindUsersTest {
val cache = createCache()
val pubkey = KeyPair().pubKey.toHexKey()
cache.getOrCreateUser(pubkey)
retained.add(cache.getOrCreateUser(pubkey))
val results = cache.findUsersStartingWith("test", 10)
assertTrue(results.isEmpty(), "User without metadata should not match name search")
@@ -61,6 +76,7 @@ class FindUsersTest {
val pubkey = KeyPair().pubKey.toHexKey()
cache.consumeMetadata(fakeMetadata(pubkey, "vitor", "Vitor Pamplona"))
cache.retainUser(pubkey)
val results = cache.findUsersStartingWith("Vitor", 10)
assertEquals(1, results.size, "Should find user by display name")
@@ -73,6 +89,7 @@ class FindUsersTest {
val pubkey = KeyPair().pubKey.toHexKey()
cache.consumeMetadata(fakeMetadata(pubkey, "vitor"))
cache.retainUser(pubkey)
val results = cache.findUsersStartingWith("vit", 10)
assertEquals(1, results.size, "Should find user by name prefix")
@@ -84,6 +101,7 @@ class FindUsersTest {
val pubkey = KeyPair().pubKey.toHexKey()
cache.consumeMetadata(fakeMetadata(pubkey, "Vitor", "Vitor Pamplona"))
cache.retainUser(pubkey)
val lower = cache.findUsersStartingWith("vitor", 10)
assertEquals(1, lower.size, "Should find case-insensitively (lowercase)")
@@ -97,7 +115,7 @@ class FindUsersTest {
val cache = createCache()
val pubkey = KeyPair().pubKey.toHexKey()
cache.getOrCreateUser(pubkey)
retained.add(cache.getOrCreateUser(pubkey))
val results = cache.findUsersStartingWith(pubkey.take(8), 10)
assertEquals(1, results.size, "Should find user by pubkey prefix")
@@ -107,9 +125,11 @@ class FindUsersTest {
fun multipleUsersWithMetadata() {
val cache = createCache()
cache.consumeMetadata(fakeMetadata(KeyPair().pubKey.toHexKey(), "alice"))
cache.consumeMetadata(fakeMetadata(KeyPair().pubKey.toHexKey(), "bob"))
cache.consumeMetadata(fakeMetadata(KeyPair().pubKey.toHexKey(), "alex"))
listOf("alice", "bob", "alex").forEach { name ->
val pubkey = KeyPair().pubKey.toHexKey()
cache.consumeMetadata(fakeMetadata(pubkey, name))
cache.retainUser(pubkey)
}
val results = cache.findUsersStartingWith("al", 10)
assertEquals(2, results.size, "Should find alice and alex")
@@ -120,7 +140,7 @@ class FindUsersTest {
val cache = createCache()
// Simulate users created from kind 1 notes (no metadata)
repeat(10) { cache.getOrCreateUser(KeyPair().pubKey.toHexKey()) }
repeat(10) { retained.add(cache.getOrCreateUser(KeyPair().pubKey.toHexKey())) }
assertEquals(10, cache.userCount())
@@ -135,10 +155,10 @@ class FindUsersTest {
cache.consumeMetadata(fakeMetadata(pubkey, "testuser", "Test User"))
val user = cache.getUserIfExists(pubkey)
val user = cache.retainUser(pubkey)
val metadata = user?.metadataOrNull()
assertTrue(metadata != null, "Metadata should exist after consumeMetadata")
assertNotNull(metadata, "Metadata should exist after consumeMetadata")
assertTrue(
metadata.anyNameOrAddressContains(
listOf(