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,119 @@
package repo
import (
"context"
"testing"
"github.com/agentmail/gateway/internal/db"
)
// 权限询问的决策者必须是人:Agent 收不到 SendToUser,桥的 await 永不 resolve。
// 生产事故:pi 把任务派给自己的另一条会话 → 那条会话要跑 bash → 权限邮件发给 "pi"
// → pi 不是人类用户 → 整条会话永久阻塞。
//
// 修法是顺着会话的邮件链上溯找最近的人类 —— 派活的人才是该点头的人。
func TestNearestHumanInThread(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
mustExec(t, ctx, `INSERT INTO users (username, display_name, password_hash, role)
VALUES ('alice','Alice','x','user')`)
for _, a := range []string{"opencode", "dsh", "pi"} {
if err := CreateOrUpdateAgent(ctx, a, "s", "test", nil); err != nil {
t.Fatalf("注册 %s: %v", a, err)
}
}
t.Run("沿链上溯找到派活的人", func(t *testing.T) {
sid, err := CreateSession(ctx, nil, "opencode", "任务链", "/home")
if err != nil {
t.Fatalf("建会话: %v", err)
}
// alice → opencode → dsh,dsh 触发权限询问
m1, err := CreateMail(ctx, sid, nil, "alice", "", "opencode", "", "任务", "请帮忙", nil)
if err != nil {
t.Fatalf("封1: %v", err)
}
m2, err := CreateMail(ctx, sid, &m1, "opencode", "", "dsh", "", "转派", "你来看", nil)
if err != nil {
t.Fatalf("封2: %v", err)
}
if _, err := CreateMail(ctx, sid, &m2, "dsh", "", "opencode", "", "进展", "做了一半", nil); err != nil {
t.Fatalf("封3: %v", err)
}
for _, agent := range []string{"dsh", "opencode"} {
got, err := NearestHumanInThread(ctx, sid, agent)
if err != nil {
t.Fatalf("%s: %v", agent, err)
}
if got != "alice" {
t.Errorf("%s 触发权限时应路由到 alice,得到 %q", agent, got)
}
}
})
t.Run("全 Agent 链返回空串", func(t *testing.T) {
// 没有人类参与的链条:调用方据此拒绝请求,而不是转给一个
// 对上下文一无所知的管理员。
sid, err := CreateSession(ctx, nil, "opencode", "纯 Agent", "/tmp")
if err != nil {
t.Fatalf("建会话: %v", err)
}
m1, err := CreateMail(ctx, sid, nil, "opencode", "", "dsh", "", "干活", "go", nil)
if err != nil {
t.Fatalf("封1: %v", err)
}
if _, err := CreateMail(ctx, sid, &m1, "dsh", "", "opencode", "", "好", "ok", nil); err != nil {
t.Fatalf("封2: %v", err)
}
got, err := NearestHumanInThread(ctx, sid, "dsh")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != "" {
t.Errorf("链上没有人类时应返回空串,得到 %q", got)
}
})
t.Run("skipAgent 是自己时不会把自己当人", func(t *testing.T) {
// pi 给自己的另一条会话派活正是事故场景:链上只有 pi 一个名字。
sid, err := CreateSession(ctx, nil, "pi", "自派", "/home")
if err != nil {
t.Fatalf("建会话: %v", err)
}
if _, err := CreateMail(ctx, sid, nil, "pi", "", "pi", "", "拆任务", "自己干", nil); err != nil {
t.Fatalf("封1: %v", err)
}
got, err := NearestHumanInThread(ctx, sid, "pi")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != "" {
t.Errorf("自派链上没有人类,应返回空串,得到 %q", got)
}
})
t.Run("空会话不报错", func(t *testing.T) {
sid, err := CreateSession(ctx, nil, "opencode", "空", "/tmp")
if err != nil {
t.Fatalf("建会话: %v", err)
}
got, err := NearestHumanInThread(ctx, sid, "opencode")
if err != nil {
t.Fatalf("空会话应返回空串而非报错,得到 err=%v", err)
}
if got != "" {
t.Errorf("空会话应返回空串,得到 %q", got)
}
})
}
func mustExec(t *testing.T, ctx context.Context, q string, args ...any) {
t.Helper()
if _, err := db.DB.ExecContext(ctx, q, args...); err != nil {
t.Fatalf("exec %s: %v", q, err)
}
}