- model 新增 ContentTypeDocument + Content 的四字段 + DocumentMessage 构造器 + ValidateDocumentParts(文档部件只允许 RoleUser,非 user ⇒ ErrDocumentOnNonUser,fail-loud)。 - 两个编码器入口(buildMessages / buildAnthropicMessages)对文档部件报 ErrBadRequest: 本期 document 永不抵达编码器,且绝不静默丢块。 - history:拆出 types.go(Message 增 Document 列),新增 document.go(DocumentRef 同时是 落库 JSON 与 DTO 形状,6 字段含 status ⇒ §4.4 的「读不到要可见」;坏列回 unreadable 不静默)。 - chat_messages 加 document 列(迁移 0015 + NewStore 幂等前滚);historyMessage.Document DTO; session_persist 的 messageTextImage → messageTextImageDocument(保留窄视图以免牵连 steer 配对)。 - 只落「引用 + 统计」,绝不落抽取正文(TestPersistedDocumentHoldsNoBody 把全部列拼起来扫正文)。 验收(主会话独立复跑,非自报):隔离 worktree(HEAD + 本切片)go build/vet 绿、 全树 58 包 0 FAIL、linux/amd64 + darwin/arm64 交叉编译绿、18 条 Document* 测试 PASS。 变异独立复现:ValidateDocumentParts 开头 return nil ⇒ 非 user 负例红。
93 lines
4.0 KiB
Go
93 lines
4.0 KiB
Go
package model
|
||
|
||
import (
|
||
"errors"
|
||
"testing"
|
||
)
|
||
|
||
// 本文件是文档内容类型的构造器与两条负例(design/24 §2.2、§2.3):
|
||
// ① 文档部件抵达任一编码器 → ErrBadRequest(fail-loud,绝不静默丢块);
|
||
// ② 文档部件挂在非 RoleUser 回合上 → ErrDocumentOnNonUser(编程错误)。
|
||
// 断言一律用 errors.Is 对哨兵错误,绝不按错误文本子串。
|
||
|
||
// TestDocumentMessageCarriesReference 验证构造器带上文本与服务端签发的引用。
|
||
func TestDocumentMessageCarriesReference(t *testing.T) {
|
||
t.Parallel()
|
||
doc := Document{Name: "a1b2.txt", MIME: "text/plain", URL: "/api/v1/documents/a1b2.txt", Size: 42}
|
||
msg := DocumentMessage(RoleUser, "读一下", doc)
|
||
if msg.Role != RoleUser {
|
||
t.Fatalf("role = %q, want %q", msg.Role, RoleUser)
|
||
}
|
||
if len(msg.Content) != 2 || msg.Content[0].Type != ContentTypeText || msg.Content[0].Text != "读一下" {
|
||
t.Fatalf("content = %+v, want text + document", msg.Content)
|
||
}
|
||
part := msg.Content[1]
|
||
if part.Type != ContentTypeDocument {
|
||
t.Fatalf("part type = %q, want %q", part.Type, ContentTypeDocument)
|
||
}
|
||
if part.DocumentName != doc.Name || part.DocumentMIME != doc.MIME || part.DocumentURL != doc.URL || part.DocumentSize != doc.Size {
|
||
t.Fatalf("document part = %+v, want %+v", part, doc)
|
||
}
|
||
}
|
||
|
||
// TestDocumentMessageWithoutText 验证文本为空时只留文档部件(与 ImageMessage
|
||
// 同一口径:空文本不造空片段)。
|
||
func TestDocumentMessageWithoutText(t *testing.T) {
|
||
t.Parallel()
|
||
msg := DocumentMessage(RoleUser, "", Document{Name: "a1b2.txt"})
|
||
if len(msg.Content) != 1 || msg.Content[0].Type != ContentTypeDocument {
|
||
t.Fatalf("content = %+v, want a single document part", msg.Content)
|
||
}
|
||
}
|
||
|
||
// TestEncodeContentRejectsDocument 是负例①(OpenAI 侧):未解析的文档部件抵达
|
||
// encodeContent 必须返回 ErrBadRequest,而不是被静默跳过(静默丢块会让模型
|
||
// 与用户都不知道附件没送到)。
|
||
func TestEncodeContentRejectsDocument(t *testing.T) {
|
||
t.Parallel()
|
||
_, err := encodeContent([]Content{{Type: ContentTypeDocument, DocumentName: "a1b2.txt"}})
|
||
if !errors.Is(err, ErrBadRequest) {
|
||
t.Fatalf("err = %v, want wrap of ErrBadRequest", err)
|
||
}
|
||
}
|
||
|
||
// TestEncodeAnthropicContentRejectsDocument 是负例①(Anthropic 侧)。
|
||
func TestEncodeAnthropicContentRejectsDocument(t *testing.T) {
|
||
t.Parallel()
|
||
_, err := encodeAnthropicContent([]Content{{Type: ContentTypeDocument, DocumentName: "a1b2.txt"}})
|
||
if !errors.Is(err, ErrBadRequest) {
|
||
t.Fatalf("err = %v, want wrap of ErrBadRequest", err)
|
||
}
|
||
}
|
||
|
||
// TestBuildMessagesRejectsDocumentOnNonUser 是负例②:文档部件挂到
|
||
// assistant/system 回合上是编程错误(design/11 §10.2 的用户内容归属红线),
|
||
// 两个编码器都必须报错。这里刻意用 ErrDocumentOnNonUser 而不是 ErrBadRequest
|
||
// 断言,以便区分「角色错了」与「类型没人支持」两种失败。
|
||
func TestBuildMessagesRejectsDocumentOnNonUser(t *testing.T) {
|
||
t.Parallel()
|
||
for _, role := range []Role{RoleAssistant, RoleSystem} {
|
||
msg := DocumentMessage(role, "读一下", Document{Name: "a1b2.txt"})
|
||
if _, err := buildMessages([]Message{msg}); !errors.Is(err, ErrDocumentOnNonUser) {
|
||
t.Fatalf("openai buildMessages(%s): err = %v, want wrap of ErrDocumentOnNonUser", role, err)
|
||
}
|
||
if _, _, err := buildAnthropicMessages([]Message{msg}); !errors.Is(err, ErrDocumentOnNonUser) {
|
||
t.Fatalf("anthropic buildAnthropicMessages(%s): err = %v, want wrap of ErrDocumentOnNonUser", role, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestValidateDocumentPartsAcceptsUserDocument 验证用户回合上的文档部件通过
|
||
// 校验:报错的是「挂错角色」,不是「带了文档」本身。
|
||
func TestValidateDocumentPartsAcceptsUserDocument(t *testing.T) {
|
||
t.Parallel()
|
||
msgs := []Message{
|
||
TextMessage(RoleSystem, "你是昔涟"),
|
||
DocumentMessage(RoleUser, "读一下", Document{Name: "a1b2.txt"}),
|
||
TextMessage(RoleAssistant, "好"),
|
||
}
|
||
if err := ValidateDocumentParts(msgs); err != nil {
|
||
t.Fatalf("ValidateDocumentParts = %v, want nil", err)
|
||
}
|
||
}
|