package repo import ( "context" "testing" "github.com/agentmail/gateway/internal/models" "github.com/google/uuid" ) // ─── InheritedMode:继承、收紧、不存在的父会话 ─── // parent nil → 返回 requested 的规范化值 func TestInheritedMode_NilParent(t *testing.T) { setupTestDB(t) ctx := context.Background() got := InheritedMode(ctx, nil, "plan") if got != models.ModePlan { t.Errorf("nil parent + plan: got %q, want plan", got) } got = InheritedMode(ctx, nil, "workspace") if got != models.ModeWorkspace { t.Errorf("nil parent + workspace: got %q, want workspace", got) } got = InheritedMode(ctx, nil, "full") if got != models.ModeFull { t.Errorf("nil parent + full: got %q, want full", got) } } // parent plan → 子会话只能 plan(不能提权) func TestInheritedMode_ParentPlan_Tightens(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) id := createTestSession(t, ctx, "pi", "/ws") perm, err := SetSessionPermissionMode(ctx, id, models.ModePlan) if err != nil { t.Fatal(err) } if perm.Mode != models.ModePlan { t.Fatal("expected plan") } // 请求 workspace(更宽松)→ 应被收紧为 plan got := InheritedMode(ctx, &id, models.ModeWorkspace) if got != models.ModePlan { t.Errorf("plan parent + workspace request: got %q, want plan", got) } // 请求 full → 同样收紧 got = InheritedMode(ctx, &id, models.ModeFull) if got != models.ModePlan { t.Errorf("plan parent + full request: got %q, want plan", got) } // 请求 plan → 保持 plan got = InheritedMode(ctx, &id, models.ModePlan) if got != models.ModePlan { t.Errorf("plan parent + plan request: got %q, want plan", got) } } // parent workspace → 子会话 workspace 或更严 func TestInheritedMode_ParentWorkspace(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) id := createTestSession(t, ctx, "pi", "/ws") _, err := SetSessionPermissionMode(ctx, id, models.ModeWorkspace) if err != nil { t.Fatal(err) } // 请求 full → 收紧为 workspace(子不能比父更松) got := InheritedMode(ctx, &id, models.ModeFull) if got != models.ModeWorkspace { t.Errorf("workspace parent + full: got %q, want workspace", got) } // 请求 plan → 保留 plan(比父更严,允许) got = InheritedMode(ctx, &id, models.ModePlan) if got != models.ModePlan { t.Errorf("workspace parent + plan: got %q, want plan", got) } } // parent full → 子会话可请求任意档位 func TestInheritedMode_ParentFull(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) id := createTestSession(t, ctx, "pi", "/ws") _, err := SetSessionPermissionMode(ctx, id, models.ModeFull) if err != nil { t.Fatal(err) } got := InheritedMode(ctx, &id, models.ModeWorkspace) if got != models.ModeWorkspace { t.Errorf("full parent + workspace: got %q, want workspace", got) } got = InheritedMode(ctx, &id, models.ModePlan) if got != models.ModePlan { t.Errorf("full parent + plan: got %q, want plan", got) } } // 脏值 fallback:非法的 requested 值在 InheritedMode 里被规范化为默认档 func TestInheritedMode_InvalidRequested(t *testing.T) { setupTestDB(t) ctx := context.Background() got := InheritedMode(ctx, nil, "elephant") if got != models.DefaultPermissionMode { t.Errorf("invalid requested: got %q, want %q", got, models.DefaultPermissionMode) } } // parent 不存在时(查不到)回落:ModeAtMost(default, req) func TestInheritedMode_InvalidParent(t *testing.T) { setupTestDB(t) ctx := context.Background() fakeID := uuid.New() got := InheritedMode(ctx, &fakeID, "full") want := models.ModeAtMost(models.DefaultPermissionMode, "full") if got != want { t.Errorf("invalid parent + full: got %q, want %q (modeAtMost(default, full))", got, want) } } // ─── SetSessionPermissionMode roundtrip + dirty value normalization ─── func TestSetSessionPermissionMode_Roundtrip(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "dsh", 20) id := createTestSession(t, ctx, "dsh", "/ws") perm, err := SetSessionPermissionMode(ctx, id, models.ModeFull) if err != nil { t.Fatal(err) } if perm.Mode != models.ModeFull || perm.Enforcement != "advisory" { t.Errorf("full mode: got mode=%q enforcement=%q", perm.Mode, perm.Enforcement) } // 读出来一致 got := SessionPermissionMode(ctx, id) if got != models.ModeFull { t.Errorf("read back: got %q, want full", got) } } func TestSetSessionPermissionMode_DirtyValue_FailClosed(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) id := createTestSession(t, ctx, "pi", "/ws") perm, err := SetSessionPermissionMode(ctx, id, "BOGUS") if err != nil { t.Fatal(err) } if perm.Mode != models.DefaultPermissionMode { t.Errorf("dirty value: got %q, want %q (fail-closed to default)", perm.Mode, models.DefaultPermissionMode) } } // ─── 脏值归一化测试(覆盖 NormalizePermissionMode 本身) ─── func TestNormalizePermissionMode_Inputs(t *testing.T) { tests := []struct { input string want string }{ {"plan", "plan"}, {"workspace", "workspace"}, {"full", "full"}, // 大小写/空白不归一:NormalizePermissionMode 只接受精确匹配的合法档位, // 其余一律 fail-closed 到默认档(workspace)—— 不 trim 不 lowercase, // 避免「我以为给了 plan 实际拿到别的」这种隐式转换造成的安全错觉。 {"Plan", models.DefaultPermissionMode}, {" PLAN ", models.DefaultPermissionMode}, {"", models.DefaultPermissionMode}, {"bogus", models.DefaultPermissionMode}, {"F ULL", models.DefaultPermissionMode}, } for _, tt := range tests { got := models.NormalizePermissionMode(tt.input) if got != tt.want { t.Errorf("NormalizePermissionMode(%q) = %q, want %q", tt.input, got, tt.want) } } } // ─── 负向对照:plan 档不能提权 ─── func TestInheritedMode_PlanCannotEscalate(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) parent := createTestSession(t, ctx, "pi", "/ws") _, _ = SetSessionPermissionMode(ctx, parent, models.ModePlan) child := InheritedMode(ctx, &parent, models.ModeFull) if child != models.ModePlan { t.Errorf("SECURITY FAIL: plan session escalated to %q via InheritedMode", child) } } // ─── 三层继承链 ─── func TestInheritedMode_ThreeLevelChain(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) root := createTestSession(t, ctx, "pi", "/ws") _, _ = SetSessionPermissionMode(ctx, root, models.ModeFull) child := InheritedMode(ctx, &root, models.ModeWorkspace) // workspace < full → workspace childID := createTestSession(t, ctx, "pi", "/ws") _, _ = SetSessionPermissionMode(ctx, childID, child) grandchild := InheritedMode(ctx, &childID, models.ModeFull) // full vs workspace → workspace if grandchild != models.ModeWorkspace { t.Errorf("grandchild: got %q, want workspace", grandchild) } // plan → workspace → plan chain planChild := InheritedMode(ctx, &root, models.ModePlan) // plan < full → plan planChildID := createTestSession(t, ctx, "pi", "/ws") _, _ = SetSessionPermissionMode(ctx, planChildID, planChild) grandchild2 := InheritedMode(ctx, &planChildID, models.ModeFull) // full vs plan → plan if grandchild2 != models.ModePlan { t.Errorf("plan chain grandchild: got %q, want plan", grandchild2) } } // ─── calendar_events permission_mode roundtrip ─── func TestCalendarEventPermissionMode_Roundtrip(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) // CreateCalendarEvent 应规范化档位 e := &models.CalendarEvent{ Title: "测试日程", AgentName: "pi", ToAddress: "pi@/home/program/agentmail", PermissionMode: "full", Status: "active", CreatedBy: "jianf", } created, err := CreateCalendarEvent(ctx, e) if err != nil { t.Fatal(err) } if created.PermissionMode != "full" { t.Errorf("created event permission_mode: got %q, want full", created.PermissionMode) } // 读回来一致 got, err := GetCalendarEvent(ctx, created.EventID) if err != nil { t.Fatal(err) } if got.PermissionMode != "full" { t.Errorf("read back: got %q, want full", got.PermissionMode) } } func TestCalendarEventPermissionMode_DirtyValue_Normalized(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) e := &models.CalendarEvent{ Title: "脏值日程", AgentName: "pi", ToAddress: "pi@/home/program/agentmail", PermissionMode: "INVALID", Status: "active", CreatedBy: "jianf", } created, err := CreateCalendarEvent(ctx, e) if err != nil { t.Fatal(err) } if created.PermissionMode != models.DefaultPermissionMode { t.Errorf("dirty value: got %q, want %q", created.PermissionMode, models.DefaultPermissionMode) } } func TestCalendarEventPermissionMode_UpdateRoundtrip(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) e := &models.CalendarEvent{ Title: "更新日程", AgentName: "pi", ToAddress: "pi@/home/program/agentmail", PermissionMode: "workspace", Status: "active", CreatedBy: "jianf", } created, err := CreateCalendarEvent(ctx, e) if err != nil { t.Fatal(err) } created.PermissionMode = "plan" if err := UpdateCalendarEvent(ctx, created.EventID, created); err != nil { t.Fatal(err) } got, err := GetCalendarEvent(ctx, created.EventID) if err != nil { t.Fatal(err) } if got.PermissionMode != "plan" { t.Errorf("after update: got %q, want plan", got.PermissionMode) } } // ─── adopt 接管时会话档位必须写入 ─── func TestAdoptPlatformSession_WritesDefaultMode(t *testing.T) { setupTestDB(t) ctx := context.Background() seedAgent(t, "pi", 20) id, err := AdoptPlatformSession(ctx, "pi", "plat-123", "my-proj", "/ws", "接管测试") if err != nil { t.Fatal(err) } // 接管会话应显式写入默认档位(不是靠 DB 默认值) mode := SessionPermissionMode(ctx, id) if mode != models.DefaultPermissionMode { t.Errorf("adopt session mode: got %q, want %q", mode, models.DefaultPermissionMode) } } // ─── helpers ─── func createTestSession(t *testing.T, ctx context.Context, agent, workspace string) uuid.UUID { t.Helper() id, err := CreateSession(ctx, nil, agent, "test subject", workspace) if err != nil { t.Fatalf("create session: %v", err) } return id } // init 确保每个 test 函数执行前 DB 足够干净 func init() { // 空 —— setupTestDB 在每个测试函数内调用 }