Files
Cyrene_v2/internal/services/channelbridge/inbound_documents_honesty_test.go
T
AskaEth 2a43035f5d
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
refactor+feat(documents,channelbridge): 共享存储核心 + IM 入站附件摄取(主人 2.3)
- 抽共享存储核心:internal/runtime/documents/store.go(22 项白名单 + 字节嗅探唯一裁决 + 内容寻址 sha256[:32]+ext + 去重 + 限额 + 路径双检)+ store_meta.go(documents 表唯一写入实现)。

- gateway 改为调用 store,**对外行为零变化**(机器证明:既有 documents_test/chat_documents_test/chat_documents_bounds_test 三个文件 0 行改动且全绿;状态码/响应字段/错误码/审计动作名/限额文档口径全部逐字保留;maxChatDocuments 改为引用 documents.MaxPerMessage)。

- IM 入站附件摄取(channelbridge):文件类媒体 → store.Save → 挂 model.DocumentMessage(RoleUser) 部件 ⇒ 后续 resolveDocuments 自动抽取/注入/清单;挂载点在「会话解析 + 黑白名单 + 最小打扰」三重闸门之后(被拦消息绝不在盘上留文件);**不支持的格式不静默**(诚实提示行 + 带来源日志,只记 name/mime/size,名字做展示净化防伪造行);图片/语音保持既有链路。

- 主会话独立验收:隔离 worktree(M1 的 12 文件)build/vet 绿、全树 59 包 0 FAIL、双平台交叉编译绿、12 条 Inbound 用例 PASS;变异独立复现(关闭 IM 摄取 ⇒ 5 条红,含「落盘文件数 = [] want 1」)。

- ⚠️ 真机边界(需裁决):现网四个适配器上报的文件媒体**只带 URL**(telegram FileID / qq-obv11 file|url / weixin·kook CDN URL),Media.Data 恒空 ⇒ 本切片只摄**内联字节**,URL 一律「诚实提示,不代取远程」(避免第三份 SSRF 实现)。真机要让「她读到 IM 文件」,需各适配器入站时下载并内联(凭据在各自渠道里,推荐)或新增共享受守卫抓取器。

- 另登记:IM 附件暂未写 chat_messages.document 列(二手回合看不到引用,建议并入 S7);channelbridge 既有测试夹具在 -race 下有数据竞争(stubSessions.GetOrCreate,与本次无关)。
2026-10-04 19:25:00 +08:00

