Files
MailUI4Agents/server/internal/repo/repo_test.go

229 lines
7.9 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 repo
import (
"context"
"fmt"
"strings"
"testing"
"github.com/agentmail/gateway/internal/db"
"github.com/google/uuid"
)
// ListSessionsFor 的 Scan 列数必须与 SELECT 一致。
//
// 这个测试存在的理由预算两列max_rounds/used_rounds加进了 SELECT 却忘了加进
// Scan于是 /me/sessions 整个 500 —— 联系人栏一条数据都拉不到,
// 而错误信息只是 "Failed to list sessions",看不出是列数不匹配。
// 列数错位是纯结构问题,一个最小用例就能钉住。
func TestListSessionsForScanMatchesSelect(t *testing.T) {
setupTestDB(t)
if _, err := db.DB.ExecContext(context.Background(),
`INSERT INTO users (username, display_name, password_hash, role)
VALUES ('alice', 'Alice', 'x', 'user')`); err != nil {
t.Fatalf("seed user: %v", err)
}
sid := seedSessionRow(t, "list-scan")
if _, err := db.DB.ExecContext(context.Background(),
`UPDATE sessions SET max_rounds = 7, used_rounds = 3 WHERE session_id = $1`,
sid); err != nil {
t.Fatalf("set budget: %v", err)
}
seedMailIn(t, sid, "alice", "opencode", "hello")
// 无过滤(管理员 all=true 走这条)
all, err := ListSessionsFor(context.Background(), "", 50)
if err != nil {
t.Fatalf("列出全部会话失败: %v", err)
}
if len(all) != 1 {
t.Fatalf("应有 1 个会话,实际 %d", len(all))
}
// 预算两列要真的读出来,不是零值
if all[0].MaxRounds != 7 || all[0].UsedRounds != 3 {
t.Errorf("预算未读出max=%d used=%d期望 7/3",
all[0].MaxRounds, all[0].UsedRounds)
}
if all[0].MailCount != 1 {
t.Errorf("邮件数应为 1实际 %d —— 列顺序可能错位", all[0].MailCount)
}
// 带用户过滤普通用户走这条SQL 分支不同,要分别验)
mine, err := ListSessionsFor(context.Background(), "alice", 50)
if err != nil {
t.Fatalf("列出自己的会话失败: %v", err)
}
if len(mine) != 1 {
t.Fatalf("alice 参与过该会话,应能看到,实际 %d 个", len(mine))
}
if mine[0].MaxRounds != 7 || mine[0].MailCount != 1 {
t.Errorf("过滤分支的列顺序错位:%+v", mine[0])
}
// 与自己无关的人看不到
other, err := ListSessionsFor(context.Background(), "bob", 50)
if err != nil {
t.Fatalf("列出 bob 的会话失败: %v", err)
}
if len(other) != 0 {
t.Errorf("bob 未参与该会话,不该看到,实际 %d 个", len(other))
}
// 归档会话不出现在列表里
if _, err := db.DB.ExecContext(context.Background(),
`UPDATE sessions SET status = 'archived' WHERE session_id = $1`, sid); err != nil {
t.Fatalf("archive: %v", err)
}
after, err := ListSessionsFor(context.Background(), "", 50)
if err != nil {
t.Fatalf("归档后列出失败: %v", err)
}
if len(after) != 0 {
t.Errorf("归档会话不该出现在列表里,实际 %d 个", len(after))
}
}
// 工作列表卡片视图需要「这条线索在干什么 / 还剩几个来回 / 最新进展是什么」,
// 这些都从 ListContactsFor 一次取回 —— 否则卡片要为每条会话再打一次库。
func TestListContactsForCardFields(t *testing.T) {
setupTestDB(t)
if _, err := db.DB.ExecContext(context.Background(),
`INSERT INTO users (username, display_name, password_hash, role)
VALUES ('alice', 'Alice', 'x', 'user')`); err != nil {
t.Fatalf("seed user: %v", err)
}
sid := seedSessionRow(t, "card-fields")
if _, err := db.DB.ExecContext(context.Background(),
`UPDATE sessions SET subject = '缓存层选型评估', max_rounds = 5, used_rounds = 2
WHERE session_id = $1`, sid); err != nil {
t.Fatalf("set session: %v", err)
}
// 三封:最早一封决定联系人身份,最后一封决定「最新进展」
seedMailIn(t, sid, "alice", "opencode", "第一封")
seedMailIn(t, sid, "opencode", "alice", "第二封")
last := seedMailIn(t, sid, "opencode", "alice", "第三封")
if _, err := db.DB.ExecContext(context.Background(),
`UPDATE mails SET body = '已经跑完压测Redis 方案在这个负载下明显更稳。'
WHERE mail_id = $1`, last); err != nil {
t.Fatalf("set body: %v", err)
}
got, err := ListContactsFor(context.Background(), "alice", false)
if err != nil {
t.Fatalf("列出联系人失败: %v", err)
}
if len(got) != 1 {
t.Fatalf("应有 1 个联系人,实际 %d", len(got))
}
c := got[0]
if c.Subject != "缓存层选型评估" {
t.Errorf("主题未带回:%q", c.Subject)
}
if c.MaxRounds != 5 || c.UsedRounds != 2 {
t.Errorf("预算未带回:%d/%d期望 5/2", c.UsedRounds, c.MaxRounds)
}
// 最新进展取的是【最后】一封,不是第一封
if c.LastFrom != "opencode" {
t.Errorf("最新发件人应为 opencode实际 %q", c.LastFrom)
}
if !strings.Contains(c.LastPreview, "Redis 方案") {
t.Errorf("最新摘要应来自最后一封,实际 %q", c.LastPreview)
}
// 联系人身份仍取最早一封的对端
if c.AgentName != "opencode" {
t.Errorf("联系人应为 opencode实际 %q", c.AgentName)
}
if c.MailCount != 3 {
t.Errorf("邮件数应为 3实际 %d —— 列顺序可能错位", c.MailCount)
}
if c.Address != "opencode@.card-fields" && !strings.HasSuffix(c.Address, ".card-fields") {
t.Errorf("地址应带会话别名,实际 %q", c.Address)
}
}
// 摘要按字符截断,不按字节 —— 中文一字三字节,裸切会留半个字符。
func TestPreviewRunes(t *testing.T) {
cases := []struct {
in string
n int
want string
}{
{"短文本", 10, "短文本"},
{" 两边有空白 ", 10, "两边有空白"},
{"", 5, ""},
{"一二三四五六", 3, "一二三…"},
{"abcdefgh", 3, "abc…"},
}
for _, c := range cases {
if got := previewRunes(c.in, c.n); got != c.want {
t.Errorf("previewRunes(%q, %d) = %q期望 %q", c.in, c.n, got, c.want)
}
}
// 截断结果必须是合法 UTF-8不含替换字符
long := strings.Repeat("汉字", 200)
got := previewRunes(long, 90)
if strings.ContainsRune(got, '\uFFFD') {
t.Error("截断产生了 U+FFFD说明按字节切了")
}
if n := len([]rune(got)); n != 91 { // 90 + 省略号
t.Errorf("截断后应为 90 字符 + 省略号,实际 %d 字符", n)
}
}
// 时间戳精度回归SQLite 的 CURRENT_TIMESTAMP 只有秒,同秒插入的多行排序不确定,
// 「会话里最早那封」(决定联系人身份)与「最后那封」(决定最新进展)都会取错。
// NOW() 现在返回毫秒精度,且 mails 的 INSERT 显式传它 —— 这两点都要钉住。
func TestMailTimestampSubSecond(t *testing.T) {
setupTestDB(t)
sid := seedSessionRow(t, "ts-precision")
// 连续插 8 封(不显式给时间戳,走 CreateMail 里的 NOW()
ids := make([]uuid.UUID, 0, 8)
for i := 0; i < 8; i++ {
id, err := CreateMail(context.Background(), sid, nil,
"alice", "", "opencode", "", fmt.Sprintf("第%d封", i), "body", nil)
if err != nil {
t.Fatalf("创建邮件 %d 失败: %v", i, err)
}
ids = append(ids, id)
}
// 至少要出现亚秒差异,否则说明 NOW() 又退回秒精度
var distinct int
if err := db.DB.QueryRowContext(context.Background(),
`SELECT COUNT(DISTINCT created_at) FROM mails WHERE session_id = $1`,
sid).Scan(&distinct); err != nil {
t.Fatalf("统计不同时间戳失败: %v", err)
}
if distinct < 2 {
var sample string
db.DB.QueryRowContext(context.Background(),
`SELECT CAST(created_at AS TEXT) FROM mails WHERE session_id = $1 LIMIT 1`,
sid).Scan(&sample)
t.Fatalf("8 封邮件只有 %d 个不同时间戳(样例 %q—— NOW() 精度不足,"+
"同秒邮件的先后顺序会由随机 UUID 决定", distinct, sample)
}
// GetSessionMails 按时间升序,顺序必须与插入顺序一致
got, err := GetSessionMails(context.Background(), sid)
if err != nil {
t.Fatalf("取会话邮件失败: %v", err)
}
if len(got) != len(ids) {
t.Fatalf("应有 %d 封,实际 %d", len(ids), len(got))
}
for i, m := range got {
if m.ID != ids[i] {
t.Errorf("第 %d 封顺序错位:期望 %s实际 %s主题 %q",
i, ids[i], m.ID, m.Subject)
}
}
}