Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5b4d271
Translate the full meaning caption, not a silent suffix
Borisserz Sep 16, 2026
abaf721
Verify complete long-caption requests and visible translation failures
Chuloo Sep 16, 2026
da34338
Add conversation diagnostics, error guidance, and adaptive voice poli…
Chuloo Sep 16, 2026
84f011f
Merge remote-tracking branch 'origin/main' into codex/conversation-re…
Chuloo Sep 16, 2026
15f451b
Merge remote-tracking branch 'origin/review-57' into codex/conversati…
Chuloo Sep 16, 2026
04c6185
Merge remote-tracking branch 'origin/review-61' into codex/conversati…
Chuloo Sep 16, 2026
9919f35
Protect active typing and verify native error and inactivity flows
Chuloo Sep 16, 2026
869febb
Merge remote-tracking branch 'origin/review-57' into codex/conversati…
Chuloo Sep 16, 2026
a205c31
Merge remote-tracking branch 'origin/review-57' into codex/conversati…
Chuloo Sep 16, 2026
63a5cfa
Merge remote-tracking branch 'origin/review-61' into codex/conversati…
Chuloo Sep 16, 2026
6872f28
Merge branch 'codex/conversation-foundation' into codex/conversation-…
Chuloo Sep 16, 2026
36afad6
Keep help requests authoritative and document verified conversation UX
Chuloo Sep 16, 2026
3dd0870
Record pending approval for draft publication
Chuloo Sep 16, 2026
5e3da99
Record authorization to publish the draft review
Chuloo Sep 16, 2026
8132a0a
Merge branch 'review-48' into codex/conversation-reliability
Chuloo Sep 16, 2026
6fbee72
Merge branch 'review-49' into codex/conversation-reliability
Chuloo Sep 16, 2026
5aee14c
Refine quiet-session UX, defer accent work, and verify combined relia…
Chuloo Sep 16, 2026
2f27fd2
Align release specification regression with preview 8
Chuloo Sep 16, 2026
9d99ba0
Reconcile merged dependencies and record final device verification
Chuloo Sep 16, 2026
b5ecfc4
Address final close logging and timeout parity review
Chuloo Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ android {
applicationId = "chat.mural.android"
minSdk = 26
targetSdk = 36
versionCode = 7
versionCode = 8
versionName = "0.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "MANAGED_API_ORIGIN", buildString(muralConfiguration("mural.apiOrigin")))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,51 @@ class CaptionParityTest {
compose.runOnIdle { assertEquals(1, vm.session!!.fragments.count { it.speaker == Speaker.user }) }
}

@Test fun longMeaningRequestKeepsEveryCharacterAndCachesTheFullRevision() {
val caption = "UNIQUE_START " + "我喜欢咖啡。 ".repeat(600) + " UNIQUE_END"
response = "The entire caption, including its beginning and end."
show("zh", caption, "")
requests.clear()
compose.runOnIdle {
MuralViewModel::class.java.getDeclaredMethod("scheduleTranslation", Boolean::class.javaPrimitiveType)
.apply { isAccessible = true }.invoke(vm, true)
}
compose.waitUntil(10_000) { vm.meaning == response }
val request = Json.parseToJsonElement(checkNotNull(requests.poll(2, TimeUnit.SECONDS))).jsonObject
assertEquals(caption, request.getValue("input").jsonArray.single().jsonObject.getValue("content").jsonPrimitive.content)
compose.runOnIdle {
val passage = vm.session!!.passages.single()
assertEquals(response, vm.session!!.translations[MeaningRequest.cacheKey(passage.revisionKey, "English")])
}
compose.onNodeWithTag("start-conversation").assertIsDisplayed()
compose.onNodeWithTag("floating-navigation").assertIsDisplayed()
}

@Test fun oversizedHostedMeaningShowsLimitWithoutDispatchOrRetry() {
show("zh", "我".repeat(8193), "")
requests.clear()
compose.runOnIdle {
val hosted = MuralViewModel::class.java.getDeclaredField("hostedSessionIDs").apply { isAccessible = true }
hosted.set(vm, setOf(vm.session!!.id))
MuralViewModel::class.java.getDeclaredMethod("scheduleTranslation", Boolean::class.javaPrimitiveType)
.apply { isAccessible = true }.invoke(vm, true)
}
compose.waitUntil(10_000) { vm.meaningLimitReached }
compose.onNodeWithText(compose.activity.getString(R.string.talk_meaning_too_long)).assertIsDisplayed()
compose.onNodeWithText(compose.activity.getString(R.string.talk_retry_meaning_button)).assertDoesNotExist()
assertTrue("An oversized hosted caption must not reach the provider", requests.isEmpty())
compose.runOnIdle {
MuralViewModel::class.java.getDeclaredField("hostedSessionIDs").apply { isAccessible = true }.set(vm, emptySet<String>())
}
show("zh", "你好。", "")
compose.runOnIdle {
MuralViewModel::class.java.getDeclaredMethod("scheduleTranslation", Boolean::class.javaPrimitiveType)
.apply { isAccessible = true }.invoke(vm, true)
}
compose.waitUntil(10_000) { vm.meaning == response }
compose.runOnIdle { assertFalse(vm.meaningLimitReached) }
}

