@quenty/studio-bridge 0.22.1 → 0.24.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 +25 -0
- package/README.md +36 -8
- package/dist/src/bridge/bridge-connection.d.ts.map +1 -1
- package/dist/src/bridge/bridge-connection.js +1 -0
- package/dist/src/bridge/bridge-connection.js.map +1 -1
- package/dist/src/bridge/bridge-session.d.ts +11 -16
- package/dist/src/bridge/bridge-session.d.ts.map +1 -1
- package/dist/src/bridge/bridge-session.js +37 -29
- package/dist/src/bridge/bridge-session.js.map +1 -1
- package/dist/src/bridge/bridge-session.test.js +35 -0
- package/dist/src/bridge/bridge-session.test.js.map +1 -1
- package/dist/src/bridge/internal/bridge-host.d.ts +2 -0
- package/dist/src/bridge/internal/bridge-host.d.ts.map +1 -1
- package/dist/src/bridge/internal/bridge-host.js +1 -0
- package/dist/src/bridge/internal/bridge-host.js.map +1 -1
- package/dist/src/bridge/internal/host-protocol.d.ts.map +1 -1
- package/dist/src/bridge/internal/host-protocol.js +23 -10
- package/dist/src/bridge/internal/host-protocol.js.map +1 -1
- package/dist/src/bridge/types.d.ts +5 -2
- package/dist/src/bridge/types.d.ts.map +1 -1
- package/dist/src/bridge/types.js.map +1 -1
- package/dist/src/commands/console/exec/exec.d.ts +13 -3
- package/dist/src/commands/console/exec/exec.d.ts.map +1 -1
- package/dist/src/commands/console/exec/exec.js +82 -7
- package/dist/src/commands/console/exec/exec.js.map +1 -1
- package/dist/src/commands/console/exec/exec.test.js +63 -1
- package/dist/src/commands/console/exec/exec.test.js.map +1 -1
- package/dist/src/commands/framework/action-loader.test.js +1 -1
- package/dist/src/commands/framework/action-loader.test.js.map +1 -1
- package/dist/src/commands/playtest/player/playtest-players.d.ts.map +1 -1
- package/dist/src/commands/playtest/player/playtest-players.js +15 -93
- package/dist/src/commands/playtest/player/playtest-players.js.map +1 -1
- package/dist/src/commands/playtest/player/playtest-players.test.js +34 -21
- package/dist/src/commands/playtest/player/playtest-players.test.js.map +1 -1
- package/dist/src/commands/playtest/playtest-sessions.d.ts +19 -3
- package/dist/src/commands/playtest/playtest-sessions.d.ts.map +1 -1
- package/dist/src/commands/playtest/playtest-sessions.js +54 -74
- package/dist/src/commands/playtest/playtest-sessions.js.map +1 -1
- package/dist/src/commands/playtest/playtest-sessions.test.js +74 -8
- package/dist/src/commands/playtest/playtest-sessions.test.js.map +1 -1
- package/dist/src/commands/playtest/run/run.d.ts +9 -7
- package/dist/src/commands/playtest/run/run.d.ts.map +1 -1
- package/dist/src/commands/playtest/run/run.js +33 -38
- package/dist/src/commands/playtest/run/run.js.map +1 -1
- package/dist/src/commands/process/close/close.d.ts +4 -0
- package/dist/src/commands/process/close/close.d.ts.map +1 -1
- package/dist/src/commands/process/close/close.js +54 -4
- package/dist/src/commands/process/close/close.js.map +1 -1
- package/dist/src/commands/process/close/close.test.js +36 -3
- package/dist/src/commands/process/close/close.test.js.map +1 -1
- package/dist/src/commands/process/info/info.d.ts +15 -2
- package/dist/src/commands/process/info/info.d.ts.map +1 -1
- package/dist/src/commands/process/info/info.js +44 -4
- package/dist/src/commands/process/info/info.js.map +1 -1
- package/dist/src/commands/process/info/info.test.js +48 -0
- package/dist/src/commands/process/info/info.test.js.map +1 -1
- package/dist/src/commands/process/launch/launch.d.ts.map +1 -1
- package/dist/src/commands/process/launch/launch.js +22 -3
- package/dist/src/commands/process/launch/launch.js.map +1 -1
- package/dist/src/commands/process/launch/launch.test.js +27 -5
- package/dist/src/commands/process/launch/launch.test.js.map +1 -1
- package/dist/src/commands/process/list/list.d.ts +49 -6
- package/dist/src/commands/process/list/list.d.ts.map +1 -1
- package/dist/src/commands/process/list/list.js +185 -18
- package/dist/src/commands/process/list/list.js.map +1 -1
- package/dist/src/commands/process/list/list.test.js +92 -1
- package/dist/src/commands/process/list/list.test.js.map +1 -1
- package/dist/src/commands/process/run/run.d.ts +1 -1
- package/dist/src/commands/process/run/run.d.ts.map +1 -1
- package/dist/src/commands/process/run/run.js +3 -3
- package/dist/src/commands/process/run/run.js.map +1 -1
- package/dist/src/docker/docker-delegator.js +1 -1
- package/dist/src/docker/docker-delegator.js.map +1 -1
- package/dist/src/docker/docker-delegator.test.js +1 -1
- package/dist/src/docker/docker-delegator.test.js.map +1 -1
- package/dist/src/plugin/playtest-bridge.d.ts +36 -0
- package/dist/src/plugin/playtest-bridge.d.ts.map +1 -0
- package/dist/src/plugin/playtest-bridge.js +65 -0
- package/dist/src/plugin/playtest-bridge.js.map +1 -0
- package/dist/src/plugin/playtest-bridge.test.d.ts +2 -0
- package/dist/src/plugin/playtest-bridge.test.d.ts.map +1 -0
- package/dist/src/plugin/playtest-bridge.test.js +35 -0
- package/dist/src/plugin/playtest-bridge.test.js.map +1 -0
- package/dist/src/process/studio-launch-registry.d.ts +14 -0
- package/dist/src/process/studio-launch-registry.d.ts.map +1 -0
- package/dist/src/process/studio-launch-registry.js +37 -0
- package/dist/src/process/studio-launch-registry.js.map +1 -0
- package/dist/src/process/studio-launch-registry.test.d.ts +2 -0
- package/dist/src/process/studio-launch-registry.test.d.ts.map +1 -0
- package/dist/src/process/studio-launch-registry.test.js +38 -0
- package/dist/src/process/studio-launch-registry.test.js.map +1 -0
- package/dist/src/process/studio-process-closer.d.ts +3 -1
- package/dist/src/process/studio-process-closer.d.ts.map +1 -1
- package/dist/src/process/studio-process-closer.js +29 -11
- package/dist/src/process/studio-process-closer.js.map +1 -1
- package/dist/src/process/studio-process-identity.d.ts +10 -0
- package/dist/src/process/studio-process-identity.d.ts.map +1 -0
- package/dist/src/process/studio-process-identity.js +63 -0
- package/dist/src/process/studio-process-identity.js.map +1 -0
- package/dist/src/process/studio-process-identity.test.d.ts +2 -0
- package/dist/src/process/studio-process-identity.test.d.ts.map +1 -0
- package/dist/src/process/studio-process-identity.test.js +30 -0
- package/dist/src/process/studio-process-identity.test.js.map +1 -0
- package/dist/src/roblox-mcp/index.d.ts +3 -0
- package/dist/src/roblox-mcp/index.d.ts.map +1 -0
- package/dist/src/roblox-mcp/index.js +3 -0
- package/dist/src/roblox-mcp/index.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.d.ts +60 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.js +171 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.d.ts +2 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.js +104 -0
- package/dist/src/roblox-mcp/mcp/mcp-stdio-client.test.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.d.ts +2 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.js +22 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.js.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.d.ts +2 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.js +42 -0
- package/dist/src/roblox-mcp/mcp/repair-list-result.test.js.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.d.ts +34 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.d.ts.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.js +190 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.js.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.d.ts +2 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.js +123 -0
- package/dist/src/roblox-mcp/roblox-studio-mcp.test.js.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.d.ts +39 -0
- package/dist/src/roblox-mcp/studio/studio-session.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.js +175 -0
- package/dist/src/roblox-mcp/studio/studio-session.js.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.d.ts +2 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.js +213 -0
- package/dist/src/roblox-mcp/studio/studio-session.test.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.d.ts +19 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.js +121 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.d.ts +2 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.js +107 -0
- package/dist/src/roblox-mcp/studio-mcp-locator.test.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.d.ts +23 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.js +65 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.js.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.d.ts +2 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.d.ts.map +1 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.js +81 -0
- package/dist/src/roblox-mcp/studio-mcp-snapshot.test.js.map +1 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.d.ts +27 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.d.ts.map +1 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.js +55 -0
- package/dist/src/roblox-mcp/test/fake-mcp-server.js.map +1 -0
- package/dist/src/server/web-socket-protocol.d.ts +50 -15
- package/dist/src/server/web-socket-protocol.d.ts.map +1 -1
- package/dist/src/server/web-socket-protocol.js +63 -13
- package/dist/src/server/web-socket-protocol.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/docs/playtest-bridge.md +21 -0
- package/package.json +5 -5
- package/src/bridge/bridge-connection.ts +1 -0
- package/src/bridge/bridge-session.test.ts +51 -0
- package/src/bridge/bridge-session.ts +66 -47
- package/src/bridge/internal/bridge-host.ts +3 -0
- package/src/bridge/internal/host-protocol.ts +22 -9
- package/src/bridge/types.ts +5 -0
- package/src/commands/console/exec/exec.test.ts +94 -0
- package/src/commands/console/exec/exec.ts +130 -16
- package/src/commands/console/exec/execute.lua +147 -5
- package/src/commands/console/exec/execute.spec.lua +22 -2
- package/src/commands/framework/action-loader.test.ts +1 -1
- package/src/commands/playtest/player/playtest-players.lua +127 -0
- package/src/commands/playtest/player/playtest-players.spec.lua +48 -0
- package/src/commands/playtest/player/playtest-players.test.ts +40 -21
- package/src/commands/playtest/player/playtest-players.ts +30 -110
- package/src/commands/playtest/playtest-sessions.test.ts +116 -14
- package/src/commands/playtest/playtest-sessions.ts +77 -88
- package/src/commands/playtest/run/run.ts +44 -58
- package/src/commands/playtest/start/start-playtest.lua +105 -6
- package/src/commands/process/close/close.test.ts +52 -3
- package/src/commands/process/close/close.ts +78 -4
- package/src/commands/process/info/info.test.ts +76 -0
- package/src/commands/process/info/info.ts +74 -6
- package/src/commands/process/launch/launch.test.ts +33 -5
- package/src/commands/process/launch/launch.ts +41 -3
- package/src/commands/process/list/list.test.ts +130 -1
- package/src/commands/process/list/list.ts +290 -22
- package/src/commands/process/run/run.ts +4 -4
- package/src/docker/docker-delegator.test.ts +1 -1
- package/src/docker/docker-delegator.ts +1 -1
- package/src/plugin/playtest-bridge.test.ts +53 -0
- package/src/plugin/playtest-bridge.ts +98 -0
- package/src/process/studio-launch-registry.test.ts +56 -0
- package/src/process/studio-launch-registry.ts +63 -0
- package/src/process/studio-process-closer.ts +36 -11
- package/src/process/studio-process-identity.test.ts +45 -0
- package/src/process/studio-process-identity.ts +90 -0
- package/src/roblox-mcp/index.ts +17 -0
- package/src/roblox-mcp/mcp/mcp-stdio-client.test.ts +148 -0
- package/src/roblox-mcp/mcp/mcp-stdio-client.ts +270 -0
- package/src/roblox-mcp/mcp/repair-list-result.test.ts +52 -0
- package/src/roblox-mcp/mcp/repair-list-result.ts +23 -0
- package/src/roblox-mcp/roblox-studio-mcp.test.ts +168 -0
- package/src/roblox-mcp/roblox-studio-mcp.ts +330 -0
- package/src/roblox-mcp/studio/studio-session.test.ts +284 -0
- package/src/roblox-mcp/studio/studio-session.ts +279 -0
- package/src/roblox-mcp/studio-mcp-locator.test.ts +186 -0
- package/src/roblox-mcp/studio-mcp-locator.ts +180 -0
- package/src/roblox-mcp/studio-mcp-snapshot.test.ts +113 -0
- package/src/roblox-mcp/studio-mcp-snapshot.ts +104 -0
- package/src/roblox-mcp/test/fake-mcp-server.ts +74 -0
- package/src/server/web-socket-protocol.ts +119 -29
- package/templates/studio-bridge-plugin/playtest/StudioBridgePlaytest.client.lua +32 -0
- package/templates/studio-bridge-plugin/playtest/StudioBridgePlaytest.server.lua +40 -0
- package/templates/studio-bridge-plugin/src/Shared/ActionRouter.lua +25 -3
- package/templates/studio-bridge-plugin/src/Shared/PrecompiledSources.lua +47 -0
- package/templates/studio-bridge-plugin/src/Shared/PrecompiledSources.spec.lua +57 -0
- package/templates/studio-bridge-plugin/src/Shared/StudioBridgeClient.lua +357 -0
- package/templates/studio-bridge-plugin/src/Shared/StudioBridgeSessionUtils.lua +29 -3
- package/templates/studio-bridge-plugin/src/Shared/StudioBridgeSessionUtils.spec.lua +35 -2
- package/templates/studio-bridge-plugin/src/StudioBridgePlugin.server.lua +16 -292
- package/src/commands/playtest/run/playtest-runner.luau +0 -264
- package/src/commands/playtest/run/playtest-runner.spec.lua +0 -71
- package/src/commands/playtest/run/run-playtest-script.lua +0 -139
- package/src/commands/playtest/run/run-playtest-script.spec.lua +0 -43
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# The planted playtest bridge
|
|
2
|
+
|
|
3
|
+
With Studio's **Load user plugins on playtest** off, the studio-bridge plugin never loads into a playtest. So `startPlaytest` plants the same client (`templates/studio-bridge-plugin/src/Shared/StudioBridgeClient.lua`) into the edit place's ServerScriptService. The playtest copies the edit place, so the client starts in the playtest's server as an ordinary Script and connects as its server session. In play mode it also plants the client's twin as a LocalScript in StarterPlayerScripts, which every player's client runs and connects as its client session; a LocalScript there can open the WebSocket to the bridge and read the test args, measured in a Studio playtest client. With the setting on, nothing is planted and the plugin is the playtest's sessions.
|
|
4
|
+
|
|
5
|
+
## What an ordinary playtest Script can do
|
|
6
|
+
|
|
7
|
+
These limits shape the design. They were measured in a Studio playtest server:
|
|
8
|
+
|
|
9
|
+
| Capability | Result |
|
|
10
|
+
|---|---|
|
|
11
|
+
| WebSocket to the bridge on localhost (`CreateWebStreamClient`) | Works, even with Allow HTTP Requests off |
|
|
12
|
+
| `HttpService:GetAsync` to localhost | Refused, so the port comes from the StudioTestService test args instead of a scan |
|
|
13
|
+
| `loadstring` | Off. `LoadStringEnabled` isn't scriptable, not even from a plugin |
|
|
14
|
+
| Setting `ModuleScript.Source` | Needs the `PluginOrOpenCloud` capability |
|
|
15
|
+
| `SerializationService` | `GetService` returns `nil` |
|
|
16
|
+
|
|
17
|
+
So the planted client can't compile code. The edit session compiles everything ahead of time: each action, and any script `playtest run` will send, becomes a ModuleScript returning its function and its own source text. The router finds a source by its exact text (`PrecompiledSources.lua`, built by `src/plugin/playtest-bridge.ts`). New code sent to a planted session fails and asks for the setting. Privileged compiling, for example through Studio's MCP `execute_luau`, would plug in at `ActionRouter:SetCompiler`.
|
|
18
|
+
|
|
19
|
+
A precompiled source that fails to load is skipped, which would hide its compile error. So `startPlaytest` compiles every one in the edit session before planting.
|
|
20
|
+
|
|
21
|
+
`LogService.MessageOut` fires a frame or two after a `print` in a playtest server, later than in a plugin. That's why `execute.lua` waits several frames before it stops capturing output.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/studio-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "WebSocket-based bridge for running Luau scripts in Roblox Studio",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
"studio-bridge": "./dist/src/cli/cli.js"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@quenty/cli-output-helpers": "1.
|
|
19
|
-
"@quenty/nevermore-cli-helpers": "1.
|
|
18
|
+
"@quenty/cli-output-helpers": "1.20.0",
|
|
19
|
+
"@quenty/nevermore-cli-helpers": "1.21.0",
|
|
20
20
|
"@quenty/nevermore-deploy": "1.1.1",
|
|
21
|
-
"@quenty/nevermore-template-helpers": "1.
|
|
21
|
+
"@quenty/nevermore-template-helpers": "1.22.0",
|
|
22
22
|
"execa": "^9.6.1",
|
|
23
23
|
"ws": "^8.18.0",
|
|
24
24
|
"yargs": "^18.0.0",
|
|
@@ -41,5 +41,5 @@
|
|
|
41
41
|
"test:watch": "vitest",
|
|
42
42
|
"test:integration": "node dist/test/integration/smoke-test.js"
|
|
43
43
|
},
|
|
44
|
-
"gitHead": "
|
|
44
|
+
"gitHead": "2d4d5cbd9339e7be7e36d30aa91f7146225cb500"
|
|
45
45
|
}
|
|
@@ -687,6 +687,7 @@ export class BridgeConnection extends EventEmitter {
|
|
|
687
687
|
origin: info.launchId ? 'managed' : 'user',
|
|
688
688
|
launchId: info.launchId,
|
|
689
689
|
playerName: info.playerName,
|
|
690
|
+
playtestId: info.playtestId,
|
|
690
691
|
context,
|
|
691
692
|
instanceId: info.instanceId ?? info.sessionId,
|
|
692
693
|
placeId: 0,
|
|
@@ -162,6 +162,57 @@ describe('BridgeSession', () => {
|
|
|
162
162
|
expect(timeout).toBe(10_000);
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
+
it('sends inputs base64-encoded', async () => {
|
|
166
|
+
const handle = new MockTransportHandle();
|
|
167
|
+
handle.sendActionAsync.mockResolvedValueOnce({
|
|
168
|
+
type: 'scriptComplete',
|
|
169
|
+
sessionId: 'session-1',
|
|
170
|
+
payload: { success: true },
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const session = new BridgeSession(createSessionInfo(), handle);
|
|
174
|
+
await session.execAsync('return ...', undefined, undefined, [
|
|
175
|
+
Buffer.from('hello'),
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
const [msg] = handle.sendActionAsync.mock.calls[0];
|
|
179
|
+
expect((msg as any).payload.inputs).toEqual(['aGVsbG8=']);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('sends --run-as and carries back where the code ran', async () => {
|
|
183
|
+
const handle = new MockTransportHandle();
|
|
184
|
+
handle.sendActionAsync.mockResolvedValueOnce({
|
|
185
|
+
type: 'scriptComplete',
|
|
186
|
+
sessionId: 'session-1',
|
|
187
|
+
payload: { success: true, ranAs: 'game' },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const session = new BridgeSession(createSessionInfo(), handle);
|
|
191
|
+
const result = await session.execAsync(
|
|
192
|
+
'return 1',
|
|
193
|
+
undefined,
|
|
194
|
+
undefined,
|
|
195
|
+
undefined,
|
|
196
|
+
'game'
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const [msg] = handle.sendActionAsync.mock.calls[0];
|
|
200
|
+
expect((msg as any).payload.runAs).toBe('game');
|
|
201
|
+
expect(result.ranAs).toBe('game');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('refuses inputs over the size limit without sending them', async () => {
|
|
205
|
+
const handle = new MockTransportHandle();
|
|
206
|
+
const session = new BridgeSession(createSessionInfo(), handle);
|
|
207
|
+
|
|
208
|
+
await expect(
|
|
209
|
+
session.execAsync('return ...', undefined, undefined, [
|
|
210
|
+
Buffer.alloc(192 * 1024 * 1024 + 1),
|
|
211
|
+
])
|
|
212
|
+
).rejects.toThrow('that can be sent to Studio with a script');
|
|
213
|
+
expect(handle.sendActionAsync).not.toHaveBeenCalled();
|
|
214
|
+
});
|
|
215
|
+
|
|
165
216
|
it('returns error info from error response', async () => {
|
|
166
217
|
const handle = new MockTransportHandle();
|
|
167
218
|
handle.sendActionAsync.mockResolvedValueOnce({
|
|
@@ -41,6 +41,17 @@ import {
|
|
|
41
41
|
type ActionSource,
|
|
42
42
|
} from '../commands/framework/action-loader.js';
|
|
43
43
|
import { OutputHelper } from '@quenty/cli-output-helpers';
|
|
44
|
+
import {
|
|
45
|
+
MAX_EXECUTE_INPUT_BYTES,
|
|
46
|
+
type PlaytestBridgePayload,
|
|
47
|
+
type PlaytestPlayerPayload,
|
|
48
|
+
type PlaytestPlayersRequest,
|
|
49
|
+
} from '../server/web-socket-protocol.js';
|
|
50
|
+
|
|
51
|
+
export interface PlaytestPlayersResult {
|
|
52
|
+
players: PlaytestPlayerPayload[];
|
|
53
|
+
remaining: string[];
|
|
54
|
+
}
|
|
44
55
|
|
|
45
56
|
const EXECUTE_REPLY_GRACE_MS = 5_000;
|
|
46
57
|
|
|
@@ -57,18 +68,6 @@ const DEFAULT_TIMEOUTS: Record<string, number> = {
|
|
|
57
68
|
endPlaytest: 5_000,
|
|
58
69
|
};
|
|
59
70
|
|
|
60
|
-
/** Time a playtest gets to start and stop on top of its script's own timeout. */
|
|
61
|
-
const PLAYTEST_STARTUP_ALLOWANCE_MS = 60_000;
|
|
62
|
-
|
|
63
|
-
export interface RunPlaytestScriptOptions {
|
|
64
|
-
mode: PlaytestMode;
|
|
65
|
-
script: string;
|
|
66
|
-
runnerSource: string;
|
|
67
|
-
timeoutSec: number;
|
|
68
|
-
/** Passed to the script as `...`. */
|
|
69
|
-
args?: string[];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
71
|
/** Map an `execute`-style reply onto an ExecResult. */
|
|
73
72
|
function toExecResult(result: PluginMessage): ExecResult {
|
|
74
73
|
if (result.type === 'scriptComplete') {
|
|
@@ -82,6 +81,7 @@ function toExecResult(result: PluginMessage): ExecResult {
|
|
|
82
81
|
error: result.payload.error,
|
|
83
82
|
traceback: result.payload.traceback,
|
|
84
83
|
returnValues: result.payload.returnValues,
|
|
84
|
+
...(result.payload.ranAs ? { ranAs: result.payload.ranAs } : {}),
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -156,8 +156,19 @@ export class BridgeSession extends EventEmitter {
|
|
|
156
156
|
async execAsync(
|
|
157
157
|
code: string,
|
|
158
158
|
timeout?: number,
|
|
159
|
-
args?: string[]
|
|
159
|
+
args?: string[],
|
|
160
|
+
inputs?: Buffer[],
|
|
161
|
+
runAs?: 'plugin' | 'game'
|
|
160
162
|
): Promise<ExecResult> {
|
|
163
|
+
const inputBytes = (inputs ?? []).reduce((sum, b) => sum + b.length, 0);
|
|
164
|
+
if (inputBytes > MAX_EXECUTE_INPUT_BYTES) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`The inputs are ${_formatMiB(inputBytes)}, over the ${_formatMiB(
|
|
167
|
+
MAX_EXECUTE_INPUT_BYTES
|
|
168
|
+
)} that can be sent to Studio with a script`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
161
172
|
this._assertConnected();
|
|
162
173
|
await this._ensureActionsAsync();
|
|
163
174
|
|
|
@@ -173,42 +184,13 @@ export class BridgeSession extends EventEmitter {
|
|
|
173
184
|
script: code,
|
|
174
185
|
timeoutMs,
|
|
175
186
|
...(args && args.length > 0 ? { args } : {}),
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
timeoutMs + EXECUTE_REPLY_GRACE_MS
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
return toExecResult(result);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Run a script in a fresh playtest's server, started from this edit
|
|
186
|
-
* session, and resolve with its result once the playtest has ended. Works
|
|
187
|
-
* without the playtest's own sessions connecting: the plugin plants
|
|
188
|
-
* `runnerSource` in the playtest to run the script and end the playtest.
|
|
189
|
-
*/
|
|
190
|
-
async runPlaytestScriptAsync(
|
|
191
|
-
options: RunPlaytestScriptOptions
|
|
192
|
-
): Promise<ExecResult> {
|
|
193
|
-
this._assertConnected();
|
|
194
|
-
await this._ensureActionsAsync();
|
|
195
|
-
|
|
196
|
-
const result = await this._handle.sendActionAsync<PluginMessage>(
|
|
197
|
-
{
|
|
198
|
-
type: 'runPlaytestScript',
|
|
199
|
-
sessionId: this._info.sessionId,
|
|
200
|
-
requestId: randomUUID(),
|
|
201
|
-
payload: {
|
|
202
|
-
mode: options.mode,
|
|
203
|
-
script: options.script,
|
|
204
|
-
runnerSource: options.runnerSource,
|
|
205
|
-
timeoutSec: options.timeoutSec,
|
|
206
|
-
...(options.args && options.args.length > 0
|
|
207
|
-
? { args: options.args }
|
|
187
|
+
...(inputs && inputs.length > 0
|
|
188
|
+
? { inputs: inputs.map((input) => input.toString('base64')) }
|
|
208
189
|
: {}),
|
|
190
|
+
...(runAs ? { runAs } : {}),
|
|
209
191
|
},
|
|
210
192
|
},
|
|
211
|
-
|
|
193
|
+
timeoutMs + EXECUTE_REPLY_GRACE_MS
|
|
212
194
|
);
|
|
213
195
|
|
|
214
196
|
return toExecResult(result);
|
|
@@ -442,7 +424,8 @@ export class BridgeSession extends EventEmitter {
|
|
|
442
424
|
playtestId: string,
|
|
443
425
|
bridgePort: number,
|
|
444
426
|
timeoutMs: number,
|
|
445
|
-
players?: number
|
|
427
|
+
players?: number,
|
|
428
|
+
bridge?: PlaytestBridgePayload
|
|
446
429
|
): Promise<PlaytestEndResult> {
|
|
447
430
|
this._assertConnected();
|
|
448
431
|
await this._ensureActionsAsync();
|
|
@@ -458,6 +441,7 @@ export class BridgeSession extends EventEmitter {
|
|
|
458
441
|
bridgePort,
|
|
459
442
|
instanceId: this._info.instanceId,
|
|
460
443
|
...(players !== undefined ? { players } : {}),
|
|
444
|
+
...(bridge ? { bridge } : {}),
|
|
461
445
|
},
|
|
462
446
|
},
|
|
463
447
|
timeoutMs
|
|
@@ -500,6 +484,37 @@ export class BridgeSession extends EventEmitter {
|
|
|
500
484
|
throw pluginError('endPlaytestResult', result);
|
|
501
485
|
}
|
|
502
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Read or change the players of the playtest this session belongs to.
|
|
489
|
+
* `timeoutMs` bounds the whole request, including any wait in the plugin.
|
|
490
|
+
*/
|
|
491
|
+
async playtestPlayersAsync(
|
|
492
|
+
request: PlaytestPlayersRequest,
|
|
493
|
+
timeoutMs: number
|
|
494
|
+
): Promise<PlaytestPlayersResult> {
|
|
495
|
+
this._assertConnected();
|
|
496
|
+
await this._ensureActionsAsync();
|
|
497
|
+
|
|
498
|
+
const result = await this._handle.sendActionAsync<PluginMessage>(
|
|
499
|
+
{
|
|
500
|
+
type: 'playtestPlayers',
|
|
501
|
+
sessionId: this._info.sessionId,
|
|
502
|
+
requestId: randomUUID(),
|
|
503
|
+
payload: request,
|
|
504
|
+
},
|
|
505
|
+
timeoutMs
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
if (result.type === 'playtestPlayersResult') {
|
|
509
|
+
return {
|
|
510
|
+
players: result.payload.players ?? [],
|
|
511
|
+
remaining: result.payload.remaining ?? [],
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
throw pluginError('playtestPlayersResult', result);
|
|
516
|
+
}
|
|
517
|
+
|
|
503
518
|
private _assertConnected(): void {
|
|
504
519
|
if (!this._handle.isConnected) {
|
|
505
520
|
throw new SessionDisconnectedError(this._info.sessionId);
|
|
@@ -609,3 +624,7 @@ export class BridgeSession extends EventEmitter {
|
|
|
609
624
|
OutputHelper.verbose('[actions] Action sync complete');
|
|
610
625
|
}
|
|
611
626
|
}
|
|
627
|
+
|
|
628
|
+
function _formatMiB(bytes: number): string {
|
|
629
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
|
|
630
|
+
}
|
|
@@ -51,6 +51,8 @@ export interface PluginSessionInfo {
|
|
|
51
51
|
launchId?: string;
|
|
52
52
|
/** Player name from register, for a multiplayer test client. */
|
|
53
53
|
playerName?: string;
|
|
54
|
+
/** Playtest id from register, for a playtest studio-bridge started. */
|
|
55
|
+
playtestId?: string;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
const SHUTDOWN_TIMEOUT_MS = 2_000;
|
|
@@ -427,6 +429,7 @@ export class BridgeHost extends EventEmitter {
|
|
|
427
429
|
placeFile: msg.payload.placeFile,
|
|
428
430
|
launchId: msg.payload.launchId,
|
|
429
431
|
playerName: msg.payload.playerName,
|
|
432
|
+
playtestId: msg.payload.playtestId,
|
|
430
433
|
});
|
|
431
434
|
};
|
|
432
435
|
|
|
@@ -255,6 +255,13 @@ const ServerMessageSchema = z.discriminatedUnion('type', [
|
|
|
255
255
|
bridgePort: z.number(),
|
|
256
256
|
instanceId: z.string(),
|
|
257
257
|
players: z.number().int().min(1).optional(),
|
|
258
|
+
bridge: z
|
|
259
|
+
.object({
|
|
260
|
+
entrySource: z.string(),
|
|
261
|
+
modules: z.record(z.string(), z.string()),
|
|
262
|
+
precompiled: z.array(z.string()),
|
|
263
|
+
})
|
|
264
|
+
.optional(),
|
|
258
265
|
})
|
|
259
266
|
.loose(),
|
|
260
267
|
}),
|
|
@@ -265,18 +272,24 @@ const ServerMessageSchema = z.discriminatedUnion('type', [
|
|
|
265
272
|
payload: z.object({}).loose(),
|
|
266
273
|
}),
|
|
267
274
|
z.object({
|
|
268
|
-
type: z.literal('
|
|
275
|
+
type: z.literal('playtestPlayers'),
|
|
269
276
|
sessionId: z.string(),
|
|
270
277
|
requestId: z.string(),
|
|
271
|
-
payload: z
|
|
272
|
-
.object({
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
278
|
+
payload: z.discriminatedUnion('op', [
|
|
279
|
+
z.object({ op: z.literal('list') }),
|
|
280
|
+
z.object({
|
|
281
|
+
op: z.literal('add'),
|
|
282
|
+
count: z.number().int().min(1),
|
|
276
283
|
timeoutSec: z.number(),
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
284
|
+
}),
|
|
285
|
+
z.object({
|
|
286
|
+
op: z.literal('remove'),
|
|
287
|
+
kick: z.array(z.string()),
|
|
288
|
+
names: z.array(z.string()),
|
|
289
|
+
timeoutSec: z.number(),
|
|
290
|
+
}),
|
|
291
|
+
z.object({ op: z.literal('leave') }),
|
|
292
|
+
]),
|
|
280
293
|
}),
|
|
281
294
|
z.object({
|
|
282
295
|
type: z.literal('error'),
|
package/src/bridge/types.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
OutputLevel,
|
|
14
14
|
SerializedReturnValue,
|
|
15
15
|
PlaytestMode,
|
|
16
|
+
RunAs,
|
|
16
17
|
} from '../server/web-socket-protocol.js';
|
|
17
18
|
|
|
18
19
|
// Re-export protocol types used in the public API
|
|
@@ -23,6 +24,7 @@ export type {
|
|
|
23
24
|
OutputLevel,
|
|
24
25
|
SerializedReturnValue,
|
|
25
26
|
PlaytestMode,
|
|
27
|
+
RunAs,
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
export type SessionContext = 'edit' | 'client' | 'server';
|
|
@@ -41,6 +43,8 @@ export interface SessionInfo {
|
|
|
41
43
|
launchId?: string;
|
|
42
44
|
/** The player of a multiplayer test client session. */
|
|
43
45
|
playerName?: string;
|
|
46
|
+
/** The playtest studio-bridge started this session's DataModel for. */
|
|
47
|
+
playtestId?: string;
|
|
44
48
|
context: SessionContext;
|
|
45
49
|
instanceId: string;
|
|
46
50
|
placeId: number;
|
|
@@ -67,6 +71,7 @@ export interface ExecResult {
|
|
|
67
71
|
* no return channel at all (a run that never reached a return) — distinct from `[]`, which means it returned nothing.
|
|
68
72
|
*/
|
|
69
73
|
returnValues?: SerializedReturnValue[];
|
|
74
|
+
ranAs?: RunAs;
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
export interface StateResult {
|
|
@@ -6,6 +6,7 @@ import { describe, it, expect, vi } from 'vitest';
|
|
|
6
6
|
import {
|
|
7
7
|
execCommand,
|
|
8
8
|
execHandlerAsync,
|
|
9
|
+
formatExecText,
|
|
9
10
|
runHandlerAsync,
|
|
10
11
|
type ExecResult,
|
|
11
12
|
} from './exec.js';
|
|
@@ -15,14 +16,25 @@ vi.mock('fs/promises', () => ({
|
|
|
15
16
|
readFile: vi.fn(),
|
|
16
17
|
}));
|
|
17
18
|
|
|
19
|
+
vi.mock('../../../roblox-mcp/index.js', () => ({
|
|
20
|
+
tryExecuteLuauAsync: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
18
23
|
import * as fs from 'fs/promises';
|
|
24
|
+
import { tryExecuteLuauAsync } from '../../../roblox-mcp/index.js';
|
|
19
25
|
|
|
20
26
|
function createMockSession(execResult: {
|
|
21
27
|
success: boolean;
|
|
22
28
|
output: Array<{ level: string; body: string }>;
|
|
23
29
|
error?: string;
|
|
30
|
+
ranAs?: string;
|
|
24
31
|
}) {
|
|
25
32
|
return {
|
|
33
|
+
info: {
|
|
34
|
+
sessionId: 'place1-aaaa-edit',
|
|
35
|
+
instanceId: 'place1-aaaa',
|
|
36
|
+
context: 'edit',
|
|
37
|
+
},
|
|
26
38
|
execAsync: vi.fn().mockResolvedValue(execResult),
|
|
27
39
|
} as any;
|
|
28
40
|
}
|
|
@@ -96,6 +108,88 @@ describe('execHandlerAsync', () => {
|
|
|
96
108
|
]);
|
|
97
109
|
});
|
|
98
110
|
|
|
111
|
+
it('forwards inputs to session.execAsync', async () => {
|
|
112
|
+
const session = createMockSession({
|
|
113
|
+
success: true,
|
|
114
|
+
output: [],
|
|
115
|
+
});
|
|
116
|
+
const input = Buffer.from('hello');
|
|
117
|
+
|
|
118
|
+
await execHandlerAsync(session, {
|
|
119
|
+
scriptContent: 'return ...',
|
|
120
|
+
args: ['Player1'],
|
|
121
|
+
inputs: [input],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(session.execAsync).toHaveBeenCalledWith(
|
|
125
|
+
'return ...',
|
|
126
|
+
undefined,
|
|
127
|
+
['Player1'],
|
|
128
|
+
[input]
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('forwards --run-as and reports where the code ran', async () => {
|
|
133
|
+
const session = createMockSession({
|
|
134
|
+
success: true,
|
|
135
|
+
output: [],
|
|
136
|
+
ranAs: 'game',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const result = await execHandlerAsync(session, {
|
|
140
|
+
scriptContent: 'return 1',
|
|
141
|
+
runAs: 'game',
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(session.execAsync).toHaveBeenCalledWith(
|
|
145
|
+
'return 1',
|
|
146
|
+
undefined,
|
|
147
|
+
undefined,
|
|
148
|
+
undefined,
|
|
149
|
+
'game'
|
|
150
|
+
);
|
|
151
|
+
expect(result.ranAs).toBe('game');
|
|
152
|
+
expect(formatExecText({ ...result, output: [] })).toContain(
|
|
153
|
+
'(ran as game)'
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('runs --run-as mcp through Studio MCP, targeting the session Studio', async () => {
|
|
158
|
+
const session = createMockSession({ success: true, output: [] });
|
|
159
|
+
vi.mocked(tryExecuteLuauAsync).mockResolvedValueOnce({
|
|
160
|
+
success: true,
|
|
161
|
+
value: '42',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const result = await execHandlerAsync(session, {
|
|
165
|
+
scriptContent: 'return 42',
|
|
166
|
+
runAs: 'mcp',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(session.execAsync).not.toHaveBeenCalled();
|
|
170
|
+
expect(tryExecuteLuauAsync).toHaveBeenCalledWith(
|
|
171
|
+
'return 42',
|
|
172
|
+
expect.objectContaining({ target: session.info.instanceId })
|
|
173
|
+
);
|
|
174
|
+
expect(result).toMatchObject({
|
|
175
|
+
success: true,
|
|
176
|
+
returnValues: ['42'],
|
|
177
|
+
ranAs: 'mcp',
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('refuses args with --run-as mcp, which only takes the code', async () => {
|
|
182
|
+
const session = createMockSession({ success: true, output: [] });
|
|
183
|
+
|
|
184
|
+
await expect(
|
|
185
|
+
execHandlerAsync(session, {
|
|
186
|
+
scriptContent: 'return ...',
|
|
187
|
+
args: ['a'],
|
|
188
|
+
runAs: 'mcp',
|
|
189
|
+
})
|
|
190
|
+
).rejects.toThrow("--run-as mcp can't pass args");
|
|
191
|
+
});
|
|
192
|
+
|
|
99
193
|
it('passes undefined timeout when not specified', async () => {
|
|
100
194
|
const session = createMockSession({
|
|
101
195
|
success: true,
|