diff --git a/internal/app/hostcaps_test.go b/internal/app/hostcaps_test.go index ee94b3f..04ed066 100644 --- a/internal/app/hostcaps_test.go +++ b/internal/app/hostcaps_test.go @@ -26,11 +26,15 @@ func TestCapabilityExplainProviderExplainsBoundActorOnly(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := st.ReplaceRolePermissions("admin", role.RoleID, role.RoleVersion, []store.RolePermissionChange{ - {ActionID: "project.create", Allowed: true, CapAllowed: true}, - {ActionID: "document.read", Allowed: true, CapAllowed: true}, - {ActionID: "document.review", Allowed: false, CapAllowed: true}, - }, "explain-provider"); err != nil { + // 收窄模型(R2):职位角色只携带单一 project.create 行;document.* / + // project.* 等其他动作由系统授权或项目成员关系承担,写到角色行被 + // ReplaceRolePermissions 拒绝(ErrInvalidRolePolicy)。fixture 只设 + // project.create,断言围绕这条窄权限:CreateAuthority 投影到四个 + // ProjectCreateLinkedActions、MemberBasicCollaborate 全 action-unconfigured、 + // DocumentQualify 三职责都 action-unconfigured。 + if _, err := st.ReplaceRolePermissions("admin", role.RoleID, role.RoleVersion, + []store.RolePermissionChange{{ActionID: "project.create", Allowed: true, CapAllowed: true}}, + "explain-provider"); err != nil { t.Fatal(err) } if _, err := st.AssignUserRole("admin", alice.ID, role.RoleID, alice.StateVersion, "explain-provider-assign"); err != nil { @@ -59,8 +63,11 @@ func TestCapabilityExplainProviderExplainsBoundActorOnly(t *testing.T) { if err != nil || member.UserID != alice.ID || len(member.Actions) == 0 { t.Fatalf("member=%+v err=%v", member, err) } + // 角色权限投影:document.read 不在职位权限(只有 project.create),因此不能 + // 被 Source=CapabilitySourceRole 标记为允许;来源必须是空、Reason 必须是 + // "action-unconfigured"——这条边界保护正是窄模型的保护意图。 for _, action := range member.Actions { - if action.ActionID == "document.read" && (!action.Allowed || action.Source != extension.CapabilitySourceRole) { + if action.ActionID == "document.read" && (action.Allowed || action.Source == extension.CapabilitySourceRole || action.Reason != "action-unconfigured") { t.Fatalf("document.read=%+v", action) } } @@ -68,8 +75,13 @@ func TestCapabilityExplainProviderExplainsBoundActorOnly(t *testing.T) { if err != nil || document.UserID != alice.ID || len(document.Duties) != 3 { t.Fatalf("document=%+v err=%v", document, err) } + // 三职责不被职位权限(project.create)直接资格化,因此 Qualified=false、 + // DefaultAllowed=false、Source 空、Reason 为 action-unconfigured;这与 + // 历史上「Allowed=false CapAllowed=true 单独写入」的职责资格语义不同—— + // 那条路径已被 ReplaceRolePermissions 拒绝,本测试在不依赖该历史路径的 + // 前提下保留职责资格投影的边界验证。 for _, duty := range document.Duties { - if duty.ActionID == "document.review" && (!duty.Qualified || duty.DefaultAllowed) { + if duty.ActionID == "document.review" && (duty.Qualified || duty.DefaultAllowed || duty.Source == extension.CapabilitySourceRole || duty.Reason != "action-unconfigured") { t.Fatalf("document.review=%+v", duty) } }