123 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package channelbridge
// 本文件承载 IM 入站附件摄取的**诚实性与边界**用例(design/24 §4.4;切片 M1),
// 与 inbound_documents_test.go 是同一批夹具的搬家(夹具留在那边,同包可见):
//
// - 拒绝原因必须**分类正确**:超限说超限,绝不笼统说成「格式不支持」;
// - 渠道只给远程引用(目前适配器的现实形态)时不代取、不静默;
// - 图片类媒体保持现状(不走文档摄取);
// - 组合根未接线时照旧跑回合,但模型知道附件读不到。
import (
"context"
"strings"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/documents"
)
// TestInboundOversizeFileIsHonestAboutSize 钉住诚实口径的**分类**:体积超限必须
// 说「超过体积上限」,绝不能笼统说成「格式不支持」——把原因说错也是不诚实。
func TestInboundOversizeFileIsHonestAboutSize(t *testing.T) {
t.Parallel()
env := newIngestEnv(t, documents.StoreConfig{MaxBytes: 8}, true)
msg := fileMessage("这个大吗", "big.txt", []byte("这是明显超过八个字节的正文\n"), "text/plain")
env.bridge.process(context.Background(), msg)
if names := docDirNames(t, env.uploads); len(names) != 0 {
t.Fatalf("超限附件被落库了:%v", names)
}
visible := env.model.visibleText()
if !strings.Contains(visible, "体积上限") {
t.Fatalf("超限附件的提示没有点明体积上限:%q", visible)
}
if strings.Contains(visible, "格式不在可读白名单内") {
t.Fatalf("超限附件被说成了「格式不支持」(原因说错):%q", visible)
}
if log := env.logs.String(); !strings.Contains(log, "inbound attachment rejected") {
t.Fatalf("缺少带来源的拒绝日志:%q", log)
}
}
// TestInboundRemoteOnlyMediaIsHonestWithoutFetch 钉住「渠道只给远程引用」这一现实形态
// (适配器目前上报 URL/平台文件 id,而不是内联字节):本切片**不代取**,但绝不静默。
func TestInboundRemoteOnlyMediaIsHonestWithoutFetch(t *testing.T) {
t.Parallel()
env := newIngestEnv(t, documents.StoreConfig{}, true)
msg := directMessage()
msg.Content = "这个文件里是什么"
msg.Media = []adapter.Media{{
Kind: adapter.MediaFile, URL: "https://cdn.example.com/secret-token=abc/report.pdf",
Name: "report.pdf", Size: 1024, MIMEType: "application/pdf",
}}
env.bridge.process(context.Background(), msg)
if names := docDirNames(t, env.uploads); len(names) != 0 {
t.Fatalf("远程引用的附件被落库了:%v", names)
}
visible := env.model.visibleText()
if !strings.Contains(visible, "report.pdf") || !strings.Contains(visible, "远程引用") {
t.Fatalf("模型可见视图里没有诚实提示行:%q", visible)
}
log := env.logs.String()
if !strings.Contains(log, "no inlined bytes") {
t.Fatalf("缺少「无内联字节」的带来源日志:%q", log)
}
// 远程引用可能带凭据,绝不进日志。
if strings.Contains(log, "secret-token=abc") {
t.Fatalf("日志里出现了远程引用原串(可能含凭据):%q", log)
}
}
// TestInboundImageMediaKeepsExistingPath 钉住边界:图片类媒体**不**走文档摄取
// (design/24 §4.2 末:图片走既有视觉链路),因此不落库、不挂文档部件、不产生提示行。
func TestInboundImageMediaKeepsExistingPath(t *testing.T) {
t.Parallel()
env := newIngestEnv(t, documents.StoreConfig{}, true)
msg := directMessage()
msg.Content = "看这张图"
msg.Media = []adapter.Media{{
Kind: adapter.MediaImage, Name: "photo.png", MIMEType: "image/png",
Data: []byte("\x89PNG\r\n\x1a\n"), Size: 8,
}}
env.bridge.process(context.Background(), msg)
if names := docDirNames(t, env.uploads); len(names) != 0 {
t.Fatalf("图片类媒体被文档摄取落库了:%v", names)
}
visible := env.model.visibleText()
if strings.Contains(visible, "[附件 ") || strings.Contains(visible, "无法读取") {
t.Fatalf("图片类媒体产生了文档清单/提示行:%q", visible)
}
if !strings.Contains(visible, "看这张图") {
t.Fatalf("用户文本丢了:%q", visible)
}
if log := env.logs.String(); strings.Contains(log, "attachment") {
t.Fatalf("图片类媒体产生了附件日志:%q", log)
}
}
// TestInboundFileMediaWithoutStoreIsHonest 钉住「未接线」路径:组合根没给落盘配置时,
// 带文件的消息照旧能跑(模型收到诚实提示),只是不落库 —— 绝不假装读到了。
func TestInboundFileMediaWithoutStoreIsHonest(t *testing.T) {
t.Parallel()
env := newIngestEnv(t, documents.StoreConfig{}, false)
msg := fileMessage("这个文件呢", "notes.txt", []byte("正文\n"), "text/plain")
env.bridge.process(context.Background(), msg)
if env.model.calls() == 0 {
t.Fatal("未接线时回合没有跑到模型")
}
if !strings.Contains(env.model.visibleText(), "未接线文档存储") {
t.Fatalf("未接线时没有诚实提示:%q", env.model.visibleText())
}
if log := env.logs.String(); !strings.Contains(log, "document store not wired") {
t.Fatalf("未接线时没有带来源的日志:%q", log)
}
}