fix(settings): keep the Coach entry for a managed record

The Settings Coach entry now follows whether the Coach can answer, as
the sidebar does, with Settings, AI keeping the stored memory in reach.
A managed record has no Settings, AI, so while its Coach could not
answer yet (no operator key, no consent) the guardian lost the only way
to the record's Coach data scope and its switch. For a managed record
the entry follows the module switch alone again.
This commit is contained in:
Marc-André Bombeck committed 2026-11-12 13:05:37 +01:00
1 parent 8b6d9826ea
commit 5b8543f471
2 files changed
+40 -3

No files matched your search

@@ -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<string, unknown> | 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(<AiSection />)).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(<SettingsShell active="coach">body</SettingsShell>),
).toContain('href="/settings/coach"');
});
});
+15 -1
View File
@@ -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) =>