kaloko 0.16.0 → 0.17.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 +7 -0
- package/package.json +3 -2
- package/schemas/evidence.v1.json +27 -4
- package/schemas/run.v1.json +19 -0
- package/skills/kaloko/SKILL.md +5 -2
- package/skills/kaloko/reference/collaboration.md +7 -1
- package/skills/kaloko/reference/evidence.md +44 -3
- package/skills/kaloko/reference/execution.md +5 -0
- package/skills/kaloko/reference/mcp.md +2 -1
- package/src/cli.mjs +84 -21
- package/src/lib/build.mjs +2 -0
- package/src/lib/cli-args.mjs +80 -7
- package/src/lib/docs-core.mjs +3 -1
- package/src/lib/doctor.mjs +11 -1
- package/src/lib/evidence-core.mjs +21 -10
- package/src/lib/lanes-core.mjs +61 -0
- package/src/lib/run-refs.mjs +55 -0
- package/src/lib/run.mjs +4 -1
- package/src/lib/scenario.mjs +2 -1
- package/src/lib/signoff.mjs +4 -0
- package/src/lib/update-check.mjs +132 -0
- package/viewer/index.html +45 -9
package/README.md
CHANGED
|
@@ -66,6 +66,13 @@ npx kaloko preview # local canvas
|
|
|
66
66
|
npx kaloko share --pr # upload to kaloko.app, comment on the PR
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
When a run belongs to a record outside Kaloko, say a change request in your QMS,
|
|
70
|
+
name it at the start: `--ref CC-2026-001` (repeat it for more, or set
|
|
71
|
+
`KALOKO_REFS` in CI) and, if you like, `--purpose dry_run`, `formal` or `smoke`
|
|
72
|
+
(`KALOKO_PURPOSE`). The canvas shows both, and `npx kaloko runs --ref CC-2026-001`
|
|
73
|
+
or `GET /api/v1/orgs/<org>/runs?ref=CC-2026-001` lists every run of that record
|
|
74
|
+
with its verdict, environment and purpose.
|
|
75
|
+
|
|
69
76
|
After a fix, walk only what the change reaches: `npx kaloko affected` explains
|
|
70
77
|
which steps the changed files touch (routes, imports, `affects:` globs in the
|
|
71
78
|
scenario), and `npx kaloko walk --affected` walks them. Several locales walk at
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kaloko",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Your AI agents build; Kaloko walks every screen, puts it on one canvas and records who approved what.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"qa",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
|
-
"test": "node scripts/test.mjs"
|
|
35
|
+
"test": "node scripts/test.mjs",
|
|
36
|
+
"version": "node scripts/cli-catalogue.mjs && git add worker/src/cli-latest.json"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"ajv": "^8.20.0",
|
package/schemas/evidence.v1.json
CHANGED
|
@@ -19,6 +19,18 @@
|
|
|
19
19
|
"time": { "type": ["string", "null"], "description": "ISO 8601 time in UTC (…Z), from the service's clock." },
|
|
20
20
|
"sha256": { "type": ["string", "null"], "pattern": "^[0-9a-f]{64}$", "description": "SHA-256, lowercase hex." },
|
|
21
21
|
"refs": { "type": "array", "items": { "type": "string" }, "description": "Requirement references as written in the scenario (refs:), free text, never interpreted." },
|
|
22
|
+
"session": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"description": "A person in the flow (a role): the key of the scenario's sessions: and its title.",
|
|
25
|
+
"required": ["key", "title"],
|
|
26
|
+
"properties": { "key": { "type": "string" }, "title": { "type": "string", "description": "The session's title (a localized title in its default or first language); the key when it has none." } }
|
|
27
|
+
},
|
|
28
|
+
"lane": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"description": "A lane of the canvas (a sub-process of the flow).",
|
|
31
|
+
"required": ["index", "label"],
|
|
32
|
+
"properties": { "index": { "type": "integer", "minimum": 0, "description": "The lane's number, from 0 (the canvas link #lane=<index>)." }, "label": { "type": ["string", "null"], "description": "The scenario's lanes: label, or the session's title when the sessions draw the lanes; null for an unnamed lane." } }
|
|
33
|
+
},
|
|
22
34
|
"run": {
|
|
23
35
|
"type": "object",
|
|
24
36
|
"required": ["generated_at", "acceptance", "signing", "record", "run", "scenario", "steps", "signatures", "trace"],
|
|
@@ -57,7 +69,9 @@
|
|
|
57
69
|
"started_at": { "$ref": "#/$defs/time", "description": "When the walk started (the walking machine's clock)." },
|
|
58
70
|
"finished_at": { "$ref": "#/$defs/time", "description": "When the manifest was built (the walking machine's clock)." },
|
|
59
71
|
"version": { "type": ["string", "null"], "description": "Design version, for runs of design environments." },
|
|
60
|
-
"app_version": { "type": ["string", "null"], "description": "Version of the application under test, when the run names one." }
|
|
72
|
+
"app_version": { "type": ["string", "null"], "description": "Version of the application under test, when the run names one." },
|
|
73
|
+
"refs": { "type": "array", "items": { "type": "string" }, "description": "External records the run serves, e.g. a change-control record (kaloko start --ref). Empty when the run names none, and for records made before runs carried them." },
|
|
74
|
+
"purpose": { "enum": ["dry_run", "formal", "smoke", null], "description": "Why the run was walked (kaloko start --purpose): a label, null when the run does not say." }
|
|
61
75
|
}
|
|
62
76
|
},
|
|
63
77
|
"scenario": {
|
|
@@ -68,7 +82,9 @@
|
|
|
68
82
|
"version": { "type": ["integer", "string", "null"], "description": "The scenario's own version: field, when set." },
|
|
69
83
|
"file": { "type": ["string", "null"], "description": "Path of the scenario file in the repository." },
|
|
70
84
|
"sha256": { "$ref": "#/$defs/sha256", "description": "SHA-256 of the scenario file as walked (CLI 0.15+; null before)." },
|
|
71
|
-
"refs": { "$ref": "#/$defs/refs" }
|
|
85
|
+
"refs": { "$ref": "#/$defs/refs" },
|
|
86
|
+
"lanes": { "type": ["array", "null"], "items": { "$ref": "#/$defs/lane" }, "description": "The flow's lanes (its sub-processes), in order: every named lane and every lane a step sits in. Named by the scenario's lanes:, or, without them, one per session. Null when the run's manifest could not be read." },
|
|
87
|
+
"sessions": { "type": ["array", "null"], "items": { "$ref": "#/$defs/session" }, "description": "The people in the flow (roles), in the scenario's order. Empty for a flow without sessions; null when the run's manifest could not be read." }
|
|
72
88
|
}
|
|
73
89
|
},
|
|
74
90
|
"environment": { "type": ["object", "null"], "description": "{ name, url }: the environment and its base URL." },
|
|
@@ -101,6 +117,8 @@
|
|
|
101
117
|
"title": { "type": ["string", "null"] },
|
|
102
118
|
"refs": { "$ref": "#/$defs/refs" },
|
|
103
119
|
"status": { "enum": ["pass", "partial", "fail", "skip"] },
|
|
120
|
+
"session": { "oneOf": [{ "$ref": "#/$defs/session" }, { "type": "null" }], "description": "The role that performs the step (the step's session:), or null." },
|
|
121
|
+
"lane": { "oneOf": [{ "$ref": "#/$defs/lane" }, { "type": "null" }], "description": "The lane the step sits in on the canvas (its sub-process); #lane=<index> on the canvas URL opens it." },
|
|
104
122
|
"decision": { "type": ["object", "null"], "description": "The step's current decision: { id, state: approved | returned, by, at, via, agent?, counts_as_person, note?, attachments?: [{ id, name, type, bytes, sha256, by, at, path: decisions/<step>/<file> in the evidence package }], pin?, shape? }." },
|
|
105
123
|
"decisions": { "type": "array", "description": "Every decision on the step, oldest first, shaped like decision; superseded ones too (resolved_at, deleted: true without content)." },
|
|
106
124
|
"carried": { "type": ["object", "null"], "description": "An approval carried over from an earlier accepted run: { from_run, compared_run, kind, approved_by, approved_at, approved_via }." },
|
|
@@ -184,7 +202,7 @@
|
|
|
184
202
|
},
|
|
185
203
|
"traceRow": {
|
|
186
204
|
"type": "object",
|
|
187
|
-
"description": "One row of the trace matrix: a requirement, a criterion, a step in one locale and viewport, its screenshot, the result and the decisions. A criterion's requirements are its own refs, its step's and its scenario's (requirement_level); a criterion without any gets one row with requirement null. The CSV has these columns in this order.",
|
|
205
|
+
"description": "One row of the trace matrix: a requirement, a criterion, a step in one locale and viewport with its role (session) and lane, its screenshot, the result and the decisions. A criterion's requirements are its own refs, its step's and its scenario's (requirement_level); a criterion without any gets one row with requirement null. The CSV has these columns in this order.",
|
|
188
206
|
"properties": {
|
|
189
207
|
"requirement": { "type": ["string", "null"] },
|
|
190
208
|
"requirement_level": { "enum": ["criterion", "step", "scenario", null] },
|
|
@@ -220,7 +238,11 @@
|
|
|
220
238
|
"run_signature_meaning": { "type": ["string", "null"] },
|
|
221
239
|
"run_signer_name": { "type": ["string", "null"] },
|
|
222
240
|
"run_signed_at": { "$ref": "#/$defs/time" },
|
|
223
|
-
"run_signer_time_zone": { "type": ["string", "null"] }
|
|
241
|
+
"run_signer_time_zone": { "type": ["string", "null"] },
|
|
242
|
+
"session": { "type": ["string", "null"], "description": "Key of the role that performs the step (the step's session:)." },
|
|
243
|
+
"session_title": { "type": ["string", "null"] },
|
|
244
|
+
"lane": { "type": ["integer", "null"], "description": "The step's lane (sub-process), from 0." },
|
|
245
|
+
"lane_label": { "type": ["string", "null"] }
|
|
224
246
|
}
|
|
225
247
|
},
|
|
226
248
|
"release": {
|
|
@@ -231,6 +253,7 @@
|
|
|
231
253
|
"release": { "type": "object", "description": "{ product, version, released_on, tag, commit, runs: [run id] }." },
|
|
232
254
|
"runs": { "type": "array", "items": { "$ref": "#/$defs/run" }, "description": "The evidence of each run of the release the caller can read." },
|
|
233
255
|
"missing": { "type": "array", "items": { "type": "object", "properties": { "run": { "type": "string" }, "reason": { "type": "string" } } }, "description": "Runs of the release without evidence here, with the reason." },
|
|
256
|
+
"filter": { "type": "object", "properties": { "ref": { "type": "string" }, "purpose": { "type": "string" } }, "description": "Present when the request narrowed the release to the runs of one record (?ref=) or purpose (?purpose=); runs then holds only those; missing still lists the runs that could not be read, since their references are unknown here." },
|
|
234
257
|
"trace": { "type": "array", "items": { "$ref": "#/$defs/traceRow" } }
|
|
235
258
|
}
|
|
236
259
|
},
|
package/schemas/run.v1.json
CHANGED
|
@@ -332,6 +332,25 @@
|
|
|
332
332
|
}
|
|
333
333
|
}
|
|
334
334
|
},
|
|
335
|
+
"refs": {
|
|
336
|
+
"type": "array",
|
|
337
|
+
"maxItems": 20,
|
|
338
|
+
"uniqueItems": true,
|
|
339
|
+
"items": {
|
|
340
|
+
"type": "string",
|
|
341
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:/#+-]{0,79}$"
|
|
342
|
+
},
|
|
343
|
+
"description": "External records the run serves, e.g. a change-control record (kaloko start --ref, KALOKO_REFS). The service filters runs by them (GET …/runs?ref=)."
|
|
344
|
+
},
|
|
345
|
+
"purpose": {
|
|
346
|
+
"enum": [
|
|
347
|
+
"dry_run",
|
|
348
|
+
"formal",
|
|
349
|
+
"smoke",
|
|
350
|
+
null
|
|
351
|
+
],
|
|
352
|
+
"description": "Why the run was walked (kaloko start --purpose, KALOKO_PURPOSE): a dry run, a formal run or a smoke run. A label; it changes nothing about how the run is walked or decided."
|
|
353
|
+
},
|
|
335
354
|
"sessions": {
|
|
336
355
|
"type": [
|
|
337
356
|
"object",
|
package/skills/kaloko/SKILL.md
CHANGED
|
@@ -210,9 +210,12 @@ only when a person says that value may be uploaded.
|
|
|
210
210
|
tools (relative URLs, no CDNs, state in the URL, deterministic content,
|
|
211
211
|
links between steps; what `kaloko design lint` checks):
|
|
212
212
|
[reference/portable-html.md](reference/portable-html.md)
|
|
213
|
-
- Command reference: `kaloko help
|
|
213
|
+
- Command reference: `kaloko help` (`kaloko help --json` lists every command,
|
|
214
|
+
sub-command and option as data); when something does not work (Chrome,
|
|
214
215
|
token, environment URL, evaluator key), run `kaloko doctor` first and fix
|
|
215
|
-
what it marks ✕.
|
|
216
|
+
what it marks ✕. When a command answers "… is in kaloko X (you have Y)",
|
|
217
|
+
the project's kaloko is too old for it: tell the person and suggest the
|
|
218
|
+
update command from that line; change the dependency only when they agree.
|
|
216
219
|
|
|
217
220
|
Project configuration is `kaloko.config.yml` in the repository root.
|
|
218
221
|
Project skills may add conventions (paths, test data, locales); follow them
|
|
@@ -134,6 +134,12 @@ choice of branch at every decision node. `#replay=1&step=<id>&path=a,b,c`
|
|
|
134
134
|
deep-links a specific path, so an issue comment can point at "the sign-in
|
|
135
135
|
branch, step 3". Replay reads the manifest only; nothing is uploaded.
|
|
136
136
|
|
|
137
|
+
A link can also open the map on one part of the flow: `#lane=<index>` fits
|
|
138
|
+
and highlights that lane (lanes count from 0, in the order of the scenario's
|
|
139
|
+
`lanes:`, or of its `sessions:` when it has no lanes), and `#session=<key>`
|
|
140
|
+
highlights one person's steps across all lanes. `get_run_summary` lists the
|
|
141
|
+
run's `lanes` and `sessions`, and every step names its `lane` and `session`.
|
|
142
|
+
|
|
137
143
|
For people who work through a list rather than a flow (QA leads, content
|
|
138
144
|
and SEO teams, big runs), switch the canvas to **Index**. Use *Map | Index*
|
|
139
145
|
in the top bar, the key `I`, or `#mode=index` in the URL.
|
|
@@ -277,7 +283,7 @@ Everything above is available over the API with an organization token:
|
|
|
277
283
|
`…/signoff.html`: the signed acceptance record of an accepted run.
|
|
278
284
|
- `GET /api/v1/orgs/<org>/notifications[?unread=1]`, `POST` with
|
|
279
285
|
`{"read": "all"}` or `{"read": [ids]}`
|
|
280
|
-
- `GET /api/v1/orgs/<org>/runs[?project=&scenario=&kind=&verdict=&author=&environment=&branch=&q=][&latest=1]`
|
|
286
|
+
- `GET /api/v1/orgs/<org>/runs[?project=&scenario=&kind=&verdict=&author=&environment=&branch=&ref=&purpose=&q=][&latest=1]`
|
|
281
287
|
(`latest=1`: the newest run per scenario with `older`), `GET /api/v1/orgs/<org>/runs/<id>`
|
|
282
288
|
- Paging: runs, `comments`, `notifications`, `tokens` and `audit` take
|
|
283
289
|
`?limit=` and `?cursor=`. The body keeps its shape (an array stays an
|
|
@@ -21,8 +21,12 @@ described). Within v1 fields are only added; read the fields you need and ignore
|
|
|
21
21
|
is signed elsewhere; your document says so ("executed in Kaloko; approval recorded in <your eQMS>").
|
|
22
22
|
- `record.sha256` and `record.kind` (`acceptance` or `execution`): the signed record the evidence came from. Put the
|
|
23
23
|
hash into your document so a reader can tie it to the package.
|
|
24
|
-
- `scenario` (`version`, `file`, `sha256`, `refs`), `environment`, `git`, `tools` (CLI,
|
|
25
|
-
service version).
|
|
24
|
+
- `scenario` (`version`, `file`, `sha256`, `refs`, `lanes`, `sessions`), `environment`, `git`, `tools` (CLI,
|
|
25
|
+
Node.js, OS, browsers, service version). `scenario.lanes` lists the flow's lanes (`{ index, label }`, the
|
|
26
|
+
sub-processes) and `scenario.sessions` its roles (`{ key, title }`); without `lanes:` in the scenario, each session
|
|
27
|
+
is a lane.
|
|
28
|
+
- `steps[].session` (`{ key, title }`, the role that performs the step, or null) and `steps[].lane`
|
|
29
|
+
(`{ index, label }`; `label` is null for an unnamed lane).
|
|
26
30
|
- `steps[].criteria[]`: the criterion text as walked, `refs`, and `results[]` per locale and viewport with
|
|
27
31
|
`assessed_by` (`person`, `deterministic`, `ai`, `none`) and `assessor` (an e-mail or a model id). `advisory: true`
|
|
28
32
|
means an AI result no person decided: never present it as a person's verdict.
|
|
@@ -31,7 +35,8 @@ described). Within v1 fields are only added; read the fields you need and ignore
|
|
|
31
35
|
- `signatures[]` (name, e-mail, `signed_at`, `time_zone`, `local_time`, `meaning`, `auth`, `withdrawn`) and
|
|
32
36
|
`decision_changes[]` with reasons.
|
|
33
37
|
- `audit[]`: the run's audit trail; `trace[]`: the trace matrix, one row per requirement, criterion, step, locale and
|
|
34
|
-
viewport (columns in `TRACE_COLUMNS` order, the same as `trace.csv`).
|
|
38
|
+
viewport (columns in `TRACE_COLUMNS` order, the same as `trace.csv`). Each row names the step's role and lane in
|
|
39
|
+
`session`, `session_title`, `lane` and `lane_label` (the last four columns of the CSV).
|
|
35
40
|
- A release (`kind: release`) has `runs[]` (each a run evidence), `missing[]` with reasons and one `trace[]`.
|
|
36
41
|
|
|
37
42
|
## Getting it
|
|
@@ -48,6 +53,42 @@ Over the API with a read token: `GET /api/v1/orgs/<org>/runs/<id>/evidence`, `
|
|
|
48
53
|
`…/audit.csv`, and `GET /api/v1/orgs/<org>/evidence?product=<p>&version=<v>`. Over MCP: `get_evidence` with `run_id`, or
|
|
49
54
|
`product` and `version`.
|
|
50
55
|
|
|
56
|
+
## A screen next to the test step
|
|
57
|
+
|
|
58
|
+
A documentation portal that shows a step's screenshot beside the test step fetches it with a read token limited to
|
|
59
|
+
the project (send it as `Authorization: Bearer …` from the portal's server; never put the token in a page):
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
GET /api/v1/orgs/<org>/runs/<run>/steps/<step>/screen?locale=en&viewport=desktop&size=thumb&sha256=<hex>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `locale` and `viewport` default to the run's first ones that the step has. A value the run does not have, a value
|
|
66
|
+
given twice, or `size` other than `thumb` or `full` answers 400. A screenshot the step does not have answers 404.
|
|
67
|
+
Errors after the run check carry `canvas`, the link to the step.
|
|
68
|
+
- The answer is the image with `ETag` (the SHA-256 of the file served) and `X-Kaloko-SHA256` (the SHA-256 of the
|
|
69
|
+
step's screenshot, the one `steps[].captures[].sha256` and the trace matrix list, for `thumb` too). `If-None-Match`
|
|
70
|
+
answers 304. `X-Kaloko-Locale`, `X-Kaloko-Viewport` and `X-Kaloko-Size` say what was served; a capture without a
|
|
71
|
+
thumbnail answers the full screenshot with `X-Kaloko-Size: full`. `Link` holds the step on the canvas.
|
|
72
|
+
- Store run, step, locale, viewport and that SHA-256, and send it back as `sha256`. When the run holds a different
|
|
73
|
+
file the answer is 409 with the run's `sha256`, so the portal never shows a picture nobody checked.
|
|
74
|
+
- Access is the run's: guests and project-limited tokens get 404 outside their projects, and nothing is served without
|
|
75
|
+
a token or a session (public links included). The image cannot be framed by other sites; link the reader to the
|
|
76
|
+
step on the canvas, where they sign in with their own account.
|
|
77
|
+
|
|
78
|
+
Over MCP: `get_step_screen` with `run_id`, `step` and optionally `locale`, `viewport`, `size`, `sha256` answers the
|
|
79
|
+
hashes, the canvas link and a `url` pinned to that file (no image bytes).
|
|
80
|
+
|
|
81
|
+
## Runs of an external record
|
|
82
|
+
|
|
83
|
+
When the run serves a record in the client's QMS (a change request, a validation protocol), start it with
|
|
84
|
+
`--ref CC-2026-001` (repeatable; `KALOKO_REFS` in CI, comma or space separated) and, when known,
|
|
85
|
+
`--purpose dry_run|formal|smoke` (`KALOKO_PURPOSE`). Up to 20 references of up to 80 characters: letters, digits and
|
|
86
|
+
`. _ - / : # +`. They are stored in the run (`refs`, `purpose` next to `issue`), shown in the canvas header, and the
|
|
87
|
+
evidence carries them as `run.refs` and `run.purpose`. Find the runs of a record with
|
|
88
|
+
`GET /api/v1/orgs/<org>/runs?ref=CC-2026-001[&purpose=formal]`, `kaloko runs --ref CC-2026-001` or MCP `list_runs`
|
|
89
|
+
with `ref`; the release evidence takes `&ref=` and `&purpose=` too (`kaloko evidence --release <v> --ref …`). Never
|
|
90
|
+
invent a reference: ask which record the run is for, or leave it out.
|
|
91
|
+
|
|
51
92
|
## When it is ready
|
|
52
93
|
|
|
53
94
|
An organization admin adds a webhook with the event `evidence.ready` (Settings → Integrations → Webhooks). It fires
|
|
@@ -24,6 +24,11 @@ call `kaloko session reset` themselves when the plan starts as a new visitor.
|
|
|
24
24
|
`start` prints a unique test e-mail address for the run; use it in forms so
|
|
25
25
|
`mail` can find the message.
|
|
26
26
|
|
|
27
|
+
`start --ref CC-2026-001 [--ref …] [--purpose dry_run|formal|smoke]` links the
|
|
28
|
+
run to records outside Kaloko (a change request, a protocol) and says why it
|
|
29
|
+
was walked; `KALOKO_REFS` and `KALOKO_PURPOSE` do the same in CI. See
|
|
30
|
+
`evidence.md` → Runs of an external record.
|
|
31
|
+
|
|
27
32
|
## Choosing a strategy per step
|
|
28
33
|
|
|
29
34
|
- `playwright` — stable, repeatable steps. The script exports
|
|
@@ -27,12 +27,13 @@ is null. Projects and products come complete, without pages.
|
|
|
27
27
|
|
|
28
28
|
| Tool | Use it for |
|
|
29
29
|
|---|---|
|
|
30
|
-
| `list_runs` | published runs, newest first; filters `scenario`, `project`, `verdict`, `author`, `environment`, `branch`, `limit`; `cursor` for the next page |
|
|
30
|
+
| `list_runs` | published runs, newest first; filters `scenario`, `project`, `verdict`, `author`, `environment`, `branch`, `ref` (a record the run serves), `purpose` (`dry_run`, `formal`, `smoke`), `limit`; `cursor` for the next page |
|
|
31
31
|
| `get_run_summary` | one run: statuses per step and locale, criteria that need attention with evidence (semantic ones with score and the evaluator's one-sentence `reason` per locale and viewport), regions ignored in pixel diffs and regions seen moving (`ignored_regions`, `unstable_regions`), screenshot URLs, verdict, baseline, canvas URL, open feedback |
|
|
32
32
|
| `get_feedback` | run verdict with note, approved steps, unresolved rejections and comments, failed manual criteria (`run_id`, or `scenario` for its latest run) |
|
|
33
33
|
| `list_notifications` | the token owner's inbox (`unread_only`, `limit`, `cursor`) |
|
|
34
34
|
| `list_projects` | projects of the organization with counts; the slug is the `project` filter of `list_runs` and the value for `project:` in a scenario |
|
|
35
35
|
| `upsert_project` | creates a project (`slug`, `name`, `visibility`, `product`; `create: true` refuses an existing one) or renames it; the same rules as `kaloko projects create\|rename\|sync` (a full token; renaming is for the project's admin). Ask before renaming |
|
|
36
|
+
| `get_step_screen` | one step's screenshot of a run, pinned (`run_id`, `step`, `locale`, `viewport`, `size` thumb or full, `sha256`): the SHA-256 of the screenshot and of the file, a `url` that fetches exactly that file with the same token, the canvas link; see [evidence.md](evidence.md) |
|
|
36
37
|
| `get_criteria_verdicts` | reviewer verdicts on manual or overridden criteria of a run |
|
|
37
38
|
| `get_stack_items` | the pages of a stack step (`run_id`, `step`; filters `status`, `criterion`, text): path, URL, title, status and failing criteria with evidence, screenshot URLs |
|
|
38
39
|
| `get_step_recording` | a step's recording and trace (`run_id`, `step`, `locale`, `viewport`, `trace: true` for the full trace): video URLs, duration, its chapter of a flow recording, failed attempts, the trace summary (failed requests, console errors, slowest request); see [recording.md](recording.md). `get_run_summary` lists trace summaries per step too |
|
package/src/cli.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import yaml from 'js-yaml'
|
|
|
8
8
|
import { loadConfig, environment, urlFor, baseUrl, CONFIG_FILE, LEGACY_CONFIG_FILE } from './lib/config.mjs'
|
|
9
9
|
import { loadScenario, parseScenario, validateScenario, scenarioWarnings } from './lib/scenario.mjs'
|
|
10
10
|
import { createRun, resolveRun, readCaptures, listRuns } from './lib/run.mjs'
|
|
11
|
+
import { startRefs, normalizePurpose, RUN_PURPOSES } from './lib/run-refs.mjs'
|
|
11
12
|
import { gitMeta, projectAndIssue, appVersion } from './lib/git.mjs'
|
|
12
13
|
import { startBrowser, stopBrowser, connectBrowser, currentPage, launchEphemeral, launchWithRetry, consoleLog, allStates, resetSession, acquireWalkLock } from './lib/browser.mjs'
|
|
13
14
|
import { captureStep, captureCrop, captureOptions, acceptDialogs } from './lib/capture.mjs'
|
|
@@ -39,7 +40,7 @@ import { formatReviewQueue, shareReviewLine } from './lib/review.mjs'
|
|
|
39
40
|
import { healthCmd } from './lib/health.mjs'
|
|
40
41
|
import { htmlViewAllowed } from './lib/htmlview.mjs'
|
|
41
42
|
import { service, serviceUrl } from './lib/service.mjs'
|
|
42
|
-
import { parseArgs, helpFor, checkOptions, closest } from './lib/cli-args.mjs'
|
|
43
|
+
import { parseArgs, helpFor, checkOptions, closest, catalogue, unknownOptions, subcommandsFor, takesWords } from './lib/cli-args.mjs'
|
|
43
44
|
import { issueCmd, scheduleCmd } from './lib/integrations-cli.mjs'
|
|
44
45
|
import { login } from './lib/login.mjs'
|
|
45
46
|
import { initNextSteps, loginNextSteps } from './lib/onboarding.mjs'
|
|
@@ -67,6 +68,7 @@ import { startScope, scopeKeys, dimensionLine, baseViewports, engineKeys, appDev
|
|
|
67
68
|
import { deviceConfig, prepareAndroid, readyAndroid, finishAndroid, prepareSimulator, applyAppAppearance } from './lib/apps/devices.mjs'
|
|
68
69
|
import { attachAndroidWebView } from './lib/apps/webview.mjs'
|
|
69
70
|
import { piiSettings, piiFindings, piiRequested, guardAllow, formatFindings } from './lib/pii.mjs'
|
|
71
|
+
import { updateHint } from './lib/update-check.mjs'
|
|
70
72
|
|
|
71
73
|
|
|
72
74
|
/** Options every command takes and the exit codes; the end of kaloko help and of the help of each command. */
|
|
@@ -78,9 +80,10 @@ const COMMON_HELP = `Common options (every command)
|
|
|
78
80
|
JSON, progress and notes go to stderr, and an error is one line on stderr:
|
|
79
81
|
{"error":{"message":"…","code":"…","next":"…"}} with the exit code below
|
|
80
82
|
--help, -h Help for one command, like kaloko help <command> (kaloko help export run: one
|
|
81
|
-
sub-command)
|
|
83
|
+
sub-command); kaloko help --json lists every command, sub-command and option
|
|
82
84
|
--version, -v The version of kaloko (also kaloko version)
|
|
83
85
|
KALOKO_AGENT=<name> Labels writes by an agent
|
|
86
|
+
KALOKO_NO_UPDATE_CHECK=1 An unknown command or option does not ask kaloko.app whether a newer kaloko has it
|
|
84
87
|
Durations (--timeout, --wait, --expires, --older-than) take a number in the option's unit or one with s, m, h or d:
|
|
85
88
|
90s, 10m, 2h, 14d
|
|
86
89
|
|
|
@@ -141,6 +144,9 @@ Run
|
|
|
141
144
|
start --scenario <file> --env <name> [--locales cs,en] [--viewports desktop,mobile] [--executor <name>]
|
|
142
145
|
[--app-version <v>] (default: the git tag on HEAD; documentation captures carry it)
|
|
143
146
|
Create a run directory and make it current (--json: the run id and its folder)
|
|
147
|
+
[--ref CC-2026-001] [--ref …] Records the run serves (change control, protocol); repeat it or separate
|
|
148
|
+
with commas; KALOKO_REFS in CI. kaloko runs --ref lists the runs of a record
|
|
149
|
+
[--purpose dry_run|formal|smoke] Why the run was walked, shown as a label; KALOKO_PURPOSE in CI
|
|
144
150
|
[--viewports iphone-15,ipad-landscape,desktop] Viewport names of the scenario or device presets
|
|
145
151
|
(kaloko devices lists them; <preset>-landscape turns one sideways)
|
|
146
152
|
[--browsers chromium,webkit,firefox] [--color-schemes light,dark] [--reduced-motion no-preference,reduce]
|
|
@@ -267,10 +273,11 @@ Results
|
|
|
267
273
|
projects sync [--json] Make Kaloko match project:, project_name: and product: of the config: create
|
|
268
274
|
the project when it is missing, rename it when the name differs, add it to the
|
|
269
275
|
product. A share never renames a project; it says when the names differ
|
|
270
|
-
runs [--scenario id] [--env e] [--all] [--limit N] [--cursor c] [--json]
|
|
276
|
+
runs [--scenario id] [--env e] [--ref r] [--purpose p] [--all] [--limit N] [--cursor c] [--json]
|
|
271
277
|
Shared runs: the newest per scenario (with "+ N older"); --all or --scenario lists every
|
|
272
278
|
run: --all to the last one page by page, otherwise the newest 300; --limit N lists at
|
|
273
|
-
most N, --cursor c goes on where a list stopped (it prints the cursor)
|
|
279
|
+
most N, --cursor c goes on where a list stopped (it prints the cursor); --ref r lists
|
|
280
|
+
every run of a record, --purpose p only dry runs, formal runs or smoke runs
|
|
274
281
|
trash <run-id>... | restore <run-id>
|
|
275
282
|
Move runs to the trash (uploader or admin) and back; restorable until purge
|
|
276
283
|
prune [--scenario id] [--env e] [--keep 3] [--older-than 14d|48h] [--dry-run | --yes] [--json]
|
|
@@ -310,7 +317,7 @@ Results
|
|
|
310
317
|
Check a record or an evidence package someone handed over (no token needed); exit 1
|
|
311
318
|
when it does not verify
|
|
312
319
|
evidence <run-id> [--csv] [--out file] [--json]
|
|
313
|
-
evidence --release <version> [--product p] [--csv] [--out file] [--json]
|
|
320
|
+
evidence --release <version> [--product p] [--ref r] [--purpose p] [--csv] [--out file] [--json]
|
|
314
321
|
The evidence of a run or a release (Business, schema qawalk.evidence.v1): acceptance
|
|
315
322
|
and signing state, criteria as walked with requirement refs, results, screenshots
|
|
316
323
|
with SHA-256, decisions, signatures, audit trail; --csv prints the trace matrix
|
|
@@ -1255,6 +1262,9 @@ const commands = {
|
|
|
1255
1262
|
async start (args) {
|
|
1256
1263
|
const { config, root } = await context(args)
|
|
1257
1264
|
if (!args.scenario || !args.env) throw new UsageError('Usage: kaloko start --scenario <file> --env <name>')
|
|
1265
|
+
// the records the run serves and why it was walked (#126): checked before anything is created
|
|
1266
|
+
const { refs, purpose, problems: refIssues } = startRefs(args, process.env)
|
|
1267
|
+
if (refIssues.length) throw new UserError(refIssues.join('\n'))
|
|
1258
1268
|
const scenarioFile = resolve(args.scenario)
|
|
1259
1269
|
const scenario = await loadScenario(scenarioFile)
|
|
1260
1270
|
const env = environment(config, args.env)
|
|
@@ -1274,11 +1284,12 @@ const commands = {
|
|
|
1274
1284
|
const appVer = typeof args['app-version'] === 'string' ? args['app-version'].slice(0, 80) : await appVersion(root)
|
|
1275
1285
|
// the product the flow documents (visual changelog): its tag, or the only product of the config
|
|
1276
1286
|
const product = scenario.product || (Object.keys(config.products || {}).length === 1 ? Object.keys(config.products)[0] : null)
|
|
1277
|
-
const { dir, meta } = await createRun({ output: config.output, scenario, scenarioFile, env, scope, git, executor, root, project, issue, appVersion: appVer, product, pii })
|
|
1287
|
+
const { dir, meta } = await createRun({ output: config.output, scenario, scenarioFile, env, scope, git, executor, root, project, issue, refs, purpose, appVersion: appVer, product, pii })
|
|
1278
1288
|
const addr = recipient(env.mail, meta.short)
|
|
1279
1289
|
// --json: what a script needs to pin the run (KALOKO_RUN) and find its files
|
|
1280
|
-
if (args.json) { say(JSON.stringify({ run: meta.id, dir, env: env.name, readonly: !!env.readonly, locales, viewports: vpNames, project: project.slug, issue: issue || null, test_email: addr || null, pii: pii?.mode || null }, null, 2)); return }
|
|
1290
|
+
if (args.json) { say(JSON.stringify({ run: meta.id, dir, env: env.name, readonly: !!env.readonly, locales, viewports: vpNames, project: project.slug, issue: issue || null, refs, purpose, test_email: addr || null, pii: pii?.mode || null }, null, 2)); return }
|
|
1281
1291
|
say(`Run ${meta.id}\n dir: ${relative(process.cwd(), dir)}\n env: ${env.name}${env.readonly ? ' (readonly)' : ''}\n locales: ${locales.join(', ')} viewports: ${vpNames.length > 12 ? `${vpNames.slice(0, 12).join(', ')} … (${vpNames.length})` : vpNames.join(', ')}${dimensionLine(scope)}\n project: ${project.slug}${issue ? ` issue: ${issue.repo ? issue.repo + '#' : '#'}${issue.number}` : ''}`)
|
|
1292
|
+
if (refs.length || purpose) say(` ${[refs.length && `refs: ${refs.join(', ')}`, purpose && `purpose: ${purpose}`].filter(Boolean).join(' ')}`)
|
|
1282
1293
|
if (pii) say(` personal data: ${pii.mode === 'pseudonymize' ? 'replaced with fake values' : 'hidden'} in every capture`)
|
|
1283
1294
|
if (addr) say(` test e-mail: ${addr}`)
|
|
1284
1295
|
say(` other agents may start runs here too: pin this one with --run ${meta.id} or KALOKO_RUN=${meta.id}`)
|
|
@@ -2328,7 +2339,11 @@ const commands = {
|
|
|
2328
2339
|
async runs (args) {
|
|
2329
2340
|
const { config } = await context(args)
|
|
2330
2341
|
const client = service(config)
|
|
2331
|
-
|
|
2342
|
+
// --ref: every run of a record (#126), not only the newest per scenario
|
|
2343
|
+
const ref = typeof args.ref === 'string' ? args.ref : null
|
|
2344
|
+
const purpose = args.purpose === undefined ? null : normalizePurpose(args.purpose === true ? '' : args.purpose)
|
|
2345
|
+
if (args.purpose !== undefined && !purpose) throw new UsageError(`--purpose takes one of ${RUN_PURPOSES.join(', ')}.`)
|
|
2346
|
+
const all = !!args.all || typeof args.scenario === 'string' || !!ref
|
|
2332
2347
|
const envName = envOption(args)
|
|
2333
2348
|
// --limit N: at most N runs; --all follows the service's cursors to the last run, otherwise the newest RUNS_CAP;
|
|
2334
2349
|
// --cursor continues where an earlier list stopped (the cursor it printed)
|
|
@@ -2339,18 +2354,18 @@ const commands = {
|
|
|
2339
2354
|
let next = null
|
|
2340
2355
|
do {
|
|
2341
2356
|
const page = Math.min(1000, want - runs.length)
|
|
2342
|
-
const q = new URLSearchParams({ ...(all ? {} : { latest: '1' }), ...(typeof args.scenario === 'string' ? { scenario: args.scenario } : {}), ...(envName ? { environment: envName } : {}), limit: String(page), ...(cursor ? { cursor } : {}) })
|
|
2357
|
+
const q = new URLSearchParams({ ...(all ? {} : { latest: '1' }), ...(typeof args.scenario === 'string' ? { scenario: args.scenario } : {}), ...(envName ? { environment: envName } : {}), ...(ref ? { ref } : {}), ...(purpose ? { purpose } : {}), limit: String(page), ...(cursor ? { cursor } : {}) })
|
|
2343
2358
|
const got = await client.page(`/orgs/${config.org}/runs?${q}`)
|
|
2344
2359
|
runs.push(...got.data)
|
|
2345
2360
|
next = cursor = got.next
|
|
2346
2361
|
} while (next && runs.length < want)
|
|
2347
2362
|
// more runs than were listed: say how to go on (on stderr, so --json stays one JSON array)
|
|
2348
|
-
if (next) console.error(`Showing ${runs.length}; there are more. kaloko runs${args.all ? ' --all' : ''}${typeof args.scenario === 'string' ? ` --scenario ${args.scenario}` : ''}${envName ? ` --env ${envName}` : ''} --cursor ${next} lists the next ones${all && !args.all ? ', --all every one' : ''}.`)
|
|
2363
|
+
if (next) console.error(`Showing ${runs.length}; there are more. kaloko runs${args.all ? ' --all' : ''}${typeof args.scenario === 'string' ? ` --scenario ${args.scenario}` : ''}${envName ? ` --env ${envName}` : ''}${ref ? ` --ref ${ref}` : ''}${purpose ? ` --purpose ${purpose}` : ''} --cursor ${next} lists the next ones${all && !args.all ? ', --all every one' : ''}.`)
|
|
2349
2364
|
if (args.json) { say(JSON.stringify(runs, null, 2)); return }
|
|
2350
2365
|
if (!runs.length) { say('No shared runs match.'); return }
|
|
2351
2366
|
for (const r of runs) {
|
|
2352
2367
|
const s = r.summary || {}
|
|
2353
|
-
say(`${r.id} ${r.environment?.name || ''} ${r.verdict || 'pending'}${r.kept ? ' · kept' : ''} ${s.pass || 0}✓ ${s.partial || 0}◐ ${s.fail || 0}✕ ${r.author || ''}${r.older ? ` + ${r.older} older` : ''}`)
|
|
2368
|
+
say(`${r.id} ${r.environment?.name || ''}${r.purpose ? ` (${r.purpose})` : ''} ${r.verdict || 'pending'}${r.kept ? ' · kept' : ''} ${s.pass || 0}✓ ${s.partial || 0}◐ ${s.fail || 0}✕ ${r.author || ''}${r.older ? ` + ${r.older} older` : ''}`)
|
|
2354
2369
|
}
|
|
2355
2370
|
if (!all) say(`\nNewest run per scenario; --all or --scenario <id> lists every run.`)
|
|
2356
2371
|
},
|
|
@@ -2722,11 +2737,18 @@ const commands = {
|
|
|
2722
2737
|
/** `kaloko help [command]`: everything, or the entries of one command with the common options. */
|
|
2723
2738
|
async help (args = { _: [] }) {
|
|
2724
2739
|
const topic = args._?.[0]
|
|
2740
|
+
// kaloko help --json: every command, sub-command and option as data (kaloko help prune --json: that command)
|
|
2741
|
+
if (args.json) {
|
|
2742
|
+
const all = await cliCatalogue()
|
|
2743
|
+
if (!topic) { say(JSON.stringify(all, null, 2)); return }
|
|
2744
|
+
if (all.commands[topic]) { say(JSON.stringify({ version: all.version, commands: { [topic]: all.commands[topic] } }, null, 2)); return }
|
|
2745
|
+
return unknownCommand(topic, true, args)
|
|
2746
|
+
}
|
|
2725
2747
|
if (!topic) { say(HELP); return }
|
|
2726
2748
|
// kaloko help export run: the entries of that sub-command (all of the command's when it has none by that name)
|
|
2727
2749
|
const text = commands[topic] && topic !== 'help' ? helpFor(HELP, topic, COMMON_HELP, args._?.[1]) : null
|
|
2728
2750
|
if (text) { say(text); return }
|
|
2729
|
-
unknownCommand(topic)
|
|
2751
|
+
return unknownCommand(topic, false, args)
|
|
2730
2752
|
}
|
|
2731
2753
|
}
|
|
2732
2754
|
|
|
@@ -2735,14 +2757,41 @@ async function packageVersion () {
|
|
|
2735
2757
|
return JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8')).version
|
|
2736
2758
|
}
|
|
2737
2759
|
|
|
2738
|
-
/**
|
|
2739
|
-
function
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2760
|
+
/** Every command, sub-command and option of this kaloko as data: kaloko help --json, and the service's /cli/latest.json. */
|
|
2761
|
+
export async function cliCatalogue () {
|
|
2762
|
+
return catalogue(HELP, [...Object.keys(commands), 'version'], await packageVersion())
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
/**
|
|
2766
|
+
* "There is no command": when a newer kaloko has it, the line that says which version and how to update; else the
|
|
2767
|
+
* nearest command when it looks like a typo and the full help. Exit code 2 either way.
|
|
2768
|
+
*/
|
|
2769
|
+
async function unknownCommand (name, json = false, args = {}) {
|
|
2770
|
+
process.exitCode = EXIT.usage
|
|
2771
|
+
const update = await newerKaloko({ command: name }, args)
|
|
2772
|
+
const guess = update ? null : closest(name, Object.keys(commands))
|
|
2773
|
+
const message = `There is no command "${name}"${update ? ' in this kaloko' : ''}.${guess ? ` Did you mean kaloko ${guess}?` : ''}`
|
|
2774
|
+
if (json) { console.error(JSON.stringify(jsonError(new UsageError(message, { next: update || 'kaloko help lists the commands.' })))); return }
|
|
2775
|
+
if (update) { console.error(`${message}\n${update}`); return }
|
|
2743
2776
|
console.error(`${message} The commands are below; kaloko help <command> explains one.\n`)
|
|
2744
2777
|
say(HELP)
|
|
2745
|
-
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
/** A usage error whose next step is updating kaloko (printed on its own line). */
|
|
2781
|
+
const updateError = (reason, update) => Object.assign(new UsageError(reason, { next: update }), { update: true })
|
|
2782
|
+
|
|
2783
|
+
/**
|
|
2784
|
+
* The update line for what this kaloko does not know ({ command, sub, options }), or null: only when the newest
|
|
2785
|
+
* released kaloko (the service's /cli/latest.json, cached for a day) is newer and has exactly that. With `detail`:
|
|
2786
|
+
* { update: line, newer: [options the newer kaloko takes] }.
|
|
2787
|
+
*/
|
|
2788
|
+
async function newerKaloko (typed, args = {}, detail = false) {
|
|
2789
|
+
try {
|
|
2790
|
+
let config = {}
|
|
2791
|
+
try { ({ config } = await loadConfig(process.cwd(), typeof args.config === 'string' ? args.config : undefined)) } catch {}
|
|
2792
|
+
const r = await updateHint(typed, { installed: await packageVersion(), config, root: packageRoot, args })
|
|
2793
|
+
return detail ? r : r?.line || null
|
|
2794
|
+
} catch { return null }
|
|
2746
2795
|
}
|
|
2747
2796
|
|
|
2748
2797
|
/** A line of output that is a JSON document (the --json answer), told apart from progress and notes. */
|
|
@@ -2782,13 +2831,27 @@ async function runCommand (args, json) {
|
|
|
2782
2831
|
if ((!args._.length && args.version) || args._[0] === 'version') { say(json ? JSON.stringify({ version: await packageVersion() }) : await packageVersion()); return }
|
|
2783
2832
|
const name = args._.shift() || 'help'
|
|
2784
2833
|
const cmd = commands[name]
|
|
2785
|
-
if (!cmd) { if (args.help) say(HELP); else unknownCommand(name, json); return }
|
|
2834
|
+
if (!cmd) { if (args.help) say(HELP); else await unknownCommand(name, json, args); return }
|
|
2786
2835
|
// kaloko <command> --help: the command's help instead of running it
|
|
2787
2836
|
if (args.help && name !== 'help') { say(helpFor(HELP, name, COMMON_HELP, args._[0]) || HELP); return }
|
|
2837
|
+
// what this kaloko does not know: a word after a command that takes none (kaloko projects create) and options its
|
|
2838
|
+
// help does not name. A newer kaloko may have them; only then a line says which version and how to update.
|
|
2839
|
+
const word = name !== 'help' && typeof args._[0] === 'string' && !subcommandsFor(HELP, name).has(args._[0]) ? args._[0] : null
|
|
2840
|
+
const unknown = unknownOptions(HELP, name, args)
|
|
2788
2841
|
try {
|
|
2789
|
-
|
|
2842
|
+
if (word && !takesWords(HELP, name)) {
|
|
2843
|
+
const update = await newerKaloko({ command: name, sub: word }, args)
|
|
2844
|
+
if (update) throw updateError(`There is no "kaloko ${name} ${word}" in this kaloko.`, update)
|
|
2845
|
+
}
|
|
2846
|
+
const newer = unknown.length ? await newerKaloko({ command: name, options: unknown }, args, true) : null
|
|
2847
|
+
for (const line of checkOptions(HELP, name, args, newer || {})) console.error(line)
|
|
2790
2848
|
await cmd(args)
|
|
2791
2849
|
} catch (error) {
|
|
2850
|
+
// a usage error over a sub-command a newer kaloko has (kaloko tokens <new>): say so instead of only the help
|
|
2851
|
+
if (error instanceof UsageError && !error.update && !error.next && word) {
|
|
2852
|
+
const update = await newerKaloko({ command: name, sub: word }, args)
|
|
2853
|
+
if (update) error = updateError(error.reason, update)
|
|
2854
|
+
}
|
|
2792
2855
|
const known = error instanceof UserError
|
|
2793
2856
|
if (json) {
|
|
2794
2857
|
// the reason in one JSON line on stderr, the documented exit code; a bug also prints its stack before it
|
|
@@ -2798,7 +2861,7 @@ async function runCommand (args, json) {
|
|
|
2798
2861
|
return
|
|
2799
2862
|
}
|
|
2800
2863
|
// a usage mistake: the command's help says what is wrong; no support line, exit code 2
|
|
2801
|
-
if (error instanceof UsageError) { console.error(`${error.message}${/kaloko help /.test(error.message) ? '' : `\nkaloko help ${name} shows how to use it.`}`); process.exitCode = EXIT.usage } else if (known) { console.error(`${error.message}\n${SUPPORT_LINE}`); process.exitCode = exitCodeOf(error) } else {
|
|
2864
|
+
if (error instanceof UsageError && error.update) { console.error(`${error.reason}\n${error.next}`); process.exitCode = EXIT.usage } else if (error instanceof UsageError) { console.error(`${error.message}${/kaloko help /.test(error.message) ? '' : `\nkaloko help ${name} shows how to use it.`}`); process.exitCode = EXIT.usage } else if (known) { console.error(`${error.message}\n${SUPPORT_LINE}`); process.exitCode = exitCodeOf(error) } else {
|
|
2802
2865
|
console.error(`Kaloko stopped on an unexpected error (a bug on our side). Send the output below to ${SUPPORT_EMAIL} with the command you ran.`)
|
|
2803
2866
|
throw error
|
|
2804
2867
|
}
|
package/src/lib/build.mjs
CHANGED
|
@@ -69,6 +69,8 @@ export async function buildManifest ({ run, scenario, config, root, catalog = []
|
|
|
69
69
|
git: meta.git,
|
|
70
70
|
project: meta.project || null,
|
|
71
71
|
issue: meta.issue || null,
|
|
72
|
+
...(meta.refs?.length ? { refs: meta.refs } : {}),
|
|
73
|
+
...(meta.purpose ? { purpose: meta.purpose } : {}),
|
|
72
74
|
// version of the application under test; `version` is the number of a design version (#42)
|
|
73
75
|
app_version: meta.app_version ?? meta.version ?? null,
|
|
74
76
|
environment: meta.environment,
|