Files
ai-gateway-go/internal/workbench/inbox_test.go
T
LLMGuardX Dev 87c2b04174 0.11.3: 旗舰版第四轮完善(统一审批中心/工具治理/平台环境变量/数字员工入口/个人渠道/报表多维/租户配额)
- 统一审批中心:模型/资源/渠道/工具四类申请聚合审批,通过自动开通
  (marketplace 安装/渠道授权),outbox 双向站内信;门户可发起/撤回。
- 工具治理:rate_limit_rpm(固定窗口原子 upsert,多实例共享)+ approval_required
  (首次调用自动发起审批,批准前一律拒绝)。
- 平台环境变量:平台级注入 skill/MCP 运行时,个人可覆盖;系统管理员可写。
- 数字员工会话入口:门户列表/对话/调用记录,复用用户运行时凭据。
- 个人渠道:webhook 入站令牌 SHA-256 摘要 + constant-time 校验,绑定已批准
  模型,用量归属用户 Key。
- 报表多维:工具调用/审批授权/安全事件三组统计端点与页面。
- 租户配额:部门 Key/月 Token 上限,运行时凭据开通强制校验,概览展示用量。
- 迁移 000042-000045;修复渠道空 API Key NOT NULL 违约与 inet 扫描;
  25 包测试通过,前后端构建通过,端到端验证完成。
2026-08-13 13:41:22 +08:00

134 lines
7.7 KiB
Go

