package repo import ( "context" "testing" "github.com/agentmail/gateway/internal/models" ) // 待决权限的**失效时刻**必须在每条读路径上都透出。 // // # 为什么值得钉一个测试 // // 2026-09-11 端到端实测:人在界面上批准了一条待办,接口回 200,但那件事什么都没 // 发生。原因是提出询问的桥在回合超时(默认 10 分钟)后拆掉了 worker 与它的决策 // 路由表,此后到达的决策只会作为通知投递给 Agent,不恢复当时那次工具调用。 // // 而数据库里那条 permission_requests 的 result 永远是 NULL —— 界面据此把它渲染成 // 「待决策」,于是一条早就没人等的待办会永远挂在授权页上,人点了也白点。 // // 服务端不必替插件宣布裁决(那是否还在等是插件进程内的状态),但它必须给出 // **这条待办什么时候算过期**,否则任何客户端都无从显示。这个测试锁的就是这件事: // 不是「某个字段存在于结构体」,而是「五个读函数都把它带出来了」—— // 漏掉任何一个读路径都不会报错,字段只会静默变成零值。 func TestPermissionDeadlineVisibleOnEveryReadPath(t *testing.T) { setupTestDB(t) ctx := context.Background() sid, err := CreateSession(ctx, nil, "agent-d", "等一个决策", "") if err != nil { t.Fatal(err) } pendingID, err := CreatePermissionMail(ctx, sid, "agent-d", "alice", "删除 build/", "rm -rf build/", []string{"同意", "拒绝"}, "permission", false) if err != nil { t.Fatal(err) } if err := CreatePermissionRequest(ctx, pendingID, sid, "agent-d", "删除 build/", []string{"同意", "拒绝"}, "rm -rf build/", "permission", false); err != nil { t.Fatal(err) } // 同一会话里再放一条**已决策**的待办:它不该带失效时刻 —— // 已决策的不再是待办,给它一个过期时间只会让界面把历史记录也标成过期。 settledID, err := CreatePermissionMail(ctx, sid, "agent-d", "alice", "读取配置", "cat cfg", []string{"同意", "拒绝"}, "permission", false) if err != nil { t.Fatal(err) } if err := CreatePermissionRequest(ctx, settledID, sid, "agent-d", "读取配置", []string{"同意", "拒绝"}, "cat cfg", "permission", false); err != nil { t.Fatal(err) } if _, err := DecidePermission(ctx, settledID, "alice", "同意"); err != nil { t.Fatal(err) } assertPending := func(what string, m *models.Mail) { t.Helper() if m == nil { t.Fatalf("%s 返回 nil", what) } if m.PermissionExpiresAt == nil { t.Fatalf("%s:待决权限没有失效时刻(字段没进该读路径时会是 nil,"+ "界面就只能永远显示「待决策」)", what) } want := models.PermissionDeadline(m.CreatedAt) if !m.PermissionExpiresAt.Equal(want) { t.Errorf("%s:失效时刻 = %v,期望 %v(= created_at + 等待窗口)", what, m.PermissionExpiresAt, want) } } // 1) GetMailByID —— 单封详情。 got, err := GetMailByID(ctx, pendingID) if err != nil { t.Fatal(err) } assertPending("GetMailByID", got) // 2) ListInbox —— 授权页列表的来源。 inbox, err := ListInbox(ctx, "alice", "all", 50) if err != nil { t.Fatal(err) } if !assertDeadlineIn(inbox, pendingID, t) { t.Error("ListInbox 未带出待决权限的失效时刻") } // 3) GetSessionMails —— 会话视图。 sessMails, err := GetSessionMails(ctx, sid) if err != nil { t.Fatal(err) } if !assertDeadlineIn(sessMails, pendingID, t) { t.Error("GetSessionMails 未带出待决权限的失效时刻") } // 4) GetSessionMailByID —— 会话内的单封。 one, err := GetSessionMailByID(ctx, sid, pendingID) if err != nil { t.Fatal(err) } assertPending("GetSessionMailByID", one) // 5) ListSentBy —— Agent 发出的那侧。 sent, err := ListSentBy(ctx, "agent-d", 50) if err != nil { t.Fatal(err) } if !assertDeadlineIn(sent, pendingID, t) { t.Error("ListSentBy 未带出待决权限的失效时刻") } // 反向断言:已决策的那条不该有失效时刻。 decided, err := GetMailByID(ctx, settledID) if err != nil { t.Fatal(err) } if decided.PermissionExpiresAt != nil { t.Errorf("已决策的权限邮件不该带失效时刻,实际 %v", decided.PermissionExpiresAt) } } // assertDeadlineIn 在列表里找那封邮件并校验失效时刻。 func assertDeadlineIn(mails []models.Mail, id interface{ String() string }, t *testing.T) bool { t.Helper() for i := range mails { if mails[i].ID.String() != id.String() { continue } m := &mails[i] if m.PermissionExpiresAt == nil { return false } return m.PermissionExpiresAt.Equal(models.PermissionDeadline(m.CreatedAt)) } t.Fatalf("列表里找不到邮件 %s", id) return false }