@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
package/README.md
CHANGED
|
@@ -36,11 +36,7 @@ Get your API key from **Settings → API Keys** in the [Aximo dashboard](https:/
|
|
|
36
36
|
|
|
37
37
|
## Configuration
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
1. Value in `$AXIMO_HOME/config.json` (written by `aximo configure`)
|
|
42
|
-
2. Environment variable
|
|
43
|
-
3. Default (for base URL only: `https://aximo.autify.com`)
|
|
39
|
+
Each credential resolves independently: the value in `$AXIMO_HOME/config.json` (written by `aximo configure`) wins, then the environment variable, then the default.
|
|
44
40
|
|
|
45
41
|
| Variable | Required | Default | Notes |
|
|
46
42
|
| ---------------- | -------- | -------------------------- | ------------------------------------------------------------ |
|
|
@@ -48,60 +44,60 @@ The CLI resolves each credential independently in this order:
|
|
|
48
44
|
| `AXIMO_BASE_URL` | No | `https://aximo.autify.com` | Override for sandbox / self-hosted |
|
|
49
45
|
| `AXIMO_HOME` | No | `~/.aximo` | Data directory (config, device credentials, embedded Python) |
|
|
50
46
|
|
|
51
|
-
|
|
47
|
+
Set them interactively or pass flags — config is written to `$AXIMO_HOME/config.json` (mode `0600` where the OS supports it; Windows falls back to file ACLs), and an empty prompt keeps the existing value:
|
|
52
48
|
|
|
53
49
|
```bash
|
|
54
50
|
aximo configure # interactive prompts
|
|
55
51
|
aximo configure --base-url URL --api-key KEY # non-interactive
|
|
56
52
|
```
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
### Using environment variables
|
|
61
|
-
|
|
62
|
-
If you'd rather keep credentials out of a file (CI, ephemeral shells), set env vars:
|
|
54
|
+
Or keep credentials out of a file (CI, ephemeral shells) with env vars:
|
|
63
55
|
|
|
64
56
|
```bash
|
|
65
|
-
# macOS / Linux
|
|
66
57
|
export AXIMO_API_KEY=your-api-key
|
|
67
58
|
export AXIMO_BASE_URL=https://aximo-sbx.autify.dev # optional, prod is the default
|
|
68
|
-
|
|
69
|
-
# Windows (PowerShell, persistent)
|
|
70
|
-
[Environment]::SetEnvironmentVariable("AXIMO_API_KEY", "your-api-key", "User")
|
|
71
|
-
[Environment]::SetEnvironmentVariable("AXIMO_BASE_URL", "https://aximo-sbx.autify.dev", "User")
|
|
72
59
|
```
|
|
73
60
|
|
|
61
|
+
Credentials and device registrations are **per-environment** — to switch between production and sandbox (`https://aximo-sbx.autify.dev`), re-run `aximo configure` with the other `--base-url`.
|
|
62
|
+
|
|
74
63
|
## Commands
|
|
75
64
|
|
|
76
65
|
Run `aximo <command> --help` for detailed usage. `aximo schema [command.path]` returns machine-readable argument introspection (e.g. `aximo schema session.create`).
|
|
77
66
|
|
|
78
|
-
| Command
|
|
79
|
-
|
|
|
80
|
-
| `aximo configure`
|
|
81
|
-
| `aximo health check`
|
|
82
|
-
| `aximo project list`
|
|
83
|
-
| `aximo session create`
|
|
84
|
-
| `aximo session get`
|
|
85
|
-
| `aximo
|
|
86
|
-
| `aximo
|
|
87
|
-
| `aximo
|
|
88
|
-
| `aximo
|
|
89
|
-
| `aximo
|
|
90
|
-
| `aximo
|
|
91
|
-
| `aximo
|
|
92
|
-
| `aximo
|
|
93
|
-
| `aximo
|
|
94
|
-
| `aximo
|
|
95
|
-
| `aximo
|
|
96
|
-
| `aximo
|
|
97
|
-
| `aximo
|
|
98
|
-
| `aximo
|
|
99
|
-
| `aximo
|
|
100
|
-
| `aximo device
|
|
101
|
-
| `aximo device
|
|
102
|
-
| `aximo device
|
|
103
|
-
| `aximo device
|
|
104
|
-
| `aximo
|
|
67
|
+
| Command | Description |
|
|
68
|
+
| ---------------------------- | ---------------------------------------------------- |
|
|
69
|
+
| `aximo configure` | Save credentials to `$AXIMO_HOME/config.json` |
|
|
70
|
+
| `aximo health check` | Check API reachability |
|
|
71
|
+
| `aximo project list` | List organization projects |
|
|
72
|
+
| `aximo session create` | Create and queue a test session |
|
|
73
|
+
| `aximo session get` | Get session status |
|
|
74
|
+
| `aximo session save-as-case` | Save a finished session as a reusable test case |
|
|
75
|
+
| `aximo case create` | Create a test case |
|
|
76
|
+
| `aximo case delete` | Delete a test case |
|
|
77
|
+
| `aximo plan create` | Create a test plan |
|
|
78
|
+
| `aximo plan add-case` | Add a case to a plan |
|
|
79
|
+
| `aximo plan remove-case` | Remove a case from a plan |
|
|
80
|
+
| `aximo trigger case run` | Trigger a dashboard-created test case |
|
|
81
|
+
| `aximo trigger case get` | Get triggered case status |
|
|
82
|
+
| `aximo trigger plan run` | Trigger a test plan |
|
|
83
|
+
| `aximo trigger plan get` | Get plan run status |
|
|
84
|
+
| `aximo upload create` | Create an upload slot for an APK/IPA |
|
|
85
|
+
| `aximo upload list` | List previously uploaded apps |
|
|
86
|
+
| `aximo upload get` | Poll upload processing status |
|
|
87
|
+
| `aximo upload put` | Upload binary file to the slot |
|
|
88
|
+
| `aximo upload submit` | Finalize upload and trigger processing |
|
|
89
|
+
| `aximo device list` | List mobile devices from Device Farm |
|
|
90
|
+
| `aximo device setup` | First-time BYOD onboarding (API + Python + register) |
|
|
91
|
+
| `aximo device register` | Register this machine as a BYOD device |
|
|
92
|
+
| `aximo device connect` | Start the BYOD agent and connect to the relay |
|
|
93
|
+
| `aximo device status` | List your organization's BYOD devices |
|
|
94
|
+
| `aximo device ping` | Check your BYOD device's server-side status |
|
|
95
|
+
| `aximo device doctor` | Diagnose BYOD setup (env, Python, relay, status) |
|
|
96
|
+
| `aximo device logs` | Show the BYOD agent log |
|
|
97
|
+
| `aximo device unregister` | Remove a BYOD device server-side + clear local creds |
|
|
98
|
+
| `aximo device reset` | Remove BYOD local state (Python, deps, credentials) |
|
|
99
|
+
| `aximo schema` | Show CLI surface as JSON |
|
|
100
|
+
| `aximo mcp` | Run a stdio MCP server for agents (see below) |
|
|
105
101
|
|
|
106
102
|
Mutating commands support `--json '<raw body>'` for direct OpenAPI payloads and `--dry-run` to preview requests without executing.
|
|
107
103
|
|
|
@@ -137,6 +133,34 @@ aximo session get <sessionId>
|
|
|
137
133
|
|
|
138
134
|
Desktop sessions default to `platform=desktop`, `model=sonnet`. Mobile sessions require both `--device-arn` and `--upload-arn`.
|
|
139
135
|
|
|
136
|
+
### Test cases
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
aximo case create --project-id <id> --scenario <text> \
|
|
140
|
+
[--type web|mobile|desktop] [--title <t>] [--description <d>] \
|
|
141
|
+
[--format gherkin|markdown|plain_text] [--start-url <url>] \
|
|
142
|
+
[--directory-id <id>] [--json '<body>'] [--dry-run]
|
|
143
|
+
|
|
144
|
+
aximo case delete <caseId> [--dry-run]
|
|
145
|
+
|
|
146
|
+
aximo session save-as-case <sessionId> \
|
|
147
|
+
[--title <t>] [--type web|mobile|desktop] \
|
|
148
|
+
[--scenario <text>] [--format gherkin|markdown|plain_text] \
|
|
149
|
+
[--json '<body>'] [--dry-run]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`case create` returns a `caseId` you can run with `aximo trigger case run`. `session save-as-case` persists a finished session (terminal status, not yet linked to a case) as a reusable case, carrying over its scenario and run config. Cases created either way have API access enabled by default, so they are triggerable immediately. `case delete` permanently removes a case: it is dropped from any plans it belongs to, and past run sessions are kept but detached.
|
|
153
|
+
|
|
154
|
+
### Test plans
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
aximo plan create --project-id <id> --title <t> [--description <d>] [--json '<body>'] [--dry-run]
|
|
158
|
+
aximo plan add-case <planId> --case-id <caseId> [--json '<body>'] [--dry-run]
|
|
159
|
+
aximo plan remove-case <planId> <caseId> [--dry-run]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Build a runnable plan: `plan create` → `plan add-case` (repeat per case) → `aximo trigger plan run`. Plans created this way have API access enabled by default. Cases must belong to the same workspace as the plan.
|
|
163
|
+
|
|
140
164
|
### Triggers (test cases and plans)
|
|
141
165
|
|
|
142
166
|
```bash
|
|
@@ -146,7 +170,7 @@ aximo trigger plan run <planId> [...]
|
|
|
146
170
|
aximo trigger plan get <planId> <planRunId>
|
|
147
171
|
```
|
|
148
172
|
|
|
149
|
-
Triggers
|
|
173
|
+
Triggers a test case or plan. API access must be enabled on the resource — on by default for cases/plans created via this CLI; for dashboard-created ones, enable it in the Aximo dashboard settings. `trigger case run` returns a `sessionId` you can poll; `trigger plan run` returns a per-case breakdown.
|
|
150
174
|
|
|
151
175
|
### Uploads
|
|
152
176
|
|
|
@@ -226,6 +250,33 @@ aximo device connect # start the agent; keep the terminal open while tests r
|
|
|
226
250
|
|
|
227
251
|
Your device shows up in the Aximo UI under **Settings → Devices** with status `online` while connected. In the session form, pick your device as the execution target; Aximo routes the agent's actions through the tunnel to your machine.
|
|
228
252
|
|
|
253
|
+
### Self-hosted servers (self-signed / custom CA)
|
|
254
|
+
|
|
255
|
+
On-prem Aximo servers often present a self-signed certificate (a private "Aximo CA"). BYOD has **two** TLS clients: the Node CLI (API calls) and the embedded Python desktop agent (the `wss://…/devices/tunnel` relay). They trust a custom CA differently, so configure both:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
# 1. Save the server's CA on this machine (or copy aximo-ca.pem from your admin).
|
|
259
|
+
openssl s_client -connect YOUR_SERVER:8443 -showcerts </dev/null 2>/dev/null \
|
|
260
|
+
| awk '/BEGIN CERTIFICATE/{i++} i==2{print} /END CERTIFICATE/{if(i==2) exit}' > ~/aximo-ca.pem
|
|
261
|
+
|
|
262
|
+
# 2. Point the CLI (Node) at it. Used for API calls AND auto-forwarded to the agent.
|
|
263
|
+
export NODE_EXTRA_CA_CERTS="$HOME/aximo-ca.pem"
|
|
264
|
+
|
|
265
|
+
aximo device connect
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
With `NODE_EXTRA_CA_CERTS` set, `device connect` automatically builds a combined trust store (the embedded Python's `certifi` roots **plus** your CA) and points the agent at it via `SSL_CERT_FILE`, so the relay connection verifies without extra steps.
|
|
269
|
+
|
|
270
|
+
To override the agent's trust store explicitly, set `SSL_CERT_FILE` yourself. It must include public roots **and** your CA — `SSL_CERT_FILE` replaces the entire store, so your CA alone would break any other TLS the agent does (browser launch, uploads):
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
PYBIN="$HOME/.aximo/python/bin/python3"
|
|
274
|
+
cat "$($PYBIN -m certifi)" ~/aximo-ca.pem > ~/.aximo/agent-ca-bundle.pem
|
|
275
|
+
export SSL_CERT_FILE="$HOME/.aximo/agent-ca-bundle.pem"
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Persist whichever variables you use in your shell profile. Public-CA servers (e.g. `aximo.autify.com`) need none of this.
|
|
279
|
+
|
|
229
280
|
### Managing devices
|
|
230
281
|
|
|
231
282
|
```bash
|
|
@@ -335,17 +386,33 @@ The agent disables DPI-awareness and uses a calibrated scroll amount — if you
|
|
|
335
386
|
|
|
336
387
|
Default `$AXIMO_HOME` is `~/.aximo` on macOS/Linux, `%USERPROFILE%\.aximo` on Windows. Override via the `AXIMO_HOME` env var.
|
|
337
388
|
|
|
338
|
-
|
|
389
|
+
> Devices are registered per-environment (see [Configuration](#configuration)) — a device registered against sandbox can't connect to production and vice versa.
|
|
390
|
+
|
|
391
|
+
## MCP server
|
|
392
|
+
|
|
393
|
+
`aximo mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server over stdio, exposing your recorded sessions, screenshots, and test cases to MCP-capable agents (Claude Code, Claude Desktop, Cursor, Windsurf, VS Code, …). The motivating use: let an agent read a finished session and generate a Playwright spec without you piping JSON between commands.
|
|
394
|
+
|
|
395
|
+
It reuses the same credentials as every other command, so run `aximo configure` (or set `AXIMO_API_KEY`) first.
|
|
396
|
+
|
|
397
|
+
**Claude Code** — one command:
|
|
339
398
|
|
|
340
399
|
```bash
|
|
341
|
-
|
|
342
|
-
|
|
400
|
+
claude mcp add aximo -- aximo mcp
|
|
401
|
+
```
|
|
343
402
|
|
|
344
|
-
|
|
345
|
-
|
|
403
|
+
**Other clients** — point them at the `aximo mcp` stdio command. Claude Desktop, Cursor, and Windsurf use the `mcpServers` key:
|
|
404
|
+
|
|
405
|
+
```json
|
|
406
|
+
{
|
|
407
|
+
"mcpServers": {
|
|
408
|
+
"aximo": { "command": "aximo", "args": ["mcp"] }
|
|
409
|
+
}
|
|
410
|
+
}
|
|
346
411
|
```
|
|
347
412
|
|
|
348
|
-
|
|
413
|
+
VS Code (Copilot agent mode) uses the same entry under a `servers` key in `.vscode/mcp.json`.
|
|
414
|
+
|
|
415
|
+
The server exposes 16 tools: 10 read-only ones for inspecting sessions and cases (`aximo_get_session_timeline` is the load-bearing one for Playwright export) plus 6 that author content (create and delete cases, create 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.
|
|
349
416
|
|
|
350
417
|
## Agent Skills
|
|
351
418
|
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import pkg from "../package.json" with { type: "json" };
|
|
|
5
5
|
// is JSON-RPC.
|
|
6
6
|
const args = process.argv.slice(2);
|
|
7
7
|
if (args[0] === "mcp" && args.some((a) => a === "--help" || a === "-h")) {
|
|
8
|
-
process.stderr.write(`aximo mcp — stdio MCP server exposing
|
|
8
|
+
process.stderr.write(`aximo mcp — stdio MCP server exposing Aximo data plus create operations.\n` +
|
|
9
9
|
`\n` +
|
|
10
10
|
`Wire into an MCP client (e.g. Claude Desktop, Claude Code) by pointing it\n` +
|
|
11
11
|
`at this binary. Auth uses ~/.aximo/config.json (run \`aximo configure\`).\n` +
|
|
@@ -23,6 +23,8 @@ const main = defineCommand({
|
|
|
23
23
|
configure: () => import("./commands/configure.js").then((m) => m.default),
|
|
24
24
|
health: () => import("./commands/health/index.js").then((m) => m.default),
|
|
25
25
|
session: () => import("./commands/session/index.js").then((m) => m.default),
|
|
26
|
+
case: () => import("./commands/case/index.js").then((m) => m.default),
|
|
27
|
+
plan: () => import("./commands/plan/index.js").then((m) => m.default),
|
|
26
28
|
trigger: () => import("./commands/trigger/index.js").then((m) => m.default),
|
|
27
29
|
device: () => import("./commands/device/index.js").then((m) => m.default),
|
|
28
30
|
upload: () => import("./commands/upload/index.js").then((m) => m.default),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import { HttpResponse, http, server, TEST_BASE_URL } from "../../test-utils.js";
|
|
3
|
+
import { runCaseCreate } from "./create.js";
|
|
4
|
+
import { runCaseDelete } from "./delete.js";
|
|
5
|
+
beforeAll(() => {
|
|
6
|
+
process.env["AXIMO_API_KEY"] = "test-key";
|
|
7
|
+
process.env["AXIMO_BASE_URL"] = TEST_BASE_URL;
|
|
8
|
+
server.listen();
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => server.resetHandlers());
|
|
11
|
+
afterAll(() => server.close());
|
|
12
|
+
const MIN = {
|
|
13
|
+
projectId: "proj-1",
|
|
14
|
+
scenario: "Open the login page and verify it loads",
|
|
15
|
+
};
|
|
16
|
+
describe("runCaseCreate", () => {
|
|
17
|
+
it("POSTs body and returns success envelope", async () => {
|
|
18
|
+
let seen;
|
|
19
|
+
server.use(http.post(`${TEST_BASE_URL}/api/cases`, async ({ request: req }) => {
|
|
20
|
+
seen = await req.json();
|
|
21
|
+
return HttpResponse.json({ success: true, caseId: "c1" }, { status: 201 });
|
|
22
|
+
}));
|
|
23
|
+
const result = await runCaseCreate({ ...MIN, type: "web", title: "Login" });
|
|
24
|
+
expect(result.ok).toBe(true);
|
|
25
|
+
if (result.ok && !("dryRun" in result.data))
|
|
26
|
+
expect(result.data.caseId).toBe("c1");
|
|
27
|
+
expect(seen).toMatchObject({
|
|
28
|
+
projectId: "proj-1",
|
|
29
|
+
scenario: "Open the login page and verify it loads",
|
|
30
|
+
type: "web",
|
|
31
|
+
title: "Login",
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
it("rejects missing projectId", async () => {
|
|
35
|
+
const result = await runCaseCreate({ scenario: "x" });
|
|
36
|
+
expect(result.ok).toBe(false);
|
|
37
|
+
if (!result.ok)
|
|
38
|
+
expect(result.error.code).toBe("validation_error");
|
|
39
|
+
});
|
|
40
|
+
it("rejects missing scenario", async () => {
|
|
41
|
+
const result = await runCaseCreate({ projectId: "p1" });
|
|
42
|
+
expect(result.ok).toBe(false);
|
|
43
|
+
if (!result.ok)
|
|
44
|
+
expect(result.error.code).toBe("validation_error");
|
|
45
|
+
});
|
|
46
|
+
it("--json replaces individual flags and POSTs raw body", async () => {
|
|
47
|
+
let seen;
|
|
48
|
+
server.use(http.post(`${TEST_BASE_URL}/api/cases`, async ({ request: req }) => {
|
|
49
|
+
seen = await req.json();
|
|
50
|
+
return HttpResponse.json({ success: true, caseId: "c2" }, { status: 201 });
|
|
51
|
+
}));
|
|
52
|
+
const raw = JSON.stringify({ projectId: "p1", scenario: "raw", extra: true });
|
|
53
|
+
const result = await runCaseCreate({ json: raw });
|
|
54
|
+
expect(result.ok).toBe(true);
|
|
55
|
+
expect(seen).toEqual({ projectId: "p1", scenario: "raw", extra: true });
|
|
56
|
+
});
|
|
57
|
+
it("--dry-run returns request without calling API", async () => {
|
|
58
|
+
const result = await runCaseCreate({ ...MIN, dryRun: true });
|
|
59
|
+
expect(result.ok).toBe(true);
|
|
60
|
+
if (result.ok && "dryRun" in result.data) {
|
|
61
|
+
expect(result.data.dryRun).toBe(true);
|
|
62
|
+
expect(result.data.method).toBe("POST");
|
|
63
|
+
expect(result.data.url).toBe(`${TEST_BASE_URL}/api/cases`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
it("returns unauthorized envelope when AXIMO_API_KEY unset", async () => {
|
|
67
|
+
const prev = process.env["AXIMO_API_KEY"];
|
|
68
|
+
delete process.env["AXIMO_API_KEY"];
|
|
69
|
+
try {
|
|
70
|
+
const result = await runCaseCreate(MIN);
|
|
71
|
+
expect(result.ok).toBe(false);
|
|
72
|
+
if (!result.ok)
|
|
73
|
+
expect(result.error.code).toBe("unauthorized");
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
if (prev !== undefined)
|
|
77
|
+
process.env["AXIMO_API_KEY"] = prev;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
it("maps 404 to not_found", async () => {
|
|
81
|
+
server.use(http.post(`${TEST_BASE_URL}/api/cases`, () => HttpResponse.json({ error: "Project not found" }, { status: 404 })));
|
|
82
|
+
const result = await runCaseCreate(MIN);
|
|
83
|
+
expect(result.ok).toBe(false);
|
|
84
|
+
if (!result.ok)
|
|
85
|
+
expect(result.error.code).toBe("not_found");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe("runCaseDelete", () => {
|
|
89
|
+
it("DELETEs the case path and returns success", async () => {
|
|
90
|
+
let called = false;
|
|
91
|
+
server.use(http.delete(`${TEST_BASE_URL}/api/cases/c1`, () => {
|
|
92
|
+
called = true;
|
|
93
|
+
return HttpResponse.json({ success: true });
|
|
94
|
+
}));
|
|
95
|
+
const result = await runCaseDelete({ caseId: "c1" });
|
|
96
|
+
expect(result.ok).toBe(true);
|
|
97
|
+
expect(called).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
it("rejects missing caseId", async () => {
|
|
100
|
+
const result = await runCaseDelete({});
|
|
101
|
+
expect(result.ok).toBe(false);
|
|
102
|
+
if (!result.ok)
|
|
103
|
+
expect(result.error.code).toBe("validation_error");
|
|
104
|
+
});
|
|
105
|
+
it("maps 404 to not_found", async () => {
|
|
106
|
+
server.use(http.delete(`${TEST_BASE_URL}/api/cases/c1`, () => HttpResponse.json({ error: "Test case not found" }, { status: 404 })));
|
|
107
|
+
const result = await runCaseDelete({ caseId: "c1" });
|
|
108
|
+
expect(result.ok).toBe(false);
|
|
109
|
+
if (!result.ok)
|
|
110
|
+
expect(result.error.code).toBe("not_found");
|
|
111
|
+
});
|
|
112
|
+
it("--dry-run returns a DELETE request without calling API", async () => {
|
|
113
|
+
const result = await runCaseDelete({ caseId: "c1", dryRun: true });
|
|
114
|
+
expect(result.ok).toBe(true);
|
|
115
|
+
if (result.ok && "dryRun" in result.data) {
|
|
116
|
+
expect(result.data.method).toBe("DELETE");
|
|
117
|
+
expect(result.data.url).toBe(`${TEST_BASE_URL}/api/cases/c1`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ClientDeps } from "../../client.js";
|
|
2
|
+
import { type DryRunResult } from "../../mutate.js";
|
|
3
|
+
import type { CreateCaseResult, Envelope } from "../../types.js";
|
|
4
|
+
export interface CreateCaseOptions {
|
|
5
|
+
projectId?: unknown;
|
|
6
|
+
scenario?: unknown;
|
|
7
|
+
type?: unknown;
|
|
8
|
+
title?: unknown;
|
|
9
|
+
description?: unknown;
|
|
10
|
+
format?: unknown;
|
|
11
|
+
startUrl?: unknown;
|
|
12
|
+
directoryId?: unknown;
|
|
13
|
+
json?: unknown;
|
|
14
|
+
dryRun?: unknown;
|
|
15
|
+
}
|
|
16
|
+
export declare function runCaseCreate(options: CreateCaseOptions, deps?: ClientDeps): Promise<Envelope<CreateCaseResult | DryRunResult>>;
|
|
17
|
+
declare const _default: import("citty").CommandDef<{
|
|
18
|
+
readonly "project-id": {
|
|
19
|
+
readonly type: "string";
|
|
20
|
+
};
|
|
21
|
+
readonly scenario: {
|
|
22
|
+
readonly type: "string";
|
|
23
|
+
};
|
|
24
|
+
readonly type: {
|
|
25
|
+
readonly type: "string";
|
|
26
|
+
readonly description: "web | mobile | desktop";
|
|
27
|
+
};
|
|
28
|
+
readonly title: {
|
|
29
|
+
readonly type: "string";
|
|
30
|
+
};
|
|
31
|
+
readonly description: {
|
|
32
|
+
readonly type: "string";
|
|
33
|
+
};
|
|
34
|
+
readonly format: {
|
|
35
|
+
readonly type: "string";
|
|
36
|
+
readonly description: "gherkin | markdown | plain_text";
|
|
37
|
+
};
|
|
38
|
+
readonly "start-url": {
|
|
39
|
+
readonly type: "string";
|
|
40
|
+
};
|
|
41
|
+
readonly "directory-id": {
|
|
42
|
+
readonly type: "string";
|
|
43
|
+
};
|
|
44
|
+
readonly json: {
|
|
45
|
+
readonly type: "string";
|
|
46
|
+
readonly description: "Raw JSON request body (replaces individual flags)";
|
|
47
|
+
};
|
|
48
|
+
readonly "dry-run": {
|
|
49
|
+
readonly type: "boolean";
|
|
50
|
+
readonly description: "Validate and show request without calling API";
|
|
51
|
+
};
|
|
52
|
+
}>;
|
|
53
|
+
export default _default;
|
|
@@ -0,0 +1,71 @@
|
|
|
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 runCaseCreate(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
|
+
scenario: requireString("scenario", options.scenario),
|
|
20
|
+
type: optionalString("type", options.type),
|
|
21
|
+
title: optionalString("title", options.title),
|
|
22
|
+
description: optionalString("description", options.description),
|
|
23
|
+
format: optionalString("format", options.format),
|
|
24
|
+
startUrl: optionalString("startUrl", options.startUrl),
|
|
25
|
+
directoryId: optionalString("directoryId", options.directoryId),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return validationError(err);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (options.dryRun) {
|
|
33
|
+
return dryRunEnvelope("POST", "/api/cases", body);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const res = await request("/api/cases", { method: "POST", body }, deps ?? {});
|
|
37
|
+
return formatSuccess(res);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return handleApiError(err);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export default defineCommand({
|
|
44
|
+
meta: { name: "create", description: "Create a test case" },
|
|
45
|
+
args: {
|
|
46
|
+
"project-id": { type: "string" },
|
|
47
|
+
scenario: { type: "string" },
|
|
48
|
+
type: { type: "string", description: "web | mobile | desktop" },
|
|
49
|
+
title: { type: "string" },
|
|
50
|
+
description: { type: "string" },
|
|
51
|
+
format: { type: "string", description: "gherkin | markdown | plain_text" },
|
|
52
|
+
"start-url": { type: "string" },
|
|
53
|
+
"directory-id": { type: "string" },
|
|
54
|
+
json: { type: "string", description: "Raw JSON request body (replaces individual flags)" },
|
|
55
|
+
"dry-run": { type: "boolean", description: "Validate and show request without calling API" },
|
|
56
|
+
},
|
|
57
|
+
async run({ args }) {
|
|
58
|
+
printResult(await runCaseCreate({
|
|
59
|
+
projectId: args["project-id"],
|
|
60
|
+
scenario: args.scenario,
|
|
61
|
+
type: args.type,
|
|
62
|
+
title: args.title,
|
|
63
|
+
description: args.description,
|
|
64
|
+
format: args.format,
|
|
65
|
+
startUrl: args["start-url"],
|
|
66
|
+
directoryId: args["directory-id"],
|
|
67
|
+
json: args.json,
|
|
68
|
+
dryRun: args["dry-run"],
|
|
69
|
+
}));
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ClientDeps } from "../../client.js";
|
|
2
|
+
import { type DryRunResult } from "../../mutate.js";
|
|
3
|
+
import type { DeleteCaseResult, Envelope } from "../../types.js";
|
|
4
|
+
export interface DeleteCaseOptions {
|
|
5
|
+
caseId?: unknown;
|
|
6
|
+
dryRun?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export declare function runCaseDelete(options: DeleteCaseOptions, deps?: ClientDeps): Promise<Envelope<DeleteCaseResult | DryRunResult>>;
|
|
9
|
+
declare const _default: import("citty").CommandDef<{
|
|
10
|
+
readonly caseId: {
|
|
11
|
+
readonly type: "positional";
|
|
12
|
+
readonly description: "Case ID";
|
|
13
|
+
readonly required: true;
|
|
14
|
+
};
|
|
15
|
+
readonly "dry-run": {
|
|
16
|
+
readonly type: "boolean";
|
|
17
|
+
readonly description: "Validate and show request without calling API";
|
|
18
|
+
};
|
|
19
|
+
}>;
|
|
20
|
+
export default _default;
|
|
@@ -0,0 +1,39 @@
|
|
|
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 runCaseDelete(options, deps) {
|
|
8
|
+
let caseId;
|
|
9
|
+
try {
|
|
10
|
+
caseId = requireId("caseId", options.caseId);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
return validationError(err);
|
|
14
|
+
}
|
|
15
|
+
const path = `/api/cases/${encodeURIComponent(caseId)}`;
|
|
16
|
+
if (options.dryRun) {
|
|
17
|
+
return dryRunEnvelope("DELETE", path);
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const res = await request(path, { method: "DELETE" }, deps ?? {});
|
|
21
|
+
return formatSuccess(res);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
return handleApiError(err);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export default defineCommand({
|
|
28
|
+
meta: { name: "delete", description: "Delete a test case" },
|
|
29
|
+
args: {
|
|
30
|
+
caseId: { type: "positional", description: "Case ID", required: true },
|
|
31
|
+
"dry-run": { type: "boolean", description: "Validate and show request without calling API" },
|
|
32
|
+
},
|
|
33
|
+
async run({ args }) {
|
|
34
|
+
printResult(await runCaseDelete({
|
|
35
|
+
caseId: args.caseId,
|
|
36
|
+
dryRun: args["dry-run"],
|
|
37
|
+
}));
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
export default defineCommand({
|
|
3
|
+
meta: { name: "case", description: "Test case commands" },
|
|
4
|
+
subCommands: {
|
|
5
|
+
create: () => import("./create.js").then((m) => m.default),
|
|
6
|
+
delete: () => import("./delete.js").then((m) => m.default),
|
|
7
|
+
},
|
|
8
|
+
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { defineCommand } from "citty";
|
|
2
1
|
import { spawnSync } from "node:child_process";
|
|
3
2
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
3
|
import { platform } from "node:os";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
5
|
import { WebSocket } from "ws";
|
|
6
6
|
import { getBaseUrl, request } from "../../client.js";
|
|
7
|
-
import { formatSuccess, printResult } from "../../output.js";
|
|
8
7
|
import { loadConfig } from "../../config.js";
|
|
9
8
|
import { readCachedManifest } from "../../desktop-server/agent-sync.js";
|
|
10
9
|
import { installedPythonTag, PYTHON_RELEASE, PYTHON_VERSION } from "../../desktop-server/setup.js";
|
|
10
|
+
import { formatSuccess, printResult } from "../../output.js";
|
|
11
11
|
import { getAgentRequirementsPath, getAgentScriptPath, getDepsMarkerPath, getDeviceConfigPath, getEmbeddedPythonBinary, getPythonDir, getPythonMarkerPath, } from "./paths.js";
|
|
12
12
|
function redactKey(value) {
|
|
13
13
|
if (value.length <= 8)
|
|
@@ -249,12 +249,21 @@ async function checkAgentUpToDate(deps) {
|
|
|
249
249
|
function relayUrlFromBase() {
|
|
250
250
|
return `${getBaseUrl().replace(/^http/, "ws")}/devices/tunnel`;
|
|
251
251
|
}
|
|
252
|
+
// The relay is a Socket.IO / engine.io endpoint (websocket-only), which answers
|
|
253
|
+
// only at <path>/?EIO=4&transport=websocket. A raw WebSocket to the bare path
|
|
254
|
+
// 404s or hangs, so the old probe reported a false negative even when the relay
|
|
255
|
+
// was healthy. Probe the engine.io upgrade directly — a successful open (HTTP
|
|
256
|
+
// 101) means the relay is up; device auth is enforced later, after this opens.
|
|
257
|
+
export function relayProbeUrl() {
|
|
258
|
+
return `${relayUrlFromBase()}/?EIO=4&transport=websocket`;
|
|
259
|
+
}
|
|
252
260
|
const RELAY_CHECK_NAME = "relay handshake";
|
|
253
261
|
function checkRelayReachable() {
|
|
254
262
|
return new Promise((resolve) => {
|
|
255
263
|
const url = relayUrlFromBase();
|
|
264
|
+
const probeUrl = relayProbeUrl();
|
|
256
265
|
let settled = false;
|
|
257
|
-
const ws = new WebSocket(
|
|
266
|
+
const ws = new WebSocket(probeUrl, { handshakeTimeout: 5_000 });
|
|
258
267
|
const done = (check) => {
|
|
259
268
|
if (settled)
|
|
260
269
|
return;
|
|
@@ -280,7 +289,7 @@ function checkRelayReachable() {
|
|
|
280
289
|
done({
|
|
281
290
|
name: RELAY_CHECK_NAME,
|
|
282
291
|
status: "pass",
|
|
283
|
-
message: `
|
|
292
|
+
message: `engine.io upgrade to ${probeUrl} succeeded (device auth not verified)`,
|
|
284
293
|
});
|
|
285
294
|
});
|
|
286
295
|
ws.on("error", (err) => {
|