@unotest/viewer 0.25.0 → 0.26.1

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,214 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.26.1] - 2026-08-30
4
+
5
+ ### Patch Changes
6
+
7
+ - Fix: a run started from a box's viewer stayed pending and never reached
8
+ the history
9
+
10
+ The viewer watched the run directories of environments it could see on
11
+ disk — overlay files `unotest/.env.<name>` and existing `.runs.<name>`
12
+ directories. A box has neither at start: environment values come from the
13
+ daemon, not from a file, and the runs directory appears with the first
14
+ run. So the viewer watched only the base root while every run was filed
15
+ under `.runs.<environment>`: the tab stayed PENDING, the history stayed
16
+ empty and the run 404-ed by its own id, until a restart happened to find
17
+ the directory already there. The environment a viewer was started for is
18
+ now known to it by definition, whatever the disk says.
19
+
20
+ - Fix: a run's captured output landed outside the run's directory
21
+
22
+ `stdout.log` / `stderr.log` were written to `<runs-root>/<runId>/` while
23
+ the run itself lives under its date shard — two orphan directories next to
24
+ the shard, holding the only copy of what the child said.
25
+
26
+ - Fix: a viewer started for an environment listed no runs at all
27
+
28
+ The standalone launcher read the project root, the port, the host and the
29
+ target from its environment — but not `UNOTEST_ENV`. A box starts one
30
+ viewer per environment and names it exactly that way, so every one of them
31
+ came up on the BASE runs directory while its runs were being filed under
32
+ `.runs.<environment>`: an empty history on a box that had just finished a
33
+ suite. Found on the first end-to-end run of the local box stand.
34
+
35
+ - 9424f6b: Fix: screenshots stayed blank in the viewer when runs write to a separate
36
+ artifact root
37
+
38
+ A run whose artifacts do not live under the working directory — every run on
39
+ a box, where the sources are a disposable copy of a test bundle — emitted
40
+ `screenshot` events whose path was relativised against the working directory.
41
+ That came out as `../../../…`, which the viewer's asset route refuses to
42
+ serve, so every step preview rendered as a broken image. Artifact paths are
43
+ now relative to the artifact root, which is still the project root on every
44
+ local run.
45
+
46
+ The failure bundle had the mirror-image problem: it was written to
47
+ `.unotest/failures/` under the working directory, so on a box it landed
48
+ inside the bundle copy — outside the tree the viewer serves, and discarded on
49
+ the next bundle push. It now hangs off the artifact root too, which leaves
50
+ local runs byte-for-byte unchanged.
51
+
52
+ The inspector's Semantic DOM, Console, Network and Snapshot panes were dark
53
+ for a third reason, and this one bit local runs as well: they composed the
54
+ run's directory in the browser and left out the date shard runs have been
55
+ filed under since M-10, plus the environment suffix on a box. The viewer's
56
+ asset route now takes `?file=<name-relative-to-the-run-dir>` and resolves the
57
+ directory itself — runs root, target suffix, environment suffix and shard are
58
+ all things only the server knows. `?path=` stays for the whole paths the
59
+ server ships on run events.
60
+
61
+ - Updated dependencies [9424f6b]
62
+ - @unotest/protocol@0.26.1
63
+ - @unotest/core@0.26.1
64
+ - @unotest/dsl@0.26.1
65
+
66
+ ## [0.26.0] - 2026-08-30
67
+
68
+ ### Minor Changes
69
+
70
+ - 3eede7c: The viewer can now be hosted: it shows which test bundle an environment
71
+ is running, and lets you switch it.
72
+
73
+ This is the other half of `bundle push`. Nothing changes for a local
74
+ viewer — everything here appears only when something is hosting it.
75
+
76
+ - **The artifact root and the project root are now genuinely separate.**
77
+ The viewer already took `UNOTEST_ARTIFACTS_ROOT` for the run queue;
78
+ run discovery, the run index, snapshots, log capture and the viewer's
79
+ own lock file now honour it too. Locally the two are the same directory
80
+ and nothing moves. Where they differ — sources in a disposable copy of a
81
+ bundle, history in the environment beside it — the history is no longer
82
+ read out of (or written into) the sources.
83
+ - **Bundle badge in the status bar.** `main@1a2b3c4`, or `wip` for a
84
+ bundle packed from an uncommitted tree, with the author, the pinned
85
+ `@unotest/web` and who switched the environment last. Clicking it lists
86
+ the bundles pushed to this environment and switches to one — that runs
87
+ as a maintenance ticket on the host, so it waits for the environment to
88
+ be free instead of pulling the code out from under a running suite.
89
+ - **Runs remember their bundle.** `manifest.json` and `GET /api/runs`
90
+ carry `bundleId` (from `UNOTEST_BUNDLE_ID`, set by the host); the runs
91
+ list marks the ones that ran on another bundle, so "was this red run
92
+ even the code I am looking at?" has an answer.
93
+ - **The contract is a file, not a mode.** `@unotest/protocol` gains
94
+ `WorkspaceFile` — the host writes `unotest/.workspace.json` beside the
95
+ artifacts and the viewer reads it, the same way the guard's session
96
+ header narrows the UI. No file, no bundle UI; the viewer never learns
97
+ what a box is. Values of environment variables and secrets never appear
98
+ in it — only their names.
99
+ - **Queue tickets can name a bundle.** `QueueTicket.source.bundleId` is
100
+ recorded when a run is queued, so a run executes the code it was
101
+ ordered with even if the environment moved on while it waited.
102
+
103
+ - dbfa36f: Your CI can now ask a box to run the suite it just pushed, and a box can
104
+ report the result back to GitHub.
105
+
106
+ ```sh
107
+ npx @unotest/web bundle push --run --env test --collection smoke
108
+ ```
109
+
110
+ `--run` sends the second half of the CI recipe: upload the bundle, then
111
+ order a run of it. Two requests rather than one flag on the upload,
112
+ because a bundle is _content_ — pushing an identical tree twice is normal
113
+ and answers `exists` — while "run it" is an event. The same split is what
114
+ lets a deploy script run a bundle somebody pushed hours ago.
115
+
116
+ - **The box answers as soon as the runs are queued** (202) with their run
117
+ ids. A suite takes minutes to hours; an HTTP request held open for that
118
+ long is a timeout in somebody's proxy, not a result. The ids are real
119
+ from the moment you get them, so a CI log can print a link into the
120
+ viewer straight away.
121
+ - **`--env <name>` is required with `--run`.** Which environments a box
122
+ has is the operator's decision, and guessing one would be guessing
123
+ which system gets tested.
124
+ - **Without `--collection`, the box runs what that environment normally
125
+ runs** — the collections its `schedules` name. Naming them explicitly is
126
+ the deploy-check case; leaving it out is the "same as every night" case.
127
+ - **`--pr <number>` coalesces.** A newer push of the same pull request
128
+ withdraws its older runs that are still _waiting_; a run that already
129
+ started is never killed. Three pushes in five minutes cost one suite.
130
+ `--json` reports how many were withdrawn, so "my run vanished" has an
131
+ answer in the log of the run that replaced it.
132
+ - **Refusals are typed and say what to do**: no such environment, an
133
+ environment belonging to another project, a bundle the box no longer
134
+ holds, nothing to run.
135
+
136
+ On a hosted viewer, the queue panel now names the change each machine
137
+ ticket belongs to (`PR #412@1a2b3c4`) instead of a row of identical "ci"
138
+ entries, and the history gained a **this bundle only** filter for the
139
+ moment you are judging the bundle in front of you rather than the last
140
+ month of runs.
141
+
142
+ Nothing changes for a local `npx @unotest/web viewer`: there are no
143
+ bundles there, so neither the badge nor the filter appears.
144
+
145
+ - c43aa00: Runs of one project now take turns instead of colliding.
146
+
147
+ Until now nothing coordinated them: the viewer refused a second run with
148
+ HTTP 409, and that was the whole defence — a `npx @unotest/web e2e` in a
149
+ terminal, an agent's `run_test` and a scheduled suite would all start on
150
+ top of each other, driving the same browser and the same seeded database
151
+ at the same time. The 409 protected the UI, not the machine.
152
+
153
+ - **A filesystem queue**, in `unotest/.queue<target>[.<env>]/` beside the
154
+ runs root it belongs to. A run writes a ticket, waits until it is at
155
+ the head, takes a slot, runs, and gives the slot back. There is no
156
+ daemon: whoever holds the slot executes the run itself, so nothing new
157
+ has to be installed, started or kept alive. Only `open("wx")`, rename,
158
+ unlink and mtime — no `flock`, which lies on network and container
159
+ filesystems. A crashed producer is reaped by whoever comes next.
160
+ - **Every producer is in it**: the CLI (`e2e`, `collection`, `author`),
161
+ the viewer's Run button, and the MCP `run_test` (which spawns the CLI).
162
+ A collection is ONE run — its `workers` still run in parallel inside
163
+ it, because a child process carries its parent's lease and skips the
164
+ queue.
165
+ - **The viewer shows the queue**: the Active panel lists what is waiting,
166
+ including tickets a terminal or an agent wrote, with a button to take
167
+ any of them back out. `GET /api/queue`, `DELETE /api/queue/:ticket`,
168
+ and a `queue:changed` websocket message.
169
+ - **Config**: `queue.concurrency` (default 1) and `queue.enabled`
170
+ (default true) in `unotest.config`. Both the CLI and the viewer read
171
+ the same file, so they cannot disagree about how many slots exist.
172
+ - **Escape hatches**: `UNOTEST_NO_QUEUE=1` runs without queueing;
173
+ Ctrl-C while waiting gives up your place in line and runs nothing.
174
+ - **For boxes**: `UNOTEST_QUEUE_GLOBAL_DIR` + `UNOTEST_QUEUE_GLOBAL_SLOTS`
175
+ add a host-wide budget several environments share (a collection weighs
176
+ its worker count), and `UNOTEST_ARTIFACTS_ROOT` puts `.runs` / `.queue`
177
+ somewhere other than the directory the tests live in — so the history
178
+ outlives a throwaway copy of the tests.
179
+
180
+ ### Migration notes
181
+
182
+ **`POST /api/run` no longer answers 409.** Ordering a run while another
183
+ one is active used to fail with `ActiveRunConflictError` (HTTP 409); it
184
+ now succeeds and the run waits. The response changed shape with it:
185
+
186
+ ```diff
187
+ - { runId, kind, ref, pid, startedAt }
188
+ + { runId, kind, ref, status: "queued" | "running", ticket, acceptedAt }
189
+ ```
190
+
191
+ `runId` is still final and immediately usable — open the tab on it as
192
+ before — but the run may not have started yet, and `pid` is not known at
193
+ that point. `POST /api/runs/:runId/abort` covers both states: it
194
+ withdraws a waiting ticket or SIGTERMs a running child.
195
+
196
+ Anything that treated 409 as "busy, try later" should now simply order
197
+ the run. Anything that relied on "only one run can exist" should read
198
+ `GET /api/queue`. To keep the old immediate-start behaviour (without the
199
+ protection), set `queue.enabled: false`.
200
+
201
+ ### Patch Changes
202
+
203
+ - Updated dependencies [3eede7c]
204
+ - Updated dependencies [6d1c612]
205
+ - Updated dependencies [dbfa36f]
206
+ - Updated dependencies [c43aa00]
207
+ - Updated dependencies [8ec4177]
208
+ - @unotest/protocol@0.26.0
209
+ - @unotest/core@0.26.0
210
+ - @unotest/dsl@0.26.0
211
+
3
212
  ## [0.25.0] - 2026-08-29
