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) } }