merge: W9 — 文档附件接到 UI 与发送链路(选文件 / 待发芯片 / chat.documents / 分享分流)

This commit is contained in:
AskaEth committed 2026-10-04 18:48:42 +08:00
commit ab76798bff
14 files changed
+1668 -65

No files matched your search

@@ -0,0 +1,103 @@
package top.yeij.cyrene.ui.chat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import top.yeij.cyrene.core.error.CyreneError
import top.yeij.cyrene.data.api.CyreneException
/**
* 附件上传失败的文案(纯函数)。
*
* 这里锚的是**每个拒绝码对应的正确动作**:这四个码在用户能做的事上各不相同
* (415/413 = 换文件、400 = 少选几个、404 = 重传、401/403 = 重新登录),折成一句
* 「附件失败」等于让用户去猜。文案由**服务端**给(含真实上限),客户端**不自己编数字**。
*
* 另一个锚点是「这是上传这一步失败」:网络类失败必须点明,否则用户看到「无法连接到服务器」
* 会以为整条消息没发出去,而实际上文字与附件一起回到了输入框。
*/
class DocumentUploadFailureTest {
private fun server(code: String, message: String, status: Int = 400) =
CyreneException(CyreneError.Server(code, message, status))
@Test
fun `unsupported media type keeps the server wording instead of inventing one`() {
val message = documentUploadFailureMessage(
server("unsupported_media_type", "extension not allowed or content is not plain text", 415),
)
assertEquals("extension not allowed or content is not plain text", message)
}
@Test
fun `too large keeps the server limit so the number is the real one`() {
val message = documentUploadFailureMessage(
server("document_too_large", "file exceeds the 20971520 byte limit", 413),
)
assertEquals("file exceeds the 20971520 byte limit", message)
}
@Test
fun `too many documents keeps the server wording as well`() {
val message = documentUploadFailureMessage(server("too_many_documents", "at most 5 documents", 400))
assertEquals("at most 5 documents", message)
}
@Test
fun `a missing document says to send it again`() {
val message = documentUploadFailureMessage(server("document_not_found", "document not found", 404))
assertEquals("document not found", message)
}
@Test
fun `an expired session asks for a re-login, not for another file`() {
val message = documentUploadFailureMessage(
CyreneException(CyreneError.Unauthorized("unauthorized", "auth: unauthorized")),
)
assertTrue(message, message.contains("重新登录"))
assertFalse("鉴权失败不该劝用户换文件", message.contains("换"))
}
@Test
fun `a network failure names the step that failed and keeps the reason`() {
val message = documentUploadFailureMessage(
CyreneException(CyreneError.Local(CyreneError.LocalKind.NoNetwork)),
)
assertEquals("附件上传失败,网络不可用,请检查连接", message)
}
@Test
fun `an unknown server code still carries the server message`() {
val message = documentUploadFailureMessage(server("weird_new_code", "something went wrong", 500))
assertEquals("附件上传失败,something went wrong", message)
}
@Test
fun `a non Cyrene throwable still produces an actionable message`() {
assertEquals("附件上传失败,请重试", documentUploadFailureMessage(IllegalStateException("boom")))
assertEquals("附件上传失败,请重试", documentUploadFailureMessage(RuntimeException()))
}
@Test
fun `no failure path ever claims the attachment was sent`() {
val messages = listOf(
documentUploadFailureMessage(server("unsupported_media_type", "x", 415)),
documentUploadFailureMessage(server("document_too_large", "x", 413)),
documentUploadFailureMessage(server("too_many_documents", "x", 400)),
documentUploadFailureMessage(server("document_not_found", "x", 404)),
documentUploadFailureMessage(CyreneException(CyreneError.Local(CyreneError.LocalKind.Timeout))),
documentUploadFailureMessage(IllegalStateException()),
)
// 文案里绝不能出现「已发送 / 已上传」这类暗示:失败即整轮回滚,附件与正文一起回到输入框。
assertTrue(messages.toString(), messages.none { it.contains("已发送") || it.contains("已上传") })
}
}