import { describe, expect, it } from "@effect/vitest";import type { EffectivePolicy, Owner } from "@executor-js/sdk/shared"; import { buildAccountGroups, type ToolSummary } from "./tool-tree"; // A trivial always-approve plugin default — the policy field is required on a// ToolSummary but irrelevant to grouping.const approve: EffectivePolicy = { action: "approve", source: "plugin-default", pattern: "*",}; const tool = (input: { readonly id: string; readonly name: string; readonly owner?: Owner; readonly connection?: string;}): ToolSummary => ({ id: input.id, name: input.name, description: undefined, policy: approve, owner: input.owner, connection: input.connection,}); describe("buildAccountGroups", () => { it("groups tools by (owner, connection) and badges each section by owner", () => { const groups = buildAccountGroups([ tool({ id: "u:axiom-mcp:axiom.query", name: "axiom.query", owner: "user", connection: "axiom-mcp", }), tool({ id: "o:vercel-api:vercel.deploy", name: "vercel.deploy", owner: "org", connection: "vercel-api", }), ]); // Workspace (org) sorts before Personal (user). expect(groups.map((g) => g.label)).toEqual(["Workspace · vercel-api", "Personal · axiom-mcp"]); expect(groups.map((g) => g.owner)).toEqual(["org", "user"]); expect(groups[0]!.tools.map((t) => t.name)).toEqual(["vercel.deploy"]); expect(groups[1]!.tools.map((t) => t.name)).toEqual(["axiom.query"]); }); it("Axiom regression: a user-owned connection's tools render in the merged, account-grouped view", () => { // 18 user-owned tools, zero org rows — the case that showed 0 under the old // owner=org default. Here they all land in one Personal section. const tools = Array.from({ length: 18 }, (_, i) => tool({ id: `u:axiom-mcp:axiom.t${i}`, name: `axiom.t${i}`, owner: "user", connection: "axiom-mcp", }), ); const groups = buildAccountGroups(tools); expect(groups).toHaveLength(1); expect(groups[0]!.owner).toBe("user"); expect(groups[0]!.label).toBe("Personal · axiom-mcp"); expect(groups[0]!.tools).toHaveLength(18); }); it("does NOT dedupe the same tool name across two connections — it appears under each", () => { const groups = buildAccountGroups([ tool({ id: "o:prod:vercel.deploy", name: "vercel.deploy", owner: "org", connection: "prod" }), tool({ id: "u:scratch:vercel.deploy", name: "vercel.deploy", owner: "user", connection: "scratch", }), ]); expect(groups).toHaveLength(2); // Same tool name, once per account. expect(groups.flatMap((g) => g.tools).map((t) => t.name)).toEqual([ "vercel.deploy", "vercel.deploy", ]); expect(groups.map((g) => g.label)).toEqual(["Workspace · prod", "Personal · scratch"]); }); it("sorts multiple connections within an owner by connection name", () => { const groups = buildAccountGroups([ tool({ id: "o:zeta:i.a", name: "i.a", owner: "org", connection: "zeta" }), tool({ id: "o:alpha:i.b", name: "i.b", owner: "org", connection: "alpha" }), ]); expect(groups.map((g) => g.connection)).toEqual(["alpha", "zeta"]); }); it("falls back to a single owner-only label when tools carry no owner/connection (flat case)", () => { const groups = buildAccountGroups([ tool({ id: "vercel.deploy", name: "vercel.deploy" }), tool({ id: "vercel.list", name: "vercel.list" }), ]); // Default owner is org; no connection → owner-only label, single group. expect(groups).toHaveLength(1); expect(groups[0]!.label).toBe("Workspace"); expect(groups[0]!.tools).toHaveLength(2); });});
import { describe, expect, it } from "@effect/vitest";
import type { EffectivePolicy, Owner } from "@executor-js/sdk/shared";
import { buildAccountGroups, type ToolSummary } from "./tool-tree";
// A trivial always-approve plugin default — the policy field is required on a
// ToolSummary but irrelevant to grouping.
const approve: EffectivePolicy = {
action: "approve",
source: "plugin-default",
pattern: "*",
};
const tool = (input: {
readonly id: string;
readonly name: string;
readonly owner?: Owner;
readonly connection?: string;
}): ToolSummary => ({
id: input.id,
name: input.name,
description: undefined,
policy: approve,
owner: input.owner,
connection: input.connection,
});
describe("buildAccountGroups", () => {
it("groups tools by (owner, connection) and badges each section by owner", () => {
const groups = buildAccountGroups([
tool({
id: "u:axiom-mcp:axiom.query",
name: "axiom.query",
owner: "user",
connection: "axiom-mcp",
}),
tool({
id: "o:vercel-api:vercel.deploy",
name: "vercel.deploy",
owner: "org",
connection: "vercel-api",
}),
]);
// Workspace (org) sorts before Personal (user).
expect(groups.map((g) => g.label)).toEqual(["Workspace · vercel-api", "Personal · axiom-mcp"]);
expect(groups.map((g) => g.owner)).toEqual(["org", "user"]);
expect(groups[0]!.tools.map((t) => t.name)).toEqual(["vercel.deploy"]);
expect(groups[1]!.tools.map((t) => t.name)).toEqual(["axiom.query"]);
});
it("Axiom regression: a user-owned connection's tools render in the merged, account-grouped view", () => {
// 18 user-owned tools, zero org rows — the case that showed 0 under the old
// owner=org default. Here they all land in one Personal section.
const tools = Array.from({ length: 18 }, (_, i) =>
tool({
id: `u:axiom-mcp:axiom.t${i}`,
name: `axiom.t${i}`,
owner: "user",
connection: "axiom-mcp",
}),
);
const groups = buildAccountGroups(tools);
expect(groups).toHaveLength(1);
expect(groups[0]!.owner).toBe("user");
expect(groups[0]!.label).toBe("Personal · axiom-mcp");
expect(groups[0]!.tools).toHaveLength(18);
});
it("does NOT dedupe the same tool name across two connections — it appears under each", () => {
const groups = buildAccountGroups([
tool({ id: "o:prod:vercel.deploy", name: "vercel.deploy", owner: "org", connection: "prod" }),
tool({
id: "u:scratch:vercel.deploy",
name: "vercel.deploy",
owner: "user",
connection: "scratch",
}),
]);
expect(groups).toHaveLength(2);
// Same tool name, once per account.
expect(groups.flatMap((g) => g.tools).map((t) => t.name)).toEqual([
"vercel.deploy",
"vercel.deploy",
]);
expect(groups.map((g) => g.label)).toEqual(["Workspace · prod", "Personal · scratch"]);
});
it("sorts multiple connections within an owner by connection name", () => {
const groups = buildAccountGroups([
tool({ id: "o:zeta:i.a", name: "i.a", owner: "org", connection: "zeta" }),
tool({ id: "o:alpha:i.b", name: "i.b", owner: "org", connection: "alpha" }),
]);
expect(groups.map((g) => g.connection)).toEqual(["alpha", "zeta"]);
});
it("falls back to a single owner-only label when tools carry no owner/connection (flat case)", () => {
const groups = buildAccountGroups([
tool({ id: "vercel.deploy", name: "vercel.deploy" }),
tool({ id: "vercel.list", name: "vercel.list" }),
]);
// Default owner is org; no connection → owner-only label, single group.
expect(groups).toHaveLength(1);
expect(groups[0]!.label).toBe("Workspace");
expect(groups[0]!.tools).toHaveLength(2);
});
});