diff --git a/src/cli/skills-cli.commands.test.ts b/src/cli/skills-cli.commands.test.ts new file mode 100644 index 00000000000..48b4164903d --- /dev/null +++ b/src/cli/skills-cli.commands.test.ts @@ -0,0 +1,124 @@ +import { Command } from "commander"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const loadConfigMock = vi.fn(); +const resolveAgentWorkspaceDirMock = vi.fn(); +const resolveDefaultAgentIdMock = vi.fn(); +const buildWorkspaceSkillStatusMock = vi.fn(); +const formatSkillsListMock = vi.fn(); +const formatSkillInfoMock = vi.fn(); +const formatSkillsCheckMock = vi.fn(); + +const runtime = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), +}; + +vi.mock("../config/config.js", () => ({ + loadConfig: loadConfigMock, +})); + +vi.mock("../agents/agent-scope.js", () => ({ + resolveAgentWorkspaceDir: resolveAgentWorkspaceDirMock, + resolveDefaultAgentId: resolveDefaultAgentIdMock, +})); + +vi.mock("../agents/skills-status.js", () => ({ + buildWorkspaceSkillStatus: buildWorkspaceSkillStatusMock, +})); + +vi.mock("./skills-cli.format.js", () => ({ + formatSkillsList: formatSkillsListMock, + formatSkillInfo: formatSkillInfoMock, + formatSkillsCheck: formatSkillsCheckMock, +})); + +vi.mock("../runtime.js", () => ({ + defaultRuntime: runtime, +})); + +let registerSkillsCli: typeof import("./skills-cli.js").registerSkillsCli; + +beforeAll(async () => { + ({ registerSkillsCli } = await import("./skills-cli.js")); +}); + +describe("registerSkillsCli", () => { + const report = { + workspaceDir: "/tmp/workspace", + managedSkillsDir: "/tmp/workspace/.skills", + skills: [], + }; + + async function runCli(args: string[]) { + const program = new Command(); + registerSkillsCli(program); + await program.parseAsync(args, { from: "user" }); + } + + beforeEach(() => { + vi.clearAllMocks(); + loadConfigMock.mockReturnValue({ gateway: {} }); + resolveDefaultAgentIdMock.mockReturnValue("main"); + resolveAgentWorkspaceDirMock.mockReturnValue("/tmp/workspace"); + buildWorkspaceSkillStatusMock.mockReturnValue(report); + formatSkillsListMock.mockReturnValue("skills-list-output"); + formatSkillInfoMock.mockReturnValue("skills-info-output"); + formatSkillsCheckMock.mockReturnValue("skills-check-output"); + }); + + it("runs list command with resolved report and formatter options", async () => { + await runCli(["skills", "list", "--eligible", "--verbose", "--json"]); + + expect(buildWorkspaceSkillStatusMock).toHaveBeenCalledWith("/tmp/workspace", { + config: { gateway: {} }, + }); + expect(formatSkillsListMock).toHaveBeenCalledWith( + report, + expect.objectContaining({ + eligible: true, + verbose: true, + json: true, + }), + ); + expect(runtime.log).toHaveBeenCalledWith("skills-list-output"); + }); + + it("runs info command and forwards skill name", async () => { + await runCli(["skills", "info", "peekaboo", "--json"]); + + expect(formatSkillInfoMock).toHaveBeenCalledWith( + report, + "peekaboo", + expect.objectContaining({ json: true }), + ); + expect(runtime.log).toHaveBeenCalledWith("skills-info-output"); + }); + + it("runs check command and writes formatter output", async () => { + await runCli(["skills", "check"]); + + expect(formatSkillsCheckMock).toHaveBeenCalledWith(report, expect.any(Object)); + expect(runtime.log).toHaveBeenCalledWith("skills-check-output"); + }); + + it("uses list formatter for default skills action", async () => { + await runCli(["skills"]); + + expect(formatSkillsListMock).toHaveBeenCalledWith(report, {}); + expect(runtime.log).toHaveBeenCalledWith("skills-list-output"); + }); + + it("reports runtime errors when report loading fails", async () => { + loadConfigMock.mockImplementationOnce(() => { + throw new Error("config exploded"); + }); + + await runCli(["skills", "list"]); + + expect(runtime.error).toHaveBeenCalledWith("Error: config exploded"); + expect(runtime.exit).toHaveBeenCalledWith(1); + expect(buildWorkspaceSkillStatusMock).not.toHaveBeenCalled(); + }); +}); diff --git a/src/cli/skills-cli.ts b/src/cli/skills-cli.ts index 6ed962564df..49f288f36c0 100644 --- a/src/cli/skills-cli.ts +++ b/src/cli/skills-cli.ts @@ -13,6 +13,27 @@ export type { } from "./skills-cli.format.js"; export { formatSkillInfo, formatSkillsCheck, formatSkillsList } from "./skills-cli.format.js"; +type SkillStatusReport = Awaited< + ReturnType<(typeof import("../agents/skills-status.js"))["buildWorkspaceSkillStatus"]> +>; + +async function loadSkillsStatusReport(): Promise { + const config = loadConfig(); + const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); + const { buildWorkspaceSkillStatus } = await import("../agents/skills-status.js"); + return buildWorkspaceSkillStatus(workspaceDir, { config }); +} + +async function runSkillsAction(render: (report: SkillStatusReport) => string): Promise { + try { + const report = await loadSkillsStatusReport(); + defaultRuntime.log(render(report)); + } catch (err) { + defaultRuntime.error(String(err)); + defaultRuntime.exit(1); + } +} + /** * Register the skills CLI commands */ @@ -33,16 +54,7 @@ export function registerSkillsCli(program: Command) { .option("--eligible", "Show only eligible (ready to use) skills", false) .option("-v, --verbose", "Show more details including missing requirements", false) .action(async (opts) => { - try { - const config = loadConfig(); - const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); - const { buildWorkspaceSkillStatus } = await import("../agents/skills-status.js"); - const report = buildWorkspaceSkillStatus(workspaceDir, { config }); - defaultRuntime.log(formatSkillsList(report, opts)); - } catch (err) { - defaultRuntime.error(String(err)); - defaultRuntime.exit(1); - } + await runSkillsAction((report) => formatSkillsList(report, opts)); }); skills @@ -51,16 +63,7 @@ export function registerSkillsCli(program: Command) { .argument("", "Skill name") .option("--json", "Output as JSON", false) .action(async (name, opts) => { - try { - const config = loadConfig(); - const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); - const { buildWorkspaceSkillStatus } = await import("../agents/skills-status.js"); - const report = buildWorkspaceSkillStatus(workspaceDir, { config }); - defaultRuntime.log(formatSkillInfo(report, name, opts)); - } catch (err) { - defaultRuntime.error(String(err)); - defaultRuntime.exit(1); - } + await runSkillsAction((report) => formatSkillInfo(report, name, opts)); }); skills @@ -68,29 +71,11 @@ export function registerSkillsCli(program: Command) { .description("Check which skills are ready vs missing requirements") .option("--json", "Output as JSON", false) .action(async (opts) => { - try { - const config = loadConfig(); - const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); - const { buildWorkspaceSkillStatus } = await import("../agents/skills-status.js"); - const report = buildWorkspaceSkillStatus(workspaceDir, { config }); - defaultRuntime.log(formatSkillsCheck(report, opts)); - } catch (err) { - defaultRuntime.error(String(err)); - defaultRuntime.exit(1); - } + await runSkillsAction((report) => formatSkillsCheck(report, opts)); }); // Default action (no subcommand) - show list skills.action(async () => { - try { - const config = loadConfig(); - const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); - const { buildWorkspaceSkillStatus } = await import("../agents/skills-status.js"); - const report = buildWorkspaceSkillStatus(workspaceDir, { config }); - defaultRuntime.log(formatSkillsList(report, {})); - } catch (err) { - defaultRuntime.error(String(err)); - defaultRuntime.exit(1); - } + await runSkillsAction((report) => formatSkillsList(report, {})); }); }