@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 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
- npx @unotest/web bundle push --run --env ENVIRONMENT
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
- npx @unotest/web box access larch
402
- npx @unotest/web bundle push --run --env ENVIRONMENT
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 prints.
261
- The token must be minted with `--values` on the box. Admins also see and
262
- change them on the box's admin page. Manual: [An environment's values on a
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 a token may look at.
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
 
@@ -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;