chore: directory migration - gateway→server, web→client/electron

This commit is contained in:
2026-09-08 19:16:35 +08:00
parent fd9f99a3f9
commit f9d757b5e5
243 changed files with 5095 additions and 228 deletions

View File

@ -0,0 +1,126 @@
package repo
import (
"context"
"testing"
"github.com/google/uuid"
)
// 连续 relay 跳数上限守的是一个真实事故:会话 f3d824ce(dsh 与 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(期望 5)B=%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)
}
}