@autifyhq/aximo-cli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -50
- package/dist/cli.js +3 -1
- package/dist/commands/case/case.test.d.ts +1 -0
- package/dist/commands/case/case.test.js +120 -0
- package/dist/commands/case/create.d.ts +53 -0
- package/dist/commands/case/create.js +71 -0
- package/dist/commands/case/delete.d.ts +20 -0
- package/dist/commands/case/delete.js +39 -0
- package/dist/commands/case/index.d.ts +2 -0
- package/dist/commands/case/index.js +8 -0
- package/dist/commands/device/doctor.d.ts +1 -0
- package/dist/commands/device/doctor.js +13 -4
- package/dist/commands/device/doctor.test.js +9 -3
- package/dist/commands/device/paths.d.ts +1 -0
- package/dist/commands/device/paths.js +3 -0
- package/dist/commands/mcp/server.test.js +9 -3
- package/dist/commands/mcp/tools.js +81 -0
- package/dist/commands/mcp/tools.test.js +77 -1
- package/dist/commands/plan/add-case.d.ts +30 -0
- package/dist/commands/plan/add-case.js +58 -0
- package/dist/commands/plan/create.d.ts +31 -0
- package/dist/commands/plan/create.js +56 -0
- package/dist/commands/plan/index.d.ts +2 -0
- package/dist/commands/plan/index.js +9 -0
- package/dist/commands/plan/plan.test.d.ts +1 -0
- package/dist/commands/plan/plan.test.js +104 -0
- package/dist/commands/plan/remove-case.d.ts +26 -0
- package/dist/commands/plan/remove-case.js +43 -0
- package/dist/commands/schema.js +40 -1
- package/dist/commands/session/index.js +1 -0
- package/dist/commands/session/save-as-case.d.ts +43 -0
- package/dist/commands/session/save-as-case.js +69 -0
- package/dist/commands/session/save-as-case.test.d.ts +1 -0
- package/dist/commands/session/save-as-case.test.js +58 -0
- package/dist/desktop-server/setup-tls.test.d.ts +1 -0
- package/dist/desktop-server/setup-tls.test.js +83 -0
- package/dist/desktop-server/setup.d.ts +17 -0
- package/dist/desktop-server/setup.js +53 -1
- package/dist/index.d.ts +12 -7
- package/dist/index.js +12 -7
- package/dist/types.d.ts +40 -0
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
2
1
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
2
|
import { tmpdir } from "node:os";
|
|
4
3
|
import { join } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { HttpResponse, http, server, TEST_BASE_URL } from "../../test-utils.js";
|
|
6
|
+
import { relayProbeUrl, runDeviceDoctor } from "./doctor.js";
|
|
7
7
|
let tempHome;
|
|
8
8
|
beforeAll(() => {
|
|
9
9
|
tempHome = mkdtempSync(join(tmpdir(), "aximo-doctor-"));
|
|
@@ -87,3 +87,9 @@ describe("runDeviceDoctor", () => {
|
|
|
87
87
|
expect(findCheck(result.data.checks, "agent files")?.status).toBe("warn");
|
|
88
88
|
});
|
|
89
89
|
});
|
|
90
|
+
describe("relayProbeUrl", () => {
|
|
91
|
+
it("targets the engine.io websocket endpoint, not the bare relay path", () => {
|
|
92
|
+
const wsBase = TEST_BASE_URL.replace(/^http/, "ws");
|
|
93
|
+
expect(relayProbeUrl()).toBe(`${wsBase}/devices/tunnel/?EIO=4&transport=websocket`);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -5,6 +5,7 @@ export declare function getPythonMarkerPath(): string;
|
|
|
5
5
|
export declare function getDepsMarkerPath(): string;
|
|
6
6
|
export declare function getCacheDir(): string;
|
|
7
7
|
export declare function getRemoteLogPath(): string;
|
|
8
|
+
export declare function getAgentCaBundlePath(): string;
|
|
8
9
|
export declare function getAgentDir(): string;
|
|
9
10
|
export declare function getAgentScriptPath(): string;
|
|
10
11
|
export declare function getAgentRequirementsPath(): string;
|
|
@@ -21,6 +21,9 @@ export function getCacheDir() {
|
|
|
21
21
|
export function getRemoteLogPath() {
|
|
22
22
|
return join(getAximoDir(), "remote.log");
|
|
23
23
|
}
|
|
24
|
+
export function getAgentCaBundlePath() {
|
|
25
|
+
return join(getAximoDir(), "agent-ca-bundle.pem");
|
|
26
|
+
}
|
|
24
27
|
export function getAgentDir() {
|
|
25
28
|
return join(getAximoDir(), "agent");
|
|
26
29
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
2
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
2
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
4
|
-
import {
|
|
3
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
4
|
+
import { HttpResponse, http, server, TEST_BASE_URL } from "../../test-utils.js";
|
|
5
5
|
import { createMcpServer } from "./server.js";
|
|
6
6
|
beforeAll(() => {
|
|
7
7
|
process.env["AXIMO_API_KEY"] = "test-key";
|
|
@@ -24,7 +24,7 @@ async function connect() {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
describe("createMcpServer wiring", () => {
|
|
27
|
-
it("exposes all
|
|
27
|
+
it("exposes all 16 tools through tools/list with valid JSON Schema", async () => {
|
|
28
28
|
const { client, cleanup } = await connect();
|
|
29
29
|
try {
|
|
30
30
|
const { tools } = await client.listTools();
|
|
@@ -39,6 +39,12 @@ describe("createMcpServer wiring", () => {
|
|
|
39
39
|
"aximo_list_cases",
|
|
40
40
|
"aximo_list_projects",
|
|
41
41
|
"aximo_list_sessions",
|
|
42
|
+
"aximo_create_case",
|
|
43
|
+
"aximo_delete_case",
|
|
44
|
+
"aximo_create_plan",
|
|
45
|
+
"aximo_add_case_to_plan",
|
|
46
|
+
"aximo_remove_case_from_plan",
|
|
47
|
+
"aximo_save_session_as_case",
|
|
42
48
|
].sort());
|
|
43
49
|
const timeline = tools.find((t) => t.name === "aximo_get_session_timeline");
|
|
44
50
|
expect(timeline?.inputSchema?.required).toContain("sessionId");
|
|
@@ -194,5 +194,86 @@ export function defineTools(deps = {}) {
|
|
|
194
194
|
return await request(`/api/cases/${encodeURIComponent(caseId)}`, {}, deps);
|
|
195
195
|
}),
|
|
196
196
|
},
|
|
197
|
+
{
|
|
198
|
+
name: "aximo_create_case",
|
|
199
|
+
title: "Create test case",
|
|
200
|
+
description: "Create a saved test case in a workspace. The case is API-enabled, so it can be triggered immediately. Returns the new caseId.",
|
|
201
|
+
inputSchema: {
|
|
202
|
+
projectId: z.string().uuid(),
|
|
203
|
+
scenario: z.string().min(1),
|
|
204
|
+
type: z.enum(["web", "mobile", "desktop"]).optional(),
|
|
205
|
+
title: z.string().min(1).max(500).optional(),
|
|
206
|
+
description: z.string().optional(),
|
|
207
|
+
format: z.enum(["gherkin", "markdown", "plain_text"]).optional(),
|
|
208
|
+
startUrl: z.string().url().optional(),
|
|
209
|
+
directoryId: z.string().uuid().optional(),
|
|
210
|
+
},
|
|
211
|
+
handler: async (input) => call(async () => {
|
|
212
|
+
return await request("/api/cases", { method: "POST", body: input }, deps);
|
|
213
|
+
}),
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "aximo_delete_case",
|
|
217
|
+
title: "Delete test case",
|
|
218
|
+
description: "Permanently delete a test case. It is removed from any plans it belongs to; past run sessions are kept but detached. This cannot be undone.",
|
|
219
|
+
inputSchema: {
|
|
220
|
+
caseId: z.string().uuid(),
|
|
221
|
+
},
|
|
222
|
+
handler: async ({ caseId }) => call(async () => {
|
|
223
|
+
return await request(`/api/cases/${encodeURIComponent(caseId)}`, { method: "DELETE" }, deps);
|
|
224
|
+
}),
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: "aximo_create_plan",
|
|
228
|
+
title: "Create test plan",
|
|
229
|
+
description: "Create an empty test plan in a workspace. Add cases with aximo_add_case_to_plan, then run it. Returns the new planId.",
|
|
230
|
+
inputSchema: {
|
|
231
|
+
projectId: z.string().uuid(),
|
|
232
|
+
title: z.string().min(1).max(500),
|
|
233
|
+
description: z.string().optional(),
|
|
234
|
+
},
|
|
235
|
+
handler: async (input) => call(async () => {
|
|
236
|
+
return await request("/api/plans", { method: "POST", body: input }, deps);
|
|
237
|
+
}),
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: "aximo_add_case_to_plan",
|
|
241
|
+
title: "Add case to plan",
|
|
242
|
+
description: "Append a test case to the end of a plan. The case must belong to the same workspace as the plan.",
|
|
243
|
+
inputSchema: {
|
|
244
|
+
planId: z.string().uuid(),
|
|
245
|
+
caseId: z.string().uuid(),
|
|
246
|
+
},
|
|
247
|
+
handler: async ({ planId, caseId }) => call(async () => {
|
|
248
|
+
return await request(`/api/plans/${encodeURIComponent(planId)}/cases`, { method: "POST", body: { caseId } }, deps);
|
|
249
|
+
}),
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: "aximo_remove_case_from_plan",
|
|
253
|
+
title: "Remove case from plan",
|
|
254
|
+
description: "Remove a test case from a plan. Idempotent.",
|
|
255
|
+
inputSchema: {
|
|
256
|
+
planId: z.string().uuid(),
|
|
257
|
+
caseId: z.string().uuid(),
|
|
258
|
+
},
|
|
259
|
+
handler: async ({ planId, caseId }) => call(async () => {
|
|
260
|
+
return await request(`/api/plans/${encodeURIComponent(planId)}/cases/${encodeURIComponent(caseId)}`, { method: "DELETE" }, deps);
|
|
261
|
+
}),
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: "aximo_save_session_as_case",
|
|
265
|
+
title: "Save session as test case",
|
|
266
|
+
description: "Persist a finished agent session as a reusable test case and link the two. The session must be in a terminal state and not already linked to a case. The scenario defaults to the session summary. Returns the new caseId and projectId.",
|
|
267
|
+
inputSchema: {
|
|
268
|
+
sessionId: z.string().uuid(),
|
|
269
|
+
title: z.string().min(1).max(500).optional(),
|
|
270
|
+
type: z.enum(["web", "mobile", "desktop"]).optional(),
|
|
271
|
+
scenario: z.string().min(1).optional(),
|
|
272
|
+
format: z.enum(["gherkin", "markdown", "plain_text"]).optional(),
|
|
273
|
+
},
|
|
274
|
+
handler: async ({ sessionId, ...body }) => call(async () => {
|
|
275
|
+
return await request(`/api/sessions/${encodeURIComponent(sessionId)}/save-as-case`, { method: "POST", body }, deps);
|
|
276
|
+
}),
|
|
277
|
+
},
|
|
197
278
|
];
|
|
198
279
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
-
import { HttpResponse,
|
|
2
|
+
import { HttpResponse, http, server, TEST_BASE_URL } from "../../test-utils.js";
|
|
3
3
|
import { defineTools } from "./tools.js";
|
|
4
4
|
beforeAll(() => {
|
|
5
5
|
process.env["AXIMO_API_KEY"] = "test-key";
|
|
@@ -31,6 +31,12 @@ describe("defineTools", () => {
|
|
|
31
31
|
"aximo_get_session_snapshot",
|
|
32
32
|
"aximo_list_cases",
|
|
33
33
|
"aximo_get_case",
|
|
34
|
+
"aximo_create_case",
|
|
35
|
+
"aximo_delete_case",
|
|
36
|
+
"aximo_create_plan",
|
|
37
|
+
"aximo_add_case_to_plan",
|
|
38
|
+
"aximo_remove_case_from_plan",
|
|
39
|
+
"aximo_save_session_as_case",
|
|
34
40
|
]);
|
|
35
41
|
});
|
|
36
42
|
});
|
|
@@ -203,3 +209,73 @@ describe("call() error mapping", () => {
|
|
|
203
209
|
expect(payload.hint).toMatch(/AXIMO_BASE_URL/);
|
|
204
210
|
});
|
|
205
211
|
});
|
|
212
|
+
describe("write tools", () => {
|
|
213
|
+
it("aximo_create_case POSTs the body to /api/cases", async () => {
|
|
214
|
+
let seen = null;
|
|
215
|
+
server.use(http.post(`${TEST_BASE_URL}/api/cases`, async ({ request: req }) => {
|
|
216
|
+
seen = { method: req.method, body: await req.json() };
|
|
217
|
+
return HttpResponse.json({ success: true, caseId: "c1" }, { status: 201 });
|
|
218
|
+
}));
|
|
219
|
+
const tool = byName(defineTools(), "aximo_create_case");
|
|
220
|
+
const result = await tool.handler({ projectId: "p1", scenario: "do a thing", type: "web" });
|
|
221
|
+
expect(result.isError).toBeUndefined();
|
|
222
|
+
expect(parsePayload(result)).toEqual({ success: true, caseId: "c1" });
|
|
223
|
+
expect(seen.method).toBe("POST");
|
|
224
|
+
expect(seen.body).toMatchObject({ projectId: "p1", scenario: "do a thing", type: "web" });
|
|
225
|
+
});
|
|
226
|
+
it("aximo_create_plan POSTs the body to /api/plans", async () => {
|
|
227
|
+
let body;
|
|
228
|
+
server.use(http.post(`${TEST_BASE_URL}/api/plans`, async ({ request: req }) => {
|
|
229
|
+
body = await req.json();
|
|
230
|
+
return HttpResponse.json({ success: true, planId: "pl1" }, { status: 201 });
|
|
231
|
+
}));
|
|
232
|
+
const tool = byName(defineTools(), "aximo_create_plan");
|
|
233
|
+
const result = await tool.handler({ projectId: "p1", title: "Regression" });
|
|
234
|
+
expect(parsePayload(result)).toEqual({ success: true, planId: "pl1" });
|
|
235
|
+
expect(body).toMatchObject({ projectId: "p1", title: "Regression" });
|
|
236
|
+
});
|
|
237
|
+
it("aximo_add_case_to_plan POSTs caseId to the plan-cases path", async () => {
|
|
238
|
+
let body;
|
|
239
|
+
server.use(http.post(`${TEST_BASE_URL}/api/plans/pl1/cases`, async ({ request: req }) => {
|
|
240
|
+
body = await req.json();
|
|
241
|
+
return HttpResponse.json({ success: true }, { status: 201 });
|
|
242
|
+
}));
|
|
243
|
+
const tool = byName(defineTools(), "aximo_add_case_to_plan");
|
|
244
|
+
const result = await tool.handler({ planId: "pl1", caseId: "c1" });
|
|
245
|
+
expect(parsePayload(result)).toEqual({ success: true });
|
|
246
|
+
expect(body).toEqual({ caseId: "c1" });
|
|
247
|
+
});
|
|
248
|
+
it("aximo_remove_case_from_plan DELETEs the plan-case path", async () => {
|
|
249
|
+
let method = null;
|
|
250
|
+
server.use(http.delete(`${TEST_BASE_URL}/api/plans/pl1/cases/c1`, ({ request: req }) => {
|
|
251
|
+
method = req.method;
|
|
252
|
+
return HttpResponse.json({ success: true });
|
|
253
|
+
}));
|
|
254
|
+
const tool = byName(defineTools(), "aximo_remove_case_from_plan");
|
|
255
|
+
const result = await tool.handler({ planId: "pl1", caseId: "c1" });
|
|
256
|
+
expect(parsePayload(result)).toEqual({ success: true });
|
|
257
|
+
expect(method).toBe("DELETE");
|
|
258
|
+
});
|
|
259
|
+
it("aximo_delete_case DELETEs the case path", async () => {
|
|
260
|
+
let method = null;
|
|
261
|
+
server.use(http.delete(`${TEST_BASE_URL}/api/cases/c1`, ({ request: req }) => {
|
|
262
|
+
method = req.method;
|
|
263
|
+
return HttpResponse.json({ success: true });
|
|
264
|
+
}));
|
|
265
|
+
const tool = byName(defineTools(), "aximo_delete_case");
|
|
266
|
+
const result = await tool.handler({ caseId: "c1" });
|
|
267
|
+
expect(parsePayload(result)).toEqual({ success: true });
|
|
268
|
+
expect(method).toBe("DELETE");
|
|
269
|
+
});
|
|
270
|
+
it("aximo_save_session_as_case POSTs to the save-as-case path without the id in the body", async () => {
|
|
271
|
+
let body;
|
|
272
|
+
server.use(http.post(`${TEST_BASE_URL}/api/sessions/s1/save-as-case`, async ({ request: req }) => {
|
|
273
|
+
body = await req.json();
|
|
274
|
+
return HttpResponse.json({ success: true, caseId: "c1", projectId: "p1" }, { status: 201 });
|
|
275
|
+
}));
|
|
276
|
+
const tool = byName(defineTools(), "aximo_save_session_as_case");
|
|
277
|
+
const result = await tool.handler({ sessionId: "s1", title: "Saved" });
|
|
278
|
+
expect(parsePayload(result)).toEqual({ success: true, caseId: "c1", projectId: "p1" });
|
|
279
|
+
expect(body).toEqual({ title: "Saved" });
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ClientDeps } from "../../client.js";
|
|
2
|
+
import { type DryRunResult } from "../../mutate.js";
|
|
3
|
+
import type { Envelope, PlanCaseResult } from "../../types.js";
|
|
4
|
+
export interface PlanAddCaseOptions {
|
|
5
|
+
planId?: unknown;
|
|
6
|
+
caseId?: unknown;
|
|
7
|
+
json?: unknown;
|
|
8
|
+
dryRun?: unknown;
|
|
9
|
+
}
|
|
10
|
+
export declare function runPlanAddCase(options: PlanAddCaseOptions, deps?: ClientDeps): Promise<Envelope<PlanCaseResult | DryRunResult>>;
|
|
11
|
+
declare const _default: import("citty").CommandDef<{
|
|
12
|
+
readonly planId: {
|
|
13
|
+
readonly type: "positional";
|
|
14
|
+
readonly description: "Plan ID";
|
|
15
|
+
readonly required: true;
|
|
16
|
+
};
|
|
17
|
+
readonly "case-id": {
|
|
18
|
+
readonly type: "string";
|
|
19
|
+
readonly description: "Case ID to add";
|
|
20
|
+
};
|
|
21
|
+
readonly json: {
|
|
22
|
+
readonly type: "string";
|
|
23
|
+
readonly description: "Raw JSON request body (replaces individual flags)";
|
|
24
|
+
};
|
|
25
|
+
readonly "dry-run": {
|
|
26
|
+
readonly type: "boolean";
|
|
27
|
+
readonly description: "Validate and show request without calling API";
|
|
28
|
+
};
|
|
29
|
+
}>;
|
|
30
|
+
export default _default;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { request } from "../../client.js";
|
|
3
|
+
import { handleApiError } from "../../errors.js";
|
|
4
|
+
import { dryRunEnvelope, resolveJsonOverride } from "../../mutate.js";
|
|
5
|
+
import { formatSuccess, printResult, validationError } from "../../output.js";
|
|
6
|
+
import { requireId, requireString } from "../../validate.js";
|
|
7
|
+
export async function runPlanAddCase(options, deps) {
|
|
8
|
+
let planId;
|
|
9
|
+
let body;
|
|
10
|
+
const override = resolveJsonOverride(options.json);
|
|
11
|
+
if (override && "error" in override)
|
|
12
|
+
return override.error;
|
|
13
|
+
try {
|
|
14
|
+
planId = requireId("planId", options.planId);
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
return validationError(err);
|
|
18
|
+
}
|
|
19
|
+
if (override) {
|
|
20
|
+
body = override.body;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
try {
|
|
24
|
+
body = { caseId: requireString("caseId", options.caseId) };
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
return validationError(err);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const path = `/api/plans/${encodeURIComponent(planId)}/cases`;
|
|
31
|
+
if (options.dryRun) {
|
|
32
|
+
return dryRunEnvelope("POST", path, body);
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const res = await request(path, { method: "POST", body }, deps ?? {});
|
|
36
|
+
return formatSuccess(res);
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
return handleApiError(err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export default defineCommand({
|
|
43
|
+
meta: { name: "add-case", description: "Add a case to a plan" },
|
|
44
|
+
args: {
|
|
45
|
+
planId: { type: "positional", description: "Plan ID", required: true },
|
|
46
|
+
"case-id": { type: "string", description: "Case ID to add" },
|
|
47
|
+
json: { type: "string", description: "Raw JSON request body (replaces individual flags)" },
|
|
48
|
+
"dry-run": { type: "boolean", description: "Validate and show request without calling API" },
|
|
49
|
+
},
|
|
50
|
+
async run({ args }) {
|
|
51
|
+
printResult(await runPlanAddCase({
|
|
52
|
+
planId: args.planId,
|
|
53
|
+
caseId: args["case-id"],
|
|
54
|
+
json: args.json,
|
|
55
|
+
dryRun: args["dry-run"],
|
|
56
|
+
}));
|
|
57
|
+
},
|
|
58
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ClientDeps } from "../../client.js";
|
|
2
|
+
import { type DryRunResult } from "../../mutate.js";
|
|
3
|
+
import type { CreatePlanResult, Envelope } from "../../types.js";
|
|
4
|
+
export interface CreatePlanOptions {
|
|
5
|
+
projectId?: unknown;
|
|
6
|
+
title?: unknown;
|
|
7
|
+
description?: unknown;
|
|
8
|
+
json?: unknown;
|
|
9
|
+
dryRun?: unknown;
|
|
10
|
+
}
|
|
11
|
+
export declare function runPlanCreate(options: CreatePlanOptions, deps?: ClientDeps): Promise<Envelope<CreatePlanResult | DryRunResult>>;
|
|
12
|
+
declare const _default: import("citty").CommandDef<{
|
|
13
|
+
readonly "project-id": {
|
|
14
|
+
readonly type: "string";
|
|
15
|
+
};
|
|
16
|
+
readonly title: {
|
|
17
|
+
readonly type: "string";
|
|
18
|
+
};
|
|
19
|
+
readonly description: {
|
|
20
|
+
readonly type: "string";
|
|
21
|
+
};
|
|
22
|
+
readonly json: {
|
|
23
|
+
readonly type: "string";
|
|
24
|
+
readonly description: "Raw JSON request body (replaces individual flags)";
|
|
25
|
+
};
|
|
26
|
+
readonly "dry-run": {
|
|
27
|
+
readonly type: "boolean";
|
|
28
|
+
readonly description: "Validate and show request without calling API";
|
|
29
|
+
};
|
|
30
|
+
}>;
|
|
31
|
+
export default _default;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { request } from "../../client.js";
|
|
3
|
+
import { handleApiError } from "../../errors.js";
|
|
4
|
+
import { dryRunEnvelope, resolveJsonOverride } from "../../mutate.js";
|
|
5
|
+
import { formatSuccess, printResult, validationError } from "../../output.js";
|
|
6
|
+
import { optionalString, requireString } from "../../validate.js";
|
|
7
|
+
export async function runPlanCreate(options, deps) {
|
|
8
|
+
let body;
|
|
9
|
+
const override = resolveJsonOverride(options.json);
|
|
10
|
+
if (override && "error" in override)
|
|
11
|
+
return override.error;
|
|
12
|
+
if (override) {
|
|
13
|
+
body = override.body;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
try {
|
|
17
|
+
body = {
|
|
18
|
+
projectId: requireString("projectId", options.projectId),
|
|
19
|
+
title: requireString("title", options.title),
|
|
20
|
+
description: optionalString("description", options.description),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
return validationError(err);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (options.dryRun) {
|
|
28
|
+
return dryRunEnvelope("POST", "/api/plans", body);
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const res = await request("/api/plans", { method: "POST", body }, deps ?? {});
|
|
32
|
+
return formatSuccess(res);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
return handleApiError(err);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export default defineCommand({
|
|
39
|
+
meta: { name: "create", description: "Create a test plan" },
|
|
40
|
+
args: {
|
|
41
|
+
"project-id": { type: "string" },
|
|
42
|
+
title: { type: "string" },
|
|
43
|
+
description: { type: "string" },
|
|
44
|
+
json: { type: "string", description: "Raw JSON request body (replaces individual flags)" },
|
|
45
|
+
"dry-run": { type: "boolean", description: "Validate and show request without calling API" },
|
|
46
|
+
},
|
|
47
|
+
async run({ args }) {
|
|
48
|
+
printResult(await runPlanCreate({
|
|
49
|
+
projectId: args["project-id"],
|
|
50
|
+
title: args.title,
|
|
51
|
+
description: args.description,
|
|
52
|
+
json: args.json,
|
|
53
|
+
dryRun: args["dry-run"],
|
|
54
|
+
}));
|
|
55
|
+
},
|
|
56
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
export default defineCommand({
|
|
3
|
+
meta: { name: "plan", description: "Test plan commands" },
|
|
4
|
+
subCommands: {
|
|
5
|
+
create: () => import("./create.js").then((m) => m.default),
|
|
6
|
+
"add-case": () => import("./add-case.js").then((m) => m.default),
|
|
7
|
+
"remove-case": () => import("./remove-case.js").then((m) => m.default),
|
|
8
|
+
},
|
|
9
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { HttpResponse, http, server, TEST_BASE_URL } from "../../test-utils.js";
|
|
3
|
+
import { runPlanAddCase } from "./add-case.js";
|
|
4
|
+
import { runPlanCreate } from "./create.js";
|
|
5
|
+
import { runPlanRemoveCase } from "./remove-case.js";
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
process.env["AXIMO_API_KEY"] = "test-key";
|
|
8
|
+
process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
|
|
9
|
+
server.listen();
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => server.resetHandlers());
|
|
12
|
+
afterAll(() => server.close());
|
|
13
|
+
describe("runPlanCreate", () => {
|
|
14
|
+
it("POSTs body and returns success envelope", async () => {
|
|
15
|
+
let seen;
|
|
16
|
+
server.use(http.post(`${TEST_BASE_URL}/api/plans`, async ({ request: req }) => {
|
|
17
|
+
seen = await req.json();
|
|
18
|
+
return HttpResponse.json({ success: true, planId: "pl1" }, { status: 201 });
|
|
19
|
+
}));
|
|
20
|
+
const result = await runPlanCreate({ projectId: "proj-1", title: "Regression" });
|
|
21
|
+
expect(result.ok).toBe(true);
|
|
22
|
+
if (result.ok && !("dryRun" in result.data))
|
|
23
|
+
expect(result.data.planId).toBe("pl1");
|
|
24
|
+
expect(seen).toMatchObject({ projectId: "proj-1", title: "Regression" });
|
|
25
|
+
});
|
|
26
|
+
it("rejects missing title", async () => {
|
|
27
|
+
const result = await runPlanCreate({ projectId: "p1" });
|
|
28
|
+
expect(result.ok).toBe(false);
|
|
29
|
+
if (!result.ok)
|
|
30
|
+
expect(result.error.code).toBe("validation_error");
|
|
31
|
+
});
|
|
32
|
+
it("--dry-run returns request without calling API", async () => {
|
|
33
|
+
const result = await runPlanCreate({ projectId: "p1", title: "t", dryRun: true });
|
|
34
|
+
expect(result.ok).toBe(true);
|
|
35
|
+
if (result.ok && "dryRun" in result.data) {
|
|
36
|
+
expect(result.data.url).toBe(`${TEST_BASE_URL}/api/plans`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe("runPlanAddCase", () => {
|
|
41
|
+
it("POSTs caseId to the plan-cases path", async () => {
|
|
42
|
+
let seen;
|
|
43
|
+
server.use(http.post(`${TEST_BASE_URL}/api/plans/pl1/cases`, async ({ request: req }) => {
|
|
44
|
+
seen = await req.json();
|
|
45
|
+
return HttpResponse.json({ success: true }, { status: 201 });
|
|
46
|
+
}));
|
|
47
|
+
const result = await runPlanAddCase({ planId: "pl1", caseId: "c1" });
|
|
48
|
+
expect(result.ok).toBe(true);
|
|
49
|
+
expect(seen).toEqual({ caseId: "c1" });
|
|
50
|
+
});
|
|
51
|
+
it("rejects missing caseId", async () => {
|
|
52
|
+
const result = await runPlanAddCase({ planId: "pl1" });
|
|
53
|
+
expect(result.ok).toBe(false);
|
|
54
|
+
if (!result.ok)
|
|
55
|
+
expect(result.error.code).toBe("validation_error");
|
|
56
|
+
});
|
|
57
|
+
it("rejects an injected plan id (requireId)", async () => {
|
|
58
|
+
const result = await runPlanAddCase({ planId: "pl1/../x", caseId: "c1" });
|
|
59
|
+
expect(result.ok).toBe(false);
|
|
60
|
+
if (!result.ok)
|
|
61
|
+
expect(result.error.code).toBe("validation_error");
|
|
62
|
+
});
|
|
63
|
+
it("maps 409 to validation_error", async () => {
|
|
64
|
+
server.use(http.post(`${TEST_BASE_URL}/api/plans/pl1/cases`, () => HttpResponse.json({ error: "Case already in plan" }, { status: 409 })));
|
|
65
|
+
const result = await runPlanAddCase({ planId: "pl1", caseId: "c1" });
|
|
66
|
+
expect(result.ok).toBe(false);
|
|
67
|
+
if (!result.ok)
|
|
68
|
+
expect(result.error.code).toBe("validation_error");
|
|
69
|
+
});
|
|
70
|
+
it("--dry-run returns request without calling API", async () => {
|
|
71
|
+
const result = await runPlanAddCase({ planId: "pl1", caseId: "c1", dryRun: true });
|
|
72
|
+
expect(result.ok).toBe(true);
|
|
73
|
+
if (result.ok && "dryRun" in result.data) {
|
|
74
|
+
expect(result.data.method).toBe("POST");
|
|
75
|
+
expect(result.data.url).toBe(`${TEST_BASE_URL}/api/plans/pl1/cases`);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
describe("runPlanRemoveCase", () => {
|
|
80
|
+
it("DELETEs the plan-case path", async () => {
|
|
81
|
+
let called = false;
|
|
82
|
+
server.use(http.delete(`${TEST_BASE_URL}/api/plans/pl1/cases/c1`, () => {
|
|
83
|
+
called = true;
|
|
84
|
+
return HttpResponse.json({ success: true });
|
|
85
|
+
}));
|
|
86
|
+
const result = await runPlanRemoveCase({ planId: "pl1", caseId: "c1" });
|
|
87
|
+
expect(result.ok).toBe(true);
|
|
88
|
+
expect(called).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
it("rejects missing caseId", async () => {
|
|
91
|
+
const result = await runPlanRemoveCase({ planId: "pl1" });
|
|
92
|
+
expect(result.ok).toBe(false);
|
|
93
|
+
if (!result.ok)
|
|
94
|
+
expect(result.error.code).toBe("validation_error");
|
|
95
|
+
});
|
|
96
|
+
it("--dry-run returns a DELETE request without calling API", async () => {
|
|
97
|
+
const result = await runPlanRemoveCase({ planId: "pl1", caseId: "c1", dryRun: true });
|
|
98
|
+
expect(result.ok).toBe(true);
|
|
99
|
+
if (result.ok && "dryRun" in result.data) {
|
|
100
|
+
expect(result.data.method).toBe("DELETE");
|
|
101
|
+
expect(result.data.url).toBe(`${TEST_BASE_URL}/api/plans/pl1/cases/c1`);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type ClientDeps } from "../../client.js";
|
|
2
|
+
import { type DryRunResult } from "../../mutate.js";
|
|
3
|
+
import type { Envelope, PlanCaseResult } from "../../types.js";
|
|
4
|
+
export interface PlanRemoveCaseOptions {
|
|
5
|
+
planId?: unknown;
|
|
6
|
+
caseId?: unknown;
|
|
7
|
+
dryRun?: unknown;
|
|
8
|
+
}
|
|
9
|
+
export declare function runPlanRemoveCase(options: PlanRemoveCaseOptions, deps?: ClientDeps): Promise<Envelope<PlanCaseResult | DryRunResult>>;
|
|
10
|
+
declare const _default: import("citty").CommandDef<{
|
|
11
|
+
readonly planId: {
|
|
12
|
+
readonly type: "positional";
|
|
13
|
+
readonly description: "Plan ID";
|
|
14
|
+
readonly required: true;
|
|
15
|
+
};
|
|
16
|
+
readonly caseId: {
|
|
17
|
+
readonly type: "positional";
|
|
18
|
+
readonly description: "Case ID";
|
|
19
|
+
readonly required: true;
|
|
20
|
+
};
|
|
21
|
+
readonly "dry-run": {
|
|
22
|
+
readonly type: "boolean";
|
|
23
|
+
readonly description: "Validate and show request without calling API";
|
|
24
|
+
};
|
|
25
|
+
}>;
|
|
26
|
+
export default _default;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { request } from "../../client.js";
|
|
3
|
+
import { handleApiError } from "../../errors.js";
|
|
4
|
+
import { dryRunEnvelope } from "../../mutate.js";
|
|
5
|
+
import { formatSuccess, printResult, validationError } from "../../output.js";
|
|
6
|
+
import { requireId } from "../../validate.js";
|
|
7
|
+
export async function runPlanRemoveCase(options, deps) {
|
|
8
|
+
let planId;
|
|
9
|
+
let caseId;
|
|
10
|
+
try {
|
|
11
|
+
planId = requireId("planId", options.planId);
|
|
12
|
+
caseId = requireId("caseId", options.caseId);
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
return validationError(err);
|
|
16
|
+
}
|
|
17
|
+
const path = `/api/plans/${encodeURIComponent(planId)}/cases/${encodeURIComponent(caseId)}`;
|
|
18
|
+
if (options.dryRun) {
|
|
19
|
+
return dryRunEnvelope("DELETE", path);
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const res = await request(path, { method: "DELETE" }, deps ?? {});
|
|
23
|
+
return formatSuccess(res);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
return handleApiError(err);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export default defineCommand({
|
|
30
|
+
meta: { name: "remove-case", description: "Remove a case from a plan" },
|
|
31
|
+
args: {
|
|
32
|
+
planId: { type: "positional", description: "Plan ID", required: true },
|
|
33
|
+
caseId: { type: "positional", description: "Case ID", required: true },
|
|
34
|
+
"dry-run": { type: "boolean", description: "Validate and show request without calling API" },
|
|
35
|
+
},
|
|
36
|
+
async run({ args }) {
|
|
37
|
+
printResult(await runPlanRemoveCase({
|
|
38
|
+
planId: args.planId,
|
|
39
|
+
caseId: args.caseId,
|
|
40
|
+
dryRun: args["dry-run"],
|
|
41
|
+
}));
|
|
42
|
+
},
|
|
43
|
+
});
|