testeiya 0.6.0 → 0.8.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/PIPELINES.md +332 -0
- package/README.md +114 -1
- package/dist/prompt/testomat.io.js +2 -2
- package/dist/prompt/testomat.io.js.map +1 -1
- package/dist/src/args.js +104 -1
- package/dist/src/args.js.map +1 -1
- package/dist/src/checkpoint.js +0 -21
- package/dist/src/checkpoint.js.map +1 -1
- package/dist/src/cli.js +42 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/git.js +68 -0
- package/dist/src/git.js.map +1 -0
- package/dist/src/mcp.js +4 -20
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/output.js +16 -3
- package/dist/src/output.js.map +1 -1
- package/dist/src/pipeline/command-stage.js +27 -0
- package/dist/src/pipeline/command-stage.js.map +1 -0
- package/dist/src/pipeline/diagram.js +103 -0
- package/dist/src/pipeline/diagram.js.map +1 -0
- package/dist/src/pipeline/gate.js +59 -0
- package/dist/src/pipeline/gate.js.map +1 -0
- package/dist/src/pipeline/launcher.js +74 -0
- package/dist/src/pipeline/launcher.js.map +1 -0
- package/dist/src/pipeline/log.js +70 -0
- package/dist/src/pipeline/log.js.map +1 -0
- package/dist/src/pipeline/pipeline-file.js +242 -0
- package/dist/src/pipeline/pipeline-file.js.map +1 -0
- package/dist/src/pipeline/pipeline.js +415 -0
- package/dist/src/pipeline/pipeline.js.map +1 -0
- package/dist/src/pipeline/report.js +61 -0
- package/dist/src/pipeline/report.js.map +1 -0
- package/dist/src/pipeline/spec.js +2 -0
- package/dist/src/pipeline/spec.js.map +1 -0
- package/dist/src/pipeline/stage.js +150 -0
- package/dist/src/pipeline/stage.js.map +1 -0
- package/dist/src/pipeline/state.js +70 -0
- package/dist/src/pipeline/state.js.map +1 -0
- package/dist/src/pipeline/task-stage.js +84 -0
- package/dist/src/pipeline/task-stage.js.map +1 -0
- package/dist/src/pipeline/variables.js +41 -0
- package/dist/src/pipeline/variables.js.map +1 -0
- package/dist/src/run.js +6 -3
- package/dist/src/run.js.map +1 -1
- package/mcp-tools.json +1092 -2110
- package/package.json +7 -5
- package/pipelines/api-testing.yml +39 -0
- package/pipelines/issue-test-cases.yml +41 -0
- package/pipelines/pr-review.yml +43 -0
- package/pipelines/regression.yml +39 -0
- package/pipelines/web-testing.yml +50 -0
- package/prompt/testomat.io.ts +5 -5
- package/skills/skills.lock.json +2 -2
- package/skills/testomatio/test-management/detect-useless-tests/SKILL.md +1 -1
- package/skills/testomatio/test-management/improve-test-cases/SKILL.md +1 -1
- package/skills/testomatio/test-management/qa-test-code-coverage/SKILL.md +2 -2
- package/skills/testomatio/test-management/qa-write-test-cases/SKILL.md +3 -3
- package/skills/testomatio/test-management/scan-automation-project/SKILL.md +8 -8
- package/skills/testomatio/test-management/sync-test-cases-with-tms/SKILL.md +9 -9
- package/skills/testomatio/test-management/sync-test-cases-with-tms/references/TESTOMATIO_CLI.md +4 -4
package/PIPELINES.md
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# Pipeline file format
|
|
2
|
+
|
|
3
|
+
A pipeline is a QA flow in one YAML file: agent tasks and shell commands, run in
|
|
4
|
+
order, passing files through one folder. One command runs it:
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
testeiya pipeline <name|path.yml> [options]
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
This page is the reference for the file. The [README](README.md#pipelines) shows
|
|
11
|
+
how to use pipelines in CI.
|
|
12
|
+
|
|
13
|
+
## A complete example
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
name: regression
|
|
17
|
+
description: Run the tests a change can break, then explain the failures.
|
|
18
|
+
|
|
19
|
+
summary: |
|
|
20
|
+
Lead with failures this change caused. Flaky tests go last.
|
|
21
|
+
|
|
22
|
+
env:
|
|
23
|
+
BASE_BRANCH: ${BASE_BRANCH:-origin/main}
|
|
24
|
+
|
|
25
|
+
gates:
|
|
26
|
+
coverage:
|
|
27
|
+
file: [coverage.tests.yml]
|
|
28
|
+
message: No coverage.tests.yml. Create one with /qa-test-code-coverage.
|
|
29
|
+
testomatio:
|
|
30
|
+
env: [TESTOMATIO]
|
|
31
|
+
message: No Testomat.io project. Set TESTOMATIO to the project's API key.
|
|
32
|
+
local:
|
|
33
|
+
run: test -z "$REMOTE"
|
|
34
|
+
message: Tests run on the Testomat.io CI profile. No local log to read.
|
|
35
|
+
test-command:
|
|
36
|
+
env: [TEST_COMMAND]
|
|
37
|
+
message: No test command. Set TEST_COMMAND, e.g. "npx playwright test".
|
|
38
|
+
|
|
39
|
+
when: [coverage]
|
|
40
|
+
|
|
41
|
+
stages:
|
|
42
|
+
- id: tests
|
|
43
|
+
when: [testomatio]
|
|
44
|
+
run: |
|
|
45
|
+
npx -y @testomatio/reporter run --kind detect --warn \
|
|
46
|
+
--filter "coverage:file=coverage.tests.yml,diff=$BASE_BRANCH" \
|
|
47
|
+
${REMOTE:+--remote "$REMOTE"} ${TEST_COMMAND:+"$TEST_COMMAND"}
|
|
48
|
+
timeout: 30m
|
|
49
|
+
|
|
50
|
+
- id: failures
|
|
51
|
+
when: [local, test-command]
|
|
52
|
+
session: triage
|
|
53
|
+
task: |
|
|
54
|
+
Read the test run in tests.log. For each failure, say if this change caused it,
|
|
55
|
+
if it is flaky, or if the test is out of date. /debug-fix-failed-flaky-autotests
|
|
56
|
+
Fail the run if this change caused a failure.
|
|
57
|
+
inputs: [tests.log]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Top level
|
|
61
|
+
|
|
62
|
+
| Key | Required | Meaning |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `name` | no | Shown in logs and `pipeline.json`. Default: the file name |
|
|
65
|
+
| `description` | no | One line, shown by `testeiya pipeline` |
|
|
66
|
+
| `summary` | no | Extra instructions for the final summary |
|
|
67
|
+
| `env` | no | Variables for every stage. Values expand `${NAME}` |
|
|
68
|
+
| `gates` | no | Named checks, defined once. See [Gates](#gates) |
|
|
69
|
+
| `when` | no | Gates for the whole run. One fails: nothing runs, exit 0 |
|
|
70
|
+
| `stages` | yes | The stages, run in order. At least one |
|
|
71
|
+
|
|
72
|
+
An unknown key is an error, found before anything runs.
|
|
73
|
+
|
|
74
|
+
## Stages
|
|
75
|
+
|
|
76
|
+
| Key | Meaning |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `id` | Required. Lowercase words joined by dashes. Unique. Names the stage's files. `pipeline` and `summary` are reserved |
|
|
79
|
+
| `task` | An agent stage: the task text. `/skill` names expand |
|
|
80
|
+
| `run` | A command stage: a shell command, run with `sh` in the checkout |
|
|
81
|
+
| `when` | Gates this stage needs. One fails: the stage is skipped with its message |
|
|
82
|
+
| `session` | A named, persistent conversation. See [Sessions](#sessions) |
|
|
83
|
+
| `inputs` | Files the stage reads, from the pipeline folder |
|
|
84
|
+
| `outputs` | Files the stage must leave in the pipeline folder |
|
|
85
|
+
| `each_line` | Run once per non-empty line of this file. See [Loops](#loops) |
|
|
86
|
+
| `each_file` | Run once per file matching this mask |
|
|
87
|
+
| `stopOn` | `fail`, `gate`, or both as a list. See [Stopping](#stopping) |
|
|
88
|
+
| `timeout` | `90s`, `30m`, `1h`, or seconds. Per item in a loop. Default: none |
|
|
89
|
+
|
|
90
|
+
- A stage has exactly one of `task` or `run`.
|
|
91
|
+
- A stage has at most one of `each_line` or `each_file`.
|
|
92
|
+
- A list key also takes a single value: `when: staging` is `when: [staging]`.
|
|
93
|
+
- Explorbot, the reporter and test runners are plain `run` commands. They have
|
|
94
|
+
no stage type of their own.
|
|
95
|
+
|
|
96
|
+
## Gates
|
|
97
|
+
|
|
98
|
+
A gate is a named check with a message. It is defined once under `gates:` and
|
|
99
|
+
named in `when:`, at the top level or in a stage.
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
gates:
|
|
103
|
+
staging:
|
|
104
|
+
env: [EXPLORBOT_URL]
|
|
105
|
+
message: No staging URL. Set EXPLORBOT_URL to test in a browser.
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
| Check | Passes when |
|
|
109
|
+
|---|---|
|
|
110
|
+
| `env: [A, B]` | every variable is set and not empty |
|
|
111
|
+
| `file: [path]` | every path exists, relative to the checkout |
|
|
112
|
+
| `run: <command>` | the command exits 0 within 30 seconds. Its output is hidden |
|
|
113
|
+
| `changes: {…}` | the changed files match, see [Changes](#changes) |
|
|
114
|
+
|
|
115
|
+
- A gate needs a `message` and at least one check. All its checks must pass.
|
|
116
|
+
- Every gate is checked once, before the first stage. `--dry-run` prints them all.
|
|
117
|
+
- A name in `when` that `gates` does not define is an error.
|
|
118
|
+
- `ai` is built in: a model and a provider key resolve. The summary needs it, so
|
|
119
|
+
a pipeline without one exits 2 before anything runs.
|
|
120
|
+
|
|
121
|
+
### Gates from the CI job
|
|
122
|
+
|
|
123
|
+
The job can add a gate without editing the file:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
testeiya pipeline regression --gate 'pr:test -n "$PR_NUMBER"'
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- `--gate NAME:COMMAND` splits on the first colon. It is a `run` gate added to
|
|
130
|
+
the top-level `when`.
|
|
131
|
+
- When it fails the whole run is skipped and exits 0.
|
|
132
|
+
- It is checked before `--output`, so `-o "gh:pr#$PR_NUMBER"` is fine when the
|
|
133
|
+
number only exists when the gate passes.
|
|
134
|
+
- A name the file already defines is an error.
|
|
135
|
+
|
|
136
|
+
### Checking a pipeline
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
testeiya pipeline-check regression --set TEST_COMMAND="npx playwright test"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- Validates the file the way a run does, then checks every gate it declares,
|
|
143
|
+
used or not, plus `ai` and any `--gate`.
|
|
144
|
+
- Prints each gate as passed or not, with its `message` when it does not pass.
|
|
145
|
+
- Runs no stage and spends no tokens. Takes `--set`, `--gate`, `--dir`, `--model`
|
|
146
|
+
and `--json`.
|
|
147
|
+
- Exit code: `0` every gate passes, `1` one does not, `2` the file is invalid.
|
|
148
|
+
- `--diagram` also writes `<name>.diagram.md` to the current folder: a Mermaid
|
|
149
|
+
chart of the stages in order, the files they pass on (dotted), and the gates
|
|
150
|
+
each one needs, green when it passes and red when it does not.
|
|
151
|
+
|
|
152
|
+
### Changes
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
gates:
|
|
156
|
+
api-changed:
|
|
157
|
+
changes:
|
|
158
|
+
paths: ["src/api/**", "openapi.yaml"] # at least one changed file matches
|
|
159
|
+
ignore: ["**/*.md", "**/*.lock"] # these never count
|
|
160
|
+
min_files: 3 # at least this many count
|
|
161
|
+
message: No API code changed.
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Every key is optional. All given must hold, and at least one file must count.
|
|
165
|
+
|
|
166
|
+
- First round: the diff from the merge base with the target branch. The branch
|
|
167
|
+
comes from `GITHUB_BASE_REF`, `CI_MERGE_REQUEST_TARGET_BRANCH_NAME`,
|
|
168
|
+
`BITBUCKET_PR_DESTINATION_BRANCH`, then `BASE_BRANCH`, then `origin/main`.
|
|
169
|
+
- Later rounds: the diff since the commit the last round ran on.
|
|
170
|
+
- Git cannot say (a shallow clone): a pipeline that uses `changes` exits 2.
|
|
171
|
+
Check out with `fetch-depth: 0`.
|
|
172
|
+
- The list is written to `changes.txt` in the pipeline folder.
|
|
173
|
+
|
|
174
|
+
## Variables
|
|
175
|
+
|
|
176
|
+
- `${NAME}` and `${NAME:-default}` expand in `task`, `env`, `outputs` and gate
|
|
177
|
+
values. Unset is empty. `run` goes to the shell, which uses the same rules and
|
|
178
|
+
more, like `${NAME:+word}`.
|
|
179
|
+
- `--set KEY=VALUE` and the job's own environment reach every stage. `--set` wins
|
|
180
|
+
over `env:`.
|
|
181
|
+
- The runner sets:
|
|
182
|
+
|
|
183
|
+
| Variable | Value |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `PIPELINE_DIR` | absolute path of the pipeline folder |
|
|
186
|
+
| `PIPELINE_NAME` | the pipeline's name |
|
|
187
|
+
| `PIPELINE_ROUND` | 1, 2, … |
|
|
188
|
+
| `ITEM` | in a loop: the line, or the file's path |
|
|
189
|
+
| `ITEM_NAME` | in an `each_file` loop: the file name without its extension |
|
|
190
|
+
| `PIPELINE_RESUME` | `1` when a command stage is retried |
|
|
191
|
+
|
|
192
|
+
`TESTEIYA_FOLLOW_UP`, `TESTEIYA_THREAD` and `TESTEIYA_SESSION_FILE` are removed:
|
|
193
|
+
they belong to a single task.
|
|
194
|
+
|
|
195
|
+
## Files
|
|
196
|
+
|
|
197
|
+
Every stage runs in the checkout. Paths in `inputs`, `outputs` and loops resolve
|
|
198
|
+
against the pipeline folder, `$PIPELINE_DIR`. A path ending in `/` is a folder.
|
|
199
|
+
|
|
200
|
+
Each stage leaves its own files:
|
|
201
|
+
|
|
202
|
+
| File | Stage | Content |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| `<id>.md` | agent | the stage's report |
|
|
205
|
+
| `<id>.json` | agent | status, reason, tokens, session id |
|
|
206
|
+
| `<id>.log` | command | stdout and stderr |
|
|
207
|
+
|
|
208
|
+
- A later stage reads them by name: `inputs: [review.md]`.
|
|
209
|
+
- An `outputs` entry may not reuse one of these names.
|
|
210
|
+
- Every input must come from an earlier stage, from the runner (`changes.txt`),
|
|
211
|
+
or exist in the folder at start.
|
|
212
|
+
- After a stage, every `outputs` entry must exist, or the stage is `broken`.
|
|
213
|
+
- An agent stage's task gets a block that names the folder, the files to read,
|
|
214
|
+
the files to write, and inputs a skipped stage did not produce.
|
|
215
|
+
- A command stage whose inputs all come from skipped stages is skipped too. An
|
|
216
|
+
agent stage still runs and is told what is missing.
|
|
217
|
+
|
|
218
|
+
## Loops
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
- id: explore
|
|
222
|
+
each_line: endpoints.txt
|
|
223
|
+
run: npx -y explorbot api explore "$ITEM"
|
|
224
|
+
|
|
225
|
+
- id: sync
|
|
226
|
+
each_file: test-cases/*.md
|
|
227
|
+
task: Sync ${ITEM} to Testomat.io. /sync-test-cases-with-tms
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- `each_line` runs once per non-empty line. `each_file` runs once per matching
|
|
231
|
+
file, sorted, with `*` and `**`.
|
|
232
|
+
- Both resolve against the pipeline folder and are matched when the stage
|
|
233
|
+
starts, so earlier stages can write them.
|
|
234
|
+
- Nothing to loop over: the stage is skipped.
|
|
235
|
+
- Items run one after another. A failed item does not stop the others. The stage
|
|
236
|
+
takes the worst item's status.
|
|
237
|
+
- Each item writes `<id>.1.md`, `<id>.2.md`, … or `<id>.1.log`, …, joined into
|
|
238
|
+
`<id>.md` or `<id>.log` at the end.
|
|
239
|
+
- An agent loop with a `session` runs every item in that conversation.
|
|
240
|
+
|
|
241
|
+
## Sessions
|
|
242
|
+
|
|
243
|
+
- `session: <name>` keeps the conversation in `.sessions/<name>.jsonl` in the
|
|
244
|
+
pipeline folder.
|
|
245
|
+
- Stages with the same name share one conversation. Different names never see
|
|
246
|
+
each other's reasoning, only each other's files.
|
|
247
|
+
- No `session`: a fresh conversation, saved nowhere.
|
|
248
|
+
- A session lasts as long as the folder. Kept between rounds, a stage on round 2
|
|
249
|
+
continues its round 1 work.
|
|
250
|
+
|
|
251
|
+
## The summary
|
|
252
|
+
|
|
253
|
+
A pipeline with more than one stage ends with a summary. It is not written in
|
|
254
|
+
the file: the runner adds it.
|
|
255
|
+
|
|
256
|
+
- It runs the `pipeline-report` skill with the `summary:` text, reads every
|
|
257
|
+
stage's files, and writes `summary.md`.
|
|
258
|
+
- Its report is the only thing sent to `--output`. Every other stage runs with
|
|
259
|
+
`--no-thread` and posts nothing.
|
|
260
|
+
- It explains the pipeline's verdict and never changes it.
|
|
261
|
+
- It runs after the last stage, and after a stage that stopped the pipeline. Not
|
|
262
|
+
after Ctrl-C, and not when the run was skipped.
|
|
263
|
+
- A one-stage pipeline has no summary. That stage's report is the report.
|
|
264
|
+
|
|
265
|
+
## Stopping
|
|
266
|
+
|
|
267
|
+
| Status | Agent stage | Command stage |
|
|
268
|
+
|---|---|---|
|
|
269
|
+
| `pass` | verdict pass | exit 0 |
|
|
270
|
+
| `fail` | verdict fail | non-zero exit |
|
|
271
|
+
| `broken` | crashed, or an `outputs` file is missing | an `outputs` file is missing |
|
|
272
|
+
| `timeout` | ran past `timeout` | same |
|
|
273
|
+
| `skipped` | a gate in `when` failed, or nothing to loop over | same, or its inputs were skipped |
|
|
274
|
+
| `not-run` | an earlier stage stopped the pipeline | same |
|
|
275
|
+
|
|
276
|
+
- `broken` and `timeout` always stop the pipeline.
|
|
277
|
+
- `stopOn: fail` stops it when this stage fails.
|
|
278
|
+
- `stopOn: gate` stops it when this stage is skipped by a gate.
|
|
279
|
+
|
|
280
|
+
Exit code: `0` pass or skipped, `1` any fail, broken or timeout, `2` bad usage
|
|
281
|
+
or no AI provider, `130` interrupted. `--exit-zero` forgives `fail` only.
|
|
282
|
+
|
|
283
|
+
## Rounds
|
|
284
|
+
|
|
285
|
+
Without `--dir` a pipeline runs once in a temp folder. Keep `--dir` between
|
|
286
|
+
runs, as a CI cache per pull request, and each run is a round:
|
|
287
|
+
|
|
288
|
+
| Folder holds | Same commit | New commit |
|
|
289
|
+
|---|---|---|
|
|
290
|
+
| nothing | round 1 | round 1 |
|
|
291
|
+
| a stopped round | retry from the stage that stopped | new round |
|
|
292
|
+
| a finished round | the same round again | new round |
|
|
293
|
+
|
|
294
|
+
- A retry keeps stages that passed or failed and uses the folder's copy of the
|
|
295
|
+
file, so the stages cannot change half-way.
|
|
296
|
+
- A new round clears the last round's files, keeps `.sessions/`, and counts
|
|
297
|
+
`changes` from the last round's commit. Stages with a session are told what
|
|
298
|
+
changed. The summary updates its comment.
|
|
299
|
+
- `--dir` may not be the checkout or a folder above it.
|
|
300
|
+
|
|
301
|
+
`pipeline.json` in the folder holds the round, each stage's status and the
|
|
302
|
+
round history. `--json` prints it.
|
|
303
|
+
|
|
304
|
+
## In CI
|
|
305
|
+
|
|
306
|
+
Find what only the job knows in a step of its own, then run the pipeline in one
|
|
307
|
+
command:
|
|
308
|
+
|
|
309
|
+
```yaml
|
|
310
|
+
- name: Find pull request
|
|
311
|
+
env:
|
|
312
|
+
GH_TOKEN: ${{ github.token }}
|
|
313
|
+
run: |
|
|
314
|
+
gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" \
|
|
315
|
+
--jq '.[0] // empty | "PR_NUMBER=\(.number)\nPR_TITLE=\(.title)\nPR_URL=\(.html_url)"' \
|
|
316
|
+
>> "$GITHUB_ENV"
|
|
317
|
+
|
|
318
|
+
- name: Run regression
|
|
319
|
+
env:
|
|
320
|
+
TESTOMATIO: ${{ secrets.TESTOMATIO }}
|
|
321
|
+
GH_TOKEN: ${{ github.token }}
|
|
322
|
+
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
|
323
|
+
TESTEIYA_MODEL: openrouter/anthropic/claude-haiku-4.5
|
|
324
|
+
run: |
|
|
325
|
+
npx -y testeiya@latest pipeline regression \
|
|
326
|
+
--gate 'pr:test -n "$PR_NUMBER"' \
|
|
327
|
+
--set REMOTE=github \
|
|
328
|
+
--set BASE_BRANCH=${{ github.event.before }} \
|
|
329
|
+
--set "TESTOMATIO_TITLE=PR #$PR_NUMBER $PR_TITLE" \
|
|
330
|
+
--set "TESTOMATIO_DESCRIPTION=$PR_URL" \
|
|
331
|
+
-o "gh:pr#$PR_NUMBER"
|
|
332
|
+
```
|
package/README.md
CHANGED
|
@@ -81,6 +81,7 @@ testeiya models anthropic # list what your key can reach
|
|
|
81
81
|
```bash
|
|
82
82
|
testeiya task "<task>" # run one task, deliver a report, exit
|
|
83
83
|
testeiya ask "<question>" # answer a question, no report
|
|
84
|
+
testeiya pipeline <name> # run several stages in a row, see Pipelines
|
|
84
85
|
testeiya skills # list the skills bundled with this package
|
|
85
86
|
testeiya sessions # list saved sessions for this folder
|
|
86
87
|
```
|
|
@@ -187,6 +188,117 @@ TESTOMATIO=tstmt_xxx testeiya task \
|
|
|
187
188
|
--output audits/$(date +%F).md
|
|
188
189
|
```
|
|
189
190
|
|
|
191
|
+
## Pipelines
|
|
192
|
+
|
|
193
|
+
Some QA flows take more than one run, and not every step is the agent: review a
|
|
194
|
+
pull request, run Explorbot against staging, then explain what broke. A
|
|
195
|
+
pipeline is that flow in one YAML file, run by one command.
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
testeiya pipeline # list the bundled pipelines
|
|
199
|
+
testeiya pipeline web-testing -o gh:pr-comment
|
|
200
|
+
testeiya pipeline .github/testeiya/my-flow.yml --dry-run
|
|
201
|
+
testeiya pipeline-check .github/testeiya/my-flow.yml # check the file and every gate
|
|
202
|
+
testeiya pipeline-check regression --diagram # also draw it as a Mermaid chart
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
| Pipeline | What it does |
|
|
206
|
+
|---|---|
|
|
207
|
+
| `pr-review` | requirements, QA review, test cases, sync to Testomat.io |
|
|
208
|
+
| `issue-test-cases` | an issue or epic, with its sub-issues, becomes test cases; set `ISSUE_URL` |
|
|
209
|
+
| `web-testing` | review, an Explorbot plan, `explorbot test`, what broke |
|
|
210
|
+
| `api-testing` | review lists changed endpoints, Explorbot tests each one |
|
|
211
|
+
| `regression` | runs the tests `coverage.tests.yml` maps to the change, explains failures |
|
|
212
|
+
|
|
213
|
+
To change one, copy it from `pipelines/` into your repo, edit it, and pass the
|
|
214
|
+
path.
|
|
215
|
+
|
|
216
|
+
### The file
|
|
217
|
+
|
|
218
|
+
```yaml
|
|
219
|
+
name: web-testing
|
|
220
|
+
description: Review a pull request, test it in a browser, explain what broke.
|
|
221
|
+
|
|
222
|
+
gates:
|
|
223
|
+
staging:
|
|
224
|
+
env: [EXPLORBOT_URL]
|
|
225
|
+
message: No staging URL. Set EXPLORBOT_URL to test in a browser.
|
|
226
|
+
browser:
|
|
227
|
+
run: ls "${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}" | grep -q chromium
|
|
228
|
+
message: No browser. Run npx playwright install --with-deps chromium.
|
|
229
|
+
|
|
230
|
+
env:
|
|
231
|
+
EXPLORBOT_OUTPUT: ${PIPELINE_DIR}/explorbot
|
|
232
|
+
|
|
233
|
+
stages:
|
|
234
|
+
- id: review
|
|
235
|
+
session: qa
|
|
236
|
+
task: Review this pull request. Write browser scenarios to plan.md. /explorbot-plan
|
|
237
|
+
outputs: [plan.md]
|
|
238
|
+
- id: explore
|
|
239
|
+
when: [staging, browser]
|
|
240
|
+
run: npx -y explorbot test "$PIPELINE_DIR/plan.md" '*'
|
|
241
|
+
timeout: 30m
|
|
242
|
+
- id: results
|
|
243
|
+
session: qa
|
|
244
|
+
task: Read the Explorbot report under explorbot/ and say what broke.
|
|
245
|
+
inputs: [explorbot/]
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
- A stage is a `task` for the agent or a `run` shell command.
|
|
249
|
+
- Stages pass files through one folder, `$PIPELINE_DIR`. An agent stage writes
|
|
250
|
+
`<id>.md` and `<id>.json`, a command writes `<id>.log`. `inputs` and
|
|
251
|
+
`outputs` name the rest.
|
|
252
|
+
- `gates` are named checks: `env`, `file`, `run`, or `changes` (`paths`,
|
|
253
|
+
`ignore`, `min_files`). `when:` names the gates a stage needs. One fails and
|
|
254
|
+
the stage is skipped with its message. In the top-level `when:` it skips the
|
|
255
|
+
whole run.
|
|
256
|
+
- `session: <name>` keeps a conversation. Stages with the same name share it.
|
|
257
|
+
No `session` is a fresh one.
|
|
258
|
+
- `each_line: file.txt` and `each_file: cases/*.md` run a stage once per item,
|
|
259
|
+
as `$ITEM`.
|
|
260
|
+
- `stopOn: fail` stops the pipeline when the stage fails, `stopOn: gate` when it
|
|
261
|
+
was skipped. A broken or timed-out stage always stops it.
|
|
262
|
+
|
|
263
|
+
The full format is in [PIPELINES.md](PIPELINES.md).
|
|
264
|
+
|
|
265
|
+
A pipeline with more than one stage ends with a summary written by the
|
|
266
|
+
`pipeline-report` skill. It is the only thing posted to `--output`. Every other
|
|
267
|
+
stage runs with `--no-thread`, so nothing else touches the pull request.
|
|
268
|
+
|
|
269
|
+
### Rounds
|
|
270
|
+
|
|
271
|
+
Keep `--dir` between runs, as a CI cache per pull request, and each run is a
|
|
272
|
+
round. A re-run on the same commit retries from the stage that stopped. A new
|
|
273
|
+
commit starts a new round: `changes` count from the last round's commit, stages
|
|
274
|
+
with a session are told what changed, and the summary updates its comment.
|
|
275
|
+
|
|
276
|
+
```yaml
|
|
277
|
+
- uses: actions/cache/restore@v4
|
|
278
|
+
with:
|
|
279
|
+
path: ${{ runner.temp }}/qa
|
|
280
|
+
key: qa-pr-${{ github.event.number }}-${{ github.run_id }}
|
|
281
|
+
restore-keys: qa-pr-${{ github.event.number }}-
|
|
282
|
+
- run: npx testeiya@latest pipeline pr-review --dir ${{ runner.temp }}/qa -o gh:pr-comment --exit-zero
|
|
283
|
+
- uses: actions/cache/save@v4
|
|
284
|
+
if: always()
|
|
285
|
+
with:
|
|
286
|
+
path: ${{ runner.temp }}/qa
|
|
287
|
+
key: qa-pr-${{ github.event.number }}-${{ github.run_id }}-${{ github.run_attempt }}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Stages ignore `TESTEIYA_FOLLOW_UP`, `TESTEIYA_THREAD` and `TESTEIYA_SESSION_FILE`:
|
|
291
|
+
those belong to a single task. Use `--thread` on the pipeline instead.
|
|
292
|
+
|
|
293
|
+
`--set KEY=VALUE` sets a variable for every stage. `--gate NAME:COMMAND` adds a
|
|
294
|
+
check from the CI job: when the command fails, the whole run is skipped and exits
|
|
295
|
+
0, before any output is checked. Use it for what only the job knows, like
|
|
296
|
+
whether this push belongs to a pull request. `--dry-run` prints every
|
|
297
|
+
gate and stage and spends nothing. `testeiya pipeline-check` checks the file and
|
|
298
|
+
every gate it declares, used or not, and prints the message of each one that
|
|
299
|
+
does not pass. `--diagram` also writes the pipeline as a Mermaid chart.
|
|
300
|
+
`pipeline.json` in the folder holds each stage's status; `--json` prints it.
|
|
301
|
+
|
|
190
302
|
## CI setup
|
|
191
303
|
|
|
192
304
|
Testeiya needs three things in any CI system: a provider key, a model name, and
|
|
@@ -394,7 +506,8 @@ testeiya task "<task>" --output report.md --output run.json --output gh:pr-comme
|
|
|
394
506
|
| `gh:pr#123` | posted on that pull request |
|
|
395
507
|
|
|
396
508
|
Posting uses the GitHub CLI and resolves the PR number from `GITHUB_EVENT_PATH`,
|
|
397
|
-
`GITHUB_REF`, or `gh pr view
|
|
509
|
+
`GITHUB_REF`, or `gh pr view`. On a push with no PR of its own, like a merge to
|
|
510
|
+
main, it posts on the PR that brought the commit in. Inside Actions it just works. Every
|
|
398
511
|
destination is checked before the run starts, so a missing `gh` costs no tokens.
|
|
399
512
|
|
|
400
513
|
`--footer "<text>"` adds a line under the report and `--header "<text>"` adds
|
|
@@ -37,11 +37,11 @@ export function testomatio(options) {
|
|
|
37
37
|
}
|
|
38
38
|
function dynamicData(tms) {
|
|
39
39
|
if (tms === "mcp-proxy") {
|
|
40
|
-
return "Get them through the
|
|
40
|
+
return "Get them through the Testomat.io tools, one per entity (`tests`, `runs`, `testruns`, `issues`, …); pick the operation with its `command` param. The `mcp` tool searches them when the name is unclear.";
|
|
41
41
|
}
|
|
42
42
|
if (tms === "cli-only") {
|
|
43
43
|
return "There are no Testomat.io tools in this session. Get them from the REST API: `curl` with the `TESTOMATIO` token as the Authorization header against `$TESTOMATIO_URL/api/v2`. Keep to documented endpoints; never invent paths or parameters.";
|
|
44
44
|
}
|
|
45
|
-
return "Get them through the `testomatio-<slug>` MCP tools, one set per project.";
|
|
45
|
+
return "Get them through the `testomatio-<slug>` MCP tools, one set per project and one tool per entity; pick the operation with its `command` param.";
|
|
46
46
|
}
|
|
47
47
|
//# sourceMappingURL=testomat.io.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testomat.io.js","sourceRoot":"","sources":["../../prompt/testomat.io.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,OAA0B;IACnD,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QACvB,OAAO,MAAM,CAAA;;;;;;;KAOZ,CAAC;IACJ,CAAC;IAED,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,OAAO,CAAC,UAAU;QAAE,GAAG,GAAG,OAAO,OAAO,CAAC,UAAU,KAAK,CAAC;IAC7D,OAAO,MAAM,CAAA;;;;;6FAK8E,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC;;;;;;;;8IAQyB,GAAG;;GAE9I,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,GAAc;IACjC,IAAI,GAAG,KAAK,WAAW,EAAE,CAAC;QACxB,OAAO,
|
|
1
|
+
{"version":3,"file":"testomat.io.js","sourceRoot":"","sources":["../../prompt/testomat.io.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,OAA0B;IACnD,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QACvB,OAAO,MAAM,CAAA;;;;;;;KAOZ,CAAC;IACJ,CAAC;IAED,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,OAAO,CAAC,UAAU;QAAE,GAAG,GAAG,OAAO,OAAO,CAAC,UAAU,KAAK,CAAC;IAC7D,OAAO,MAAM,CAAA;;;;;6FAK8E,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC;;;;;;;;8IAQyB,GAAG;;GAE9I,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,GAAc;IACjC,IAAI,GAAG,KAAK,WAAW,EAAE,CAAC;QACxB,OAAO,wMAAwM,CAAC;IAClN,CAAC;IACD,IAAI,GAAG,KAAK,UAAU,EAAE,CAAC;QACvB,OAAO,8OAA8O,CAAC;IACxP,CAAC;IACD,OAAO,+IAA+I,CAAC;AACzJ,CAAC"}
|
package/dist/src/args.js
CHANGED
|
@@ -4,6 +4,7 @@ export const WELCOME = `
|
|
|
4
4
|
|
|
5
5
|
testeiya task "analyze this pr and find inconsistencies"
|
|
6
6
|
testeiya ask "is this feature ready"
|
|
7
|
+
testeiya pipeline web-testing
|
|
7
8
|
|
|
8
9
|
testeiya doctor check your setup
|
|
9
10
|
testeiya --help options and environment
|
|
@@ -14,6 +15,8 @@ export const USAGE = `testeiya, an AI testing agent
|
|
|
14
15
|
Usage:
|
|
15
16
|
testeiya task "<task>" run one task and write a report
|
|
16
17
|
testeiya ask "<question>" answer a question
|
|
18
|
+
testeiya pipeline <name> run a multi-stage pipeline, or list them with no name
|
|
19
|
+
testeiya pipeline-check <name> check a pipeline file and its gates, run nothing
|
|
17
20
|
testeiya doctor check what a run would resolve
|
|
18
21
|
testeiya models [pattern] list the models your key can reach
|
|
19
22
|
testeiya skills [pattern] list the skills bundled with this package
|
|
@@ -35,8 +38,15 @@ Options:
|
|
|
35
38
|
--header <text> line added above the report
|
|
36
39
|
--footer <text> line added under the report
|
|
37
40
|
--no-default-footer do not sign the report
|
|
41
|
+
--no-thread keep out of the comment thread: post nothing, read no comments
|
|
38
42
|
--json machine-readable output
|
|
39
43
|
--probe doctor only: test the key with one request
|
|
44
|
+
|
|
45
|
+
Pipeline options:
|
|
46
|
+
--dir <path> the pipeline folder; keep it between runs for rounds
|
|
47
|
+
--dry-run show gates and stages, spend no tokens
|
|
48
|
+
--set KEY=VALUE set a variable for every stage, repeatable
|
|
49
|
+
--gate NAME:COMMAND skip the whole run unless COMMAND exits 0, repeatable
|
|
40
50
|
-h, --help show this
|
|
41
51
|
-v, --version show version
|
|
42
52
|
|
|
@@ -173,6 +183,28 @@ Skills
|
|
|
173
183
|
loaded before the run, e.g. "/qa-thinking". A name we do not ship is left
|
|
174
184
|
alone, so a stray "/word" in the task is just a word.
|
|
175
185
|
|
|
186
|
+
Pipelines
|
|
187
|
+
|
|
188
|
+
A pipeline is several stages in one YAML file: agent tasks and shell commands,
|
|
189
|
+
run in order, passing files through one folder. "testeiya pipeline" lists the
|
|
190
|
+
bundled ones; pass a path to run your own.
|
|
191
|
+
|
|
192
|
+
testeiya pipeline web-testing -o gh:pr-comment
|
|
193
|
+
testeiya pipeline .github/testeiya/regression.yml --set TEST_COMMAND="npx playwright test"
|
|
194
|
+
|
|
195
|
+
--gate adds a check from the CI job, e.g. --gate 'pr:test -n "$PR_NUMBER"'.
|
|
196
|
+
When it fails the whole run is skipped and exits 0.
|
|
197
|
+
|
|
198
|
+
A stage whose gate fails is skipped with the gate's message. A final summary
|
|
199
|
+
reads every stage and is the only thing posted. Keep --dir between runs, as a
|
|
200
|
+
CI cache per pull request, and a re-run on the same commit retries from the
|
|
201
|
+
stage that stopped, while a new commit starts a round told what changed.
|
|
202
|
+
--dry-run shows the plan and spends nothing. "testeiya pipeline-check" checks
|
|
203
|
+
the file and every gate it declares, and prints the message of each gate that
|
|
204
|
+
does not pass. It exits 1 when one does not, 2 when the file is invalid.
|
|
205
|
+
--diagram also writes <name>.diagram.md: a Mermaid chart of the stages, the
|
|
206
|
+
files they pass on, and the gates, coloured by the check.
|
|
207
|
+
|
|
176
208
|
Testomat.io
|
|
177
209
|
|
|
178
210
|
Set TESTOMATIO to a project API key and the agent reads and writes that
|
|
@@ -180,7 +212,7 @@ Testomat.io
|
|
|
180
212
|
Add the project id, with --project or TESTOMATIO_PROJECT_ID, and it also gets
|
|
181
213
|
the Testomat.io tools. The MCP server needs the id: a token alone does not say
|
|
182
214
|
which project to talk to.`;
|
|
183
|
-
export const COMMANDS = ["task", "ask", "models", "skills", "doctor", "sessions", "help"];
|
|
215
|
+
export const COMMANDS = ["task", "ask", "pipeline", "pipeline-check", "models", "skills", "doctor", "sessions", "help"];
|
|
184
216
|
export function parseCliArgs(argv) {
|
|
185
217
|
const first = argv[0];
|
|
186
218
|
if (!first)
|
|
@@ -212,6 +244,28 @@ export function parseCliArgs(argv) {
|
|
|
212
244
|
args.prompt = text;
|
|
213
245
|
if ((first === "models" || first === "skills") && text)
|
|
214
246
|
args.pattern = text;
|
|
247
|
+
if ((first === "pipeline" || first === "pipeline-check") && text)
|
|
248
|
+
args.pipeline = text;
|
|
249
|
+
if (first === "pipeline-check" && !text)
|
|
250
|
+
return { error: "pipeline-check needs a pipeline name or a .yml path" };
|
|
251
|
+
if (typeof values.dir === "string")
|
|
252
|
+
args.dir = values.dir;
|
|
253
|
+
if (values["dry-run"])
|
|
254
|
+
args.dryRun = true;
|
|
255
|
+
if (values.diagram)
|
|
256
|
+
args.diagram = true;
|
|
257
|
+
if (Array.isArray(values.set)) {
|
|
258
|
+
const sets = parseSets(values.set);
|
|
259
|
+
if (typeof sets === "string")
|
|
260
|
+
return { error: sets };
|
|
261
|
+
args.sets = sets;
|
|
262
|
+
}
|
|
263
|
+
if (Array.isArray(values.gate)) {
|
|
264
|
+
const gates = parseGates(values.gate);
|
|
265
|
+
if (typeof gates === "string")
|
|
266
|
+
return { error: gates };
|
|
267
|
+
args.gates = gates;
|
|
268
|
+
}
|
|
215
269
|
if (Array.isArray(values.output))
|
|
216
270
|
args.outputs = values.output;
|
|
217
271
|
if (values.json)
|
|
@@ -246,6 +300,8 @@ export function parseCliArgs(argv) {
|
|
|
246
300
|
args.footer = values.footer;
|
|
247
301
|
if (values["no-default-footer"])
|
|
248
302
|
args.noDefaultFooter = true;
|
|
303
|
+
if (values["no-thread"])
|
|
304
|
+
args.noThread = true;
|
|
249
305
|
if (args.session && (args.continueLast || args.resume || args.name || args.noSession)) {
|
|
250
306
|
return { error: "--session already names and continues a session" };
|
|
251
307
|
}
|
|
@@ -273,17 +329,64 @@ const RUN_OPTIONS = {
|
|
|
273
329
|
header: { type: "string" },
|
|
274
330
|
footer: { type: "string" },
|
|
275
331
|
"no-default-footer": { type: "boolean" },
|
|
332
|
+
"no-thread": { type: "boolean" },
|
|
333
|
+
help: HELP_FLAG,
|
|
334
|
+
};
|
|
335
|
+
const PIPELINE_OPTIONS = {
|
|
336
|
+
output: RUN_OPTIONS.output,
|
|
337
|
+
json: JSON_FLAG,
|
|
338
|
+
model: { type: "string" },
|
|
339
|
+
project: { type: "string" },
|
|
340
|
+
thread: { type: "string" },
|
|
341
|
+
"exit-zero": { type: "boolean" },
|
|
342
|
+
header: { type: "string" },
|
|
343
|
+
footer: { type: "string" },
|
|
344
|
+
"no-default-footer": { type: "boolean" },
|
|
345
|
+
dir: { type: "string" },
|
|
346
|
+
"dry-run": { type: "boolean" },
|
|
347
|
+
set: { type: "string", multiple: true },
|
|
348
|
+
gate: { type: "string", multiple: true },
|
|
276
349
|
help: HELP_FLAG,
|
|
277
350
|
};
|
|
278
351
|
const OPTIONS = {
|
|
279
352
|
task: RUN_OPTIONS,
|
|
280
353
|
ask: RUN_OPTIONS,
|
|
354
|
+
pipeline: PIPELINE_OPTIONS,
|
|
355
|
+
"pipeline-check": {
|
|
356
|
+
json: JSON_FLAG,
|
|
357
|
+
model: { type: "string" },
|
|
358
|
+
dir: PIPELINE_OPTIONS.dir,
|
|
359
|
+
set: PIPELINE_OPTIONS.set,
|
|
360
|
+
gate: PIPELINE_OPTIONS.gate,
|
|
361
|
+
diagram: { type: "boolean" },
|
|
362
|
+
help: HELP_FLAG,
|
|
363
|
+
},
|
|
281
364
|
models: { json: JSON_FLAG, help: HELP_FLAG },
|
|
282
365
|
skills: { json: JSON_FLAG, help: HELP_FLAG },
|
|
283
366
|
doctor: { json: JSON_FLAG, probe: { type: "boolean" }, model: { type: "string" }, help: HELP_FLAG },
|
|
284
367
|
sessions: { json: JSON_FLAG, help: HELP_FLAG },
|
|
285
368
|
help: {},
|
|
286
369
|
};
|
|
370
|
+
function parseSets(values) {
|
|
371
|
+
const sets = {};
|
|
372
|
+
for (const value of values) {
|
|
373
|
+
const equals = value.indexOf("=");
|
|
374
|
+
if (equals <= 0)
|
|
375
|
+
return `--set takes KEY=VALUE, got "${value}"`;
|
|
376
|
+
sets[value.slice(0, equals)] = value.slice(equals + 1);
|
|
377
|
+
}
|
|
378
|
+
return sets;
|
|
379
|
+
}
|
|
380
|
+
function parseGates(values) {
|
|
381
|
+
const gates = {};
|
|
382
|
+
for (const value of values) {
|
|
383
|
+
const colon = value.indexOf(":");
|
|
384
|
+
if (colon <= 0 || !value.slice(colon + 1).trim())
|
|
385
|
+
return `--gate takes NAME:COMMAND, got "${value}"`;
|
|
386
|
+
gates[value.slice(0, colon)] = value.slice(colon + 1);
|
|
387
|
+
}
|
|
388
|
+
return gates;
|
|
389
|
+
}
|
|
287
390
|
function isCommand(value) {
|
|
288
391
|
return COMMANDS.includes(value);
|
|
289
392
|
}
|