merge: W3 — B1 /upload(大图走上传,历史只留 URL)

This commit is contained in:
AskaEth committed 2026-10-04 17:44:23 +08:00
commit 664fd8dcef
11 files changed
+869 -6

No files matched your search

@@ -0,0 +1,89 @@
package top.yeij.cyrene.ui.chat
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import top.yeij.cyrene.core.error.CyreneError
import top.yeij.cyrene.data.api.CyreneException
/**
* 上传失败的文案(纯函数)。
*
* 这里锚的是**行为承诺**,不是措辞本身:上传失败时这条消息**没有发出去**(文本与图一起
* 回到输入框),而且**绝不**退回内联 base64——退回就等于把「省下历史重量」当场作废,
* 还是在用户看不见的情况下换了一条语义不同的路。
*
* 两个服务端拒绝码对应的是「换一张图」而不是「重试」,必须与网络失败分开说。
*/
class ImageUploadFailureTest {
private fun server(code: String, message: String, status: Int = 400) =
CyreneException(CyreneError.Server(code, message, status))
@Test
fun `payload_too_large asks for a smaller image, not a retry`() {
val message = uploadFailureMessage(server("payload_too_large", "file exceeds the 5MB limit", 413))
assertEquals("图片超过服务端 5 MB 上限,请换一张更小的", message)
}
@Test
fun `unsupported_media_type says the format is refused`() {
val message = uploadFailureMessage(
server("unsupported_media_type", "only png/jpeg/gif/webp/bmp images are accepted", 415),
)
assertEquals("服务端不接受这张图片的格式,请换一张", message)
}
@Test
fun `a network failure names the step that failed and keeps the reason`() {
val message = uploadFailureMessage(CyreneException(CyreneError.Local(CyreneError.LocalKind.NoNetwork)))
assertEquals("图片上传失败,网络不可用,请检查连接", message)
}
@Test
fun `a timeout keeps its own wording`() {
val message = uploadFailureMessage(CyreneException(CyreneError.Local(CyreneError.LocalKind.Timeout)))
assertEquals("图片上传失败,连接超时,服务器没有响应", message)
}
@Test
fun `a server error keeps the server message instead of inventing one`() {
val message = uploadFailureMessage(server("upload_failed", "failed to store upload", 500))
assertEquals("图片上传失败,failed to store upload", message)
}
@Test
fun `an expired session is still an upload failure, never a silent success`() {
val message = uploadFailureMessage(CyreneException(CyreneError.Unauthorized("unauthorized", "auth: unauthorized")))
assertTrue(message, message.startsWith("图片上传失败"))
}
@Test
fun `a non Cyrene throwable still produces an actionable message`() {
assertEquals("图片上传失败,请重试", uploadFailureMessage(IllegalStateException("boom")))
assertEquals("图片上传失败,请重试", uploadFailureMessage(RuntimeException()))
}
@Test
fun `no failure path ever falls back to inlining the image`() {
// 文案里绝不能出现「改用内联 / 直接发送」这类暗示:回退内联正是要消灭的成本。
val messages = listOf(
uploadFailureMessage(server("payload_too_large", "x", 413)),
uploadFailureMessage(server("unsupported_media_type", "x", 415)),
uploadFailureMessage(server("upload_failed", "x", 500)),
uploadFailureMessage(CyreneException(CyreneError.Local(CyreneError.LocalKind.ConnectFailed))),
uploadFailureMessage(IllegalStateException()),
)
assertTrue(
messages.toString(),
messages.all { it.contains("换一张") || it.contains("重试") || it.contains("上传失败") },
)
}
}