import { NavigateFunction } from "react-router"; import { describe, expect, it, vi } from "vitest"; import { getCommandRegistryActions, getSidebarCommandActions } from "./commandRegistry"; const t = vi.fn().mockImplementation((value: string) => value) as any; const routing = { protocol: "https", webAppHost: "app.example.test", adminAppHost: "admin.example.test", apiHost: "api.example.test", appBaseDomain: "example.test", }; function rootData(tenantApiKeys = true, admin = false, routingConfig?: typeof routing, crm = false) { return { appConfiguration: { app: { features: { tenantApiKeys, crm } }, portals: { enabled: false, forTenants: false }, }, user: { admin }, routing: routingConfig, } as any; } function appData(options?: { admin?: boolean; permissions?: string[]; tenantApiKeys?: boolean }) { return { currentTenant: { id: "tenant_1", slug: "acme" }, entities: [ { name: "company", titlePlural: "models.company.plural", slug: "companies", showInSidebar: true }, { name: "contact", titlePlural: "models.contact.plural", slug: "contacts", showInSidebar: true }, { name: "opportunity", titlePlural: "models.opportunity.plural", slug: "opportunities", showInSidebar: true }, ], myTenants: [{ id: "tenant_2", slug: "other" }], permissions: options?.permissions ?? [ "app.settings.members.view", "app.settings.subscription.view", "app.settings.account.view", "app.settings.apiKeys.view", "app.settings.auditTrails.view", "entity.company.view", "entity.contact.view", "entity.opportunity.view", ], isSuperAdmin: false, user: { id: "user_1", admin: options?.admin ?? true }, } as any; } function navigateMock() { return vi.fn() as unknown as NavigateFunction & { mock: ReturnType["mock"] }; } function perform(action: { perform?: (action: any) => void; id: string }) { action.perform?.({ id: action.id }); } function expectNoLegacyPrefixes(targets: string[]) { targets.forEach((target) => { expect(target).not.toMatch(/^\/app\//); expect(target).not.toMatch(/^\/admin(?:\/|$)/); }); } describe("command registry", () => { it("builds app tenant commands with host-aware tenant URLs and scope metadata", () => { const navigate = navigateMock(); const actions = getCommandRegistryActions({ layout: "app", t, navigate, appData: appData(), rootData: rootData() }); perform(actions.find((action) => action.id === "dashboard")!); perform(actions.find((action) => action.id === "members")!); perform(actions.find((action) => action.id === "apiKeys")!); expect(navigate).toHaveBeenNthCalledWith(1, "/acme/dashboard"); expect(navigate).toHaveBeenNthCalledWith(2, "/acme/settings/members"); expect(navigate).toHaveBeenNthCalledWith(3, "/acme/settings/api"); expect(actions.find((action) => action.id === "members")?.scope).toMatchObject({ layout: "app", tenantSlug: "acme", permission: "app.settings.members.view", }); expect(actions.find((action) => action.id === "apiKeys")?.scope).toMatchObject({ featureFlag: "tenantApiKeys", permission: "app.settings.apiKeys.view" }); expectNoLegacyPrefixes(navigate.mock.calls.map(([target]) => target)); }); it("omits app commands for empty app data", () => { expect(getCommandRegistryActions({ layout: "app", t, navigate: navigateMock(), rootData: rootData() })).toEqual([]); }); it("filters tenant settings commands by permissions and feature flags", () => { const actions = getCommandRegistryActions({ layout: "app", t, navigate: navigateMock(), appData: appData({ permissions: ["app.settings.account.view"] }), rootData: rootData(false), }); expect(actions.some((action) => action.id === "account")).toBe(true); expect(actions.some((action) => action.id === "members")).toBe(false); expect(actions.some((action) => action.id === "subscription")).toBe(false); expect(actions.some((action) => action.id === "apiKeys")).toBe(false); expect(actions.some((action) => action.id === "auditTrails")).toBe(false); expect(actions.some((action) => action.id === "events")).toBe(false); }); it("filters tenant entity commands through permission-aware scope metadata", () => { const actions = getCommandRegistryActions({ layout: "app", t, navigate: navigateMock(), appData: appData({ permissions: ["entity.company.view"] }), rootData: rootData(), }); expect(actions.find((action) => action.id === "entity-company")?.scope).toMatchObject({ layout: "app", tenantSlug: "acme", permission: "entity.company.view", }); expect(actions.some((action) => action.id === "entity-contact")).toBe(false); expect(actions.some((action) => action.id === "entity-opportunity")).toBe(false); }); it("omits CRM command while the CRM route tree is disabled", () => { const disabled = getCommandRegistryActions({ layout: "app", t, navigate: navigateMock(), appData: appData(), rootData: rootData(true, true, routing, false), }); const enabled = getCommandRegistryActions({ layout: "app", t, navigate: navigateMock(), appData: appData(), rootData: rootData(true, true, routing, true), }); expect(disabled.some((action) => action.id === "crm")).toBe(false); expect(enabled.some((action) => action.id === "crm")).toBe(false); }); it("keeps platform commands available when scoped tenant commands are denied", () => { const actions = getCommandRegistryActions({ layout: "app", t, navigate: navigateMock(), appData: appData({ admin: false, permissions: [] }), rootData: rootData(), }); expect(actions.some((action) => action.id === "entity-company")).toBe(false); expect(actions.some((action) => action.id === "create account")).toBe(true); expect(actions.some((action) => action.id === "logout")).toBe(true); }); it("omits switch-to-admin for non-admin app users", () => { const actions = getCommandRegistryActions({ layout: "app", t, navigate: navigateMock(), appData: appData({ admin: false }), rootData: rootData() }); expect(actions.some((action) => action.id === "switch to admin")).toBe(false); }); it("switches app admins to the admin dashboard instead of the admin host root", () => { const navigate = navigateMock(); const actions = getCommandRegistryActions({ layout: "app", t, navigate, appData: appData({ admin: true }), rootData: rootData(true, true, routing) }); vi.stubGlobal("window", undefined); try { perform(actions.find((action) => action.id === "switch to admin")!); } finally { vi.unstubAllGlobals(); } expect(navigate).toHaveBeenCalledWith("https://admin.example.test/dashboard"); }); it("filters admin sidebar commands for non-superadmin contexts while preserving platform commands", () => { const actions = getCommandRegistryActions({ layout: "admin", t, navigate: navigateMock(), rootData: rootData() }); expect(actions.some((action) => action.id === "/dashboard")).toBe(false); expect(actions.some((action) => action.id === "create account")).toBe(true); expect(actions.some((action) => action.id === "logout")).toBe(true); }); it("builds admin commands when app data has no current tenant", () => { const actions = getCommandRegistryActions({ layout: "admin", t, navigate: navigateMock(), appData: { ...appData({ admin: true }), currentTenant: null, isSuperAdmin: true } as any, rootData: rootData(true, true), }); expect(actions.some((action) => action.id === "/dashboard")).toBe(true); expect(actions.every((action) => action.scope.layout === "admin")).toBe(true); }); it("builds admin commands with host-aware paths and app fallback switch", () => { const navigate = navigateMock(); const actions = getCommandRegistryActions({ layout: "admin", t, navigate, rootData: rootData(true, true) }); perform(actions.find((action) => action.id === "/dashboard")!); perform(actions.find((action) => action.id === "/settings/profile")!); perform(actions.find((action) => action.name === "admin.switchToApp")!); expect(navigate).toHaveBeenNthCalledWith(1, "/dashboard"); expect(navigate).toHaveBeenNthCalledWith(2, "/settings/profile"); expect(navigate).toHaveBeenNthCalledWith(3, "/"); expect(actions.find((action) => action.id === "/dashboard")?.scope).toMatchObject({ layout: "admin", platform: true, adminOnly: true }); expect(actions.find((action) => action.name === "admin.switchToApp")?.scope).toMatchObject({ layout: "admin", platform: true, adminOnly: true }); expectNoLegacyPrefixes(navigate.mock.calls.map(([target]) => target)); }); it("does not expose expandable admin sidebar parents as command destinations", () => { const actions = getCommandRegistryActions({ layout: "admin", t, navigate: navigateMock(), appData: { ...appData({ admin: true, permissions: ["admin.accounts.view", "admin.settings.view"], }), currentTenant: null, isSuperAdmin: true, } as any, rootData: rootData(true, true), }); expect(actions.some((action) => action.id === "/accounts" && action.name === "app.sidebar.accountsAndUsers")).toBe(false); expect(actions.some((action) => action.id === "/settings" && action.name === "app.sidebar.settings")).toBe(false); expect(actions.some((action) => action.id === "/accounts" && action.name === "admin.tenants.title")).toBe(true); expect(actions.some((action) => action.id === "/settings/profile")).toBe(true); }); it("does not expose disabled sidebar items as command destinations", () => { const actions = getSidebarCommandActions( [ { title: "Settings", path: "/settings", items: [ { title: "Profile", path: "/settings/profile" }, { title: "Disabled Plugin", path: "/settings/plugin", disabled: true }, ], }, ], "admin" ); expect(actions.some((action) => action.id === "/settings/profile")).toBe(true); expect(actions.some((action) => action.id === "/settings/plugin")).toBe(false); }); it("uses parent sidebar titles as command subtitles", () => { const actions = getSidebarCommandActions( [ { title: "Settings", path: "/settings", items: [{ title: "Profile", path: "/settings/profile" }], }, ], "admin" ); expect(actions.find((action) => action.id === "/settings/profile")?.subtitle).toBe("Settings"); }); it("builds admin public-app commands with absolute app-host URLs when routing is configured", () => { const navigate = navigateMock(); const actions = getCommandRegistryActions({ layout: "admin", t, navigate, rootData: rootData(true, true, routing) }); expect(actions.find((action) => action.name === "admin.switchToApp")?.id).toBe("https://app.example.test/"); vi.stubGlobal("window", undefined); try { perform(actions.find((action) => action.name === "admin.switchToApp")!); perform(actions.find((action) => action.id === "create account")!); } finally { vi.unstubAllGlobals(); } expect(navigate).toHaveBeenNthCalledWith(1, "https://app.example.test/"); expect(navigate).toHaveBeenNthCalledWith(2, "https://app.example.test/new-account"); }); it("passes docs commands through with registry scope and navigation", () => { const navigate = navigateMock(); const actions = getCommandRegistryActions({ layout: "docs", t, navigate }); const firstAction = actions[0]; perform(firstAction); expect(firstAction.scope).toMatchObject({ layout: "docs", platform: true }); expect(navigate).toHaveBeenCalledWith(firstAction.id); }); it("exposes app, admin, and docs commands directly through the registry", () => { expect(getCommandRegistryActions({ layout: "app", t, navigate: navigateMock(), appData: appData(), rootData: rootData() })[0]).toMatchObject({ id: "dashboard", scope: { layout: "app" }, }); expect(getCommandRegistryActions({ layout: "admin", t, navigate: navigateMock(), rootData: rootData(true, true) })[0]).toMatchObject({ id: "/dashboard", scope: { layout: "admin" }, }); expect(getCommandRegistryActions({ layout: "docs", t, navigate: navigateMock() })[0]).toMatchObject({ scope: { layout: "docs" } }); }); });