@autifyhq/aximo-cli 0.11.1 → 0.12.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 +1 -1
- package/dist/commands/mcp/server.test.js +2 -1
- package/dist/commands/mcp/tools.js +49 -7
- package/dist/commands/mcp/tools.test.js +119 -2
- package/dist/types.d.ts +63 -5
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -660,7 +660,7 @@ claude mcp add aximo -- aximo mcp
|
|
|
660
660
|
|
|
661
661
|
VS Code (Copilot agent mode) uses the same entry under a `servers` key in `.vscode/mcp.json`.
|
|
662
662
|
|
|
663
|
-
The server exposes
|
|
663
|
+
The server exposes 22 tools: 14 read-only ones for inspecting sessions, cases, plans, and plan runs (`aximo_get_session_timeline` is the load-bearing one for Playwright export; `aximo_list_plans` and `aximo_get_plan` cover plan discovery; `aximo_list_plan_runs` and `aximo_get_plan_run` cover run-status polling) plus 8 that author content (create, update, and delete cases, create and update plans, save a session as a case). See the [MCP reference](./skills/aximo-cli/references/mcp.md) for per-client config paths, the full tool table, the Playwright export prompt, and a no-client smoke test.
|
|
664
664
|
|
|
665
665
|
## Agent Skills
|
|
666
666
|
|
|
@@ -24,7 +24,7 @@ async function connect() {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
describe("createMcpServer wiring", () => {
|
|
27
|
-
it("exposes all
|
|
27
|
+
it("exposes all 22 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();
|
|
@@ -40,6 +40,7 @@ describe("createMcpServer wiring", () => {
|
|
|
40
40
|
"aximo_list_projects",
|
|
41
41
|
"aximo_list_sessions",
|
|
42
42
|
"aximo_create_case",
|
|
43
|
+
"aximo_update_case",
|
|
43
44
|
"aximo_delete_case",
|
|
44
45
|
"aximo_create_plan",
|
|
45
46
|
"aximo_add_case_to_plan",
|
|
@@ -186,7 +186,7 @@ export function defineTools(deps = {}) {
|
|
|
186
186
|
{
|
|
187
187
|
name: "aximo_get_case",
|
|
188
188
|
title: "Get test case",
|
|
189
|
-
description: "Get the full scenario text and metadata for a saved test case. Useful for grounding generated Playwright specs.",
|
|
189
|
+
description: "Get the full scenario text and metadata for a saved test case, including its per-target default configuration (mobile, web hostname overrides, desktop, multi-device) — the run trigger's per-run override fields supersede those defaults. Useful for grounding generated Playwright specs.",
|
|
190
190
|
inputSchema: {
|
|
191
191
|
caseId: z.string().uuid(),
|
|
192
192
|
},
|
|
@@ -201,17 +201,37 @@ export function defineTools(deps = {}) {
|
|
|
201
201
|
inputSchema: {
|
|
202
202
|
projectId: z.string().uuid(),
|
|
203
203
|
scenario: z.string().min(1),
|
|
204
|
-
type: z.enum(["web", "mobile", "desktop"]).optional(),
|
|
204
|
+
type: z.enum(["web", "mobile", "desktop", "multi_device"]).optional(),
|
|
205
205
|
title: z.string().min(1).max(500).optional(),
|
|
206
206
|
description: z.string().optional(),
|
|
207
207
|
format: z.enum(["gherkin", "markdown", "plain_text"]).optional(),
|
|
208
|
-
startUrl: z.string().url().optional(),
|
|
208
|
+
startUrl: z.string().url().nullable().optional(),
|
|
209
209
|
directoryId: z.string().uuid().optional(),
|
|
210
210
|
},
|
|
211
211
|
handler: async (input) => call(async () => {
|
|
212
212
|
return await request("/api/cases", { method: "POST", body: input }, deps);
|
|
213
213
|
}),
|
|
214
214
|
},
|
|
215
|
+
{
|
|
216
|
+
name: "aximo_update_case",
|
|
217
|
+
title: "Update test case",
|
|
218
|
+
description: "Update a test case's title, description, scenario, type, format, or startUrl (pass null to clear startUrl). Omitted fields are left unchanged; at least one field is required. Per-target device configuration cannot be changed over the API, and changing type does not create or clear it (e.g. switching to mobile leaves apiMobileConfig null until set in the dashboard).",
|
|
219
|
+
inputSchema: {
|
|
220
|
+
caseId: z.string().uuid(),
|
|
221
|
+
title: z.string().min(1).max(500).optional(),
|
|
222
|
+
description: z.string().optional(),
|
|
223
|
+
scenario: z.string().min(1).optional(),
|
|
224
|
+
type: z.enum(["web", "mobile", "desktop", "multi_device"]).optional(),
|
|
225
|
+
format: z.enum(["gherkin", "markdown", "plain_text"]).optional(),
|
|
226
|
+
startUrl: z.string().url().nullable().optional(),
|
|
227
|
+
},
|
|
228
|
+
handler: async ({ caseId, ...body }) => call(async () => {
|
|
229
|
+
if (Object.keys(body).length === 0) {
|
|
230
|
+
throw new Error("at least one field to update is required");
|
|
231
|
+
}
|
|
232
|
+
return await request(`/api/cases/${encodeURIComponent(caseId)}`, { method: "PATCH", body }, deps);
|
|
233
|
+
}),
|
|
234
|
+
},
|
|
215
235
|
{
|
|
216
236
|
name: "aximo_delete_case",
|
|
217
237
|
title: "Delete test case",
|
|
@@ -306,17 +326,39 @@ export function defineTools(deps = {}) {
|
|
|
306
326
|
{
|
|
307
327
|
name: "aximo_list_plan_runs",
|
|
308
328
|
title: "List plan runs",
|
|
309
|
-
description: "List
|
|
329
|
+
description: "List plan runs, newest first, without per-case detail. Pass planId for one plan's runs, or projectId for run history across every plan in a workspace (each item then carries its plan's id and title); exactly one of the two is required, and status filtering is available with projectId. Use aximo_get_plan_run for one run's per-case breakdown. Paginate by passing the previous page's nextCursor as cursor. A run's status reflects execution, not the test verdict: read passedCases/failedCases to judge outcomes.",
|
|
310
330
|
inputSchema: {
|
|
311
|
-
planId: z
|
|
331
|
+
planId: z
|
|
332
|
+
.string()
|
|
333
|
+
.uuid()
|
|
334
|
+
.optional()
|
|
335
|
+
.describe("Exactly one of planId or projectId is required."),
|
|
336
|
+
projectId: z
|
|
337
|
+
.string()
|
|
338
|
+
.uuid()
|
|
339
|
+
.optional()
|
|
340
|
+
.describe("Exactly one of planId or projectId is required."),
|
|
341
|
+
status: z
|
|
342
|
+
.enum(["created", "running", "completed", "failed", "cancelled"])
|
|
343
|
+
.optional()
|
|
344
|
+
.describe("Only with projectId: restrict to runs with this execution status."),
|
|
312
345
|
limit: z.number().int().positive().max(50).optional(),
|
|
313
346
|
cursor: z
|
|
314
347
|
.string()
|
|
315
348
|
.optional()
|
|
316
349
|
.describe("nextCursor from the previous page (ISO 8601 createdAt of its last run)."),
|
|
317
350
|
},
|
|
318
|
-
handler: async ({ planId, ...rest }) => call(async () => {
|
|
319
|
-
|
|
351
|
+
handler: async ({ planId, projectId, status, ...rest }) => call(async () => {
|
|
352
|
+
if (!planId === !projectId) {
|
|
353
|
+
throw new Error("pass exactly one of planId or projectId");
|
|
354
|
+
}
|
|
355
|
+
if (planId) {
|
|
356
|
+
if (status) {
|
|
357
|
+
throw new Error("the status filter requires projectId");
|
|
358
|
+
}
|
|
359
|
+
return await request(`/api/triggers/plans/${encodeURIComponent(planId)}/runs`, { query: buildQuery(rest) }, deps);
|
|
360
|
+
}
|
|
361
|
+
return await request("/api/plan-runs", { query: buildQuery({ projectId, status, ...rest }) }, deps);
|
|
320
362
|
}),
|
|
321
363
|
},
|
|
322
364
|
{
|
|
@@ -32,6 +32,7 @@ describe("defineTools", () => {
|
|
|
32
32
|
"aximo_list_cases",
|
|
33
33
|
"aximo_get_case",
|
|
34
34
|
"aximo_create_case",
|
|
35
|
+
"aximo_update_case",
|
|
35
36
|
"aximo_delete_case",
|
|
36
37
|
"aximo_create_plan",
|
|
37
38
|
"aximo_add_case_to_plan",
|
|
@@ -146,6 +147,62 @@ describe("aximo_list_projects", () => {
|
|
|
146
147
|
expect(new URL(captured.url).pathname).toBe("/api/organizations/projects");
|
|
147
148
|
});
|
|
148
149
|
});
|
|
150
|
+
describe("aximo_update_case", () => {
|
|
151
|
+
const caseId = "55555555-5555-4555-9555-555555555555";
|
|
152
|
+
it("PATCHes /api/cases/:id with only the provided fields", async () => {
|
|
153
|
+
let body = null;
|
|
154
|
+
const payload = {
|
|
155
|
+
success: true,
|
|
156
|
+
case: {
|
|
157
|
+
id: caseId,
|
|
158
|
+
projectId: "p1",
|
|
159
|
+
type: "web",
|
|
160
|
+
title: "Renamed",
|
|
161
|
+
description: null,
|
|
162
|
+
scenario: "Given a renamed page",
|
|
163
|
+
format: "gherkin",
|
|
164
|
+
startUrl: null,
|
|
165
|
+
updatedAt: "2026-08-14T00:00:00.000Z",
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
server.use(http.patch(`${TEST_BASE_URL}/api/cases/${caseId}`, async ({ request: req }) => {
|
|
169
|
+
body = await req.json();
|
|
170
|
+
return HttpResponse.json(payload);
|
|
171
|
+
}));
|
|
172
|
+
const tool = byName(defineTools(), "aximo_update_case");
|
|
173
|
+
const result = await tool.handler({
|
|
174
|
+
caseId,
|
|
175
|
+
title: "Renamed",
|
|
176
|
+
scenario: "Given a renamed page",
|
|
177
|
+
});
|
|
178
|
+
expect(result.isError).toBeUndefined();
|
|
179
|
+
expect(parsePayload(result)).toEqual(payload);
|
|
180
|
+
expect(body).toEqual({ title: "Renamed", scenario: "Given a renamed page" });
|
|
181
|
+
});
|
|
182
|
+
it("rejects an update with no fields instead of issuing an empty PATCH", async () => {
|
|
183
|
+
let called = false;
|
|
184
|
+
server.use(http.patch(`${TEST_BASE_URL}/api/cases/${caseId}`, () => {
|
|
185
|
+
called = true;
|
|
186
|
+
return HttpResponse.json({ success: true });
|
|
187
|
+
}));
|
|
188
|
+
const tool = byName(defineTools(), "aximo_update_case");
|
|
189
|
+
const result = await tool.handler({ caseId });
|
|
190
|
+
expect(result.isError).toBe(true);
|
|
191
|
+
expect(parsePayload(result).error).toBe("at least one field to update is required");
|
|
192
|
+
expect(called).toBe(false);
|
|
193
|
+
});
|
|
194
|
+
it("passes startUrl: null through as an explicit clear, not an empty update", async () => {
|
|
195
|
+
let body = null;
|
|
196
|
+
server.use(http.patch(`${TEST_BASE_URL}/api/cases/${caseId}`, async ({ request: req }) => {
|
|
197
|
+
body = await req.json();
|
|
198
|
+
return HttpResponse.json({ success: true });
|
|
199
|
+
}));
|
|
200
|
+
const tool = byName(defineTools(), "aximo_update_case");
|
|
201
|
+
const result = await tool.handler({ caseId, startUrl: null });
|
|
202
|
+
expect(result.isError).toBeUndefined();
|
|
203
|
+
expect(body).toEqual({ startUrl: null });
|
|
204
|
+
});
|
|
205
|
+
});
|
|
149
206
|
describe("aximo_list_plans", () => {
|
|
150
207
|
const projectId = "44444444-4444-4444-8444-444444444444";
|
|
151
208
|
it("calls /api/plans, passing filters through as query params", async () => {
|
|
@@ -255,7 +312,7 @@ describe("aximo_list_plan_runs", () => {
|
|
|
255
312
|
items: [
|
|
256
313
|
{
|
|
257
314
|
id: "88888888-8888-4888-9888-888888888888",
|
|
258
|
-
status: "
|
|
315
|
+
status: "completed",
|
|
259
316
|
triggerSource: "api",
|
|
260
317
|
executionMode: "parallel",
|
|
261
318
|
totalCases: 3,
|
|
@@ -292,6 +349,66 @@ describe("aximo_list_plan_runs", () => {
|
|
|
292
349
|
expect(result.isError).toBe(true);
|
|
293
350
|
expect(parsePayload(result).error).toBe("Test plan not found");
|
|
294
351
|
});
|
|
352
|
+
it("calls /api/plan-runs when given a projectId, passing status through", async () => {
|
|
353
|
+
const projectId = "44444444-4444-4444-8444-444444444444";
|
|
354
|
+
let url = null;
|
|
355
|
+
const payload = {
|
|
356
|
+
items: [
|
|
357
|
+
{
|
|
358
|
+
id: "88888888-8888-4888-9888-888888888888",
|
|
359
|
+
planId,
|
|
360
|
+
plan: { id: planId, title: "Nightly regression" },
|
|
361
|
+
status: "failed",
|
|
362
|
+
triggerSource: "api",
|
|
363
|
+
executionMode: "parallel",
|
|
364
|
+
totalCases: 3,
|
|
365
|
+
completedCases: 3,
|
|
366
|
+
passedCases: 2,
|
|
367
|
+
failedCases: 1,
|
|
368
|
+
startedAt: "2026-08-01T00:00:00.000Z",
|
|
369
|
+
completedAt: "2026-08-01T00:10:00.000Z",
|
|
370
|
+
createdAt: "2026-08-01T00:00:00.000Z",
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
nextCursor: "2026-08-01T00:00:00.000Z",
|
|
374
|
+
};
|
|
375
|
+
server.use(http.get(`${TEST_BASE_URL}/api/plan-runs`, ({ request: req }) => {
|
|
376
|
+
url = new URL(req.url);
|
|
377
|
+
return HttpResponse.json(payload);
|
|
378
|
+
}));
|
|
379
|
+
const tool = byName(defineTools(), "aximo_list_plan_runs");
|
|
380
|
+
const result = await tool.handler({
|
|
381
|
+
projectId,
|
|
382
|
+
status: "failed",
|
|
383
|
+
limit: 5,
|
|
384
|
+
cursor: "2026-08-02T00:00:00.000Z",
|
|
385
|
+
});
|
|
386
|
+
expect(result.isError).toBeUndefined();
|
|
387
|
+
expect(parsePayload(result)).toEqual(payload);
|
|
388
|
+
expect(url).not.toBeNull();
|
|
389
|
+
expect(url.searchParams.get("projectId")).toBe(projectId);
|
|
390
|
+
expect(url.searchParams.get("status")).toBe("failed");
|
|
391
|
+
expect(url.searchParams.get("limit")).toBe("5");
|
|
392
|
+
expect(url.searchParams.get("cursor")).toBe("2026-08-02T00:00:00.000Z");
|
|
393
|
+
});
|
|
394
|
+
it("rejects calls with both, or neither, of planId and projectId", async () => {
|
|
395
|
+
const tool = byName(defineTools(), "aximo_list_plan_runs");
|
|
396
|
+
const both = await tool.handler({
|
|
397
|
+
planId,
|
|
398
|
+
projectId: "44444444-4444-4444-8444-444444444444",
|
|
399
|
+
});
|
|
400
|
+
expect(both.isError).toBe(true);
|
|
401
|
+
expect(parsePayload(both).error).toBe("pass exactly one of planId or projectId");
|
|
402
|
+
const neither = await tool.handler({});
|
|
403
|
+
expect(neither.isError).toBe(true);
|
|
404
|
+
expect(parsePayload(neither).error).toBe("pass exactly one of planId or projectId");
|
|
405
|
+
});
|
|
406
|
+
it("rejects the status filter without projectId", async () => {
|
|
407
|
+
const tool = byName(defineTools(), "aximo_list_plan_runs");
|
|
408
|
+
const result = await tool.handler({ planId, status: "failed" });
|
|
409
|
+
expect(result.isError).toBe(true);
|
|
410
|
+
expect(parsePayload(result).error).toBe("the status filter requires projectId");
|
|
411
|
+
});
|
|
295
412
|
});
|
|
296
413
|
describe("aximo_get_plan_run", () => {
|
|
297
414
|
const planId = "77777777-7777-4777-9777-777777777777";
|
|
@@ -299,7 +416,7 @@ describe("aximo_get_plan_run", () => {
|
|
|
299
416
|
it("calls /api/triggers/plans/:id/runs/:runId and returns the run with cases", async () => {
|
|
300
417
|
const payload = {
|
|
301
418
|
id: planRunId,
|
|
302
|
-
status: "
|
|
419
|
+
status: "completed",
|
|
303
420
|
triggerSource: "api",
|
|
304
421
|
executionMode: "sequential",
|
|
305
422
|
totalCases: 1,
|
package/dist/types.d.ts
CHANGED
|
@@ -161,7 +161,7 @@ export interface PlanDetail {
|
|
|
161
161
|
export interface PlanRunSummary {
|
|
162
162
|
id: string;
|
|
163
163
|
status: string;
|
|
164
|
-
triggerSource: string
|
|
164
|
+
triggerSource: string;
|
|
165
165
|
executionMode: string;
|
|
166
166
|
totalCases: number;
|
|
167
167
|
completedCases: number;
|
|
@@ -171,8 +171,15 @@ export interface PlanRunSummary {
|
|
|
171
171
|
completedAt: string | null;
|
|
172
172
|
createdAt: string;
|
|
173
173
|
}
|
|
174
|
-
export interface
|
|
175
|
-
|
|
174
|
+
export interface ProjectPlanRunSummary extends PlanRunSummary {
|
|
175
|
+
planId: string;
|
|
176
|
+
plan: {
|
|
177
|
+
id: string;
|
|
178
|
+
title: string;
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export interface PlanRunList<T = PlanRunSummary> {
|
|
182
|
+
items: T[];
|
|
176
183
|
nextCursor?: string | undefined;
|
|
177
184
|
}
|
|
178
185
|
export interface Device {
|
|
@@ -236,11 +243,11 @@ export interface UpdateCaseResult {
|
|
|
236
243
|
case: {
|
|
237
244
|
id: string;
|
|
238
245
|
projectId: string;
|
|
239
|
-
type:
|
|
246
|
+
type: CaseType;
|
|
240
247
|
title: string;
|
|
241
248
|
description: string | null;
|
|
242
249
|
scenario: string;
|
|
243
|
-
format:
|
|
250
|
+
format: CaseFormat;
|
|
244
251
|
startUrl: string | null;
|
|
245
252
|
updatedAt: string;
|
|
246
253
|
};
|
|
@@ -366,6 +373,53 @@ export interface SessionCapture {
|
|
|
366
373
|
createdAt: string;
|
|
367
374
|
updatedAt: string;
|
|
368
375
|
}
|
|
376
|
+
export interface CaseAppInfo {
|
|
377
|
+
uploadArn: string;
|
|
378
|
+
uploadType: "own_app" | "existing" | "sample";
|
|
379
|
+
appName: string | null;
|
|
380
|
+
bundleId?: string;
|
|
381
|
+
}
|
|
382
|
+
export interface CaseMobileConfig {
|
|
383
|
+
deviceArn: string;
|
|
384
|
+
deviceName: string;
|
|
385
|
+
platform?: "ANDROID" | "IOS";
|
|
386
|
+
uploadArn?: string;
|
|
387
|
+
uploadType?: "own_app" | "existing" | "sample";
|
|
388
|
+
appName?: string;
|
|
389
|
+
bundleId?: string;
|
|
390
|
+
apps?: CaseAppInfo[];
|
|
391
|
+
}
|
|
392
|
+
export interface CaseWebConfig {
|
|
393
|
+
hostnameOverrideIds?: string[];
|
|
394
|
+
}
|
|
395
|
+
export interface CaseDesktopConfig {
|
|
396
|
+
userDeviceId: string;
|
|
397
|
+
name: string;
|
|
398
|
+
platform: "windows" | "macos" | "linux";
|
|
399
|
+
os: string | null;
|
|
400
|
+
}
|
|
401
|
+
export interface CaseMultiDeviceWebDevice {
|
|
402
|
+
kind: "web";
|
|
403
|
+
name: string;
|
|
404
|
+
startUrl?: string | null;
|
|
405
|
+
userDeviceId?: string;
|
|
406
|
+
localDeviceName?: string;
|
|
407
|
+
}
|
|
408
|
+
export interface CaseMultiDeviceMobileDevice {
|
|
409
|
+
kind: "mobile";
|
|
410
|
+
name: string;
|
|
411
|
+
mobilePlatform: "android" | "ios";
|
|
412
|
+
deviceArn?: string;
|
|
413
|
+
deviceName?: string;
|
|
414
|
+
userDeviceId?: string;
|
|
415
|
+
localDeviceName?: string;
|
|
416
|
+
uploadArn?: string;
|
|
417
|
+
uploadType?: "own_app" | "existing" | "sample";
|
|
418
|
+
appName?: string;
|
|
419
|
+
bundleId?: string;
|
|
420
|
+
apps?: CaseAppInfo[];
|
|
421
|
+
}
|
|
422
|
+
export type CaseMultiDeviceDevice = CaseMultiDeviceWebDevice | CaseMultiDeviceMobileDevice;
|
|
369
423
|
export interface CaseDetail {
|
|
370
424
|
id: string;
|
|
371
425
|
projectId: string;
|
|
@@ -380,4 +434,8 @@ export interface CaseDetail {
|
|
|
380
434
|
lastStatus: string | null;
|
|
381
435
|
createdAt: string;
|
|
382
436
|
updatedAt: string;
|
|
437
|
+
apiMobileConfig: CaseMobileConfig | null;
|
|
438
|
+
apiWebConfig: CaseWebConfig | null;
|
|
439
|
+
apiDesktopConfig: CaseDesktopConfig | null;
|
|
440
|
+
apiMultiDeviceConfig: CaseMultiDeviceDevice[] | null;
|
|
383
441
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autifyhq/aximo-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Autify Aximo Command Line Interface (CLI)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"autify",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"oxfmt": "0.45.0",
|
|
60
60
|
"oxlint": "1.60.0",
|
|
61
61
|
"postject": "1.0.0-alpha.6",
|
|
62
|
+
"resedit": "3.0.2",
|
|
62
63
|
"secretlint": "11.7.1",
|
|
63
64
|
"tsx": "4.21.0",
|
|
64
65
|
"typescript": "6.0.2",
|