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

127 lines
3.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"
"testing"
"github.com/google/uuid"
)
// 连续 relay 跳数上限守的是一个真实事故:会话 f3d824cedsh 与 opencode 联调
// llmsproxy共 41 封,最后一封人类意图的邮件之后每一封都是 relay:summary
// 间隔从 15 分钟一路缩到 5 秒。双方都没有「决定继续」,因为双方都不在做决定 ——
// 发信是插件代劳的,而免配额通道让整个回路里没有任何一处在计数。
func TestTrailingRelayHopsEmptySession(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
sid, _ := CreateSession(ctx, nil, "admin", "空会话", "/tmp/ws")
n, err := CountTrailingRelayHops(ctx, sid)
if err != nil {
t.Fatalf("空会话应正常返回: %v", err)
}
if n != 0 {
t.Fatalf("空会话跳数应为 0实为 %d", n)
}
}
func TestTrailingRelayHopsCountsOnlyRelay(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
sid, _ := CreateSession(ctx, nil, "admin", "混合", "/tmp/ws")
// 人类发一封(非 relay
mustMail(t, sid, "admin", "", "dsh", "/tmp/ws", nil)
// 插件连续转发三封
for i := 0; i < 3; i++ {
seedRelayMail(t, ctx, sid, "dsh", "opencode")
}
n, err := CountTrailingRelayHops(ctx, sid)
if err != nil {
t.Fatalf("数跳数: %v", err)
}
if n != 3 {
t.Fatalf("尾部连续 relay 应为 3实为 %d", n)
}
}
func TestTrailingRelayHopsResetsOnAutonomousSend(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
// 「连续」是这条规则的关键:中间只要有一封自主发信(模型真的决定说什么)
// 或人类插话,计数就归零。否则正常的「模型回一封、插件补一封总结」
// 会被误判成回路。
sid, _ := CreateSession(ctx, nil, "admin", "打断", "/tmp/ws")
for i := 0; i < 4; i++ {
seedRelayMail(t, ctx, sid, "dsh", "opencode")
}
// 模型亲手发了一封 —— 链条到此为止
mustMail(t, sid, "dsh", "", "opencode", "/tmp/ws", nil)
seedRelayMail(t, ctx, sid, "opencode", "dsh")
n, err := CountTrailingRelayHops(ctx, sid)
if err != nil {
t.Fatalf("数跳数: %v", err)
}
if n != 1 {
t.Fatalf("自主发信之后只剩 1 跳,实为 %d", n)
}
}
func TestTrailingRelayHopsReachesLimit(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
// 攒到上限:此时 handler 应当拒绝下一封 relay。
sid, _ := CreateSession(ctx, nil, "admin", "到顶", "/tmp/ws")
mustMail(t, sid, "admin", "", "dsh", "/tmp/ws", nil)
for i := 0; i < MaxRelayHops(); i++ {
seedRelayMail(t, ctx, sid, "dsh", "opencode")
}
n, _ := CountTrailingRelayHops(ctx, sid)
if n < MaxRelayHops() {
t.Fatalf("应达到上限 %d实为 %d", MaxRelayHops(), n)
}
}
func TestTrailingRelayHopsIsPerSession(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
// 一条会话的回路不该影响另一条:两个 Agent 在 A 会话里刷爆了,
// B 会话的正常自动转发仍应放行。
a, _ := CreateSession(ctx, nil, "admin", "A", "/tmp/a")
b, _ := CreateSession(ctx, nil, "admin", "B", "/tmp/b")
for i := 0; i < 5; i++ {
seedRelayMail(t, ctx, a, "dsh", "opencode")
}
seedRelayMail(t, ctx, b, "dsh", "admin")
na, _ := CountTrailingRelayHops(ctx, a)
nb, _ := CountTrailingRelayHops(ctx, b)
if na != 5 || nb != 1 {
t.Fatalf("跳数应按会话独立计A=%d期望 5B=%d期望 1", na, nb)
}
}
// seedRelayMail 建一封走 relay 通道的邮件(同时占幂等键并关联 mail_id
// 复刻 handler.SendMail 的真实写入顺序。
func seedRelayMail(t *testing.T, ctx context.Context, sid uuid.UUID, from, to string) {
t.Helper()
key := "relay-" + uuid.NewString()
if err := ClaimRelay(ctx, from, key, "summary"); err != nil {
t.Fatalf("占幂等键: %v", err)
}
mid, err := CreateMail(ctx, sid, nil, from, "", to, "", "Re: 主题", "正文", nil)
if err != nil {
t.Fatalf("建邮件: %v", err)
}
if err := BindRelayMail(ctx, from, key, mid); err != nil {
t.Fatalf("关联 relay: %v", err)
}
}