Files
kandev/apps/web/components/app-sidebar/sections/settings/settings-menu-branches.test.ts
T
2026-09-05 18:44:43 +00:00

509 lines
18 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
buildAgentsBranch,
buildBranchRoot,
buildExecutorsBranch,
buildWorkspacesBranch,
findActiveNodePath,
findNodeKeyPath,
findSubtreeKeys,
settingsMenuRowKey,
type SettingsMenuNode,
} from "./settings-menu-branches";
const WORKSPACE_ID = "ws-1";
const WORKSPACES_HREF = "/settings/workspaces";
const EXECUTORS_HREF = "/settings/executors";
const WORKSPACES = [{ id: WORKSPACE_ID, name: "Main Workspace" }];
const ORDERED_WORKSPACES = [
{ id: "ws-first", name: "First" },
{ id: "ws-active", name: "Active" },
{ id: "ws-last", name: "Last" },
];
// Hoisted so the filter tests can build fixtures without re-spelling the
// display name (sonar duplicate-literal rule).
const AGENT_DISPLAY_NAME = "Claude Code";
const AGENTS = [
{
name: "claude-code",
profiles: [
{ id: "profile-1", name: "Default", agentDisplayName: "Claude Code" },
{ id: "profile-2", name: "Review", agentDisplayName: "Claude Code" },
],
},
];
const EXECUTORS = [
{
id: "exec-1",
name: "Local Executor",
type: "local",
profiles: [{ id: "exec-profile-1", name: "Local" }],
},
];
function hrefsOf(nodes: readonly SettingsMenuNode[]): Array<string | null> {
return nodes.map((node) => node.href);
}
/** The Integrations tab among a workspace node's children. */
function integrationsTabOf(tabs: readonly SettingsMenuNode[]): SettingsMenuNode | undefined {
return tabs.find((node) => node.href?.endsWith("/integrations"));
}
function integrationSlugsOf(tabs: readonly SettingsMenuNode[]): Array<string | undefined> {
return (integrationsTabOf(tabs)?.children ?? []).map((node) => node.integrationSlug);
}
describe("buildWorkspacesBranch", () => {
it("includes the Canvases tab only for an enabled canvas feature", () => {
const [workspace] = buildWorkspacesBranch(WORKSPACES, null, undefined, [], {
canvasesEnabled: true,
});
expect(hrefsOf(workspace.children ?? [])).toContain(
`/settings/workspaces/${WORKSPACE_ID}/canvases`,
);
});
it("places the active workspace first without disturbing the remaining order", () => {
const branch = buildWorkspacesBranch(ORDERED_WORKSPACES, "ws-active");
expect(branch.map((workspace) => workspace.key)).toEqual([
"workspace:ws-active",
"workspace:ws-first",
"workspace:ws-last",
]);
expect(branch[0].badge).toBe("active");
});
it("hangs each workspace's tabs under it, minus its own overview page", () => {
const [workspace] = buildWorkspacesBranch(WORKSPACES);
expect(workspace.href).toBe(`/settings/workspaces/${WORKSPACE_ID}`);
expect(hrefsOf(workspace.children ?? [])).toEqual([
`/settings/workspaces/${WORKSPACE_ID}/repositories`,
`/settings/workspaces/${WORKSPACE_ID}/workflows`,
`/settings/workspaces/${WORKSPACE_ID}/integrations`,
`/settings/workspaces/${WORKSPACE_ID}/automations`,
`/settings/workspaces/${WORKSPACE_ID}/secrets`,
]);
});
it("goes one level deeper for integrations, the menu's deepest branch", () => {
const [workspace] = buildWorkspacesBranch(WORKSPACES);
const integrations = integrationsTabOf(workspace.children ?? []);
expect(hrefsOf(integrations?.children ?? [])).toContain(
`/settings/workspaces/${WORKSPACE_ID}/integrations/github`,
);
});
it("appends registered provider integrations using the native workspace route", () => {
const IntegrationIcon = () => null;
const [workspace] = buildWorkspacesBranch(WORKSPACES, null, undefined, [
{ id: "bitbucket", pluginId: "plugin-bitbucket", label: "Bitbucket", icon: IntegrationIcon },
]);
const integration = integrationsTabOf(workspace.children ?? [])?.children?.find(
(node) => node.key === `workspace:${WORKSPACE_ID}:integrations:bitbucket`,
);
expect(integration).toMatchObject({
href: `/settings/workspaces/${WORKSPACE_ID}/integrations/bitbucket`,
label: { text: "Bitbucket" },
icon: IntegrationIcon,
});
});
it("filters plugin integrations per workspace and keeps unknown state visible", () => {
const IntegrationIcon = () => null;
const workspaces = [
{ id: "workspace-disabled", name: "Disabled" },
{ id: "workspace-enabled", name: "Enabled" },
{ id: "workspace-unknown", name: "Unknown" },
];
const enabledByWorkspace: Record<string, boolean | undefined> = {
"workspace-disabled": false,
"workspace-enabled": true,
};
const [disabled, enabled, unknown] = buildWorkspacesBranch(
workspaces,
null,
undefined,
[
{
id: "bitbucket",
pluginId: "plugin-bitbucket",
label: "Bitbucket",
icon: IntegrationIcon,
},
],
{
pluginIntegrationEnabled: (_integrationId, workspaceId) => enabledByWorkspace[workspaceId],
},
);
expect(integrationSlugsOf(disabled.children ?? [])).not.toContain("bitbucket");
expect(integrationSlugsOf(enabled.children ?? [])).toContain("bitbucket");
expect(integrationSlugsOf(unknown.children ?? [])).toContain("bitbucket");
});
it("gives every tab and every integration its own mark", () => {
const [workspace] = buildWorkspacesBranch(WORKSPACES);
const tabs = workspace.children ?? [];
const integrations = integrationsTabOf(tabs)?.children ?? [];
// A row with no icon sits text-first and breaks the column the rest form.
expect(tabs.every((node) => node.icon !== undefined)).toBe(true);
expect(integrations.every((node) => node.icon !== undefined)).toBe(true);
// Marks identify — two sections sharing one would defeat the point.
expect(new Set(tabs.map((node) => node.icon)).size).toBe(tabs.length);
expect(new Set(integrations.map((node) => node.icon)).size).toBe(integrations.length);
});
it("percent-encodes an id that would otherwise break the path", () => {
const [workspace] = buildWorkspacesBranch([{ id: "a/b", name: "Slashy" }]);
expect(workspace.href).toBe("/settings/workspaces/a%2Fb");
});
});
describe("buildWorkspacesBranch integration visibility", () => {
function integrationSlugsOf(nodes: readonly SettingsMenuNode[]): Array<string | undefined> {
return (integrationsTabOf(nodes)?.children ?? []).map((node) => node.integrationSlug);
}
it("lists every integration when no visible set is given — the default", () => {
const [workspace] = buildWorkspacesBranch(WORKSPACES);
expect(integrationSlugsOf(workspace.children ?? [])).toEqual([
"azure-devops",
"github",
"gitlab",
"jira",
"linear",
"sentry",
]);
});
it("drops the integrations missing from the visible set, keeping catalog order", () => {
const [workspace] = buildWorkspacesBranch(
WORKSPACES,
null,
() => new Set(["azure-devops", "linear"] as const),
);
expect(integrationSlugsOf(workspace.children ?? [])).toEqual(["azure-devops", "linear"]);
});
it("resolves the visible set per workspace, since the toggles are per workspace", () => {
// Disabling GitHub in one workspace must not strip it from another's
// branch, which is exactly what a single shared set used to do.
const branch = buildWorkspacesBranch(
[
{ id: "ws-1", name: "First" },
{ id: "ws-2", name: "Second" },
],
null,
(workspaceId) =>
workspaceId === "ws-1" ? new Set(["jira"] as const) : new Set(["github"] as const),
);
expect(integrationSlugsOf(branch[0].children ?? [])).toEqual(["jira"]);
expect(integrationSlugsOf(branch[1].children ?? [])).toEqual(["github"]);
});
it("never consults whether an integration is configured — the badge owns that", () => {
// Nothing about credentials reaches this builder: an integration the user
// has never connected is listed exactly like a connected one, so the
// visible set is the only thing that can remove a row.
const [workspace] = buildWorkspacesBranch(WORKSPACES, null, () => new Set(["github"] as const));
expect(integrationSlugsOf(workspace.children ?? [])).toEqual(["github"]);
});
it("leaves the Integrations row navigable when the set hides all of them", () => {
// An empty branch would otherwise render a chevron opening onto nothing.
const [workspace] = buildWorkspacesBranch(WORKSPACES, null, () => new Set());
const integrations = integrationsTabOf(workspace.children ?? []);
expect(integrations?.children).toEqual([]);
expect(integrations?.href).toBe(`/settings/workspaces/${WORKSPACE_ID}/integrations`);
});
});
describe("isUserRecord", () => {
it("marks the nodes the user created: workspaces and profiles", () => {
const [workspace] = buildWorkspacesBranch(WORKSPACES);
const [agent] = buildAgentsBranch(AGENTS);
const [executor] = buildExecutorsBranch(EXECUTORS);
expect(workspace.isUserRecord).toBe(true);
expect(agent.children?.every((node) => node.isUserRecord)).toBe(true);
expect(executor.children?.every((node) => node.isUserRecord)).toBe(true);
});
it("leaves the agent and the executor themselves unmarked", () => {
const [agent] = buildAgentsBranch(AGENTS);
const [executor] = buildExecutorsBranch(EXECUTORS);
// `Claude` and `Local` ship with kandev and read the same in every install.
// The user authors the profiles under them, not the agent or executor.
expect(agent.isUserRecord).toBeFalsy();
expect(executor.isUserRecord).toBeFalsy();
});
it("leaves fixed structure unmarked, including at a record's own depth", () => {
const [workspace] = buildWorkspacesBranch(WORKSPACES);
const tabs = workspace.children ?? [];
const integrations = integrationsTabOf(tabs)?.children ?? [];
// These are the assertions that hold the line: a workspace's tabs are a
// fixed six, and the integrations a fixed seven, in every install — even
// though `GitHub` sits at the same indent as a workspace name.
expect(tabs.every((node) => !node.isUserRecord)).toBe(true);
expect(integrations.length).toBeGreaterThan(0);
expect(integrations.every((node) => !node.isUserRecord)).toBe(true);
});
it("does not mark a branch root, which is a menu row", () => {
const root = buildBranchRoot(
{ href: WORKSPACES_HREF, labelKey: "common:workspaces" },
buildWorkspacesBranch(WORKSPACES),
);
expect(root.isUserRecord).toBeFalsy();
});
});
describe("buildAgentsBranch", () => {
it("gives the agent no page of its own, only its profiles", () => {
const [agent] = buildAgentsBranch(AGENTS);
// `/settings/agents/<name>` redirects back to the index for a saved agent.
expect(agent.href).toBeNull();
expect(agent.ownsPrefixes).toEqual(["/settings/agents/claude-code/"]);
expect(hrefsOf(agent.children ?? [])).toEqual([
"/settings/agents/claude-code/profiles/profile-1",
"/settings/agents/claude-code/profiles/profile-2",
]);
});
it("drops an agent with no profiles rather than rendering a dead row", () => {
expect(buildAgentsBranch([{ name: "empty", profiles: [] }])).toEqual([]);
});
it("omits disabled profiles from the branch only when the hide setting is on", () => {
const mixedAgent = {
name: "claude-code",
profiles: [
{ id: "profile-1", name: "Default", agentDisplayName: AGENT_DISPLAY_NAME },
{ id: "profile-2", name: "Retired", agentDisplayName: AGENT_DISPLAY_NAME, enabled: false },
{ id: "profile-3", name: "Legacy", agentDisplayName: AGENT_DISPLAY_NAME },
],
};
const [agent] = buildAgentsBranch([mixedAgent], undefined, true);
// Enabled and legacy (no `enabled` field) profiles stay; only `enabled ===
// false` is omitted.
expect(hrefsOf(agent.children ?? [])).toEqual([
"/settings/agents/claude-code/profiles/profile-1",
"/settings/agents/claude-code/profiles/profile-3",
]);
});
it("keeps disabled profiles listed when the hide setting is off or absent", () => {
const mixedAgent = {
name: "claude-code",
profiles: [
{ id: "profile-1", name: "Default", agentDisplayName: AGENT_DISPLAY_NAME },
{ id: "profile-2", name: "Retired", agentDisplayName: AGENT_DISPLAY_NAME, enabled: false },
],
};
const [agent] = buildAgentsBranch([mixedAgent]);
const [agentWithOff] = buildAgentsBranch([mixedAgent], undefined, false);
expect(hrefsOf(agent.children ?? [])).toHaveLength(2);
expect(hrefsOf(agentWithOff.children ?? [])).toHaveLength(2);
});
});
describe("buildExecutorsBranch", () => {
it("uses the executor-scoped profile route, the one with an executor crumb", () => {
const [executor] = buildExecutorsBranch(EXECUTORS);
expect(executor.href).toBe("/settings/executor/exec-1");
expect(hrefsOf(executor.children ?? [])).toEqual([
"/settings/executor/exec-1/profile/exec-profile-1",
]);
});
it("makes a configured Kubernetes executor disclosure-only and links its profile root", () => {
const [executor] = buildExecutorsBranch([
{
id: "cluster/primary",
name: "Kubernetes",
type: "k8s",
profiles: [{ id: "profile/primary", name: "Primary" }],
},
]);
expect(executor.href).toBeNull();
expect(hrefsOf(executor.children ?? [])).toEqual(["/settings/executors/profile%2Fprimary"]);
});
it("keeps the Kubernetes connection route reachable for an executor without profiles", () => {
const [executor] = buildExecutorsBranch([
{
id: "cluster/primary",
name: "Kubernetes",
type: "k8s",
profiles: [],
},
]);
expect(executor.href).toBe("/settings/executors/k8s/cluster%2Fprimary");
expect(executor.children).toEqual([]);
});
});
describe("findActiveNodePath", () => {
const forest = [
buildBranchRoot(
{
href: WORKSPACES_HREF,
labelKey: "common:workspaces",
activePrefixes: ["/settings/workspaces/", "/settings/workspace/"],
},
buildWorkspacesBranch(WORKSPACES),
),
buildBranchRoot(
{ href: "/settings/agents", labelKey: "common:agents" },
buildAgentsBranch(AGENTS),
),
buildBranchRoot(
{
href: "/settings/executors",
labelKey: "common:executors",
activePrefixes: ["/settings/executors/", "/settings/executor/"],
},
buildExecutorsBranch(EXECUTORS),
),
];
it("descends to the deepest node owning the route", () => {
expect(
findActiveNodePath(forest, `/settings/workspaces/${WORKSPACE_ID}/integrations/github`),
).toEqual([
settingsMenuRowKey(WORKSPACES_HREF),
`workspace:${WORKSPACE_ID}`,
`workspace:${WORKSPACE_ID}:integrations`,
`workspace:${WORKSPACE_ID}:integrations:github`,
]);
});
it("stops at the workspace when the route is its own page", () => {
expect(findActiveNodePath(forest, `/settings/workspaces/${WORKSPACE_ID}`)).toEqual([
settingsMenuRowKey(WORKSPACES_HREF),
`workspace:${WORKSPACE_ID}`,
]);
});
it("reaches a profile through an agent that has no page", () => {
expect(findActiveNodePath(forest, "/settings/agents/claude-code/profiles/profile-2")).toEqual([
settingsMenuRowKey("/settings/agents"),
"agent:claude-code",
"agent:claude-code:profile:profile-2",
]);
});
it("reaches a Kubernetes profile through its disclosure-only executor", () => {
const kubernetesForest = [
buildBranchRoot(
{
href: EXECUTORS_HREF,
labelKey: "common:executors",
activePrefixes: ["/settings/executors/", "/settings/executor/"],
},
buildExecutorsBranch([
{
id: "cluster-primary",
name: "Kubernetes",
type: "k8s",
profiles: [{ id: "profile-primary", name: "Primary" }],
},
]),
),
];
expect(findActiveNodePath(kubernetesForest, `${EXECUTORS_HREF}/profile-primary`)).toEqual([
settingsMenuRowKey(EXECUTORS_HREF),
"executor:cluster-primary",
"executor:cluster-primary:profile:profile-primary",
]);
});
it("stops at the row when the route has no node — the row stays active", () => {
// The install catalogue is not an agent, and the flat profile spelling has
// no executor to nest under. Both are the row's own pages.
expect(findActiveNodePath(forest, "/settings/agents/browse")).toEqual([
settingsMenuRowKey("/settings/agents"),
]);
expect(findActiveNodePath(forest, "/settings/executors/exec-profile-1")).toEqual([
settingsMenuRowKey(EXECUTORS_HREF),
]);
});
it("claims the row's alternate route spellings through activePrefixes", () => {
// `/settings/executor/<id>` is not under `/settings/executors`, but the
// Executors row owns it — and so must its branch.
expect(findActiveNodePath(forest, "/settings/executor/exec-1")).toEqual([
settingsMenuRowKey(EXECUTORS_HREF),
"executor:exec-1",
]);
});
it("returns nothing for a route outside every branch", () => {
expect(findActiveNodePath(forest, "/settings/preferences/appearance")).toEqual([]);
});
it("does not let one workspace claim another's routes by prefix", () => {
const twoWorkspaces = buildWorkspacesBranch([
{ id: "ws-1", name: "One" },
{ id: "ws-10", name: "Ten" },
]);
expect(findActiveNodePath(twoWorkspaces, "/settings/workspaces/ws-10/secrets")).toEqual([
"workspace:ws-10",
"workspace:ws-10:secrets",
]);
});
});
describe("key path helpers", () => {
const workspaces = buildWorkspacesBranch(WORKSPACES);
it("names every ancestor needed to reveal a node", () => {
expect(findNodeKeyPath(workspaces, `workspace:${WORKSPACE_ID}:integrations:jira`)).toEqual([
`workspace:${WORKSPACE_ID}`,
`workspace:${WORKSPACE_ID}:integrations`,
`workspace:${WORKSPACE_ID}:integrations:jira`,
]);
});
it("names the node and everything under it when collapsing", () => {
const keys = findSubtreeKeys(workspaces, `workspace:${WORKSPACE_ID}:integrations`);
expect(keys[0]).toBe(`workspace:${WORKSPACE_ID}:integrations`);
expect(keys).toContain(`workspace:${WORKSPACE_ID}:integrations:github`);
expect(keys).not.toContain(`workspace:${WORKSPACE_ID}:secrets`);
});
it("returns nothing for a key no node carries — a stale stored branch", () => {
expect(findNodeKeyPath(workspaces, "workspace:deleted")).toEqual([]);
expect(findSubtreeKeys(workspaces, "workspace:deleted")).toEqual([]);
});
});