package app import ( "context" "errors" "testing" "agent-system-backend/internal/store" "agent-system-backend/pkg/extension" ) func TestCapabilityExplainProviderExplainsBoundActorOnly(t *testing.T) { st, err := store.OpenSQLite(t.TempDir() + "/capability-explain.db") if err != nil { t.Fatal(err) } t.Cleanup(func() { st.Close() }) if err := st.CreateAccount("admin", "Admin", "hash", true); err != nil { t.Fatal(err) } if err := st.CreateAccount("alice", "Alice", "hash", false); err != nil { t.Fatal(err) } alice, _ := st.GetAccountByOwner("alice") role, err := st.CreateRole("admin", store.RoleCreate{Name: "项目负责人"}) 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 { t.Fatal(err) } if _, err := st.AssignUserRole("admin", alice.ID, role.RoleID, alice.StateVersion, "explain-provider-assign"); err != nil { t.Fatal(err) } h := inprocHost{caps: hostcaps{st: st}, pluginID: "rdm", owner: "alice", actor: "alice"} provider, ok := any(h).(extension.CapabilityExplainProvider) if !ok { t.Fatal("dispatch host missing CapabilityExplainProvider") } ctx := context.Background() capability, err := provider.CurrentCapabilityExplain(ctx) if err != nil { t.Fatal(err) } if capability.SchemaVersion != extension.CapabilityExplainSchemaVersion || capability.UserID != alice.ID || capability.Owner != "alice" || capability.RoleID != role.RoleID || !capability.ProjectCreate.Allowed || capability.ProjectCreate.Source != extension.CapabilitySourceRole || len(capability.Linked) != 4 || len(capability.Conflicts) != 0 { t.Fatalf("capability=%+v", capability) } snapshot, err := any(h).(extension.RolePolicyProvider).CurrentRolePolicy(ctx) if err != nil || snapshot.Linked.Source != capability.ProjectCreate.Source || len(snapshot.Linked.Actions) != len(capability.Linked) { t.Fatalf("snapshot linked=%+v err=%v", snapshot.Linked, err) } member, err := provider.CurrentMemberBasicCollaborate(ctx) if err != nil || member.UserID != alice.ID || len(member.Actions) == 0 { t.Fatalf("member=%+v err=%v", member, err) } for _, action := range member.Actions { if action.ActionID == "document.read" && (!action.Allowed || action.Source != extension.CapabilitySourceRole) { t.Fatalf("document.read=%+v", action) } } document, err := provider.CurrentDocumentQualify(ctx) if err != nil || document.UserID != alice.ID || len(document.Duties) != 3 { t.Fatalf("document=%+v err=%v", document, err) } for _, duty := range document.Duties { if duty.ActionID == "document.review" && (!duty.Qualified || duty.DefaultAllowed) { t.Fatalf("document.review=%+v", duty) } } // A scoped Host without a dispatch actor cannot explain anyone. scoped := any(inprocHost{caps: hostcaps{st: st}, pluginID: "rdm", owner: "alice"}).(extension.CapabilityExplainProvider) if _, err := scoped.CurrentCapabilityExplain(ctx); !errors.Is(err, store.ErrAccountForbidden) { t.Fatalf("scoped capability: %v", err) } if _, err := scoped.CurrentMemberBasicCollaborate(ctx); !errors.Is(err, store.ErrAccountForbidden) { t.Fatalf("scoped member: %v", err) } if _, err := scoped.CurrentDocumentQualify(ctx); !errors.Is(err, store.ErrAccountForbidden) { t.Fatalf("scoped document: %v", err) } ghost := any(inprocHost{caps: hostcaps{st: st}, pluginID: "rdm", owner: "ghost", actor: "ghost"}).(extension.CapabilityExplainProvider) if _, err := ghost.CurrentCapabilityExplain(ctx); !errors.Is(err, store.ErrAccountForbidden) { t.Fatalf("unknown actor: %v", err) } canceled, cancel := context.WithCancel(ctx) cancel() if _, err := provider.CurrentCapabilityExplain(canceled); !errors.Is(err, context.Canceled) { t.Fatalf("canceled: %v", err) } }