Skip to content

Commit 8df4f8f

Browse files
Merge branch 'worktree-agent-a7072e57'
2 parents 8792c9f + 9767c49 commit 8df4f8f

4 files changed

Lines changed: 687 additions & 0 deletions

File tree

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
3+
vi.mock("../../utils/logger.js", () => ({
4+
logger: {
5+
debug: vi.fn(),
6+
info: vi.fn(),
7+
warn: vi.fn(),
8+
error: vi.fn(),
9+
},
10+
}));
11+
12+
import { registerPrompts } from "../index.js";
13+
14+
type PromptHandler = (params: Record<string, string | undefined>) => {
15+
messages: Array<{
16+
role: string;
17+
content: { type: string; text: string };
18+
}>;
19+
};
20+
21+
function createMockServer() {
22+
const prompts = new Map<string, PromptHandler>();
23+
return {
24+
prompt: vi.fn(
25+
(
26+
name: string,
27+
_description: string,
28+
_schema: unknown,
29+
handler: PromptHandler
30+
) => {
31+
prompts.set(name, handler);
32+
}
33+
),
34+
prompts,
35+
};
36+
}
37+
38+
describe("prompts", () => {
39+
let server: ReturnType<typeof createMockServer>;
40+
41+
beforeEach(() => {
42+
vi.clearAllMocks();
43+
server = createMockServer();
44+
registerPrompts(server as unknown as Parameters<typeof registerPrompts>[0]);
45+
});
46+
47+
it("registers all 4 prompts", () => {
48+
expect(server.prompts.size).toBe(4);
49+
expect(server.prompts.has("deploy-stack")).toBe(true);
50+
expect(server.prompts.has("troubleshoot-container")).toBe(true);
51+
expect(server.prompts.has("security-audit")).toBe(true);
52+
expect(server.prompts.has("cleanup-environment")).toBe(true);
53+
});
54+
55+
describe("deploy-stack", () => {
56+
it("returns messages with correct tool names", () => {
57+
const handler = server.prompts.get("deploy-stack")!;
58+
const result = handler({ environmentId: "env-1" });
59+
60+
expect(result.messages).toHaveLength(1);
61+
expect(result.messages[0].role).toBe("user");
62+
expect(result.messages[0].content.type).toBe("text");
63+
64+
const text = result.messages[0].content.text;
65+
expect(text).toContain("environment env-1");
66+
expect(text).toContain("arcane_environment_get");
67+
expect(text).toContain("arcane_project_create");
68+
expect(text).toContain("arcane_project_up");
69+
expect(text).toContain("arcane_container_list");
70+
});
71+
72+
it("includes compose path when provided", () => {
73+
const handler = server.prompts.get("deploy-stack")!;
74+
const result = handler({
75+
environmentId: "env-1",
76+
composePath: "/opt/docker-compose.yml",
77+
});
78+
79+
const text = result.messages[0].content.text;
80+
expect(text).toContain("/opt/docker-compose.yml");
81+
});
82+
83+
it("asks for compose content when no path given", () => {
84+
const handler = server.prompts.get("deploy-stack")!;
85+
const result = handler({ environmentId: "env-1" });
86+
87+
const text = result.messages[0].content.text;
88+
expect(text).toContain("Ask the user for the compose file content");
89+
});
90+
});
91+
92+
describe("security-audit", () => {
93+
it("references vulnerability tools", () => {
94+
const handler = server.prompts.get("security-audit")!;
95+
const result = handler({ environmentId: "env-1" });
96+
97+
const text = result.messages[0].content.text;
98+
expect(text).toContain("arcane_vulnerability_get_scanner_status");
99+
expect(text).toContain("arcane_vulnerability_get_environment_summary");
100+
expect(text).toContain("arcane_vulnerability_list_all");
101+
expect(text).toContain("arcane_vulnerability_list");
102+
expect(text).toContain("arcane_image_update_check_all");
103+
expect(text).toContain("arcane_port_list");
104+
expect(text).toContain("arcane_network_list");
105+
});
106+
107+
it("returns proper message structure", () => {
108+
const handler = server.prompts.get("security-audit")!;
109+
const result = handler({ environmentId: "env-1" });
110+
111+
expect(result.messages).toHaveLength(1);
112+
expect(result.messages[0]).toHaveProperty("role", "user");
113+
expect(result.messages[0].content).toHaveProperty("type", "text");
114+
expect(typeof result.messages[0].content.text).toBe("string");
115+
});
116+
});
117+
118+
describe("troubleshoot-container", () => {
119+
it("returns diagnostic workflow with correct tools", () => {
120+
const handler = server.prompts.get("troubleshoot-container")!;
121+
const result = handler({
122+
environmentId: "env-1",
123+
containerId: "my-app",
124+
});
125+
126+
const text = result.messages[0].content.text;
127+
expect(text).toContain("my-app");
128+
expect(text).toContain("arcane_container_get");
129+
expect(text).toContain("arcane_dashboard_get_action_items");
130+
expect(text).toContain("arcane_image_update_check_by_id");
131+
});
132+
});
133+
134+
describe("cleanup-environment", () => {
135+
it("returns cleanup workflow with correct tools", () => {
136+
const handler = server.prompts.get("cleanup-environment")!;
137+
const result = handler({ environmentId: "env-1" });
138+
139+
const text = result.messages[0].content.text;
140+
expect(text).toContain("arcane_dashboard_get");
141+
expect(text).toContain("arcane_container_list");
142+
expect(text).toContain("arcane_image_prune");
143+
expect(text).toContain("arcane_volume_prune");
144+
expect(text).toContain("arcane_network_prune");
145+
});
146+
147+
it("returns proper message structure", () => {
148+
const handler = server.prompts.get("cleanup-environment")!;
149+
const result = handler({ environmentId: "env-1" });
150+
151+
expect(result.messages).toHaveLength(1);
152+
expect(result.messages[0].role).toBe("user");
153+
expect(result.messages[0].content.type).toBe("text");
154+
});
155+
});
156+
});
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest";
2+
3+
const mockClient = {
4+
get: vi.fn(),
5+
post: vi.fn(),
6+
put: vi.fn(),
7+
patch: vi.fn(),
8+
delete: vi.fn(),
9+
getBaseUrl: vi.fn(() => "https://arcane.test"),
10+
getDefaultEnvironmentId: vi.fn(() => "env-1"),
11+
};
12+
13+
vi.mock("../../client/arcane-client.js", () => ({
14+
getArcaneClient: vi.fn(() => mockClient),
15+
}));
16+
17+
vi.mock("../../utils/error-handler.js", () => ({
18+
formatError: vi.fn((err: unknown) =>
19+
err instanceof Error ? err.message : String(err)
20+
),
21+
}));
22+
23+
vi.mock("../../utils/logger.js", () => ({
24+
logger: {
25+
debug: vi.fn(),
26+
info: vi.fn(),
27+
warn: vi.fn(),
28+
error: vi.fn(),
29+
},
30+
}));
31+
32+
import { registerResources } from "../index.js";
33+
34+
type ResourceHandler = (uri: URL) => Promise<{
35+
contents: Array<{ uri: string; mimeType: string; text: string }>;
36+
}>;
37+
38+
function createMockServer() {
39+
const resources = new Map<string, ResourceHandler>();
40+
return {
41+
resource: vi.fn(
42+
(
43+
name: string,
44+
_uri: string,
45+
_meta: unknown,
46+
handler: ResourceHandler
47+
) => {
48+
resources.set(name, handler);
49+
}
50+
),
51+
resources,
52+
};
53+
}
54+
55+
describe("resources", () => {
56+
let server: ReturnType<typeof createMockServer>;
57+
58+
beforeEach(() => {
59+
vi.clearAllMocks();
60+
server = createMockServer();
61+
registerResources(server as unknown as Parameters<typeof registerResources>[0]);
62+
});
63+
64+
it("registers both resources", () => {
65+
expect(server.resources.has("arcane-environments")).toBe(true);
66+
expect(server.resources.has("arcane-version")).toBe(true);
67+
expect(server.resources.size).toBe(2);
68+
});
69+
70+
describe("arcane-environments", () => {
71+
it("returns formatted environment list", async () => {
72+
mockClient.get.mockResolvedValueOnce({
73+
data: [
74+
{ id: "env-1", name: "Production", apiUrl: "https://prod.arcane.test", status: "active" },
75+
{ id: "env-2", name: "Staging", status: "active" },
76+
],
77+
pagination: { total: 2, start: 0, limit: 100 },
78+
});
79+
80+
const handler = server.resources.get("arcane-environments")!;
81+
const result = await handler(new URL("arcane://environments"));
82+
83+
expect(result.contents).toHaveLength(1);
84+
const text = result.contents[0].text;
85+
expect(text).toContain("Arcane Environments (2 total)");
86+
expect(text).toContain("Production (ID: env-1) [ACTIVE]");
87+
expect(text).toContain("URL: https://prod.arcane.test");
88+
expect(text).toContain("Staging (ID: env-2) [ACTIVE]");
89+
expect(result.contents[0].uri).toBe("arcane://environments");
90+
expect(result.contents[0].mimeType).toBe("text/plain");
91+
});
92+
93+
it("returns 'No environments found' when empty", async () => {
94+
mockClient.get.mockResolvedValueOnce({
95+
data: [],
96+
pagination: { total: 0, start: 0, limit: 100 },
97+
});
98+
99+
const handler = server.resources.get("arcane-environments")!;
100+
const result = await handler(new URL("arcane://environments"));
101+
102+
expect(result.contents[0].text).toBe("No environments found.");
103+
});
104+
105+
it("returns error message when client fails", async () => {
106+
mockClient.get.mockRejectedValueOnce(new Error("Auth failed"));
107+
108+
const handler = server.resources.get("arcane-environments")!;
109+
const result = await handler(new URL("arcane://environments"));
110+
111+
expect(result.contents[0].text).toContain("Error reading environments");
112+
expect(result.contents[0].text).toContain("Auth failed");
113+
});
114+
});
115+
116+
describe("arcane-version", () => {
117+
it("returns version info", async () => {
118+
const handler = server.resources.get("arcane-version")!;
119+
const result = await handler(new URL("arcane://version"));
120+
121+
const text = result.contents[0].text;
122+
expect(text).toContain("Arcane MCP Server");
123+
expect(text).toContain("API Base URL: https://arcane.test");
124+
expect(text).toContain("Default Environment: env-1");
125+
expect(text).toContain("Protocol: MCP");
126+
expect(result.contents[0].uri).toBe("arcane://version");
127+
});
128+
129+
it("shows 'not set' when no default environment", async () => {
130+
mockClient.getDefaultEnvironmentId.mockReturnValueOnce(undefined);
131+
132+
const handler = server.resources.get("arcane-version")!;
133+
const result = await handler(new URL("arcane://version"));
134+
135+
expect(result.contents[0].text).toContain("Default Environment: not set");
136+
});
137+
138+
it("returns error when client setup fails", async () => {
139+
// getBaseUrl throws when client can't initialize
140+
mockClient.getBaseUrl.mockImplementationOnce(() => {
141+
throw new Error("Config missing");
142+
});
143+
144+
const handler = server.resources.get("arcane-version")!;
145+
const result = await handler(new URL("arcane://version"));
146+
147+
expect(result.contents[0].text).toContain("Error reading version");
148+
expect(result.contents[0].text).toContain("Config missing");
149+
});
150+
});
151+
});

0 commit comments

Comments
 (0)