browser-broker 0.1.0 → 0.2.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/.env.example CHANGED
@@ -156,7 +156,9 @@
156
156
  # browser named in both lists above, so a browser added here gains its own
157
157
  # discovery and keeper-tab lines in the health report.
158
158
 
159
- # Which browser binary the signed-in browsers launch.
159
+ # Which engine the signed-in browsers name. Does not decide which binary
160
+ # launches: per-engine executable resolution is not built, so every browser
161
+ # launches the automation library's own Chromium. See RELEASES.md.
160
162
  # Options: chrome, brave, msedge — all Chromium over the same remote-debugging
161
163
  # protocol, which is why the choice is a path rather than a driver.
162
164
  # One engine per kind, never per browser: an engine per entry would put an
@@ -164,10 +166,9 @@
164
166
  # Default: msedge
165
167
  # BROKER_REGULAR_BROWSER_ENGINE=msedge
166
168
 
167
- # Which browser binary the clean-room browsers launch. MAY DIFFER from the
168
- # engine above: a person signs into the signed-in browsers by hand, so which
169
- # binary those are can follow what they already use, while nobody signs into
170
- # a clean-room browser at all.
169
+ # Which engine the clean-room browsers name. Accepted and validated, and read
170
+ # by nothing: one driver serves every browser in the process and only the
171
+ # regular engine is handed to it, so a differing value reaches no launch.
171
172
  # Options: chrome, brave, msedge
172
173
  # Default: msedge
173
174
  # BROKER_PRIVATE_BROWSER_ENGINE=msedge
package/README.md CHANGED
@@ -61,23 +61,33 @@ hanging the report.
61
61
  service to keep running: the process is started by whatever calls it and exits with it. So getting it
62
62
  working is an install and one more fetch below — there is no step after that.
63
63
 
