diff --git a/src/components/settings/__tests__/coach-entry-follows-capability.test.tsx b/src/components/settings/__tests__/coach-entry-follows-capability.test.tsx index c30b49e8c..79d0eef04 100644 --- a/src/components/settings/__tests__/coach-entry-follows-capability.test.tsx +++ b/src/components/settings/__tests__/coach-entry-follows-capability.test.tsx @@ -15,7 +15,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; -const h = vi.hoisted(() => ({ coachAvailable: true })); +const h = vi.hoisted(() => ({ + coachAvailable: true, + active: null as Record | null, +})); vi.mock("next/navigation", () => ({ usePathname: () => "/settings/ai", @@ -24,7 +27,12 @@ vi.mock("next/navigation", () => ({ vi.mock("@/hooks/use-mounted", () => ({ useMounted: () => true })); vi.mock("@/hooks/use-auth", () => ({ useAuth: () => ({ - user: { id: "u1", role: "USER", modules: { coach: true } }, + user: { + id: "u1", + role: "USER", + modules: { coach: true }, + accountAccess: { active: h.active }, + }, isAuthenticated: true, isLoading: false, refetch: vi.fn(), @@ -68,6 +76,7 @@ function render(node: React.ReactNode) { describe("the Coach entry in Settings", () => { beforeEach(() => { h.coachAvailable = true; + h.active = null; }); it("is listed while the Coach can answer, and the memory lives there", () => { @@ -86,4 +95,18 @@ describe("the Coach entry in Settings", () => { ).not.toContain('href="/settings/coach"'); expect(render()).toContain('data-slot="stored-coach-memory"'); }); + + it("stays for a managed record whose Coach cannot answer yet", () => { + // A record has no Settings, AI: the guardian reaches the record's Coach + // data scope and switch through this entry only. + h.coachAvailable = false; + h.active = { + recordKind: "managed", + level: "manage", + manageableDomains: [], + }; + expect( + render(body), + ).toContain('href="/settings/coach"'); + }); }); diff --git a/src/components/settings/settings-shell.tsx b/src/components/settings/settings-shell.tsx index a2bcb5185..8d6924f5c 100644 --- a/src/components/settings/settings-shell.tsx +++ b/src/components/settings/settings-shell.tsx @@ -356,6 +356,20 @@ function deriveActiveSlug( return isSettingsSectionSlug(candidate) ? candidate : "account"; } +/** + * The module map the section list reads, the sidebar's. A managed record has + * no Settings, AI to list its Coach settings (its data scope, its switch), so + * there the Coach entry follows the module switch alone, not whether the + * record's Coach can answer yet. + */ +function useSectionModules() { + const { user } = useAuth(); + const navModules = useNavModules(); + return user?.accountAccess?.active?.recordKind === "managed" + ? user.modules + : navModules; +} + export function SettingsShell({ active, heading, @@ -391,7 +405,7 @@ export function SettingsShell({ // effect (which the lint rule flags as a cascading render), and no risk of // the two passes disagreeing. const hydrated = useMounted(); - const modules = useNavModules(); + const modules = useSectionModules(); const activeRecord = user?.accountAccess?.active ?? null; const allVisibleSections = SETTINGS_SECTIONS.filter( (section) =>