import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("../lib/config.js", () => ({ getConfig: vi.fn(() => ({ apiUrl: "https://api.skills-hub.ai", accessToken: "tok", })), getAuthHeader: vi.fn(() => ({ Authorization: "Bearer tok" })), saveConfig: vi.fn(), ensureAuth: vi.fn(), })); vi.mock("../lib/api-client.js", () => ({ apiRequest: vi.fn(), })); import { orgPolicyCommand } from "./org-policy.js"; import { apiRequest } from "../lib/api-client.js"; import { ensureAuth } from "../lib/config.js"; const mockApiRequest = vi.mocked(apiRequest); const mockEnsureAuth = vi.mocked(ensureAuth); const mockEntry = { id: "pe-1", skill: { slug: "code-review", name: "Code Review" }, department: null, team: null, createdAt: "2026-07-01T00:00:00Z", }; const mockDeptEntry = { id: "pe-2", skill: { slug: "risky-skill", name: "Risky Skill" }, department: { slug: "eng", name: "Engineering" }, team: null, createdAt: "2026-07-01T00:00:00Z", }; const mockTeamEntry = { id: "pe-3", skill: { slug: "blocked-skill", name: "Blocked Skill" }, department: null, team: { slug: "identity", name: "Identity", department: { slug: "eng", name: "Engineering" }, }, createdAt: "2026-07-01T00:00:00Z", }; beforeEach(() => { vi.clearAllMocks(); }); describe("org policy get", () => { it("requires authentication", async () => { mockApiRequest .mockResolvedValueOnce({ slug: "acme", policyMode: "OPEN" }) .mockResolvedValueOnce([]); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync(["node", "policy", "get", "acme"], { from: "node", }); expect(mockEnsureAuth).toHaveBeenCalled(); log.mockRestore(); }); it("fetches org detail and policy entries, prints the open-mode explanation for OPEN", async () => { mockApiRequest .mockResolvedValueOnce({ slug: "acme", policyMode: "OPEN" }) .mockResolvedValueOnce([]); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync(["node", "policy", "get", "acme"], { from: "node", }); expect(mockApiRequest).toHaveBeenCalledWith("/api/v1/orgs/acme"); expect(mockApiRequest).toHaveBeenCalledWith("/api/v1/orgs/acme/policy"); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("OPEN"); expect(output).toContain("Members can install any skill from the catalog"); log.mockRestore(); }); it("lists entries for ALLOWLIST mode", async () => { mockApiRequest .mockResolvedValueOnce({ slug: "acme", policyMode: "ALLOWLIST" }) .mockResolvedValueOnce([mockEntry]); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync(["node", "policy", "get", "acme"], { from: "node", }); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("1 org-level allowed skill(s)"); expect(output).toContain("code-review"); expect(output).not.toContain("Members can install any skill"); log.mockRestore(); }); it("lists entries for BLOCKLIST mode with the blocked verb", async () => { mockApiRequest .mockResolvedValueOnce({ slug: "acme", policyMode: "BLOCKLIST" }) .mockResolvedValueOnce([mockEntry]); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync(["node", "policy", "get", "acme"], { from: "node", }); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("1 org-level blocked skill(s)"); log.mockRestore(); }); it("prints raw JSON with --json", async () => { mockApiRequest .mockResolvedValueOnce({ slug: "acme", policyMode: "ALLOWLIST" }) .mockResolvedValueOnce([mockEntry]); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync( ["node", "policy", "get", "acme", "--json"], { from: "node" }, ); expect(log).toHaveBeenCalledTimes(1); const parsed = JSON.parse(log.mock.calls[0][0] as string) as { policyMode: string; entries: Array<{ id: string }>; }; expect(parsed.policyMode).toBe("ALLOWLIST"); expect(parsed.entries).toHaveLength(1); expect(parsed.entries[0].id).toBe("pe-1"); log.mockRestore(); }); it("URL-encodes the org slug", async () => { mockApiRequest .mockResolvedValueOnce({ slug: "my org", policyMode: "OPEN" }) .mockResolvedValueOnce([]); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync(["node", "policy", "get", "my org"], { from: "node", }); expect(mockApiRequest).toHaveBeenCalledWith("/api/v1/orgs/my%20org"); expect(mockApiRequest).toHaveBeenCalledWith("/api/v1/orgs/my%20org/policy"); log.mockRestore(); }); it("handles API errors with exit 1", async () => { mockApiRequest.mockRejectedValue(new Error("Not a member")); const errLog = vi.spyOn(console, "error").mockImplementation(() => {}); const exit = vi.spyOn(process, "exit").mockImplementation(() => { throw new Error("exit"); }); await expect( orgPolicyCommand.parseAsync(["node", "policy", "get", "acme"], { from: "node", }), ).rejects.toThrow("exit"); expect(exit).toHaveBeenCalledWith(1); exit.mockRestore(); errLog.mockRestore(); }); }); describe("org policy set-mode", () => { it("requires authentication", async () => { mockApiRequest.mockResolvedValue(undefined); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync( ["node", "policy", "set-mode", "acme", "OPEN"], { from: "node" }, ); expect(mockEnsureAuth).toHaveBeenCalled(); log.mockRestore(); }); it("uppercases the mode and PATCHes the policy", async () => { mockApiRequest.mockResolvedValue(undefined); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync( ["node", "policy", "set-mode", "acme", "allowlist"], { from: "node" }, ); expect(mockApiRequest).toHaveBeenCalledWith("/api/v1/orgs/acme/policy", { method: "PATCH", body: JSON.stringify({ mode: "ALLOWLIST" }), }); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("Policy mode set to ALLOWLIST"); log.mockRestore(); }); it("rejects invalid modes with exit 1 and no API call", async () => { const errLog = vi.spyOn(console, "error").mockImplementation(() => {}); const exit = vi.spyOn(process, "exit").mockImplementation(() => { throw new Error("exit"); }); await expect( orgPolicyCommand.parseAsync( ["node", "policy", "set-mode", "acme", "superopen"], { from: "node" }, ), ).rejects.toThrow("exit"); expect(exit).toHaveBeenCalledWith(1); expect(mockApiRequest).not.toHaveBeenCalled(); const errOutput = errLog.mock.calls.map((c) => c[0]).join("\n"); expect(errOutput).toContain("Invalid mode"); exit.mockRestore(); errLog.mockRestore(); }); it("handles API errors with exit 1", async () => { mockApiRequest.mockRejectedValue(new Error("Admin role required")); const errLog = vi.spyOn(console, "error").mockImplementation(() => {}); const exit = vi.spyOn(process, "exit").mockImplementation(() => { throw new Error("exit"); }); await expect( orgPolicyCommand.parseAsync( ["node", "policy", "set-mode", "acme", "BLOCKLIST"], { from: "node" }, ), ).rejects.toThrow("exit"); expect(exit).toHaveBeenCalledWith(1); exit.mockRestore(); errLog.mockRestore(); }); }); describe("org policy add", () => { it("requires authentication", async () => { mockApiRequest.mockResolvedValue(undefined); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync( ["node", "policy", "add", "acme", "code-review"], { from: "node" }, ); expect(mockEnsureAuth).toHaveBeenCalled(); log.mockRestore(); }); it("POSTs the skill slug to the policy list", async () => { mockApiRequest.mockResolvedValue(undefined); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync( ["node", "policy", "add", "acme", "code-review"], { from: "node" }, ); expect(mockApiRequest).toHaveBeenCalledWith("/api/v1/orgs/acme/policy", { method: "POST", body: JSON.stringify({ skillSlug: "code-review" }), }); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("Added code-review"); log.mockRestore(); }); it("handles API errors with exit 1", async () => { mockApiRequest.mockRejectedValue(new Error("Skill not found")); const errLog = vi.spyOn(console, "error").mockImplementation(() => {}); const exit = vi.spyOn(process, "exit").mockImplementation(() => { throw new Error("exit"); }); await expect( orgPolicyCommand.parseAsync( ["node", "policy", "add", "acme", "no-such-skill"], { from: "node" }, ), ).rejects.toThrow("exit"); expect(exit).toHaveBeenCalledWith(1); exit.mockRestore(); errLog.mockRestore(); }); }); describe("org policy remove", () => { it("requires authentication", async () => { mockApiRequest.mockResolvedValue(undefined); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync( ["node", "policy", "remove", "acme", "code-review"], { from: "node" }, ); expect(mockEnsureAuth).toHaveBeenCalled(); log.mockRestore(); }); it("DELETEs the policy entry", async () => { mockApiRequest.mockResolvedValue(undefined); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync( ["node", "policy", "remove", "acme", "code-review"], { from: "node" }, ); expect(mockApiRequest).toHaveBeenCalledWith( "/api/v1/orgs/acme/policy/code-review", { method: "DELETE" }, ); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("Removed code-review"); log.mockRestore(); }); it("URL-encodes the org slug and skill slug", async () => { mockApiRequest.mockResolvedValue(undefined); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync( ["node", "policy", "remove", "my org", "weird skill"], { from: "node" }, ); expect(mockApiRequest).toHaveBeenCalledWith( "/api/v1/orgs/my%20org/policy/weird%20skill", { method: "DELETE" }, ); log.mockRestore(); }); it("handles API errors with exit 1", async () => { mockApiRequest.mockRejectedValue(new Error("Entry not found")); const errLog = vi.spyOn(console, "error").mockImplementation(() => {}); const exit = vi.spyOn(process, "exit").mockImplementation(() => { throw new Error("exit"); }); await expect( orgPolicyCommand.parseAsync( ["node", "policy", "remove", "acme", "code-review"], { from: "node" }, ), ).rejects.toThrow("exit"); expect(exit).toHaveBeenCalledWith(1); exit.mockRestore(); errLog.mockRestore(); }); }); describe("org policy get — scoped entries", () => { it("renders dept/team-scoped entries as blocks separately from org-level", async () => { mockApiRequest .mockResolvedValueOnce({ slug: "acme", policyMode: "ALLOWLIST" }) .mockResolvedValueOnce([mockEntry, mockDeptEntry, mockTeamEntry]); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync(["node", "policy", "get", "acme"], { from: "node", }); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("1 org-level allowed skill(s)"); expect(output).toContain("2 department/team block(s)"); expect(output).toContain("blocked for dept:eng"); expect(output).toContain("blocked for team:eng/identity"); log.mockRestore(); }); it("shows scoped blocks even under OPEN mode", async () => { mockApiRequest .mockResolvedValueOnce({ slug: "acme", policyMode: "OPEN" }) .mockResolvedValueOnce([mockDeptEntry]); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync(["node", "policy", "get", "acme"], { from: "node", }); const output = log.mock.calls.map((c) => c[0]).join("\n"); expect(output).toContain("Members can install any skill"); expect(output).toContain("1 department/team block(s)"); expect(output).toContain("blocked for dept:eng"); log.mockRestore(); }); }); describe("org policy add/remove — scope flags", () => { it("add --department includes departmentSlug in the POST body", async () => { mockApiRequest.mockResolvedValue(undefined); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync( ["node", "policy", "add", "acme", "risky", "--department", "eng"], { from: "node" }, ); expect(mockApiRequest).toHaveBeenCalledWith("/api/v1/orgs/acme/policy", { method: "POST", body: JSON.stringify({ skillSlug: "risky", departmentSlug: "eng" }), }); log.mockRestore(); }); it("add --team --department includes both slugs in the POST body", async () => { mockApiRequest.mockResolvedValue(undefined); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync( [ "node", "policy", "add", "acme", "risky", "--department", "eng", "--team", "identity", ], { from: "node" }, ); expect(mockApiRequest).toHaveBeenCalledWith("/api/v1/orgs/acme/policy", { method: "POST", body: JSON.stringify({ skillSlug: "risky", departmentSlug: "eng", teamSlug: "identity", }), }); log.mockRestore(); }); it("remove --department appends the department query param to DELETE", async () => { mockApiRequest.mockResolvedValue(undefined); const log = vi.spyOn(console, "log").mockImplementation(() => {}); await orgPolicyCommand.parseAsync( ["node", "policy", "remove", "acme", "risky", "--department", "eng"], { from: "node" }, ); expect(mockApiRequest).toHaveBeenCalledWith( "/api/v1/orgs/acme/policy/risky?department=eng", { method: "DELETE" }, ); log.mockRestore(); }); it("--team without --department exits 1 with no API call (add)", async () => { const errLog = vi.spyOn(console, "error").mockImplementation(() => {}); const exit = vi.spyOn(process, "exit").mockImplementation(() => { throw new Error("exit"); }); await expect( orgPolicyCommand.parseAsync( ["node", "policy", "add", "acme", "risky", "--team", "identity"], { from: "node" }, ), ).rejects.toThrow("exit"); expect(exit).toHaveBeenCalledWith(1); expect(mockApiRequest).not.toHaveBeenCalled(); exit.mockRestore(); errLog.mockRestore(); }); });