Files
MailUI4Agents/gateway/internal/models/permission_mode_test.go
JianFeeeee a44fd6949b feat: 权限档位体系(三档 plan/workspace/full + 四桥 from_session_id)
L2 核心改动:sessions 表补 permission_mode / permission_enforcement 两列
(sqlite + pg 同步),三桥 lib/permission-mode.js 翻译档位到平台原生配置,
homeagent advisory 模式提示词告知模型实际强制力。四桥全部携带 from_session_id
供 relay 去重与会话回溯。

FromHuman / ToHuman 判据已加入心跳 payload 与 notify/mail.go。
2026-09-06 15:16:49 +08:00

161 lines
6.1 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 models
// 权限档位的判据测试。
//
// 为什么值得单独一组测试:`ModeAtMost` 被两处调用(子会话继承 / 平台向更严取整),
// 两处若各写一遍必有一处写成「取更宽松」。而 `NormalizePermissionMode` 的保守
// 取向(非法值 → workspace 而非 full是安全属性拼错一个档位名不该换来更大权限。
import "testing"
func TestValidPermissionMode(t *testing.T) {
for _, m := range []string{ModePlan, ModeWorkspace, ModeFull} {
if !ValidPermissionMode(m) {
t.Fatalf("%q 应当合法", m)
}
}
for _, m := range []string{"", "PLAN", "readonly", "danger-full-access", "workspace-write"} {
if ValidPermissionMode(m) {
t.Fatalf("%q 不该合法", m)
}
}
}
// 非法值必须落到 workspace不能落到 full。
// 拼错一个档位名换来全权是最不该有的失败方向。
func TestNormalizePermissionMode_FailsClosed(t *testing.T) {
for _, in := range []string{"", "full-access", "plan ", "FULL", "无", "workspace-write"} {
got := NormalizePermissionMode(in)
if got != DefaultPermissionMode {
t.Fatalf("NormalizePermissionMode(%q) = %q应当是默认档 %q", in, got, DefaultPermissionMode)
}
}
if DefaultPermissionMode == ModeFull {
t.Fatal("默认档不能是 full —— 「我忘了收紧」与「我确实需要全权」会无法区分")
}
}
func TestNormalizePermissionMode_KeepsValid(t *testing.T) {
for _, m := range []string{ModePlan, ModeWorkspace, ModeFull} {
if got := NormalizePermissionMode(m); got != m {
t.Fatalf("合法档位应原样返回:%q → %q", m, got)
}
}
}
// ModeAtMost 取更严的一档 —— 子会话继承与平台取整共用这一个判据。
func TestModeAtMost(t *testing.T) {
cases := []struct{ a, b, want string }{
{ModePlan, ModeFull, ModePlan},
{ModeFull, ModePlan, ModePlan},
{ModeWorkspace, ModeFull, ModeWorkspace},
{ModeFull, ModeWorkspace, ModeWorkspace},
{ModePlan, ModeWorkspace, ModePlan},
{ModeWorkspace, ModePlan, ModePlan},
{ModeFull, ModeFull, ModeFull},
{ModePlan, ModePlan, ModePlan},
{ModeWorkspace, ModeWorkspace, ModeWorkspace},
}
for _, c := range cases {
if got := ModeAtMost(c.a, c.b); got != c.want {
t.Fatalf("ModeAtMost(%q,%q) = %qwant %q", c.a, c.b, got, c.want)
}
}
}
// 未知值归到默认档workspace而不是最严的 plan。
//
// 为什么不是 plan脏数据的含义应该在整个包里只有一个 ——
// NormalizePermissionMode / ModeNeedsHuman 都把它当默认档ModeAtMost
// 若单独把它当 plan同一个脏值就有两种语义且 ModeAtMost 不可交换
// (单元测试当场抓到过)。一致比“局部更严”重要:默认档本身已经是安全的。
func TestModeAtMost_UnknownFallsToDefault(t *testing.T) {
if got := ModeAtMost("garbage", ModeFull); got != DefaultPermissionMode {
t.Fatalf("未知档位应归默认档,得到 %q", got)
}
if got := ModeAtMost(ModeFull, "garbage"); got != DefaultPermissionMode {
t.Fatalf("未知档位应归默认档,得到 %q", got)
}
// 脏值不得抬升权限:与 plan 相遇时仍然是 plan 胜出。
if got := ModeAtMost("garbage", ModePlan); got != ModePlan {
t.Fatalf("脏值不该把 plan 抬成更宽松的档,得到 %q", got)
}
}
// ModeAtMost 必须可交换:两处调用点传参顺序不同,结果不能不同。
func TestModeAtMost_Commutative(t *testing.T) {
all := append([]string{"garbage", ""}, PermissionModes...)
for _, a := range all {
for _, b := range all {
if ModeAtMost(a, b) != ModeAtMost(b, a) {
t.Fatalf("ModeAtMost 不可交换:(%q,%q)=%q 但 (%q,%q)=%q",
a, b, ModeAtMost(a, b), b, a, ModeAtMost(b, a))
}
}
}
}
// 只有 workspace 档需要人 —— 这一条直接决定「找不到人类时怎么办」。
//
// plan 档当场拒绝、full 档自动放行,两者都不问人,所以只有 workspace 档会
// 走到「这条链上有没有人类」这个问题,找不到就是 409。permission.go 里那段
// 「退回第一个管理员」的兜底正因此必须删掉:它让 409 分支永远不可达。
func TestModeNeedsHuman(t *testing.T) {
if ModeNeedsHuman(ModePlan) {
t.Fatal("plan 档不该问人:语义就是这轮不动手,直接拒绝即可")
}
if !ModeNeedsHuman(ModeWorkspace) {
t.Fatal("workspace 档必须问人:越界时需要人点头")
}
if ModeNeedsHuman(ModeFull) {
t.Fatal("full 档不该问人:已声明全权,再问一遍只是噪音")
}
}
func TestModeNeedsHuman_NormalizesInput(t *testing.T) {
// 脏数据走默认档workspace→ 需要人。宁可多问一次,不可静默放行。
if !ModeNeedsHuman("garbage") {
t.Fatal("认不出的档位应当按默认档处理,即需要人")
}
if !ModeNeedsHuman("") {
t.Fatal("空档位应当按默认档处理,即需要人")
}
}
// ─── 强制力 ───
func TestValidEnforcement(t *testing.T) {
if !ValidEnforcement(EnforcementNative) || !ValidEnforcement(EnforcementAdvisory) {
t.Fatal("native / advisory 都应合法")
}
for _, e := range []string{"", "NATIVE", "none", "enforced"} {
if ValidEnforcement(e) {
t.Fatalf("%q 不该合法", e)
}
}
}
// 保守方向是 advisory没自报过的插件不能替它宣称档位在那里是被强制的。
func TestNormalizeEnforcement_FailsClosed(t *testing.T) {
for _, in := range []string{"", "garbage", "NATIVE", "native "} {
if got := NormalizeEnforcement(in); got != EnforcementAdvisory {
t.Fatalf("NormalizeEnforcement(%q) = %q应当是 advisory", in, got)
}
}
if got := NormalizeEnforcement(EnforcementNative); got != EnforcementNative {
t.Fatalf("显式 native 应原样保留,得到 %q", got)
}
}
// PermissionModes 的顺序是 ModeAtMost 的依据,不能被随手改动。
func TestPermissionModesOrder(t *testing.T) {
if len(PermissionModes) != 3 {
t.Fatalf("档位应当是三个,得到 %d 个", len(PermissionModes))
}
if PermissionModes[0] != ModePlan ||
PermissionModes[1] != ModeWorkspace ||
PermissionModes[2] != ModeFull {
t.Fatalf("PermissionModes 必须按宽松程度递增排列plan < workspace < full得到 %v", PermissionModes)
}
}