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

216 lines
6.4 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"
"time"
"github.com/agentmail/gateway/internal/models"
)
// Agent 只能看到自己建的日程。别人的日程里可能有它无权知道的会议与地址。
func TestListCalendarEventsCreatedBy(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
mine := seedEvent(t, &models.CalendarEvent{
Title: "pi 自己建的", EventTime: time.Now().Add(time.Hour), CreatedBy: "pi",
})
seedEvent(t, &models.CalendarEvent{
Title: "dsh 建的", EventTime: time.Now().Add(time.Hour), CreatedBy: "dsh",
})
seedEvent(t, &models.CalendarEvent{
Title: "人建的", EventTime: time.Now().Add(time.Hour), CreatedBy: "jianf",
})
from := time.Now().Add(-time.Hour)
to := time.Now().AddDate(0, 1, 0)
got, err := ListCalendarEventsCreatedBy(ctx, "pi", from, to, "active")
if err != nil {
t.Fatalf("列出: %v", err)
}
if len(got) != 1 {
t.Fatalf("pi 应只看到 1 条,得到 %d", len(got))
}
if got[0].EventID != mine.EventID {
t.Errorf("看到了别人的事件:%s", got[0].Title)
}
// 没建过任何事件的 Agent 得到空数组而不是 nilnil 序列化成 null 前端会崩)
empty, err := ListCalendarEventsCreatedBy(ctx, "opencode", from, to, "active")
if err != nil {
t.Fatalf("列出: %v", err)
}
if empty == nil {
t.Error("应返回空数组而不是 nil")
}
if len(empty) != 0 {
t.Errorf("应为空,得到 %d 条", len(empty))
}
}
// 「发给我但不是我建的」同样不返回:那些事件的编辑权不属于我,
// 列出来只会让模型试图改它然后拿到 404。
func TestListCalendarEventsCreatedByIgnoresRecipient(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedEvent(t, &models.CalendarEvent{
Title: "dsh 建的、发给 pi",
EventTime: time.Now().Add(time.Hour),
CreatedBy: "dsh",
Recipients: []string{"pi"},
})
got, err := ListCalendarEventsCreatedBy(ctx, "pi",
time.Now().Add(-time.Hour), time.Now().AddDate(0, 1, 0), "active")
if err != nil {
t.Fatalf("列出: %v", err)
}
if len(got) != 0 {
t.Errorf("收件人不等于创建者,不该出现在列表里(得到 %d 条)", len(got))
}
}
func TestListCalendarEventsCreatedByStatusFilter(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
seedEvent(t, &models.CalendarEvent{
Title: "生效中", EventTime: time.Now().Add(time.Hour), CreatedBy: "pi", Status: "active",
})
seedEvent(t, &models.CalendarEvent{
Title: "已暂停", EventTime: time.Now().Add(time.Hour), CreatedBy: "pi", Status: "paused",
})
from := time.Now().Add(-time.Hour)
to := time.Now().AddDate(0, 1, 0)
if got, _ := ListCalendarEventsCreatedBy(ctx, "pi", from, to, "active"); len(got) != 1 {
t.Errorf("active 过滤应给 1 条,得到 %d", len(got))
}
// 空串 = 不过滤handler 里 status=all 映射成空串)
if got, _ := ListCalendarEventsCreatedBy(ctx, "pi", from, to, ""); len(got) != 2 {
t.Errorf("不过滤应给 2 条,得到 %d", len(got))
}
}
// 总量上限的依据。速率限制压不住「每小时建 19 条连建一周」,
// 而日历事件是长效的 —— 攒下来的每条都持续产生提醒。
func TestCountActiveEventsBy(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
for i := 0; i < 3; i++ {
seedEvent(t, &models.CalendarEvent{
Title: "生效", EventTime: time.Now().Add(time.Hour), CreatedBy: "pi", Status: "active",
})
}
// cancelled 与 paused 不该计入「生效中」
seedEvent(t, &models.CalendarEvent{
Title: "取消了", EventTime: time.Now().Add(time.Hour), CreatedBy: "pi", Status: "cancelled",
})
seedEvent(t, &models.CalendarEvent{
Title: "暂停了", EventTime: time.Now().Add(time.Hour), CreatedBy: "pi", Status: "paused",
})
seedEvent(t, &models.CalendarEvent{
Title: "别人的", EventTime: time.Now().Add(time.Hour), CreatedBy: "dsh", Status: "active",
})
n, err := CountActiveEventsBy(ctx, "pi")
if err != nil {
t.Fatalf("计数: %v", err)
}
if n != 3 {
t.Errorf("pi 的生效事件应为 3得到 %d", n)
}
if n, _ := CountActiveEventsBy(ctx, "从来没建过"); n != 0 {
t.Errorf("没建过应为 0得到 %d", n)
}
}
// ─── 速率限制 ───
func TestAllowAgentCalendarEvent(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
limit := CalendarRateLimit()
for i := 0; i < limit; i++ {
ok, _ := AllowAgentCalendarEvent(ctx, "pi")
if !ok {
t.Fatalf("第 %d 次(上限 %d就被拒了", i+1, limit)
}
}
ok, retry := AllowAgentCalendarEvent(ctx, "pi")
if ok {
t.Error("超过上限应被拒")
}
if retry <= 0 {
t.Errorf("被拒时应给出 retryAfter得到 %d", retry)
}
}
// 日历桶与新建会话桶必须独立:建满 20 条日程不该连带堵住新建会话。
func TestCalendarRateBucketIsSeparateFromSession(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
for i := 0; i < CalendarRateLimit(); i++ {
AllowAgentCalendarEvent(ctx, "pi")
}
if ok, _ := AllowAgentCalendarEvent(ctx, "pi"); ok {
t.Fatal("准备阶段:日历桶应已满")
}
// 新建会话桶应完全不受影响
if ok, _ := AllowNewSession(ctx, "pi"); !ok {
t.Error("日历桶满不该堵住新建会话 —— 两个桶必须独立")
}
}
// 不同 Agent 的桶互不干扰。
func TestCalendarRateBucketPerAgent(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
for i := 0; i < CalendarRateLimit(); i++ {
AllowAgentCalendarEvent(ctx, "pi")
}
if ok, _ := AllowAgentCalendarEvent(ctx, "dsh"); !ok {
t.Error("pi 建满不该影响 dsh")
}
}
// 创建失败要归还名额:那次创建实际没有发生。
func TestReleaseAgentCalendarEvent(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
for i := 0; i < CalendarRateLimit(); i++ {
AllowAgentCalendarEvent(ctx, "pi")
}
if ok, _ := AllowAgentCalendarEvent(ctx, "pi"); ok {
t.Fatal("准备阶段:应已满")
}
// 归还一个(模拟刚才那次被拒之前的失败创建)
ReleaseAgentCalendarEvent(ctx, "pi")
if ok, _ := AllowAgentCalendarEvent(ctx, "pi"); !ok {
t.Error("归还名额后应能再建一条")
}
}
// 空 Agent 名放行且不记账:这条路径只在鉴权已经失败时才可能走到,
// 记账会污染桶bucket 名变成 "calendar:")。
func TestCalendarRateEmptyAgentPassesThrough(t *testing.T) {
setupTestDB(t)
ctx := context.Background()
for i := 0; i < CalendarRateLimit()+5; i++ {
if ok, _ := AllowAgentCalendarEvent(ctx, ""); !ok {
t.Fatal("空 Agent 名应一律放行")
}
}
}