@dichovsky/testrail-api-client 1.0.0 → 2.1.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 +22 -0
- package/dist/cli/auth.d.ts +21 -0
- package/dist/cli/auth.js +16 -0
- package/dist/cli/body.d.ts +42 -0
- package/dist/cli/body.js +89 -0
- package/dist/cli/dispatch.d.ts +16 -0
- package/dist/cli/dispatch.js +87 -0
- package/dist/cli/handler-context.d.ts +43 -0
- package/dist/cli/handler-context.js +2 -0
- package/dist/cli/handlers/case-write.d.ts +4 -0
- package/dist/cli/handlers/case-write.js +26 -0
- package/dist/cli/handlers/case.d.ts +4 -0
- package/dist/cli/handlers/case.js +11 -0
- package/dist/cli/handlers/milestone.d.ts +4 -0
- package/dist/cli/handlers/milestone.js +15 -0
- package/dist/cli/handlers/project.d.ts +4 -0
- package/dist/cli/handlers/project.js +11 -0
- package/dist/cli/handlers/result-write.d.ts +4 -0
- package/dist/cli/handlers/result-write.js +40 -0
- package/dist/cli/handlers/result.d.ts +3 -0
- package/dist/cli/handlers/result.js +11 -0
- package/dist/cli/handlers/run-write.d.ts +10 -0
- package/dist/cli/handlers/run-write.js +29 -0
- package/dist/cli/handlers/run.d.ts +4 -0
- package/dist/cli/handlers/run.js +15 -0
- package/dist/cli/handlers/suite.d.ts +4 -0
- package/dist/cli/handlers/suite.js +10 -0
- package/dist/cli/handlers/user.d.ts +4 -0
- package/dist/cli/handlers/user.js +11 -0
- package/dist/cli/ids.d.ts +6 -0
- package/dist/cli/ids.js +20 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +198 -0
- package/dist/cli/install-skill.d.ts +35 -0
- package/dist/cli/install-skill.js +71 -0
- package/dist/cli/metadata.d.ts +37 -0
- package/dist/cli/metadata.js +151 -0
- package/dist/cli/output.d.ts +28 -0
- package/dist/cli/output.js +84 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -266
- package/dist/client-core.d.ts +16 -7
- package/dist/client-core.js +153 -27
- package/dist/client.d.ts +274 -118
- package/dist/client.js +404 -463
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/errors.d.ts +11 -9
- package/dist/errors.js +12 -8
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/modules/attachments.d.ts +19 -0
- package/dist/modules/attachments.js +64 -0
- package/dist/modules/cases.d.ts +13 -0
- package/dist/modules/cases.js +58 -0
- package/dist/modules/configurations.d.ts +14 -0
- package/dist/modules/configurations.js +37 -0
- package/dist/modules/datasets.d.ts +12 -0
- package/dist/modules/datasets.js +28 -0
- package/dist/modules/metadata.d.ts +14 -0
- package/dist/modules/metadata.js +31 -0
- package/dist/modules/milestones.d.ts +12 -0
- package/dist/modules/milestones.js +36 -0
- package/dist/modules/plans.d.ts +16 -0
- package/dist/modules/plans.js +59 -0
- package/dist/modules/projects.d.ts +36 -0
- package/dist/modules/projects.js +55 -0
- package/dist/modules/reports.d.ts +9 -0
- package/dist/modules/reports.js +16 -0
- package/dist/modules/results.d.ts +14 -0
- package/dist/modules/results.js +69 -0
- package/dist/modules/runs.d.ts +14 -0
- package/dist/modules/runs.js +57 -0
- package/dist/modules/sections.d.ts +16 -0
- package/dist/modules/sections.js +37 -0
- package/dist/modules/sharedSteps.d.ts +12 -0
- package/dist/modules/sharedSteps.js +28 -0
- package/dist/modules/suites.d.ts +37 -0
- package/dist/modules/suites.js +54 -0
- package/dist/modules/tests.d.ts +9 -0
- package/dist/modules/tests.js +25 -0
- package/dist/modules/users.d.ts +18 -0
- package/dist/modules/users.js +62 -0
- package/dist/modules/variables.d.ts +11 -0
- package/dist/modules/variables.js +24 -0
- package/dist/schemas.d.ts +544 -0
- package/dist/schemas.js +419 -0
- package/dist/types.d.ts +1 -55
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +4 -0
- package/package.json +23 -15
- package/skill/SKILL.md +395 -0
- package/src/cli/auth.ts +37 -0
- package/src/cli/body.ts +100 -0
- package/src/cli/dispatch.ts +91 -0
- package/src/cli/handler-context.ts +46 -0
- package/src/cli/handlers/case-write.ts +26 -0
- package/src/cli/handlers/case.ts +13 -0
- package/src/cli/handlers/milestone.ts +19 -0
- package/src/cli/handlers/project.ts +13 -0
- package/src/cli/handlers/result-write.ts +40 -0
- package/src/cli/handlers/result.ts +14 -0
- package/src/cli/handlers/run-write.ts +30 -0
- package/src/cli/handlers/run.ts +19 -0
- package/src/cli/handlers/suite.ts +12 -0
- package/src/cli/handlers/user.ts +13 -0
- package/src/cli/ids.ts +20 -0
- package/src/cli/index.ts +224 -0
- package/src/cli/install-skill.ts +89 -0
- package/src/cli/metadata.ts +194 -0
- package/src/cli/output.ts +96 -0
- package/src/cli.ts +1 -286
- package/src/client-core.ts +183 -67
- package/src/client.ts +414 -483
- package/src/constants.ts +1 -0
- package/src/errors.ts +18 -11
- package/src/index.ts +50 -8
- package/src/modules/attachments.ts +125 -0
- package/src/modules/cases.ts +78 -0
- package/src/modules/configurations.ts +68 -0
- package/src/modules/datasets.ts +44 -0
- package/src/modules/metadata.ts +63 -0
- package/src/modules/milestones.ts +54 -0
- package/src/modules/plans.ts +89 -0
- package/src/modules/projects.ts +67 -0
- package/src/modules/reports.ts +23 -0
- package/src/modules/results.ts +90 -0
- package/src/modules/runs.ts +70 -0
- package/src/modules/sections.ts +55 -0
- package/src/modules/sharedSteps.ts +44 -0
- package/src/modules/suites.ts +67 -0
- package/src/modules/tests.ts +28 -0
- package/src/modules/users.ts +87 -0
- package/src/modules/variables.ts +36 -0
- package/src/schemas.ts +551 -0
- package/src/types.ts +11 -60
- package/src/utils.ts +5 -0
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testrail-cli
|
|
3
|
+
description: Use the `testrail` CLI to query and write TestRail projects, suites, cases, runs, results, milestones, and users from the shell. Trigger when the user asks to look up, list, fetch, count, inspect, create, update, or publish TestRail entities, or when TESTRAIL_BASE_URL / TESTRAIL_EMAIL / TESTRAIL_API_KEY are set in the environment.
|
|
4
|
+
version: 2.1.0
|
|
5
|
+
license: MIT
|
|
6
|
+
homepage: https://github.com/dichovsky/testrail-api-client
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# `testrail` CLI
|
|
10
|
+
|
|
11
|
+
A zero-dependency Node CLI for the TestRail REST API. Covers query (`get`,
|
|
12
|
+
`list`) and write (`add`, `update`, `add-bulk`, `close`) operations across
|
|
13
|
+
projects, suites, cases, runs, results, milestones, and users.
|
|
14
|
+
|
|
15
|
+
This skill is designed for **coding agents** running shell commands. It is
|
|
16
|
+
not a TestRail user manual. For the browser UI, see TestRail's own docs.
|
|
17
|
+
|
|
18
|
+
## When to use this skill
|
|
19
|
+
|
|
20
|
+
- The user mentions TestRail by name, or asks about test cases, test runs,
|
|
21
|
+
test results, or test plans in a TestRail context.
|
|
22
|
+
- `TESTRAIL_BASE_URL` / `TESTRAIL_EMAIL` / `TESTRAIL_API_KEY` are set in the
|
|
23
|
+
environment.
|
|
24
|
+
- The user wants to: look up, list, fetch, count, inspect, create, update,
|
|
25
|
+
or publish TestRail entities from the shell or from CI.
|
|
26
|
+
|
|
27
|
+
## Install / verify
|
|
28
|
+
|
|
29
|
+
The CLI ships with the npm package `@dichovsky/testrail-api-client` and
|
|
30
|
+
exposes the `testrail` binary. Verify it is available:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx testrail --version
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
If you installed the package locally, `npx testrail` runs the local copy
|
|
37
|
+
without a global install.
|
|
38
|
+
|
|
39
|
+
## Authentication
|
|
40
|
+
|
|
41
|
+
The CLI requires three credentials. Provide them either as environment
|
|
42
|
+
variables (preferred — keeps secrets out of argv and shell history) or as
|
|
43
|
+
flags:
|
|
44
|
+
|
|
45
|
+
| Purpose | Env var | Flag |
|
|
46
|
+
| ------------- | ------------------- | ------------------ |
|
|
47
|
+
| TestRail URL | `TESTRAIL_BASE_URL` | `--base-url <url>` |
|
|
48
|
+
| Account email | `TESTRAIL_EMAIL` | `--email <email>` |
|
|
49
|
+
| API key | `TESTRAIL_API_KEY` | `--api-key <key>` |
|
|
50
|
+
|
|
51
|
+
If credentials are missing, the CLI exits 1 with `Error: Missing auth...`
|
|
52
|
+
on stderr. Never echo or log the API key.
|
|
53
|
+
|
|
54
|
+
## Command surface
|
|
55
|
+
|
|
56
|
+
<!-- GENERATED:command-table -->
|
|
57
|
+
| Resource | Action | Path args | Body | Description |
|
|
58
|
+
| --- | --- | --- | --- | --- |
|
|
59
|
+
| project | get | `<project_id>` | — | Fetch a single project by ID |
|
|
60
|
+
| project | list | — | — | List all projects (paginated) |
|
|
61
|
+
| suite | get | `<suite_id>` | — | Fetch a single suite by ID |
|
|
62
|
+
| suite | list | — | — | List suites in a project |
|
|
63
|
+
| case | get | `<case_id>` | — | Fetch a single test case by ID |
|
|
64
|
+
| case | list | — | — | List cases in a project (optionally filtered by suite) |
|
|
65
|
+
| run | get | `<run_id>` | — | Fetch a single run by ID |
|
|
66
|
+
| run | list | — | — | List runs in a project (paginated) |
|
|
67
|
+
| result | list | — | — | List results for a run (paginated) |
|
|
68
|
+
| milestone | get | `<milestone_id>` | — | Fetch a single milestone by ID |
|
|
69
|
+
| milestone | list | — | — | List milestones in a project (paginated) |
|
|
70
|
+
| user | get | `<user_id>` | — | Fetch a single user by ID |
|
|
71
|
+
| user | list | — | — | List users (paginated) |
|
|
72
|
+
| case | add | `<section_id>` | `AddCasePayloadSchema` | Create a new test case under a section |
|
|
73
|
+
| case | update | `<case_id>` | `UpdateCasePayloadSchema` | Update an existing test case (partial fields) |
|
|
74
|
+
| run | add | `<project_id>` | `AddRunPayloadSchema` | Create a new test run in a project |
|
|
75
|
+
| run | close | `<run_id>` | — (no body) | Close a test run (no body) |
|
|
76
|
+
| result | add | `<run_id>` `<case_id>` | `AddResultPayloadSchema` | Record a single result for a case in a run |
|
|
77
|
+
| result | add-bulk | `<run_id>` | `AddResultsForCasesPayloadSchema` | Record multiple results for cases in one API call |
|
|
78
|
+
<!-- /GENERATED:command-table -->
|
|
79
|
+
|
|
80
|
+
## Body input for write actions
|
|
81
|
+
|
|
82
|
+
For body-bearing write actions (all except `run close`), provide the JSON
|
|
83
|
+
payload via **exactly one** of:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# (a) inline string — best for short payloads, agent-generated
|
|
87
|
+
testrail case add 5 --data '{"title":"New case"}'
|
|
88
|
+
|
|
89
|
+
# (b) file — best for large/repeated payloads, reviewable in git
|
|
90
|
+
testrail case add 5 --data-file payload.json
|
|
91
|
+
|
|
92
|
+
# (c) piped stdin — best for shell composition with jq / curl / etc.
|
|
93
|
+
echo '{"title":"New case"}' | testrail case add 5
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The CLI exits 1 if zero or more than one body source is provided.
|
|
97
|
+
|
|
98
|
+
### `--dry-run`
|
|
99
|
+
|
|
100
|
+
Add `--dry-run` to validate the payload against the Zod schema and print
|
|
101
|
+
what _would_ be sent, without making an API call. Useful for verifying
|
|
102
|
+
payload shape before consuming TestRail rate limit.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
testrail case add 5 --data '{"title":"x"}' --dry-run
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Payload schemas
|
|
109
|
+
|
|
110
|
+
Each write action validates its body against a Zod schema with
|
|
111
|
+
`.passthrough()` — required fields must match types exactly (no
|
|
112
|
+
coercion; `"5"` is rejected where `5` is expected), and TestRail
|
|
113
|
+
`custom_*` fields pass through untouched.
|
|
114
|
+
|
|
115
|
+
<!-- GENERATED:payload-schemas -->
|
|
116
|
+
### `AddCasePayloadSchema` (used by `case add`)
|
|
117
|
+
|
|
118
|
+
```jsonc
|
|
119
|
+
{
|
|
120
|
+
"title": "string (required)",
|
|
121
|
+
"template_id": "number?",
|
|
122
|
+
"type_id": "number?",
|
|
123
|
+
"priority_id": "number?",
|
|
124
|
+
"estimate": "string?",
|
|
125
|
+
"milestone_id": "number?",
|
|
126
|
+
"refs": "string?",
|
|
127
|
+
"custom_fields": "Record<string, unknown>?"
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `UpdateCasePayloadSchema` (used by `case update`)
|
|
132
|
+
|
|
133
|
+
```jsonc
|
|
134
|
+
{
|
|
135
|
+
"title": "string?",
|
|
136
|
+
"template_id": "number?",
|
|
137
|
+
"type_id": "number?",
|
|
138
|
+
"priority_id": "number?",
|
|
139
|
+
"estimate": "string?",
|
|
140
|
+
"milestone_id": "number?",
|
|
141
|
+
"refs": "string?",
|
|
142
|
+
"custom_fields": "Record<string, unknown>?"
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### `AddRunPayloadSchema` (used by `run add`)
|
|
147
|
+
|
|
148
|
+
```jsonc
|
|
149
|
+
{
|
|
150
|
+
"name": "string (required)",
|
|
151
|
+
"suite_id": "number?",
|
|
152
|
+
"description": "string?",
|
|
153
|
+
"milestone_id": "number?",
|
|
154
|
+
"assignedto_id": "number?",
|
|
155
|
+
"include_all": "boolean?",
|
|
156
|
+
"case_ids": "number[]?",
|
|
157
|
+
"refs": "string?"
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `AddResultPayloadSchema` (used by `result add`)
|
|
162
|
+
|
|
163
|
+
```jsonc
|
|
164
|
+
{
|
|
165
|
+
"status_id": "number (required)",
|
|
166
|
+
"comment": "string?",
|
|
167
|
+
"version": "string?",
|
|
168
|
+
"elapsed": "string?",
|
|
169
|
+
"defects": "string?",
|
|
170
|
+
"assignedto_id": "number?",
|
|
171
|
+
"custom_fields": "Record<string, unknown>?"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### `AddResultsForCasesPayloadSchema` (used by `result add-bulk`)
|
|
176
|
+
|
|
177
|
+
```jsonc
|
|
178
|
+
{
|
|
179
|
+
"results": "object[] (required)"
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
<!-- /GENERATED:payload-schemas -->
|
|
183
|
+
|
|
184
|
+
For the authoritative type definitions, see `src/schemas.ts` in the
|
|
185
|
+
package source.
|
|
186
|
+
|
|
187
|
+
## Output
|
|
188
|
+
|
|
189
|
+
By default, `testrail` emits pretty-printed JSON to stdout. Use `--format
|
|
190
|
+
table` for column-aligned human-readable output. Use `--quiet` to
|
|
191
|
+
suppress stdout entirely (rely on exit code).
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
testrail project get 1 # JSON (default)
|
|
195
|
+
testrail project list --format table # Table
|
|
196
|
+
testrail run get 5 --quiet # Exit code 0/1 only
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Filtering output (preserve context budget)
|
|
200
|
+
|
|
201
|
+
The CLI emits the full JSON object for each entity. For list endpoints
|
|
202
|
+
with hundreds of items, that can blow the agent's context window. Filter
|
|
203
|
+
at the shell when possible:
|
|
204
|
+
|
|
205
|
+
**Preferred:** `jq` (if available — most dev/CI environments have it):
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
testrail run get 5 | jq '.passed_count'
|
|
209
|
+
testrail case list --project-id 1 | jq '.[] | {id, title}'
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Fallback:** Node one-liner (always available since the package itself
|
|
213
|
+
is a Node CLI):
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
testrail run get 5 | node -e 'const d=JSON.parse(require("fs").readFileSync(0));console.log(d.passed_count)'
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Recipes
|
|
220
|
+
|
|
221
|
+
### 1. Smoke-test auth & connectivity
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
testrail user list --limit 1 --quiet && echo "auth OK" || echo "auth FAILED"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Exit code 0 = creds resolve and TestRail responds; 1 = anything broken.
|
|
228
|
+
|
|
229
|
+
### 2. Fetch a project
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
testrail project get 5
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 3. List projects with pagination
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
testrail project list --limit 25 --offset 0
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### 4. List suites under a project
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
testrail suite list --project-id 5
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 5. List cases in a specific suite
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
testrail case list --project-id 5 --suite-id 12
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### 6. Extract just the IDs from any list (generic pattern)
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
testrail case list --project-id 5 | jq '.[].id'
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### 7. Count pass/fail for a run
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
testrail run get 42 | jq '{passed: .passed_count, failed: .failed_count}'
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 8. Page through a large result list
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
offset=0
|
|
269
|
+
while true; do
|
|
270
|
+
page=$(testrail result list --run-id 100 --limit 100 --offset $offset)
|
|
271
|
+
count=$(echo "$page" | jq 'length')
|
|
272
|
+
[ "$count" -eq 0 ] && break
|
|
273
|
+
echo "$page" | jq -c '.[]'
|
|
274
|
+
offset=$((offset + count))
|
|
275
|
+
done
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### 9. Author a new test case
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
testrail case add 12 --data '{
|
|
282
|
+
"title": "Login page accepts SSO redirect",
|
|
283
|
+
"type_id": 1,
|
|
284
|
+
"priority_id": 3,
|
|
285
|
+
"refs": "JIRA-1234"
|
|
286
|
+
}'
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### 10. Update a test case (partial fields)
|
|
290
|
+
|
|
291
|
+
```bash
|
|
292
|
+
testrail case update 87 --data '{"title": "Renamed", "priority_id": 4}'
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### 11. Create a CI test run
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
RUN=$(testrail run add 5 --data '{
|
|
299
|
+
"name": "CI build #'"$CI_BUILD_NUMBER"'",
|
|
300
|
+
"include_all": false,
|
|
301
|
+
"case_ids": [42, 43, 44]
|
|
302
|
+
}')
|
|
303
|
+
RUN_ID=$(echo "$RUN" | jq '.id')
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### 12. Publish bulk results from a CI run
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
testrail result add-bulk "$RUN_ID" --data-file /tmp/results.json
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Where `/tmp/results.json` has shape:
|
|
313
|
+
|
|
314
|
+
```json
|
|
315
|
+
{
|
|
316
|
+
"results": [
|
|
317
|
+
{ "case_id": 42, "status_id": 1, "comment": "passed" },
|
|
318
|
+
{ "case_id": 43, "status_id": 5, "comment": "failed: timeout" }
|
|
319
|
+
]
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### 13. Close a run when CI finishes
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
testrail run close "$RUN_ID"
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### 14. Validate a payload before sending (`--dry-run`)
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
testrail result add 100 42 --data '{"status_id":1,"comment":"sanity check"}' --dry-run
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Prints the parsed payload + a `"dryRun": true` marker; no API call made.
|
|
336
|
+
|
|
337
|
+
## Errors & exit codes
|
|
338
|
+
|
|
339
|
+
| Exit | Meaning |
|
|
340
|
+
| ---- | ---------------------------------------------------------------------------------- |
|
|
341
|
+
| `0` | Success |
|
|
342
|
+
| `1` | Any failure: bad auth, invalid args, validation, 4xx/5xx HTTP, rate limit, timeout |
|
|
343
|
+
|
|
344
|
+
Errors are written to stderr in the form `Error: <message>`. Common
|
|
345
|
+
causes:
|
|
346
|
+
|
|
347
|
+
- `Missing auth.` → env vars / flags not set.
|
|
348
|
+
- `<param> must be a positive integer` → bad path arg (e.g. `project get abc`).
|
|
349
|
+
- `Unknown resource '<x>'. Use: project, suite, case, run, result, milestone, user`
|
|
350
|
+
- `Unknown action '<a>' for <r>. Use: get, list, …`
|
|
351
|
+
- `Body required.` → write action invoked with no `--data` / `--data-file` / stdin.
|
|
352
|
+
- `Invalid JSON: …` → malformed body.
|
|
353
|
+
- `Payload validation failed: …` → body shape doesn't match the Zod schema.
|
|
354
|
+
- `TestRail API error: 404 Not Found …` → 4xx/5xx response from TestRail.
|
|
355
|
+
|
|
356
|
+
## Limits & gotchas
|
|
357
|
+
|
|
358
|
+
- **Rate limit:** 100 requests / 60s by default (configurable on the
|
|
359
|
+
programmatic client; the CLI uses defaults). The CLI throws an error
|
|
360
|
+
rather than queueing on overflow.
|
|
361
|
+
- **GET cache:** GET responses are cached in-process for ~5 minutes by
|
|
362
|
+
default. POSTs invalidate the entire cache. Stale reads are possible
|
|
363
|
+
if the same `testrail` invocation re-fetches the same endpoint within
|
|
364
|
+
the TTL.
|
|
365
|
+
- **Retry:** 5xx responses, 429s, and network errors are retried with
|
|
366
|
+
exponential backoff (max 3 attempts). 4xx and timeout errors are not
|
|
367
|
+
retried.
|
|
368
|
+
- **No coercion on write payloads:** `"5"` is **not** silently converted
|
|
369
|
+
to `5`. This is intentional — catches agent template-substitution
|
|
370
|
+
bugs at the CLI boundary rather than the API call site.
|
|
371
|
+
- **`custom_*` fields:** Pass through `.passthrough()` schemas unchanged.
|
|
372
|
+
Field naming follows TestRail's `custom_<field-system-name>` convention.
|
|
373
|
+
|
|
374
|
+
## When NOT to use this skill
|
|
375
|
+
|
|
376
|
+
- **Writing TypeScript/JavaScript code that imports the package.**
|
|
377
|
+
This skill documents the CLI surface only. For programmatic use,
|
|
378
|
+
read `README.md` and `CODEMAP.md` in the package — the programmatic
|
|
379
|
+
API exposes 101 methods, far beyond what the CLI covers.
|
|
380
|
+
- **Project / suite / section / milestone / user CRUD** beyond the 6
|
|
381
|
+
write actions listed above. Use the TestRail web UI for structural
|
|
382
|
+
setup (or the programmatic API).
|
|
383
|
+
- **Deletes** of any kind. The CLI deliberately does not expose `delete`
|
|
384
|
+
actions to keep agents away from irrecoverable operations.
|
|
385
|
+
- **Attachments** (upload/download). Not in the CLI surface; use the
|
|
386
|
+
programmatic API.
|
|
387
|
+
- **Browser/UI workflows.** This is a non-interactive CLI.
|
|
388
|
+
|
|
389
|
+
## See also
|
|
390
|
+
|
|
391
|
+
- `README.md` — package install, programmatic API overview, configuration
|
|
392
|
+
- `CODEMAP.md` — every public method, type, error class, and constant
|
|
393
|
+
- `src/schemas.ts` — Zod payload schemas (source of truth)
|
|
394
|
+
- `BACKLOG.md` — deferred CLI/skill features tracked for future releases
|
|
395
|
+
- TestRail API docs: <https://support.testrail.com/hc/en-us/articles/7077083596436-Introduction-to-the-TestRail-API>
|
package/src/cli/auth.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { TestRailConfig } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export interface AuthFlags {
|
|
4
|
+
baseUrl: string | undefined;
|
|
5
|
+
email: string | undefined;
|
|
6
|
+
apiKey: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AuthEnv {
|
|
10
|
+
TESTRAIL_BASE_URL?: string;
|
|
11
|
+
TESTRAIL_EMAIL?: string;
|
|
12
|
+
TESTRAIL_API_KEY?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type AuthResolution = { ok: true; config: TestRailConfig } | { ok: false; error: string };
|
|
16
|
+
|
|
17
|
+
export const MISSING_AUTH_MESSAGE =
|
|
18
|
+
'Missing auth. Set TESTRAIL_BASE_URL, TESTRAIL_EMAIL, TESTRAIL_API_KEY or use --base-url, --email, --api-key flags.';
|
|
19
|
+
|
|
20
|
+
export function resolveAuth(flags: AuthFlags, env: AuthEnv): AuthResolution {
|
|
21
|
+
const baseUrl = flags.baseUrl ?? env.TESTRAIL_BASE_URL;
|
|
22
|
+
const email = flags.email ?? env.TESTRAIL_EMAIL;
|
|
23
|
+
const apiKey = flags.apiKey ?? env.TESTRAIL_API_KEY;
|
|
24
|
+
|
|
25
|
+
if (
|
|
26
|
+
baseUrl === undefined ||
|
|
27
|
+
baseUrl === '' ||
|
|
28
|
+
email === undefined ||
|
|
29
|
+
email === '' ||
|
|
30
|
+
apiKey === undefined ||
|
|
31
|
+
apiKey === ''
|
|
32
|
+
) {
|
|
33
|
+
return { ok: false, error: MISSING_AUTH_MESSAGE };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { ok: true, config: { baseUrl, email, apiKey } };
|
|
37
|
+
}
|
package/src/cli/body.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import type { z } from 'zod';
|
|
3
|
+
import type { BodyInput } from './handler-context.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Source of a parsed body. Reported alongside the resolution result so
|
|
7
|
+
* handlers (or callers writing recipes) can log which input mechanism the
|
|
8
|
+
* agent actually used.
|
|
9
|
+
*/
|
|
10
|
+
export type BodySource = 'data' | 'file' | 'stdin';
|
|
11
|
+
|
|
12
|
+
export type BodyResolution<T> = { ok: true; payload: T; source: BodySource } | { ok: false; error: string };
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a write-action body from one of three mutually-exclusive sources:
|
|
16
|
+
*
|
|
17
|
+
* - `--data <json-string>` (provided via `BodyInput.dataFlag`)
|
|
18
|
+
* - `--data-file <path>` (provided via `BodyInput.dataFileFlag`; read via
|
|
19
|
+
* readFileSync so failures surface as a structured `ok: false` rather than
|
|
20
|
+
* crashing the CLI)
|
|
21
|
+
* - stdin (provided via `BodyInput.readStdin` thunk; the caller is
|
|
22
|
+
* responsible for non-TTY detection — only set the thunk when stdin is
|
|
23
|
+
* piped. The resolver invokes the thunk *only* when stdin is the
|
|
24
|
+
* selected source, so read actions and no-body writes never drain it.)
|
|
25
|
+
*
|
|
26
|
+
* After source resolution: JSON-parse the raw string, then validate against
|
|
27
|
+
* the supplied Zod schema. No coercion is applied (Q8 lock from
|
|
28
|
+
* SKILL-PLAN.md) — wrong types are rejected immediately so agent payload
|
|
29
|
+
* bugs surface at the CLI boundary rather than the API call site.
|
|
30
|
+
*
|
|
31
|
+
* Typed via `S extends z.ZodTypeAny` so the inferred payload type carries
|
|
32
|
+
* through to the caller — `resolveBody(input, AddCasePayloadSchema)` returns
|
|
33
|
+
* `BodyResolution<AddCasePayload>`, not `BodyResolution<unknown>`.
|
|
34
|
+
*
|
|
35
|
+
* @param input raw inputs from the CLI argv + stdin reader
|
|
36
|
+
* @param schema Zod schema for the expected payload shape
|
|
37
|
+
*/
|
|
38
|
+
export function resolveBody<S extends z.ZodTypeAny>(input: BodyInput, schema: S): BodyResolution<z.infer<S>> {
|
|
39
|
+
const sources = [
|
|
40
|
+
input.dataFlag !== undefined ? 'data' : null,
|
|
41
|
+
input.dataFileFlag !== undefined ? 'file' : null,
|
|
42
|
+
input.readStdin !== undefined ? 'stdin' : null,
|
|
43
|
+
].filter((s): s is BodySource => s !== null);
|
|
44
|
+
|
|
45
|
+
if (sources.length === 0) {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
error: 'Body required. Pass a JSON payload via --data <json>, --data-file <path>, or stdin pipe.',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (sources.length > 1) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
error: `Multiple body sources provided (${sources.join(', ')}). Use exactly one of --data, --data-file, or stdin.`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let raw: string;
|
|
59
|
+
let source: BodySource;
|
|
60
|
+
if (input.dataFlag !== undefined) {
|
|
61
|
+
raw = input.dataFlag;
|
|
62
|
+
source = 'data';
|
|
63
|
+
} else if (input.dataFileFlag !== undefined) {
|
|
64
|
+
try {
|
|
65
|
+
raw = readFileSync(input.dataFileFlag, 'utf-8');
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: `Cannot read --data-file '${input.dataFileFlag}': ${e instanceof Error ? e.message : String(e)}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
source = 'file';
|
|
73
|
+
} else {
|
|
74
|
+
// stdin is the only remaining source. Invoke the thunk now (and only
|
|
75
|
+
// now) so the underlying readFileSync(0) call happens lazily.
|
|
76
|
+
try {
|
|
77
|
+
raw = (input.readStdin as () => string)();
|
|
78
|
+
} catch (e) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
error: `Cannot read stdin: ${e instanceof Error ? e.message : String(e)}`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
source = 'stdin';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let parsed: unknown;
|
|
88
|
+
try {
|
|
89
|
+
parsed = JSON.parse(raw);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return { ok: false, error: `Invalid JSON: ${e instanceof Error ? e.message : String(e)}` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const result = schema.safeParse(parsed);
|
|
95
|
+
if (!result.success) {
|
|
96
|
+
return { ok: false, error: `Payload validation failed: ${result.error.message}` };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { ok: true, payload: result.data, source };
|
|
100
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Handler } from './handler-context.js';
|
|
2
|
+
import { handleProjectGet, handleProjectList } from './handlers/project.js';
|
|
3
|
+
import { handleSuiteGet, handleSuiteList } from './handlers/suite.js';
|
|
4
|
+
import { handleCaseGet, handleCaseList } from './handlers/case.js';
|
|
5
|
+
import { handleCaseAdd, handleCaseUpdate } from './handlers/case-write.js';
|
|
6
|
+
import { handleRunGet, handleRunList } from './handlers/run.js';
|
|
7
|
+
import { handleRunAdd, handleRunClose } from './handlers/run-write.js';
|
|
8
|
+
import { handleResultList } from './handlers/result.js';
|
|
9
|
+
import { handleResultAdd, handleResultAddBulk } from './handlers/result-write.js';
|
|
10
|
+
import { handleMilestoneGet, handleMilestoneList } from './handlers/milestone.js';
|
|
11
|
+
import { handleUserGet, handleUserList } from './handlers/user.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Single source of truth: every supported resource:action mapped to its handler.
|
|
15
|
+
* Adding an action is a one-line change here — no parallel registry to keep in
|
|
16
|
+
* sync. `dispatch()` derives both the known-resources list and the
|
|
17
|
+
* known-actions-per-resource list from this map's keys, so error wording stays
|
|
18
|
+
* accurate even as the map evolves.
|
|
19
|
+
*
|
|
20
|
+
* Keys are inserted in the canonical display order (`get` before `list`, etc.);
|
|
21
|
+
* JavaScript object iteration order is insertion-order-preserving for string
|
|
22
|
+
* keys, which keeps error messages stable.
|
|
23
|
+
*/
|
|
24
|
+
const HANDLERS: Record<string, Handler> = {
|
|
25
|
+
'project:get': handleProjectGet,
|
|
26
|
+
'project:list': handleProjectList,
|
|
27
|
+
'suite:get': handleSuiteGet,
|
|
28
|
+
'suite:list': handleSuiteList,
|
|
29
|
+
'case:get': handleCaseGet,
|
|
30
|
+
'case:list': handleCaseList,
|
|
31
|
+
'case:add': handleCaseAdd,
|
|
32
|
+
'case:update': handleCaseUpdate,
|
|
33
|
+
'run:get': handleRunGet,
|
|
34
|
+
'run:list': handleRunList,
|
|
35
|
+
'run:add': handleRunAdd,
|
|
36
|
+
'run:close': handleRunClose,
|
|
37
|
+
'result:list': handleResultList,
|
|
38
|
+
'result:add': handleResultAdd,
|
|
39
|
+
'result:add-bulk': handleResultAddBulk,
|
|
40
|
+
'milestone:get': handleMilestoneGet,
|
|
41
|
+
'milestone:list': handleMilestoneList,
|
|
42
|
+
'user:get': handleUserGet,
|
|
43
|
+
'user:list': handleUserList,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const RESOURCES: Record<string, readonly string[]> = (() => {
|
|
47
|
+
const grouped: Record<string, string[]> = {};
|
|
48
|
+
for (const key of Object.keys(HANDLERS)) {
|
|
49
|
+
const [resource, action] = key.split(':');
|
|
50
|
+
if (resource === undefined || action === undefined) continue;
|
|
51
|
+
const existing = grouped[resource];
|
|
52
|
+
if (existing === undefined) {
|
|
53
|
+
grouped[resource] = [action];
|
|
54
|
+
} else {
|
|
55
|
+
existing.push(action);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return grouped;
|
|
59
|
+
})();
|
|
60
|
+
|
|
61
|
+
export type DispatchResult = { ok: true; handler: Handler } | { ok: false; error: string };
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Returns every registered `resource:action` key. Used by the
|
|
65
|
+
* metadata↔dispatch consistency tests to catch handlers added without a
|
|
66
|
+
* matching metadata entry — the inverse of the metadata-first check.
|
|
67
|
+
*/
|
|
68
|
+
export function getRegisteredActions(): readonly string[] {
|
|
69
|
+
return Object.keys(HANDLERS);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function dispatch(resource: string, action: string): DispatchResult {
|
|
73
|
+
const actions = RESOURCES[resource];
|
|
74
|
+
if (actions === undefined) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: `Unknown resource '${resource}'. Use: ${Object.keys(RESOURCES).join(', ')}`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (!actions.includes(action)) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
error: `Unknown action '${action}' for ${resource}. Use: ${actions.join(', ')}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const handler = HANDLERS[`${resource}:${action}`];
|
|
87
|
+
if (handler === undefined) {
|
|
88
|
+
return { ok: false, error: `No handler registered for ${resource}:${action}` };
|
|
89
|
+
}
|
|
90
|
+
return { ok: true, handler };
|
|
91
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { TestRailClient } from '../client.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parsed CLI argument bundle passed to every handler.
|
|
5
|
+
*
|
|
6
|
+
* `pathParams` is the slice of positional args after `[resource, action]` —
|
|
7
|
+
* read handlers consume `pathParams[0]` (the single id), write handlers may
|
|
8
|
+
* consume multiple (e.g., `result add <run_id> <case_id>` uses [0] and [1]).
|
|
9
|
+
*/
|
|
10
|
+
export interface HandlerArgs {
|
|
11
|
+
pathParams: readonly string[];
|
|
12
|
+
projectId?: string;
|
|
13
|
+
suiteId?: string;
|
|
14
|
+
runId?: string;
|
|
15
|
+
caseId?: string;
|
|
16
|
+
limit?: string;
|
|
17
|
+
offset?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Raw inputs for the body-source resolver. The handler decides whether to
|
|
22
|
+
* consume any of these (write handlers do; read handlers ignore). When all
|
|
23
|
+
* three are absent for a write action, the resolver emits a "body required"
|
|
24
|
+
* error.
|
|
25
|
+
*
|
|
26
|
+
* `readStdin` is a thunk rather than pre-read contents so stdin is *only*
|
|
27
|
+
* drained when the resolver actually selects it as the body source. Read
|
|
28
|
+
* actions and no-body writes (`run close`) never invoke it — avoiding the
|
|
29
|
+
* "tail -f | testrail run close" hang and the cost of slurping a large
|
|
30
|
+
* redirected stdin that the handler will throw away.
|
|
31
|
+
*/
|
|
32
|
+
export interface BodyInput {
|
|
33
|
+
dataFlag?: string;
|
|
34
|
+
dataFileFlag?: string;
|
|
35
|
+
readStdin?: () => string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface HandlerContext {
|
|
39
|
+
client: TestRailClient;
|
|
40
|
+
args: HandlerArgs;
|
|
41
|
+
bodyInput: BodyInput;
|
|
42
|
+
dryRun: boolean;
|
|
43
|
+
out: (data: unknown) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type Handler = (ctx: HandlerContext) => Promise<void>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HandlerContext } from '../handler-context.js';
|
|
2
|
+
import { parseId } from '../ids.js';
|
|
3
|
+
import { resolveBody } from '../body.js';
|
|
4
|
+
import { AddCasePayloadSchema, UpdateCasePayloadSchema } from '../../schemas.js';
|
|
5
|
+
|
|
6
|
+
export async function handleCaseAdd(ctx: HandlerContext): Promise<void> {
|
|
7
|
+
const sectionId = parseId(ctx.args.pathParams[0], 'section_id');
|
|
8
|
+
const body = resolveBody(ctx.bodyInput, AddCasePayloadSchema);
|
|
9
|
+
if (!body.ok) throw new Error(body.error);
|
|
10
|
+
if (ctx.dryRun) {
|
|
11
|
+
ctx.out({ dryRun: true, action: 'case add', sectionId, payload: body.payload, source: body.source });
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
ctx.out(await ctx.client.addCase(sectionId, body.payload));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function handleCaseUpdate(ctx: HandlerContext): Promise<void> {
|
|
18
|
+
const caseId = parseId(ctx.args.pathParams[0], 'case_id');
|
|
19
|
+
const body = resolveBody(ctx.bodyInput, UpdateCasePayloadSchema);
|
|
20
|
+
if (!body.ok) throw new Error(body.error);
|
|
21
|
+
if (ctx.dryRun) {
|
|
22
|
+
ctx.out({ dryRun: true, action: 'case update', caseId, payload: body.payload, source: body.source });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
ctx.out(await ctx.client.updateCase(caseId, body.payload));
|
|
26
|
+
}
|