summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSho Sakuma <me@m1sk9.dev>2026-08-05 03:45:52 +0900
committerSho Sakuma <me@m1sk9.dev>2026-08-05 03:45:52 +0900
commit2223cf96de63f57cf076b30e8d758c8fc5899d4e (patch)
tree6bdd25a541a730d3c7ed5cc5719c1143864ffe3a
parente25bfe0e39771ceac287f93a235aa5954a154236 (diff)
downloadLunaticChat-2223cf96de63f57cf076b30e8d758c8fc5899d4e.tar.gz
LunaticChat-2223cf96de63f57cf076b30e8d758c8fc5899d4e.tar.bz2
LunaticChat-2223cf96de63f57cf076b30e8d758c8fc5899d4e.zip
fix: stop a slow reply from pinning a word to hiragana
Making a conversion timeout an ordinary exception put it in the same arm as a hard API failure, where caching the hiragana fallback is deliberate - so one slow reply recorded the unconverted form and that word rendered as hiragana for the life of the cache. A timeout says the request was slow, not that the word has no conversion, so it now returns the fallback without caching it and the next message asks again. The retry test builds its own remembering cache: the shared fixture's get() always returns null, so against it the API is called every time and the test would have passed even with the timeout cached. Co-Authored-By: Claude <noreply@anthropic.com>
-rw-r--r--platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverter.kt7
-rw-r--r--platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverterTest.kt25
2 files changed, 30 insertions, 2 deletions
diff --git a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverter.kt b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverter.kt
index b502d63..e1a11f8 100644
--- a/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverter.kt
+++ b/platform-paper/src/main/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverter.kt
@@ -93,6 +93,13 @@ class RomanjiConverter(
// pin every word of the message to its unconverted form for good, because the words
// are converted concurrently and the timeout cancels all of them at once.
throw e
+ } catch (e: ConversionTimeoutException) {
+ // Returned without caching: a timeout says the request was slow, not that the word
+ // has no conversion, so recording the hiragana would pin it for the life of the
+ // cache over one slow reply. A hard API failure is different - the fallback is
+ // cached there deliberately.
+ logger.warning("Timed out converting $hiragana, leaving it uncached: ${e.message}")
+ return hiragana
} catch (e: Exception) {
logger.warning("Failed to convert $hiragana: ${e.message}")
hiragana // Use hiragana if API fails
diff --git a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverterTest.kt b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverterTest.kt
index 69baa32..c8314d7 100644
--- a/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverterTest.kt
+++ b/platform-paper/src/test/kotlin/dev/m1sk9/lunaticChat/paper/converter/RomanjiConverterTest.kt
@@ -302,7 +302,7 @@ class RomanjiConverterTest {
}
@Test
- fun `an API timeout degrades to hiragana like any other failure`() =
+ fun `an API timeout degrades to hiragana without caching it`() =
runBlocking {
val (converter, cache, apiClient) = createConverter()
coEvery { apiClient.convert("おはよう") } throws ConversionTimeoutException(1.seconds)
@@ -310,7 +310,28 @@ class RomanjiConverterTest {
val result = converter.convert("ohayou")
assertEquals("おはよう", result)
- verify(exactly = 1) { cache.put("ohayou", "おはよう") }
+ // A slow reply says nothing about the word, so caching the hiragana would pin it to its
+ // unconverted form for the life of the cache.
+ verify(exactly = 0) { cache.put(any(), any()) }
+ }
+
+ @Test
+ fun `a word that timed out is converted on the next attempt`() =
+ runBlocking {
+ // A cache that actually remembers, unlike the shared fixture whose get() always returns
+ // null - which would let this pass even if the timeout had been cached.
+ val entries = mutableMapOf<String, String>()
+ val cache = mockk<ConversionCache>(relaxed = true)
+ every { cache.get(any()) } answers { entries[firstArg()] }
+ every { cache.put(any(), any()) } answers { entries[firstArg()] = secondArg() }
+ val apiClient = mockk<GoogleIMEClient>(relaxed = true)
+ val converter = RomanjiConverter(cache, apiClient, TestUtils.TestLogger())
+
+ coEvery { apiClient.convert("おはよう") } throws ConversionTimeoutException(1.seconds)
+ converter.convert("ohayou")
+ coEvery { apiClient.convert("おはよう") } returns "おはよう御座います"
+
+ assertEquals("おはよう御座います", converter.convert("ohayou"))
}
@Test