64
- You need **Node 22.18 or newer**.
64
+ You need **Node 22.18 or newer**. Either path below also needs a browser binary, which is a separate
65
+ fetch from either install step — see [Browser binary](#browser-binary) once you've picked a path.
65
66
 
66
67
  ### From the registry
67
68
 
68
- The package ships compiled JavaScript, so nothing is built on your machine:
69
+ The package ships compiled JavaScript, so **nothing is compiled** on your machine. That is not the
70
+ same as nothing to set up: a browser binary is still owed, and this path does not fetch it — see
71
+ [Browser binary](#browser-binary) below before your first `broker doctor`.
69
72
 
70
73
  ```bash
71
- npx browser-broker doctor
74
+ npx -p browser-broker broker doctor
72
75
  ```
73
76
 
77
+ The package installs two executables — `broker`, the command line, and `broker-tool`, the surface a
78
+ client spawns — and neither is named for the package, so `npx browser-broker` cannot tell which you
79
+ meant and refuses. `-p` names the package and the word after it names the executable.
80
+
74
81
  A client that spawns the tool surface names the same package, and npm revalidates the version on
75
82
  every run — so a published release arrives without anything being pulled or rebuilt by hand:
76
83
 
77
84
  ```json
78
85
  {
79
86
  "mcpServers": {
80
- "browser-broker": { "command": "npx", "args": ["-y", "browser-broker"] }
87
+ "browser-broker": {
88
+ "command": "npx",
89
+ "args": ["-y", "-p", "browser-broker", "broker-tool"]
90
+ }
81
91
  }
82
92
  }
83
93
  ```
@@ -97,20 +107,8 @@ cd browser-broker
97
107
  npm install
98
108
  ```
99
109
 
100
- That compiles the one runtime dependency's native binding, which is *not* the only part of the install
101
- that does real work: this repository depends on `playwright-core`, not the full `playwright`
102
- distribution, precisely because the browser binary is spawned by this service, detached and by path,
103
- rather than downloaded and managed by the package. `playwright-core` does not fetch a browser on
104
- install, so a checkout that has never had one fetched by some other tooling has none, and `broker
105
- doctor`'s automation check will genuinely fail with exit code 11 until you run:
106
-
107
- ```bash
108
- npx playwright-core install chromium
109
- ```
110
-
111
- Run this once per machine, before the first `broker doctor`. It is the same install mechanism the
112
- full `playwright` package would run automatically on `npm install`; `playwright-core` just does not
113
- run it for you. Then run the broker itself:
110
+ That compiles the one runtime dependency's native binding. It does not fetch a browser binary see
111
+ [Browser binary](#browser-binary) below before your first `broker doctor`. Then run the broker itself:
114
112
 
115
113
  ```bash
116
114
  node src/bin/broker.ts
@@ -134,6 +132,23 @@ To get the command on your path as `broker`, link the package from the checkout:
134
132
  npm link # then: broker --help
135
133
  ```
136
134
 
135
+ ### Browser binary
136
+
137
+ **Neither install path above fetches a browser.** This repository depends on `playwright-core`, not
138
+ the full `playwright` distribution, precisely because the browser binary is spawned by this service,
139
+ detached and by path, rather than downloaded and managed by the package. `playwright-core` does not
140
+ fetch a browser on install, so a machine that has never had one fetched by some other tooling has
141
+ none, and `broker doctor`'s automation check will genuinely fail with exit code 11 until you run:
142
+
143
+ ```bash
144
+ npx playwright-core install chromium
145
+ ```
146
+
147
+ Run this once per machine, before the first `broker doctor` — it applies whether you installed from
148
+ the registry or from a checkout, because it is a fetch neither install step performs. It is the same
149
+ install mechanism the full `playwright` package would run automatically on `npm install`;
150
+ `playwright-core` just does not run it for you.
151
+
137
152
  ### Configuring it
138
153
 
139
154
  **Nothing needs setting.** Every value is an environment variable with a working default, so the
@@ -156,9 +171,16 @@ called, and a caller that names no browser gets the first signed-in one:
156
171
  BROKER_REGULAR_BROWSERS=regular,checkout # persistent, signed in, at most 3
157
172
  BROKER_PRIVATE_BROWSERS=private # ephemeral, at most 3
158
173
  BROKER_REGULAR_BROWSER_ENGINE=msedge # chrome | brave | msedge
159
- BROKER_PRIVATE_BROWSER_ENGINE=msedge # may differ from the line above
174
+ BROKER_PRIVATE_BROWSER_ENGINE=msedge # accepted and validated, and read by nothing
160
175
  ```
161
176
 
177
+ **Neither engine variable changes which binary launches.** Resolving an engine name to an executable
178
+ is deliberately not built, so every browser launches the automation library's own Chromium whichever
179
+ engine is named, and a machine still has to fetch that Chromium once. The private variable goes one
180
+ step less far than the regular one: a single driver serves every browser in the process and only the
181
+ regular engine is handed to it, so the private value is validated at startup and then read by nothing
182
+ at all. `RELEASES.md` has the fuller account.
183
+
162
184
  Two signed-in browsers is how two identities are exercised at once: tabs within one browser share
163
185
  its cookie jar, so they are isolated from other browsers and not from each other. **Note that each
164
186
  browser is a process before it holds a single tab**, which the tab budget does not count —
@@ -198,6 +220,24 @@ it is rarely the checkout. Nothing else is required: there is no port to configu
198
220
  issue, and no process to have started first — the client starts it, and it exits when the client
199
221
  closes the pipe.
200
222
 
223
+ **If that configuration file is itself synchronised between machines, an absolute path is the one
224
+ thing in it that cannot travel.** A home directory differs per machine and often per user, so one
225
+ entry naming a checkout is correct on the machine it was written on and names nothing on the other
226
+ — where it fails as a connection that closes immediately, which reads as a broken service rather
227
+ than as a path that does not exist. Give each machine its own entry under its own server name, or
228
+ point the shared entry at the published package, which carries no machine's path:
229
+
230
+ ```json
231
+ {
232
+ "mcpServers": {
233
+ "browser-broker": {
234
+ "command": "npx",
235
+ "args": ["-y", "-p", "browser-broker", "broker-tool"]
236
+ }
237
+ }
238
+ }
239
+ ```
240
+
201
241
  To point it at a store other than the default, add the environment to the same block:
202
242
 
203
243
  ```json
package/RELEASES.md CHANGED
@@ -1,97 +1,255 @@
1
- # Release notes
2
-
3
- **What changes between versions, and specifically what changes *behaviour* on an installation that
4
- sets nothing.**
5
-
6
- That second half is the reason this file exists rather than the commit log being enough. A new
7
- setting with a neutral default is not news: an installation that does not set it behaves exactly as
8
- before. **A changed default is news**, because it moves an installation that has taken no action and
9
- made no decision. `docs/plans/DECISIONS.md` §6.3 puts a changed default here rather than in a quiet
10
- edit for that reason.
11
-
12
- Entries are newest first. Each names what moved, what an installation has to do about it, and what
13
- happens if it does nothing.
14
-
15
- ---
16
-
17
- ## Unreleased
18
-
19
- ### The package is published, and it ships compiled JavaScript
20
-
21
- **What moved.** The service is installable from the registry as `browser-broker`, so a caller can
22
- spawn it without a checkout. The manifest's `bin` entries now name emitted JavaScript under `dist/`
23
- rather than the TypeScript sources.
24
-
25
- **Why the build exists**, given that the development path deliberately has none: **Node refuses to
26
- strip types from any file under a `node_modules` path**, and an installed package is a directory
27
- under `node_modules`. A manifest whose `bin` named a `.ts` file would install cleanly and then fail
28
- on the machine of whoever installed it. There is no flag that changes this. The compiler therefore
29
- runs once per release rather than on every machine that consumes the package, and `erasableSyntaxOnly`
30
- stays on so the sources still run unbuilt — the two paths execute the same dialect.
31
-
32
- **What an installation has to do.** Nothing. A checkout is unaffected: `node src/bin/broker.ts` still
33
- runs the sources with no build. An installation that would rather not track a checkout can point at
34
- the package instead, and npm revalidates the version on every spawn:
35
-
36
- ```json
37
- { "command": "npx", "args": ["-y", "browser-broker"] }
38
- ```
39
-
40
- **Worth weighing before switching:** `npx` performs a registry round-trip on every spawn, costing
41
- seconds where a path on disk costs a fraction of one. It buys an upgrade path, not speed.
42
-
43
- **One surface changes what it reports.** The tool handshake's `serverInfo.version` was the literal
44
- `0.0.0` while the package was unversioned, and now reads the manifest — so a client logging it sees
45
- the released version rather than a placeholder.
46
-
47
-
48
- ### Behaviour change: the default browser engine is Edge
49
-
50
- **What moved.** A browser launched by this service uses **`msedge`** by default. The previous
51
- behaviour was to launch whatever `chromium.executablePath()` resolved to the Chromium build the
52
- automation library had fetched.
53
-
54
- **Why.** Edge is present on every Windows machine, so a fresh install runs with nothing set, with no
55
- separate browser download step. That is `DECISIONS.md` §6.1's *"a fresh install runs with nothing
56
- set"* applied to the one prerequisite `npm install` genuinely could not cover.
57
-
58
- **What an installation has to do.** Nothing, if Edge is acceptable. To keep the prior behaviour, or
59
- to pick a different browser, set the engine per kind:
60
-
61
- ```bash
62
- BROKER_REGULAR_BROWSER_ENGINE=chrome # chrome | brave | msedge
63
- BROKER_PRIVATE_BROWSER_ENGINE=chrome # may differ from the line above
64
- ```
65
-
66
- **If it does nothing:** browsers launch under Edge. **Profiles are per browser and are not shared
67
- between engines**, so an installation whose signed-in profile was established under a different
68
- binary will find that browser signed out, and a person will be asked to sign in once more with
69
- `broker login`. Nothing is destroyed `setup.profile_never_destroyed` still holds, and the earlier
70
- profile directory is left exactly where it is.
71
-
72
- ### Browsers are a configured list, and `browser` on a claim is optional
73
-
74
- **What moved.** Two things, both reversals of recorded decisions — the argument for each is in
75
- `DECISIONS.md` §13i:
76
-
77
- - **The fixed pair of browsers becomes a bounded list per kind**, at most three each, named in
78
- configuration. A name is what a caller claims by and what its profile directory is called.
79
- - **`browser` on `browser_claim` becomes optional.** Unstated resolves to the first signed-in
80
- browser; `regular` or `private` resolves to the first of that kind; a configured name resolves to
81
- that browser exactly.
82
-
83
- **What an installation has to do.** Nothing. The defaults name one browser of each kind, `regular`
84
- and `private`, which is the pair that existed before so an installation that sets nothing has the
85
- same two browsers under the same two names, and a caller that states `browser` explicitly is
86
- unaffected.
87
-
88
- **Worth reading before configuring more than the default two:** the tab budget counts *tabs*, and
89
- each browser costs a process **before it holds a single tab**. `BROKER_TAB_BUDGET=15` with six
90
- browsers is six browser processes, plus up to fifteen tabs, plus six keeper tabs that are not
91
- counted against the budget at all. `.env.example` states this beside the variables.
92
-
93
- **One schema step.** The store gains a `kind` column on `browsers` and drops the check constraint
94
- that limited a browser's name to two literals. It is applied on the next spawn, like every step, and
95
- the two existing rows are backfilled to their own kinds. **A store stepped by this build is not
96
- readable by an earlier one**, which is the ordinary direction — a build refuses a store newer than
97
- itself rather than downgrading it.
1
+ # Release notes
2
+
3
+ **What changes between versions, and specifically what changes *behaviour* on an installation that
4
+ sets nothing.**
5
+
6
+ That second half is the reason this file exists rather than the commit log being enough. A new
7
+ setting with a neutral default is not news: an installation that does not set it behaves exactly as
8
+ before. **A changed default is news**, because it moves an installation that has taken no action and
9
+ made no decision. `docs/plans/DECISIONS.md` §6.3 puts a changed default here rather than in a quiet
10
+ edit for that reason.
11
+
12
+ Entries are newest first. Each names what moved, what an installation has to do about it, and what
13
+ happens if it does nothing.
14
+
15
+ ---
16
+
17
+ ## 0.2.0
18
+
19
+ ### Behaviour change: `browser_status` asks whether the browser is still there
20
+
21
+ **What moved.** Liveness was derived from rows and a clock. `browser_status` now asks the operating
22
+ system whether the browser process is still answering, and a session that has died is let go, so the
23
+ next acquisition relaunches rather than handing back a dead one.
24
+
25
+ **Why this is a behaviour change and not a new check.** A lease whose browser had died reported
26
+ `active`, with its expiry advancing, and no sequence of calls from the tool surface could recover it:
27
+ releasing and reclaiming reached the same settled session, because the provider memoised it for the
28
+ life of the process. The lease looked healthy from every angle a caller could see, and every page
29
+ call on it failed.
30
+
31
+ **What an installation has to do.** Nothing.
32
+
33
+ **If it does nothing:** `browser_status` may report a browser gone where it reported `active`. **That
34
+ is the check working, not a new fault appearing** — the browser was already gone, and the report is
35
+ what changed.
36
+
37
+ **The discriminator is load-bearing, and worth knowing about.** A row that says `running` while
38
+ nothing answers is a browser that died; a row that says `stopped` never started. Acquisition is lazy,
39
+ so a freshly granted lease on a machine with no browser installed is the ordinary case, not an error
40
+ probing without that distinction reports those leases as expired.
41
+
42
+ ### A claim says when a browser's tabs are stranded
43
+
44
+ **What moved.** Three reports about tabs stranded mid-close:
45
+
46
+ - **A grant now carries the count**, from the same query and the same instant. A browser holding a
47
+ backlog of tabs left by sessions killed mid-lease can grant a lease that cannot be used: the claim
48
+ succeeds and every navigate on it fails. Five consecutive claims were granted against such a
49
+ backlog before anything said so.
50
+ - **`broker doctor` splits its stranded count per browser** rather than reporting one total, so
51
+ clearing one browser moves the number it is judged by.
52
+ - **`broker reconcile` says when the tab blocking it belongs to the caller's own lease**, given a
53
+ `--session-id` to know it by.
54
+
55
+ **What an installation has to do.** Nothing. The count rides on a grant that is genuinely
56
+ granted capacity was taken and the lease is active so this is a note on a successful response
57
+ rather than a refusal, and it is absent rather than zero when there is no backlog.
58
+
59
+ ### Command-line help lists the flags a command is refused for omitting, and an unknown flag is refused
60
+
61
+ **What moved.** `broker claim --help` documents `--session-id` and `--purpose`, the two flags a claim
62
+ is refused by name for omitting, and `broker reconcile --help` documents its optional `--session-id`.
63
+ A help text listing three optional flags and neither required one teaches a reader how to call the
64
+ command unsuccessfully.
65
+
66
+ An unknown flag on `snapshot`, `events` or `reconcile` is refused, naming the flag and the accepted
67
+ set, rather than being accepted and discarded.
68
+
69
+ **What an installation has to do.** Nothing. A caller passing correct flags is unaffected.
70
+
71
+ **The refusal's reach is narrower than the help fix, and worth stating plainly:** it covers those
72
+ three commands. `claim` and the other operation commands parse their arguments by a different route,
73
+ so `broker claim --session x` still does not name the typo. The completeness gate covering the
74
+ documented commands is a hand-written list.
75
+
76
+ ### `act emulate` says how long its effect lasts
77
+
78
+ **What moved.** An `emulate` result carries `emulationScope`, naming the bound the preference lives
79
+ within and the path that works: emulate and capture within one invocation, or use the tool surface,
80
+ where one connection spans the calls.
81
+
82
+ The preference is scoped to the connection that set it — the tab survives and the emulation binding
83
+ does not — so an emulate driven from a separate invocation than the capture that reads it returns
84
+ `accepted` and changes nothing on the page. A reviewer checking dark mode that way gets a clean
85
+ result and a light screenshot. The harm is the silence rather than the impersistence.
86
+
87
+ **What an installation has to do.** Nothing; the field is additive. `pageDriven` is unchanged and
88
+ still reports `true` here, because a browser genuinely was reached.
89
+
90
+ ### `browser_capture` can ask for a higher-resolution tier
91
+
92
+ **What moved.** `browser_capture` takes `tier` — `"detail"` or `"max"` — and `reason` is bounded at 8
93
+ to 200 characters and required alongside `tier="max"`. The default has no name and is what passing
94
+ nothing gives you. A new refusal, `capture.tier_known`, names both accepted words when a tier is not
95
+ one of them.
96
+
97
+ The ladder was built end to end, and the pipeline refuses the top rung without a written reason,
98
+ while no surface could populate either field — so the escalation rollup could only ever read as
99
+ *nobody escalates*, which is a measurement of the surface rather than of any caller.
100
+
101
+ **What an installation has to do.** Nothing. A capture that names no tier gets the default.
102
+
103
+ ### ⚠ Behaviour change: `wait_ms` on a navigate is now honoured
104
+
105
+ **What moved.** `browser_navigate` has advertised a `wait_ms` argument for some time, typed,
106
+ documented and accepted without complaint. **Nothing read it.** It now reaches the browser as the
107
+ navigation's timeout, and it is bounded by the lease: a wait longer than the lease can live is
108
+ refused, and the refusal names the accepted range rather than quietly clamping the value.
109
+
110
+ **Why this is a behaviour change and not a new feature.** An installation that has set nothing is
111
+ unaffected — but a *caller* that was already passing `wait_ms`, exactly as the schema invited, was
112
+ having it discarded and now is not. Nothing about that caller's code changes and its behaviour does.
113
+
114
+ **What an installation has to do.** Nothing. A caller passing no wait is unaffected: the argument is
115
+ omitted rather than defaulted, so the browser's own default still applies, and this service does not
116
+ invent a number the browser library owns.
117
+
118
+ **Worth knowing before relying on it:** the wait bounds the **load**, and does not cover work the
119
+ page starts afterwards. A canvas or a lazily-loaded region can still be unfinished when the call
120
+ returns, so two captures differing only in this argument tell you nothing about how long the page
121
+ was given to settle — see `browser_capture` on how to tell. An inert argument is worse than a
122
+ missing one precisely because a caller draws conclusions from it, and that is the conclusion most
123
+ easily drawn here.
124
+
125
+ ### Dialog and form-filling become reachable from the command line, and a contradictory answer is refused
126
+
127
+ **What moved.** Two things a command-line caller had no way to do at all:
128
+
129
+ - `act dialog` and `act fill_form` take nested arguments that the flat command-line parser could not
130
+ produce, so **no argument a person could type would ever parse**. They now assemble from ordinary
131
+ flags — `--accept` / `--dismiss`, `--prompt-text`, and a repeatable `--field`.
132
+ - **A contradictory dialog answer is now refused instead of resolved silently.** Asking to accept
133
+ and dismiss the same dialog used to take whichever the code read first. A caller answering a
134
+ destructive `confirm()` deserves a refusal rather than a coin flip, and now gets one.
135
+
136
+ **What an installation has to do.** Nothing. Callers already sending a single, coherent answer — by
137
+ either surface — are unaffected.
138
+
139
+ ### Capture responses point at `compare_to`
140
+
141
+ **What moved.** A capture response now carries a hint naming the comparison it could have made. The
142
+ comparison itself already existed and is roughly two orders of magnitude cheaper than reading a
143
+ screenshot back to answer *did this change* — and in all recorded history it had been used once,
144
+ because nothing pointed at it.
145
+
146
+ **What an installation has to do.** Nothing; the hint is additive. It is listed here because the
147
+ cheapest path through this service was, in practice, unreachable, and a caller that never learned
148
+ the verb existed was not making an informed choice.
149
+
150
+ ### Closing a tab is recorded, and the doctor counts what was left behind
151
+
152
+ **What moved.** Tab closes now write their outcome, rows nothing could reach are settled, and
153
+ `broker doctor` reports tabs stranded in `closing`. Without that, a released lease can leave its tab
154
+ open with the record stuck mid-close and **nothing anywhere says so** — a state reachable only by
155
+ looking at the browser.
156
+
157
+ **What an installation has to do.** Nothing. **If it does nothing:** `broker doctor` may now report
158
+ stranded tabs that no check could see before. That is the check working, not a new fault appearing.
159
+
160
+ ### The package is published, and it ships compiled JavaScript
161
+
162
+ **What moved.** The service is installable from the registry as `browser-broker`, so a caller can
163
+ spawn it without a checkout. The manifest's `bin` entries now name emitted JavaScript under `dist/`
164
+ rather than the TypeScript sources.
165
+
166
+ **Why the build exists**, given that the development path deliberately has none: **Node refuses to
167
+ strip types from any file under a `node_modules` path**, and an installed package is a directory
168
+ under `node_modules`. A manifest whose `bin` named a `.ts` file would install cleanly and then fail
169
+ on the machine of whoever installed it. There is no flag that changes this. The compiler therefore
170
+ runs once per release rather than on every machine that consumes the package, and `erasableSyntaxOnly`
171
+ stays on so the sources still run unbuilt — the two paths execute the same dialect.
172
+
173
+ **What an installation has to do.** Nothing. A checkout is unaffected: `node src/bin/broker.ts` still
174
+ runs the sources with no build. An installation that would rather not track a checkout can point at
175
+ the package instead, and npm revalidates the version on every spawn:
176
+
177
+ ```json
178
+ { "command": "npx", "args": ["-y", "-p", "browser-broker", "broker-tool"] }
179
+ ```
180
+
181
+ The `-p` is not decoration: the package ships two executables and neither is named for the package,
182
+ so `npx browser-broker` cannot choose between them and refuses to run at all.
183
+
184
+ **Worth weighing before switching:** `npx` performs a registry round-trip on every spawn, costing
185
+ seconds where a path on disk costs a fraction of one. It buys an upgrade path, not speed.
186
+
187
+ **One surface changes what it reports.** The tool handshake's `serverInfo.version` was the literal
188
+ `0.0.0` while the package was unversioned, and now reads the manifest — so a client logging it sees
189
+ the released version rather than a placeholder.
190
+
191
+
192
+ ### The default browser engine is named Edge, but resolving it is not built yet
193
+
194
+ **What moved.** `BROKER_REGULAR_BROWSER_ENGINE` and `BROKER_PRIVATE_BROWSER_ENGINE` default to
195
+ **`msedge`** rather than being unset. `DECISIONS.md` §13i chose Edge as the default *name* because it
196
+ is present on every Windows machine, applying §6.1's *"a fresh install runs with nothing set"* to
197
+ which engine a fresh install prefers.
198
+
199
+ **What this does not do: it does not change which binary launches, and it does not remove the browser
200
+ download step.** Resolving an engine name to an executable path is deliberately not built (§13i) —
201
+ `executablePathForEngine` only looks up a path this process was *given* for that engine, and nothing
202
+ in this repository supplies one. So the launch still falls through to `chromium.executablePath()`
203
+ regardless of which engine is named, and a machine that has never fetched a Chromium build still
204
+ needs to, exactly as before this change. The earlier release note claiming this default removed the
205
+ download step was wrong and is corrected here.
206
+
207
+ **What an installation has to do.** Nothing, either way — the engine setting has no observable effect
208
+ yet. Setting it now is preparation for when per-engine resolution is built, not a way to skip the
209
+ browser fetch:
210
+
211
+ ```bash
212
+ BROKER_REGULAR_BROWSER_ENGINE=chrome # chrome | brave | msedge — has no effect on which binary launches
213
+ BROKER_PRIVATE_BROWSER_ENGINE=chrome # accepted and validated, and read by nothing — see below
214
+ ```
215
+
216
+ Both are parsed and validated, and an unaccepted value is refused at startup and named — that much is
217
+ finished. **The private variable goes one step less far than the regular one:** one driver serves
218
+ every browser in a process, and only the regular engine is handed to it, so the private value is
219
+ validated and then never read by anything at all. It is not merely ineffective downstream; it has no
220
+ downstream.
221
+
222
+ **If it does nothing:** browsers keep launching under whichever Chromium build `playwright-core`
223
+ resolves, same as before this change. **No profile is signed out by setting an engine**, which an
224
+ earlier note claimed would happen: a profile directory is named for its *browser*, not for an engine,
225
+ so there is no engine-keyed profile to move between and nothing to sign in again.
226
+ `setup.profile_never_destroyed` holds, as it did before. The per-engine profile and sign-in
227
+ consequences described in `DECISIONS.md` §13i are what would follow once resolution is wired, and
228
+ describe nothing that happens now.
229
+
230
+ ### Browsers are a configured list, and `browser` on a claim is optional
231
+
232
+ **What moved.** Two things, both reversals of recorded decisions — the argument for each is in
233
+ `DECISIONS.md` §13i:
234
+
235
+ - **The fixed pair of browsers becomes a bounded list per kind**, at most three each, named in
236
+ configuration. A name is what a caller claims by and what its profile directory is called.
237
+ - **`browser` on `browser_claim` becomes optional.** Unstated resolves to the first signed-in
238
+ browser; `regular` or `private` resolves to the first of that kind; a configured name resolves to
239
+ that browser exactly.
240
+
241
+ **What an installation has to do.** Nothing. The defaults name one browser of each kind, `regular`
242
+ and `private`, which is the pair that existed before — so an installation that sets nothing has the
243
+ same two browsers under the same two names, and a caller that states `browser` explicitly is
244
+ unaffected.
245
+
246
+ **Worth reading before configuring more than the default two:** the tab budget counts *tabs*, and
247
+ each browser costs a process **before it holds a single tab**. `BROKER_TAB_BUDGET=15` with six
248
+ browsers is six browser processes, plus up to fifteen tabs, plus six keeper tabs that are not
249
+ counted against the budget at all. `.env.example` states this beside the variables.
250
+
251
+ **One schema step.** The store gains a `kind` column on `browsers` and drops the check constraint
252
+ that limited a browser's name to two literals. It is applied on the next spawn, like every step, and
253
+ the two existing rows are backfilled to their own kinds. **A store stepped by this build is not
254
+ readable by an earlier one**, which is the ordinary direction — a build refuses a store newer than
255
+ itself rather than downgrading it.
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-broker",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "description": "Leases over tabs in a fixed set of browsers: bounded capacity, a queue, reclamation, and an enforced capture policy.",
6
6
  "type": "module",
@@ -19,12 +19,13 @@
19
19
  "check:operations": "node scripts/check-operations.mjs",
20
20
  "check:argument-refusals": "node scripts/check-argument-refusals.mjs",
21
21
  "check:injected-tests": "node scripts/check-injected-tests.mjs",
22
+ "check:argument-reachability": "node scripts/check-argument-reachability.mjs",
22
23
  "typecheck": "tsc --noEmit",
23
24
  "lint": "eslint .",
24
25
  "format": "prettier --write .",
25
26
  "format:check": "prettier --check .",
26
27
  "test": "node --test \"tests/**/*.test.mjs\" \"tests/**/*.test.ts\"",
27
- "check": "npm run check:external-refs && npm run check:doc-links && npm run check:arbitration && npm run check:capture-isolation && npm run check:artifact-path && npm run typecheck && npm run lint && npm run format:check && npm test && npm run check:install && npm run check:operations && npm run check:argument-refusals && npm run check:injected-tests",
28
+ "check": "npm run check:external-refs && npm run check:doc-links && npm run check:arbitration && npm run check:capture-isolation && npm run check:artifact-path && npm run typecheck && npm run lint && npm run format:check && npm test && npm run check:install && npm run check:operations && npm run check:argument-refusals && npm run check:argument-reachability && npm run check:injected-tests && npm run check:package",
28
29
  "check:arbitration": "node scripts/check-arbitration.mjs",
29
30
  "check:artifact-path": "node scripts/check-artifact-path.mjs",
30
31
  "build": "tsc --project tsconfig.build.json",
@@ -150,7 +150,11 @@ export async function makeServiceSubject() {
150
150
  return {
151
151
  service: runtime.service,
152
152
  // The fake's own log, which is what makes "a refusal touched no browser"
153
- // checkable rather than asserted.
153
+ // checkable rather than asserted — and the entries are passed whole, so a
154
+ // case can also ask what the browser was told, not merely that it was
155
+ // spoken to. Dropping the arguments here would make a route that forwards
156
+ // an argument and one that discards it indistinguishable to every case in
157
+ // the table.
154
158
  driverCalls: () => driver.calls,
155
159
  // **The same predicate the capacity check uses**, per `driver.ts`'s
156
160
  // requirement for this reading — not a count of rows in `claims`, which
@@ -402,8 +402,22 @@ export class FakeBrowserDriver {
402
402
  this.#tabsFor(tab.browser).delete(tab.driverTabId);
403
403
  return Promise.resolve();
404
404
  },
405
- navigate: (tab, url) => {
406
- const failure = this.#enter({ name: 'navigate', browser, tab, detail: { url } });
405
+ navigate: (tab, url, waitMs) => {
406
+ // The wait is recorded even though nothing here waits, because what a
407
+ // test needs to assert is that the service *asked* for it. An argument
408
+ // that is accepted and dropped between the caller and the driver looks
409
+ // identical from the outside to one that was honoured, and the only
410
+ // place that difference is observable is this log.
411
+ //
412
+ // Recorded as an absent key when the caller omitted it, rather than as
413
+ // an explicit undefined, so a test can tell "asked for no wait" from
414
+ // "asked for a wait of nothing".
415
+ const failure = this.#enter({
416
+ name: 'navigate',
417
+ browser,
418
+ tab,
419
+ detail: { url, ...(waitMs === undefined ? {} : { waitMs }) },
420
+ });
407
421
  if (failure)
408
422
  return Promise.reject(failure);
409
423
  return Promise.resolve({ url, title: `fake page at ${url}`, status: 200 });
@@ -493,9 +493,17 @@ class RealBrowserSession {
493
493
  * The address after redirects rather than the one asked for, because those
494
494
  * differ constantly and the caller needs the one it got.
495
495
  */
496
- async navigate(tab, url) {
496
+ async navigate(tab, url, waitMs) {
497
497
  const page = await this.#page(tab);
498
- const response = await page.goto(url);
498
+ // Passed as the navigation's own timeout rather than as a pause taken
499
+ // afterwards, so a page that arrives early returns early and the argument
500
+ // only ever costs what the page costs.
501
+ //
502
+ // Absent means the library's configured default applies. Spread rather
503
+ // than passed as `{ timeout: undefined }`, because an explicit undefined
504
+ // and an omitted key are the same to this library only by convention, and
505
+ // relying on that convention would make the default this service's to own.
506
+ const response = await page.goto(url, ...(waitMs === undefined ? [] : [{ timeout: waitMs }]));
499
507
  return {
500
508
  url: page.url(),
501
509
  title: await page.title(),