@quenty/studio-bridge 0.1.1-canary.656.73a74af.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/build-scripts/transform-add-session-attribute.luau +17 -0
  3. package/dist/src/index.d.ts +0 -1
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/index.js +0 -1
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/plugin/plugin-injector.d.ts.map +1 -1
  8. package/dist/src/plugin/plugin-injector.js +11 -34
  9. package/dist/src/plugin/plugin-injector.js.map +1 -1
  10. package/dist/src/process/index.d.ts +0 -2
  11. package/dist/src/process/index.d.ts.map +1 -1
  12. package/dist/src/process/index.js +0 -1
  13. package/dist/src/process/index.js.map +1 -1
  14. package/dist/src/server/studio-bridge-server.d.ts +1 -1
  15. package/dist/src/server/studio-bridge-server.d.ts.map +1 -1
  16. package/dist/src/server/studio-bridge-server.js +38 -12
  17. package/dist/src/server/studio-bridge-server.js.map +1 -1
  18. package/dist/src/server/studio-bridge-server.test.js +94 -19
  19. package/dist/src/server/studio-bridge-server.test.js.map +1 -1
  20. package/dist/src/server/web-socket-protocol.d.ts +7 -1
  21. package/dist/src/server/web-socket-protocol.d.ts.map +1 -1
  22. package/dist/src/server/web-socket-protocol.js +8 -4
  23. package/dist/src/server/web-socket-protocol.js.map +1 -1
  24. package/dist/src/server/web-socket-protocol.smoke.test.js +18 -9
  25. package/dist/src/server/web-socket-protocol.smoke.test.js.map +1 -1
  26. package/dist/src/server/web-socket-protocol.test.js +47 -5
  27. package/dist/src/server/web-socket-protocol.test.js.map +1 -1
  28. package/dist/tsconfig.tsbuildinfo +1 -1
  29. package/package.json +5 -5
  30. package/src/index.ts +0 -1
  31. package/src/plugin/plugin-injector.ts +19 -45
  32. package/src/process/index.ts +0 -3
  33. package/src/server/studio-bridge-server.test.ts +113 -21
  34. package/src/server/studio-bridge-server.ts +66 -15
  35. package/src/server/web-socket-protocol.smoke.test.ts +18 -9
  36. package/src/server/web-socket-protocol.test.ts +61 -5
  37. package/src/server/web-socket-protocol.ts +15 -4
  38. package/templates/default-test-place/default.project.json +1 -1
  39. package/templates/studio-bridge-plugin/default.project.json +1 -1
  40. package/templates/studio-bridge-plugin/{plugin-template.server.lua → src/StudioBridgePlugin.server.lua} +18 -4
  41. package/dist/src/process/place-builder.d.ts +0 -16
  42. package/dist/src/process/place-builder.d.ts.map +0 -1
  43. package/dist/src/process/place-builder.js +0 -45
  44. package/dist/src/process/place-builder.js.map +0 -1
  45. package/src/process/place-builder.ts +0 -63
@@ -14,6 +14,19 @@ import { StudioBridgeServer, type StudioBridgePhase } from './studio-bridge-serv
14
14
  // Mocks — replace external side-effects with no-ops
15
15
  // ---------------------------------------------------------------------------
16
16
 
