@unotest/web 0.38.0 → 0.40.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 +452 -0
- package/README.md +18 -1
- package/dist/config/schema.js +1 -1
- package/dist/driver/index.js +1 -1
- package/dist/dsl/index.js +1 -1
- package/dist/dsl/web-dsl-language-service.js +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/runner/cli.js +1 -1
- package/dist/runner/init/chrome-probe.js +1 -1
- package/dist/runner/init.d.ts +1 -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/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,457 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.40.0] - 2026-09-19
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- fa848e9: A box that finishes coming up tells the person who rented it
|
|
8
|
+
|
|
9
|
+
Renting a box takes minutes, and until now it ended in silence. Somebody who
|
|
10
|
+
closed the tab while the machine was being built learned nothing: not that it
|
|
11
|
+
was up, not where it answers, not what to do next. A rental that says nothing
|
|
12
|
+
looks like a rental that did not work.
|
|
13
|
+
|
|
14
|
+
The service now sends one message, at the moment the box becomes ready, to the
|
|
15
|
+
address on the account. It names the box's address, points at the card on the
|
|
16
|
+
dashboard where the login and password stay readable for as long as the box
|
|
17
|
+
lives, and gives the three commands that put a suite on it:
|
|
18
|
+
|
|
19
|
+
npx @unotest/web login
|
|
20
|
+
npx @unotest/web box access <box>
|
|
21
|
+
npx @unotest/web bundle push --run --env <environment> --collection examples
|
|
22
|
+
|
|
23
|
+
The last one names a collection because a box runs a collection or a schedule,
|
|
24
|
+
never a scenario by name, and a project straight out of `init` has no schedule:
|
|
25
|
+
without the flag the run is refused before it starts. `examples` is what `init`
|
|
26
|
+
writes, and the message says to name your own instead where you have one.
|
|
27
|
+
|
|
28
|
+
The environment is filled in only when the box has one. A box can have several
|
|
29
|
+
— `box create` sends every environment the project has — and a command naming
|
|
30
|
+
the first of them reads as a command naming the only one, which would send a
|
|
31
|
+
run somewhere the reader did not choose. With several, the command carries
|
|
32
|
+
`<env>` and the message says where the names are written. Same rule as the
|
|
33
|
+
CLI's own next step, so the two do not answer one question differently.
|
|
34
|
+
|
|
35
|
+
The message carries no password. The break-glass password of a box is on its
|
|
36
|
+
card, which can be looked at again; a copy in a mailbox is one nobody can
|
|
37
|
+
withdraw. The type the message is built from has no field for it, so it cannot
|
|
38
|
+
be added back by accident.
|
|
39
|
+
|
|
40
|
+
It is sent once. The guard is the transition itself, taken inside the write
|
|
41
|
+
lock that makes a box ready, so a resumed build, the billing tick and a read
|
|
42
|
+
of the card all find the box already ready and announce nothing. A mail server
|
|
43
|
+
that is down is logged and does not touch the rental: the customer paid for a
|
|
44
|
+
machine, not for mail.
|
|
45
|
+
|
|
46
|
+
- 42548f7: A box deleted by hand tells its owner too, and the letter says which deletion happened
|
|
47
|
+
|
|
48
|
+
A box could go two ways, and only one of them said anything. When the balance
|
|
49
|
+
stopped covering a box's next hour, the billing tick deleted it and wrote to
|
|
50
|
+
the owner. When a person deleted one — from the dashboard, the CLI or the REST
|
|
51
|
+
API — the machine was released, the record was settled, and nobody was told.
|
|
52
|
+
|
|
53
|
+
That silence is only harmless while the person who presses delete and the
|
|
54
|
+
person who pays for the account are the same person. An account can have
|
|
55
|
+
several people on it, an API key, and a dashboard open where it should not be.
|
|
56
|
+
The one deletion a customer most needs to hear about is the one they did not
|
|
57
|
+
make, and it was the one that arrived as nothing at all.
|
|
58
|
+
|
|
59
|
+
Every deletion now ends in the same letter, from the same place, and the
|
|
60
|
+
letter's first paragraph says how the box went: the balance could not cover
|
|
61
|
+
the next hour, or somebody with access to the account asked for it. The second
|
|
62
|
+
version names no sum — what the balance happened to be when a person pressed
|
|
63
|
+
delete explains nothing about why the box is gone, and a figure printed beside
|
|
64
|
+
a deletion reads as its cause. It points at the dashboard instead, for a reader
|
|
65
|
+
whose next question is who else has access. Everything after that first
|
|
66
|
+
paragraph is one text for both: the machine and what was stored on it are gone,
|
|
67
|
+
the scenarios in the customer's own repository are not, and the name is never
|
|
68
|
+
reused.
|
|
69
|
+
|
|
70
|
+
One letter per deletion, not per caller. The transition to deleted is claimed
|
|
71
|
+
under the same write lock that settles the record, and only the caller that
|
|
72
|
+
made it writes. A person and the billing tick arriving at one box in the same
|
|
73
|
+
moment therefore send one letter between them — previously they could each
|
|
74
|
+
send their own.
|
|
75
|
+
|
|
76
|
+
A box the fleet has no record of is now settled rather than retried forever.
|
|
77
|
+
The tick used to treat "no such box" from the fleet as a failure to delete,
|
|
78
|
+
leaving the record live and billable and trying again every hour; it now ends
|
|
79
|
+
the way it does when a person deletes such a box, which is that there is
|
|
80
|
+
nothing to release and the record is closed.
|
|
81
|
+
|
|
82
|
+
The journal says which way a box went, on both paths. Until now a deletion by
|
|
83
|
+
hand left no line at all, and the only way to tell one from an eviction was the
|
|
84
|
+
absence of the tick's own lines.
|
|
85
|
+
|
|
86
|
+
A mail server that is down still never holds a deletion up: the machine is
|
|
87
|
+
released and the record settled first, and a failed send is a line in the log.
|
|
88
|
+
|
|
89
|
+
- cb4a7e5: `box access` without a name lists the boxes the account already has, instead of advising a second one
|
|
90
|
+
|
|
91
|
+
Run in a directory that does not know a box — a fresh checkout, a second
|
|
92
|
+
project — `box access`, `box status`, `box destroy` and `box picker`
|
|
93
|
+
refused with three things to try, and the only one anybody can follow
|
|
94
|
+
without thinking was `box create`. For somebody who already rents a box,
|
|
95
|
+
following it rents and pays for another.
|
|
96
|
+
|
|
97
|
+
The refusal now asks the account first. With one box it names it and gives
|
|
98
|
+
the command back ready to run:
|
|
99
|
+
|
|
100
|
+
no box named and none configured — this account has one box,
|
|
101
|
+
"larch": run `box access larch`
|
|
102
|
+
|
|
103
|
+
With several it lists them and asks which. Only an account with no boxes
|
|
104
|
+
at all is still told to create one, and a cloud that refused or could not
|
|
105
|
+
be reached is told the same as before — a cloud that did not answer is
|
|
106
|
+
never read as "you have no boxes".
|
|
107
|
+
|
|
108
|
+
- 1cb7ba5: Reading a box takes the same environment name that ordered the run
|
|
109
|
+
|
|
110
|
+
`bundle push --run --env prod` has always taken the bare environment
|
|
111
|
+
name and let the box fill the project in from the token. The commands
|
|
112
|
+
and MCP tools that read the results back wanted the full
|
|
113
|
+
`acme/prod`, and a bare name came back as a 404 that talked about the
|
|
114
|
+
environment rather than about its form:
|
|
115
|
+
|
|
116
|
+
✗ this box does not serve 'prod'
|
|
117
|
+
|
|
118
|
+
So the name that worked in one command failed in the next, and nothing
|
|
119
|
+
on screen said why.
|
|
120
|
+
|
|
121
|
+
Both halves now take both spellings. `box runs`, `box run`, `box queue`
|
|
122
|
+
and `box screenshot` — and `box_runs`, `box_run`, `box_run_download`,
|
|
123
|
+
`box_queue`, `box_screenshot` — translate the name against the box's own
|
|
124
|
+
list before the request, exactly the list `box envs` prints. A bare name
|
|
125
|
+
only one project on the box has is sent as that project's full name; a
|
|
126
|
+
bare name several projects share is refused before anything is asked,
|
|
127
|
+
naming them:
|
|
128
|
+
|
|
129
|
+
✗ box runs: more than one project on this box has an environment
|
|
130
|
+
called "test" (acme/test, beta/test) — name the one you mean in
|
|
131
|
+
full.
|
|
132
|
+
|
|
133
|
+
A name the box serves in neither form is refused the same way, listing
|
|
134
|
+
what it does serve. Where the list cannot be had — a box that did not
|
|
135
|
+
answer, a refused token, an empty list, which does not prove there are
|
|
136
|
+
no environments — the name goes as it was typed and the box decides,
|
|
137
|
+
exactly as before; its refusal now names the form as well as the
|
|
138
|
+
command that lists the names.
|
|
139
|
+
|
|
140
|
+
### Patch Changes
|
|
141
|
+
|
|
142
|
+
- 9878745: `bundle push` that refuses leaves nothing behind in the project
|
|
143
|
+
|
|
144
|
+
The environment check now stands in front of the packing step, not only in
|
|
145
|
+
front of the upload. Packing writes: a suite that has no `package-lock.json`
|
|
146
|
+
yet gets one, in your repository, next to your scenarios. So a push refused
|
|
147
|
+
for naming an environment the box does not have still ended with a file that
|
|
148
|
+
was not there before the command — the refusal cost something after all, and
|
|
149
|
+
the next `git status` showed it.
|
|
150
|
+
|
|
151
|
+
The order is now: the flags, the box address, the token, the environment, and
|
|
152
|
+
only then the suite is packed. Nothing that can be refused happens after the
|
|
153
|
+
first write.
|
|
154
|
+
|
|
155
|
+
What was already right stays: `--dry-run` still packs and still prints its
|
|
156
|
+
bundle id, without asking for a box. The refusals about a missing address and
|
|
157
|
+
a missing token answer in the same words and with the same exit codes as
|
|
158
|
+
before — they simply answer sooner, before the lockfile is touched. A push
|
|
159
|
+
that goes through writes the lockfile exactly as it always did.
|
|
160
|
+
|
|
161
|
+
- 8cc5b20: `bundle push --run` checks the environment name before uploading, and names the environments itself
|
|
162
|
+
|
|
163
|
+
Pushing with an environment name the box does not have uploaded the whole
|
|
164
|
+
bundle first, and only then came back with a refusal — so the last thing
|
|
165
|
+
on screen was "uploaded", the wasted upload looked like success, and the
|
|
166
|
+
refusal read like a problem with the box. The advice that followed was to
|
|
167
|
+
ask the operator which environments exist; on a rented box the customer
|
|
168
|
+
is the operator.
|
|
169
|
+
|
|
170
|
+
The name is now checked against the box's own list first, with the read
|
|
171
|
+
token `box envs` uses. An unknown name is refused before anything is
|
|
172
|
+
uploaded, and the refusal names what the box does serve:
|
|
173
|
+
|
|
174
|
+
✗ no environment named "staging" on https://larch.box.unotest.com
|
|
175
|
+
— nothing was uploaded.
|
|
176
|
+
it serves acme/test, acme/prod — push into one of them with
|
|
177
|
+
--env test
|
|
178
|
+
|
|
179
|
+
Both spellings the box itself gives are accepted, `acme/test` and the
|
|
180
|
+
bare `test`. Where the list cannot be had — no read token, a box that did
|
|
181
|
+
not answer, an empty list, which does not prove there are none — the push
|
|
182
|
+
goes ahead exactly as before and the box decides. Refusals that still
|
|
183
|
+
come from the box now point at `box envs` instead of an operator.
|
|
184
|
+
|
|
185
|
+
- 8a9c112: A fresh project can start a remote run: `init` writes a collection, and the refusal says what a box runs
|
|
186
|
+
|
|
187
|
+
A box runs a collection or a schedule; the local runner takes a scenario by
|
|
188
|
+
name. Both are true, and nothing said they differ — so a project created by
|
|
189
|
+
`init`, which wrote neither a collection nor a schedule, could not start a
|
|
190
|
+
remote run at all. The command the box's own screen offers came back with
|
|
191
|
+
"nothing to run", and the refusal named the state without naming the
|
|
192
|
+
difference or anything to try.
|
|
193
|
+
|
|
194
|
+
`init` now writes `unotest/e2e/_collections/examples.yaml`, naming the
|
|
195
|
+
examples it already creates. They hit the public playground, so the first
|
|
196
|
+
remote run passes before the box can reach the customer's own application:
|
|
197
|
+
|
|
198
|
+
npx @unotest/web bundle push --run --env <environment> --collection examples
|
|
199
|
+
|
|
200
|
+
A run asked for without a collection is still refused — that has not
|
|
201
|
+
changed — but the refusal now says that a box runs a collection or a
|
|
202
|
+
schedule rather than a scenario by name, and what to pass instead.
|
|
203
|
+
|
|
204
|
+
- 3f9bcb3: The box's screens fit a phone: the header wraps instead of running off the side
|
|
205
|
+
|
|
206
|
+
Four things sit in the header of every guard page — the box's own address,
|
|
207
|
+
who is looking, the link to the other half of the guard, and the way out. On
|
|
208
|
+
a screen 320 px wide they did not fit on one line, and what did not fit was
|
|
209
|
+
pushed past the right edge, taking the page's width with it. The whole screen
|
|
210
|
+
then scrolled sideways, and the control left outside was the one that signs a
|
|
211
|
+
viewer out: a box could not be left from a phone.
|
|
212
|
+
|
|
213
|
+
The header now wraps. On a narrow screen it takes a second row, and every
|
|
214
|
+
control stays reachable; a long box address or a long directory name wraps
|
|
215
|
+
with it instead of widening the row. Nothing changes on a wide screen — the
|
|
216
|
+
header is one row of the same height it always was.
|
|
217
|
+
|
|
218
|
+
- 7131b87: New box keeps the environment you named, so a box can be rented again
|
|
219
|
+
|
|
220
|
+
Renting a box from the cabinet was impossible for anyone who used the
|
|
221
|
+
Environment field the way it asks to be used. Typing `prod` and pressing Enter
|
|
222
|
+
turned the name into a tag, as it should; submitting the form then answered
|
|
223
|
+
"Name what runs at this address — prod, staging, pr-42." and came back with
|
|
224
|
+
the field empty and the tag gone. The form was refusing the very name it had
|
|
225
|
+
just been given, and there was no way through it: the only path to a first box
|
|
226
|
+
through the cabinet was closed.
|
|
227
|
+
|
|
228
|
+
The tag the browser makes now travels under the name the server reads
|
|
229
|
+
environments back from. It had been travelling under the name of the text box
|
|
230
|
+
it was typed in, so a row with one tag arrived with an address and no names at
|
|
231
|
+
all, and the refusal about names was the server honestly reporting what it
|
|
232
|
+
received.
|
|
233
|
+
|
|
234
|
+
Two things beside the fix, so this class of failure is harder to repeat. The
|
|
235
|
+
name a tag posts under is stated once, in the form, and the browser reads it
|
|
236
|
+
from there rather than spelling it out a second time. And the reader on the
|
|
237
|
+
server now accepts either half of the field repeated — two tags on one address
|
|
238
|
+
have always arrived as a repeated field, and a reader that expects a single
|
|
239
|
+
value drops a repeated one whole rather than in part.
|
|
240
|
+
|
|
241
|
+
A form submitted with scripting off is unchanged: the typed name still posts on
|
|
242
|
+
its own, one name per submit.
|
|
243
|
+
|
|
244
|
+
- e1db36f: `picker off` says how long the withdrawal takes, as one number
|
|
245
|
+
|
|
246
|
+
Switching the Picker off — a box's with `box picker off`, a project's with
|
|
247
|
+
`picker off` — withdraws its token through a revocation list that two
|
|
248
|
+
services read on their own schedules: the cloud rewrites the list, the
|
|
249
|
+
grounding service re-reads it. The command used to say only that the token
|
|
250
|
+
stops working "once the list is refreshed", because the CLI knew neither
|
|
251
|
+
interval and half a figure read as the whole would have been wrong.
|
|
252
|
+
|
|
253
|
+
The cloud now adds both halves and hands the sum over, and the command
|
|
254
|
+
says it:
|
|
255
|
+
|
|
256
|
+
✓ The Picker token for this box is revoked — the grounder stops
|
|
257
|
+
accepting it within about 2 minutes, once it refreshes its
|
|
258
|
+
revocation list
|
|
259
|
+
|
|
260
|
+
The dashboard's own notices after switching off say the same figure. When
|
|
261
|
+
the cloud cannot name it — the grounding service has not answered yet —
|
|
262
|
+
the sentence stays as it was, with no number: a default would be a
|
|
263
|
+
promise nothing keeps.
|
|
264
|
+
|
|
265
|
+
- 36d1dba: `bundle push --run --env` now names an environment the box will accept, and refuses one it cannot.
|
|
266
|
+
|
|
267
|
+
A box lists its environments as `<project>/<environment>`, and that is the
|
|
268
|
+
form its viewer API and every `box` subcommand take. The door that orders
|
|
269
|
+
runs takes the bare environment instead and fills the project in from the
|
|
270
|
+
token that pushed. The pre-upload check accepted both forms but carried
|
|
271
|
+
whichever was typed straight through, and a refusal advised the listed one
|
|
272
|
+
— so a push that followed our own advice was refused by the box after the
|
|
273
|
+
bundle had already been uploaded.
|
|
274
|
+
|
|
275
|
+
Both forms are still accepted, with one exception named below. Whichever is
|
|
276
|
+
given, the run is now ordered under the name the box runs, and a refusal
|
|
277
|
+
advises that same name.
|
|
278
|
+
|
|
279
|
+
On a box that serves several projects, a listed name is translated only
|
|
280
|
+
while the bare name it becomes belongs to one of them. Where two projects
|
|
281
|
+
share an environment name, the run would be ordered inside whichever
|
|
282
|
+
project your token belongs to — not necessarily the one you named — so the
|
|
283
|
+
push is refused before the upload instead, and the refusal names both:
|
|
284
|
+
|
|
285
|
+
✗ "beta/prod" cannot be ordered by that name on
|
|
286
|
+
https://larch.box.unotest.com — nothing was uploaded.
|
|
287
|
+
acme/prod and beta/prod are both called "prod", and a run is
|
|
288
|
+
ordered by the bare name inside the project your token belongs
|
|
289
|
+
to — push with --env prod if that project is yours
|
|
290
|
+
|
|
291
|
+
A bare name several projects share is left as it was typed: that is the
|
|
292
|
+
form the box expects, and the token is what says which project it means.
|
|
293
|
+
|
|
294
|
+
None of this reaches a box that has not named an environment yet. A box
|
|
295
|
+
with nothing to list answers with an empty list, which does not prove it
|
|
296
|
+
serves nothing, so the push goes ahead and the box decides — exactly as
|
|
297
|
+
before. Until a suite has been pushed to it once, a wrong name is still
|
|
298
|
+
answered by the box after the upload, not by this check.
|
|
299
|
+
|
|
300
|
+
- 5193108: The New box dialog closes again after the form has refused something
|
|
301
|
+
|
|
302
|
+
A dialog in the dashboard could be opened two ways and closed only one. The
|
|
303
|
+
Create box button opens it with a link to `#new-box`, and the × and the veil
|
|
304
|
+
close it by dropping that target again. A form the server refused comes back
|
|
305
|
+
open too — the refusal has to be visible — but that copy is opened by a class
|
|
306
|
+
in the served markup, and a link to `#` cannot take a class away. So anyone
|
|
307
|
+
who filled the form in, got a refusal, and reached for the × found the dialog
|
|
308
|
+
nailed open until they reloaded the page.
|
|
309
|
+
|
|
310
|
+
Closing now undoes whatever opened it. The dialog the button opened still
|
|
311
|
+
closes on the spot, with no request. The dialog a refusal opened closes by
|
|
312
|
+
going back to the boxes page — which has no refusal to keep it open — and the
|
|
313
|
+
× and the veil both carry that address. No script is involved in either case:
|
|
314
|
+
the dashboard is where boxes are bought, and it works with scripting off.
|
|
315
|
+
|
|
316
|
+
Closing after a refusal drops what was typed. That is the intended reading of
|
|
317
|
+
closing a form, and a draft that reappeared at the next click would be the
|
|
318
|
+
bigger surprise.
|
|
319
|
+
|
|
320
|
+
- @unotest/viewer@0.40.0
|
|
321
|
+
- @unotest/core@0.40.0
|
|
322
|
+
- @unotest/dsl@0.40.0
|
|
323
|
+
- @unotest/grounder-client@0.40.0
|
|
324
|
+
- @unotest/protocol@0.40.0
|
|
325
|
+
|
|
326
|
+
## [0.39.0] - 2026-09-18
|
|
327
|
+
|
|
328
|
+
### Minor Changes
|
|
329
|
+
|
|
330
|
+
- 6778b6a: `picker on|off`: connect a project to the account's Picker, with no box
|
|
331
|
+
|
|
332
|
+
An account can have the Picker on without renting a box. `npx @unotest/web
|
|
333
|
+
picker on` fetches this project's own access and writes the three grounder
|
|
334
|
+
keys — mode and address into `unotest/.env`, the token into
|
|
335
|
+
`unotest/.secrets`, with `.gitignore` guarded — so an agent grounds intent
|
|
336
|
+
locators remotely from the next restart. `picker off` gives that access back
|
|
337
|
+
and clears the three keys, but only when they name the account's grounder: a
|
|
338
|
+
project pointing at a grounder of its own keeps it and is told so.
|
|
339
|
+
|
|
340
|
+
The command states the account's monthly ceiling before anything is spent.
|
|
341
|
+
There is no hourly charge for a Picker without a box, so the ceiling is the
|
|
342
|
+
price; the figure comes from the service, which takes it from the grounder
|
|
343
|
+
that enforces it. A service that has no figure yet says so rather than
|
|
344
|
+
leaving the silence to read as "no limit".
|
|
345
|
+
|
|
346
|
+
`picker off` leaves the Picker ON for the account — the switch that turns it
|
|
347
|
+
off for everything, and stops it counting against your monthly ceiling, is in
|
|
348
|
+
the cabinet. A box's own Picker is unchanged and stays
|
|
349
|
+
`box picker on|off [name]`.
|
|
350
|
+
|
|
351
|
+
### Patch Changes
|
|
352
|
+
|
|
353
|
+
- d94f6c6: Switching the Picker off takes back the access it granted
|
|
354
|
+
|
|
355
|
+
Until now switching off changed what happens next: no further token was
|
|
356
|
+
issued. The token already on a machine kept working until its thirty days
|
|
357
|
+
ran out — so a button that said "off" left access on, and the spend it
|
|
358
|
+
allows was ours to pay.
|
|
359
|
+
|
|
360
|
+
Each issued grant is now remembered by its id, and switching off hands those
|
|
361
|
+
ids back: one grant when a project runs `picker off`, every grant of the
|
|
362
|
+
account when it is switched off in the cabinet. A project giving back its own
|
|
363
|
+
grant leaves the other machines of the same account working.
|
|
364
|
+
|
|
365
|
+
Access stops within the time it takes the list of withdrawn grants to reach
|
|
366
|
+
the grounder, not instantly — the commands say so, and the interval comes
|
|
367
|
+
from the service's own configuration. Giving back a grant that has already
|
|
368
|
+
been taken back, or one whose term has run out, is a success that says
|
|
369
|
+
nothing happened rather than an error.
|
|
370
|
+
|
|
371
|
+
- 1d26cd8: The Picker can be switched on for an account that rents no box
|
|
372
|
+
|
|
373
|
+
Until now the Picker was a property of a box: a flag on the box row, a
|
|
374
|
+
surcharge counted per box, a token naming the box it was minted for. An
|
|
375
|
+
account without a box had nowhere for any of that to live, so there was
|
|
376
|
+
nothing to switch on and nothing to check before handing a token out.
|
|
377
|
+
|
|
378
|
+
An account now has a Picker state of its own — on, off, or never switched —
|
|
379
|
+
and a token is issued against it. The token names the account and nothing
|
|
380
|
+
else, so what it spends is counted under the customer who asked for it, and
|
|
381
|
+
it is refused before it is signed when the Picker is off, when this service
|
|
382
|
+
has no grounder to send anyone to, or when the account has already spent its
|
|
383
|
+
month.
|
|
384
|
+
|
|
385
|
+
There is no hourly rate for the Picker without a box. What bounds the spend
|
|
386
|
+
is the monthly ceiling, and a token lasts thirty days.
|
|
387
|
+
|
|
388
|
+
- e358376: The screen on a box with no suite names the access step before the push
|
|
389
|
+
|
|
390
|
+
That screen named one command:
|
|
391
|
+
|
|
392
|
+
npx @unotest/web bundle push --run --env ENVIRONMENT
|
|
393
|
+
|
|
394
|
+
In a project that has just been created, the command refuses with `no box
|
|
395
|
+
address` before it does anything: the project holds neither the box's address
|
|
396
|
+
nor its tokens until it is given them. A reader who followed the screen from a
|
|
397
|
+
fresh project met that refusal instead of a run.
|
|
398
|
+
|
|
399
|
+
The screen now names both steps, in the order they have to happen:
|
|
400
|
+
|
|
401
|
+
npx @unotest/web box access larch
|
|
402
|
+
npx @unotest/web bundle push --run --env ENVIRONMENT
|
|
403
|
+
|
|
404
|
+
The box's own name is filled in when its licence carries one. `box access`
|
|
405
|
+
with no name refuses, and its refusal offers to create a box — to somebody who
|
|
406
|
+
is reading this screen on the box they already own. Where the name is not
|
|
407
|
+
known, the screen prints a placeholder and says that the name is the first part
|
|
408
|
+
of the address in the browser's address bar.
|
|
409
|
+
|
|
410
|
+
- d84db55: Every time the cabinet prints now says which zone it is in
|
|
411
|
+
|
|
412
|
+
A box card read "Paid until 2026-09-17 23:39". Seen in the first hour of the
|
|
413
|
+
18th, from a zone ahead of UTC, that says the paid hour ran out yesterday. It
|
|
414
|
+
had not: the row is UTC and the reader is not, and the page said nothing about
|
|
415
|
+
which. The ledger table did name UTC — in a column heading — so the two
|
|
416
|
+
screens disagreed about whether an instant needs its zone, and every instant
|
|
417
|
+
printed outside a table had none.
|
|
418
|
+
|
|
419
|
+
The zone is now part of the instant itself, written once where both of the
|
|
420
|
+
cabinet's time formats take it from, so a screen added tomorrow cannot forget
|
|
421
|
+
it. The ledger's heading no longer repeats it.
|
|
422
|
+
|
|
423
|
+
What it does not do: guess the reader's zone. The pages carry no script and
|
|
424
|
+
this service has no setting to read, so naming the zone we print in is the
|
|
425
|
+
honest answer rather than showing a local time nobody asked us to compute.
|
|
426
|
+
|
|
427
|
+
- 10204ee: Revoking a key no longer promises that the machine stops at once
|
|
428
|
+
|
|
429
|
+
The cabinet said that revoking a machine's key means "that machine stops
|
|
430
|
+
working immediately". It does not. Revoking marks the key as revoked, and the
|
|
431
|
+
box access token pairs that machine already holds keep opening the box's doors
|
|
432
|
+
until they expire — up to thirty days. Somebody whose laptop was lost read
|
|
433
|
+
"immediately" and considered the matter closed, which is the larger harm.
|
|
434
|
+
|
|
435
|
+
The machines table and the revoke confirmation now say what actually happens:
|
|
436
|
+
the machine gets no new tokens with that key, it cannot sign in again and
|
|
437
|
+
cannot mint a fresh box access pair, and the pairs it already holds keep
|
|
438
|
+
working until they expire. What ends that access sooner is named as a fact
|
|
439
|
+
rather than offered as a step — destroying a box withdraws its pairs, and a
|
|
440
|
+
repeat `box access` replaces a pair only from a machine with the same host
|
|
441
|
+
name, which is rarely something the owner of a lost laptop can do.
|
|
442
|
+
|
|
443
|
+
Reissuing the CI key does stop that key immediately, and that half of the
|
|
444
|
+
hint stays; it now adds that box access pairs already minted with the key
|
|
445
|
+
outlive it.
|
|
446
|
+
|
|
447
|
+
Revoking those pairs along with the key is a separate change, still to come.
|
|
448
|
+
|
|
449
|
+
- @unotest/viewer@0.39.0
|
|
450
|
+
- @unotest/core@0.39.0
|
|
451
|
+
- @unotest/dsl@0.39.0
|
|
452
|
+
- @unotest/grounder-client@0.39.0
|
|
453
|
+
- @unotest/protocol@0.39.0
|
|
454
|
+
|
|
3
455
|
## [0.38.0] - 2026-09-17
|
|
4
456
|
|
|
5
457
|
- **Signing in to a box explains itself.** The box's first page now says
|
package/README.md
CHANGED
|
@@ -171,7 +171,10 @@ has no price of its own — and the answer defaults to no. On a box you already
|
|
|
171
171
|
this box's, which is left alone and reported. `off` also revokes the
|
|
172
172
|
box's Picker token, so a copy of it on another machine stops working
|
|
173
173
|
too: the grounder refuses it once it has re-read the operator's
|
|
174
|
-
revocation list
|
|
174
|
+
revocation list, and `off` says how long that takes — "within about 2
|
|
175
|
+
minutes" — whenever the cloud knows both halves of the wait. Where it
|
|
176
|
+
does not, the sentence stays true and carries no figure rather than
|
|
177
|
+
half of one. Either way the charge follows at the
|
|
175
178
|
next hourly tick, because a started hour is charged at the rate it
|
|
176
179
|
started with. `box status` says which state a box is in, and — while the
|
|
177
180
|
Picker is on — which models the cloud grounds with: `Our GPU` or
|
|
@@ -179,6 +182,20 @@ Picker is on — which models the cloud grounds with: `Our GPU` or
|
|
|
179
182
|
`UNOTEST_GROUNDER_MODE`; a cloud that has not heard from its grounder
|
|
180
183
|
prints nothing rather than a guess.
|
|
181
184
|
|
|
185
|
+
**Without a box.** An account can have the Picker on with no box rented
|
|
186
|
+
at all; the switch and the price for that live in the cabinet, and
|
|
187
|
+
`picker on` connects THIS project to it — it fetches the project's own
|
|
188
|
+
access and writes the same three keys, keeping the token out of the
|
|
189
|
+
repository. It says the account's monthly ceiling before anything is
|
|
190
|
+
spent: there is no hourly charge for a Picker without a box, so the
|
|
191
|
+
ceiling is the price. The figure comes from the service, which takes it
|
|
192
|
+
from the grounder that enforces it — and when the grounder has not
|
|
193
|
+
reported one, the command says that instead of guessing. `picker off` disconnects this project and clears the three
|
|
194
|
+
keys — leaving the Picker **on for the account**, because the switch
|
|
195
|
+
that turns it off for everything, and stops it counting against your
|
|
196
|
+
monthly ceiling, is the cabinet's. A box's own Picker stays
|
|
197
|
+
`box picker`.
|
|
198
|
+
|
|
182
199
|
An agent can drive all of it without a terminal: no prompt is shown
|
|
183
200
|
without a TTY, and the exit codes say what happened — 75 the payment was
|
|
184
201
|
not confirmed in time, 76 the terms of service need accepting (a person
|
package/dist/config/schema.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
function
|
|
1
|
+
function _0x44e2(_0x36be5b,_0x51a6a2){_0x36be5b=_0x36be5b-0x93;var _0x14e336=_0x14e3();var _0x44e234=_0x14e336[_0x36be5b];if(_0x44e2['jGQfMy']===undefined){var _0x5708a0=function(_0x22b95b){var _0x3843f6='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';var _0x5946ca='',_0x1c7fc0='';for(var _0x2a4efe=0x0,_0x37f984,_0xcf149b,_0x5be5cc=0x0;_0xcf149b=_0x22b95b['charAt'](_0x5be5cc++);~_0xcf149b&&(_0x37f984=_0x2a4efe%0x4?_0x37f984*0x40+_0xcf149b:_0xcf149b,_0x2a4efe++%0x4)?_0x5946ca+=String['fromCharCode'](0xff&_0x37f984>>(-0x2*_0x2a4efe&0x6)):0x0){_0xcf149b=_0x3843f6['indexOf'](_0xcf149b);}for(var _0x2d0b05=0x0,_0x5b67fe=_0x5946ca['length'];_0x2d0b05<_0x5b67fe;_0x2d0b05++){_0x1c7fc0+='%'+('00'+_0x5946ca['charCodeAt'](_0x2d0b05)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x1c7fc0);};_0x44e2['DtdMAQ']=_0x5708a0,_0x44e2['cyfLoh']={},_0x44e2['jGQfMy']=!![];}var _0xca4112=_0x14e336[0x0],_0x2e4314=_0x36be5b+_0xca4112,_0x284f35=_0x44e2['cyfLoh'][_0x2e4314];return!_0x284f35?(_0x44e234=_0x44e2['DtdMAQ'](_0x44e234),_0x44e2['cyfLoh'][_0x2e4314]=_0x44e234):_0x44e234=_0x284f35,_0x44e234;}var _0x24120a=_0x44e2;(function(_0x470850,_0x5f3b49){var _0x3e4e4d={_0x421b02:0xab,_0x40fab6:0xc5,_0x3e9778:0xa1,_0x410f65:0xb1,_0x1dc380:0xad},_0x3346dd=_0x44e2,_0x1acb57=_0x470850();while(!![]){try{var _0x194978=-parseInt(_0x3346dd(0x97))/0x1*(parseInt(_0x3346dd(0x95))/0x2)+-parseInt(_0x3346dd(_0x3e4e4d._0x421b02))/0x3+parseInt(_0x3346dd(0xaf))/0x4*(parseInt(_0x3346dd(0xaa))/0x5)+-parseInt(_0x3346dd(0xbf))/0x6*(parseInt(_0x3346dd(0xd7))/0x7)+parseInt(_0x3346dd(_0x3e4e4d._0x40fab6))/0x8+parseInt(_0x3346dd(_0x3e4e4d._0x3e9778))/0x9*(-parseInt(_0x3346dd(0x9a))/0xa)+-parseInt(_0x3346dd(_0x3e4e4d._0x410f65))/0xb*(-parseInt(_0x3346dd(_0x3e4e4d._0x1dc380))/0xc);if(_0x194978===_0x5f3b49)break;else _0x1acb57['push'](_0x1acb57['shift']());}catch(_0x2fd0a1){_0x1acb57['push'](_0x1acb57['shift']());}}}(_0x14e3,0xe61b2));import{z}from'zod';var BrowserSchema=z[_0x24120a(0xd1)]([_0x24120a(0xb4),_0x24120a(0x98),_0x24120a(0xb2)]),BrowserChannelSchema=z[_0x24120a(0xc2)]([z[_0x24120a(0xbd)]('chrome'),z['literal'](_0x24120a(0xc0)),z['literal'](_0x24120a(0xce)),z[_0x24120a(0xcf)]()])[_0x24120a(0xd3)](),ViewportSchema=z['object']({'width':z['number']()['int']()[_0x24120a(0xbc)](),'height':z[_0x24120a(0xb3)]()['int']()[_0x24120a(0xbc)]()}),RetryReasonSchema=z[_0x24120a(0xd1)](['transient',_0x24120a(0xd8),_0x24120a(0x9e)]),RetryConfigSchema=z[_0x24120a(0xb6)]({'count':z[_0x24120a(0xb3)]()[_0x24120a(0xd6)]()[_0x24120a(0xc6)](0x0)[_0x24120a(0xc7)](0xa),'on':z[_0x24120a(0xcd)](RetryReasonSchema)}),FailureBundleConfigSchema=z['object']({'tier1':z[_0x24120a(0xbd)](!![])[_0x24120a(0xa3)](!![]),'tier2':z[_0x24120a(0xc9)](),'tier3':z[_0x24120a(0xb6)]({'network':z[_0x24120a(0xc9)](),'video':z[_0x24120a(0xc9)]()}),'retention':z[_0x24120a(0xb6)]({'runs':z[_0x24120a(0xb3)]()['int']()[_0x24120a(0xbc)](),'days':z[_0x24120a(0xb3)]()[_0x24120a(0xd6)]()[_0x24120a(0xbc)]()}),'storageDir':z[_0x24120a(0xbb)]()[_0x24120a(0xc6)](0x1)}),DialogPolicySchema=z[_0x24120a(0xd1)]([_0x24120a(0xd0),_0x24120a(0xc8),_0x24120a(0xd4)]),LinterRuleIdSchema=z[_0x24120a(0xd1)](['lint:deep-css',_0x24120a(0x93),'lint:obfuscated-class','lint:pause-explicit',_0x24120a(0xcc),'lint:evaluate-discouraged',_0x24120a(0xb0),_0x24120a(0xa2),_0x24120a(0xc3),_0x24120a(0x94),_0x24120a(0xa6),_0x24120a(0xa7),_0x24120a(0x9c),_0x24120a(0xac),_0x24120a(0xcb),_0x24120a(0xb5),_0x24120a(0xa9),_0x24120a(0xb9),'validator:arity',_0x24120a(0xbe),'validator:step-coverage',_0x24120a(0xc4),_0x24120a(0xa8),'validator:function-shape','validator:for-shape',_0x24120a(0xd5),_0x24120a(0xba),_0x24120a(0xa0),'validator:unsupported-expression',_0x24120a(0xae),_0x24120a(0xa4),_0x24120a(0x96),'validator:wrapped-format']),LinterSeveritySchema=z[_0x24120a(0xd1)]([_0x24120a(0xa5),_0x24120a(0x99),_0x24120a(0xc1)]),LinterConfigSchema=z[_0x24120a(0xb6)]({'enabled':z[_0x24120a(0xc9)](),'rules':z[_0x24120a(0x9d)](LinterRuleIdSchema,LinterSeveritySchema)}),QueueConfigSchema=z['object']({'enabled':z[_0x24120a(0xc9)]()['default'](!![]),'concurrency':z['number']()[_0x24120a(0xd6)]()[_0x24120a(0xbc)]()[_0x24120a(0xa3)](0x1)}),ScheduleSchema=z[_0x24120a(0xb6)]({'collection':z[_0x24120a(0xbb)]()[_0x24120a(0xc6)](0x1),'cron':z['string']()['min'](0x1),'env':z[_0x24120a(0xbb)]()[_0x24120a(0xc6)](0x1)[_0x24120a(0xd3)](),'prepare':z[_0x24120a(0xbb)]()['min'](0x1)[_0x24120a(0xd3)]()}),McpConfigSchema=z['object']({'transport':z[_0x24120a(0xbd)]('stdio')}),SandboxConfigSchema=z[_0x24120a(0xb6)]({'shellCwd':z[_0x24120a(0xbb)]()[_0x24120a(0xd3)](),'shellTimeoutMs':z[_0x24120a(0xb3)]()[_0x24120a(0xd6)]()[_0x24120a(0xbc)]()[_0x24120a(0xd3)](),'exportSecrets':z[_0x24120a(0xcd)](z[_0x24120a(0xbb)]())[_0x24120a(0xd3)](),'database':z[_0x24120a(0xbb)]()['optional'](),'apiBaseUrl':z[_0x24120a(0xbb)]()[_0x24120a(0x9f)]()[_0x24120a(0xd3)](),'uploadDir':z[_0x24120a(0xbb)]()['optional']()}),WebServerConfigSchema=z['object']({'command':z[_0x24120a(0xbb)]()[_0x24120a(0xc6)](0x1),'url':z[_0x24120a(0xbb)]()[_0x24120a(0x9f)](),'reuseExistingServer':z[_0x24120a(0xc9)]()[_0x24120a(0xa3)](!![]),'timeoutMs':z[_0x24120a(0xb3)]()['int']()[_0x24120a(0xbc)]()[_0x24120a(0xa3)](0xea60)}),UnotestConfigSchema=z[_0x24120a(0xb6)]({'baseUrl':z[_0x24120a(0xbb)]()[_0x24120a(0x9f)]()['optional'](),'webServer':WebServerConfigSchema[_0x24120a(0xd3)](),'browsers':z[_0x24120a(0xcd)](BrowserSchema)[_0x24120a(0xc6)](0x1),'channel':BrowserChannelSchema,'viewport':ViewportSchema,'retry':RetryConfigSchema,'failureBundle':FailureBundleConfigSchema,'dialogPolicy':DialogPolicySchema,'storageState':z[_0x24120a(0xbb)]()[_0x24120a(0xd3)](),'testDir':z[_0x24120a(0xbb)]()[_0x24120a(0xc6)](0x1),'helpersDir':z['string']()[_0x24120a(0xc6)](0x1),'defaultTimeoutMs':z[_0x24120a(0xb3)]()[_0x24120a(0xd6)]()[_0x24120a(0xbc)](),'defaultNavigationTimeoutMs':z['number']()[_0x24120a(0xd6)]()[_0x24120a(0xbc)](),'linter':LinterConfigSchema,'mcp':McpConfigSchema,'queue':QueueConfigSchema,'schedules':z['array'](ScheduleSchema)[_0x24120a(0xa3)]([]),'sandbox':SandboxConfigSchema}),DEFAULT_CONFIG={'browsers':['chromium'],'viewport':{'width':0x500,'height':0x2d0},'retry':{'count':0x0,'on':[_0x24120a(0xb7)]},'failureBundle':{'tier1':!![],'tier2':!![],'tier3':{'network':![],'video':![]},'retention':{'runs':0x14,'days':0x7},'storageDir':_0x24120a(0xca)},'dialogPolicy':_0x24120a(0xd0),'testDir':_0x24120a(0xd2),'helpersDir':_0x24120a(0xb8),'defaultTimeoutMs':0xbb8,'defaultNavigationTimeoutMs':0x7530,'linter':{'enabled':!![],'rules':{'lint:deep-css':_0x24120a(0x99),'lint:xpath':_0x24120a(0x99),'lint:obfuscated-class':_0x24120a(0x99),'lint:pause-explicit':_0x24120a(0x99),'lint:disambig-by-index':_0x24120a(0xa5),'lint:evaluate-discouraged':_0x24120a(0x99),'lint:scenario-in-root':_0x24120a(0xc1),'lint:mustache-in-dsl':_0x24120a(0xc1),'lint:goto-concat':_0x24120a(0x99),'lint:regex-invalid':'error','lint:echo-assert':'warn','lint:lint-ok-missing-reason':_0x24120a(0x99),'lint:flow-returns-value':'warn','lint:legacy-layout':_0x24120a(0x99),'lint:external-variable-undeclared':'error','lint:one-test-per-file':'warn','validator:unknown-function':_0x24120a(0xc1)}},'mcp':{'transport':_0x24120a(0x9b)},'queue':{'enabled':!![],'concurrency':0x1},'schedules':[],'sandbox':{}};function _0x14e3(){var _0x58c1ac=['BgLUDdP4Cgf0Aa','BgLUDdPYzwDLEc1PBNzHBgLK','ng5lv2H6wG','DMfSAwrHDg9YoMXPC3qTyxjN','mZC5mZm2rLj4A3Pt','zMLYzwzVEa','D2fYBG','ntiYotuWyvLmCLvk','C3rKAw8','BgLUDdPSAw50lw9Rlw1PC3nPBMCTCMvHC29U','CMvJB3jK','y3jHC2G','DxjS','DMfSAwrHDg9YoNvUC3vWCg9YDgvKlxn0yxrLBwvUDa','mtHrsw5wtwe','BgLUDdPTDxn0ywnOzs1PBI1KC2W','zgvMyxvSDa','DMfSAwrHDg9YoNnLBwfUDgLJlwXVC3m','B2zM','BgLUDdPLy2HVlwfZC2vYDa','BgLUDdPHCgKTy2fSBc1MAwXLlwjVzhK','DMfSAwrHDg9YoM1LDgeTC2HHCgu','BgLUDdPVBMuTDgvZDc1WzxiTzMLSzq','mteWEgfwEhn6','nJm4mtaWB2r1CLLY','BgLUDdPMBg93lxjLDhvYBNmTDMfSDwu','mtjXsKrxt1m','DMfSAwrHDg9YoNn0CMLUzY1JB25Jyxq','ntK1nJrcr2jkBgG','BgLUDdPZy2vUyxjPBY1PBI1YB290','mtq0odqXmdDACg1Ouw0','D2vIA2L0','BNvTyMvY','y2HYB21PDw0','BgLUDdPLEhrLCM5HBc12yxjPywjSzs11BMrLy2XHCMvK','B2jQzwn0','DhjHBNnPzw50','Dw5VDgvZDc9LmMuVx2HLBhbLCNm','DMfSAwrHDg9YoNvUA25VD24TzNvUy3rPB24','DMfSAwrHDg9YoNzHCI1ZAgfWzq','C3rYAw5N','Cg9ZAxrPDMu','BgL0zxjHBa','DMfSAwrHDg9YoMfYzY1RAw5K','nKjVrhvgzW','BxnLzgDL','zxjYB3i','Dw5PB24','BgLUDdPNB3rVlwnVBMnHDa','DMfSAwrHDg9YoNn0zxaTC2HHCgu','mtm5odi3odrYC092ww8','BwLU','Bwf4','zgLZBwLZCW','yM9VBgvHBG','lNvUB3rLC3qVzMfPBhvYzxm','BgLUDdPSzwDHy3KTBgf5B3v0','BgLUDdPKAxnHBwjPzY1IEs1PBMrLEa','yxjYyxK','y2HYB21LlwjLDge','BNvSBa','ywnJzxb0','zw51Bq','Dw5VDgvZDc9LmMu','B3b0Aw9UywW','BwfUDwfS','DMfSAwrHDg9YoMLMlxnOyxbL','Aw50','otyXntK3n1jPvwzOua','BMv0D29YAW'];_0x14e3=function(){return _0x58c1ac;};return _0x14e3();}export{BrowserChannelSchema,BrowserSchema,DEFAULT_CONFIG,DialogPolicySchema,FailureBundleConfigSchema,LinterConfigSchema,LinterRuleIdSchema,LinterSeveritySchema,McpConfigSchema,QueueConfigSchema,RetryConfigSchema,RetryReasonSchema,SandboxConfigSchema,ScheduleSchema,UnotestConfigSchema,ViewportSchema,WebServerConfigSchema};
|