From 61224530742205880d84c6de0b0d7fcc8f407a1b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 5 Oct 2026 11:11:07 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(android):=20=E4=BA=91=E7=AC=94=E8=AE=B0?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E5=A4=B1=E8=B4=A5=E6=97=B6=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E5=8F=A3=E8=BF=B0=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Core 在 webhook 返回前就删除云笔记的历史和录音。地址未配置、网络失败或 HTTP 错误时,识别文本只存在于完成事件里,原来的错误提示会把它丢掉。失败时改写入键盘剪贴板,成功提交仍不留本地副本。 Co-authored-by: lubaiqing9 --- .../app/android/kotlin/CloudNoteDelivery.kt | 39 +++++++++++++++ .../app/android/kotlin/OpenLessImeService.kt | 46 ++++++++++++++++-- .../kotlin/test/CloudNoteDeliveryTest.kt | 39 +++++++++++++++ ...roid-cloud-note-delivery-contract.test.mjs | 47 +++++++++++++++++++ .../app/scripts/copy-android-scaffolding.mjs | 2 + 5 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 openless-all/app/android/kotlin/CloudNoteDelivery.kt create mode 100644 openless-all/app/android/kotlin/test/CloudNoteDeliveryTest.kt create mode 100644 openless-all/app/scripts/android-cloud-note-delivery-contract.test.mjs diff --git a/openless-all/app/android/kotlin/CloudNoteDelivery.kt b/openless-all/app/android/kotlin/CloudNoteDelivery.kt new file mode 100644 index 000000000..750321030 --- /dev/null +++ b/openless-all/app/android/kotlin/CloudNoteDelivery.kt @@ -0,0 +1,39 @@ +package com.openless.app + +/** + * What to do with a cloud-note transcript the webhook did not accept. + * + * The backend deletes the recording and history before the IME learns whether + * the webhook succeeded. A failed submit therefore has to keep the text the + * completion event already delivered, or the dictation is gone. + */ +internal enum class CloudNoteFailure { + MissingDestination, + Http, + Network, +} + +internal fun cloudNoteShouldRetain(text: String): Boolean = text.isNotBlank() + +internal fun cloudNoteFailureDetail(failure: CloudNoteFailure, httpCode: Int = 0): Pair { + return when (failure) { + CloudNoteFailure.MissingDestination -> + "请先在设置中填写云笔记的地址和 Token" to "Add the cloud note URL and token in Settings" + CloudNoteFailure.Http -> + "云笔记提交失败($httpCode)" to "Cloud note submit failed ($httpCode)" + CloudNoteFailure.Network -> + "云笔记提交失败,请检查网络" to "Cloud note submit failed. Check the network" + } +} + +internal fun cloudNoteFailureMessage( + detail: Pair, + retained: Boolean, +): Pair { + val (zh, en) = detail + return if (retained) { + "$zh,内容已保存在键盘剪贴板" to "$en. The text was saved in the keyboard clipboard." + } else { + zh to en + } +} diff --git a/openless-all/app/android/kotlin/OpenLessImeService.kt b/openless-all/app/android/kotlin/OpenLessImeService.kt index be1543462..222cf5502 100644 --- a/openless-all/app/android/kotlin/OpenLessImeService.kt +++ b/openless-all/app/android/kotlin/OpenLessImeService.kt @@ -3844,7 +3844,7 @@ class OpenLessImeService : InputMethodService(), OpenLessOverlayBridge.OverlaySt recording = false processing = false if (url.isEmpty() || token.isEmpty()) { - setState("error", "请先在设置中填写云笔记的地址/Token") + reportCloudNoteFailure(requestId, text, CloudNoteFailure.MissingDestination) return } setState("thinking", "正在提交云笔记") @@ -3867,22 +3867,60 @@ class OpenLessImeService : InputMethodService(), OpenLessOverlayBridge.OverlaySt connection.outputStream.use { it.write(body.toByteArray(Charsets.UTF_8)) } val code = connection.responseCode mainHandler.post { - if (requestId != imeRequestId) return@post if (code in 200..299) { + if (requestId != imeRequestId) return@post setState("done", "已提交云笔记", QUICK_NOTE_CONFIRMATION_DELAY_MS) } else { - setState("error", "云笔记提交失败($code)") + reportCloudNoteFailure(requestId, text, CloudNoteFailure.Http, code) } } } catch (error: Throwable) { android.util.Log.w("OpenLessImeService", "cloud note webhook submit failed: ${error.javaClass.simpleName}") mainHandler.post { - if (requestId == imeRequestId) setState("error", "云笔记提交失败,请检查网络") + reportCloudNoteFailure(requestId, text, CloudNoteFailure.Network) } } finally { connection?.disconnect() } }.start() } + /** + * Keeps a transcript the webhook did not accept. Core already removed the + * recording and history, so the completion text is the only remaining copy. + * A newer recording may own the status line; the clipboard entry still has + * to be written, and a toast reports it without touching that session. + */ + private fun reportCloudNoteFailure( + requestId: Long, + text: String, + failure: CloudNoteFailure, + httpCode: Int = 0, + ) { + val retained = if (cloudNoteShouldRetain(text)) { + try { + OpenLessClipboardHistory.recordCopy(this, text) + true + } catch (error: Throwable) { + android.util.Log.w( + "OpenLessImeService", + "cloud note clipboard retain failed: ${error.javaClass.simpleName}", + ) + false + } + } else { + false + } + val (messageZh, messageEn) = cloudNoteFailureMessage( + cloudNoteFailureDetail(failure, httpCode), + retained, + ) + val message = ui(messageZh, messageEn) + if (requestId == imeRequestId) { + setState("error", message) + } else { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + } + } + private fun commitImeText(text: String) { if (awaitingEditReplacement) { finishEditWithSpokenReplacement(text) diff --git a/openless-all/app/android/kotlin/test/CloudNoteDeliveryTest.kt b/openless-all/app/android/kotlin/test/CloudNoteDeliveryTest.kt new file mode 100644 index 000000000..d5c00fc07 --- /dev/null +++ b/openless-all/app/android/kotlin/test/CloudNoteDeliveryTest.kt @@ -0,0 +1,39 @@ +package com.openless.app +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class CloudNoteDeliveryTest { + @Test fun blankTranscriptIsNotRetained() { + assertFalse(cloudNoteShouldRetain("")) + assertFalse(cloudNoteShouldRetain(" ")) + } + + @Test fun failedSubmitKeepsTheTranscriptInTheMessage() { + assertTrue(cloudNoteShouldRetain("明天开会")) + val message = cloudNoteFailureMessage( + cloudNoteFailureDetail(CloudNoteFailure.Http, 500), + retained = true, + ) + assertEquals("云笔记提交失败(500),内容已保存在键盘剪贴板", message.first) + assertEquals( + "Cloud note submit failed (500). The text was saved in the keyboard clipboard.", + message.second, + ) + } + + @Test fun missingDestinationAndNetworkFailuresAlsoSayTheTextWasKept() { + val missing = cloudNoteFailureMessage( + cloudNoteFailureDetail(CloudNoteFailure.MissingDestination), + retained = true, + ) + assertEquals("请先在设置中填写云笔记的地址和 Token,内容已保存在键盘剪贴板", missing.first) + val network = cloudNoteFailureMessage( + cloudNoteFailureDetail(CloudNoteFailure.Network), + retained = false, + ) + assertEquals("云笔记提交失败,请检查网络", network.first) + assertFalse(network.second.contains("clipboard")) + } +} diff --git a/openless-all/app/scripts/android-cloud-note-delivery-contract.test.mjs b/openless-all/app/scripts/android-cloud-note-delivery-contract.test.mjs new file mode 100644 index 000000000..33e917926 --- /dev/null +++ b/openless-all/app/scripts/android-cloud-note-delivery-contract.test.mjs @@ -0,0 +1,47 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +const servicePath = fileURLToPath( + new URL('../android/kotlin/OpenLessImeService.kt', import.meta.url), +); +const copyScriptPath = fileURLToPath(new URL('./copy-android-scaffolding.mjs', import.meta.url)); +const service = readFileSync(servicePath, 'utf8'); +const copyScript = readFileSync(copyScriptPath, 'utf8'); + +function functionBody(source, signature) { + const signatureIndex = source.indexOf(signature); + assert.notEqual(signatureIndex, -1, `missing function: ${signature}`); + const openBrace = source.indexOf('{', signatureIndex); + assert.notEqual(openBrace, -1, `missing opening brace: ${signature}`); + let depth = 0; + for (let index = openBrace; index < source.length; index += 1) { + if (source[index] === '{') depth += 1; + if (source[index] === '}') depth -= 1; + if (depth === 0) return source.slice(openBrace + 1, index); + } + assert.fail(`missing closing brace: ${signature}`); +} + +const submit = functionBody(service, 'private fun submitCloudNoteText'); +const report = functionBody(service, 'private fun reportCloudNoteFailure'); +const successStart = submit.indexOf('if (code in 200..299)'); +const successElse = submit.indexOf('} else {', successStart); +assert.ok(successStart !== -1 && successElse > successStart, 'missing webhook success branch'); +const successBranch = submit.slice(successStart, successElse); + +assert.match(submit, /CloudNoteFailure\.MissingDestination/); +assert.match(submit, /CloudNoteFailure\.Http/); +assert.match(submit, /CloudNoteFailure\.Network/); +assert.match(report, /OpenLessClipboardHistory\.recordCopy/); +assert.match(report, /cloudNoteShouldRetain\(text\)/); +assert.doesNotMatch( + successBranch, + /recordCopy|reportCloudNoteFailure|cloudNoteShouldRetain/, + 'a successful webhook submit must not keep a local copy', +); +assert.match(copyScript, /'CloudNoteDelivery\.kt'/); +assert.match(copyScript, /'CloudNoteDeliveryTest\.kt'/); + +console.log('android cloud note delivery contract passed'); diff --git a/openless-all/app/scripts/copy-android-scaffolding.mjs b/openless-all/app/scripts/copy-android-scaffolding.mjs index 1378c307d..0e5f121c4 100644 --- a/openless-all/app/scripts/copy-android-scaffolding.mjs +++ b/openless-all/app/scripts/copy-android-scaffolding.mjs @@ -74,6 +74,7 @@ const KOTLIN_FILES = [ 'OpenLessRuntimeService.kt', 'OpenLessBackendWarmupActivity.kt', 'OpenLessClipboardHistory.kt', + 'CloudNoteDelivery.kt', 'OpenLessKeyboardSettingsActivity.kt', 'OpenLessProcessRestartStats.kt', 'OpenLessBuildInfo.kt', @@ -81,6 +82,7 @@ const KOTLIN_FILES = [ const KOTLIN_TEST_FILES = [ 'ImeLearningPolicyTest.kt', + 'CloudNoteDeliveryTest.kt', 'OpenLessContentReaderTest.kt', 'OpenLessCredentialCipherTest.kt', 'OpenLessShizukuBridgeTest.kt', From 1cbdd957343e703a5c1094e14068b04029d6ce8b Mon Sep 17 00:00:00 2001 From: Chris233 Date: Tue, 20 Oct 2026 18:06:37 +0800 Subject: [PATCH 2/2] fix(android): retain cloud notes when clipboard persistence fails --- .../app/android/kotlin/CloudNoteDelivery.kt | 10 +- .../kotlin/OpenLessClipboardHistory.kt | 33 ++++-- .../app/android/kotlin/OpenLessImeService.kt | 17 +-- ...penLessClipboardHistoryInstrumentedTest.kt | 111 ++++++++++++++++++ .../kotlin/test/CloudNoteDeliveryTest.kt | 18 +++ ...roid-cloud-note-delivery-contract.test.mjs | 7 +- .../app/scripts/copy-android-scaffolding.mjs | 1 + 7 files changed, 171 insertions(+), 26 deletions(-) create mode 100644 openless-all/app/android/kotlin/androidTest/OpenLessClipboardHistoryInstrumentedTest.kt diff --git a/openless-all/app/android/kotlin/CloudNoteDelivery.kt b/openless-all/app/android/kotlin/CloudNoteDelivery.kt index 750321030..58c15a160 100644 --- a/openless-all/app/android/kotlin/CloudNoteDelivery.kt +++ b/openless-all/app/android/kotlin/CloudNoteDelivery.kt @@ -29,11 +29,13 @@ internal fun cloudNoteFailureDetail(failure: CloudNoteFailure, httpCode: Int = 0 internal fun cloudNoteFailureMessage( detail: Pair, retained: Boolean, + persisted: Boolean, ): Pair { val (zh, en) = detail - return if (retained) { - "$zh,内容已保存在键盘剪贴板" to "$en. The text was saved in the keyboard clipboard." - } else { - zh to en + return when { + persisted -> "$zh,内容已保存在键盘剪贴板" to "$en. The text was saved in the keyboard clipboard." + retained -> "$zh,内容仅暂存在键盘剪贴板,应用重启后会丢失,请及时复制" to + "$en. The text is only in memory in the keyboard clipboard. Copy it before the app restarts." + else -> zh to en } } diff --git a/openless-all/app/android/kotlin/OpenLessClipboardHistory.kt b/openless-all/app/android/kotlin/OpenLessClipboardHistory.kt index a9133feba..a9abbfb82 100644 --- a/openless-all/app/android/kotlin/OpenLessClipboardHistory.kt +++ b/openless-all/app/android/kotlin/OpenLessClipboardHistory.kt @@ -4,6 +4,9 @@ import android.content.Context import org.json.JSONArray import org.json.JSONObject import java.io.File +import java.io.FileOutputStream +import java.nio.file.Files +import java.nio.file.StandardCopyOption /** One remembered clipboard entry. */ data class ClipboardEntry(val text: String, val timestamp: Long, val favorite: Boolean = false) @@ -18,6 +21,8 @@ data class ClipboardEntry(val text: String, val timestamp: Long, val favorite: B object OpenLessClipboardHistory { private const val FILE_NAME = "openless_clipboard_history.json" private const val MAX_ENTRIES = 200 + // A failed disk write must leave the text available in the clipboard panel until retry or process exit. + private var unsavedEntries: List? = null enum class Category { ALL, RECENT, TEXT, NUMBER, LINK } @@ -43,6 +48,7 @@ object OpenLessClipboardHistory { @Synchronized fun load(context: Context): List { + unsavedEntries?.let { return it } return try { val target = file(context) if (!target.exists()) return emptyList() @@ -58,8 +64,10 @@ object OpenLessClipboardHistory { } @Synchronized - private fun save(context: Context, entries: List) { - try { + private fun save(context: Context, entries: List): Boolean { + val target = file(context) + val temporary = File(target.parentFile, "$FILE_NAME.tmp") + return try { val array = JSONArray() entries.forEach { entry -> array.put( @@ -70,9 +78,19 @@ object OpenLessClipboardHistory { }, ) } - file(context).writeText(array.toString()) + FileOutputStream(temporary).use { output -> + output.write(array.toString().toByteArray(Charsets.UTF_8)) + output.fd.sync() + } + Files.move(temporary.toPath(), target.toPath(), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING) + unsavedEntries = null + true } catch (error: Exception) { + unsavedEntries = entries android.util.Log.w("OpenLessClipboardHistory", "failed to save history", error) + false + } finally { + temporary.delete() } } @@ -82,11 +100,12 @@ object OpenLessClipboardHistory { * instead of creating a second row). Also used to bump an existing * history entry to the front when the user pastes it from the history * browser, per the "after a selected clip is committed, move it to the front of the - * clipboard history" requirement. + * clipboard history" requirement. Returns true only after the entry is persisted; + * on failure, load() still exposes the in-memory copy. */ @Synchronized - fun recordCopy(context: Context, text: String) { - if (text.isBlank()) return + fun recordCopy(context: Context, text: String): Boolean { + if (text.isBlank()) return false val current = load(context).toMutableList() // Preserve an existing favorite flag rather than silently clearing // it just because the same text got copied again. @@ -94,7 +113,7 @@ object OpenLessClipboardHistory { current.removeAll { it.text == text } current.add(0, ClipboardEntry(text, System.currentTimeMillis(), wasFavorite)) while (current.size > MAX_ENTRIES) current.removeAt(current.lastIndex) - save(context, current) + return save(context, current) } /** Flips one entry's favorite flag by text — swipe-right on a clipboard history row. Preserves list order. */ diff --git a/openless-all/app/android/kotlin/OpenLessImeService.kt b/openless-all/app/android/kotlin/OpenLessImeService.kt index c4a55f11f..3eb130dd0 100644 --- a/openless-all/app/android/kotlin/OpenLessImeService.kt +++ b/openless-all/app/android/kotlin/OpenLessImeService.kt @@ -3914,23 +3914,12 @@ class OpenLessImeService : InputMethodService(), OpenLessOverlayBridge.OverlaySt failure: CloudNoteFailure, httpCode: Int = 0, ): String { - val retained = if (cloudNoteShouldRetain(text)) { - try { - OpenLessClipboardHistory.recordCopy(this, text) - true - } catch (error: Throwable) { - android.util.Log.w( - "OpenLessImeService", - "cloud note clipboard retain failed: ${error.javaClass.simpleName}", - ) - false - } - } else { - false - } + val retained = cloudNoteShouldRetain(text) + val persisted = retained && OpenLessClipboardHistory.recordCopy(this, text) val (messageZh, messageEn) = cloudNoteFailureMessage( cloudNoteFailureDetail(failure, httpCode), retained, + persisted, ) val message = ui(messageZh, messageEn) if (requestId == imeRequestId) { diff --git a/openless-all/app/android/kotlin/androidTest/OpenLessClipboardHistoryInstrumentedTest.kt b/openless-all/app/android/kotlin/androidTest/OpenLessClipboardHistoryInstrumentedTest.kt new file mode 100644 index 000000000..0afbadd8d --- /dev/null +++ b/openless-all/app/android/kotlin/androidTest/OpenLessClipboardHistoryInstrumentedTest.kt @@ -0,0 +1,111 @@ +package com.openless.app + +import android.content.ContextWrapper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import java.io.File +import org.json.JSONArray +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class OpenLessClipboardHistoryInstrumentedTest { + @get:Rule val temporaryFolder = TemporaryFolder() + private lateinit var directory: File + private lateinit var context: ContextWrapper + private val history get() = File(directory, "openless_clipboard_history.json") + private val staging get() = File(directory, "openless_clipboard_history.json.tmp") + + @Before fun setUp() { + directory = temporaryFolder.newFolder("clipboard") + context = object : ContextWrapper(null) { + override fun getFilesDir(): File = directory + } + } + + @After fun tearDown() { + unblock(staging) + unblock(history) + // A successful save clears the singleton's temporary copy before the next test. + OpenLessClipboardHistory.clearNonFavorites(context) + } + + private fun block(path: File) { + assertTrue(path.mkdir()) + File(path, "blocker").writeText("keep the directory nonempty") + } + + private fun unblock(path: File) { + if (!path.isDirectory) return + assertTrue(File(path, "blocker").delete()) + assertTrue(path.delete()) + } + + private fun texts() = OpenLessClipboardHistory.load(context).map { it.text } + + @Test fun blankTextDoesNotClaimPersistence() { + assertFalse(OpenLessClipboardHistory.recordCopy(context, " ")) + assertFalse(history.exists()) + assertTrue(texts().isEmpty()) + } + + @Test fun successfulWritesPreserveFavoriteAndDeduplicate() { + assertTrue(OpenLessClipboardHistory.recordCopy(context, "favorite")) + OpenLessClipboardHistory.toggleFavorite(context, "favorite") + assertTrue(OpenLessClipboardHistory.recordCopy(context, "favorite")) + val entries = OpenLessClipboardHistory.load(context) + assertEquals(listOf("favorite"), entries.map { it.text }) + assertTrue(entries.single().favorite) + assertTrue(JSONArray(history.readText()).getJSONObject(0).getBoolean("favorite")) + } + + @Test fun failedWritesKeepBothNotesAndLeaveThePreviousFileIntact() { + assertTrue(OpenLessClipboardHistory.recordCopy(context, "saved")) + val before = history.readText() + block(staging) + + assertFalse(OpenLessClipboardHistory.recordCopy(context, "first failed note")) + assertFalse(OpenLessClipboardHistory.recordCopy(context, "second failed note")) + assertEquals(listOf("second failed note", "first failed note", "saved"), texts()) + assertEquals(before, history.readText()) + + unblock(staging) + assertTrue(OpenLessClipboardHistory.recordCopy(context, "second failed note")) + assertEquals(listOf("second failed note", "first failed note", "saved"), texts()) + val persisted = JSONArray(history.readText()) + assertEquals(3, persisted.length()) + assertEquals("first failed note", persisted.getJSONObject(1).getString("text")) + assertFalse(staging.exists()) + } + + @Test fun failedReplacementAlsoRetainsTheText() { + block(history) + assertFalse(OpenLessClipboardHistory.recordCopy(context, "not replaced")) + assertEquals(listOf("not replaced"), texts()) + assertTrue(File(history, "blocker").isFile) + assertFalse(staging.exists()) + + unblock(history) + assertTrue(OpenLessClipboardHistory.recordCopy(context, "retry")) + assertEquals(listOf("retry", "not replaced"), texts()) + assertEquals(2, JSONArray(history.readText()).length()) + } + + @Test fun temporaryHistoryStillSupportsFavoriteClearAndDelete() { + block(staging) + assertFalse(OpenLessClipboardHistory.recordCopy(context, "keep")) + assertFalse(OpenLessClipboardHistory.recordCopy(context, "discard")) + OpenLessClipboardHistory.toggleFavorite(context, "keep") + OpenLessClipboardHistory.clearNonFavorites(context) + assertEquals(listOf("keep"), texts()) + assertTrue(OpenLessClipboardHistory.load(context).single().favorite) + OpenLessClipboardHistory.delete(context, "keep") + assertTrue(texts().isEmpty()) + } +} diff --git a/openless-all/app/android/kotlin/test/CloudNoteDeliveryTest.kt b/openless-all/app/android/kotlin/test/CloudNoteDeliveryTest.kt index d5c00fc07..e79fed4a3 100644 --- a/openless-all/app/android/kotlin/test/CloudNoteDeliveryTest.kt +++ b/openless-all/app/android/kotlin/test/CloudNoteDeliveryTest.kt @@ -15,6 +15,7 @@ class CloudNoteDeliveryTest { val message = cloudNoteFailureMessage( cloudNoteFailureDetail(CloudNoteFailure.Http, 500), retained = true, + persisted = true, ) assertEquals("云笔记提交失败(500),内容已保存在键盘剪贴板", message.first) assertEquals( @@ -27,13 +28,30 @@ class CloudNoteDeliveryTest { val missing = cloudNoteFailureMessage( cloudNoteFailureDetail(CloudNoteFailure.MissingDestination), retained = true, + persisted = true, ) assertEquals("请先在设置中填写云笔记的地址和 Token,内容已保存在键盘剪贴板", missing.first) val network = cloudNoteFailureMessage( cloudNoteFailureDetail(CloudNoteFailure.Network), retained = false, + persisted = false, ) assertEquals("云笔记提交失败,请检查网络", network.first) assertFalse(network.second.contains("clipboard")) } + + @Test fun failedDiskWriteWarnsThatTheClipboardCopyIsTemporary() { + val message = cloudNoteFailureMessage( + cloudNoteFailureDetail(CloudNoteFailure.Http, 503), + retained = true, + persisted = false, + ) + assertEquals("云笔记提交失败(503),内容仅暂存在键盘剪贴板,应用重启后会丢失,请及时复制", message.first) + assertEquals( + "Cloud note submit failed (503). The text is only in memory in the keyboard clipboard. Copy it before the app restarts.", + message.second, + ) + assertFalse(message.first.contains("已保存")) + assertFalse(message.second.contains("was saved")) + } } diff --git a/openless-all/app/scripts/android-cloud-note-delivery-contract.test.mjs b/openless-all/app/scripts/android-cloud-note-delivery-contract.test.mjs index ef76f8438..58149cd98 100644 --- a/openless-all/app/scripts/android-cloud-note-delivery-contract.test.mjs +++ b/openless-all/app/scripts/android-cloud-note-delivery-contract.test.mjs @@ -55,7 +55,11 @@ assert.doesNotMatch( 'stale submissions must still finish their draft callback', ); assert.match(report, /return message/); -assert.match(report, /OpenLessClipboardHistory\.recordCopy/); +assert.match( + report, + /val persisted = retained && OpenLessClipboardHistory\.recordCopy\(this, text\)/, +); +assert.match(report, /cloudNoteFailureMessage\([\s\S]*?retained,\s+persisted,/); assert.match(report, /cloudNoteShouldRetain\(text\)/); assert.doesNotMatch( successBranch, @@ -64,5 +68,6 @@ assert.doesNotMatch( ); assert.match(copyScript, /'CloudNoteDelivery\.kt'/); assert.match(copyScript, /'CloudNoteDeliveryTest\.kt'/); +assert.match(copyScript, /'OpenLessClipboardHistoryInstrumentedTest\.kt'/); console.log('android cloud note delivery contract passed'); diff --git a/openless-all/app/scripts/copy-android-scaffolding.mjs b/openless-all/app/scripts/copy-android-scaffolding.mjs index b202d98e7..bfe5677c8 100644 --- a/openless-all/app/scripts/copy-android-scaffolding.mjs +++ b/openless-all/app/scripts/copy-android-scaffolding.mjs @@ -106,6 +106,7 @@ const KOTLIN_TEST_FILES = [ 'OpenLessAccessibilityComponentIdsTest.kt', ]; const KOTLIN_ANDROID_TEST_FILES = [ + 'OpenLessClipboardHistoryInstrumentedTest.kt', 'OpenLessCredentialVaultInstrumentedTest.kt', 'ImeLearningMigrationTest.kt', 'OpenLessMigrationBundleInstrumentedTest.kt',