package workbench
import (
"encoding/json"
"testing"
)
// TestInboxPlanMapsEvents 覆盖 inboxPlan 纯函数:每个支持的事件类型都要产出
// 预期类别 / 收件人类别 / 文案关键词,未知事件返回 nil。
func TestInboxPlanMapsEvents(t *testing.T) {
payload := func(values map[string]any) json.RawMessage {
encoded, err := json.Marshal(values)
if err != nil {
t.Fatal(err)
}
return encoded
}
cases := []struct {
name string
eventType string
values map[string]any
wantKind string // recipient_kind
wantCategory string
wantTitle string
wantUserID string
wantAll bool
wantRequest bool
wantPref bool
}{
{name: "model_access.requested 通知全部管理员审批", eventType: "model_access.requested", values: map[string]any{"model": "gpt-5"}, wantKind: "admin", wantCategory: "approval", wantTitle: "新的模型访问申请", wantAll: true},
{name: "model_access.decided 已批准回执给申请用户", eventType: "model_access.decided", values: map[string]any{"status": "approved"}, wantKind: "portal", wantCategory: "approval", wantTitle: "模型申请已处理", wantRequest: true},
{name: "model_access.decided 已驳回文案", eventType: "model_access.decided", values: map[string]any{"status": "rejected"}, wantKind: "portal", wantCategory: "approval", wantTitle: "模型申请已处理", wantRequest: true},
{name: "marketplace.installed 发给安装用户", eventType: "marketplace.installed", values: map[string]any{"code": "report-bot", "portal_user_id": "11111111-1111-1111-1111-111111111111"}, wantKind: "portal", wantCategory: "resource", wantTitle: "资源已安装", wantUserID: "11111111-1111-1111-1111-111111111111"},
{name: "knowledge_document.ready 发给执行管理员", eventType: "knowledge_document.ready", values: map[string]any{"chunk_count": "12", "actor_id": "22222222-2222-2222-2222-222222222222"}, wantKind: "admin", wantCategory: "system", wantTitle: "知识文档已入库", wantUserID: "22222222-2222-2222-2222-222222222222"},
{name: "knowledge_document.reprocessed 发给执行管理员", eventType: "knowledge_document.reprocessed", values: map[string]any{"actor_id": "22222222-2222-2222-2222-222222222222"}, wantKind: "admin", wantCategory: "system", wantTitle: "知识文档已重新处理", wantUserID: "22222222-2222-2222-2222-222222222222"},
{name: "knowledge_document.embedding_failed 降级提示", eventType: "knowledge_document.embedding_failed", values: map[string]any{"actor_id": "22222222-2222-2222-2222-222222222222"}, wantKind: "admin", wantCategory: "system", wantTitle: "知识文档向量化失败", wantUserID: "22222222-2222-2222-2222-222222222222"},
{name: "scheduled_task.completed 发给创建者", eventType: "scheduled_task.completed", values: map[string]any{"task_code": "daily-report", "actor_id": "33333333-3333-3333-3333-333333333333"}, wantKind: "admin", wantCategory: "task_result", wantTitle: "定时任务已执行", wantUserID: "33333333-3333-3333-3333-333333333333"},
{name: "scheduled_task.failed 发给创建者", eventType: "scheduled_task.failed", values: map[string]any{"task_code": "daily-report", "error": "timeout", "actor_id": "33333333-3333-3333-3333-333333333333"}, wantKind: "admin", wantCategory: "task_result", wantTitle: "定时任务执行失败", wantUserID: "33333333-3333-3333-3333-333333333333"},
{name: "security.login_detected 发登录提醒且受偏好约束", eventType: "security.login_detected", values: map[string]any{"portal_user_id": "44444444-4444-4444-4444-444444444444", "ip": "203.0.113.7"}, wantKind: "portal", wantCategory: "security", wantTitle: "新设备登录提醒", wantUserID: "44444444-4444-4444-4444-444444444444", wantPref: true},
{name: "resource_access.requested 通知全部管理员审批", eventType: "resource_access.requested", values: map[string]any{"resource_type": "channel", "resource_code": "corp_wecom"}, wantKind: "admin", wantCategory: "approval", wantTitle: "新的资源权限申请", wantAll: true},
{name: "resource_access.decided 回执给申请用户", eventType: "resource_access.decided", values: map[string]any{"portal_user_id": "44444444-4444-4444-4444-444444444444", "resource_type": "skill", "resource_code": "sql-helper", "status": "approved"}, wantKind: "portal", wantCategory: "approval", wantTitle: "资源申请已处理", wantUserID: "44444444-4444-4444-4444-444444444444"},
{name: "tool_approval.requested 通知管理员审批工具", eventType: "tool_approval.requested", values: map[string]any{"tool_code": "shell_exec", "tool_id": "55555555-5555-5555-5555-555555555555"}, wantKind: "admin", wantCategory: "approval", wantTitle: "工具使用待审批", wantAll: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
drafts := inboxPlan(tc.eventType, payload(tc.values))
if len(drafts) != 1 {
t.Fatalf("expected exactly one draft, got %d", len(drafts))
}
draft := drafts[0]
if draft.RecipientKind != tc.wantKind {
t.Errorf("recipient_kind = %q, want %q", draft.RecipientKind, tc.wantKind)
}
if draft.Category != tc.wantCategory {
t.Errorf("category = %q, want %q", draft.Category, tc.wantCategory)
}
if draft.Title != tc.wantTitle {
t.Errorf("title = %q, want %q", draft.Title, tc.wantTitle)
}
if draft.UserID != tc.wantUserID {
t.Errorf("user_id = %q, want %q", draft.UserID, tc.wantUserID)
}
if draft.AllAdmins != tc.wantAll {
t.Errorf("all_admins = %v, want %v", draft.AllAdmins, tc.wantAll)
}
if draft.RequestUser != tc.wantRequest {
t.Errorf("request_user = %v, want %v", draft.RequestUser, tc.wantRequest)
}
if draft.NotifyPref != tc.wantPref {
t.Errorf("notify_pref = %v, want %v", draft.NotifyPref, tc.wantPref)
}
})
}
if drafts := inboxPlan("some.unknown.event", payload(map[string]any{})); drafts != nil {
t.Fatalf("unknown event should map to no drafts, got %+v", drafts)
}
}
// TestInboxPlanModelAccessRejectedBody 校验驳回与批准的不同正文文案。
func TestInboxPlanModelAccessRejectedBody(t *testing.T) {
values := func(status string) json.RawMessage {
encoded, _ := json.Marshal(map[string]any{"status": status})
return encoded
}
approved := inboxPlan("model_access.decided", values("approved"))
rejected := inboxPlan("model_access.decided", values("rejected"))
if !contains(approved[0].Body, "已批准") {
t.Errorf("approved body should mention 已批准, got %q", approved[0].Body)
}
if !contains(rejected[0].Body, "已驳回") {
t.Errorf("rejected body should mention 已驳回, got %q", rejected[0].Body)
}
}
func contains(haystack, needle string) bool {
for i := 0; i+len(needle) <= len(haystack); i++ {
if haystack[i:i+len(needle)] == needle {
return true
}
}
return false
}
// TestPayloadValue 校验 payloadValue 对字符串与数字两类取值的兼容(事件载荷
// 中数字可能被 JSON 解码为 float64)。
func TestPayloadValue(t *testing.T) {
payload := json.RawMessage(`{"model":"gpt-5","chunk_count":12,"active":true}`)
if got := payloadValue(payload, "model"); got != "gpt-5" {
t.Errorf("string key = %q, want gpt-5", got)
}
if got := payloadValue(payload, "chunk_count"); got != "12" {
t.Errorf("numeric key = %q, want 12", got)
}
if got := payloadValue(payload, "missing"); got != "" {
t.Errorf("missing key = %q, want empty", got)
}
}
func TestValidInboxLink(t *testing.T) {
cases := map[string]bool{
"": true, "/portal/inbox": true, "https://example.com/notice": true,
"http://example.com": true, "javascript:alert(1)": false,
"data:text/html,x": false, "//example.com/path": false, "portal/inbox": false,
}
for link, want := range cases {
if got := validInboxLink(link); got != want {
t.Errorf("validInboxLink(%q) = %v, want %v", link, got, want)
}
}
}