Files
MailUI4Agents/gateway/internal/repo/defaultsession_test.go
JianFeeeee 79c4171c9d feat: L0 线协议冻结 + 附件链路修复 + 人/Agent 区分
L0 核心:
- 严格解码 Decode(DisallowUnknownFields) 全覆盖 29 个 DecodeBody 调用点
- DecodeLenient 心跳专用:容忍新字段但回报 unknown_fields
- 400 消息列出本端点接受的全部字段(jsonFieldNames 反射 tag)
- 日历 status 校验(create 补字段 + update 拦非法值)
- 新增 strictdecode_test.go 10 例 + blob/list_test.go 6 例

A-4 附件挂载回滚:checkAttachable 在 CreateMail 前校验,失败按
解挂→释放 relay→删邮件→退预算回滚,幽灵邮件这条路堵住了

A-5 反向 GC:blob.Store.List() 枚举磁盘(跳 .upload-*),
SweepUnreferencedBlobs 按 attachments + calendar_attachments 反查,
48h 年龄下限兜上传窗口。已接进每小时 sweep 循环

C 人/Agent 区分:四个读路径 + threadCols 补 from_human / to_human
(EXISTS users 判定),models.Mail 加 ToHuman。前端判据从
workspace 启发式改成显式布尔,mailCounterpart/sessionCounterpart
从 session_workspace 取 path(修 dsh@dsh 拼接 bug)

契约文档:SSE new_mail 补 4 字段(in_reply_to/from_human/
permission_mode/permission_enforcement),B-5 加 B-5.6
(Agent→Agent 不转发),B-3.4 MUST 改条件式,心跳补 mode_enforcement
+ unknown_fields,demo 死链修复 + from_human 检查
验收清单加 Agent→Agent 负向对照项
2026-09-06 15:18:06 +08:00