@Test fun lookupKeepsItsSentenceAndDismissalCancelsTheOldResult() {
val sentence = "Quiero un café con leche."
show("es", sentence, "I want a coffee with milk.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package chat.mural

import android.graphics.Bitmap
import androidx.compose.runtime.MutableState
import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import chat.mural.core.*
import chat.mural.network.APIClient
import org.junit.*
import org.junit.Assert.*
import org.junit.runner.RunWith
import java.io.File

@RunWith(AndroidJUnit4::class)
class ConversationPolicyTest {
@get:Rule val compose = createAndroidComposeRule<MainActivity>()
private lateinit var vm: MuralViewModel
private lateinit var original: String
@Before fun prepare() {
assertEquals("chat.mural.android.uitest", compose.activity.packageName)
vm = compose.awaitHistoryLoaded()
compose.runOnIdle {
original = ArchiveCodec.encode(vm.archive)
vm.updatePreferences(vm.archive.preferences.copy(hasOnboarded = true, aiConsentVersion = 1))
}
}
@Suppress("UNCHECKED_CAST") private fun state(name: String, value: Any?) {
val field = MuralViewModel::class.java.getDeclaredField(name + "\$delegate").apply { isAccessible = true }
(field.get(vm) as MutableState<Any?>).value = value
}
@After fun restore() {
if (!::original.isInitialized) return
compose.runOnIdle {
state("session", null); state("state", "idle"); state("inactivitySeconds", null); vm.dismissError()
val restored = ArchiveCodec.decode(original); state("archive", restored); vm.updatePreferences(restored.preferences)
}
}
private fun capture(name: String) {
compose.waitForIdle()
val auto = InstrumentationRegistry.getInstrumentation().uiAutomation
auto.waitForIdle(500, 5000)
val dir = File(compose.activity.filesDir, "conversation-policy").apply { mkdirs() }
File(dir, "$name.png").outputStream().use { auto.takeScreenshot()!!.compress(Bitmap.CompressFormat.PNG, 100, it) }
}
@Test fun countdownIsVisibleAndTypingClearsTheWarning() {
compose.runOnIdle {
state("session", SessionRecord(languageID = vm.language.id, fragments = mutableListOf(
Fragment(speaker = Speaker.assistant, text = "Hei! Hvordan går det?", startMS = 0, endMS = 1000))))
MuralViewModel::class.java.getDeclaredField("voiceSession").apply { isAccessible = true }.setBoolean(vm, true)
state("state", "active"); state("inactivitySeconds", 5)
}
compose.onNodeWithTag("conversation-status").assertTextEquals(compose.activity.getString(R.string.talk_inactivity_warning, 5)).assertIsDisplayed()
val warningBounds = compose.onNodeWithTag("conversation-status").fetchSemanticsNode().boundsInRoot
val reportBounds = compose.onNodeWithTag("report-current-utterance").fetchSemanticsNode().boundsInRoot
assertTrue("Countdown must leave room for the report control", warningBounds.right <= reportBounds.left + 1)
capture("countdown")
compose.onNodeWithText(compose.activity.getString(R.string.talk_type_button)).performClick()
compose.onNodeWithTag("typed-reply-input").performTextInput("Hola")
compose.runOnIdle { assertNull(vm.inactivitySeconds) }
capture("typing-grace")
compose.onNodeWithContentDescription(compose.activity.getString(R.string.common_close)).performClick()
}
@Test fun quotaErrorUsesBillingAdviceAndSafeProviderReference() {
compose.runOnIdle {
MuralViewModel::class.java.getDeclaredMethod("presentError", Throwable::class.java, Int::class.javaPrimitiveType)
.apply { isAccessible = true }.invoke(vm, APIClient.APIException.Http(429, "insufficient_quota", "req_ui_fixture"), R.string.error_voice_connect_failed)
}
compose.onNodeWithText(compose.activity.getString(R.string.error_provider_quota), substring = true).assertIsDisplayed()
compose.onNodeWithText("req_ui_fixture", substring = true).assertIsDisplayed()
capture("quota-error")
compose.onNodeWithText(compose.activity.getString(R.string.common_ok)).performClick()
compose.runOnIdle { assertNull(vm.error) }
}
}
Loading