From 6b02aa0f654b76a4c3a5602f2e7cc2f21d3d6dfe Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 22:56:52 +0000 Subject: [PATCH 1/2] fix(server): gracefully degrade when vault is locked during browser search RankingPreferences.load() and PersonalizationPreferences.load() both document themselves as fail-soft, but store.get() was unguarded and threw when the DEK was absent. On a passphrase-mode device the process lifecycle wipes the DEK on background, so a browser search while the app is backgrounded crashed with "vault is locked: DEK not present in memory". Wrap store.get() in runCatching so locked-vault reads fall back to empty defaults instead of propagating the error to the served page. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_01AAUvwoGuyk1ToQf9WSU7A1 --- .../data/prefs/PersonalizationPreferences.kt | 2 +- .../data/prefs/RankingPreferences.kt | 2 +- .../searchmob/data/RankingPreferencesTest.kt | 24 +++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/searchmob/data/prefs/PersonalizationPreferences.kt b/app/src/main/java/org/searchmob/data/prefs/PersonalizationPreferences.kt index c4b7dd0..ee4ec02 100644 --- a/app/src/main/java/org/searchmob/data/prefs/PersonalizationPreferences.kt +++ b/app/src/main/java/org/searchmob/data/prefs/PersonalizationPreferences.kt @@ -14,7 +14,7 @@ import org.searchmob.engine.rank.Personalizer */ class PersonalizationPreferences(private val store: PreferencesStore) { suspend fun load(): PersonalizationModel { - val raw = store.get(KEY) ?: return PersonalizationModel() + val raw = runCatching { store.get(KEY) }.getOrNull() ?: return PersonalizationModel() return Personalizer.fromJson(raw) } diff --git a/app/src/main/java/org/searchmob/data/prefs/RankingPreferences.kt b/app/src/main/java/org/searchmob/data/prefs/RankingPreferences.kt index c7fd783..6f5f979 100644 --- a/app/src/main/java/org/searchmob/data/prefs/RankingPreferences.kt +++ b/app/src/main/java/org/searchmob/data/prefs/RankingPreferences.kt @@ -16,7 +16,7 @@ import org.searchmob.engine.rank.RankingRules */ class RankingPreferences(private val store: PreferencesStore) { suspend fun load(): RankingRules { - val raw = store.get(KEY) ?: return withDefaultLenses(RankingRules.EMPTY) + val raw = runCatching { store.get(KEY) }.getOrNull() ?: return withDefaultLenses(RankingRules.EMPTY) val parsed = runCatching { json.decodeFromString(raw) }.getOrDefault(RankingRules.EMPTY) return withDefaultLenses(parsed) } diff --git a/app/src/test/java/org/searchmob/data/RankingPreferencesTest.kt b/app/src/test/java/org/searchmob/data/RankingPreferencesTest.kt index 967b04e..93e531b 100644 --- a/app/src/test/java/org/searchmob/data/RankingPreferencesTest.kt +++ b/app/src/test/java/org/searchmob/data/RankingPreferencesTest.kt @@ -39,6 +39,21 @@ private class FakeRankingStore : PreferencesStore { override suspend fun clear() = map.clear() } +/** A store that throws on every read, simulating a locked vault (DEK wiped from memory). */ +private class LockedStore : PreferencesStore { + override fun observe(): Flow = throw IllegalStateException("vault is locked: DEK not present in memory") + + override suspend fun getAll(): Preferences = error("vault is locked: DEK not present in memory") + + override suspend fun get(key: String): String? = error("vault is locked: DEK not present in memory") + + override suspend fun put(key: String, value: String) = error("vault is locked: DEK not present in memory") + + override suspend fun remove(key: String) = error("vault is locked: DEK not present in memory") + + override suspend fun clear() = error("vault is locked: DEK not present in memory") +} + class RankingPreferencesTest { @Test fun domainRuleSetAndClear() = @@ -92,4 +107,13 @@ class RankingPreferencesTest { val prefs = RankingPreferences(FakeRankingStore()) assertEquals(false, prefs.importJson("not json")) } + + @Test + fun loadReturnsEmptyWhenVaultIsLocked() = + runTest { + val prefs = RankingPreferences(LockedStore()) + val rules = prefs.load() + assertEquals(DEFAULT_SAMPLE_LENSES, rules.lenses) + assertTrue(rules.domainRules.isEmpty()) + } } From 533c49a491698eabef80e98037716a88dc6aef01 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 28 Jun 2026 22:59:10 +0000 Subject: [PATCH 2/2] style(test): fix ktlint violations in LockedStore test helper Extract a shared locked() helper to stay under the 120-char line limit and match the project's multi-line parameter style for put(). Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_01AAUvwoGuyk1ToQf9WSU7A1 --- .../searchmob/data/RankingPreferencesTest.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/test/java/org/searchmob/data/RankingPreferencesTest.kt b/app/src/test/java/org/searchmob/data/RankingPreferencesTest.kt index 93e531b..1b03b18 100644 --- a/app/src/test/java/org/searchmob/data/RankingPreferencesTest.kt +++ b/app/src/test/java/org/searchmob/data/RankingPreferencesTest.kt @@ -41,17 +41,22 @@ private class FakeRankingStore : PreferencesStore { /** A store that throws on every read, simulating a locked vault (DEK wiped from memory). */ private class LockedStore : PreferencesStore { - override fun observe(): Flow = throw IllegalStateException("vault is locked: DEK not present in memory") + private fun locked(): Nothing = error("vault is locked: DEK not present in memory") - override suspend fun getAll(): Preferences = error("vault is locked: DEK not present in memory") + override fun observe(): Flow = locked() - override suspend fun get(key: String): String? = error("vault is locked: DEK not present in memory") + override suspend fun getAll(): Preferences = locked() - override suspend fun put(key: String, value: String) = error("vault is locked: DEK not present in memory") + override suspend fun get(key: String): String? = locked() - override suspend fun remove(key: String) = error("vault is locked: DEK not present in memory") + override suspend fun put( + key: String, + value: String, + ) = locked() + + override suspend fun remove(key: String) = locked() - override suspend fun clear() = error("vault is locked: DEK not present in memory") + override suspend fun clear() = locked() } class RankingPreferencesTest {