@el4cteo/rbx-studio-mcp 0.6.8 → 0.7.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.
@@ -1,267 +1,328 @@
1
- /**
2
- * Checks that agents sharing one bridge do not share one target.
3
- *
4
- * This exists because the bug it covers is invisible from inside either agent:
5
- * a call with no studioId succeeds either way, and the only symptom of getting
6
- * it wrong is edits landing in a place nobody asked for. Two Claude sessions
7
- * pointed at two places is the shipping claim, so it gets a test rather than a
8
- * paragraph.
9
- *
10
- * Runs against the real Bridge with fake Studio sessions -- no Studio, no
11
- * sockets -- so it is fast enough to sit in `npm test`.
12
- */
13
- import assert from "node:assert/strict";
14
- import { Bridge } from "../dist/bridge/rpc.js";
15
- import { LocalBridge } from "../dist/bridge/api.js";
16
-
17
- const identity = (studioId, placeId) => ({
18
- studioId,
19
- placeName: `Place ${placeId}`,
20
- placeId,
21
- pluginVersion: "test",
22
- buildId: "test",
23
- protocolVersion: 1,
24
- transport: "poll",
25
- context: "edit",
26
- });
27
-
28
- function twoStudios() {
29
- const bridge = new Bridge();
30
- bridge.attach(identity("studio-a", 111), null);
31
- bridge.attach(identity("studio-b", 222), null);
32
- return bridge;
33
- }
34
-
35
- // A client that chose nothing has no target, and one that chose has its own.
36
- {
37
- const bridge = twoStudios();
38
- const alice = new LocalBridge(bridge);
39
- const bob = new LocalBridge(bridge);
40
-
41
- assert.equal((await alice.sessions()).activeId, null, "unchosen client has no target");
42
- assert.equal((await bob.sessions()).activeId, null, "unchosen client has no target");
43
-
44
- await alice.setActive("studio-a");
45
-
46
- assert.equal((await alice.sessions()).activeId, "studio-a", "chooser sees its choice");
47
- assert.equal((await alice.sessions()).activeIsChosen, true);
48
-
49
- // The whole point: Alice choosing must not move Bob.
50
- assert.equal((await bob.sessions()).activeId, null, "another client is unaffected");
51
- assert.equal((await bob.sessions()).activeIsChosen, false);
52
-
53
- await bob.setActive("studio-b");
54
- assert.equal((await alice.sessions()).activeId, "studio-a", "choices do not overwrite");
55
- assert.equal((await bob.sessions()).activeId, "studio-b");
56
- }
57
-
58
- // An un-addressed call goes to this client's own choice, not to another's.
59
- {
60
- const bridge = twoStudios();
61
- const alice = new LocalBridge(bridge);
62
- const bob = new LocalBridge(bridge);
63
- await alice.setActive("studio-a");
64
- await bob.setActive("studio-b");
65
-
66
- const target = (client) =>
67
- new Promise((resolve) => {
68
- // Every session is poll-transport with no waiter, so the command lands on
69
- // a queue; whichever queue grew is where it was routed.
70
- void client.call("studio.ping", {}, { timeoutMs: 50 }).catch(() => {});
71
- setTimeout(() => {
72
- for (const studioId of ["studio-a", "studio-b"]) {
73
- const session = bridge.sessions.get(studioId);
74
- if (session.queue.length > 0) {
75
- session.queue.length = 0;
76
- resolve(studioId);
77
- return;
78
- }
79
- }
80
- resolve(null);
81
- }, 10);
82
- });
83
-
84
- assert.equal(await target(alice), "studio-a", "alice's call follows alice's choice");
85
- assert.equal(await target(bob), "studio-b", "bob's call follows bob's choice");
86
- }
87
-
88
- // A lone Studio needs no choice, and losing a chosen one goes ambiguous again
89
- // rather than silently promoting the survivor.
90
- {
91
- const bridge = new Bridge();
92
- bridge.attach(identity("only", 111), null);
93
- const alice = new LocalBridge(bridge);
94
- assert.equal((await alice.sessions()).activeId, "only", "one Studio is never ambiguous");
95
- assert.equal((await alice.sessions()).activeIsChosen, false, "and was not chosen");
96
-
97
- bridge.attach(identity("second", 222), null);
98
- assert.equal((await alice.sessions()).activeId, null, "a second Studio makes it ambiguous");
99
-
100
- await alice.setActive("second");
101
- bridge.detach("second");
102
- assert.equal((await alice.sessions()).activeId, "only", "one left is unambiguous again");
103
- assert.equal(
104
- (await alice.sessions()).activeIsChosen,
105
- false,
106
- "but the survivor was never chosen",
107
- );
108
- }
109
-
110
- /**
111
- * Counting the agents that share one bridge, and noticing when one leaves.
112
- *
113
- * The Studio console shows this count and announces the departure, so getting
114
- * it wrong is not a cosmetic fault -- it either claims a second agent is
115
- * editing the user's place when none is, or stays silent when one really has
116
- * gone. Both are the kind of wrong that is only visible from outside the
117
- * process, which is what this covers.
118
- */
119
- {
120
- const bridge = new Bridge();
121
- const seen = [];
122
- bridge.watchClients((count) => seen.push(count));
123
-
124
- assert.equal(bridge.clientCount(), 0, "a fresh bridge has no clients");
125
-
126
- const alice = new LocalBridge(bridge);
127
- assert.equal(bridge.clientCount(), 1, "constructing a local bridge registers it");
128
-
129
- bridge.noteClient("peer-1");
130
- assert.equal(bridge.clientCount(), 2, "a peer counts too");
131
-
132
- bridge.noteClient("peer-1");
133
- assert.equal(bridge.clientCount(), 2, "and saying hello twice is not two peers");
134
-
135
- assert.deepEqual(seen, [1, 2], "only real changes are announced");
136
-
137
- assert.equal(bridge.forgetClient("peer-1"), true, "goodbye drops the peer");
138
- assert.equal(bridge.clientCount(), 1, "leaving one behind");
139
- assert.equal(
140
- bridge.forgetClient("peer-1"),
141
- false,
142
- "a repeated goodbye is not a second departure",
143
- );
144
- assert.deepEqual(seen, [1, 2, 1], "and is not announced twice");
145
-
146
- // A client's chosen Studio goes with it, which is the leak forgetClient was
147
- // written for and never called to fix.
148
- bridge.attach(identity("studio-a", 111), null);
149
- bridge.attach(identity("studio-b", 222), null);
150
- await alice.setActive("studio-b");
151
- assert.equal((await alice.sessions()).activeIsChosen, true, "alice chose one");
152
- alice.goodbye();
153
- assert.equal(bridge.clientCount(), 0, "and left");
154
- assert.equal(
155
- (await alice.sessions()).activeIsChosen,
156
- false,
157
- "taking her choice with her",
158
- );
159
- }
160
-
161
- /** A client killed rather than closed is swept by the reaper. */
162
- {
163
- const bridge = new Bridge();
164
- bridge.noteClient("ghost");
165
- assert.equal(bridge.clientCount(), 1, "the ghost registered");
166
-
167
- bridge.reapStale();
168
- assert.equal(bridge.clientCount(), 1, "a fresh client survives a sweep");
169
-
170
- // Reach past the clock rather than wait ninety seconds for it.
171
- const original = Date.now;
172
- Date.now = () => original() + 120_000;
173
- try {
174
- bridge.reapStale();
175
- } finally {
176
- Date.now = original;
177
- }
178
- assert.equal(bridge.clientCount(), 0, "a silent one does not");
179
- }
180
-
181
- /**
182
- * The bridge must not sweep away the process it is running inside.
183
- *
184
- * It did. `LocalBridge` announced itself once at construction and nothing ever
185
- * refreshed it, so ninety seconds later the reaper -- which cannot tell an
186
- * absent client from a quiet one -- dropped the owner from its own client list
187
- * while it was actively serving. The count then read one short, and the drop
188
- * was broadcast to the Studio console as an agent having finished. Found by a
189
- * user asking why the panel said three clients when there was one.
190
- */
191
- {
192
- const bridge = new Bridge();
193
- const owner = new LocalBridge(bridge);
194
- bridge.attach(identity("studio-a", 111), null);
195
- assert.equal(bridge.clientCount(), 1, "the owner counts as a client");
196
-
197
- const real = Date.now;
198
- Date.now = () => real() + 120_000;
199
- try {
200
- // Working is what proves it is here, and the owner works constantly.
201
- await owner.sessions();
202
- bridge.reapStale();
203
- } finally {
204
- Date.now = real;
205
- }
206
- assert.equal(bridge.clientCount(), 1, "an owner that is working is not stale");
207
-
208
- // And it still goes when it actually goes.
209
- owner.goodbye();
210
- assert.equal(bridge.clientCount(), 0, "goodbye still drops it");
211
- }
212
-
213
-
214
- /**
215
- * The console asks "who is connected", not just "how many".
216
- *
217
- * A bare count is the thing users bring to us asking whether something is
218
- * wrong -- three agents they started and three processes they forgot read
219
- * identically. The roster is what answers it, so the ordering, the naming and
220
- * the "connected at" all have to survive a keepalive.
221
- */
222
- {
223
- const bridge = new Bridge();
224
- const alice = new LocalBridge(bridge);
225
- const bob = new LocalBridge(bridge);
226
-
227
- // Nameless until the MCP handshake lands, and honest about it.
228
- assert.deepEqual(
229
- bridge.clientList().map((client) => client.name),
230
- ["unknown", "unknown"],
231
- "a client that has not introduced itself is not guessed at",
232
- );
233
-
234
- alice.describe({ name: "claude-code", version: "2.0" });
235
- bob.describe({ name: "codex", version: "1.0" });
236
-
237
- const named = bridge.clientList();
238
- assert.deepEqual(
239
- named.map((client) => client.name),
240
- ["claude-code", "codex"],
241
- "the roster is ordered by arrival, not by name",
242
- );
243
- assert.equal(named[0].version, "2.0");
244
- assert.ok(named[0].pid > 0, "a client reports which process it is");
245
-
246
- // A keepalive proves a client is still here. It does not make it new.
247
- const arrived = bridge.clientList()[0].connectedAt;
248
- const real = Date.now;
249
- Date.now = () => real() + 60_000;
250
- try {
251
- await alice.sessions();
252
- } finally {
253
- Date.now = real;
254
- }
255
- assert.equal(bridge.clientList()[0].connectedAt, arrived, "working does not reset the clock");
256
- assert.equal(bridge.clientList()[0].name, "claude-code", "working does not forget the name");
257
-
258
- // Learning a name is news, so the plugin is told; a repeat is not.
259
- let announced = 0;
260
- bridge.watchClients(() => {
261
- announced += 1;
262
- });
263
- alice.describe({ name: "claude-code", version: "2.0" });
264
- assert.equal(announced, 0, "re-stating the same name announces nothing");
265
- alice.describe({ name: "cursor", version: "1.0" });
266
- assert.equal(announced, 1, "a client that changes its name is announced");
267
- }
1
+ /**
2
+ * Checks that agents sharing one bridge do not share one target.
3
+ *
4
+ * This exists because the bug it covers is invisible from inside either agent:
5
+ * a call with no studioId succeeds either way, and the only symptom of getting
6
+ * it wrong is edits landing in a place nobody asked for. Two Claude sessions
7
+ * pointed at two places is the shipping claim, so it gets a test rather than a
8
+ * paragraph.
9
+ *
10
+ * Runs against the real Bridge with fake Studio sessions -- no Studio, no
11
+ * sockets -- so it is fast enough to sit in `npm test`.
12
+ */
13
+ import assert from "node:assert/strict";
14
+ import { Bridge } from "../dist/bridge/rpc.js";
15
+ import { LocalBridge } from "../dist/bridge/api.js";
16
+
17
+ const identity = (studioId, placeId) => ({
18
+ studioId,
19
+ placeName: `Place ${placeId}`,
20
+ placeId,
21
+ pluginVersion: "test",
22
+ buildId: "test",
23
+ protocolVersion: 1,
24
+ transport: "poll",
25
+ context: "edit",
26
+ });
27
+
28
+ function twoStudios() {
29
+ const bridge = new Bridge();
30
+ bridge.attach(identity("studio-a", 111), null);
31
+ bridge.attach(identity("studio-b", 222), null);
32
+ return bridge;
33
+ }
34
+
35
+ // A client that chose nothing has no target, and one that chose has its own.
36
+ {
37
+ const bridge = twoStudios();
38
+ const alice = new LocalBridge(bridge);
39
+ const bob = new LocalBridge(bridge);
40
+
41
+ assert.equal((await alice.sessions()).activeId, null, "unchosen client has no target");
42
+ assert.equal((await bob.sessions()).activeId, null, "unchosen client has no target");
43
+
44
+ await alice.setActive("studio-a");
45
+
46
+ assert.equal((await alice.sessions()).activeId, "studio-a", "chooser sees its choice");
47
+ assert.equal((await alice.sessions()).activeIsChosen, true);
48
+
49
+ // The whole point: Alice choosing must not move Bob.
50
+ assert.equal((await bob.sessions()).activeId, null, "another client is unaffected");
51
+ assert.equal((await bob.sessions()).activeIsChosen, false);
52
+
53
+ await bob.setActive("studio-b");
54
+ assert.equal((await alice.sessions()).activeId, "studio-a", "choices do not overwrite");
55
+ assert.equal((await bob.sessions()).activeId, "studio-b");
56
+ }
57
+
58
+ // An un-addressed call goes to this client's own choice, not to another's.
59
+ {
60
+ const bridge = twoStudios();
61
+ const alice = new LocalBridge(bridge);
62
+ const bob = new LocalBridge(bridge);
63
+ await alice.setActive("studio-a");
64
+ await bob.setActive("studio-b");
65
+
66
+ const target = (client) =>
67
+ new Promise((resolve) => {
68
+ // Every session is poll-transport with no waiter, so the command lands on
69
+ // a queue; whichever queue grew is where it was routed.
70
+ void client.call("studio.ping", {}, { timeoutMs: 50 }).catch(() => {});
71
+ setTimeout(() => {
72
+ for (const studioId of ["studio-a", "studio-b"]) {
73
+ const session = bridge.sessions.get(studioId);
74
+ if (session.queue.length > 0) {
75
+ session.queue.length = 0;
76
+ resolve(studioId);
77
+ return;
78
+ }
79
+ }
80
+ resolve(null);
81
+ }, 10);
82
+ });
83
+
84
+ assert.equal(await target(alice), "studio-a", "alice's call follows alice's choice");
85
+ assert.equal(await target(bob), "studio-b", "bob's call follows bob's choice");
86
+ }
87
+
88
+ // A lone Studio needs no choice, and losing a chosen one goes ambiguous again
89
+ // rather than silently promoting the survivor.
90
+ {
91
+ const bridge = new Bridge();
92
+ bridge.attach(identity("only", 111), null);
93
+ const alice = new LocalBridge(bridge);
94
+ assert.equal((await alice.sessions()).activeId, "only", "one Studio is never ambiguous");
95
+ assert.equal((await alice.sessions()).activeIsChosen, false, "and was not chosen");
96
+
97
+ bridge.attach(identity("second", 222), null);
98
+ assert.equal((await alice.sessions()).activeId, null, "a second Studio makes it ambiguous");
99
+
100
+ await alice.setActive("second");
101
+ bridge.detach("second");
102
+ assert.equal((await alice.sessions()).activeId, "only", "one left is unambiguous again");
103
+ assert.equal(
104
+ (await alice.sessions()).activeIsChosen,
105
+ false,
106
+ "but the survivor was never chosen",
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Counting the agents that share one bridge, and noticing when one leaves.
112
+ *
113
+ * The Studio console shows this count and announces the departure, so getting
114
+ * it wrong is not a cosmetic fault -- it either claims a second agent is
115
+ * editing the user's place when none is, or stays silent when one really has
116
+ * gone. Both are the kind of wrong that is only visible from outside the
117
+ * process, which is what this covers.
118
+ */
119
+ {
120
+ const bridge = new Bridge();
121
+ const seen = [];
122
+ bridge.watchClients((count) => seen.push(count));
123
+
124
+ assert.equal(bridge.clientCount(), 0, "a fresh bridge has no clients");
125
+
126
+ const alice = new LocalBridge(bridge);
127
+ assert.equal(bridge.clientCount(), 1, "constructing a local bridge registers it");
128
+
129
+ bridge.noteClient("peer-1");
130
+ assert.equal(bridge.clientCount(), 2, "a peer counts too");
131
+
132
+ bridge.noteClient("peer-1");
133
+ assert.equal(bridge.clientCount(), 2, "and saying hello twice is not two peers");
134
+
135
+ assert.deepEqual(seen, [1, 2], "only real changes are announced");
136
+
137
+ assert.equal(bridge.forgetClient("peer-1"), true, "goodbye drops the peer");
138
+ assert.equal(bridge.clientCount(), 1, "leaving one behind");
139
+ assert.equal(
140
+ bridge.forgetClient("peer-1"),
141
+ false,
142
+ "a repeated goodbye is not a second departure",
143
+ );
144
+ assert.deepEqual(seen, [1, 2, 1], "and is not announced twice");
145
+
146
+ // A client's chosen Studio goes with it, which is the leak forgetClient was
147
+ // written for and never called to fix.
148
+ bridge.attach(identity("studio-a", 111), null);
149
+ bridge.attach(identity("studio-b", 222), null);
150
+ await alice.setActive("studio-b");
151
+ assert.equal((await alice.sessions()).activeIsChosen, true, "alice chose one");
152
+ alice.goodbye();
153
+ assert.equal(bridge.clientCount(), 0, "and left");
154
+ assert.equal(
155
+ (await alice.sessions()).activeIsChosen,
156
+ false,
157
+ "taking her choice with her",
158
+ );
159
+ }
160
+
161
+ /** A client killed rather than closed is swept by the reaper. */
162
+ {
163
+ const bridge = new Bridge();
164
+ bridge.noteClient("ghost");
165
+ assert.equal(bridge.clientCount(), 1, "the ghost registered");
166
+
167
+ bridge.reapStale();
168
+ assert.equal(bridge.clientCount(), 1, "a fresh client survives a sweep");
169
+
170
+ // Reach past the clock rather than wait ninety seconds for it.
171
+ const original = Date.now;
172
+ Date.now = () => original() + 120_000;
173
+ try {
174
+ bridge.reapStale();
175
+ } finally {
176
+ Date.now = original;
177
+ }
178
+ assert.equal(bridge.clientCount(), 0, "a silent one does not");
179
+ }
180
+
181
+ /**
182
+ * The bridge must not sweep away the process it is running inside.
183
+ *
184
+ * It did. `LocalBridge` announced itself once at construction and nothing ever
185
+ * refreshed it, so ninety seconds later the reaper -- which cannot tell an
186
+ * absent client from a quiet one -- dropped the owner from its own client list
187
+ * while it was actively serving. The count then read one short, and the drop
188
+ * was broadcast to the Studio console as an agent having finished. Found by a
189
+ * user asking why the panel said three clients when there was one.
190
+ */
191
+ {
192
+ const bridge = new Bridge();
193
+ const owner = new LocalBridge(bridge);
194
+ bridge.attach(identity("studio-a", 111), null);
195
+ assert.equal(bridge.clientCount(), 1, "the owner counts as a client");
196
+
197
+ const real = Date.now;
198
+ Date.now = () => real() + 120_000;
199
+ try {
200
+ // Working is what proves it is here, and the owner works constantly.
201
+ await owner.sessions();
202
+ bridge.reapStale();
203
+ } finally {
204
+ Date.now = real;
205
+ }
206
+ assert.equal(bridge.clientCount(), 1, "an owner that is working is not stale");
207
+
208
+ // And it still goes when it actually goes.
209
+ owner.goodbye();
210
+ assert.equal(bridge.clientCount(), 0, "goodbye still drops it");
211
+ }
212
+
213
+
214
+ /**
215
+ * The console asks "who is connected", not just "how many".
216
+ *
217
+ * A bare count is the thing users bring to us asking whether something is
218
+ * wrong -- three agents they started and three processes they forgot read
219
+ * identically. The roster is what answers it, so the ordering, the naming and
220
+ * the "connected at" all have to survive a keepalive.
221
+ */
222
+ {
223
+ const bridge = new Bridge();
224
+ const alice = new LocalBridge(bridge);
225
+ const bob = new LocalBridge(bridge);
226
+
227
+ // Nameless until the MCP handshake lands, and honest about it.
228
+ assert.deepEqual(
229
+ bridge.clientList().map((client) => client.name),
230
+ ["unknown", "unknown"],
231
+ "a client that has not introduced itself is not guessed at",
232
+ );
233
+
234
+ alice.describe({ name: "claude-code", version: "2.0" });
235
+ bob.describe({ name: "codex", version: "1.0" });
236
+
237
+ const named = bridge.clientList();
238
+ assert.deepEqual(
239
+ named.map((client) => client.name),
240
+ ["claude-code", "codex"],
241
+ "the roster is ordered by arrival, not by name",
242
+ );
243
+ assert.equal(named[0].version, "2.0");
244
+ assert.ok(named[0].pid > 0, "a client reports which process it is");
245
+
246
+ // A keepalive proves a client is still here. It does not make it new.
247
+ const arrived = bridge.clientList()[0].connectedAt;
248
+ const real = Date.now;
249
+ Date.now = () => real() + 60_000;
250
+ try {
251
+ await alice.sessions();
252
+ } finally {
253
+ Date.now = real;
254
+ }
255
+ assert.equal(bridge.clientList()[0].connectedAt, arrived, "working does not reset the clock");
256
+ assert.equal(bridge.clientList()[0].name, "claude-code", "working does not forget the name");
257
+
258
+ // Learning a name is news, so the plugin is told; a repeat is not.
259
+ let announced = 0;
260
+ bridge.watchClients(() => {
261
+ announced += 1;
262
+ });
263
+ alice.describe({ name: "claude-code", version: "2.0" });
264
+ assert.equal(announced, 0, "re-stating the same name announces nothing");
265
+ alice.describe({ name: "cursor", version: "1.0" });
266
+ assert.equal(announced, 1, "a client that changes its name is announced");
267
+ }
268
+
269
+ // The console panel's `use` must route calls, not only studio_status -- and it
270
+ // must reach an agent that started after the user picked.
271
+ {
272
+ const bridge = twoStudios();
273
+ bridge.setActiveForAll("studio-b");
274
+ const late = new LocalBridge(bridge);
275
+ assert.equal((await late.sessions()).activeId, "studio-b");
276
+ await late.call("studio.ping", {}, { timeoutMs: 50 }).catch((cause) => {
277
+ assert.notEqual(cause.code, "AMBIGUOUS_STUDIO", "the user's pick routes the call");
278
+ });
279
+ assert.equal(bridge.sessions.get("studio-b").queue.length, 1, "the call went to the pick");
280
+
281
+ await assert.rejects(
282
+ async () => late.call("studio.ping", {}, { studioId: "gone", timeoutMs: 50 }),
283
+ (cause) => cause.code === "UNKNOWN_STUDIO",
284
+ "an unknown studioId is not reported as no Studio at all",
285
+ );
286
+ }
287
+
288
+ // A stream that dies without /bye (a crashed Studio) removes its session, and a
289
+ // replaced stream closing late does not remove the session that replaced it.
290
+ {
291
+ const { request } = await import("node:http");
292
+ const { createServer } = await import("node:net");
293
+ const { startBridgeServer } = await import("../dist/bridge/server.js");
294
+ const port = await new Promise((resolve) => {
295
+ const probe = createServer().listen(0, "127.0.0.1", () => {
296
+ const { port: free } = probe.address();
297
+ probe.close(() => resolve(free));
298
+ });
299
+ });
300
+ const server = await startBridgeServer({ port });
301
+ const open = () =>
302
+ new Promise((resolve) => {
303
+ const req = request(
304
+ { host: "127.0.0.1", port, path: "/events", method: "POST", headers: { "x-roblox-studio-mcp": "test" } },
305
+ (res) => {
306
+ res.resume();
307
+ resolve(req);
308
+ },
309
+ );
310
+ req.end(JSON.stringify({ studioId: "crashy", placeName: "p", placeId: 1 }));
311
+ });
312
+ const count = async () =>
313
+ (await (await fetch(`http://127.0.0.1:${port}/sessions`, { headers: { "x-roblox-studio-mcp": "test" } })).json())
314
+ .list.length;
315
+ const settle = () => new Promise((resolve) => setTimeout(resolve, 150));
316
+
317
+ const first = await open();
318
+ const second = await open();
319
+ first.destroy();
320
+ await settle();
321
+ assert.equal(await count(), 1, "the old stream closing leaves the new session alone");
322
+ second.destroy();
323
+ await settle();
324
+ assert.equal(await count(), 0, "a stream that dies without a goodbye is dropped");
325
+ await server.close();
326
+ }
327
+
328
+ process.stdout.write("bridge: ok\n");