@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
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { describe, it, expect } from 'vitest';
|
|
6
6
|
import type { SessionInfo } from '../../../bridge/index.js';
|
|
7
|
-
import { listSessionsHandlerAsync } from './list.js';
|
|
7
|
+
import { formatSessionsTable, listSessionsHandlerAsync } from './list.js';
|
|
8
|
+
import type { StudioMcpSnapshot } from '../../../roblox-mcp/index.js';
|
|
8
9
|
|
|
9
10
|
function createSessionInfo(overrides: Partial<SessionInfo> = {}): SessionInfo {
|
|
10
11
|
return {
|
|
@@ -81,3 +82,131 @@ describe('listSessionsHandlerAsync', () => {
|
|
|
81
82
|
expect(s.origin).toBe('managed');
|
|
82
83
|
});
|
|
83
84
|
});
|
|
85
|
+
|
|
86
|
+
describe('listSessionsHandlerAsync with Studio processes and MCP', () => {
|
|
87
|
+
const now = Date.parse('2026-11-05T00:30:00Z');
|
|
88
|
+
const snapshot = (
|
|
89
|
+
overrides: Partial<StudioMcpSnapshot> = {}
|
|
90
|
+
): StudioMcpSnapshot => ({
|
|
91
|
+
status: 'ok',
|
|
92
|
+
studios: [],
|
|
93
|
+
checkedAt: now,
|
|
94
|
+
latencyMs: 640,
|
|
95
|
+
...overrides,
|
|
96
|
+
});
|
|
97
|
+
const processes = [
|
|
98
|
+
{
|
|
99
|
+
pid: 70,
|
|
100
|
+
windowTitle: 'Place1 - Roblox Studio',
|
|
101
|
+
startedAt: now - 300_000,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
pid: 80,
|
|
105
|
+
windowTitle: 'Place1 - Roblox Studio',
|
|
106
|
+
startedAt: now - 290_000,
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
it('says no Studio is open, and how long it waited', async () => {
|
|
111
|
+
const result = await listSessionsHandlerAsync(
|
|
112
|
+
createMockConnection([]),
|
|
113
|
+
{},
|
|
114
|
+
{ listProcessesAsync: async () => [], elapsedSec: () => 12.34 }
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(result.summary).toBe('No Roblox Studio processes are open (12.3s).');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('says Studio is open without the plugin, rather than that nothing is', async () => {
|
|
121
|
+
const result = await listSessionsHandlerAsync(
|
|
122
|
+
createMockConnection([]),
|
|
123
|
+
{},
|
|
124
|
+
{ listProcessesAsync: async () => processes, elapsedSec: () => 5 }
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(result.summary).toContain(
|
|
128
|
+
'2 Studio process(es) open, but none has the studio-bridge plugin connected (5.0s)'
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('lists each Studio with its plugin session, MCP id and where it was launched', async () => {
|
|
133
|
+
const sessions = [
|
|
134
|
+
createSessionInfo({ sessionId: 'a-edit', instanceId: 'a' }),
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
const result = await listSessionsHandlerAsync(
|
|
138
|
+
createMockConnection(sessions),
|
|
139
|
+
{},
|
|
140
|
+
{
|
|
141
|
+
listProcessesAsync: async () => processes,
|
|
142
|
+
resolveSessionPidsAsync: async () => new Map([['a', 70]]),
|
|
143
|
+
readLaunchesAsync: async () => [
|
|
144
|
+
{
|
|
145
|
+
pid: 70,
|
|
146
|
+
cwd: 'C:gameshub',
|
|
147
|
+
opened: 'a new Baseplate',
|
|
148
|
+
launchedAt: now - 301_000,
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
mcpSnapshotAsync: async () =>
|
|
152
|
+
snapshot({
|
|
153
|
+
studios: [
|
|
154
|
+
{
|
|
155
|
+
studioId: 'aaaaaaaa-1',
|
|
156
|
+
placeName: 'Place1',
|
|
157
|
+
pid: 70,
|
|
158
|
+
instanceId: 'a',
|
|
159
|
+
},
|
|
160
|
+
{ studioId: 'bbbbbbbb-2', placeName: 'Place1', pid: 80 },
|
|
161
|
+
],
|
|
162
|
+
}),
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(result.sessions[0].pid).toBe(70);
|
|
167
|
+
expect(result.studios).toEqual([
|
|
168
|
+
expect.objectContaining({
|
|
169
|
+
pid: 70,
|
|
170
|
+
instanceId: 'a',
|
|
171
|
+
mcpStudioId: 'aaaaaaaa-1',
|
|
172
|
+
launch: expect.objectContaining({ cwd: 'C:gameshub' }),
|
|
173
|
+
}),
|
|
174
|
+
expect.objectContaining({ pid: 80, mcpStudioId: 'bbbbbbbb-2' }),
|
|
175
|
+
]);
|
|
176
|
+
const text = formatSessionsTable(result, now);
|
|
177
|
+
expect(text).toContain('a new Baseplate from C:gameshub');
|
|
178
|
+
expect(text).toContain('not by studio-bridge');
|
|
179
|
+
expect(text).toContain('5m');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('still lists sessions and Studios when MCP is unavailable, and says why', async () => {
|
|
183
|
+
const result = await listSessionsHandlerAsync(
|
|
184
|
+
createMockConnection([createSessionInfo({ sessionId: 's1' })]),
|
|
185
|
+
{},
|
|
186
|
+
{
|
|
187
|
+
listProcessesAsync: async () => processes,
|
|
188
|
+
mcpSnapshotAsync: async () =>
|
|
189
|
+
snapshot({ status: 'timeout', message: 'tools/call timed out' }),
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const text = formatSessionsTable(result, now);
|
|
194
|
+
expect(text).toContain('s1');
|
|
195
|
+
expect(text).toContain('MCP: timeout (tools/call timed out)');
|
|
196
|
+
expect(result.studios).toHaveLength(2);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('leaves MCP out when the check gave up or the platform has none', async () => {
|
|
200
|
+
for (const mcpSnapshotAsync of [
|
|
201
|
+
async () => undefined,
|
|
202
|
+
async () => snapshot({ status: 'unsupported' }),
|
|
203
|
+
]) {
|
|
204
|
+
const result = await listSessionsHandlerAsync(
|
|
205
|
+
createMockConnection([createSessionInfo({ sessionId: 's1' })]),
|
|
206
|
+
{},
|
|
207
|
+
{ mcpSnapshotAsync }
|
|
208
|
+
);
|
|
209
|
+
expect(formatSessionsTable(result, now)).not.toContain('MCP');
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `process list` — list connected Studio sessions
|
|
2
|
+
* `process list` — list connected Studio sessions and the Studio processes
|
|
3
|
+
* behind them.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import {
|
|
6
7
|
defineCommand,
|
|
7
8
|
type SessionTargeting,
|
|
8
9
|
} from '../../framework/define-command.js';
|
|
10
|
+
import { arg } from '../../framework/arg-builder.js';
|
|
9
11
|
import { OutputHelper } from '@quenty/cli-output-helpers';
|
|
12
|
+
import { formatDurationMs } from '@quenty/cli-output-helpers/cli-utils';
|
|
10
13
|
import {
|
|
11
14
|
formatTable,
|
|
12
15
|
type TableColumn,
|
|
@@ -14,39 +17,195 @@ import {
|
|
|
14
17
|
import type { BridgeConnection } from '../../../bridge/index.js';
|
|
15
18
|
import type { SessionInfo } from '../../../bridge/index.js';
|
|
16
19
|
import type { StudioState } from '../../../server/web-socket-protocol.js';
|
|
20
|
+
import {
|
|
21
|
+
getStudioMcpSnapshotAsync,
|
|
22
|
+
type StudioMcpSnapshot,
|
|
23
|
+
} from '../../../roblox-mcp/index.js';
|
|
24
|
+
import {
|
|
25
|
+
listStudioProcessesAsync,
|
|
26
|
+
type RunningStudioProcess,
|
|
27
|
+
} from '../../../process/studio-process-closer.js';
|
|
28
|
+
import { resolveSessionPidsAsync } from '../../../process/studio-process-identity.js';
|
|
29
|
+
import {
|
|
30
|
+
findStudioLaunch,
|
|
31
|
+
readStudioLaunchesAsync,
|
|
32
|
+
type StudioLaunchRecord,
|
|
33
|
+
} from '../../../process/studio-launch-registry.js';
|
|
17
34
|
import { colorizeState } from '../format-state.js';
|
|
18
35
|
|
|
36
|
+
export interface McpStudioState {
|
|
37
|
+
status: StudioMcpSnapshot['status'];
|
|
38
|
+
message?: string;
|
|
39
|
+
latencyMs: number;
|
|
40
|
+
checkedAt: string;
|
|
41
|
+
lastOkAt?: string;
|
|
42
|
+
settled?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface StudioProcessRow {
|
|
46
|
+
pid?: number;
|
|
47
|
+
startedAt?: string;
|
|
48
|
+
windowTitle: string;
|
|
49
|
+
instanceId?: string;
|
|
50
|
+
mcpStudioId?: string;
|
|
51
|
+
launch?: { cwd: string; opened: string; launchedAt: string };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ListedSession extends SessionInfo {
|
|
55
|
+
pid?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
19
58
|
export interface SessionsResult {
|
|
20
|
-
sessions:
|
|
59
|
+
sessions: ListedSession[];
|
|
60
|
+
studios?: StudioProcessRow[];
|
|
61
|
+
mcp?: McpStudioState;
|
|
21
62
|
summary: string;
|
|
22
63
|
}
|
|
23
64
|
|
|
65
|
+
export interface ListSessionsOptions {
|
|
66
|
+
listProcessesAsync?: () => Promise<RunningStudioProcess[]>;
|
|
67
|
+
resolveSessionPidsAsync?: (
|
|
68
|
+
processes: RunningStudioProcess[]
|
|
69
|
+
) => Promise<Map<string, number>>;
|
|
70
|
+
readLaunchesAsync?: () => Promise<StudioLaunchRecord[]>;
|
|
71
|
+
mcpSnapshotAsync?: (
|
|
72
|
+
sessions: SessionInfo[],
|
|
73
|
+
sessionPids: Map<string, number>,
|
|
74
|
+
processes: RunningStudioProcess[] | undefined
|
|
75
|
+
) => Promise<StudioMcpSnapshot | undefined>;
|
|
76
|
+
elapsedSec?: () => number;
|
|
77
|
+
}
|
|
78
|
+
|
|
24
79
|
/**
|
|
25
|
-
* List
|
|
80
|
+
* List the connected Studio sessions, and when the process list is available,
|
|
81
|
+
* every Studio process with its session, MCP id and where it was launched.
|
|
26
82
|
*/
|
|
27
83
|
export async function listSessionsHandlerAsync(
|
|
28
84
|
connection: BridgeConnection,
|
|
29
|
-
targeting: SessionTargeting = {}
|
|
85
|
+
targeting: SessionTargeting = {},
|
|
86
|
+
options: ListSessionsOptions = {}
|
|
30
87
|
): Promise<SessionsResult> {
|
|
31
|
-
const
|
|
88
|
+
const allSessions = connection.listSessions();
|
|
89
|
+
const processes = await options.listProcessesAsync?.().catch(() => undefined);
|
|
90
|
+
const sessionPids =
|
|
91
|
+
processes && options.resolveSessionPidsAsync
|
|
92
|
+
? await options.resolveSessionPidsAsync(processes).catch(() => new Map())
|
|
93
|
+
: new Map<string, number>();
|
|
94
|
+
const [launches, snapshot] = await Promise.all([
|
|
95
|
+
options.readLaunchesAsync?.().catch(() => []) ?? Promise.resolve([]),
|
|
96
|
+
options.mcpSnapshotAsync?.(allSessions, sessionPids, processes),
|
|
97
|
+
]);
|
|
32
98
|
|
|
33
|
-
|
|
99
|
+
const sessions: ListedSession[] = connection
|
|
100
|
+
.findSessions(targeting.target, targeting.context)
|
|
101
|
+
.map((s) => {
|
|
102
|
+
const pid = sessionPids.get(s.instanceId);
|
|
103
|
+
return pid !== undefined ? { ...s, pid } : s;
|
|
104
|
+
});
|
|
105
|
+
const studios = processes
|
|
106
|
+
? buildStudioRows(processes, sessionPids, launches, snapshot)
|
|
107
|
+
: undefined;
|
|
108
|
+
const mcp = snapshot ? toMcpStudioState(snapshot) : undefined;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
sessions,
|
|
112
|
+
...(studios ? { studios } : {}),
|
|
113
|
+
...(mcp ? { mcp } : {}),
|
|
114
|
+
summary: describeSummary(
|
|
115
|
+
sessions.length,
|
|
116
|
+
allSessions.length,
|
|
117
|
+
processes,
|
|
118
|
+
options.elapsedSec?.()
|
|
119
|
+
),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildStudioRows(
|
|
124
|
+
processes: RunningStudioProcess[],
|
|
125
|
+
sessionPids: Map<string, number>,
|
|
126
|
+
launches: StudioLaunchRecord[],
|
|
127
|
+
snapshot: StudioMcpSnapshot | undefined
|
|
128
|
+
): StudioProcessRow[] {
|
|
129
|
+
const instanceByPid = new Map([...sessionPids].map(([id, pid]) => [pid, id]));
|
|
130
|
+
const mcpStudios = snapshot?.status === 'ok' ? snapshot.studios : [];
|
|
131
|
+
const shown = new Set<string>();
|
|
132
|
+
|
|
133
|
+
const rows: StudioProcessRow[] = processes.map((proc) => {
|
|
134
|
+
const instanceId = instanceByPid.get(proc.pid);
|
|
135
|
+
const mcpStudio = mcpStudios.find(
|
|
136
|
+
(s) =>
|
|
137
|
+
s.pid === proc.pid ||
|
|
138
|
+
(instanceId !== undefined && s.instanceId === instanceId)
|
|
139
|
+
);
|
|
140
|
+
if (mcpStudio) shown.add(mcpStudio.studioId);
|
|
141
|
+
const launch = findStudioLaunch(launches, proc);
|
|
34
142
|
return {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
143
|
+
pid: proc.pid,
|
|
144
|
+
...(proc.startedAt !== undefined
|
|
145
|
+
? { startedAt: new Date(proc.startedAt).toISOString() }
|
|
146
|
+
: {}),
|
|
147
|
+
windowTitle: proc.windowTitle.replace(/ - Roblox Studio$/, ''),
|
|
148
|
+
...(instanceId ? { instanceId } : {}),
|
|
149
|
+
...(mcpStudio ? { mcpStudioId: mcpStudio.studioId } : {}),
|
|
150
|
+
...(launch
|
|
151
|
+
? {
|
|
152
|
+
launch: {
|
|
153
|
+
cwd: launch.cwd,
|
|
154
|
+
opened: launch.opened,
|
|
155
|
+
launchedAt: new Date(launch.launchedAt).toISOString(),
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
: {}),
|
|
40
159
|
};
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
for (const studio of mcpStudios) {
|
|
163
|
+
if (shown.has(studio.studioId)) continue;
|
|
164
|
+
rows.push({
|
|
165
|
+
windowTitle: studio.placeName || '(loading)',
|
|
166
|
+
...(studio.instanceId ? { instanceId: studio.instanceId } : {}),
|
|
167
|
+
mcpStudioId: studio.studioId,
|
|
168
|
+
});
|
|
41
169
|
}
|
|
170
|
+
return rows;
|
|
171
|
+
}
|
|
42
172
|
|
|
173
|
+
function describeSummary(
|
|
174
|
+
matching: number,
|
|
175
|
+
connected: number,
|
|
176
|
+
processes: RunningStudioProcess[] | undefined,
|
|
177
|
+
elapsedSec: number | undefined
|
|
178
|
+
): string {
|
|
179
|
+
if (matching > 0) {
|
|
180
|
+
return `${matching} session(s) connected.`;
|
|
181
|
+
}
|
|
182
|
+
if (connected > 0) {
|
|
183
|
+
return 'No sessions match --target/--context.';
|
|
184
|
+
}
|
|
185
|
+
const waited = elapsedSec !== undefined ? ` (${elapsedSec.toFixed(1)}s)` : '';
|
|
186
|
+
if (!processes) {
|
|
187
|
+
return `No active sessions${waited}. Is Studio running with the studio-bridge plugin?`;
|
|
188
|
+
}
|
|
189
|
+
if (processes.length === 0) {
|
|
190
|
+
return `No Roblox Studio processes are open${waited}.`;
|
|
191
|
+
}
|
|
192
|
+
return `${processes.length} Studio process(es) open, but none has the studio-bridge plugin connected${waited}. Run 'studio-bridge plugin install', and check the plugin is enabled in Studio.`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function toMcpStudioState(snapshot: StudioMcpSnapshot): McpStudioState {
|
|
43
196
|
return {
|
|
44
|
-
|
|
45
|
-
|
|
197
|
+
status: snapshot.status,
|
|
198
|
+
...(snapshot.message ? { message: snapshot.message } : {}),
|
|
199
|
+
latencyMs: snapshot.latencyMs,
|
|
200
|
+
checkedAt: new Date(snapshot.checkedAt).toISOString(),
|
|
201
|
+
...(snapshot.lastOkAt
|
|
202
|
+
? { lastOkAt: new Date(snapshot.lastOkAt).toISOString() }
|
|
203
|
+
: {}),
|
|
204
|
+
...(snapshot.settled === false ? { settled: false } : {}),
|
|
46
205
|
};
|
|
47
206
|
}
|
|
48
207
|
|
|
49
|
-
const sessionColumns: TableColumn<
|
|
208
|
+
const sessionColumns: TableColumn<ListedSession>[] = [
|
|
50
209
|
{
|
|
51
210
|
header: 'Session',
|
|
52
211
|
value: (s) => s.sessionId,
|
|
@@ -59,25 +218,134 @@ const sessionColumns: TableColumn<SessionInfo>[] = [
|
|
|
59
218
|
value: (s) => s.state,
|
|
60
219
|
format: (v) => colorizeState(v as StudioState),
|
|
61
220
|
},
|
|
221
|
+
{ header: 'PID', value: (s) => (s.pid !== undefined ? String(s.pid) : '') },
|
|
62
222
|
];
|
|
63
223
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
224
|
+
function studioColumns(
|
|
225
|
+
withMcp: boolean,
|
|
226
|
+
now: number
|
|
227
|
+
): TableColumn<StudioProcessRow>[] {
|
|
228
|
+
const columns: TableColumn<StudioProcessRow>[] = [
|
|
229
|
+
{
|
|
230
|
+
header: 'PID',
|
|
231
|
+
value: (s) => (s.pid !== undefined ? String(s.pid) : '?'),
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
header: 'Up',
|
|
235
|
+
value: (s) =>
|
|
236
|
+
s.startedAt
|
|
237
|
+
? formatDurationMs(Math.max(0, now - Date.parse(s.startedAt)))
|
|
238
|
+
: '',
|
|
239
|
+
},
|
|
240
|
+
{ header: 'Window', value: (s) => s.windowTitle },
|
|
241
|
+
{ header: 'Plugin', value: (s) => s.instanceId ?? 'none' },
|
|
242
|
+
];
|
|
243
|
+
if (withMcp) {
|
|
244
|
+
columns.push({
|
|
245
|
+
header: 'MCP',
|
|
246
|
+
value: (s) => (s.mcpStudioId ? s.mcpStudioId.slice(0, 8) : 'none'),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
columns.push({
|
|
250
|
+
header: 'Launched',
|
|
251
|
+
value: (s) =>
|
|
252
|
+
s.launch
|
|
253
|
+
? `${s.launch.opened} from ${s.launch.cwd}`
|
|
254
|
+
: 'not by studio-bridge',
|
|
255
|
+
});
|
|
256
|
+
return columns;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function formatMcpLine(mcp: McpStudioState, now = Date.now()): string {
|
|
260
|
+
const age = formatAgo(Date.parse(mcp.checkedAt), now);
|
|
261
|
+
if (mcp.status === 'ok') {
|
|
262
|
+
const pending =
|
|
263
|
+
mcp.settled === false
|
|
264
|
+
? ', may be incomplete (Studios take a few seconds to reach a new MCP server)'
|
|
265
|
+
: '';
|
|
266
|
+
return `MCP: ok, answered in ${mcp.latencyMs}ms, checked ${age}${pending}`;
|
|
267
|
+
}
|
|
268
|
+
const lastOk = mcp.lastOkAt
|
|
269
|
+
? `, last ok ${formatAgo(Date.parse(mcp.lastOkAt), now)}`
|
|
270
|
+
: '';
|
|
271
|
+
return OutputHelper.formatWarning(
|
|
272
|
+
`MCP: ${mcp.status}${
|
|
273
|
+
mcp.message ? ` (${mcp.message})` : ''
|
|
274
|
+
}, checked ${age}${lastOk}`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function formatSessionsTable(
|
|
279
|
+
result: SessionsResult,
|
|
280
|
+
now = Date.now()
|
|
281
|
+
): string {
|
|
282
|
+
const mcp =
|
|
283
|
+
result.mcp && result.mcp.status !== 'unsupported' ? result.mcp : undefined;
|
|
284
|
+
const lines: string[] = [];
|
|
285
|
+
if (result.sessions.length > 0) {
|
|
286
|
+
lines.push(formatTable(result.sessions, sessionColumns));
|
|
287
|
+
} else {
|
|
288
|
+
lines.push(result.summary);
|
|
289
|
+
}
|
|
290
|
+
if (result.studios && result.studios.length > 0) {
|
|
291
|
+
lines.push('');
|
|
292
|
+
lines.push(
|
|
293
|
+
formatTable(result.studios, studioColumns(mcp?.status === 'ok', now))
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (mcp) {
|
|
297
|
+
lines.push(formatMcpLine(mcp, now));
|
|
298
|
+
}
|
|
299
|
+
return lines.join('\n');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function formatAgo(at: number, now: number): string {
|
|
303
|
+
return `${formatDurationMs(Math.max(0, now - at))} ago`;
|
|
67
304
|
}
|
|
68
305
|
|
|
69
|
-
|
|
306
|
+
interface ListArgs {
|
|
307
|
+
mcp?: boolean;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export const listCommand = defineCommand<ListArgs, SessionsResult>({
|
|
70
311
|
group: 'process',
|
|
71
312
|
name: 'list',
|
|
72
|
-
description:
|
|
313
|
+
description:
|
|
314
|
+
'List connected Studio sessions and every Studio process, with where each was launched',
|
|
73
315
|
category: 'infrastructure',
|
|
74
316
|
safety: 'read',
|
|
75
317
|
scope: 'connection',
|
|
76
318
|
coversAllSessions: true,
|
|
77
|
-
args: {
|
|
319
|
+
args: {
|
|
320
|
+
mcp: arg.flag({
|
|
321
|
+
description:
|
|
322
|
+
"Ask Studio's MCP server too, which also reaches Studios without the plugin (--no-mcp to skip)",
|
|
323
|
+
default: true,
|
|
324
|
+
}),
|
|
325
|
+
},
|
|
78
326
|
cli: {
|
|
79
327
|
format: formatSessionsTable,
|
|
80
328
|
},
|
|
81
|
-
handler: async (connection,
|
|
82
|
-
listSessionsHandlerAsync(
|
|
329
|
+
handler: async (connection, args, targeting) =>
|
|
330
|
+
listSessionsHandlerAsync(
|
|
331
|
+
connection,
|
|
332
|
+
targeting,
|
|
333
|
+
connection.isRemote
|
|
334
|
+
? { elapsedSec: () => process.uptime() }
|
|
335
|
+
: {
|
|
336
|
+
listProcessesAsync: listStudioProcessesAsync,
|
|
337
|
+
resolveSessionPidsAsync: (processes) =>
|
|
338
|
+
resolveSessionPidsAsync(connection, processes),
|
|
339
|
+
readLaunchesAsync: () => readStudioLaunchesAsync(),
|
|
340
|
+
mcpSnapshotAsync: args.mcp
|
|
341
|
+
? (sessions, sessionPids, processes) =>
|
|
342
|
+
getStudioMcpSnapshotAsync({
|
|
343
|
+
sessions,
|
|
344
|
+
sessionPids,
|
|
345
|
+
windows: processes,
|
|
346
|
+
})
|
|
347
|
+
: undefined,
|
|
348
|
+
elapsedSec: () => process.uptime(),
|
|
349
|
+
}
|
|
350
|
+
),
|
|
83
351
|
});
|
|
@@ -41,7 +41,7 @@ interface ProcessRunArgs {
|
|
|
41
41
|
code?: string;
|
|
42
42
|
file?: string;
|
|
43
43
|
place?: string;
|
|
44
|
-
|
|
44
|
+
scriptTimeout?: number;
|
|
45
45
|
scriptArgs?: string[];
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -85,8 +85,8 @@ export const processRunCommand = defineCommand<
|
|
|
85
85
|
description: 'Path to a .rbxl place file',
|
|
86
86
|
alias: 'p',
|
|
87
87
|
}),
|
|
88
|
-
|
|
89
|
-
description: '
|
|
88
|
+
scriptTimeout: arg.option({
|
|
89
|
+
description: 'Cancel the script after this many milliseconds',
|
|
90
90
|
type: 'number',
|
|
91
91
|
}),
|
|
92
92
|
scriptArgs: arg.passthrough({
|
|
@@ -107,7 +107,7 @@ export const processRunCommand = defineCommand<
|
|
|
107
107
|
scriptContent,
|
|
108
108
|
packageName: 'studio-bridge',
|
|
109
109
|
placePath: args.place,
|
|
110
|
-
timeoutMs: args.
|
|
110
|
+
timeoutMs: args.scriptTimeout ?? 120_000,
|
|
111
111
|
verbose: OutputHelper.isVerbose(),
|
|
112
112
|
showLogs: true,
|
|
113
113
|
filePath,
|
|
@@ -94,7 +94,7 @@ describe('buildDockerRunArgsAsync', () => {
|
|
|
94
94
|
const bashCmd = args[args.length - 1];
|
|
95
95
|
expect(bashCmd).toContain('studio-bridge linux inject-credentials');
|
|
96
96
|
expect(bashCmd).toContain('studio-bridge process run');
|
|
97
|
-
expect(bashCmd).toContain('--timeout 120000');
|
|
97
|
+
expect(bashCmd).toContain('--script-timeout 120000');
|
|
98
98
|
expect(bashCmd).not.toContain('--verbose');
|
|
99
99
|
});
|
|
100
100
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
buildPlaytestBridgeAsync,
|
|
4
|
+
wrapPrecompiledSource,
|
|
5
|
+
} from './playtest-bridge.js';
|
|
6
|
+
|
|
7
|
+
describe('wrapPrecompiledSource', () => {
|
|
8
|
+
it('opens the function on the first line', () => {
|
|
9
|
+
const wrapped = wrapPrecompiledSource('return 1');
|
|
10
|
+
expect(wrapped.split('\n')[0]).toBe('return { fn = function(...) return 1');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('uses a long string level the source does not close', () => {
|
|
14
|
+
expect(wrapPrecompiledSource('return "]]"')).toContain('[=[x');
|
|
15
|
+
expect(wrapPrecompiledSource('return "]]" .. "]=]"')).toContain('[==[x');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('does not let a trailing ] close the string early', () => {
|
|
19
|
+
const wrapped = wrapPrecompiledSource('local t = {}\nreturn t[1]');
|
|
20
|
+
expect(wrapped).toContain('[=[x');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('keeps a leading newline in the source text', () => {
|
|
24
|
+
expect(wrapPrecompiledSource('\nreturn 1')).toContain('[[x\nreturn 1]]');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('buildPlaytestBridgeAsync', () => {
|
|
29
|
+
it('plants the Shared modules with the plugin version filled in', async () => {
|
|
30
|
+
const bridge = await buildPlaytestBridgeAsync();
|
|
31
|
+
|
|
32
|
+
expect(Object.keys(bridge.modules)).toContain('StudioBridgeClient');
|
|
33
|
+
expect(Object.keys(bridge.modules)).toContain('PrecompiledSources');
|
|
34
|
+
expect(Object.keys(bridge.modules).some((n) => n.endsWith('.spec'))).toBe(
|
|
35
|
+
false
|
|
36
|
+
);
|
|
37
|
+
expect(bridge.modules.StudioBridgeSessionUtils).not.toContain(
|
|
38
|
+
'{{PLUGIN_VERSION}}'
|
|
39
|
+
);
|
|
40
|
+
expect(bridge.entrySource).toContain('StudioBridgeClient.start');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('precompiles every action plus the given scripts', async () => {
|
|
44
|
+
const bridge = await buildPlaytestBridgeAsync(['return "mine"']);
|
|
45
|
+
|
|
46
|
+
expect(bridge.precompiled.some((s) => s.includes('return "mine"'))).toBe(
|
|
47
|
+
true
|
|
48
|
+
);
|
|
49
|
+
expect(bridge.precompiled.some((s) => s.includes('ExecuteAction'))).toBe(
|
|
50
|
+
true
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
});
|