package handler import ( "fmt" "strings" "testing" "time" "github.com/agentmail/gateway/internal/models" ) // 越窗的决策必须如实告知「这次批准不会恢复原调用」。 // // # 为什么需要这条测试 // // 2026-09-11 端到端实测:人在界面上批准了一条待办,接口回 200,但那件事什么都没 // 发生 —— 提出询问的桥在回合超时(默认 10 分钟)后已拆掉 worker 与决策路由表, // 此后的决策只会作为通知投递给 Agent。接口当时照旧回 {"status":"decided"}, // 人无从知道自己的批准落到了一个已经没人等的请求上。 // // 这条分支只有等满等待窗口才会走到,属于「人工点一遍很难覆盖」的路径, // 所以把它抽成纯函数并在这里钉住两侧:窗口内不许误报,越窗必须报。 func TestDecideResponseFlagsExpiredRequest(t *testing.T) { now := time.Date(2026, 9, 11, 20, 30, 0, 0, time.UTC) const mailID = "decision-mail-id" t.Run("窗口内:不报过期,且不出现 warning", func(t *testing.T) { createdAt := now.Add(-2 * time.Minute) resp := decideResponse(createdAt, now, mailID) if resp["status"] != "decided" { t.Errorf("status = %v,期望 decided", resp["status"]) } if resp["decision_mail_id"] != mailID { t.Errorf("decision_mail_id = %v,期望 %s", resp["decision_mail_id"], mailID) } if _, ok := resp["expired"]; ok { t.Error("窗口内不该标 expired —— 误报会让人以为批准没生效,比不报更糟") } if _, ok := resp["warning"]; ok { t.Error("窗口内不该带 warning") } }) t.Run("刚好到截止时刻:不算过期(边界取严格大于)", func(t *testing.T) { createdAt := now.Add(-models.PermissionWaitWindow) resp := decideResponse(createdAt, now, mailID) if _, ok := resp["expired"]; ok { t.Error("恰好等于截止时刻时不该算过期") } }) t.Run("越窗:必须标 expired 并附 warning,且讲清『不会恢复原调用』", func(t *testing.T) { createdAt := now.Add(-1 * time.Hour) resp := decideResponse(createdAt, now, mailID) if resp["expired"] != true { t.Errorf("expired = %v,期望 true", resp["expired"]) } wantDeadline := models.PermissionDeadline(createdAt) if got, ok := resp["expires_at"].(time.Time); !ok || !got.Equal(wantDeadline) { t.Errorf("expires_at = %v,期望 %v", resp["expires_at"], wantDeadline) } warning, _ := resp["warning"].(string) if warning == "" { t.Fatal("越窗必须带 warning —— 否则就是又一次静默成功") } // 必须说清两件事:决策已记录(所以不用重试)、不会恢复原调用(所以别等它)。 for _, want := range []string{"不会恢复", "通知"} { if !strings.Contains(warning, want) { t.Errorf("warning 里应含 %q,实际 %q", want, warning) } } // 时长要由常量推导,不能写死,否则改了窗口这句话就成了假话。 wantNum := fmt.Sprintf("%.0f", models.PermissionWaitWindow.Minutes()) if !strings.Contains(warning, wantNum) { t.Errorf("warning 应提到等待窗口(%s 分钟),实际 %q", wantNum, warning) } }) } // 等待窗口必须与插件的回合超时同量级:窗口若短于插件真实等待时间, // 界面会把「其实还在等」的待办标成过期,同样是在说假话。 func TestPermissionWaitWindowIsSane(t *testing.T) { if models.PermissionWaitWindow < 1*time.Minute { t.Fatalf("等待窗口 %v 太短,会把仍在等待的待办误标为过期", models.PermissionWaitWindow) } if models.PermissionWaitWindow > 30*time.Minute { t.Fatalf("等待窗口 %v 过长,界面几乎不会提示过期,等于没做", models.PermissionWaitWindow) } }