@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.
- package/CHANGELOG.md +37 -1
- package/build-scripts/transform-add-session-attribute.luau +17 -0
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +0 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/plugin/plugin-injector.d.ts.map +1 -1
- package/dist/src/plugin/plugin-injector.js +11 -34
- package/dist/src/plugin/plugin-injector.js.map +1 -1
- package/dist/src/process/index.d.ts +0 -2
- package/dist/src/process/index.d.ts.map +1 -1
- package/dist/src/process/index.js +0 -1
- package/dist/src/process/index.js.map +1 -1
- package/dist/src/server/studio-bridge-server.d.ts +1 -1
- package/dist/src/server/studio-bridge-server.d.ts.map +1 -1
- package/dist/src/server/studio-bridge-server.js +38 -12
- package/dist/src/server/studio-bridge-server.js.map +1 -1
- package/dist/src/server/studio-bridge-server.test.js +94 -19
- package/dist/src/server/studio-bridge-server.test.js.map +1 -1
- package/dist/src/server/web-socket-protocol.d.ts +7 -1
- package/dist/src/server/web-socket-protocol.d.ts.map +1 -1
- package/dist/src/server/web-socket-protocol.js +8 -4
- package/dist/src/server/web-socket-protocol.js.map +1 -1
- package/dist/src/server/web-socket-protocol.smoke.test.js +18 -9
- package/dist/src/server/web-socket-protocol.smoke.test.js.map +1 -1
- package/dist/src/server/web-socket-protocol.test.js +47 -5
- package/dist/src/server/web-socket-protocol.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/index.ts +0 -1
- package/src/plugin/plugin-injector.ts +19 -45
- package/src/process/index.ts +0 -3
- package/src/server/studio-bridge-server.test.ts +113 -21
- package/src/server/studio-bridge-server.ts +66 -15
- package/src/server/web-socket-protocol.smoke.test.ts +18 -9
- package/src/server/web-socket-protocol.test.ts +61 -5
- package/src/server/web-socket-protocol.ts +15 -4
- package/templates/default-test-place/default.project.json +1 -1
- package/templates/studio-bridge-plugin/default.project.json +1 -1
- package/templates/studio-bridge-plugin/{plugin-template.server.lua → src/StudioBridgePlugin.server.lua} +18 -4
- package/dist/src/process/place-builder.d.ts +0 -16
- package/dist/src/process/place-builder.d.ts.map +0 -1
- package/dist/src/process/place-builder.js +0 -45
- package/dist/src/process/place-builder.js.map +0 -1
- 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('
|
|
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
|
|
248
|
-
expect(
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
//
|
|
151
|
-
|
|
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({
|
|
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 (
|
|
329
|
+
if (
|
|
330
|
+
msg.sessionId !== this._sessionId ||
|
|
331
|
+
msg.payload.sessionId !== this._sessionId
|
|
332
|
+
) {
|
|
291
333
|
OutputHelper.verbose(
|
|
292
|
-
`[StudioBridge]
|
|
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.
|
|
465
|
-
await this.
|
|
466
|
-
this.
|
|
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
|
);
|