L2: permission mode propagation across forward/calendar/adopt + PUT endpoint + tests
P1 - forward inherits mode:
- doForward: when creating a new target session, use InheritedMode from
the source session (plan parent → plan child, cannot escalate)
- enforcement snapshot set from receiving agent
P1 - calendar_events gets permission_mode column:
- Added to 3 migration sites (sqlite init, pg init, incremental alter)
- CalendarEvent model gains PermissionMode field
- CreateCalendarEvent / UpdateCalendarEvent normalize + persist the field
- resolveCalendarSession: on new session → write event's mode;
on reuse → ModeAtMost(cur, eventMode), prevents escalation
(plan Agent's reminder fires into a workspace session = bypass)
- SendCalendarMail now takes permMode and threads it through
- fireEvent passes event.PermissionMode to all delivery paths
P1 - AdoptPlatformSession explicitly writes default mode:
- Writes DefaultPermissionMode + enforcement on adopt, instead of
relying on DB column default (avoids silent drift on schema changes)
P2 - PUT /sessions/{id}/permission endpoint:
- New handler UpdateSessionPermission (auth required, access check)
- Registers PUT route alongside existing budget/alias endpoints
- Broadcasts session_update on change
- Does NOT refresh enforcement (design: snapshot at creation)
P3 - Tests (24 new cases):
- permission_mode_test.go: InheritedMode (6 cases), SetSessionPermissionMode
roundtrip, dirty value fail-closed, NormalizePermissionMode, calendar event
roundtrip/dirty/update, adopt writes default mode, plan escalation guard,
3-level inheritance chain
- defaultsession_test.go: budget regression, permission mode regression
(pins created=false → no reset on reuse)
Deploys with: bash deploy/redeploy-gateway.sh --skip-tests
Schema migration: auto via addMissingColumns (new column default 'workspace')
This commit is contained in:
@ -25,7 +25,7 @@ var ErrEventNotFound = errors.New("calendar event not found")
|
||||
// 加了预算两列没加进 Scan,整个联系人栏 500)。
|
||||
const calendarCols = `event_id, title, description, reminder_text, agent_name, to_address,
|
||||
recipients, delivery_mode, event_time, remind_before, recurrence, recurrence_end,
|
||||
status, last_fired_at, fired_for, created_at, updated_at, created_by`
|
||||
status, last_fired_at, fired_for, permission_mode, created_at, updated_at, created_by`
|
||||
|
||||
// rowScanner 让 QueryRow 与 Rows 共用同一个 scan 实现。
|
||||
type rowScanner interface {
|
||||
@ -44,7 +44,8 @@ func scanCalendarEvent(sc rowScanner) (*models.CalendarEvent, error) {
|
||||
&e.AgentName, &e.ToAddress,
|
||||
&recipientsJSON, &e.DeliveryMode,
|
||||
&e.EventTime, &e.RemindBefore, &e.Recurrence, &e.RecurrenceEnd,
|
||||
&e.Status, &e.LastFiredAt, &e.FiredFor, &e.CreatedAt, &e.UpdatedAt, &e.CreatedBy,
|
||||
&e.Status, &e.LastFiredAt, &e.FiredFor, &e.PermissionMode,
|
||||
&e.CreatedAt, &e.UpdatedAt, &e.CreatedBy,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -88,6 +89,10 @@ func CreateCalendarEvent(ctx context.Context, e *models.CalendarEvent) (*models.
|
||||
if e.DeliveryMode == "" {
|
||||
e.DeliveryMode = models.DeliverSeparate
|
||||
}
|
||||
// 档位合法化:脏值 fail-closed 到默认档,不透传成库里的非法值
|
||||
//(否则后续读路径会拿到一个 ModeNeedsHuman 判定不了的值)。
|
||||
e.PermissionMode = models.NormalizePermissionMode(e.PermissionMode)
|
||||
|
||||
if e.Recipients == nil {
|
||||
e.Recipients = []string{}
|
||||
}
|
||||
@ -96,14 +101,14 @@ func CreateCalendarEvent(ctx context.Context, e *models.CalendarEvent) (*models.
|
||||
INSERT INTO calendar_events
|
||||
(event_id, title, description, reminder_text, agent_name, to_address,
|
||||
recipients, delivery_mode,
|
||||
event_time, remind_before, recurrence, recurrence_end, status, created_by,
|
||||
event_time, remind_before, recurrence, recurrence_end, status, permission_mode, created_by,
|
||||
created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
e.EventID, e.Title, e.Description, e.ReminderText,
|
||||
e.AgentName, e.ToAddress,
|
||||
marshalRecipients(e.Recipients), e.DeliveryMode,
|
||||
e.EventTime, e.RemindBefore, e.Recurrence, e.RecurrenceEnd,
|
||||
e.Status, e.CreatedBy, e.CreatedAt, e.UpdatedAt,
|
||||
e.Status, e.PermissionMode, e.CreatedBy, e.CreatedAt, e.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -125,19 +130,21 @@ func GetCalendarEvent(ctx context.Context, eventID string) (*models.CalendarEven
|
||||
|
||||
func UpdateCalendarEvent(ctx context.Context, eventID string, e *models.CalendarEvent) error {
|
||||
e.UpdatedAt = time.Now()
|
||||
// 档位同样在 update 路径上规范化
|
||||
e.PermissionMode = models.NormalizePermissionMode(e.PermissionMode)
|
||||
result, err := db.DB.ExecContext(ctx, `
|
||||
UPDATE calendar_events SET
|
||||
title = ?, description = ?, reminder_text = ?,
|
||||
agent_name = ?, to_address = ?,
|
||||
recipients = ?, delivery_mode = ?,
|
||||
event_time = ?, remind_before = ?, recurrence = ?, recurrence_end = ?,
|
||||
status = ?, updated_at = ?
|
||||
status = ?, permission_mode = ?, updated_at = ?
|
||||
WHERE event_id = ?`,
|
||||
e.Title, e.Description, e.ReminderText,
|
||||
e.AgentName, e.ToAddress,
|
||||
marshalRecipients(e.Recipients), e.EffectiveDeliveryMode(),
|
||||
e.EventTime, e.RemindBefore, e.Recurrence, e.RecurrenceEnd,
|
||||
e.Status, e.UpdatedAt, eventID,
|
||||
e.Status, e.PermissionMode, e.UpdatedAt, eventID,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -179,3 +179,91 @@ func TestLegacyWrapperStillWorks(t *testing.T) {
|
||||
t.Fatalf("包装函数应与原行为一致:%s vs %s", id, again)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 预算不被冲的回归用例(P0 第一项,P3 钉死) ───
|
||||
//
|
||||
// 守卫原本是 `parentMailID == nil`(表示新建),但省略 session 位复用默认会话时
|
||||
// parentMailID 也是 nil —— 于是「仅在新建时生效」的字段(预算、档位)在每封
|
||||
// 省略 session 位的信上都被重写了。实测:第一封 max_rounds=7 → 第二封省略该
|
||||
// 字段 → 预算被静默改成默认的 20。
|
||||
//
|
||||
// 修法:handler 改用 FindOrCreateDefaultSessionCreated 返回的 `created` 判据。
|
||||
// 本测试钉死「复用默认会话时 created=false」这一事实,让守卫不会倒退回去。
|
||||
func TestDefaultSessionReuseBudgetNotReset(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
ctx := context.Background()
|
||||
seedAgent(t, "pi", 20)
|
||||
|
||||
first, created, err := FindOrCreateDefaultSessionCreated(ctx, "pi", "/w", "jianf", "首封")
|
||||
if err != nil || !created {
|
||||
t.Fatalf("首封应新建:created=%v err=%v", created, err)
|
||||
}
|
||||
// 模拟发信路径:新建会话时定预算为 7(低于默认 20)
|
||||
if _, err := SetSessionBudget(ctx, first, 7); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 复用的前提是会话里有该收件人参与过的邮件
|
||||
if _, err := CreateMail(ctx, first, nil, "jianf", "", "pi", "/w", "首封", "x", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 第二封省略 session 位 → 复用默认会话,created 必须为 false
|
||||
second, created2, err := FindOrCreateDefaultSessionCreated(ctx, "pi", "/w", "jianf", "第二封")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if second != first {
|
||||
t.Fatalf("第二封应复用同一会话:%s vs %s", first, second)
|
||||
}
|
||||
if created2 {
|
||||
t.Fatal("复用默认会话时 created 必须为 false,否则预算会被默认值冲掉")
|
||||
}
|
||||
|
||||
// 既然 created2=false,发信路径不会调 SetSessionBudget → 预算仍为 7
|
||||
b, err := GetSessionBudget(ctx, first)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b.Max != 7 {
|
||||
t.Errorf("预算被冲掉:got max=%d,want 7(复用不应重设)", b.Max)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 档位同样不被冲的回归用例 ───
|
||||
//
|
||||
// 与预算同理:复用默认会话时档位也不该被重设成默认档。
|
||||
// 人指定 plan 档后,第二封信省略 session 位复用同一条会话 →
|
||||
// created=false → SetSessionPermissionMode 不被调 → 档位仍为 plan。
|
||||
func TestDefaultSessionReusePermissionModeNotReset(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
ctx := context.Background()
|
||||
seedAgent(t, "pi", 20)
|
||||
|
||||
first, created, err := FindOrCreateDefaultSessionCreated(ctx, "pi", "/w", "jianf", "首封")
|
||||
if err != nil || !created {
|
||||
t.Fatalf("首封应新建:created=%v err=%v", created, err)
|
||||
}
|
||||
// 新建会话时定档位为 plan(比默认 workspace 更严)
|
||||
if _, err := SetSessionPermissionMode(ctx, first, "plan"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := CreateMail(ctx, first, nil, "jianf", "", "pi", "/w", "首封", "x", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
second, created2, err := FindOrCreateDefaultSessionCreated(ctx, "pi", "/w", "jianf", "第二封")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if second != first {
|
||||
t.Fatalf("第二封应复用同一会话:%s vs %s", first, second)
|
||||
}
|
||||
if created2 {
|
||||
t.Fatal("复用默认会话时 created 必须为 false,否则档位会被默认值冲掉")
|
||||
}
|
||||
|
||||
mode := SessionPermissionMode(ctx, first)
|
||||
if mode != "plan" {
|
||||
t.Errorf("档位被冲掉:got %q,want plan(复用不应重设)", mode)
|
||||
}
|
||||
}
|
||||
|
||||
369
gateway/internal/repo/permission_mode_test.go
Normal file
369
gateway/internal/repo/permission_mode_test.go
Normal file
@ -0,0 +1,369 @@
|
||||
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 在每个测试函数内调用
|
||||
}
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/agentmail/gateway/internal/db"
|
||||
"github.com/agentmail/gateway/internal/models"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@ -320,6 +321,14 @@ func AdoptPlatformSession(ctx context.Context, agentName, platformID, slug, work
|
||||
platformID, id); err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
// 显式写入档位与强制力:接管一条平台会话没有父会话,
|
||||
// 只靠 DB 默认值会在「schema 列定义变动」或「迁移补列给了不同默认」时
|
||||
// 静默偏离预期 —— 显式写 'workspace' 是唯一可靠表述「这条会话是新接管的,
|
||||
// 没有继承来源」的方式。与 me.go 新建会话那条路径一致。
|
||||
if _, err := SetSessionPermissionMode(ctx, id, models.DefaultPermissionMode); err != nil {
|
||||
return uuid.Nil, err
|
||||
}
|
||||
_ = SetSessionEnforcement(ctx, id, AgentModeEnforcement(ctx, agentName))
|
||||
// 别名尽量用 slug;撞名时 EnsureSessionAlias 自动加后缀
|
||||
_, _ = EnsureSessionAlias(ctx, id, slug)
|
||||
return id, nil
|
||||
|
||||
Reference in New Issue
Block a user