@unotest/web 0.40.0 → 0.42.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 +432 -3
- package/README.md +50 -4
- package/dist/config/schema.d.ts +29 -8
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.d.ts +2 -2
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.d.ts +2 -2
- package/dist/dsl/index.js +1 -1
- package/dist/dsl/web-dsl-language-service.js +1 -1
- package/dist/inspection/page-inject.d.ts +1 -1
- package/dist/{interfaces-B34N4rfI.d.ts → interfaces-7nMNCr2h.d.ts} +6 -0
- package/dist/mcp/server.js +1 -1
- package/dist/runner/cli.js +1 -1
- package/dist/runner/init/chrome-probe.js +1 -1
- package/dist/runner/init.d.ts +4 -0
- 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 +4 -1
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,434 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.42.0] - 2026-09-24
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8f319b2: `box status` now reports the box's version: what it runs, what its channel
|
|
8
|
+
offers and where it is between the two.
|
|
9
|
+
|
|
10
|
+
A box never updates itself. A new release is offered, not installed — the
|
|
11
|
+
box keeps running the version it is on until somebody presses **Update** in
|
|
12
|
+
the cabinet, or an agent calls `POST /api/v1/boxes/<name>/update` with the
|
|
13
|
+
version it was offered. `box status` prints the offer and says where the
|
|
14
|
+
decision is made; a cloud that reports no versions leaves the CLI silent
|
|
15
|
+
about them rather than guessing.
|
|
16
|
+
|
|
17
|
+
Migration: boxes that used to install a new release on their own stop doing so
|
|
18
|
+
with this release. To move a box to a newer version, press **Update** on its
|
|
19
|
+
card in the cabinet, or call the endpoint above.
|
|
20
|
+
|
|
21
|
+
- 0c56c6d: Start the browser with a profile and extra arguments — what testing a browser extension needs
|
|
22
|
+
|
|
23
|
+
A new `launch` block in `unotest.config`:
|
|
24
|
+
|
|
25
|
+
- `launch.args` — extra browser command-line arguments, e.g.
|
|
26
|
+
`--load-extension=./ext` (extensions load in Chromium only);
|
|
27
|
+
- `launch.userDataDir` — a browser profile directory. The browser then starts
|
|
28
|
+
persistent: cookies, storage and installed extensions live in the profile.
|
|
29
|
+
`UNOTEST_USER_DATA_DIR` overrides it for a machine.
|
|
30
|
+
|
|
31
|
+
On a profile there is **no isolation between scenarios**: the browser has one
|
|
32
|
+
context, and every scenario of a run gets that same context. Each such run says
|
|
33
|
+
so in its log.
|
|
34
|
+
|
|
35
|
+
Refused with a message instead of being ignored: a profile together with
|
|
36
|
+
`storageState`, a profile in a collection of more than one worker, `launch`
|
|
37
|
+
on a box (its runs take no launch settings from the suite), and an extension
|
|
38
|
+
(`--load-extension`) on a headless run of bundled Chromium, whose headless
|
|
39
|
+
shell ignores extensions without an error. For headless runs with an
|
|
40
|
+
extension — CI — set the new `channel: "chromium"` (Chromium's new headless
|
|
41
|
+
mode, which loads them), or `UNOTEST_BROWSER_CHANNEL=chromium`; locally, run
|
|
42
|
+
headed. Without `launch` nothing changes.
|
|
43
|
+
|
|
44
|
+
- 1f1dffb: `box envs` and `box run` show how a box installed the suite: how long it
|
|
45
|
+
took, and whether the packages came from the registry or the box's own
|
|
46
|
+
npm cache.
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
acme/test main@1a2b3c4
|
|
50
|
+
installed in 12.3s from this box's cache (252 of 252 from cache)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The source is measured by the box, not read out of npm's output: before
|
|
54
|
+
an install it compares what the lockfile names against what its cache
|
|
55
|
+
already holds. On a fast registry a cold install and a warm one take the
|
|
56
|
+
same number of seconds, so the duration alone never said which had
|
|
57
|
+
happened — the counts are printed with the word so it can be checked.
|
|
58
|
+
`box run <id>` prints the same line for the bundle that run came from,
|
|
59
|
+
while the environment still serves it.
|
|
60
|
+
|
|
61
|
+
A box older than this contract says nothing about installs, and the
|
|
62
|
+
output is exactly what it was.
|
|
63
|
+
|
|
64
|
+
- 7958c4a: Recording tools refuse a misspelled field instead of silently not recording
|
|
65
|
+
|
|
66
|
+
`explore_step` with `exploration_id` (instead of `explorationId`) used to run the
|
|
67
|
+
step ad hoc, answer success and record nothing — the empty session only showed
|
|
68
|
+
up at `explore_stop`. Every `explore_*` tool, `generate_dsl_from_exploration`
|
|
69
|
+
and `save_exploration_as_test` now refuse a field they do not declare, including
|
|
70
|
+
inside an `explore_steps` step, and the error names the field they accept
|
|
71
|
+
(`exploration_id (did you mean explorationId?)`).
|
|
72
|
+
|
|
73
|
+
Ad-hoc execution is unchanged: omit `explorationId`, and the step's reply says
|
|
74
|
+
`recorded: false`.
|
|
75
|
+
|
|
76
|
+
Migration: a client that sent extra, ignored fields to these tools now gets an
|
|
77
|
+
input error listing the accepted fields — drop the extra ones.
|
|
78
|
+
|
|
79
|
+
- 92b446f: `box status` says until when the balance keeps a box, not when its paid hour ends
|
|
80
|
+
|
|
81
|
+
`box status` used to print `paid until` — the end of the hour already bought.
|
|
82
|
+
With credit left that read as "switched off within the hour". It now prints
|
|
83
|
+
`funded until <date> UTC`: the moment the box is deleted if nobody tops up,
|
|
84
|
+
computed by the cloud from the balance and every box on the account, each at
|
|
85
|
+
its own rate. A failed box prints neither.
|
|
86
|
+
|
|
87
|
+
The cloud API adds `fundedUntil` to each billed box and to `GET /balance` (absent
|
|
88
|
+
when nothing is billed); `paidUntil` stays, and means only the end of the paid
|
|
89
|
+
hour. The cabinet's box card and balance show the same date, and the low-balance
|
|
90
|
+
letter names it.
|
|
91
|
+
|
|
92
|
+
Migration: `box status` no longer prints a `paid until` line. A script that read
|
|
93
|
+
it should take `fundedUntil` from `box status --json` for the deadline, or
|
|
94
|
+
`paidUntil` there if it meant the end of the paid hour.
|
|
95
|
+
|
|
96
|
+
- c99f849: The viewer on a box has no terminal panel instead of a dead one
|
|
97
|
+
|
|
98
|
+
A box's viewer could never start its docked terminal — the PTY addon is not in
|
|
99
|
+
the box image — yet it showed the panel and the button. A host that cannot give
|
|
100
|
+
the terminal a PTY now says so with `UNOTEST_VIEWER_TERMINAL=off`, and the viewer
|
|
101
|
+
then leaves the terminal out entirely: no panel, no button, no terminal socket,
|
|
102
|
+
for every session. A box sets it for its viewers.
|
|
103
|
+
|
|
104
|
+
Nothing changes locally: the terminal is on by default, and a machine missing
|
|
105
|
+
the addon still reports it when the panel starts.
|
|
106
|
+
|
|
107
|
+
### Patch Changes
|
|
108
|
+
|
|
109
|
+
- 784c10d: A pressed Update reaches the box within minutes, and a failed one is not retried on its own
|
|
110
|
+
|
|
111
|
+
A box now checks every five minutes whether it has been asked to move to
|
|
112
|
+
another version, instead of once an hour, so pressing **Update** in the cabinet
|
|
113
|
+
(or calling the update endpoint) takes effect within minutes.
|
|
114
|
+
|
|
115
|
+
The box also reports back what it runs and how its last update ended —
|
|
116
|
+
applied, rolled back, refused or failed, and why — which is what the cabinet
|
|
117
|
+
and `box status` show.
|
|
118
|
+
|
|
119
|
+
An update that did not succeed is not tried again by the box itself: rolling
|
|
120
|
+
back takes the box's stack down and up again, and repeating that unasked would
|
|
121
|
+
be an automatic update. It is tried again when **Update** is pressed again.
|
|
122
|
+
|
|
123
|
+
- e0b822a: A reinstall no longer empties the tree a viewer is serving
|
|
124
|
+
|
|
125
|
+
A bundle whose tree already has packages is reinstalled in a copy while the
|
|
126
|
+
live tree waits aside; the viewer keeps serving it. A failed reinstall leaves
|
|
127
|
+
the live `node_modules` untouched instead of leaving the viewer answering 404
|
|
128
|
+
until an install succeeds. A tree a daemon left aside when it died mid-install
|
|
129
|
+
is put back by the next housekeeping sweep.
|
|
130
|
+
|
|
131
|
+
- 96e4977: A box that names no environments is told so, instead of being sent a name
|
|
132
|
+
it cannot match.
|
|
133
|
+
|
|
134
|
+
`box runs`, `box queue`, `box run` and the rest used to pass the name on to
|
|
135
|
+
a box with an empty list, which answered 404 and advice about the form of a
|
|
136
|
+
name — spelling advice to somebody whose box has nothing to spell. They now
|
|
137
|
+
say what is true: the box names no environments yet, a run ordered a moment
|
|
138
|
+
ago may not be listed straight away, and a box that goes on naming none has
|
|
139
|
+
none configured. Agents get the same sentence through the `box_*` MCP tools.
|
|
140
|
+
|
|
141
|
+
The command exits 2, like the other refusals of an environment name (was 1).
|
|
142
|
+
|
|
143
|
+
Migration: a script that checked for exit code 1 on this refusal should check
|
|
144
|
+
for 2.
|
|
145
|
+
|
|
146
|
+
`bundle push --run` is unchanged: it uploads and lets the box decide, because
|
|
147
|
+
a box older than this contract names nothing until its first run. What
|
|
148
|
+
changed there is the hint after the box's own refusal, which no longer points
|
|
149
|
+
back at the list that was empty.
|
|
150
|
+
|
|
151
|
+
- d1ecff6: A box lists its environments before the first suite arrives
|
|
152
|
+
|
|
153
|
+
The route table names every configured environment; one without a bundle is
|
|
154
|
+
listed without an address. `/_guard/api/envs` answers with it at once
|
|
155
|
+
(`note: "no suite yet"`), so an environment name can be checked before the
|
|
156
|
+
first push, and opening it in a browser shows a page with the commands that
|
|
157
|
+
send a suite instead of a bare 503.
|
|
158
|
+
|
|
159
|
+
Migration: the environment list a read token gets (`box envs`, the `box_envs`
|
|
160
|
+
MCP tool) now also names environments that have no suite yet, with the note
|
|
161
|
+
`no suite yet`. Reading one of them answers `unavailable` until a suite is pushed.
|
|
162
|
+
|
|
163
|
+
- Updated dependencies [c99f849]
|
|
164
|
+
- @unotest/viewer@0.42.0
|
|
165
|
+
- @unotest/core@0.42.0
|
|
166
|
+
- @unotest/dsl@0.42.0
|
|
167
|
+
- @unotest/grounder-client@0.42.0
|
|
168
|
+
- @unotest/protocol@0.42.0
|
|
169
|
+
|
|
170
|
+
## [0.41.0] - 2026-09-21
|
|
171
|
+
|
|
172
|
+
### Minor Changes
|
|
173
|
+
|
|
174
|
+
- e793fa2: `env push | set | rm`: the box and the token come from the project's own files
|
|
175
|
+
|
|
176
|
+
The values commands now resolve the box address and the project token
|
|
177
|
+
exactly the way `bundle push` does — a flag, then `UNOTEST_BOX_URL` /
|
|
178
|
+
`UNOTEST_BOX_TOKEN`, then `unotest/.env` and `unotest/.secrets`, where
|
|
179
|
+
`box create` and `box access` wrote them. A project set up with either of
|
|
180
|
+
those commands no longer answers "no box address" to `env push` while
|
|
181
|
+
`bundle push` works, and the two exports stop being typed in front of
|
|
182
|
+
every line.
|
|
183
|
+
|
|
184
|
+
`env set <env> <NAME> <value>` takes the value as an argument. Stdin
|
|
185
|
+
still works when there is none, and stays the way to pass a credential:
|
|
186
|
+
an argument is in the shell's history.
|
|
187
|
+
|
|
188
|
+
`env push --dry-run` now prints what the overlays resolved to, quoted, so
|
|
189
|
+
a value that kept its quotes or picked up a trailing space is visible
|
|
190
|
+
before it reaches a box. `--dry-run --json` carries the same under
|
|
191
|
+
`values`. A secret's value is still never printed: where it would be, the
|
|
192
|
+
mask used everywhere else (`‹secret:NAME›`) stands instead.
|
|
193
|
+
|
|
194
|
+
A push whose box lacks some of the suite's declared values no longer
|
|
195
|
+
claims the run "will stop at the first step that reads" one of them: the
|
|
196
|
+
names are what the suite DECLARES, and a declared name no scenario reads
|
|
197
|
+
costs a run nothing.
|
|
198
|
+
|
|
199
|
+
- 41b0369: A box rented from the dashboard now says how to send it a suite
|
|
200
|
+
|
|
201
|
+
A box rented from the CLI arrives with the suite already on it: `box create`
|
|
202
|
+
pushes it and prints the command that runs it. A box rented from the dashboard
|
|
203
|
+
arrives empty, and the card said nothing about that. It showed the address, the
|
|
204
|
+
login and the password, and left the customer with a machine that answers, an
|
|
205
|
+
empty viewer, and no idea that anything else was needed. Everything they had to
|
|
206
|
+
do next lived in an email and on the box's own empty screen.
|
|
207
|
+
|
|
208
|
+
The card of a ready box now carries the three commands that take a project from
|
|
209
|
+
here to a first run: sign in, take the box's tokens, push the suite. Each is a
|
|
210
|
+
field that can be copied in one click, with the box's own name already in it.
|
|
211
|
+
The environment name is filled in when the box has exactly one, and left as
|
|
212
|
+
`<env>` when it has several — picking one for the customer would send the run
|
|
213
|
+
to a site they did not mean, and nothing would refuse it, because the name is
|
|
214
|
+
perfectly valid. The collection is named too: a box runs a collection, not a
|
|
215
|
+
single scenario, and a push that names none is turned away.
|
|
216
|
+
|
|
217
|
+
Those commands are now written in one place and printed by both the card and
|
|
218
|
+
the "your box is ready" letter. They used to be the letter's alone, and a
|
|
219
|
+
second copy on the card would have been a second piece of advice the first time
|
|
220
|
+
either was edited.
|
|
221
|
+
|
|
222
|
+
Once a project has taken the box's tokens, the card stops repeating the first
|
|
223
|
+
two commands and asks only for the push. That is the one thing the cloud can
|
|
224
|
+
honestly know about the far end of a box: the suite travels from the customer's
|
|
225
|
+
machine straight to it, and nothing reports back. So the card says nothing at
|
|
226
|
+
all about runs — not "no runs yet", not "0 runs". It cannot tell a box that has
|
|
227
|
+
been testing for a week from one nobody has touched, and a page that says
|
|
228
|
+
otherwise is inventing it.
|
|
229
|
+
|
|
230
|
+
### Patch Changes
|
|
231
|
+
|
|
232
|
+
- 36bc4b3: An empty list of environments says which kind of empty it is
|
|
233
|
+
|
|
234
|
+
`box envs` on a box that listed nothing said "This box serves no environments
|
|
235
|
+
you can read." — a sentence about the token, on a list that is not filtered by
|
|
236
|
+
the token at all. People whose box was simply new went looking for a stronger
|
|
237
|
+
credential, and the box's own journal had nothing to say either way.
|
|
238
|
+
|
|
239
|
+
The list now talks about the box:
|
|
240
|
+
|
|
241
|
+
This box serves no environments yet.
|
|
242
|
+
An environment appears once a suite has been pushed to it.
|
|
243
|
+
|
|
244
|
+
A token that really is not let in still answers in its own words, with its own
|
|
245
|
+
exit code, so the two are told apart by reading them.
|
|
246
|
+
|
|
247
|
+
Behind the list, the box now records which of five things happened when it
|
|
248
|
+
looked for its route table: there is no table yet, the table cannot be read,
|
|
249
|
+
it is not JSON, its shape is wrong, or it is honestly empty. Each is one line
|
|
250
|
+
in the box's journal, naming the file and never its contents — the table holds
|
|
251
|
+
internal addresses. A table that loaded says how many environments it found,
|
|
252
|
+
because "none" and "three" were otherwise the same silence.
|
|
253
|
+
|
|
254
|
+
Two fixes come with it. A line belongs to a state rather than to a request, so
|
|
255
|
+
a busy box writes one line and not one per proxied asset. And a table that
|
|
256
|
+
failed to parse is read again a second later instead of being remembered as an
|
|
257
|
+
empty box until its timestamp changes — which is why "I tried again and it was
|
|
258
|
+
the same" used to be true.
|
|
259
|
+
|
|
260
|
+
An empty list stays a successful answer, `200` with no entries: a box with
|
|
261
|
+
nothing on it yet is a normal box, not a service that is down.
|
|
262
|
+
|
|
263
|
+
- 7bac819: A box installs a suite whose `npm ci` is slow, and stops retrying in a loop
|
|
264
|
+
|
|
265
|
+
A box could not install a suite whose dependency install went quiet for
|
|
266
|
+
more than five minutes — which is what a heavy lockfile does. The install
|
|
267
|
+
died on the 301st second, reported the runner as unavailable, and the box
|
|
268
|
+
started the whole install again in the second it failed. That repeated for
|
|
269
|
+
as long as the box existed: nothing ever ran on it, and the message blamed
|
|
270
|
+
the network rather than naming the timeout that caused it.
|
|
271
|
+
|
|
272
|
+
Three things change. The daemon no longer puts a ceiling of its own on
|
|
273
|
+
reading a run's output, so silence is not a failure; the run's deadline
|
|
274
|
+
remains the only clock that ends a run, and a backstop behind it covers a
|
|
275
|
+
connection that stops answering altogether. A run whose output stops
|
|
276
|
+
without an exit now takes its container with it — abandoned containers
|
|
277
|
+
used to fill the box's capacity while healthy environments waited for a
|
|
278
|
+
slot. And a failed install is followed by a pause that grows: 1, 2, 5, 15
|
|
279
|
+
and 30 minutes, capped there, never "never". Asking for that bundle
|
|
280
|
+
yourself — a switch or a repair — is tried at once and clears the count,
|
|
281
|
+
as does an install that succeeds, and the log says when the box will try
|
|
282
|
+
next by itself.
|
|
283
|
+
|
|
284
|
+
The pause is the box's own, never yours: a run you order by hand, and a run
|
|
285
|
+
a push to your repository orders, install the bundle at once and clear the
|
|
286
|
+
count. Only the box's unattended retry waits.
|
|
287
|
+
|
|
288
|
+
- 7880b7d: Installs on a box share one npm cache, and a suite can never reach it
|
|
289
|
+
|
|
290
|
+
Until now every install on a box started with an empty npm cache: the
|
|
291
|
+
container that runs `npm ci` was thrown away with everything it had
|
|
292
|
+
downloaded, so the second box of the same account — and the same box
|
|
293
|
+
after a restart — paid the full download again for dependencies it had
|
|
294
|
+
already had twice.
|
|
295
|
+
|
|
296
|
+
Boxes now keep one npm cache and every install uses it. The second
|
|
297
|
+
install of a dependency is taken from disk instead of the network; how
|
|
298
|
+
much time that saves depends on your lockfile and your registry, and on a
|
|
299
|
+
fast registry it can be none at all — the promise here is only that
|
|
300
|
+
what has already been downloaded is not downloaded again.
|
|
301
|
+
|
|
302
|
+
The cache is mounted into the container that installs, and into no other.
|
|
303
|
+
A container running your suite gets no access to it under any name: the
|
|
304
|
+
cache is the directory that every later install of every project on that
|
|
305
|
+
box unpacks, and a test that could write into it would be writing into
|
|
306
|
+
the next install. That boundary is asserted as a pair — the install has
|
|
307
|
+
it, the run does not — rather than as two separate claims.
|
|
308
|
+
|
|
309
|
+
A substituted cache entry is never installed. npm verifies an entry's
|
|
310
|
+
checksum when it READS from the cache, not only when it writes one: an
|
|
311
|
+
entry whose content does not match is reported as corrupted and fetched
|
|
312
|
+
again (verified on the box's own image, npm 11.16.0 — with a registry
|
|
313
|
+
reachable the correct package is installed; without one the install fails
|
|
314
|
+
rather than using the substitute).
|
|
315
|
+
|
|
316
|
+
The box keeps the cache's size in hand. It is measured on each
|
|
317
|
+
housekeeping sweep and, over the ceiling (1 GB by default, and an
|
|
318
|
+
operator can raise it or turn it off), emptied whole with a line in the
|
|
319
|
+
log — losing a cache costs a download and nothing else.
|
|
320
|
+
|
|
321
|
+
- d456001: Ordering a run in an environment now decides what that environment serves
|
|
322
|
+
|
|
323
|
+
Pushing a suite and asking for a run in a named environment is a
|
|
324
|
+
statement about that environment: it should serve the bundle you just
|
|
325
|
+
sent. Until now the box decided that separately, by matching the
|
|
326
|
+
bundle's branch — so a suite packed outside a git repository, which is
|
|
327
|
+
what `init` produces in a clean directory, matched nothing. The run
|
|
328
|
+
installed, executed and passed, while the environment kept pointing at
|
|
329
|
+
nothing: its viewer was never started, the guard was given no route for
|
|
330
|
+
it, `box envs` listed nothing, and `box runs --env <name>` answered that
|
|
331
|
+
the box does not serve that environment. Four different answers to one
|
|
332
|
+
question, after the client had been told the suite was accepted and
|
|
333
|
+
queued.
|
|
334
|
+
|
|
335
|
+
An explicit run request now selects the bundle for the environment,
|
|
336
|
+
through the same path a selection made in the viewer takes, recorded
|
|
337
|
+
against the token that pushed — so it survives a restart and the box
|
|
338
|
+
does not silently move the environment back on its next sweep. Branch
|
|
339
|
+
matching keeps deciding exactly one thing, the one it is right for:
|
|
340
|
+
which bundle the environment's SCHEDULES run.
|
|
341
|
+
|
|
342
|
+
An environment that may not be switched (`allowSwitch: false`) keeps
|
|
343
|
+
serving what its own branch deploys, and the answer now says so instead
|
|
344
|
+
of leaving it to be found out: the run goes ahead there, and the
|
|
345
|
+
acceptance names the branch the environment stays on — so it is clear
|
|
346
|
+
the result belongs to that environment's journal rather than to the
|
|
347
|
+
screen the environment shows.
|
|
348
|
+
|
|
349
|
+
The one case with nothing to show is refused out loud. An environment
|
|
350
|
+
that may not be switched and serves nothing yet has no viewer at all, so
|
|
351
|
+
a run ordered there could only end where this bug ended: executed, green
|
|
352
|
+
and invisible. The refusal names the branch to deploy to instead.
|
|
353
|
+
|
|
354
|
+
Migration note: that refusal is a new error code on the run endpoint.
|
|
355
|
+
A CLI older than this release does not know it and will report the
|
|
356
|
+
box's answer as "not a box" rather than as a refusal — the request is
|
|
357
|
+
still refused, and nothing runs. Upgrade the CLI to see the message.
|
|
358
|
+
The acceptance note is a new field on an otherwise unchanged answer, and
|
|
359
|
+
an older CLI ignores it.
|
|
360
|
+
|
|
361
|
+
- 436b683: The box's own screen gives the same first-run command as everything else
|
|
362
|
+
|
|
363
|
+
A freshly rented box shows two commands: one that points the project at the
|
|
364
|
+
box, one that sends the suite and starts it running. The second was printed
|
|
365
|
+
without `--collection`, and a box runs a collection or a schedule — never a
|
|
366
|
+
scenario by name. Copied exactly as shown, into a project straight out of
|
|
367
|
+
`init`, it came back refused with nothing to run: an answer about a project
|
|
368
|
+
that had done nothing wrong, from the first screen a new box owner reads.
|
|
369
|
+
|
|
370
|
+
The screen now prints the whole command, and says where collections live:
|
|
371
|
+
|
|
372
|
+
npx @unotest/web bundle push --run --env ENVIRONMENT --collection examples
|
|
373
|
+
|
|
374
|
+
The command is no longer written out on each surface that shows it. The box's
|
|
375
|
+
screen, the last line of `box create`, and the collection `init` writes all
|
|
376
|
+
come from one place, so the next flag to change changes in one edit rather
|
|
377
|
+
than in three. `examples` is the collection `init` creates, and the screen
|
|
378
|
+
says so — it is an example of a name, not a claim about your project.
|
|
379
|
+
|
|
380
|
+
The cabinet half is unchanged in behaviour: with exactly one environment and
|
|
381
|
+
one collection it still prints a command that can be pasted as is, and
|
|
382
|
+
placeholders where it cannot know.
|
|
383
|
+
|
|
384
|
+
- 3d8e45d: `unotest/.secrets` is now created owner-only (`0600`)
|
|
385
|
+
|
|
386
|
+
The file holds the box push and read tokens, and was created with the
|
|
387
|
+
default umask — on a shared machine any other user could read it. It is
|
|
388
|
+
now narrow from the moment it is created, and a file left wider by an
|
|
389
|
+
earlier version is narrowed the next time a command writes to it, with one
|
|
390
|
+
line saying what changed. `unotest/.env` is configuration rather than
|
|
391
|
+
secrets and keeps the width your project gave it.
|
|
392
|
+
|
|
393
|
+
- 8a436b9: Exploration tools refuse an unknown `{{NAME}}` instead of typing it
|
|
394
|
+
|
|
395
|
+
A `{{NAME}}` the session could not resolve was passed through as text, so
|
|
396
|
+
`explore_run_flow {args:["admin","{{PASSWORD}}"]}` typed the placeholder
|
|
397
|
+
into the password field and the page answered with a normal login failure.
|
|
398
|
+
It now fails with a message that names the variable, says where to declare
|
|
399
|
+
it, and lists the names the session does have — so the value never has to
|
|
400
|
+
be pasted into the tool call by hand.
|
|
401
|
+
|
|
402
|
+
A bare `SHOUTY_NAME` is unchanged: a reference where it is registered, a
|
|
403
|
+
literal otherwise. A variable declared with an empty value still
|
|
404
|
+
substitutes as empty and is reported in `variableWarnings`; a name
|
|
405
|
+
declared as a secret with an empty value is refused with its own message.
|
|
406
|
+
|
|
407
|
+
- a1b9eac: Editing a secret in the viewer's variables panel keeps `unotest/.secrets` owner-only
|
|
408
|
+
|
|
409
|
+
The panel writes a file by creating a temporary one beside it and renaming
|
|
410
|
+
it into place, and a rename replaces the target together with its
|
|
411
|
+
permissions. So a secrets file that was owner-only became world-readable
|
|
412
|
+
again after a single edit in the panel, with the box tokens inside it, and
|
|
413
|
+
adding the first secret to a project created the file wide in the first
|
|
414
|
+
place.
|
|
415
|
+
|
|
416
|
+
The temporary file is now created owner-only when the layer being written
|
|
417
|
+
holds secrets, which also narrows a file an earlier version left wide. A
|
|
418
|
+
leftover temporary file from an interrupted write is removed rather than
|
|
419
|
+
reused, so it cannot pass its own permissions on. `unotest/.env` is
|
|
420
|
+
configuration rather than secrets and keeps the width your project gave it.
|
|
421
|
+
On Windows there are no POSIX permission bits and nothing is narrowed
|
|
422
|
+
there.
|
|
423
|
+
|
|
424
|
+
- Updated dependencies [436b683]
|
|
425
|
+
- Updated dependencies [a1b9eac]
|
|
426
|
+
- @unotest/protocol@0.41.0
|
|
427
|
+
- @unotest/viewer@0.41.0
|
|
428
|
+
- @unotest/core@0.41.0
|
|
429
|
+
- @unotest/dsl@0.41.0
|
|
430
|
+
- @unotest/grounder-client@0.41.0
|
|
431
|
+
|
|
3
432
|
## [0.40.0] - 2026-09-19
|
|
4
433
|
|
|
5
434
|
### Minor Changes
|
|
@@ -389,7 +818,7 @@ picker on` fetches this project's own access and writes the three grounder
|
|
|
389
818
|
|
|
390
819
|
That screen named one command:
|
|
391
820
|
|
|
392
|
-
|
|
821
|
+
npx @unotest/web bundle push --run --env ENVIRONMENT
|
|
393
822
|
|
|
394
823
|
In a project that has just been created, the command refuses with `no box
|
|
395
824
|
address` before it does anything: the project holds neither the box's address
|
|
@@ -398,8 +827,8 @@ address` before it does anything: the project holds neither the box's address
|
|
|
398
827
|
|
|
399
828
|
The screen now names both steps, in the order they have to happen:
|
|
400
829
|
|
|
401
|
-
|
|
402
|
-
|
|
830
|
+
npx @unotest/web box access larch
|
|
831
|
+
npx @unotest/web bundle push --run --env ENVIRONMENT
|
|
403
832
|
|
|
404
833
|
The box's own name is filled in when its licence carries one. `box access`
|
|
405
834
|
with no name refuses, and its refusal offers to create a box — to somebody who
|
package/README.md
CHANGED
|
@@ -34,6 +34,11 @@ self-installs on the first `lint` / `e2e` run after upgrading; refresh by
|
|
|
34
34
|
hand with `npx @unotest/web types`. Details: the "Editor setup" section of
|
|
35
35
|
`node_modules/@unotest/web/guides/dsl-reference.md`.
|
|
36
36
|
|
|
37
|
+
Testing a browser extension? `launch` in `unotest.config` starts the browser
|
|
38
|
+
with a profile (`userDataDir`) and extra arguments (`args`, e.g.
|
|
39
|
+
`--load-extension=./ext`; Chromium only). On a profile every scenario shares
|
|
40
|
+
one browser context — no isolation between them, and the run log says so.
|
|
41
|
+
|
|
37
42
|
## 2. Open the viewer
|
|
38
43
|
|
|
39
44
|
```sh
|
|
@@ -182,6 +187,27 @@ Picker is on — which models the cloud grounds with: `Our GPU` or
|
|
|
182
187
|
`UNOTEST_GROUNDER_MODE`; a cloud that has not heard from its grounder
|
|
183
188
|
prints nothing rather than a guess.
|
|
184
189
|
|
|
190
|
+
**How a suite got installed is visible too.** `box envs` prints, under
|
|
191
|
+
each environment, how long the box took to install its bundle and where
|
|
192
|
+
the packages came from — `installed in 12.3s from this box's cache (252
|
|
193
|
+
of 252 from cache)`. `box run <id>` prints the same line for the bundle
|
|
194
|
+
that run came from, while the environment still serves it. The source is
|
|
195
|
+
the box's own measurement (what its npm cache already held of what the
|
|
196
|
+
lockfile names), not a reading of npm's output: on a fast registry a cold
|
|
197
|
+
install and a warm one take the same number of seconds, so the time alone
|
|
198
|
+
proves nothing. A box that says nothing about installs — one older than
|
|
199
|
+
this — prints nothing extra.
|
|
200
|
+
|
|
201
|
+
**A box never updates itself.** `box status` prints its version — what it
|
|
202
|
+
runs, and what its channel offers when the two differ. A new release is an
|
|
203
|
+
offer: the box keeps running what it is on until somebody presses **Update**
|
|
204
|
+
on its card in the cabinet, or an agent calls
|
|
205
|
+
`POST /api/v1/boxes/<name>/update` with the version it was offered (a
|
|
206
|
+
version that has since been overtaken is refused, with the one that is on
|
|
207
|
+
offer now). There is no `box update` command: the decision is a person's,
|
|
208
|
+
and the cabinet is where it is made. A cloud that reports no versions leaves
|
|
209
|
+
`box status` silent about them.
|
|
210
|
+
|
|
185
211
|
**Without a box.** An account can have the Picker on with no box rented
|
|
186
212
|
at all; the switch and the price for that live in the cabinet, and
|
|
187
213
|
`picker on` connects THIS project to it — it fetches the project's own
|
|
@@ -253,13 +279,18 @@ run reads:
|
|
|
253
279
|
|
|
254
280
|
```sh
|
|
255
281
|
npx @unotest/web env push dev # .env + .env.dev + .secrets + .secrets.dev
|
|
282
|
+
npx @unotest/web env set dev REGION eu-west
|
|
256
283
|
printf '%s' "$KEY" | npx @unotest/web env set dev API_KEY --secret
|
|
257
284
|
```
|
|
258
285
|
|
|
259
286
|
`.env*` become the environment's variables, `.secrets*` its secrets,
|
|
260
|
-
`APP_BASE_URL` its target; `UNOTEST_*` stay home. A value never
|
|
261
|
-
|
|
262
|
-
|
|
287
|
+
`APP_BASE_URL` its target; `UNOTEST_*` stay home. A secret's value never
|
|
288
|
+
prints; `env push --dry-run` shows what the other values resolved to. The
|
|
289
|
+
box and the token come from the same places `bundle push` reads them
|
|
290
|
+
from — flags, the environment, then `unotest/.env` and
|
|
291
|
+
`unotest/.secrets` — and the token must be minted with `--values` on the
|
|
292
|
+
box. Admins also see and change them on the box's admin page. Manual: [An
|
|
293
|
+
environment's values on a
|
|
263
294
|
box](https://docs.unotest.com/box/environments-and-values/).
|
|
264
295
|
|
|
265
296
|
A run on the box happens inside the box's own container: nothing on your
|
|
@@ -309,6 +340,16 @@ file; `--box` and `--token` win over both.
|
|
|
309
340
|
The token is never read from `unotest/.env` — that file travels inside a
|
|
310
341
|
pushed bundle, and `.secrets` does not.
|
|
311
342
|
|
|
343
|
+
`unotest/.secrets` is created owner-only (`0600`), so the tokens in it are
|
|
344
|
+
not readable by other users of a shared machine. A file left wider by an
|
|
345
|
+
earlier version is narrowed the next time a command writes to it, and the
|
|
346
|
+
command says so once. Editing a secret in the viewer's variables panel
|
|
347
|
+
keeps the same width. `unotest/.env` is configuration, not secrets, and
|
|
348
|
+
keeps whatever width your project gave it.
|
|
349
|
+
|
|
350
|
+
On Windows there are no POSIX permission bits, so nothing is narrowed there
|
|
351
|
+
and no width is promised: protect the file with the tools your system has.
|
|
352
|
+
|
|
312
353
|
Keeping it in the file is what lets your agent read runs: an MCP server is
|
|
313
354
|
started by your editor, so a token that only exists in your shell is a
|
|
314
355
|
token the agent never sees. A token added while the server is running is
|
|
@@ -320,7 +361,12 @@ hand you mint it for yourself on the box's guard, where it is shown once
|
|
|
320
361
|
and can be revoked. Either way it is read-only whatever your role on the
|
|
321
362
|
box — it cannot start a run or change a value. `UNOTEST_BOX_TOKEN` is the
|
|
322
363
|
project's push credential and cannot read runs. `box envs` lists the
|
|
323
|
-
environments
|
|
364
|
+
environments the box serves — an empty list means the box has none yet,
|
|
365
|
+
not that the token is too weak to see them. The commands that name one
|
|
366
|
+
(`box runs`, `box queue`, `box run`, …) say the same thing rather than
|
|
367
|
+
sending the name to a box that has nothing to match it against: a box
|
|
368
|
+
that names none has none configured, which is ours to put right. A run
|
|
369
|
+
you have just ordered may take a moment to appear.
|
|
324
370
|
|
|
325
371
|
## 5. Watch it run — the viewer
|
|
326
372
|
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ type Browser = z.infer<typeof BrowserSchema>;
|
|
|
10
10
|
* Ignored for firefox / webkit browsers (Playwright has no system
|
|
11
11
|
* channel for those). Chosen at init time via the browser prompt;
|
|
12
12
|
* user can flip later in unotest.config.{mjs,js,ts}. */
|
|
13
|
-
declare const BrowserChannelSchema: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<"chrome">, z.ZodLiteral<"msedge">, z.ZodLiteral<"chrome-beta">, z.ZodNull]>>;
|
|
13
|
+
declare const BrowserChannelSchema: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<"chrome">, z.ZodLiteral<"msedge">, z.ZodLiteral<"chrome-beta">, z.ZodLiteral<"chromium">, z.ZodNull]>>;
|
|
14
14
|
type BrowserChannel = z.infer<typeof BrowserChannelSchema>;
|
|
15
15
|
declare const ViewportSchema: z.ZodObject<{
|
|
16
16
|
width: z.ZodNumber;
|
|
@@ -260,7 +260,7 @@ declare const UnotestConfigSchema: z.ZodObject<{
|
|
|
260
260
|
reuseExistingServer?: boolean | undefined;
|
|
261
261
|
}>>;
|
|
262
262
|
browsers: z.ZodArray<z.ZodEnum<["chromium", "firefox", "webkit"]>, "many">;
|
|
263
|
-
channel: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<"chrome">, z.ZodLiteral<"msedge">, z.ZodLiteral<"chrome-beta">, z.ZodNull]>>;
|
|
263
|
+
channel: z.ZodOptional<z.ZodUnion<[z.ZodLiteral<"chrome">, z.ZodLiteral<"msedge">, z.ZodLiteral<"chrome-beta">, z.ZodLiteral<"chromium">, z.ZodNull]>>;
|
|
264
264
|
viewport: z.ZodObject<{
|
|
265
265
|
width: z.ZodNumber;
|
|
266
266
|
height: z.ZodNumber;
|
|
@@ -271,6 +271,19 @@ declare const UnotestConfigSchema: z.ZodObject<{
|
|
|
271
271
|
width: number;
|
|
272
272
|
height: number;
|
|
273
273
|
}>;
|
|
274
|
+
/** How the browser is started: a profile and extra arguments
|
|
275
|
+
* (`config/launch.ts`). Unset → as before: a fresh browser, a fresh
|
|
276
|
+
* context per scenario. */
|
|
277
|
+
launch: z.ZodOptional<z.ZodObject<{
|
|
278
|
+
userDataDir: z.ZodOptional<z.ZodString>;
|
|
279
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
280
|
+
}, "strict", z.ZodTypeAny, {
|
|
281
|
+
args?: string[] | undefined;
|
|
282
|
+
userDataDir?: string | undefined;
|
|
283
|
+
}, {
|
|
284
|
+
args?: string[] | undefined;
|
|
285
|
+
userDataDir?: string | undefined;
|
|
286
|
+
}>>;
|
|
274
287
|
retry: z.ZodObject<{
|
|
275
288
|
count: z.ZodNumber;
|
|
276
289
|
on: z.ZodArray<z.ZodEnum<["transient", "network", "crash"]>, "many">;
|
|
@@ -454,6 +467,8 @@ declare const UnotestConfigSchema: z.ZodObject<{
|
|
|
454
467
|
uploadDir?: string | undefined;
|
|
455
468
|
}>;
|
|
456
469
|
}, "strip", z.ZodTypeAny, {
|
|
470
|
+
defaultTimeoutMs: number;
|
|
471
|
+
defaultNavigationTimeoutMs: number;
|
|
457
472
|
browsers: ("chromium" | "firefox" | "webkit")[];
|
|
458
473
|
viewport: {
|
|
459
474
|
width: number;
|
|
@@ -479,8 +494,6 @@ declare const UnotestConfigSchema: z.ZodObject<{
|
|
|
479
494
|
dialogPolicy: "accept" | "dismiss" | "manual";
|
|
480
495
|
testDir: string;
|
|
481
496
|
helpersDir: string;
|
|
482
|
-
defaultTimeoutMs: number;
|
|
483
|
-
defaultNavigationTimeoutMs: number;
|
|
484
497
|
linter: {
|
|
485
498
|
enabled: boolean;
|
|
486
499
|
rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "lint:goto-concat" | "lint:regex-invalid" | "lint:echo-assert" | "lint:api-call-file-body" | "lint:lint-ok-missing-reason" | "lint:flow-returns-value" | "lint:legacy-layout" | "lint:external-variable-undeclared" | "lint:one-test-per-file" | "validator:unknown-function" | "validator:arity" | "validator:arg-kind" | "validator:step-coverage" | "validator:step-shape" | "validator:meta-shape" | "validator:function-shape" | "validator:for-shape" | "validator:if-shape" | "validator:var-shape" | "validator:unsupported-statement" | "validator:unsupported-expression" | "validator:string-concat" | "validator:semantic-loss" | "validator:list-arg" | "validator:wrapped-format", "off" | "warn" | "error">>;
|
|
@@ -513,9 +526,15 @@ declare const UnotestConfigSchema: z.ZodObject<{
|
|
|
513
526
|
command: string;
|
|
514
527
|
reuseExistingServer: boolean;
|
|
515
528
|
} | undefined;
|
|
516
|
-
channel?: "chrome" | "msedge" | "chrome-beta" | null | undefined;
|
|
529
|
+
channel?: "chrome" | "msedge" | "chrome-beta" | "chromium" | null | undefined;
|
|
530
|
+
launch?: {
|
|
531
|
+
args?: string[] | undefined;
|
|
532
|
+
userDataDir?: string | undefined;
|
|
533
|
+
} | undefined;
|
|
517
534
|
storageState?: string | undefined;
|
|
518
535
|
}, {
|
|
536
|
+
defaultTimeoutMs: number;
|
|
537
|
+
defaultNavigationTimeoutMs: number;
|
|
519
538
|
browsers: ("chromium" | "firefox" | "webkit")[];
|
|
520
539
|
viewport: {
|
|
521
540
|
width: number;
|
|
@@ -541,8 +560,6 @@ declare const UnotestConfigSchema: z.ZodObject<{
|
|
|
541
560
|
dialogPolicy: "accept" | "dismiss" | "manual";
|
|
542
561
|
testDir: string;
|
|
543
562
|
helpersDir: string;
|
|
544
|
-
defaultTimeoutMs: number;
|
|
545
|
-
defaultNavigationTimeoutMs: number;
|
|
546
563
|
linter: {
|
|
547
564
|
enabled: boolean;
|
|
548
565
|
rules: Partial<Record<"lint:deep-css" | "lint:xpath" | "lint:obfuscated-class" | "lint:pause-explicit" | "lint:disambig-by-index" | "lint:evaluate-discouraged" | "lint:scenario-in-root" | "lint:mustache-in-dsl" | "lint:goto-concat" | "lint:regex-invalid" | "lint:echo-assert" | "lint:api-call-file-body" | "lint:lint-ok-missing-reason" | "lint:flow-returns-value" | "lint:legacy-layout" | "lint:external-variable-undeclared" | "lint:one-test-per-file" | "validator:unknown-function" | "validator:arity" | "validator:arg-kind" | "validator:step-coverage" | "validator:step-shape" | "validator:meta-shape" | "validator:function-shape" | "validator:for-shape" | "validator:if-shape" | "validator:var-shape" | "validator:unsupported-statement" | "validator:unsupported-expression" | "validator:string-concat" | "validator:semantic-loss" | "validator:list-arg" | "validator:wrapped-format", "off" | "warn" | "error">>;
|
|
@@ -569,7 +586,11 @@ declare const UnotestConfigSchema: z.ZodObject<{
|
|
|
569
586
|
timeoutMs?: number | undefined;
|
|
570
587
|
reuseExistingServer?: boolean | undefined;
|
|
571
588
|
} | undefined;
|
|
572
|
-
channel?: "chrome" | "msedge" | "chrome-beta" | null | undefined;
|
|
589
|
+
channel?: "chrome" | "msedge" | "chrome-beta" | "chromium" | null | undefined;
|
|
590
|
+
launch?: {
|
|
591
|
+
args?: string[] | undefined;
|
|
592
|
+
userDataDir?: string | undefined;
|
|
593
|
+
} | undefined;
|
|
573
594
|
storageState?: string | undefined;
|
|
574
595
|
schedules?: {
|
|
575
596
|
collection: string;
|