diff options
| author | Sho Sakuma <me@m1sk9.dev> | 2026-08-05 03:45:52 +0900 |
|---|---|---|
| committer | Sho Sakuma <me@m1sk9.dev> | 2026-08-05 03:45:52 +0900 |
| commit | 2223cf96de63f57cf076b30e8d758c8fc5899d4e (patch) | |
| tree | 6bdd25a541a730d3c7ed5cc5719c1143864ffe3a /platform-paper | |
| parent | e25bfe0e39771ceac287f93a235aa5954a154236 (diff) | |
| download | LunaticChat-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>
Diffstat (limited to 'platform-paper')
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 |
