@unotest/web 0.31.0 → 0.33.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/CHANGELOG.md +497 -0
- package/README.md +71 -4
- package/dist/config/schema.d.ts +6 -6
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +20 -0
- package/dist/dsl/index.js +1 -1
- package/dist/dsl/web-dsl-language-service.js +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/runner/cli.d.ts +1 -6
- package/dist/runner/cli.js +1 -1
- package/dist/runner/init/chrome-probe.js +1 -1
- package/dist/runner/init.js +1 -1
- package/dist/runner/install-chromium.js +1 -1
- package/dist/runner/prepare-fix.js +1 -1
- package/dist/runner/scaffold-workspace.js +1 -1
- package/dist/runner/serve-fixture.js +1 -1
- package/dist/runner/web-runner-adapter.js +1 -1
- package/guides/agent-integration.md +148 -13
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,502 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.33.0] - 2026-09-06
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2abfd4d: Box: every run is announced — Slack, Telegram, or a webhook of your own — and the channels are managed from the guard.
|
|
8
|
+
|
|
9
|
+
A box now tells people how its runs went, whoever started them: the
|
|
10
|
+
schedule's ticks, a pull request's check, somebody pressing Run in the
|
|
11
|
+
viewer, a terminal inside the container. The daemon reads each run's own
|
|
12
|
+
journal, so the three producers are one source; the run's manifest says
|
|
13
|
+
what set it off — `trigger: { kind: "schedule" | "ci" | "manual" | "cli",
|
|
14
|
+
by? }`, written by the runner from `UNOTEST_RUN_TRIGGER` /
|
|
15
|
+
`UNOTEST_RUN_ACTOR` (a box sets them for its scheduled and CI runs, the
|
|
16
|
+
viewer for a run ordered from its UI; a terminal sets nothing and reads as
|
|
17
|
+
`cli`).
|
|
18
|
+
|
|
19
|
+
A scheduled series is announced on a **change of state**: `failed` after
|
|
20
|
+
`afterFailures` red ticks in a row, `recovered`, `still-failing` at a rule's
|
|
21
|
+
`repeatEvery`, `not-run` when the suite could not run at all (no bundle, a
|
|
22
|
+
bundle that does not install) — daily by default. A run somebody ordered is
|
|
23
|
+
announced on its own, `failed` or `passed`, every time. A withdrawn or
|
|
24
|
+
aborted run says nothing; a run whose journal stops and whose heartbeat goes
|
|
25
|
+
cold is `interrupted`.
|
|
26
|
+
|
|
27
|
+
Channels, rules, mutes and quiet hours live on the box and are managed from
|
|
28
|
+
the guard's `/_guard/notifications` page (or `boxd notify …`): a channel's
|
|
29
|
+
secret is set once and never shown again, a rule picks environments,
|
|
30
|
+
triggers, events and channels, a series or the whole project can be muted
|
|
31
|
+
until a time, and quiet hours hold reminders and `passed` back until the
|
|
32
|
+
morning. A generic webhook receives the event as JSON (`version: 1`, the
|
|
33
|
+
box named, the `trigger`) with `X-Unotest-Event` and, when the channel has
|
|
34
|
+
a secret, `X-Unotest-Signature: sha256=<hex>` — HMAC-SHA256 over the raw
|
|
35
|
+
body, the scheme GitHub uses — plus any headers the channel declares.
|
|
36
|
+
|
|
37
|
+
`@unotest/protocol`: `RunManifest.trigger` and the trigger helpers
|
|
38
|
+
(`runTriggerFromEnv`, `runTriggerEnv`, `manifestTrigger`); the session-door
|
|
39
|
+
paths, views and parsers of the notifications API (`boxNotifyProjectPath`,
|
|
40
|
+
`boxNotifyChannelPath`, `boxNotifyRulePath`, `boxNotifySeriesPath`,
|
|
41
|
+
`boxNotifyMutePath`, `boxNotifyQuietHoursPath`, `boxNotifyPreviewPath`,
|
|
42
|
+
`BoxNotifyProjectView`, …). The first draft of this feature (channels
|
|
43
|
+
declared in the box config, secrets by field) never shipped; this replaces
|
|
44
|
+
it whole.
|
|
45
|
+
|
|
46
|
+
### Patch Changes
|
|
47
|
+
|
|
48
|
+
- 5d1bde3: `lint` and `e2e` no longer fail on a project tree they cannot write to.
|
|
49
|
+
|
|
50
|
+
Editor typings (`unotest/jsconfig.json`, `.unotest/types/env.d.ts`) are a
|
|
51
|
+
convenience for a human with an IDE open, and they were being written on
|
|
52
|
+
every implicit `lint` / `e2e`. On a box that runs a suite in an isolated
|
|
53
|
+
container the bundle is mounted read-only, so that write raised `EROFS` /
|
|
54
|
+
`ENOENT` and every scenario ended as `interrupted` — a suite failing
|
|
55
|
+
because a cache could not be refreshed.
|
|
56
|
+
|
|
57
|
+
The write is now skipped when the tree refuses it (matched on the errno
|
|
58
|
+
`code`, never on a message), both artifacts report `skipped`, the result
|
|
59
|
+
carries the reason, and the run carries on; any other failure still
|
|
60
|
+
throws. `UNOTEST_EDITOR_TYPES=0` turns the whole thing off up front — a
|
|
61
|
+
box sets it on every run it starts, because there is no editor in a
|
|
62
|
+
container and the attempt is only noise in the log.
|
|
63
|
+
|
|
64
|
+
- b3e1220: Judge: the bearer token on `/judge` is compared in constant time, and the
|
|
65
|
+
docs say what a verdict sends and what it is not.
|
|
66
|
+
|
|
67
|
+
`UNOTEST_JUDGE_TOKEN` was checked with a plain string comparison, which
|
|
68
|
+
returns as soon as one character differs; both sides are now hashed and
|
|
69
|
+
compared with `timingSafeEqual`. The judge guide and the package README
|
|
70
|
+
gain a section on what leaves the machine — the one element's rendered
|
|
71
|
+
text and the rubric, nothing else; the secret redactor does not act on
|
|
72
|
+
that body; CI defaults to `fake` — and on why a verdict, being a model's
|
|
73
|
+
reading of the page's own text, is a check on wording and never a
|
|
74
|
+
security gate.
|
|
75
|
+
|
|
76
|
+
- b3e1220: Box notifications: a webhook channel's header values are credentials.
|
|
77
|
+
|
|
78
|
+
The notifications API and the guard's page now see a webhook's headers
|
|
79
|
+
as names with `{ set: true }` — the values (the receiver's
|
|
80
|
+
`Authorization`, most of the time) are never read back, like the signing
|
|
81
|
+
secret. The edit form lists the names; typing a value after one replaces
|
|
82
|
+
the set, names alone keep it. `/api/box/notify` is the guard's own route:
|
|
83
|
+
it is no longer reachable through the session proxy for any role — a
|
|
84
|
+
readonly session could previously list every project's channels with
|
|
85
|
+
their headers.
|
|
86
|
+
|
|
87
|
+
- b3e1220: Box notifications: the webhook signature now covers a timestamp.
|
|
88
|
+
|
|
89
|
+
Every webhook delivery carries `X-Unotest-Timestamp` (Unix milliseconds,
|
|
90
|
+
when it was sent), and `X-Unotest-Signature` is HMAC-SHA256 over
|
|
91
|
+
`<timestamp>.<raw body>` rather than the body alone — GitHub's scheme
|
|
92
|
+
with a timestamp in front, so a delivery captured on the wire cannot be
|
|
93
|
+
replayed to the receiver once its window (five minutes, documented) has
|
|
94
|
+
passed. The GitHub webhook the box receives is unchanged: that is
|
|
95
|
+
GitHub's contract.
|
|
96
|
+
|
|
97
|
+
The `by` of a CI-triggered run (`push a1b2c3d (branch)`) is cut at 200
|
|
98
|
+
characters — a branch name is the pusher's input, and it must not balloon
|
|
99
|
+
the run's manifest and every notification the run produces.
|
|
100
|
+
|
|
101
|
+
- b5907cf: Pin playwright to the exact release the box image ships.
|
|
102
|
+
|
|
103
|
+
`@unotest/web` depended on `playwright ^1.62.0`, so a fresh install of a
|
|
104
|
+
suite could pick a newer playwright than the browsers inside the box's
|
|
105
|
+
`mcr.microsoft.com/playwright` image, and every run on the box failed
|
|
106
|
+
with "Browser 'chromium' is not installed" the day 1.63.0 shipped. The
|
|
107
|
+
dependency is now the exact version of the image (1.62.1); the two are
|
|
108
|
+
bumped together, and a guard in the monorepo's `verify` keeps them equal.
|
|
109
|
+
|
|
110
|
+
- 5d1bde3: Building the runner no longer writes into the project tree.
|
|
111
|
+
|
|
112
|
+
`ExplorationService` created `.unotest/explorations/` in its constructor,
|
|
113
|
+
and the composition root builds it for every entry point — including an
|
|
114
|
+
`e2e` run. Where the project tree is read-only (a box mounts a pushed
|
|
115
|
+
bundle that way) the run ended before a browser was asked for. Recording
|
|
116
|
+
is an MCP activity, so the folder is now created with the first record
|
|
117
|
+
written instead.
|
|
118
|
+
|
|
119
|
+
With the editor-typings fix in the same release, an ordinary `lint` /
|
|
120
|
+
`e2e` writes nothing under the sources: run artifacts go to
|
|
121
|
+
`UNOTEST_ARTIFACTS_ROOT` (which the box makes writable), and everything
|
|
122
|
+
else that writes into the project — recording, authoring, `init`, `env
|
|
123
|
+
set`, `bundle push` — happens on a developer's machine.
|
|
124
|
+
|
|
125
|
+
- 5d1bde3: Run isolation, stage 1: a box no longer executes a test bundle inside its
|
|
126
|
+
own daemon.
|
|
127
|
+
|
|
128
|
+
box-runner (new, private): the run sidecar — the only service on a box
|
|
129
|
+
holding the docker socket, and the only one that starts a container. Three
|
|
130
|
+
authenticated routes on the `box` network (`POST /runs`, `GET
|
|
131
|
+
/runs/:id/stream` NDJSON, `DELETE /runs/:id`), a shared secret compared in
|
|
132
|
+
constant time, and an API that cannot be told anything about how a
|
|
133
|
+
container is built: no path, image, network, user, mount or flag. A request
|
|
134
|
+
names a project, an environment, a bundle, a scenario and the environment's
|
|
135
|
+
values; the bind sources are derived from the first three, resolved with
|
|
136
|
+
`realpath` and re-checked against the projects root, and must already
|
|
137
|
+
exist. The container is created over the Docker Engine API (pinned
|
|
138
|
+
`v1.43`) straight over the socket — no `docker` binary in the image and no
|
|
139
|
+
client dependency, and a container that is a JSON document has no place
|
|
140
|
+
for a value to become a flag. A daemon that refuses or is not there comes
|
|
141
|
+
back as `502 {reason}` before the run is accepted, or as an `error` event
|
|
142
|
+
on the stream after — never as a run that "exited with code null". An
|
|
143
|
+
environment variable the box does not forward is named in the sidecar's
|
|
144
|
+
log rather than dropped silently. A run gets the bundle tree read-only as its working directory, the
|
|
145
|
+
environment's `unotest/.runs.<env>` read-write, a `noexec` tmpfs `/tmp`,
|
|
146
|
+
a read-only rootfs, all capabilities dropped, `no-new-privileges`, its own
|
|
147
|
+
uid in boxd's group, memory/pids/cpu limits clamped to both the operator's ceilings and the
|
|
148
|
+
host's own size (docker refuses a container bigger than the machine
|
|
149
|
+
instead of clamping it), a 512 MB `/dev/shm`
|
|
150
|
+
(docker's default 64 MB kills Chromium mid-page; the host's IPC namespace
|
|
151
|
+
is deliberately not borrowed), and the `runs` network only. The container engine is an interface, so every rule is asserted on
|
|
152
|
+
the container that would have reached docker — including the cases where
|
|
153
|
+
the answer is no container at all.
|
|
154
|
+
|
|
155
|
+
boxd: running the SUITE is its own contract (`IScenarioRunner`), separate
|
|
156
|
+
from running a command (`ICommandRunner`, still the daemon's own `npm ci`
|
|
157
|
+
and viewers). `ContainerScenarioRunner` talks to the sidecar,
|
|
158
|
+
`ProcessScenarioRunner` keeps the old child-process shape for a box without
|
|
159
|
+
docker; `BOXD_RUNNER_KIND=container|process` chooses, and half a container
|
|
160
|
+
configuration refuses to start. Reading a bundle's schedules
|
|
161
|
+
(`unotest-web schedules --json`) executes the project's config module, so it
|
|
162
|
+
goes the same way. A run's environment is built from named parts
|
|
163
|
+
(`run-environment.ts`) instead of inheriting `process.env`, and its debug
|
|
164
|
+
tree is pointed at the run's own directory rather than the read-only
|
|
165
|
+
sources. New metrics
|
|
166
|
+
`boxd_run_container_total{outcome}` and `boxd_run_container_start_seconds`.
|
|
167
|
+
An environment's runs directory is created by the daemon with `2775`, so
|
|
168
|
+
the run's uid may write into it and the daemon's group may read it back.
|
|
169
|
+
|
|
170
|
+
box-kit: `secretsMatch` (constant-time secret comparison, moved out of
|
|
171
|
+
dist-service), `splitLines`, and `envNumber` / `envPositiveNumber` — the
|
|
172
|
+
env-reading rule the box-side services share.
|
|
173
|
+
|
|
174
|
+
`npm ci` of a bundle is a container of its own (`kind: "install"`): the
|
|
175
|
+
bundle tree is its only mount and it is writable, there is no environment
|
|
176
|
+
and no artifacts directory in reach, and a dependency's install scripts
|
|
177
|
+
RUN — a native module builds or fetches its binary as usual. That is what
|
|
178
|
+
running them inside the daemon could never allow. The container's last
|
|
179
|
+
steps, only on success, check the tree against the box's size cap, set
|
|
180
|
+
the final modes on what the install created and write the install marker
|
|
181
|
+
— each with an exit code of its own, so the daemon's log names the step
|
|
182
|
+
that failed — so a killed or oversized
|
|
183
|
+
install leaves a tree the box will not mount; a failed install takes the
|
|
184
|
+
tree with it. The daemon no longer passes `--ignore-scripts` and no
|
|
185
|
+
longer walks the tree afterwards.
|
|
186
|
+
|
|
187
|
+
The daemon runs with `umask 002` and clears `node_modules` before each
|
|
188
|
+
install, so a tree it created is one the install container (another uid
|
|
189
|
+
in its group) can actually write.
|
|
190
|
+
|
|
191
|
+
An install that fails KEEPS the tree, unmarked: nothing mounts it and the
|
|
192
|
+
next attempt reinstalls in place (`npm ci` wipes `node_modules` itself).
|
|
193
|
+
And a bundle directory holding a manifest with neither an archive nor a
|
|
194
|
+
tree behind it no longer counts as "this box has it" — a push of the same
|
|
195
|
+
content brings it back instead of being answered `already had this exact
|
|
196
|
+
bundle`.
|
|
197
|
+
|
|
198
|
+
web: `bundle push` and the docs say that a dependency's install scripts
|
|
199
|
+
run on a box, in isolation — the earlier advice to vendor them is gone.
|
|
200
|
+
|
|
201
|
+
A run ordered in the viewer now goes through the daemon's queue instead
|
|
202
|
+
of being spawned by the viewer.
|
|
203
|
+
|
|
204
|
+
`POST/GET/DELETE /api/box/envs/<project>/<env>/runs[/<runId>]` (paths and
|
|
205
|
+
wire types in `@unotest/protocol`) takes a `manual` ticket like every
|
|
206
|
+
other producer. Two callers may use it, and neither is taken on its word:
|
|
207
|
+
the guard, proving it is the guard with a shared secret from `box-init`
|
|
208
|
+
(the actor header is read only next to it — a viewer container could
|
|
209
|
+
otherwise claim to be any admin), and a viewer, with a per-environment
|
|
210
|
+
token the daemon minted when it started that viewer plus a short-lived
|
|
211
|
+
single-use ticket the guard signed over WHO clicked. The guard gates the
|
|
212
|
+
route under `admin` and requires an `Origin` on a mutation; the daemon
|
|
213
|
+
checks the token's environment, the ticket's environment and its `jti`
|
|
214
|
+
against the path.
|
|
215
|
+
|
|
216
|
+
`@unotest/viewer` gets `BoxdRunner`: on a box the Run button orders and
|
|
217
|
+
the Stop button asks the daemon, and the progress still comes from the
|
|
218
|
+
run's journal on disk. A local viewer spawns as it always did — the
|
|
219
|
+
composition root picks by whether a box handed it credentials.
|
|
220
|
+
|
|
221
|
+
- 348aba7: Run isolation, stage 2b and 3: an environment's viewer runs in a
|
|
222
|
+
container of its own, and a box ships the sidecar in its compose stack.
|
|
223
|
+
|
|
224
|
+
The viewer was the last thing on a box that executed a bundle's code next
|
|
225
|
+
to the daemon's state: it runs the UI, the language service and the
|
|
226
|
+
linter out of the bundle's own `node_modules`. It is now a container the
|
|
227
|
+
run sidecar starts and the daemon asks for — `POST /viewers`, one per
|
|
228
|
+
environment, addressed by a name both services derive
|
|
229
|
+
(`unotest-viewer-<project>-<env>-<8 hex>`, port 7788).
|
|
230
|
+
|
|
231
|
+
Each viewer gets a NETWORK of its own with exactly two other containers
|
|
232
|
+
attached: the guard, which proxies people at it, and the daemon, which it
|
|
233
|
+
orders runs from. Not the sidecar's network, not the runs network, and
|
|
234
|
+
above all not another viewer's — a viewer has no authentication of its
|
|
235
|
+
own, and two on one network would be two containers of untrusted code
|
|
236
|
+
with a route to each other. That network is INTERNAL: a viewer's server
|
|
237
|
+
makes no outbound call, so it gets none, while the network a run joins
|
|
238
|
+
stays open because a test drives an application. The name of a viewer and
|
|
239
|
+
its network is a function of the project/environment PAIR rather than of
|
|
240
|
+
the string they join to, so two environments whose names concatenate the
|
|
241
|
+
same way cannot end up sharing one. Its mounts are the bundle tree read-only and
|
|
242
|
+
the environment's `unotest/` read-write (the run history it renders and
|
|
243
|
+
the queue it withdraws tickets from); the environment's secrets and its
|
|
244
|
+
`current` link sit in the directory above and are mounted by nobody. Its
|
|
245
|
+
environment is assembled by the daemon — the target, the environment's
|
|
246
|
+
variables and secrets, its own credential — and the daemon's own
|
|
247
|
+
`process.env` no longer travels.
|
|
248
|
+
|
|
249
|
+
**Editing a suite's files from the viewer on a box no longer works**: the
|
|
250
|
+
sources are read-only there. Such edits never survived the next `bundle
|
|
251
|
+
push` anyway, which replaced the tree. Everything else in the UI is
|
|
252
|
+
unchanged.
|
|
253
|
+
|
|
254
|
+
Readiness is the docker daemon's own healthcheck, probed inside the
|
|
255
|
+
container and read through the sidecar — the daemon no longer waits on a
|
|
256
|
+
lock file, and nothing dials a viewer from the process holding the docker
|
|
257
|
+
socket. A viewer's output is streamed into the daemon's log and
|
|
258
|
+
reattached when the stream drops. A container whose bundle and values
|
|
259
|
+
still match is ADOPTED across a restart of either service, keeping the
|
|
260
|
+
credential it was born with; one whose environment is gone is stopped,
|
|
261
|
+
with its network and its token. The sidecar recognises its own containers
|
|
262
|
+
by label, never by name alone, and enforces its own ceiling on how many
|
|
263
|
+
viewers may exist (`BOX_RUNNER_MAX_VIEWERS`) — boxd knows how many
|
|
264
|
+
environments there are right up until boxd is the thing that was
|
|
265
|
+
compromised.
|
|
266
|
+
|
|
267
|
+
A run somebody stopped now ENDS in the viewer instead of hanging.
|
|
268
|
+
Stopping a run stops its container gracefully (SIGTERM, then SIGKILL
|
|
269
|
+
after `BOX_RUNNER_STOP_GRACE_SECONDS`, default 10) so the suite writes
|
|
270
|
+
the terminal event of its own journal; if it could not — the grace ran
|
|
271
|
+
out, the machine died — the daemon appends it. The same grace applies to
|
|
272
|
+
a run killed on its timeout, so a run's budget is the timeout plus the
|
|
273
|
+
grace.
|
|
274
|
+
|
|
275
|
+
A viewer that dies is noticed in about a second and replaced: the log
|
|
276
|
+
stream the daemon holds open ends with the container, and the 404 that
|
|
277
|
+
answers its immediate reattach is the signal. The route comes out of the
|
|
278
|
+
table at once — the guard then says the environment has no viewer instead
|
|
279
|
+
of proxying at a dead address — and a fresh container is started, on the
|
|
280
|
+
same backoff the daemon uses for everything else it cannot reach.
|
|
281
|
+
|
|
282
|
+
A restart of the sidecar is no longer an outage. While it cannot be
|
|
283
|
+
reached the daemon keeps the routes it published (the viewer containers
|
|
284
|
+
are up, and the guard reaches them directly) and tries again with a short
|
|
285
|
+
backoff instead of waiting for its next five-minute sweep. Editing a file
|
|
286
|
+
in the viewer on a box now answers 403 with the reason — the sources are
|
|
287
|
+
read-only there — rather than a blank 500.
|
|
288
|
+
|
|
289
|
+
The viewer no longer collapses the daemon's refusals into a blank 500:
|
|
290
|
+
the box's own status and reason reach the browser, so "that click has
|
|
291
|
+
already been used" (409), "the box cannot verify the caller" (503) and
|
|
292
|
+
"the box daemon is not answering" (503) are told apart by the person who
|
|
293
|
+
clicked.
|
|
294
|
+
|
|
295
|
+
Deployment: `compose.yaml` gains the `runner` service (the only holder of
|
|
296
|
+
the docker socket, the release ships its bundled `box-runner.mjs` beside
|
|
297
|
+
`boxd.mjs`), fixed container names for the guard and the daemon, a
|
|
298
|
+
`box_secrets` volume whose secrets `box-init` generates, and a one-off
|
|
299
|
+
migration of the projects tree to the group layout an install container
|
|
300
|
+
needs. Two values an operator fills in: `BOX_PROJECTS_HOST_DIR` (a bind
|
|
301
|
+
source is resolved by the docker daemon on the host) and `BOX_DOCKER_GID`.
|
|
302
|
+
Step by step in the box release's cutover manual.
|
|
303
|
+
|
|
304
|
+
- 9158968: Viewer: a run that started before the page was reloaded is shown again.
|
|
305
|
+
|
|
306
|
+
The live sidebar was rebuilt from WebSocket events only, and those speak
|
|
307
|
+
from the moment the page connects — a collection already in progress
|
|
308
|
+
never re-sent its `started` event, so after a reload it was missing from
|
|
309
|
+
ACTIVE, then reappeared nameless with no scenario list once its next
|
|
310
|
+
scenario began, while its children showed up one by one. On load, and on
|
|
311
|
+
an environment or target switch, the viewer now asks the server which
|
|
312
|
+
runs are live and replays each one's journal into the same state the
|
|
313
|
+
live stream feeds, so a reload mid-run shows the collection with its
|
|
314
|
+
name, scenarios and progress.
|
|
315
|
+
|
|
316
|
+
- b3e1220: Viewer: a session that may not edit variables does not see credential-shaped
|
|
317
|
+
values, nor the project's absolute path.
|
|
318
|
+
|
|
319
|
+
Behind the guard, a readonly session could read `GET /api/variables` and
|
|
320
|
+
get every box variable's value — `PASSWORD=…` included when the operator
|
|
321
|
+
had pushed it as a variable rather than a secret. The viewer now reads the
|
|
322
|
+
capabilities the guard stamps on each request: when the caller cannot edit
|
|
323
|
+
variables, a value whose name matches `PASSWORD | PASSWD | SECRET | TOKEN |
|
|
324
|
+
KEY | CREDENTIAL` is sent empty and flagged `secret`, and `GET /api/targets`
|
|
325
|
+
leaves out `projectRoot`. Display-masking by capability, not authorization —
|
|
326
|
+
a viewer without a proxy in front is unchanged, and the guard still refuses
|
|
327
|
+
every write.
|
|
328
|
+
|
|
329
|
+
- bd6b7ba: Viewer: a collection run no longer shows up nameless and without scenarios.
|
|
330
|
+
|
|
331
|
+
The journal tailer moved its cursor to the end of the file even when the
|
|
332
|
+
last line was still being written, so the half already on disk was lost
|
|
333
|
+
and the other half was later read as garbage. The longest line in a
|
|
334
|
+
journal is `collection-run:started` with the scenario list, so that was
|
|
335
|
+
the one it usually caught: the run appeared under its id instead of its
|
|
336
|
+
collection name, with "collection has no scenarios" and a `2/0 done`
|
|
337
|
+
counter. The cursor now stops at the last complete line and the rest is
|
|
338
|
+
read on the next change.
|
|
339
|
+
|
|
340
|
+
- 8846a06: Viewer: a run started the moment the viewer came up is shown, not lost
|
|
341
|
+
until the next change.
|
|
342
|
+
|
|
343
|
+
Watching a directory becomes live a moment after the watcher reports it
|
|
344
|
+
is ready, and a run directory created inside that gap was not merely
|
|
345
|
+
late — it was invisible until something else changed in the same day,
|
|
346
|
+
which in a quiet environment is the next run. Measured at 1-3% on macOS
|
|
347
|
+
with a busy machine, and the window is exactly "open the viewer, start a
|
|
348
|
+
run". The viewer now proves the watch delivers before it declares itself
|
|
349
|
+
up: it creates a directory of its own and waits to hear about it, so
|
|
350
|
+
boot completes on evidence rather than on a promise. A tree it cannot
|
|
351
|
+
write to, or a probe that never comes back, costs a warning in the log
|
|
352
|
+
and boots as before.
|
|
353
|
+
|
|
354
|
+
A directory whose name starts with a dot is never reported as a run — an
|
|
355
|
+
editor's leftovers, or the watcher's own probe, can no longer appear in
|
|
356
|
+
the run list as a phantom.
|
|
357
|
+
|
|
358
|
+
Run queue: a waiter whose process paused is no longer mistaken for dead
|
|
359
|
+
— the queue stamps its own files with its own clock.
|
|
360
|
+
|
|
361
|
+
A run waiting for a slot proves it is alive by touching its ticket, and
|
|
362
|
+
what it is judged against was the modification time the filesystem
|
|
363
|
+
wrote: on a network share that is the server's clock, and against a
|
|
364
|
+
machine busy enough to keep a process off the CPU, a ticket written a
|
|
365
|
+
moment ago could look like it belonged to a process that died. The queue
|
|
366
|
+
now stamps every file it creates with the same clock it judges them by.
|
|
367
|
+
|
|
368
|
+
- Updated dependencies [2abfd4d]
|
|
369
|
+
- Updated dependencies [b3e1220]
|
|
370
|
+
- Updated dependencies [b3e1220]
|
|
371
|
+
- Updated dependencies [5d1bde3]
|
|
372
|
+
- Updated dependencies [348aba7]
|
|
373
|
+
- Updated dependencies [9158968]
|
|
374
|
+
- Updated dependencies [b3e1220]
|
|
375
|
+
- Updated dependencies [bd6b7ba]
|
|
376
|
+
- Updated dependencies [8846a06]
|
|
377
|
+
- @unotest/protocol@0.33.0
|
|
378
|
+
- @unotest/viewer@0.33.0
|
|
379
|
+
- @unotest/core@0.33.0
|
|
380
|
+
- @unotest/dsl@0.33.0
|
|
381
|
+
- @unotest/grounder-client@0.33.0
|
|
382
|
+
|
|
383
|
+
## [0.32.0] - 2026-09-05
|
|
384
|
+
|
|
385
|
+
### Minor Changes
|
|
386
|
+
|
|
387
|
+
- 442fadf: `npx @unotest/web box …` reads a box's results from a terminal: `box envs` lists the environments a read token may look at, `box runs` their history (`--latest` collapses it to one line per scenario with its failing streak), `box run <id>` explains one run — the failure, the soft steps, the judge's verdicts — `box queue` shows who is waiting, and `box screenshot` saves a frame the run captured.
|
|
388
|
+
|
|
389
|
+
`box run --download` fetches the run's `*.unotest.zip` into `.unotest/box/` and unpacks its failure bundle into `.unotest/failures/`, where `list_failures`, `get_failure_*` and `agent_fix` already look — so a run that failed on a box is debugged with the commands a local failure is. `--no-screenshots` asks the box itself to leave the step frames out (`GET /api/runs/:id/export?screenshots=0`), which is what makes the download smaller rather than only the disk.
|
|
390
|
+
|
|
391
|
+
Reading needs a personal read token in `UNOTEST_BOX_READ_TOKEN`; the project's `UNOTEST_BOX_TOKEN` still pushes bundles and values and cannot read runs.
|
|
392
|
+
|
|
393
|
+
The viewer publishes its archive reader as `@unotest/viewer/snapshot`, so the three places that open a `*.unotest.zip` — its server, its browser bundle and now the CLI — share one implementation and one message for a file that is not an archive.
|
|
394
|
+
|
|
395
|
+
- 69fbea1: Six MCP tools close the loop that `bundle push --run` opens: `box_run` says what became of a run the agent ordered on a box, `box_run_download` unpacks its failure bundle into `.unotest/failures/` so `get_failure_trace`, `get_failure_console`, `get_failure_a11y`, `get_failure_screenshot`, `get_failure_network` and `agent_fix` work on it unchanged, and `box_envs`, `box_runs`, `box_queue` and `box_screenshot` cover the cases where the agent has no run id, no environment name, a run that never started, or a page it would rather see than read about. Their descriptions carry the route, not just the arguments.
|
|
396
|
+
|
|
397
|
+
Without a read token the tools refuse with a message saying where to mint one, so an agent meets an instruction rather than an unexplained failure.
|
|
398
|
+
|
|
399
|
+
- 9099f64: Box: personal read tokens, so an agent can read a box's runs without a browser.
|
|
400
|
+
|
|
401
|
+
A user mints a token for themselves on the guard's new **Read tokens** page
|
|
402
|
+
(`/_guard/tokens`), sees the value once, and points a client at the box with
|
|
403
|
+
`UNOTEST_BOX_READ_TOKEN`. The token is always `readonly` whatever its owner's
|
|
404
|
+
role, may only `GET`, and names the environment it means in
|
|
405
|
+
`X-Unotest-Environment: <project>/<environment>` (`GET /_guard/api/envs` lists
|
|
406
|
+
them). It is not a machine identity: it follows its owner — revoked, disabled
|
|
407
|
+
or a lapsed seat all stop it, and the refusal says which. An administrator
|
|
408
|
+
sees every token on the box and can revoke one that is not theirs; issuing,
|
|
409
|
+
first use and revocation all land in the audit trail.
|
|
410
|
+
|
|
411
|
+
`UNOTEST_BOX_TOKEN` is unchanged: the project token still pushes bundles and
|
|
412
|
+
environment values, and read tokens cannot — asking with the wrong one now
|
|
413
|
+
says which token the route wants instead of a bare "unauthenticated".
|
|
414
|
+
|
|
415
|
+
A read token is never passed on to the viewer behind the guard, so it cannot
|
|
416
|
+
end up in the logs of a service that has no use for it. A box whose licence
|
|
417
|
+
has lapsed, and an environment whose viewer is not up yet, answer a token in
|
|
418
|
+
the read contract's shape rather than with a page or a bare 503 — "wait" and
|
|
419
|
+
"renew the licence" are not the same instruction as "your token is wrong".
|
|
420
|
+
|
|
421
|
+
### Patch Changes
|
|
422
|
+
|
|
423
|
+
- 5266b30: The agent integration guide's tool catalog matches the server again. It listed fifteen per-action tools (`goto`, `click`, `fill`, `press`, …) that were removed when recording moved into `explore_step`, had no section at all for the ten exploration tools that replaced them, counted the debugger's eleven tools as six, and put the total at 39 when it is 50. An agent reading it as a map — which is exactly what this file is for — would call tools that do not exist and conclude the server is broken. Every name now comes from the list the test suite verifies against the real registry, and the section counts add up to the total.
|
|
424
|
+
- 442fadf: `BoxReadClient` reads a box's runs — environments, run history, one run's whole journal, a collection's children, the queue, a run's export zip and its individual artifacts — over the same HTTP routes a browser uses, authenticated with the personal read token in `UNOTEST_BOX_READ_TOKEN`. Every refusal arrives as a typed `BoxReadError` whose `kind` says what to do next, so throttling is never mistaken for a rejected token and a live run's export says "wait" rather than looking like a broken box. Configuration mistakes surface when a read is attempted rather than at startup, so a stale token in a project's `.env` cannot stop a local run that never touches a box.
|
|
425
|
+
|
|
426
|
+
The viewer gains a `@unotest/viewer/wire` entry point exporting its HTTP contract types (the runs page, a run's full snapshot, the queue payload and the snapshot manifest), so a client can name the shapes it parses instead of keeping a second copy of them.
|
|
427
|
+
|
|
428
|
+
- f770a25: The box address and the read token are resolved from `unotest/.env` and `unotest/.secrets` on every request, not once at startup, and the `box` CLI reads those files directly. `unotest-web box …` runs without loading the project config, so nothing had flattened `unotest/.env` into its environment — the command told people to put the address there and then refused with "no box address". And an MCP server that resolved once meant a token written while it ran was ignored until the editor reconnected it, which is exactly the restart this path exists to avoid. One rule for both settings, from both entry points: a flag, then the environment, then the project's files.
|
|
429
|
+
- 8aa0f30: Protocol contract for reading a box's run results with a personal read token: `UNOTEST_BOX_READ_TOKEN` (distinct from the project's `UNOTEST_BOX_TOKEN`, which stays a write credential for bundles and environment values), the `X-Unotest-Environment: <project>/<environment>` header every bearer request names its own environment with, the `/_guard/api/envs` listing and its `BoxReadEnvironment` entry, and typed refusals (`unauthorized`, `forbidden`, `unknown-environment`, `not-found`, `rate-limited`, `run-in-progress`, `unavailable`, `malformed`) with parsers that reject anything that is not a box answering. Throttling gets a code of its own rather than an `unauthorized` carrying `Retry-After`: a rejected token means mint a new one, a throttled one means wait and resend the same one. The viewer's `ViewerEnvOption` is now that same protocol type rather than a second copy of it. An environment whose viewer is still starting — the common state right after a bundle push — answers `unavailable` rather than looking like an unreachable box, so the advice is to wait rather than to check the address.
|
|
430
|
+
- 8aa0f30: A run exported as `*.unotest.zip` now carries everything needed to diagnose it away from the machine that produced it: `stdout.log` and `stderr.log` (previously dropped, which left the one artifact that explains a runner crash outside the bundle), the failing page's `page.html` reachable through the manifest, and the run's step screenshots — declared in a new `manifest.screenshots` list, because an importer keeps only what the manifest names and a frame absent from it did not survive the round trip.
|
|
431
|
+
- 5266b30: `UNOTEST_BOX_READ_TOKEN` is now read from `unotest/.secrets` as well as from the environment, with an exported value winning. This is what makes the `box_*` tools usable from an agent at all: an MCP server is started by the editor, so a token that exists only in a shell is a token the server never receives — the alternative was editing the editor's own JSON config and reconnecting the server. `unotest/.env` is deliberately not consulted: it travels inside a pushed bundle, while `.secrets` does not. The CLI resolves the token the same way, so one token serves both.
|
|
432
|
+
- 5266b30: `box run` and `box_run` now carry the `{tag}` of a failed soft step, in the JSON and in the printed line (`Rubric [judge-red]`, the shape a local run already prints). A data-driven test runs one `step.soft` over many cases, so its failures all share a label and the tag is the only thing that says which case failed — without it a remote reader saw "Rubric failed" three times and had to tell them apart by line number. The tag was in the run's journal all along; the summary dropped it.
|
|
433
|
+
- ee6bb51: Secrets injected by a box are masked. Masking works by value, and the registry of values was built from `unotest/.secrets` alone — a file that does not exist on a box, where the daemon passes the values as environment variables and names them in `UNOTEST_BOX_SECRET_NAMES`. The registry was therefore empty on every box run, and a password a scenario typed reached the run journal, the run's `stdout.log`, the viewer's System pane and anything an agent downloaded, in the clear. The runner now registers those values alongside the ones it reads from files, so masking no longer depends on where the run happens to be. Nothing changes on a developer's machine, where the variable is not set.
|
|
434
|
+
- 047ca68: Failure text no longer carries terminal colouring into files and replies.
|
|
435
|
+
|
|
436
|
+
Playwright paints its call log whenever the environment claims a terminal is
|
|
437
|
+
watching — `FORCE_COLOR`, which an MCP server inherits from whatever launched
|
|
438
|
+
it — and that message was copied verbatim into `steps.jsonl`, `runtime.json`
|
|
439
|
+
and the failure bundle. An agent reading the JSON got an escape sequence in
|
|
440
|
+
the middle of the sentence it was trying to parse, and the viewer rendered
|
|
441
|
+
it as a chewed-up word.
|
|
442
|
+
|
|
443
|
+
The colouring is dropped where a thrown error becomes our data, so every
|
|
444
|
+
reader of a failure gets the same clean text. Our own output is unaffected:
|
|
445
|
+
it paints at print time, which is where colour belongs.
|
|
446
|
+
|
|
447
|
+
The same on mobile: a failure's text in the run journal and in the report no
|
|
448
|
+
longer carries terminal escapes.
|
|
449
|
+
|
|
450
|
+
- 047ca68: A failure somebody paused on stays a failure, with the evidence to show for it.
|
|
451
|
+
|
|
452
|
+
A run driven through the debugger — `run_test` then `resume`, or the viewer's
|
|
453
|
+
Continue — reported `completed` after a failure it had paused on, wrote no
|
|
454
|
+
failure bundle and no `failure/` artifacts, and left `list_failures` with
|
|
455
|
+
nothing to show. Continuing past a failure is how it gets inspected; it was
|
|
456
|
+
never meant to retract it. The verdict is now decided where the run's own
|
|
457
|
+
events are seen, so the journal, `runtime.json` and the reply agree, and a
|
|
458
|
+
debug run leaves the same evidence a plain `unotest-web e2e` run does.
|
|
459
|
+
|
|
460
|
+
`abort_runtime` (and Stop, and SIGTERM) now also ends the run in
|
|
461
|
+
`runtime.json`, not only in `steps.jsonl`: the control file used to keep
|
|
462
|
+
saying `paused-step` about a run that was over, so anything reading it rather
|
|
463
|
+
than the journal saw a pause that never ended.
|
|
464
|
+
|
|
465
|
+
- c5d3cda: README links the documentation site instead of manuals that are not shipped in the package (`guides/manuals/*`), and says what a run on a box cannot reach: `localhost`, port-forwards, host tools, a judge service on your machine — with the recipe for the judge on a box.
|
|
466
|
+
- 442fadf: Secret values registered from `unotest/.secrets` are now masked in what the runner prints, not only in what it writes. The run journal and every failure artifact were redacted; the terminal line was not, and that line is also copied into the run's `stdout.log` / `stderr.log` and streamed to the viewer's System pane. Masking is applied once, where the logger is built, so child loggers and message arguments are covered too. The collection runner is covered by the same rule: its messages go through its logger rather than straight to the stream, and the logger it builds when a caller passes none reads the project's secrets the way the composition root's does.
|
|
467
|
+
- 047ca68: A `step.soft(...)` failure no longer stops a debug run — or goes missing from it.
|
|
468
|
+
|
|
469
|
+
Under a debugger (`run_test`, or `e2e --debug`), every soft failure raised
|
|
470
|
+
the debug wheel: the run stopped on each one and an agent had to `resume`
|
|
471
|
+
its way through them. Worse, it was then lost — a paused failure is consumed
|
|
472
|
+
where it paused, so it never reached the `step.soft` envelope that records
|
|
473
|
+
it, the envelope closed as if the block had passed, and a run with three
|
|
474
|
+
soft failures could finish green with none of them listed.
|
|
475
|
+
|
|
476
|
+
Pausing is now for a failure that ENDS the run. A failure under any
|
|
477
|
+
enclosing soft step is recorded and stepped over exactly as it is on the
|
|
478
|
+
command line, and `runtime.json` — which the `run_test` reply is built from —
|
|
479
|
+
carries every soft failure of the run, not just the last stop.
|
|
480
|
+
|
|
481
|
+
A hard failure still pauses: that is what the debugger is for.
|
|
482
|
+
|
|
483
|
+
The same holds on mobile: a `step.soft(...)` failure no longer stops a run
|
|
484
|
+
under the debugger, and every soft failure of a run is now part of its
|
|
485
|
+
runtime state instead of being lost at the pause.
|
|
486
|
+
|
|
487
|
+
- Updated dependencies [442fadf]
|
|
488
|
+
- Updated dependencies [442fadf]
|
|
489
|
+
- Updated dependencies [8aa0f30]
|
|
490
|
+
- Updated dependencies [8aa0f30]
|
|
491
|
+
- Updated dependencies [047ca68]
|
|
492
|
+
- Updated dependencies [047ca68]
|
|
493
|
+
- Updated dependencies [047ca68]
|
|
494
|
+
- @unotest/viewer@0.32.0
|
|
495
|
+
- @unotest/protocol@0.32.0
|
|
496
|
+
- @unotest/dsl@0.32.0
|
|
497
|
+
- @unotest/core@0.32.0
|
|
498
|
+
- @unotest/grounder-client@0.32.0
|
|
499
|
+
|
|
3
500
|
## [0.31.0] - 2026-09-04
|
|
4
501
|
|
|
5
502
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -7,6 +7,11 @@ and commit.**
|
|
|
7
7
|
MCP server + CLI runner + JavaScript DSL + semantic DOM snapshots +
|
|
8
8
|
structured failure bundles.
|
|
9
9
|
|
|
10
|
+
Full documentation — manuals, the box, CI, the judge — lives at
|
|
11
|
+
[docs.unotest.com](https://docs.unotest.com). This README is the short
|
|
12
|
+
tour; the package ships only `guides/dsl-reference.md` and
|
|
13
|
+
`guides/agent-integration.md` alongside it.
|
|
14
|
+
|
|
10
15
|
---
|
|
11
16
|
|
|
12
17
|
## 1. Setup
|
|
@@ -151,8 +156,11 @@ step. Secrets stay behind: `unotest/.env*` and `.secrets*` never travel,
|
|
|
151
156
|
because environment values belong to the environment and are injected
|
|
152
157
|
over the bundle when it runs. Install hooks (`postinstall`, `prepare`,
|
|
153
158
|
...) are removed from the packed `unotest/package.json`: a box installs
|
|
154
|
-
the suite's dependencies, it does not run a checkout's hooks.
|
|
155
|
-
install scripts
|
|
159
|
+
the suite's dependencies, it does not run a checkout's hooks. Your
|
|
160
|
+
DEPENDENCIES' install scripts do run — a native module builds or fetches
|
|
161
|
+
its binary as usual — because on a box that install happens in a
|
|
162
|
+
container of its own, with the bundle tree as its only writable mount
|
|
163
|
+
and none of the box's state in reach.
|
|
156
164
|
|
|
157
165
|
What would break on the box is refused here instead — a scenario that
|
|
158
166
|
reads a file outside `unotest/`, a missing `unotest/package-lock.json`,
|
|
@@ -170,7 +178,7 @@ a suite takes minutes, and a request held open that long is a timeout,
|
|
|
170
178
|
not a result. Watch them in the viewer, or let the box report the checks
|
|
171
179
|
back to GitHub. `--pr` makes a newer push withdraw the older runs of the
|
|
172
180
|
same pull request that are still waiting, so three pushes in five minutes
|
|
173
|
-
cost one suite. Recipe:
|
|
181
|
+
cost one suite. Recipe: [Pushing suites → A minimal CI job](https://docs.unotest.com/box/pushing-suites/#a-minimal-ci-job).
|
|
174
182
|
|
|
175
183
|
The values a suite runs with on the box — `APP_BASE_URL`, its `.env`
|
|
176
184
|
settings, its secrets — are sent separately, from the same files a local
|
|
@@ -184,7 +192,66 @@ printf '%s' "$KEY" | npx @unotest/web env set dev API_KEY --secret
|
|
|
184
192
|
`.env*` become the environment's variables, `.secrets*` its secrets,
|
|
185
193
|
`APP_BASE_URL` its target; `UNOTEST_*` stay home. A value never prints.
|
|
186
194
|
The token must be minted with `--values` on the box. Admins also see and
|
|
187
|
-
change them on the box's admin page. Manual:
|
|
195
|
+
change them on the box's admin page. Manual: [An environment's values on a
|
|
196
|
+
box](https://docs.unotest.com/box/environments-and-values/).
|
|
197
|
+
|
|
198
|
+
A run on the box happens inside the box's own container: nothing on your
|
|
199
|
+
machine is reachable from it — no `localhost`, no `kubectl port-forward`,
|
|
200
|
+
no tools installed on your laptop. Preconditions must probe the
|
|
201
|
+
environment's public URL, and `assertJudge` runs its judge in-process there
|
|
202
|
+
(`UNOTEST_JUDGE_MODE=local`, `@unotest/judge` in the suite's
|
|
203
|
+
`package.json`, the API key as a secret). Details:
|
|
204
|
+
[Judge on a box](https://docs.unotest.com/guides/judge/#on-a-box) and
|
|
205
|
+
[What a box cannot reach](https://docs.unotest.com/box/troubleshooting/#what-a-box-cannot-reach).
|
|
206
|
+
|
|
207
|
+
### Reading results from a box
|
|
208
|
+
|
|
209
|
+
`bundle push --run` gives you a run id. Read what became of it from the
|
|
210
|
+
same terminal — no browser, no shell on the box:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
npx @unotest/web box runs --env acme/test --latest
|
|
214
|
+
npx @unotest/web box run checkout-mqf3pwr1 --env acme/test
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
`box run` prints the failure, the results of soft steps and the judge's
|
|
218
|
+
verdicts. When you want the artifacts too:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
npx @unotest/web box run checkout-mqf3pwr1 --env acme/test --download
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
That saves the run's `*.unotest.zip` under `.unotest/box/` and unpacks its
|
|
225
|
+
failure bundle into `.unotest/failures/` — where `list_failures`,
|
|
226
|
+
`get_failure_*` and `agent_fix` already look, so your agent debugs a box
|
|
227
|
+
failure with the commands it uses for a local one. Add `--no-screenshots`
|
|
228
|
+
to leave the step frames on the box; they are usually most of the bytes.
|
|
229
|
+
|
|
230
|
+
Reading uses a **personal** read token, not the project one. Put it in
|
|
231
|
+
`unotest/.secrets`, next to your other project secrets:
|
|
232
|
+
|
|
233
|
+
```sh
|
|
234
|
+
UNOTEST_BOX_READ_TOKEN=ubr_...
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
and the box's address in `unotest/.env` as `UNOTEST_BOX_URL`. Both files
|
|
238
|
+
are read directly, so this works the same from the terminal and from your
|
|
239
|
+
agent. Exporting either value in your shell works too and wins over the
|
|
240
|
+
file; `--box` and `--token` win over both.
|
|
241
|
+
|
|
242
|
+
The token is never read from `unotest/.env` — that file travels inside a
|
|
243
|
+
pushed bundle, and `.secrets` does not.
|
|
244
|
+
|
|
245
|
+
Keeping it in the file is what lets your agent read runs: an MCP server is
|
|
246
|
+
started by your editor, so a token that only exists in your shell is a
|
|
247
|
+
token the agent never sees. A token added while the server is running is
|
|
248
|
+
picked up on the next call — nothing to restart.
|
|
249
|
+
|
|
250
|
+
You mint it for yourself on the box's guard ("Read tokens"), it is shown
|
|
251
|
+
once, and it is revocable. It is read-only whatever your role on the box — it cannot start
|
|
252
|
+
a run or change a value. `UNOTEST_BOX_TOKEN` is the project's push
|
|
253
|
+
credential and cannot read runs. `box envs` lists the environments a token
|
|
254
|
+
may look at.
|
|
188
255
|
|
|
189
256
|
## 5. Watch it run — the viewer
|
|
190
257
|
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -30,10 +30,10 @@ declare const RetryConfigSchema: z.ZodObject<{
|
|
|
30
30
|
on: z.ZodArray<z.ZodEnum<["transient", "network", "crash"]>, "many">;
|
|
31
31
|
}, "strip", z.ZodTypeAny, {
|
|
32
32
|
count: number;
|
|
33
|
-
on: ("
|
|
33
|
+
on: ("network" | "transient" | "crash")[];
|
|
34
34
|
}, {
|
|
35
35
|
count: number;
|
|
36
|
-
on: ("
|
|
36
|
+
on: ("network" | "transient" | "crash")[];
|
|
37
37
|
}>;
|
|
38
38
|
type RetryConfig = z.infer<typeof RetryConfigSchema>;
|
|
39
39
|
declare const FailureBundleConfigSchema: z.ZodObject<{
|
|
@@ -276,10 +276,10 @@ declare const UnotestConfigSchema: z.ZodObject<{
|
|
|
276
276
|
on: z.ZodArray<z.ZodEnum<["transient", "network", "crash"]>, "many">;
|
|
277
277
|
}, "strip", z.ZodTypeAny, {
|
|
278
278
|
count: number;
|
|
279
|
-
on: ("
|
|
279
|
+
on: ("network" | "transient" | "crash")[];
|
|
280
280
|
}, {
|
|
281
281
|
count: number;
|
|
282
|
-
on: ("
|
|
282
|
+
on: ("network" | "transient" | "crash")[];
|
|
283
283
|
}>;
|
|
284
284
|
failureBundle: z.ZodObject<{
|
|
285
285
|
/** Tier 1 is always on by D-16. Schema lock — not user-toggleable. */
|
|
@@ -461,7 +461,7 @@ declare const UnotestConfigSchema: z.ZodObject<{
|
|
|
461
461
|
};
|
|
462
462
|
retry: {
|
|
463
463
|
count: number;
|
|
464
|
-
on: ("
|
|
464
|
+
on: ("network" | "transient" | "crash")[];
|
|
465
465
|
};
|
|
466
466
|
failureBundle: {
|
|
467
467
|
tier1: true;
|
|
@@ -523,7 +523,7 @@ declare const UnotestConfigSchema: z.ZodObject<{
|
|
|
523
523
|
};
|
|
524
524
|
retry: {
|
|
525
525
|
count: number;
|
|
526
|
-
on: ("
|
|
526
|
+
on: ("network" | "transient" | "crash")[];
|
|
527
527
|
};
|
|
528
528
|
failureBundle: {
|
|
529
529
|
tier2: boolean;
|