182 lines
5.8 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"
"testing"
)
// 这一组测试钉住「省略 session 位复用默认会话」与「真的新建会话」必须可区分。
//
// 事故背景handler 层曾用 `parentMailID == nil` 判断「是不是新建会话」,
// 据此决定要不要写往返预算与权限档位。但省略 session 位复用默认会话时
// parentMailID 也是 nil —— 于是每一封续谈的信都会把这两个字段重置成默认值。
//
// 线上实测(修复前):
//
// 第一封 to=pi@/tmp/budgetprobe max_rounds=7 → budget_max 7
// 第二封 to=pi@/tmp/budgetprobe省略该字段 → budget_max 20 ← 被静默改写
//
// 而那段代码的注释本身正在论证这不该发生(「续谈已有会话若也接受这个字段,
// 每封新信都会悄悄改掉对方正在遵守的预算」)—— 意图是对的,守卫写错了。
//
// 修法FindOrCreateDefaultSessionCreated 额外返回 created
// handler 改用它而不是 parentMailID。
func TestDefaultSessionFirstCallCreates(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
id, created, err := FindOrCreateDefaultSessionCreated(ctx, "pi", "/w", "jianf", "首封")
if err != nil {
t.Fatal(err)
}
if !created {
t.Fatal("从未通信过的 name@path第一次必须报告 created=true")
}
if id.String() == "" {
t.Fatal("应返回有效会话 id")
}
}
func TestDefaultSessionReuseReportsNotCreated(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
first, created, err := FindOrCreateDefaultSessionCreated(ctx, "pi", "/w", "jianf", "首封")
if err != nil || !created {
t.Fatalf("首封应新建created=%v err=%v", created, err)
}
// 复用的前提是这条会话里有该收件人参与过的邮件EXISTS 子查询)
if _, err := CreateMail(ctx, first, nil, "jianf", "", "pi", "/w", "首封", "x", nil); err != nil {
t.Fatal(err)
}
second, created2, err := FindOrCreateDefaultSessionCreated(ctx, "pi", "/w", "jianf", "第二封")
if err != nil {
t.Fatal(err)
}
if second != first {
t.Fatalf("第二封应复用同一条默认会话first=%s second=%s", first, second)
}
if created2 {
t.Fatal("复用已有默认会话时 created 必须为 false —— 这正是预算被冲掉的根因")
}
}
// 这条是上面那个线上事故的最小复现:走 created 判据时预算不被改写。
func TestBudgetSurvivesDefaultSessionReuse(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
id, created, err := FindOrCreateDefaultSessionCreated(ctx, "pi", "/w", "jianf", "首封")
if err != nil || !created {
t.Fatalf("首封应新建created=%v err=%v", created, err)
}
// 模拟 handler只有 created 为真才设预算
if _, err := SetSessionBudget(ctx, id, 7); err != nil {
t.Fatal(err)
}
if _, err := CreateMail(ctx, id, nil, "jianf", "", "pi", "/w", "首封", "x", nil); err != nil {
t.Fatal(err)
}
_, created2, err := FindOrCreateDefaultSessionCreated(ctx, "pi", "/w", "jianf", "第二封")
if err != nil {
t.Fatal(err)
}
if created2 {
// 若这里为真handler 就会重设预算 —— 事故重现
t.Fatal("复用时 created 为真会让 handler 重设预算")
}
b, err := GetSessionBudget(ctx, id)
if err != nil {
t.Fatal(err)
}
if b.Max != 7 {
t.Fatalf("续谈不得改写预算want 7, got %d", b.Max)
}
}
// 档位与预算同一个判据一起钉住plan 档不能因为第二封信而升成 workspace。
func TestPermissionModeSurvivesDefaultSessionReuse(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
id, created, err := FindOrCreateDefaultSessionCreated(ctx, "pi", "/w", "jianf", "首封")
if err != nil || !created {
t.Fatalf("首封应新建created=%v err=%v", created, err)
}
if _, err := SetSessionPermissionMode(ctx, id, "plan"); err != nil {
t.Fatal(err)
}
if _, err := CreateMail(ctx, id, nil, "jianf", "", "pi", "/w", "首封", "x", nil); err != nil {
t.Fatal(err)
}
_, created2, err := FindOrCreateDefaultSessionCreated(ctx, "pi", "/w", "jianf", "第二封")
if err != nil {
t.Fatal(err)
}
if created2 {
t.Fatal("复用时 created 为真会让 handler 把档位重置成默认档")
}
if got := SessionPermissionMode(ctx, id); got != "plan" {
t.Fatalf("续谈不得改写档位want plan, got %s", got)
}
}
// 不同工作目录是不同的默认会话,两边各自新建。
// 这条防的是「把 created 实现成一个全局标志」之类的偷懒写法。
func TestDefaultSessionPerWorkspace(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
a, createdA, err := FindOrCreateDefaultSessionCreated(ctx, "pi", "/w1", "jianf", "甲")
if err != nil || !createdA {
t.Fatalf("/w1 应新建:%v %v", createdA, err)
}
if _, err := CreateMail(ctx, a, nil, "jianf", "", "pi", "/w1", "甲", "x", nil); err != nil {
t.Fatal(err)
}
b, createdB, err := FindOrCreateDefaultSessionCreated(ctx, "pi", "/w2", "jianf", "乙")
if err != nil {
t.Fatal(err)
}
if !createdB {
t.Fatal("/w2 是另一个工作目录,应当另建一条默认会话")
}
if a == b {
t.Fatal("不同工作目录不该共用同一条默认会话")
}
}
// 旧签名仍在别处被调用,保持行为不变(只是丢掉 created
func TestLegacyWrapperStillWorks(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedAgent(t, "pi", 20)
id, err := FindOrCreateDefaultSession(ctx, "pi", "/w", "jianf", "首封")
if err != nil {
t.Fatal(err)
}
if _, err := CreateMail(ctx, id, nil, "jianf", "", "pi", "/w", "首封", "x", nil); err != nil {
t.Fatal(err)
}
again, err := FindOrCreateDefaultSession(ctx, "pi", "/w", "jianf", "第二封")
if err != nil {
t.Fatal(err)
}
if again != id {
t.Fatalf("包装函数应与原行为一致:%s vs %s", id, again)
}
}