4
213
 
5
214
  ### Minor Changes
@@ -1,5 +1,13 @@
1
1
  import { IRunnerAdapter, IDslLanguageService, IProcessLauncher } from '@unotest/protocol';
2
2
 
3
+ interface ViewerQueueConfig {
4
+ /** With queueing off, the viewer starts every run immediately — and
5
+ * so does every terminal. Nothing serialises them. */
6
+ readonly enabled: boolean;
7
+ /** Runs allowed in flight for one environment. */
8
+ readonly concurrency: number;
9
+ }
10
+
3
11
  interface RunnerEntry {
4
12
  /** Short target id derived from the runner package name —
5
13
  * `web`/`mobile`/`unity`. Drives the UI switcher and per-target tree
@@ -38,6 +46,14 @@ interface StartViewerOptions {
38
46
  * injects its bundled `node` + runner bin so terminal commands resolve
39
47
  * without a system install). */
40
48
  terminalPathPrefix?: readonly string[];
49
+ /** Run-queue policy, read from the project's `unotest.config` by the
50
+ * launcher. Omitted (the standalone bin) = the shipped default: queue
51
+ * on, one run at a time. Both the viewer and the CLI must see the SAME
52
+ * numbers — they share one queue directory. */
53
+ queue?: ViewerQueueConfig;
54
+ /** Absolute artifact root when it is not the project root
55
+ * (`UNOTEST_ARTIFACTS_ROOT`); see `ViewerConfig.artifactsRoot`. */
56
+ artifactsRoot?: string;
41
57
  }
