@unotest/web 0.41.0 → 0.43.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,592 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.43.0] - 2026-09-27
4
+
5
+ ### Minor Changes
6
+
7
+ - 6fc8140: A run on an environment no longer inherits a host from the base `.env`
8
+
9
+ Environment files layer: `unotest/.env` first, then the `unotest/.env.<name>`
10
+ overlay an active environment selects, the later file winning. That is the
11
+ right rule for a password or a user name, and the wrong one for a host. A
12
+ project whose base file points `API_BASE_CHAT` at production, and whose
13
+ `dev` overlay does not mention it, ran `--env dev` and sent its API calls to
14
+ production. Nothing in the output said so: the reply reported the `dev`
15
+ environment, and the requests went elsewhere.
16
+
17
+ From this release a variable that becomes a HOST has to come from the active
18
+ environment's overlay, or from the process environment. Three variables count
19
+ as hosts: `APP_BASE_URL`, which every relative `goto` resolves against; the
20
+ name passed as `apiCall(..., {base: "NAME"})`; and a variable used as the URL
21
+ in `goto(NAME)`. When the value still comes from the base file, the run is
22
+ refused before the browser starts, and `lint`, `run_test` and `explore_start`
23
+ refuse identically — with a message that names the variable, both files, and
24
+ the two ways forward. Every other variable layers exactly as before.
25
+
26
+ A base value that really is meant for every environment is declared once, in
27
+ `unotest.config.{js,mjs,ts}`:
28
+
29
+ ```js
30
+ export default { env: { shared: ["API_BASE_CHAT"] } };
31
+ ```
32
+
33
+ A value the process environment supplies — a shell variable, or the values a
34
+ box injects into a run — is never affected by this rule: it is a value
35
+ somebody stated for this run, not one silently inherited from a file the
36
+ overlay was supposed to replace.
37
+
38
+ Migration: a suite that runs with `--env <name>` and keeps its host in the
39
+ base file will now refuse, naming the variable. Either add the variable to
40
+ the overlay, which is what most projects meant, or list its name under
41
+ `env: { shared: [...] }`. A suite that never uses `--env` is unaffected.
42
+
43
+ - 703185f: A run held at its failure has a screenshot, and the tools answer while it stands
44
+
45
+ A run paused on a failure left nothing on disk: no failure bundle, no
46
+ screenshot, and `get_failure_screenshot` replied "failure bundle not
47
+ found". The frame only appeared once the run was over — which is the one
48
+ moment it is least useful, because the browser has closed and the page
49
+ that broke is gone. Meanwhile the run was sitting right there, still
50
+ showing it.
51
+
52
+ The bundle is now written at the pause, from that page, so
53
+ `list_failures`, `get_failure_screenshot`, `get_failure_a11y` and the
54
+ rest answer on the first call. Each record carries `runState`: `paused`
55
+ while the run is still held — the browser shows the failure, and a
56
+ resumed run that fails again elsewhere re-takes the frame at the new
57
+ failure — then `finished` or `aborted` once the run is over. The frame
58
+ of a finished run stays the one taken at the failure: resuming continues
59
+ with the next statement, so the page moves on while the verdict does
60
+ not, and re-taking it at the end would show a page the failure never
61
+ happened on.
62
+
63
+ Also fixed: aborting a run while it was paused left its process alive
64
+ until the 30-minute inspection TTL expired. The run now ends when it is
65
+ aborted.
66
+
67
+ - 7006895: Spending and deleting wait for a person
68
+
69
+ A key of a customer account — an agent's key, a signed-in machine's, and a CI
70
+ key issued before this release — can no longer rent a box, delete one or
71
+ switch the Picker on by itself. The cloud records what was asked for and answers with a
72
+ request instead; somebody approves or declines it in the cabinet, on the
73
+ Approvals tab, and only a signed-in browser can — not the key that asked, and
74
+ not the link in the email about it. Nothing is created and nothing is charged
75
+ until that happens.
76
+
77
+ For the CLI this is a wait, not a failure. `box create`, `box destroy` and
78
+ `box picker on` print where to answer and how long the request lives, then
79
+ carry on with what the cloud finally did and print exactly what they would
80
+ have printed anyway. Ten minutes with no answer is exit code 75 — nothing was
81
+ created and nothing was charged, run the command again. A decline is exit code 2. `--yes` does not skip it: the flag says the terminal need not ask, not that
82
+ the account need not.
83
+
84
+ Only the CI key of an operator account, one that unotest runs itself, acts
85
+ with nobody watching. A pipeline that rents or deletes boxes with a
86
+ customer's key now waits for somebody to press Approve, or has to rent its
87
+ box ahead of time.
88
+
89
+ - 4582091: `box create` with a short balance points to the dashboard instead of starting a top-up
90
+
91
+ The cloud's API for agents has no top-up: credit is added by a person, by
92
+ card, under "Add credit" on the dashboard's home page. `box create` used to
93
+ start a payment from the terminal when the balance did not cover the first
94
+ hour, against an endpoint the service does not have, so on the real cloud it
95
+ failed with a 404.
96
+
97
+ Now it stops before renting anything and says what is short: the first hour's
98
+ price, the balance and the amount missing, and the dashboard page to add it
99
+ on. It exits 75 — nothing created, nothing charged — and is run again once the
100
+ credit is there. A key that cannot read the balance (the one `login` issues
101
+ cannot) gets the price and the page, and no invented shortfall. `--json`
102
+ prints `{ "needs": "top-up", "firstHourCents", "balanceCents",
103
+ "missingCents", "currency", "url" }`, with `null` for a figure the key may not
104
+ read.
105
+
106
+ The `cloud_box_create` MCP tool answers the same case as
107
+ `{ state: "top-up-needed" }` with the same figures, instead of an error.
108
+
109
+ Migration: the `--topup <eur>` flag of `box create` is removed — top-up moved
110
+ to the dashboard. A script that passes it is refused with the usage and exit
111
+ 2; drop the flag, and add credit on the dashboard's home page before renting.
112
+
113
+ - dae29a2: The cloud account as commands and as MCP tools: `box list`, `box logs`, `approvals`, and nine `cloud_*` tools
114
+
115
+ An agent could read a box it had been pointed at and nothing more: which
116
+ boxes the account had, renting one and deleting one lived in a terminal, and
117
+ the list of boxes appeared only inside an error message. The account is now
118
+ reachable from both sides through one implementation, so a box reads the same
119
+ whether a person or an agent asked.
120
+
121
+ New commands:
122
+
123
+ - `box list` — every box on the account with its state, version and projects.
124
+ `--json` prints the same objects the `cloud_boxes_list` tool returns.
125
+ - `box logs [name] [--bootstrap|--updater]` — the end of a box's own log,
126
+ without logging in to it. A box sends it on its next five-minute check, so
127
+ the first call says it has been asked and when to run again, with exit 0.
128
+ - `approvals [id]` — what is waiting for a person on this account, or what
129
+ became of one request. Answering stays in the cabinet: a terminal holding
130
+ the key cannot approve its own request.
131
+
132
+ New MCP tools, outside the `core` toolset: `cloud_boxes_list`,
133
+ `cloud_box_status`, `cloud_box_create`, `cloud_box_destroy`,
134
+ `cloud_box_access`, `cloud_box_update`, `cloud_box_logs`, `cloud_balance`,
135
+ `cloud_approval_wait`. `cloud_box_access` writes the fresh pair into the
136
+ project the way `box access` does and answers with the names it wrote, never
137
+ the values. They answer states an agent can act on instead of
138
+ errors: `not-logged-in` with the command that fixes it, `approval-pending`
139
+ with the request to wait on, `insufficient-scope`, and `declined`,
140
+ `expired` or `still-waiting` from the wait.
141
+
142
+ While a rent or a delete waits for approval, the terminal now names it in the
143
+ words the cabinet and the email use — `Rent a box "acme", size s (€0.09 per
144
+ hour)` — and prints a line when it is approved, not only when it is declined.
145
+ The sentences live in `@unotest/protocol` (`approvalActionText`,
146
+ `approvalPrice`), shared by every surface that shows a request.
147
+
148
+ `unotest-web --help` and `box --help` list the new commands, say which
149
+ commands wait for approval, and give one table of exit codes. `box <command>
150
+ --help` now prints that command's own flags wherever `--help` sits on the
151
+ line, and `approvals` refuses an option it does not know with exit 2.
152
+
153
+ Compatibility: every call to the cloud now carries an `X-Unotest-Client`
154
+ header. The cloud refuses a call that would wait for approval when the header
155
+ is missing — an older CLI could not read the request it would be handed — with
156
+ `426 client-too-old` and a message to update. Update `@unotest/web` before
157
+ renting or deleting boxes with any key of a customer account, a CI key
158
+ included.
159
+
160
+ - ba32d73: A config key the package does not know is refused, with the key you probably meant
161
+
162
+ `chanel: "chrome"` used to be dropped in silence: the run went ahead on
163
+ the default channel, printed `1 passed` and exited 0, so the one setting
164
+ you changed was the one setting that did not apply. Every section of
165
+ `unotest/unotest.config.*` is now checked by name, before the browser
166
+ starts:
167
+
168
+ ```
169
+ e2e: config validation failed:
170
+ unknown key "chanel" — did you mean "channel"?
171
+ retry: unknown key "cuont" — did you mean "count"?
172
+ ```
173
+
174
+ A key nothing is close to gets the other answer, because it usually
175
+ comes from a config written for a newer release than the installed
176
+ package:
177
+
178
+ ```
179
+ unknown key "telemetryEndpoint" — not a config key; check your
180
+ @unotest/web version if it comes from a newer release
181
+ ```
182
+
183
+ The config `unotest-web init` writes no longer offers a `viewer` section:
184
+ the viewer's port has always come from `UNOTEST_VIEWER_PORT`, and the
185
+ section was a suggestion the schema never had. A config generated by an
186
+ earlier version needs that block removed.
187
+
188
+ **Migration.** A config that carries a key this version does not know
189
+ now fails instead of running: fix the spelling, remove the key, or
190
+ install the release the key belongs to. Nothing else about the config
191
+ changes — every known field keeps its name, its default and its merge
192
+ behaviour, and a partially written section is still merged with the
193
+ defaults key by key.
194
+
195
+ - fb12602: A step that takes minutes no longer disappears into the client's background
196
+
197
+ Waiting on a slow page — a chat answer that takes four minutes to come
198
+ back, a report that builds itself — used to be a call your agent never got
199
+ an answer to: MCP clients stop waiting at about two minutes and carry on,
200
+ and whatever the step replied after that was lost. The way round it was to
201
+ cut every wait to under a hundred seconds and repeat the step.
202
+
203
+ Now a step still running after 90 seconds answers `{ status: "accepted",
204
+ taskId }` and keeps going: same timeout, same recording into the session.
205
+ Collect it with the new `explore_step_result({ taskId })` — `running`,
206
+ `done` with the reply the step would have given, or `failed`. A batch of
207
+ steps goes to the background as one task, for one id. The `durationMs` of
208
+ a collected step is the whole step, from its start — there is no threshold
209
+ to add back by hand.
210
+
211
+ While a step is out there, the browser context says so instead of being
212
+ damaged: another step, `explore_stop` and `close_context` answer `busy`
213
+ and name the task to wait for, and `explore_state` lists what is pending.
214
+
215
+ - 82d5920: Signing in to a box with one environment opens its viewer straight away
216
+
217
+ A box configured with a single environment used to answer a sign-in with a
218
+ screen holding one option and a button. There was nothing to choose, so the
219
+ screen only stood between you and the viewer behind it.
220
+
221
+ Now that sign-in lands in the viewer. The **Environments** screen still
222
+ appears whenever it has something to say — more than one environment to pick
223
+ from, a box that has not been sent a test suite yet, or a licence in its
224
+ grace period, whose warning lives there.
225
+
226
+ So that skipping the screen does not hide what it carried, the viewer's
227
+ session menu now offers **box pages**: one link back to the box's own
228
+ pages — environments, read tokens, and administration for those who have
229
+ it. It appears when the box tells the viewer where they are, so a viewer
230
+ and a box on different versions simply do without it.
231
+
232
+ - 3aee366: take_wheel now points the session at the run it takes the wheel of
233
+
234
+ An agent debugging a paused run would call `take_wheel`, take a
235
+ screenshot, and get a picture of a completely different stand — whatever
236
+ page the session had been recording on before. Nothing was wrong with the
237
+ screenshot: the wheel only ever took the control token, while the browser
238
+ stayed where it was, and the reply said `ok` either way.
239
+
240
+ `take_wheel` now also attaches this session to the run's browser, so the
241
+ next screenshot, snapshot or step reads the page being debugged. Every
242
+ `explore_step` reply carries `context` — `{kind: "run", runtimeId}` or
243
+ `{kind: "own"}` — so the source of a frame is stated rather than guessed.
244
+ A session has exactly one browser: `take_wheel` and
245
+ `attach_debug_session` point it at a run, `explore_start` and
246
+ `new_context` at your own, and `explorationId` chooses recording and
247
+ where a file is filed, never which browser a step runs on.
248
+ `release_wheel` hands the token back and deliberately leaves the browser
249
+ on the run.
250
+
251
+ Behaviour change for anything parsing the reply: `take_wheel` no longer
252
+ always answers `ok`. When it cannot join the browser it answers
253
+ `recording_open` (a live recording would be dropped — nothing is changed,
254
+ not even the wheel), `no_browser` (the run has not published one yet) or
255
+ `no_page`, each naming what to do next.
256
+
257
+ - 9034092: Opening a box's viewer while a suite is being installed no longer looks like a failure
258
+
259
+ Sending a new suite to a box replaces that environment's viewer, and for the
260
+ second or two that takes, anyone holding the page open got a bare `502` — the
261
+ status that tells a browser nothing and tells a script the box is broken.
262
+
263
+ The box now says, while it is happening, that it is replacing that viewer. A
264
+ browser gets a short page that comes back by itself once the new viewer
265
+ answers; an agent reading the box with a token gets `503` with `Retry-After`
266
+ and `unavailable`, which is the box's existing word for "wait and ask again".
267
+
268
+ A viewer that stopped for any other reason answers the same way but promises
269
+ nothing, and the page does not reload itself: nobody is bringing it back, and
270
+ saying otherwise would be worse than saying nothing.
271
+
272
+ ### Patch Changes
273
+
274
+ - b41b02e: A cloud that will not show the price list is quoted as such, not as free
275
+
276
+ `box create` and `box picker on` quote the hourly price inside the
277
+ question they ask. When the cloud refused the price list, the question was
278
+ asked with no price in it at all — and silence there reads as "this is
279
+ free", with no way for the reader to learn that a price exists and they
280
+ were not shown it.
281
+
282
+ A refusal for want of a permission now says so: `price not visible to this
283
+ key: it needs billing:read`. On a current cloud the list is under `read`,
284
+ which every key from `login` carries — so this is what an OLDER cloud says
285
+ to a newer client, which is exactly when a client must not go quiet about
286
+ money. Every other reason stays silent, because a cloud older still has no
287
+ price list at all and a command must not fail for want of a courtesy.
288
+
289
+ - 3e7e3fc: API keys carry permissions
290
+
291
+ A key used to be all or nothing: any live key could do everything the API
292
+ offers. Now every key carries a list of permissions — see the boxes, manage
293
+ them, read the balance — and every route states the one it needs. A key
294
+ without it gets `insufficient-scope`, which is a 403 and not a 401: the
295
+ credential is good, so signing in again would change nothing.
296
+
297
+ Where the list comes from depends on the key. In the cabinet you tick the
298
+ permissions when issuing a key for an agent. Signing a machine in asks for
299
+ enough to see your boxes and work with them, never your money, and the
300
+ approval page in the browser lets you narrow that before you approve — the
301
+ key then has exactly what was left ticked. `login` and `whoami` print what
302
+ the key may do.
303
+
304
+ Keys that already exist keep the permissions they had, with one exception:
305
+ spending money and deleting with nobody watching. Only the CI key of an
306
+ operator account, one that unotest runs itself, may do that. A CI key a
307
+ customer holds from before this release works like an agent's key: renting,
308
+ deleting and switching the Picker on wait for a person to approve.
309
+
310
+ One visible change for a key that cannot read the balance: `whoami` says so
311
+ on the balance line instead of failing, and signing in with such a key is
312
+ still recognised as being signed in.
313
+
314
+ - 8353232: A rental the account cannot pay is refused before anybody is asked to approve it
315
+
316
+ A key that may not spend by itself used to get an approval request for a box
317
+ even when the balance could not cover the first hour: a person was asked to
318
+ approve it, and the rental then failed at their yes. The money is now checked
319
+ first, and such a create is answered `402 insufficient-balance` with nothing
320
+ recorded and nobody written to. The check at approval stays — a balance can
321
+ move while a request waits — and a request that fails there says so in words:
322
+ the price of the hour and where to add credit.
323
+
324
+ Every `insufficient-balance` carries `topupUrl`, the dashboard page where a
325
+ person adds credit. A key that may read the account's money also gets
326
+ `firstHourCents`, `balanceCents`, `missingCents` and `currency`; any other
327
+ key gets no figures.
328
+
329
+ `box create` prints the cloud's own figures and link when the refusal carries
330
+ them, instead of counting them again.
331
+
332
+ - 2006bf9: A refused config says which field is wrong, and `channel` takes the same words everywhere
333
+
334
+ Any bad value in `unotest/unotest.config.mjs` used to come back as one
335
+ line — `config validation failed` — with no field, no value and no list of
336
+ what would have been accepted, though all three were known. Now the
337
+ refusal names them:
338
+
339
+ ```
340
+ config validation failed:
341
+ channel: expected one of "chrome" / "msedge" / "chrome-beta" / "chromium" / "bundled" / null, got "bundeld"
342
+ defaultTimeoutMs: expected number, got string
343
+ ```
344
+
345
+ And `channel: "bundled"` is accepted. `UNOTEST_BROWSER_CHANNEL` has always
346
+ taken that word — the `.env` template says the variable merely overrides
347
+ `channel` — while the config refused it. Both inputs now share one
348
+ vocabulary, and `bundled` means what it has always meant: Playwright's own
349
+ Chromium, the default.
350
+
351
+ - e7cdab7: An empty id is refused instead of quietly changing what a tool does
352
+
353
+ `explore_step` and `explore_steps` treated `explorationId: ""` as "run
354
+ this without recording": the step went through, the reply said it had
355
+ succeeded, and nothing reached the session log. The agent found out at
356
+ `explore_stop`, with an empty recording and the work already done. The
357
+ same held for `explore_record`, and for `runtimeId: ""` in the debugger
358
+ tools, where an empty value silently meant "the session you attached to".
359
+
360
+ An empty id is now an input error naming the field, exactly as the
361
+ neighbouring tools have always answered. Leaving the key out is still how
362
+ you ask for the ad-hoc mode — absence is the mode, emptiness is a typo.
363
+
364
+ - a3f89ee: The MCP server reports the version it actually is
365
+
366
+ `initialize` answered `serverInfo` with a hard-coded `0.1.0` whatever
367
+ version of the package was running, while `--version` printed the real
368
+ number. An editor showed the wrong version in its server list, and nobody
369
+ checking a session could tell a server started from npm apart from one
370
+ started out of a working copy.
371
+
372
+ Both surfaces now read the package's own manifest through one helper, so
373
+ they cannot drift apart again. A manifest that cannot be read is an error
374
+ naming the fault, not a silent `0.0.0` — an invented version reads as a
375
+ real answer in a protocol reply and in a support transcript.
376
+
377
+ - f1f3fb1: Existing boxes install the five-minute update check themselves, and report a finished update in the tick that applied it
378
+
379
+ A box rented before the five-minute check existed only had the hourly timer:
380
+ pressing **Update** in the cabinet could take up to an hour and a half to
381
+ reach it. The updater now installs that pair of systemd units itself when the
382
+ box does not have them — exactly those two, only when the file is absent, and
383
+ never replacing a unit an operator edited.
384
+
385
+ A finished update is also reported in the tick that applied it, instead of in
386
+ the next one, so the cabinet card and `box status` stop showing "updating" for
387
+ a box that is already done.
388
+
389
+ **Both start working with the update after this one.** The move onto this
390
+ version is still carried out by the updater a box runs today: it arrives on
391
+ the hourly timer — up to an hour and a half — and that first "applied" still
392
+ comes with the next hourly tick. From there on the new updater is in charge,
393
+ a pressed **Update** lands within minutes, and the cabinet shows it as done as
394
+ soon as it is.
395
+
396
+ - 74a1f9d: The viewer finishes writing its run index before it exits
397
+
398
+ On boot, and whenever a run finishes or the environment switches, the viewer
399
+ rebuilds the derived index it serves history from. Those writes are
400
+ deliberately not waited for while the server runs — a history months deep
401
+ must not hold the UI back — but they were not waited for on the way down
402
+ either. Stopping the viewer returned while files were still landing under the
403
+ run directory: a directory the caller believed had been released kept growing
404
+ for a fraction of a second, and anything that removed or archived it right
405
+ after the viewer stopped could fail or capture a half-written index.
406
+
407
+ Shutdown now waits for the index writes that are still in flight, so a
408
+ stopped viewer is a viewer that has finished writing. A rebuild that takes
409
+ longer makes the stop take longer, which is the honest cost of not leaving
410
+ half a file behind.
411
+
412
+ - Updated dependencies [6fc8140]
413
+ - Updated dependencies [4582091]
414
+ - Updated dependencies [dae29a2]
415
+ - Updated dependencies [82d5920]
416
+ - Updated dependencies [74a1f9d]
417
+ - @unotest/core@0.43.0
418
+ - @unotest/protocol@0.43.0
419
+ - @unotest/viewer@0.43.0
420
+ - @unotest/dsl@0.43.0
421
+ - @unotest/grounder-client@0.43.0
422
+
423
+ ## [0.42.0] - 2026-09-24
424
+
425
+ ### Minor Changes
426
+
427
+ - 8f319b2: `box status` now reports the box's version: what it runs, what its channel
428
+ offers and where it is between the two.
429
+
430
+ A box never updates itself. A new release is offered, not installed — the
431
+ box keeps running the version it is on until somebody presses **Update** in
432
+ the cabinet, or an agent calls `POST /api/v1/boxes/<name>/update` with the
433
+ version it was offered. `box status` prints the offer and says where the
434
+ decision is made; a cloud that reports no versions leaves the CLI silent
435
+ about them rather than guessing.
436
+
437
+ Migration: boxes that used to install a new release on their own stop doing so
438
+ with this release. To move a box to a newer version, press **Update** on its
439
+ card in the cabinet, or call the endpoint above.
440
+
441
+ - 0c56c6d: Start the browser with a profile and extra arguments — what testing a browser extension needs
442
+
443
+ A new `launch` block in `unotest.config`:
444
+
445
+ - `launch.args` — extra browser command-line arguments, e.g.
446
+ `--load-extension=./ext` (extensions load in Chromium only);
447
+ - `launch.userDataDir` — a browser profile directory. The browser then starts
448
+ persistent: cookies, storage and installed extensions live in the profile.
449
+ `UNOTEST_USER_DATA_DIR` overrides it for a machine.
450
+
451
+ On a profile there is **no isolation between scenarios**: the browser has one
452
+ context, and every scenario of a run gets that same context. Each such run says
453
+ so in its log.
454
+
455
+ Refused with a message instead of being ignored: a profile together with
456
+ `storageState`, a profile in a collection of more than one worker, `launch`
457
+ on a box (its runs take no launch settings from the suite), and an extension
458
+ (`--load-extension`) on a headless run of bundled Chromium, whose headless
459
+ shell ignores extensions without an error. For headless runs with an
460
+ extension — CI — set the new `channel: "chromium"` (Chromium's new headless
461
+ mode, which loads them), or `UNOTEST_BROWSER_CHANNEL=chromium`; locally, run
462
+ headed. Without `launch` nothing changes.
463
+
464
+ - 1f1dffb: `box envs` and `box run` show how a box installed the suite: how long it
465
+ took, and whether the packages came from the registry or the box's own
466
+ npm cache.
467
+
468
+ ```
469
+ acme/test main@1a2b3c4
470
+ installed in 12.3s from this box's cache (252 of 252 from cache)
471
+ ```
472
+
473
+ The source is measured by the box, not read out of npm's output: before
474
+ an install it compares what the lockfile names against what its cache
475
+ already holds. On a fast registry a cold install and a warm one take the
476
+ same number of seconds, so the duration alone never said which had
477
+ happened — the counts are printed with the word so it can be checked.
478
+ `box run <id>` prints the same line for the bundle that run came from,
479
+ while the environment still serves it.
480
+
481
+ A box older than this contract says nothing about installs, and the
482
+ output is exactly what it was.
483
+
484
+ - 7958c4a: Recording tools refuse a misspelled field instead of silently not recording
485
+
486
+ `explore_step` with `exploration_id` (instead of `explorationId`) used to run the
487
+ step ad hoc, answer success and record nothing — the empty session only showed
488
+ up at `explore_stop`. Every `explore_*` tool, `generate_dsl_from_exploration`
489
+ and `save_exploration_as_test` now refuse a field they do not declare, including
490
+ inside an `explore_steps` step, and the error names the field they accept
491
+ (`exploration_id (did you mean explorationId?)`).
492
+
493
+ Ad-hoc execution is unchanged: omit `explorationId`, and the step's reply says
494
+ `recorded: false`.
495
+
496
+ Migration: a client that sent extra, ignored fields to these tools now gets an
497
+ input error listing the accepted fields — drop the extra ones.
498
+
499
+ - 92b446f: `box status` says until when the balance keeps a box, not when its paid hour ends
500
+
501
+ `box status` used to print `paid until` — the end of the hour already bought.
502
+ With credit left that read as "switched off within the hour". It now prints
503
+ `funded until <date> UTC`: the moment the box is deleted if nobody tops up,
504
+ computed by the cloud from the balance and every box on the account, each at
505
+ its own rate. A failed box prints neither.
506
+
507
+ The cloud API adds `fundedUntil` to each billed box and to `GET /balance` (absent
508
+ when nothing is billed); `paidUntil` stays, and means only the end of the paid
509
+ hour. The cabinet's box card and balance show the same date, and the low-balance
510
+ letter names it.
511
+
512
+ Migration: `box status` no longer prints a `paid until` line. A script that read
513
+ it should take `fundedUntil` from `box status --json` for the deadline, or
514
+ `paidUntil` there if it meant the end of the paid hour.
515
+
516
+ - c99f849: The viewer on a box has no terminal panel instead of a dead one
517
+
518
+ A box's viewer could never start its docked terminal — the PTY addon is not in
519
+ the box image — yet it showed the panel and the button. A host that cannot give
520
+ the terminal a PTY now says so with `UNOTEST_VIEWER_TERMINAL=off`, and the viewer
521
+ then leaves the terminal out entirely: no panel, no button, no terminal socket,
522
+ for every session. A box sets it for its viewers.
523
+
524
+ Nothing changes locally: the terminal is on by default, and a machine missing
525
+ the addon still reports it when the panel starts.
526
+
527
+ ### Patch Changes
528
+
529
+ - 784c10d: A pressed Update reaches the box within minutes, and a failed one is not retried on its own
530
+
531
+ A box now checks every five minutes whether it has been asked to move to
532
+ another version, instead of once an hour, so pressing **Update** in the cabinet
533
+ (or calling the update endpoint) takes effect within minutes.
534
+
535
+ The box also reports back what it runs and how its last update ended —
536
+ applied, rolled back, refused or failed, and why — which is what the cabinet
537
+ and `box status` show.
538
+
539
+ An update that did not succeed is not tried again by the box itself: rolling
540
+ back takes the box's stack down and up again, and repeating that unasked would
541
+ be an automatic update. It is tried again when **Update** is pressed again.
542
+
543
+ - e0b822a: A reinstall no longer empties the tree a viewer is serving
544
+
545
+ A bundle whose tree already has packages is reinstalled in a copy while the
546
+ live tree waits aside; the viewer keeps serving it. A failed reinstall leaves
547
+ the live `node_modules` untouched instead of leaving the viewer answering 404
548
+ until an install succeeds. A tree a daemon left aside when it died mid-install
549
+ is put back by the next housekeeping sweep.
550
+
551
+ - 96e4977: A box that names no environments is told so, instead of being sent a name
552
+ it cannot match.
553
+
554
+ `box runs`, `box queue`, `box run` and the rest used to pass the name on to
555
+ a box with an empty list, which answered 404 and advice about the form of a
556
+ name — spelling advice to somebody whose box has nothing to spell. They now
557
+ say what is true: the box names no environments yet, a run ordered a moment
558
+ ago may not be listed straight away, and a box that goes on naming none has
559
+ none configured. Agents get the same sentence through the `box_*` MCP tools.
560
+
561
+ The command exits 2, like the other refusals of an environment name (was 1).
562
+
563
+ Migration: a script that checked for exit code 1 on this refusal should check
564
+ for 2.
565
+
566
+ `bundle push --run` is unchanged: it uploads and lets the box decide, because
567
+ a box older than this contract names nothing until its first run. What
568
+ changed there is the hint after the box's own refusal, which no longer points
569
+ back at the list that was empty.
570
+
571
+ - d1ecff6: A box lists its environments before the first suite arrives
572
+
573
+ The route table names every configured environment; one without a bundle is
574
+ listed without an address. `/_guard/api/envs` answers with it at once
575
+ (`note: "no suite yet"`), so an environment name can be checked before the
576
+ first push, and opening it in a browser shows a page with the commands that
577
+ send a suite instead of a bare 503.
578
+
579
+ Migration: the environment list a read token gets (`box envs`, the `box_envs`
580
+ MCP tool) now also names environments that have no suite yet, with the note
581
+ `no suite yet`. Reading one of them answers `unavailable` until a suite is pushed.
582
+
583
+ - Updated dependencies [c99f849]
584
+ - @unotest/viewer@0.42.0
585
+ - @unotest/core@0.42.0
586
+ - @unotest/dsl@0.42.0
587
+ - @unotest/grounder-client@0.42.0
588
+ - @unotest/protocol@0.42.0
589
+
3
590
  ## [0.41.0] - 2026-09-21
4
591
 
5
592
  ### Minor Changes
@@ -651,7 +1238,7 @@ picker on` fetches this project's own access and writes the three grounder
651
1238
 
652
1239
  That screen named one command:
653
1240
 
654
- npx @unotest/web bundle push --run --env ENVIRONMENT
1241
+ npx @unotest/web bundle push --run --env ENVIRONMENT
655
1242
 
656
1243
  In a project that has just been created, the command refuses with `no box
657
1244
  address` before it does anything: the project holds neither the box's address
@@ -660,8 +1247,8 @@ address` before it does anything: the project holds neither the box's address
660
1247
 
661
1248
  The screen now names both steps, in the order they have to happen:
662
1249
 
663
- npx @unotest/web box access larch
664
- npx @unotest/web bundle push --run --env ENVIRONMENT
1250
+ npx @unotest/web box access larch
1251
+ npx @unotest/web bundle push --run --env ENVIRONMENT
665
1252
 
666
1253
  The box's own name is filled in when its licence carries one. `box access`
667
1254
  with no name refuses, and its refusal offers to create a box — to somebody who