17
+ vi.mock('@quenty/nevermore-template-helpers', () => ({
18
+ BuildContext: {
19
+ createAsync: vi.fn(async () => ({
20
+ resolvePath: vi.fn((rel: string) => `/fake/tmp/${rel}`),
21
+ executeLuneTransformScriptAsync: vi.fn(async () => {}),
22
+ rojoBuildAsync: vi.fn(async () => undefined),
23
+ cleanupAsync: vi.fn(async () => {}),
24
+ })),
25
+ },
26
+ resolvePackagePath: vi.fn((..._args: any[]) => '/fake/transform-script.luau'),
27
+ resolveTemplatePath: vi.fn((..._args: any[]) => '/fake/default.project.json'),
28
+ }));
29
+
17
30
  vi.mock('../plugin/plugin-injector.js', () => ({
18
31
  injectPluginAsync: vi.fn(async () => ({
19
32
  pluginPath: '/fake/plugin.rbxmx',
@@ -29,13 +42,6 @@ vi.mock('../process/studio-process-manager.js', () => ({
29
42
  findPluginsFolder: vi.fn(() => '/fake/plugins'),
30
43
  }));
31
44
 
32
- vi.mock('../process/place-builder.js', () => ({
33
- buildMinimalPlaceAsync: vi.fn(async () => ({
34
- placePath: '/fake/place.rbxl',
35
- cleanupAsync: vi.fn(async () => {}),
36
- })),
37
- }));
38
-
39
45
  // ---------------------------------------------------------------------------
40
46
  // Helpers
41
47
  // ---------------------------------------------------------------------------
@@ -48,7 +54,7 @@ async function connectAndHandshake(
48
54
  port: number,
49
55
  sessionId: string
50
56
  ): Promise<WebSocket> {
51
- const ws = new WebSocket(`ws://localhost:${port}`);
57
+ const ws = new WebSocket(`ws://localhost:${port}/${sessionId}`);
52
58
 
53
59
  await new Promise<void>((resolve, reject) => {
54
60
  ws.on('open', resolve);
@@ -56,7 +62,7 @@ async function connectAndHandshake(
56
62
  });
57
63
 
58
64
  // Send hello
59
- ws.send(JSON.stringify({ type: 'hello', payload: { sessionId } }));
65
+ ws.send(JSON.stringify({ type: 'hello', sessionId, payload: { sessionId } }));
60
66
 
61
67
  // Wait for welcome
62
68
  await new Promise<void>((resolve) => {
@@ -180,7 +186,7 @@ describe('StudioBridgeServer', () => {
180
186
  expect(true).toBe(true);
181
187
  });
182
188
 
183
- it('ignores hello with wrong session ID', async () => {
189
+ it('rejects hello with wrong session ID and closes connection', async () => {
184
190
  const sessionId = 'correct-session';
185
191
  server = new StudioBridgeServer({
186
192
  placePath: '/fake/place.rbxl',
@@ -194,26 +200,66 @@ describe('StudioBridgeServer', () => {
194
200
  const wss = (server as any)._wss;
195
201
  const port: number = wss.address().port;
196
202
 
197
- // Connect with wrong session ID
198
- const ws = new WebSocket(`ws://localhost:${port}`);
203
+ // Connect with correct path but wrong session ID in message
204
+ const ws = new WebSocket(`ws://localhost:${port}/${sessionId}`);
199
205
  await new Promise<void>((resolve, reject) => {
200
206
  ws.on('open', resolve);
201
207
  ws.on('error', reject);
202
208
  });
203
209
 
210
+ // Track if server closes the connection
211
+ const closedPromise = new Promise<void>((resolve) => {
212
+ ws.on('close', () => resolve());
213
+ });
214
+
204
215
  ws.send(
205
216
  JSON.stringify({
206
217
  type: 'hello',
218
+ sessionId: 'wrong-session',
207
219
  payload: { sessionId: 'wrong-session' },
208
220
  })
209
221
  );
210
222
 
223
+ // Server should close the connection
224
+ await closedPromise;
225
+
211
226
  // Server should NOT accept — startAsync should time out
212
227
  await expect(startPromise).rejects.toThrow('Timed out waiting for Studio plugin handshake');
213
228
 
214
- ws.close();
215
229
  // Server already stopped on failure (startAsync catch cleans up)
216
230
  });
231
+
232
+ it('rejects connection to wrong WebSocket path', async () => {
233
+ const sessionId = 'correct-session';
234
+ server = new StudioBridgeServer({
235
+ placePath: '/fake/place.rbxl',
236
+ sessionId,
237
+ timeoutMs: 1_000,
238
+ });
239
+
240
+ const startPromise = server.startAsync();
241
+
242
+ await new Promise((r) => setTimeout(r, 50));
243
+ const wss = (server as any)._wss;
244
+ const port: number = wss.address().port;
245
+
246
+ // Connect with wrong path
247
+ const ws = new WebSocket(`ws://localhost:${port}/wrong-path`);
248
+
249
+ // Should get an error or unexpected response
250
+ const errorOrClose = await new Promise<string>((resolve) => {
251
+ ws.on('error', () => resolve('error'));
252
+ ws.on('unexpected-response', () => {
253
+ ws.close();
254
+ resolve('rejected');
255
+ });
256
+ });
257
+
258
+ expect(['error', 'rejected']).toContain(errorOrClose);
259
+
260
+ // startAsync should time out
261
+ await expect(startPromise).rejects.toThrow('Timed out waiting for Studio plugin handshake');
262
+ });
217
263
  });
218
264
 
219
265
  // -----------------------------------------------------------------------
@@ -221,19 +267,19 @@ describe('StudioBridgeServer', () => {
221
267
  // -----------------------------------------------------------------------
222
268
 
223
269
  describe('executeAsync', () => {
224
- it('sends execute message and returns result on scriptComplete', async () => {
270
+ it('sends execute message with sessionId and returns result on scriptComplete', async () => {
225
271
  const ready = await createReadyServer();
226
272
  server = ready.server;
227
273
  client = ready.client;
228
274
 
229
275
  // Listen for execute message from server
230
- const executePromise = new Promise<string>((resolve) => {
276
+ const executePromise = new Promise<{ script: string; sessionId: string }>((resolve) => {
231
277
  client!.on('message', (raw) => {
232
278
  const data = JSON.parse(
233
279
  typeof raw === 'string' ? raw : raw.toString('utf-8')
234
280
  );
235
281
  if (data.type === 'execute') {
236
- resolve(data.payload.script);
282
+ resolve({ script: data.payload.script, sessionId: data.sessionId });
237
283
  }
238
284
  });
239
285
  });
@@ -243,14 +289,16 @@ describe('StudioBridgeServer', () => {
243
289
  scriptContent: 'print("hello")',
244
290
  });
245
291
 
246
- // Verify the server sent the execute message with correct script
247
- const receivedScript = await executePromise;
248
- expect(receivedScript).toBe('print("hello")');
292
+ // Verify the server sent the execute message with correct script and sessionId
293
+ const received = await executePromise;
294
+ expect(received.script).toBe('print("hello")');
295
+ expect(received.sessionId).toBe(ready.sessionId);
249
296
 
250
297
  // Simulate plugin sending scriptComplete
251
298
  client!.send(
252
299
  JSON.stringify({
253
300
  type: 'scriptComplete',
301
+ sessionId: ready.sessionId,
254
302
  payload: { success: true },
255
303
  })
256
304
  );
@@ -283,6 +331,7 @@ describe('StudioBridgeServer', () => {
283
331
  client!.send(
284
332
  JSON.stringify({
285
333
  type: 'output',
334
+ sessionId: ready.sessionId,
286
335
  payload: {
287
336
  messages: [
288
337
  { level: 'Print', body: 'Line 1' },
@@ -295,6 +344,7 @@ describe('StudioBridgeServer', () => {
295
344
  client!.send(
296
345
  JSON.stringify({
297
346
  type: 'output',
347
+ sessionId: ready.sessionId,
298
348
  payload: {
299
349
  messages: [{ level: 'Error', body: 'Line 3' }],
300
350
  },
@@ -305,6 +355,7 @@ describe('StudioBridgeServer', () => {
305
355
  client!.send(
306
356
  JSON.stringify({
307
357
  type: 'scriptComplete',
358
+ sessionId: ready.sessionId,
308
359
  payload: { success: true },
309
360
  })
310
361
  );
@@ -342,6 +393,7 @@ describe('StudioBridgeServer', () => {
342
393
  client!.send(
343
394
  JSON.stringify({
344
395
  type: 'output',
396
+ sessionId: ready.sessionId,
345
397
  payload: {
346
398
  messages: [
347
399
  { level: 'Print', body: 'hello' },
@@ -354,6 +406,7 @@ describe('StudioBridgeServer', () => {
354
406
  client!.send(
355
407
  JSON.stringify({
356
408
  type: 'scriptComplete',
409
+ sessionId: ready.sessionId,
357
410
  payload: { success: true },
358
411
  })
359
412
  );
@@ -387,6 +440,7 @@ describe('StudioBridgeServer', () => {
387
440
  client!.send(
388
441
  JSON.stringify({
389
442
  type: 'scriptComplete',
443
+ sessionId: ready.sessionId,
390
444
  payload: { success: false, error: 'Script threw: boom' },
391
445
  })
392
446
  );
@@ -438,6 +492,40 @@ describe('StudioBridgeServer', () => {
438
492
  expect(result.success).toBe(false);
439
493
  expect(result.logs).toContain('Timed out after 200ms');
440
494
  });
495
+
496
+ it('ignores messages with wrong session ID during execution', async () => {
497
+ const ready = await createReadyServer();
498
+ server = ready.server;
499
+ client = ready.client;
500
+
501
+ const resultPromise = server.executeAsync({
502
+ scriptContent: 'print("test")',
503
+ timeoutMs: 500,
504
+ });
505
+
506
+ await new Promise<void>((resolve) => {
507
+ client!.on('message', (raw) => {
508
+ const data = JSON.parse(
509
+ typeof raw === 'string' ? raw : raw.toString('utf-8')
510
+ );
511
+ if (data.type === 'execute') resolve();
512
+ });
513
+ });
514
+
515
+ // Send scriptComplete with wrong session ID — should be ignored
516
+ client!.send(
517
+ JSON.stringify({
518
+ type: 'scriptComplete',
519
+ sessionId: 'wrong-session',
520
+ payload: { success: true },
521
+ })
522
+ );
523
+
524
+ // The server should ignore the above and time out
525
+ const result = await resultPromise;
526
+ expect(result.success).toBe(false);
527
+ expect(result.logs).toContain('Timed out after 500ms');
528
+ });
441
529
  });
442
530
 
443
531
  // -----------------------------------------------------------------------
@@ -472,6 +560,7 @@ describe('StudioBridgeServer', () => {
472
560
  client!.send(
473
561
  JSON.stringify({
474
562
  type: 'scriptComplete',
563
+ sessionId: ready.sessionId,
475
564
  payload: { success: true },
476
565
  })
477
566
  );
@@ -515,6 +604,7 @@ describe('StudioBridgeServer', () => {
515
604
  client!.send(
516
605
  JSON.stringify({
517
606
  type: 'output',
607
+ sessionId: ready.sessionId,
518
608
  payload: { messages: [{ level: 'Print', body: outputBody }] },
519
609
  })
520
610
  );
@@ -522,6 +612,7 @@ describe('StudioBridgeServer', () => {
522
612
  client!.send(
523
613
  JSON.stringify({
524
614
  type: 'scriptComplete',
615
+ sessionId: ready.sessionId,
525
616
  payload: { success: true },
526
617
  })
527
618
  );
@@ -577,6 +668,7 @@ describe('StudioBridgeServer', () => {
577
668
  client!.send(
578
669
  JSON.stringify({
579
670
  type: 'scriptComplete',
671
+ sessionId: ready.sessionId,
580
672
  payload: { success: true },
581
673
  })
582
674
  );
@@ -597,7 +689,7 @@ describe('StudioBridgeServer', () => {
597
689
  // -----------------------------------------------------------------------
598
690
 
599
691
  describe('stopAsync', () => {
600
- it('sends shutdown message to connected client', async () => {
692
+ it('sends shutdown message with sessionId to connected client', async () => {
601
693
  const ready = await createReadyServer();
602
694
  server = ready.server;
603
695
  client = ready.client;
@@ -610,7 +702,7 @@ describe('StudioBridgeServer', () => {
610
702
  await server.stopAsync();
611
703
 
612
704
  expect(sendSpy).toHaveBeenCalledWith(
613
- JSON.stringify({ type: 'shutdown', payload: {} })
705
+ JSON.stringify({ type: 'shutdown', sessionId: ready.sessionId, payload: {} })
614
706
  );
615
707
  });
616
708
 
@@ -10,8 +10,14 @@
10
10
  */
11
11
 
12
12
  import { randomUUID } from 'crypto';
13
+ import * as path from 'path';
13
14
  import { WebSocketServer, type WebSocket } from 'ws';
14
15
  import { OutputHelper } from '@quenty/cli-output-helpers';
16
+ import {
17
+ BuildContext,
18
+ resolvePackagePath,
19
+ resolveTemplatePath,
20
+ } from '@quenty/nevermore-template-helpers';
15
21
  import {
16
22
  type OutputLevel,
17
23
  encodeMessage,
@@ -25,10 +31,17 @@ import {
25
31
  launchStudioAsync,
26
32
  type StudioProcess,
27
33
  } from '../process/studio-process-manager.js';
28
- import {
29
- buildMinimalPlaceAsync,
30
- type BuiltPlace,
31
- } from '../process/place-builder.js';
34
+
35
+ const defaultProjectPath = resolveTemplatePath(
36
+ import.meta.url,
37
+ path.join('default-test-place', 'default.project.json')
38
+ );
39
+
40
+ const sessionAttributeTransformScript = resolvePackagePath(
41
+ import.meta.url,
42
+ 'build-scripts',
43
+ 'transform-add-session-attribute.luau'
44
+ );
32
45
 
33
46
  // ---------------------------------------------------------------------------
34
47
  // Public API types
@@ -112,7 +125,7 @@ export class StudioBridgeServer {
112
125
  private _wss: WebSocketServer | undefined;
113
126
  private _pluginHandle: InjectedPlugin | undefined;
114
127
  private _studioProc: StudioProcess | undefined;
115
- private _builtPlace: BuiltPlace | undefined;
128
+ private _placeBuildContext: BuildContext | undefined;
116
129
  private _connectedClient: WebSocket | undefined;
117
130
 
118
131
  constructor(options: StudioBridgeServerOptions = {}) {
@@ -139,16 +152,37 @@ export class StudioBridgeServer {
139
152
  this._state = 'starting';
140
153
 
141
154
  try {
155
+ this._placeBuildContext = await BuildContext.createAsync({
156
+ prefix: 'studio-bridge-',
157
+ });
158
+
142
159
  // 0. Build a minimal place if none was provided
143
160
  let placePath = this._placePath;
144
161
  if (!placePath) {
145
162
  this._onPhase?.('building');
146
- this._builtPlace = await buildMinimalPlaceAsync();
147
- placePath = this._builtPlace.placePath;
163
+ const builtPlacePath =
164
+ this._placeBuildContext.resolvePath('minimal.rbxl');
165
+ await this._placeBuildContext.rojoBuildAsync({
166
+ projectPath: defaultProjectPath,
167
+ output: builtPlacePath,
168
+ });
169
+ placePath = builtPlacePath;
148
170
  }
149
171
 
150
- // 1. Start WebSocket server
151
- this._wss = new WebSocketServer({ port: 0 });
172
+ // 0b. Stamp session ID attribute onto the place file
173
+ const transformedPlacePath = this._placeBuildContext.resolvePath(
174
+ 'studio-bridge-with-session-id.rbxl'
175
+ );
176
+ await this._placeBuildContext.executeLuneTransformScriptAsync(
177
+ sessionAttributeTransformScript,
178
+ placePath,
179
+ transformedPlacePath,
180
+ this._sessionId
181
+ );
182
+ placePath = transformedPlacePath;
183
+
184
+ // 1. Start WebSocket server (unique path rejects wrong connections at HTTP upgrade level)
185
+ this._wss = new WebSocketServer({ port: 0, path: `/${this._sessionId}` });
152
186
  const port = await startWsServerAsync(this._wss);
153
187
  OutputHelper.verbose(
154
188
  `[StudioBridge] WebSocket server listening on port ${port}`
@@ -208,6 +242,7 @@ export class StudioBridgeServer {
208
242
  this._connectedClient.send(
209
243
  encodeMessage({
210
244
  type: 'execute',
245
+ sessionId: this._sessionId,
211
246
  payload: { script: options.scriptContent },
212
247
  })
213
248
  );
@@ -247,7 +282,11 @@ export class StudioBridgeServer {
247
282
  if (this._connectedClient) {
248
283
  try {
249
284
  this._connectedClient.send(
250
- encodeMessage({ type: 'shutdown', payload: {} })
285
+ encodeMessage({
286
+ type: 'shutdown',
287
+ sessionId: this._sessionId,
288
+ payload: {},
289
+ })
251
290
  );
252
291
  } catch {
253
292
  // ignore
@@ -287,10 +326,14 @@ export class StudioBridgeServer {
287
326
  return;
288
327
  }
289
328
 
290
- if (msg.payload.sessionId !== this._sessionId) {
329
+ if (
330
+ msg.sessionId !== this._sessionId ||
331
+ msg.payload.sessionId !== this._sessionId
332
+ ) {
291
333
  OutputHelper.verbose(
292
- `[StudioBridge] Ignoring hello with wrong session ID: ${msg.payload.sessionId}`
334
+ `[StudioBridge] Rejecting hello with wrong session ID`
293
335
  );
336
+ ws.close();
294
337
  return;
295
338
  }
296
339
 
@@ -299,6 +342,7 @@ export class StudioBridgeServer {
299
342
  ws.send(
300
343
  encodeMessage({
301
344
  type: 'welcome',
345
+ sessionId: this._sessionId,
302
346
  payload: { sessionId: this._sessionId },
303
347
  })
304
348
  );
@@ -387,6 +431,13 @@ export class StudioBridgeServer {
387
431
  return;
388
432
  }
389
433
 
434
+ if (msg.sessionId !== this._sessionId) {
435
+ OutputHelper.verbose(
436
+ `[StudioBridge] Ignoring message with wrong session ID`
437
+ );
438
+ return;
439
+ }
440
+
390
441
  switch (msg.type) {
391
442
  case 'output': {
392
443
  for (const entry of msg.payload.messages) {
@@ -461,9 +512,9 @@ export class StudioBridgeServer {
461
512
  }
462
513
 
463
514
  // Remove auto-built place
464
- if (this._builtPlace) {
465
- await this._builtPlace.cleanupAsync();
466
- this._builtPlace = undefined;
515
+ if (this._placeBuildContext) {
516
+ await this._placeBuildContext.cleanupAsync();
517
+ this._placeBuildContext = undefined;
467
518
  }
468
519
 
469
520
  // Close WebSocket server — terminate lingering connections first so
@@ -16,7 +16,7 @@ import {
16
16
  * internal server, returning the port and a promise for the result.
17
17
  */
18
18
  function createTestServer(expectedSessionId: string) {
19
- const wss = new WebSocketServer({ port: 0 });
19
+ const wss = new WebSocketServer({ port: 0, path: `/${expectedSessionId}` });
20
20
  const logLines: string[] = [];
21
21
 
22
22
  const resultPromise = new Promise<{ success: boolean; logs: string }>(
@@ -27,12 +27,15 @@ function createTestServer(expectedSessionId: string) {
27
27
  const msg = decodePluginMessage(data);
28
28
  if (!msg) return;
29
29
 
30
+ if (msg.sessionId !== expectedSessionId) return;
31
+
30
32
  switch (msg.type) {
31
33
  case 'hello':
32
34
  if (msg.payload.sessionId === expectedSessionId) {
33
35
  ws.send(
34
36
  encodeMessage({
35
37
  type: 'welcome',
38
+ sessionId: expectedSessionId,
36
39
  payload: { sessionId: expectedSessionId },
37
40
  })
38
41
  );
@@ -80,7 +83,7 @@ describe('WebSocket protocol smoke test', () => {
80
83
  const port = await portPromise;
81
84
 
82
85
  // Simulate the plugin side with a plain WebSocket client
83
- const ws = new WebSocket(`ws://localhost:${port}`);
86
+ const ws = new WebSocket(`ws://localhost:${port}/${sessionId}`);
84
87
 
85
88
  await new Promise<void>((resolve, reject) => {
86
89
  ws.on('open', resolve);
@@ -88,7 +91,7 @@ describe('WebSocket protocol smoke test', () => {
88
91
  });
89
92
 
90
93
  // 1. Send hello
91
- ws.send(JSON.stringify({ type: 'hello', payload: { sessionId } }));
94
+ ws.send(JSON.stringify({ type: 'hello', sessionId, payload: { sessionId } }));
92
95
 
93
96
  // 2. Wait for welcome
94
97
  const welcome = await new Promise<ServerMessage>((resolve) => {
@@ -103,6 +106,7 @@ describe('WebSocket protocol smoke test', () => {
103
106
  });
104
107
 
105
108
  expect(welcome.type).toBe('welcome');
109
+ expect(welcome.sessionId).toBe(sessionId);
106
110
  expect(
107
111
  (welcome as { payload: { sessionId: string } }).payload.sessionId
108
112
  ).toBe(sessionId);
@@ -111,6 +115,7 @@ describe('WebSocket protocol smoke test', () => {
111
115
  ws.send(
112
116
  JSON.stringify({
113
117
  type: 'output',
118
+ sessionId,
114
119
  payload: {
115
120
  messages: [
116
121
  { level: 'Print', body: 'Hello from test' },
@@ -124,6 +129,7 @@ describe('WebSocket protocol smoke test', () => {
124
129
  ws.send(
125
130
  JSON.stringify({
126
131
  type: 'scriptComplete',
132
+ sessionId,
127
133
  payload: { success: true },
128
134
  })
129
135
  );
@@ -150,14 +156,14 @@ describe('WebSocket protocol smoke test', () => {
150
156
 
151
157
  try {
152
158
  const port = await portPromise;
153
- const ws = new WebSocket(`ws://localhost:${port}`);
159
+ const ws = new WebSocket(`ws://localhost:${port}/${sessionId}`);
154
160
 
155
161
  await new Promise<void>((resolve, reject) => {
156
162
  ws.on('open', resolve);
157
163
  ws.on('error', reject);
158
164
  });
159
165
 
160
- ws.send(JSON.stringify({ type: 'hello', payload: { sessionId } }));
166
+ ws.send(JSON.stringify({ type: 'hello', sessionId, payload: { sessionId } }));
161
167
 
162
168
  // Wait for welcome before sending more
163
169
  await new Promise<void>((resolve) => {
@@ -172,6 +178,7 @@ describe('WebSocket protocol smoke test', () => {
172
178
  ws.send(
173
179
  JSON.stringify({
174
180
  type: 'output',
181
+ sessionId,
175
182
  payload: {
176
183
  messages: [{ level: 'Error', body: 'Something went wrong' }],
177
184
  },
@@ -181,6 +188,7 @@ describe('WebSocket protocol smoke test', () => {
181
188
  ws.send(
182
189
  JSON.stringify({
183
190
  type: 'scriptComplete',
191
+ sessionId,
184
192
  payload: { success: false, error: 'Script threw an error' },
185
193
  })
186
194
  );
@@ -197,7 +205,7 @@ describe('WebSocket protocol smoke test', () => {
197
205
 
198
206
  it('rejects hello with wrong session ID', async () => {
199
207
  const sessionId = 'correct-session';
200
- const wss = new WebSocketServer({ port: 0 });
208
+ const wss = new WebSocketServer({ port: 0, path: `/${sessionId}` });
201
209
 
202
210
  try {
203
211
  const port = await new Promise<number>((resolve) => {
@@ -212,14 +220,14 @@ describe('WebSocket protocol smoke test', () => {
212
220
  ws.on('message', (raw) => {
213
221
  const data = typeof raw === 'string' ? raw : raw.toString('utf-8');
214
222
  const msg = decodePluginMessage(data);
215
- if (msg?.type === 'hello' && msg.payload.sessionId === sessionId) {
223
+ if (msg?.type === 'hello' && msg.sessionId === sessionId && msg.payload.sessionId === sessionId) {
216
224
  welcomeSent = true;
217
- ws.send(encodeMessage({ type: 'welcome', payload: { sessionId } }));
225
+ ws.send(encodeMessage({ type: 'welcome', sessionId, payload: { sessionId } }));
218
226
  }
219
227
  });
220
228
  });
221
229
 
222
- const ws = new WebSocket(`ws://localhost:${port}`);
230
+ const ws = new WebSocket(`ws://localhost:${port}/${sessionId}`);
223
231
  await new Promise<void>((resolve, reject) => {
224
232
  ws.on('open', resolve);
225
233
  ws.on('error', reject);
@@ -229,6 +237,7 @@ describe('WebSocket protocol smoke test', () => {
229
237
  ws.send(
230
238
  JSON.stringify({
231
239
  type: 'hello',
240
+ sessionId: 'wrong-session',
232
241
  payload: { sessionId: 'wrong-session' },
233
242
  })
234
243
  );