42
58
  interface ViewerHandle {
43
59
  url: string;
@@ -57,6 +73,14 @@ declare function startViewerServer(opts: StartViewerOptions): Promise<ViewerHand
57
73
  * the installed `@unotest/*` runner packages (by name — DIP boundary
58
74
  * intact). `UNOTEST_RUNNER_PKG` (legacy single-runner var) and
59
75
  * `UNOTEST_TARGET` just pick which detected target is active at boot. */
76
+ /** Everything the standalone launcher reads from its environment, in one
77
+ * pure place so the plumbing is testable without starting a server.
78
+ *
79
+ * `activeEnv` is the one that has bitten: a box starts one viewer per
80
+ * environment and says which through `UNOTEST_ENV`. Dropping it left every
81
+ * viewer reading the BASE runs directory while its runs were filed under
82
+ * `.runs.<env>` — an empty history on a box that had just run a suite. */
83
+ declare function viewerCliOptions(env: NodeJS.ProcessEnv, cwd: string): Omit<StartViewerOptions, "runners">;
60
84
  declare function runViewerCli(): Promise<void>;
61
85
 
62
- export { type RunnerEntry, type StartViewerOptions, type ViewerHandle, runViewerCli, startViewerServer };
86
+ export { type RunnerEntry, type StartViewerOptions, type ViewerHandle, runViewerCli, startViewerServer, viewerCliOptions };