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) } }