pingcode-cli-unofficial 1.7.1
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 +943 -0
- package/dist/bin/pingcode.js +17001 -0
- package/dist/bin/pingcode.js.map +1 -0
- package/package.json +51 -0
- package/skills/pingcode/SKILL.md +431 -0
- package/skills/pingcode/modules/api.md +116 -0
- package/skills/pingcode/modules/cicd.md +222 -0
- package/skills/pingcode/modules/crosscutting.md +168 -0
- package/skills/pingcode/modules/pjm.md +385 -0
- package/skills/pingcode/modules/scm.md +432 -0
- package/skills/pingcode/modules/ship.md +197 -0
- package/skills/pingcode/modules/testhub.md +386 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# 测试管理 (testhub) — `testhub`
|
|
2
|
+
|
|
3
|
+
> Part of the `pingcode` skill. Read [`../SKILL.md`](../SKILL.md) first — it carries the
|
|
4
|
+
> authentication gate, the stdout/stderr contract, the exit-code table and the rules that
|
|
5
|
+
> apply to every module. This file is only the testhub command surface plus the testhub-specific traps.
|
|
6
|
+
|
|
7
|
+
### Test libraries 测试库 — `testhub libraries`
|
|
8
|
+
|
|
9
|
+
A **test library** is testhub's parent scope, the way a product is ship's. Resolve it first: case
|
|
10
|
+
states, case types, run results, the module tree and the plan list are **all library-scoped**.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pingcode testhub libraries list --json
|
|
14
|
+
pingcode testhub libraries list --keywords payment --json
|
|
15
|
+
pingcode testhub libraries get LIB --json # name, identifier such as LIB, or id
|
|
16
|
+
pingcode testhub libraries create --name "Payments" --identifier PAY --json
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`--keywords` searches library **names** only — the identifier is not searchable server-side. The
|
|
20
|
+
`--identifier` given to `create` must be unique across the organisation and the server enforces it,
|
|
21
|
+
and `--name` is capped at **32 characters** (verified live 2026-08-02: a longer name is rejected with
|
|
22
|
+
code `100019`, exit 7).
|
|
23
|
+
There is no `libraries update` and no `libraries delete` leaf, for **two different reasons**: the
|
|
24
|
+
API publishes no library DELETE at all, so a library created here is permanent — while a library
|
|
25
|
+
PATCH *does* exist upstream and is simply not wrapped:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pingcode api PATCH /v1/testhub/libraries/<id> --set description="…" # works (verified live)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Note that a `PATCH` with `description: ""` answers 200 and **keeps the old value** (verified live
|
|
32
|
+
2026-08-04): this API has no way to clear that field.
|
|
33
|
+
|
|
34
|
+
### `testhub meta` — the ids a testhub write cannot be built without
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pingcode testhub meta case-states --library LIB --json # --state / state_id
|
|
38
|
+
pingcode testhub meta case-types --library LIB --json # --type / type_id
|
|
39
|
+
pingcode testhub meta run-statuses --library LIB --json # --status / status_id
|
|
40
|
+
pingcode testhub meta plan-types --library LIB --json # --type on `plans create`
|
|
41
|
+
pingcode testhub meta suites --library LIB --json # --suite; PATH is what --suite takes
|
|
42
|
+
pingcode testhub meta important-levels --json # --important-level; org-wide
|
|
43
|
+
pingcode testhub meta plan-states --json # --state on `plans update`; org-wide
|
|
44
|
+
pingcode testhub meta case-properties --library LIB --json # the field keys behind --set
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`important-levels` and `plan-states` are the two lookups with **no per-library variant**, so both
|
|
48
|
+
*refuse* `--library` with exit 2 rather than ignoring it (the flag is hidden from `--help`, which is
|
|
49
|
+
why it is spelled out here). The others require `--library`.
|
|
50
|
+
|
|
51
|
+
**Three different vocabularies answer to the word "state" here, and they are not interchangeable:**
|
|
52
|
+
|
|
53
|
+
| Command | Vocabulary | Scope | Used by |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| `meta case-states` | 设计 / 就绪 / 废弃 | per library | `cases update --state`, `cases bulk-update --state` |
|
|
56
|
+
| `meta plan-states` | 未开始 / 进行中 / 已完成 | organisation | `plans update --state` |
|
|
57
|
+
| `meta run-statuses` | 未测 / 通过 / 失败 / 受阻 / 跳过 | per library | `runs update --status`, `runs bulk-update --status` |
|
|
58
|
+
|
|
59
|
+
`case-properties` lists the fields **effective in a library**, and its `KEY` column is what a write
|
|
60
|
+
addresses. Read rule 11 before using one with `--set`: on this tenant every row is a *built-in*
|
|
61
|
+
field, and pushing a built-in through the properties map either answers HTTP 500 or rewrites the
|
|
62
|
+
top-level field of the same name.
|
|
63
|
+
|
|
64
|
+
`suites` accepts `--parent-id root` for the top level only, or a node id for that node's children.
|
|
65
|
+
|
|
66
|
+
### Test cases 用例 — `testhub cases`
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pingcode testhub cases list --library LIB --json
|
|
70
|
+
pingcode testhub cases list --library LIB --state 已评审 --type 功能测试 --json
|
|
71
|
+
pingcode testhub cases list --library LIB --suite "登录 / 双因素" --keywords sso --page-size 20 --page 0 --json
|
|
72
|
+
pingcode testhub cases list --library LIB --all --limit 200 --json
|
|
73
|
+
|
|
74
|
+
pingcode testhub cases get 5f0e1a2b3c4d5e6f70819200 --json # an id or a short_id
|
|
75
|
+
pingcode testhub cases get aB3dEf9h --json
|
|
76
|
+
|
|
77
|
+
pingcode testhub cases create --library LIB --title "SSO login" --dry-run --json
|
|
78
|
+
pingcode testhub cases create --library LIB --title "SSO login" \
|
|
79
|
+
--suite "登录 / 双因素" --type 功能测试 --important-level 高 --json
|
|
80
|
+
|
|
81
|
+
pingcode testhub cases update aB3dEf9h --title "SSO login (v2)" --json
|
|
82
|
+
pingcode testhub cases update aB3dEf9h --state 已评审 --json
|
|
83
|
+
pingcode testhub cases update aB3dEf9h --set 自动化=5cb7e763fda1ce4ca0010002 --json
|
|
84
|
+
|
|
85
|
+
# import many at once, and fix them up in one call afterwards
|
|
86
|
+
pingcode testhub cases bulk-create --library LIB --file cases.json --dry-run --json
|
|
87
|
+
pingcode testhub cases bulk-create --library LIB --important-level P1 --file cases.json --json
|
|
88
|
+
pingcode testhub cases bulk-update --case aB3dEf9h --case-id 5f0e…9200 --library LIB --state 就绪 --json
|
|
89
|
+
pingcode testhub cases bulk-update --library LIB --file fixes.json --json
|
|
90
|
+
|
|
91
|
+
pingcode testhub cases delete aB3dEf9h --yes --json # ⚠️ deletes the case's RUNS too
|
|
92
|
+
pingcode testhub cases history list aB3dEf9h --json # latest result of every run of this case
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`cases.json` for `bulk-create` is a JSON array (or `{"cases": [ … ]}`), one object per case:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
[{"title": "SSO login", "description": "…", "precondition": "…",
|
|
99
|
+
"type": "功能测试", "important_level": "P1", "maintenance": "wangxiao",
|
|
100
|
+
"steps": [{"description": "open /login", "expected_value": "200"}]}]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Names are resolved for you (`type`, `important_level`, `maintenance`, and `state` on
|
|
104
|
+
`bulk-update`); every key also has an `_id` twin that is sent verbatim. **`suite`/`suite_id` and
|
|
105
|
+
`state`/`state_id` are refused on `bulk-create`** — the API accepts them and lands nothing, so the
|
|
106
|
+
CLI stops you instead of letting a 60-case import land in the wrong module. Unknown keys are refused
|
|
107
|
+
for the same reason. Up to **100** entries per call.
|
|
108
|
+
|
|
109
|
+
`testhub cases list` is `POST /v1/testhub/cases/search`; the plain list endpoint is never used
|
|
110
|
+
(unfiltered it scans every visible library). `--state` is **PATCH-only**: a case is always created in
|
|
111
|
+
the library's initial state, so `cases create` has no `--state`.
|
|
112
|
+
|
|
113
|
+
### Test plans 测试计划 — `testhub plans`
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pingcode testhub plans list --library LIB --json
|
|
117
|
+
pingcode testhub plans list --library LIB --name "2026 S1 回归" --json
|
|
118
|
+
pingcode testhub plans get "2026 S1 回归" --library LIB --json # name, id or short_id
|
|
119
|
+
pingcode testhub plans create --library LIB --name "2026 S2 回归" \
|
|
120
|
+
--type 普通 --start 2026-08-10 --end 2026-08-31 --assignee 张三 --json
|
|
121
|
+
|
|
122
|
+
pingcode testhub plans update "2026 S2 回归" --library LIB --state 进行中 --json
|
|
123
|
+
pingcode testhub plans update "2026 S2 回归" --library LIB --summary "42/50 passed, 3 blocked" --json
|
|
124
|
+
pingcode testhub plans update "2026 S2 回归" --library LIB --end 2026-09-07 --json
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`create` takes all five: `--name` (unique within the library), `--type` (from
|
|
128
|
+
`testhub meta plan-types`), `--start`, `--end` and `--assignee`.
|
|
129
|
+
**Read §4c rule 12 before passing a date** — `--end` lands on 23:59:59, not midnight.
|
|
130
|
+
|
|
131
|
+
`update` is partial: only the flags you pass are sent. It is the only way to move a plan's state and
|
|
132
|
+
the only way to write the test-report `summary`. Three things to know:
|
|
133
|
+
|
|
134
|
+
- `--state` takes an **organisation-level** plan state (`meta plan-states`), not a case state;
|
|
135
|
+
- the dates are stored **verbatim** — unlike a pjm sprint or release window, the server does **not**
|
|
136
|
+
snap them to whole days (verified live 2026-08-04);
|
|
137
|
+
- an empty patch is refused locally (exit 2) because the API answers 200 to one and changes nothing,
|
|
138
|
+
and `--summary ""` is refused too: the server rejects an empty summary (`100003`), so a summary can
|
|
139
|
+
be replaced but never cleared.
|
|
140
|
+
|
|
141
|
+
There is still **no plan delete** — that endpoint does not exist.
|
|
142
|
+
|
|
143
|
+
### Runs 执行用例 — `testhub runs`
|
|
144
|
+
|
|
145
|
+
A **run** is one case scheduled inside one plan; recording a result means updating the run.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pingcode testhub runs list --library LIB --plan "2026 S1 回归" --json
|
|
149
|
+
pingcode testhub runs list --library LIB --plan "2026 S1 回归" --status 失败 --executor wangxiao --json
|
|
150
|
+
pingcode testhub runs list --plan-id 5f0e1a2b3c4d5e6f70819200 --all --limit 200 --json
|
|
151
|
+
|
|
152
|
+
pingcode testhub runs update 7hK2mQ9x --status 通过 --remark "retested on iOS" --dry-run --json
|
|
153
|
+
pingcode testhub runs update 7hK2mQ9x --status 通过 --executor wangxiao --json
|
|
154
|
+
pingcode testhub runs update 7hK2mQ9x --status 失败 \
|
|
155
|
+
--step s1=通过 --step s2=失败 --step-actual s2="500 from /login" --json
|
|
156
|
+
|
|
157
|
+
pingcode testhub runs bulk --library LIB --plan "2026 S1 回归" \
|
|
158
|
+
--add-case 5f0e1a2b3c4d5e6f70819200 --executor wangxiao --dry-run --json
|
|
159
|
+
pingcode testhub runs bulk --library LIB --plan "2026 S1 回归" --set-status 7hK2mQ9x=通过 --json
|
|
160
|
+
pingcode testhub runs bulk --library LIB --plan "2026 S1 回归" --remove-run 7hK2mQ9x --json
|
|
161
|
+
|
|
162
|
+
# add runs and record results without the plan-scoped bulk
|
|
163
|
+
pingcode testhub runs create --library LIB --plan "2026 S1 回归" --case aB3dEf9h --executor wangxiao --json
|
|
164
|
+
pingcode testhub runs bulk-create --library LIB --plan "2026 S1 回归" --case aB3dEf9h --case-id 5f0e…9200 --json
|
|
165
|
+
pingcode testhub runs bulk-update --run 7hK2mQ9x --run-id 5f0e…9200 --library LIB --status 通过 --json
|
|
166
|
+
pingcode testhub runs bulk-update --library LIB --file results.json --json
|
|
167
|
+
|
|
168
|
+
pingcode testhub runs history list 7hK2mQ9x --json # every result ever recorded
|
|
169
|
+
pingcode testhub runs history get 7hK2mQ9x <history-id> --json
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`results.json` for `bulk-update` is one object per run; `status` (or `status_id`) is **required** on
|
|
173
|
+
every entry, and `steps` is refused — use `runs update --step` for those:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{"runs": [{"run": "7hK2mQ9x", "status": "通过", "remark": "retested"},
|
|
177
|
+
{"run_id": "5f0e…9200", "status_id": "68ff…9bdb"}]}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**The two bulk-run halves fail in opposite ways, and this is the single most important thing to know
|
|
181
|
+
before scripting them:**
|
|
182
|
+
|
|
183
|
+
| Command | Endpoint | On a bad entry |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `runs bulk-create` | `POST /v1/testhub/runs/bulk` | **per-element best effort** — HTTP 200, one row per case, failures marked `failure` with a message |
|
|
186
|
+
| `runs bulk-update` | `PATCH /v1/testhub/runs/bulk` | **atomic** — one unknown run id rejects the whole batch (`100016`) and nothing is applied |
|
|
187
|
+
| `runs bulk` | `POST …/plans/{plan}/runs/bulk` | counts only; a bogus `--add-case` is skipped silently (see rule 16) |
|
|
188
|
+
|
|
189
|
+
So: read the `STATE` column of `bulk-create`, and trust `bulk-update` to be all-or-nothing. Both cap
|
|
190
|
+
at **100** entries per call, checked locally. `runs bulk-update` carries no plan or library in its
|
|
191
|
+
URL, so one call can span plans; it cannot delete anything.
|
|
192
|
+
|
|
193
|
+
**Why one of the three is called just `bulk`, and that is deliberate.** `bulk-create` adds cases and
|
|
194
|
+
`bulk-update` records results, so each says what it does to a row. Plain `runs bulk` is the
|
|
195
|
+
**composite** call — `inserts` + `updates` + `deletes` in one request — and no create/update pair
|
|
196
|
+
names that, so it keeps the bare word. It is the **only** way to *delete* a run. This is the one
|
|
197
|
+
place in the CLI where `bulk` does not mean "create many": the pjm leaves that did mean that were
|
|
198
|
+
renamed to `project sprint bulk-create` / `project version bulk-create` in the G3 closeout precisely
|
|
199
|
+
so the word is unambiguous everywhere else. The `runs` group help says the same thing.
|
|
200
|
+
|
|
201
|
+
Every name-resolvable flag has an `--x-id` twin (`--status-id`, `--executor-id`, `--plan-id`,
|
|
202
|
+
`--library-id`, …) that is sent verbatim with no lookup; the two forms are mutually exclusive.
|
|
203
|
+
|
|
204
|
+
## 4c. Testhub rules that will bite you
|
|
205
|
+
|
|
206
|
+
These are on top of §4. Testhub is the same machinery again, with a different parent scope and a
|
|
207
|
+
sharper write path.
|
|
208
|
+
|
|
209
|
+
1. **Resolve the test library first, and never carry an id across libraries.** `state_id`,
|
|
210
|
+
`type_id`, `status_id`, `suite_id` and the plan list are all **library-scoped**: two libraries
|
|
211
|
+
never share a state, type or status id, even when the names are identical. `testhub cases list`,
|
|
212
|
+
`plans list`, `plans get`, `plans create`, `runs bulk` and the five library-scoped `meta` leaves
|
|
213
|
+
(`case-states`, `case-types`, `run-statuses`, `plan-types`, `suites`) all require
|
|
214
|
+
`--library <name|id>` and refuse to guess (exit 2). `cases get`, `cases update` and `runs update`
|
|
215
|
+
do not: they read the resource first and inherit its library. `runs list` needs one only to
|
|
216
|
+
resolve a `--plan` or `--status` **by name** — `--plan-id` / `--status-id` work without it.
|
|
217
|
+
`libraries create` is the one testhub command with no parent at all.
|
|
218
|
+
2. **`--json` search is the read path.** `testhub cases list` is `POST /v1/testhub/cases/search` and
|
|
219
|
+
`testhub runs list` is `POST /v1/testhub/runs/search`; the plain `GET` lists are never called
|
|
220
|
+
(unfiltered, `GET /v1/testhub/cases` scans every library you can see). One operator per field, no
|
|
221
|
+
`$and`/`$or`, filters AND-ed, and no sorting anywhere.
|
|
222
|
+
3. **`steps[]` replaces, it never merges — so `--step` is all-or-nothing.** A run's step array is
|
|
223
|
+
overwritten wholesale, and a step that arrives without its `step_id` is re-created with a fresh
|
|
224
|
+
id, orphaning its execution history. Re-emitting an untouched step is impossible: a run step
|
|
225
|
+
reports an English status **slug** while the write needs a status **id**, and nothing joins the
|
|
226
|
+
two except the localized name, which a tenant may have renamed. Rather than guess, the CLI
|
|
227
|
+
refuses a partial `--step` edit and lists every step id you must supply. Pass a status for
|
|
228
|
+
**every** step, or none at all. The same "replaces, never merges" applies to `--set`/`properties`
|
|
229
|
+
on a case.
|
|
230
|
+
4. **`testhub runs update` always sends `status_id`, and carries the executor over for you.**
|
|
231
|
+
`status_id` is required by the API even on PATCH, so the CLI reads the run first and re-sends the
|
|
232
|
+
run's current result when you do not name one — patching only a remark is safe here, and would
|
|
233
|
+
not be if you called the API directly. The run's own executor is re-sent the same way. When the
|
|
234
|
+
run has **no** executor and you name none, `executor_id` is omitted from the body and the CLI
|
|
235
|
+
warns on stderr that the run **stays unassigned** — an omitted `executor_id` is a verified no-op
|
|
236
|
+
on PATCH (2026-08-02), it neither clears the field nor reassigns the run. If the run has no
|
|
237
|
+
recorded result at all, the CLI asks for `--status` (exit 2) instead of sending a half-formed
|
|
238
|
+
body.
|
|
239
|
+
5. **`pingcode testhub runs bulk` is the only way to delete a run** — there is no `runs delete` and
|
|
240
|
+
no run DELETE endpoint. Each of `--add-case`, `--set-status` and `--remove-run` is capped at
|
|
241
|
+
**50** entries per call (checked locally, exit 2), and the response carries **counts only**,
|
|
242
|
+
never the ids of the runs it created: re-list the plan to see them.
|
|
243
|
+
It is no longer the only way to *add* one: `runs create` and `runs bulk-create` do that and
|
|
244
|
+
report what they created.
|
|
245
|
+
**The caps differ per endpoint, and only some are real.** Verified live 2026-08-04:
|
|
246
|
+
`cases/bulk` (both halves) and `runs/bulk` (both halves) are capped at **100** by the server
|
|
247
|
+
itself — a 101st entry answers `100039` before any field is validated, *including* on the two
|
|
248
|
+
halves whose docs declare no limit. The plan-scoped `…/plans/{plan}/runs/bulk`, by contrast,
|
|
249
|
+
enforces **nothing**: 1001 entries sail past the length gate and `updates[50]` is validated, so
|
|
250
|
+
the documented 50 is not a server rule. The CLI keeps 50 there anyway — an unenforced documented
|
|
251
|
+
limit on an insert/update/delete batch is exactly the shape that silently half-applies — and uses
|
|
252
|
+
the API's own 100 everywhere else, because refusing 51–100 entries the server accepts would be
|
|
253
|
+
the CLI inventing a restriction.
|
|
254
|
+
6. **`testhub runs list` cannot filter by library.** `library.id` is on the API's exclusion list for
|
|
255
|
+
run search, so scope runs with `--plan` instead. Passing `--library` without `--plan` resolves
|
|
256
|
+
names but does not narrow the result, and the CLI warns on stderr when you do it.
|
|
257
|
+
7. **`testhub meta important-levels` takes no `--library`.** Importance levels are organisation-wide
|
|
258
|
+
— the only testhub lookup with no per-library variant — so the flag is refused with exit 2 rather
|
|
259
|
+
than accepted and ignored. It is hidden from `--help`; this line is the documentation.
|
|
260
|
+
8. **The `pcp:read:testhub:configuration` trap.** `testhub meta case-states` and `testhub meta
|
|
261
|
+
run-statuses` need that scope while their sibling `testhub meta case-types` does not. A token
|
|
262
|
+
granted only `testcase` + `testplan` can list cases, plans and runs but gets a bare 403 from
|
|
263
|
+
those two — and since they are the only source of a `state_id` and a `status_id`, that token
|
|
264
|
+
**cannot write a run at all**. The CLI rewrites the 403 to say so.
|
|
265
|
+
9. **`cases create` takes the library as `test_library_id`, and `--state` is PATCH-only.** The
|
|
266
|
+
create body field is `test_library_id` (not `library_id`), which the CLI fills from `--library`;
|
|
267
|
+
a case is created in the library's initial state and can only be moved with
|
|
268
|
+
`pingcode testhub cases update <case> --state <s>`.
|
|
269
|
+
10. **`short_id` is read-only.** `testhub cases get`, `plans get` and the run read accept an id or a
|
|
270
|
+
`short_id`, but every write documents `id` only. `testhub cases update` and `testhub runs update`
|
|
271
|
+
therefore read the resource first and use the real id — which is also where they learn the
|
|
272
|
+
library, so a name lookup works without repeating `--library`.
|
|
273
|
+
11. **`--set` keys now have a discovery command, and it is a warning as much as a lookup.**
|
|
274
|
+
`pingcode testhub meta case-properties --library LIB` lists the fields effective in a library.
|
|
275
|
+
Verified live 2026-08-04, on this tenant **all 8 rows are built-in fields** whose key is the
|
|
276
|
+
field's own name (`state_id`, `description`, `steps`, `type`, `important_level`,
|
|
277
|
+
`maintenance_uid`, `precondition`, `test_type`), and pushing one of them through the properties
|
|
278
|
+
map is worse than useless:
|
|
279
|
+
- `--set important_level=…` → **HTTP 500**;
|
|
280
|
+
- `--set description=x` → **200, and it rewrites the top-level `description`**;
|
|
281
|
+
- a *custom* property that exists organisation-wide but is not in this library's scheme →
|
|
282
|
+
**HTTP 500** as well;
|
|
283
|
+
- a key that exists nowhere → 400, refused rather than dropped.
|
|
284
|
+
|
|
285
|
+
So set built-ins with their own flags (`--state`, `--type`, `--important-level`,
|
|
286
|
+
`--description`, `--precondition`) and reserve `--set` for a custom property that appears in
|
|
287
|
+
**that library's** list. Values for select-typed properties are option ids, not labels — the
|
|
288
|
+
same trap as ship. This is also why `pingcode resolve` has **no** `testhub-case-property` kind:
|
|
289
|
+
resolving a name would hand `--set` a key that edits a different field.
|
|
290
|
+
The library-scoped list cannot even tell you which rows are custom (it returns only
|
|
291
|
+
`{id, name, type, options}`); the organisation-level list can, one call away:
|
|
292
|
+
`pingcode api GET /v1/testhub/case_properties`.
|
|
293
|
+
Still missing: there is no `--maintenance` flag anywhere, so cases cannot be filtered by
|
|
294
|
+
maintainer (only *set*, through a bulk entry's `maintenance`).
|
|
295
|
+
12. **`--start` and `--end` on `plans create`: the end date is inclusive, and that is asymmetric.**
|
|
296
|
+
Both flags accept either form:
|
|
297
|
+
- `YYYY-MM-DD`, zero-padded. `--start 2026-08-10` becomes **00:00:00 local** on that date;
|
|
298
|
+
`--end 2026-08-31` becomes **23:59:59 local** on it.
|
|
299
|
+
- a **10-digit unix seconds** integer, passed through **verbatim** on both flags — no
|
|
300
|
+
end-of-day adjustment is applied to it. Use this when you want an exact instant.
|
|
301
|
+
|
|
302
|
+
The asymmetry is deliberate: a date range means the plan runs *through* the end date, and mapping
|
|
303
|
+
both ends to midnight would silently shorten every plan by a day — an error you would never see,
|
|
304
|
+
because the CLI echoes back exactly what it sent. Local time, not UTC, so that a `plans get`
|
|
305
|
+
agrees with the `plans create` that produced it.
|
|
306
|
+
|
|
307
|
+
Everything else is refused with exit 2 **before any request**: an unpadded `2026-8-1`, slashes
|
|
308
|
+
(`08/31/2026`), an ISO string with a time in it, a 13-digit **milliseconds** value, and an
|
|
309
|
+
impossible date such as `2026-02-30` (which JavaScript would otherwise roll silently into
|
|
310
|
+
March). `--end` earlier than `--start` is refused client-side too, and the message prints both
|
|
311
|
+
resolved unix values so you can see which end moved.
|
|
312
|
+
13. **`plans create` needs all five fields, and `--assignee` has no default.** `--library`,
|
|
313
|
+
`--name`, `--type`, `--start`, `--end` and `--assignee` are all required. There is deliberately
|
|
314
|
+
no "assign it to me": an enterprise (client-credentials) token acts as the **bot user**, so a
|
|
315
|
+
default would quietly make a bot the owner 负责人 of every plan the CLI creates, and nobody would
|
|
316
|
+
notice until they went looking for whom to ask. Name a real person —
|
|
317
|
+
`pingcode settings users --keywords <name> --json` is the candidate set.
|
|
318
|
+
14. **A plan type carries no `kind`, so the CLI cannot tell you which types need more.** Iteration
|
|
319
|
+
and release plan types additionally require `sprint_id` / `version_id` (and the `project_id`
|
|
320
|
+
they make mandatory), but the plan-type resource exposes only `id` / `name` / `url` / `library`
|
|
321
|
+
— there is **no `kind` discriminator**, and guessing from the localized name is not safe because
|
|
322
|
+
tenants rename them. `testhub meta plan-types` therefore lists names only, `plans create` sends
|
|
323
|
+
just the five fields, and if you pick a type that needs more, **the server's refusal is what you
|
|
324
|
+
see** — not a local warning. Pick the plain (普通) type unless you know the tenant's setup.
|
|
325
|
+
15. **A library can be created but never updated or deleted.** `--identifier` on
|
|
326
|
+
`testhub libraries create` must be **unique across the organisation** and the server enforces it
|
|
327
|
+
(a duplicate is rejected server-side; the CLI does not pre-check, because a probe would race).
|
|
328
|
+
Testhub publishes **no library DELETE**, so a library created here is permanent: get the name
|
|
329
|
+
and identifier right the first time, and mark throwaway ones (for example `[CLI smoke] …`)
|
|
330
|
+
*before* creating them. The CLI says so on stderr after each create.
|
|
331
|
+
A library **PATCH does exist** upstream, though — this was previously documented the other way
|
|
332
|
+
round and is corrected here (verified live 2026-08-04). It is not wrapped as a leaf, so rename
|
|
333
|
+
or re-describe through the generic layer:
|
|
334
|
+
`pingcode api PATCH /v1/testhub/libraries/<id> --set name="…"`. Note it cannot *clear* a field:
|
|
335
|
+
`description: ""` answers 200 and keeps the old value.
|
|
336
|
+
16. **`runs bulk --add-case` ignores a case id that does not exist — silently, at exit 0.** Verified
|
|
337
|
+
live 2026-08-02: a bogus `--add-case` id returns `{"inserts":0,"updates":0,"deletes":0}` and
|
|
338
|
+
succeeds. There is no error and no per-entry report, because the endpoint answers with counts
|
|
339
|
+
only. **Read the counts**, and if they do not match what you asked for, re-list the plan with
|
|
340
|
+
`pingcode testhub runs list --plan <plan> --json` to see what actually landed. A bogus id in
|
|
341
|
+
`--remove-run` does fail loudly (code `100619`, exit 7), so the leniency is specific to inserts.
|
|
342
|
+
17. **`testhub cases delete` takes the case's runs with it.** Verified live 2026-08-04: deleting a
|
|
343
|
+
case with a run in a plan removed the run from the plan, and the run id stopped resolving
|
|
344
|
+
(`100603`). The CLI counts the runs before the `--yes` gate and names the number, so the
|
|
345
|
+
confirmation tells you the blast radius rather than just the case title. The case itself is only
|
|
346
|
+
**soft**-deleted — `cases list --include-deleted` still finds it with `is_deleted: 1` — but this
|
|
347
|
+
API publishes no undelete, so treat it as one-way. It is also the only DELETE in the module, and
|
|
348
|
+
the case path is **id-only**: the CLI resolves a `short_id` before sending.
|
|
349
|
+
18. **Two "history" reads, two different questions.** `runs history list <run>` is every result ever
|
|
350
|
+
recorded on one run, oldest first — the audit trail a test report needs. `cases history list
|
|
351
|
+
<case>` is one row per **run** of that case, carrying only that run's *latest* result, so its
|
|
352
|
+
row count is the number of runs and not the number of attempts. Each of its rows names the run
|
|
353
|
+
it came from, which is how you drill down. There is no per-case history *detail* path: a row is
|
|
354
|
+
a run-history record, so read it with `runs history get <run> <id>`.
|
|
355
|
+
Both paths are **id-only** (a `short_id` answers 404), so the CLI resolves the reference first.
|
|
356
|
+
A history id that belongs to a different run is reported as a **mismatch** (`100643`, exit 7),
|
|
357
|
+
not as missing; a genuinely unknown history id is exit 5.
|
|
358
|
+
Contrary to the vendor docs, the two endpoints return the **same** item shape — the case side
|
|
359
|
+
carries `executed_status` and `remark` too (verified live 2026-08-04).
|
|
360
|
+
19. **A bulk result is auditable; a pjm bulk update is not.** Every entry `runs bulk-update` applies
|
|
361
|
+
appends a row to that run's history, so `runs history list` shows batch-recorded results exactly
|
|
362
|
+
like hand-recorded ones. Do not generalise this from module to module: `project work-item
|
|
363
|
+
bulk-update` appears in no feed at all.
|
|
364
|
+
20. **`runs create` refuses a case the plan already contains** (`100605`, exit 7) rather than
|
|
365
|
+
deduplicating, and an omitted `--executor` leaves the run **unassigned** — it is not defaulted
|
|
366
|
+
to the creator. When you are adding several cases and some may already be there, prefer
|
|
367
|
+
`runs bulk-create`: it lands the new ones and reports the duplicates instead of failing the call.
|
|
368
|
+
|
|
369
|
+
## 5. Deliberately left to `pingcode api`
|
|
370
|
+
|
|
371
|
+
Reachable, just not as named commands — say that rather than reporting a limitation:
|
|
372
|
+
|
|
373
|
+
| Left out | Why | Reach it with |
|
|
374
|
+
|---|---|---|
|
|
375
|
+
| library **members** (4 endpoints) | no command needs library membership: `--executor` and `--assignee` resolve against the organisation directory | `pingcode api GET /v1/testhub/libraries/<id>/members` |
|
|
376
|
+
| library **update** | exists upstream and cannot clear a field; renaming a library is rare enough not to earn a leaf (rule 15) | `pingcode api PATCH /v1/testhub/libraries/<id> --set name="…"` |
|
|
377
|
+
| case-module (**suite**) writes | the suite tree is read to resolve `--suite`; editing it is a configuration act | `pingcode api list --module testhub --search suites` |
|
|
378
|
+
| plan **delete** | no test plan DELETE exists upstream at all | — |
|
|
379
|
+
| every configuration **write** (case states, types, property schemes) | tenant configuration, and a wrong write is felt by everyone in the library | `pingcode api list --module testhub --search plans` |
|
|
380
|
+
| `PUT /v1/testhub/runs/{run_id}` | full replacement, and documented to blank the executor when the field is omitted — `runs update` covers the same ground without that risk | `pingcode api PUT /v1/testhub/runs/<id>` |
|
|
381
|
+
| `GET /v1/testhub/cases` and `GET /v1/testhub/runs` (the simple lists) | unfiltered they scan every library the token can see; `POST …/search` is the only sane read path, and it is what `cases list` / `runs list` use | `pingcode api GET /v1/testhub/cases` |
|
|
382
|
+
| `GET /v1/testhub/plan_states/{state_id}` | the *list* (`meta plan-states`) is wired and is the only thing a plan write needs | `pingcode api GET /v1/testhub/plan_states/<id>` |
|
|
383
|
+
|
|
384
|
+
Library and plan **creation** *are* covered (`testhub libraries create`, `testhub plans create`), so
|
|
385
|
+
the CLI can bootstrap its own fixtures — but read rules 13–15 first: only the plain (普通) plan type is
|
|
386
|
+
reachable without a sprint or release, and a library cannot be deleted afterwards.
|