openclaw-cascade-plugin 1.0.12 → 1.0.14
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/dist/grpc-client.d.ts +17 -0
- package/dist/grpc-client.d.ts.map +1 -0
- package/dist/grpc-client.js +154 -0
- package/dist/grpc-client.js.map +1 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -54
- package/dist/index.js.map +1 -1
- package/dist/test-utils/mocks.d.ts +6 -4
- package/dist/test-utils/mocks.d.ts.map +1 -1
- package/dist/test-utils/mocks.js +24 -14
- package/dist/test-utils/mocks.js.map +1 -1
- package/dist/tools/desktop-automation.d.ts +2 -7
- package/dist/tools/desktop-automation.d.ts.map +1 -1
- package/dist/tools/desktop-automation.js +64 -123
- package/dist/tools/desktop-automation.js.map +1 -1
- package/dist/tools/index.d.ts +3 -10
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -17
- package/dist/tools/index.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +13 -2
- package/proto/cascade.proto +297 -0
- package/PHASE1_SUMMARY.md +0 -191
- package/PHASE3_SUMMARY.md +0 -195
- package/dist/cascade-client.d.ts +0 -53
- package/dist/cascade-client.d.ts.map +0 -1
- package/dist/cascade-client.js +0 -179
- package/dist/cascade-client.js.map +0 -1
- package/dist/python-manager.d.ts +0 -59
- package/dist/python-manager.d.ts.map +0 -1
- package/dist/python-manager.js +0 -190
- package/dist/python-manager.js.map +0 -1
- package/dist/tools/api-tools.d.ts +0 -9
- package/dist/tools/api-tools.d.ts.map +0 -1
- package/dist/tools/api-tools.js +0 -102
- package/dist/tools/api-tools.js.map +0 -1
- package/dist/tools/sandbox-tools.d.ts +0 -9
- package/dist/tools/sandbox-tools.d.ts.map +0 -1
- package/dist/tools/sandbox-tools.js +0 -79
- package/dist/tools/sandbox-tools.js.map +0 -1
- package/dist/tools/web-automation.d.ts +0 -9
- package/dist/tools/web-automation.d.ts.map +0 -1
- package/dist/tools/web-automation.js +0 -471
- package/dist/tools/web-automation.js.map +0 -1
- package/jest.setup.js +0 -19
- package/openclaw-cascade-plugin-1.0.0.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.10.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.11.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.12.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.4.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.6.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.7.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.8.tgz +0 -0
- package/openclaw-cascade-plugin-1.0.9.tgz +0 -0
- package/scripts/postinstall.js +0 -84
- package/src/a2a-client.ts +0 -66
- package/src/cascade-client.test.ts +0 -400
- package/src/cascade-client.ts +0 -198
- package/src/config.test.ts +0 -189
- package/src/config.ts +0 -137
- package/src/index.ts +0 -202
- package/src/python-manager.test.ts +0 -187
- package/src/python-manager.ts +0 -230
- package/src/test-utils/helpers.ts +0 -107
- package/src/test-utils/index.ts +0 -2
- package/src/test-utils/mocks.ts +0 -101
- package/src/tools/a2a-tools.ts +0 -162
- package/src/tools/api-tools.ts +0 -110
- package/src/tools/desktop-automation.test.ts +0 -308
- package/src/tools/desktop-automation.ts +0 -366
- package/src/tools/index.ts +0 -13
- package/src/tools/response-helpers.ts +0 -78
- package/src/tools/sandbox-tools.ts +0 -83
- package/src/tools/tool-registry.ts +0 -51
- package/src/tools/web-automation.test.ts +0 -177
- package/src/tools/web-automation.ts +0 -518
- package/src/types/index.ts +0 -133
- package/src/wsl.ts +0 -53
- package/tsconfig.json +0 -27
package/scripts/postinstall.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
|
|
5
|
-
const PLUGIN_ID = 'openclaw-cascade-plugin';
|
|
6
|
-
const DEFAULT_ENDPOINT = 'localhost:50051';
|
|
7
|
-
const defaultConfigPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
8
|
-
const legacyConfigPath = path.join(os.homedir(), '.openclaw', 'config.json');
|
|
9
|
-
const configPath = process.env.OPENCLAW_CONFIG_PATH ||
|
|
10
|
-
(fs.existsSync(defaultConfigPath) ? defaultConfigPath : legacyConfigPath);
|
|
11
|
-
|
|
12
|
-
function readConfig(filePath) {
|
|
13
|
-
if (!fs.existsSync(filePath)) {
|
|
14
|
-
return { plugins: { entries: {} } };
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
18
|
-
try {
|
|
19
|
-
return JSON.parse(content);
|
|
20
|
-
} catch (error) {
|
|
21
|
-
console.warn('[cascade] Could not parse OpenClaw config; skipping auto-update:', error.message);
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function writeConfig(filePath, config) {
|
|
27
|
-
const dir = path.dirname(filePath);
|
|
28
|
-
if (!fs.existsSync(dir)) {
|
|
29
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function ensurePluginEntry(config) {
|
|
36
|
-
config.plugins = config.plugins || {};
|
|
37
|
-
config.plugins.entries = config.plugins.entries || {};
|
|
38
|
-
|
|
39
|
-
// Migrate legacy entry if present
|
|
40
|
-
if (config.plugins.entries.cascade && !config.plugins.entries[PLUGIN_ID]) {
|
|
41
|
-
config.plugins.entries[PLUGIN_ID] = config.plugins.entries.cascade;
|
|
42
|
-
delete config.plugins.entries.cascade;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const entry = config.plugins.entries[PLUGIN_ID] || {};
|
|
46
|
-
const existingConfig = entry.config || {};
|
|
47
|
-
|
|
48
|
-
const cascadeRepoPath = process.env.CASCADE_REPO_PATH;
|
|
49
|
-
const cascadePythonModulePath = process.env.CASCADE_PYTHON_MODULE_PATH ||
|
|
50
|
-
(cascadeRepoPath ? path.join(cascadeRepoPath, 'python') : undefined);
|
|
51
|
-
|
|
52
|
-
config.plugins.entries[PLUGIN_ID] = {
|
|
53
|
-
enabled: entry.enabled !== false,
|
|
54
|
-
config: {
|
|
55
|
-
cascadeGrpcEndpoint: existingConfig.cascadeGrpcEndpoint || DEFAULT_ENDPOINT,
|
|
56
|
-
cascadePythonModulePath: existingConfig.cascadePythonModulePath || cascadePythonModulePath,
|
|
57
|
-
cascadePythonPath: existingConfig.cascadePythonPath,
|
|
58
|
-
firestoreProjectId: existingConfig.firestoreProjectId,
|
|
59
|
-
firestoreCredentialsPath: existingConfig.firestoreCredentialsPath,
|
|
60
|
-
headless: existingConfig.headless,
|
|
61
|
-
actionTimeoutMs: existingConfig.actionTimeoutMs,
|
|
62
|
-
enableA2A: existingConfig.enableA2A,
|
|
63
|
-
allowedAgents: existingConfig.allowedAgents,
|
|
64
|
-
requireAgentConfirmation: existingConfig.requireAgentConfirmation,
|
|
65
|
-
verbose: existingConfig.verbose,
|
|
66
|
-
screenshotMode: existingConfig.screenshotMode,
|
|
67
|
-
screenshotDir: existingConfig.screenshotDir
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const config = readConfig(configPath);
|
|
73
|
-
if (!config) {
|
|
74
|
-
process.exit(0);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
ensurePluginEntry(config);
|
|
78
|
-
writeConfig(configPath, config);
|
|
79
|
-
|
|
80
|
-
console.log(`[cascade] OpenClaw config updated: ${configPath}`);
|
|
81
|
-
console.log('[cascade] Plugin entry ensured under plugins.entries.openclaw-cascade-plugin');
|
|
82
|
-
if (!process.env.CASCADE_REPO_PATH && !process.env.CASCADE_PYTHON_MODULE_PATH) {
|
|
83
|
-
console.log('[cascade] Tip: set CASCADE_REPO_PATH to your repo to auto-configure PYTHONPATH.');
|
|
84
|
-
}
|
package/src/a2a-client.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A2A Client - Agent-to-Agent communication
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { A2AMessage } from './types';
|
|
6
|
-
|
|
7
|
-
export class CascadeA2AClient {
|
|
8
|
-
private agentId: string | null = null;
|
|
9
|
-
private messageHandlers = new Map<string, (payload: any) => Promise<void>>();
|
|
10
|
-
private grpcEndpoint: string;
|
|
11
|
-
private userId: string;
|
|
12
|
-
private appId: string;
|
|
13
|
-
private authToken: string;
|
|
14
|
-
|
|
15
|
-
constructor(
|
|
16
|
-
grpcEndpoint: string,
|
|
17
|
-
userId: string,
|
|
18
|
-
appId: string,
|
|
19
|
-
authToken: string
|
|
20
|
-
) {
|
|
21
|
-
this.grpcEndpoint = grpcEndpoint;
|
|
22
|
-
this.userId = userId;
|
|
23
|
-
this.appId = appId;
|
|
24
|
-
this.authToken = authToken;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async initialize(): Promise<void> {
|
|
28
|
-
// Initialize gRPC connection and register agent
|
|
29
|
-
// This is a placeholder - actual implementation would use gRPC
|
|
30
|
-
this.agentId = `openclaw-${this.userId}-${Date.now()}`;
|
|
31
|
-
console.debug(`A2A Client connecting to ${this.grpcEndpoint} for app ${this.appId}`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async sendToAgent(
|
|
35
|
-
targetRole: 'explorer' | 'worker' | 'orchestrator',
|
|
36
|
-
payload: any,
|
|
37
|
-
targetAgentId?: string
|
|
38
|
-
): Promise<void> {
|
|
39
|
-
if (!this.agentId) {
|
|
40
|
-
throw new Error('A2A client not initialized');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const message: A2AMessage = {
|
|
44
|
-
type: payload.type,
|
|
45
|
-
source: 'openclaw',
|
|
46
|
-
timestamp: Date.now(),
|
|
47
|
-
payload,
|
|
48
|
-
runId: payload.runId
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// Send via gRPC (placeholder)
|
|
52
|
-
const target = targetAgentId || targetRole;
|
|
53
|
-
console.log(`Sending message to ${target}:`, message);
|
|
54
|
-
|
|
55
|
-
// In real implementation, would use this.grpcEndpoint and this.authToken
|
|
56
|
-
console.debug(`Using endpoint: ${this.grpcEndpoint}, token: ${this.authToken ? '***' : 'none'}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
onMessage(type: string, handler: (payload: any) => Promise<void>): void {
|
|
60
|
-
this.messageHandlers.set(type, handler);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
isConnected(): boolean {
|
|
64
|
-
return this.agentId !== null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for CascadeMcpClient
|
|
3
|
-
*
|
|
4
|
-
* MCP (Model Context Protocol) client for communicating with Cascade
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { CascadeMcpClient } from './cascade-client';
|
|
8
|
-
import { createMockConfig } from './test-utils';
|
|
9
|
-
import { spawn } from 'child_process';
|
|
10
|
-
import { EventEmitter } from 'events';
|
|
11
|
-
|
|
12
|
-
// Mock child_process
|
|
13
|
-
jest.mock('child_process');
|
|
14
|
-
|
|
15
|
-
describe('CascadeMcpClient', () => {
|
|
16
|
-
let client: CascadeMcpClient;
|
|
17
|
-
let mockProcess: any;
|
|
18
|
-
let mockSpawn: jest.MockedFunction<typeof spawn>;
|
|
19
|
-
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
jest.clearAllMocks();
|
|
22
|
-
|
|
23
|
-
// Create mock process
|
|
24
|
-
const stdout = new EventEmitter();
|
|
25
|
-
const stderr = new EventEmitter();
|
|
26
|
-
const stdin = { write: jest.fn() };
|
|
27
|
-
|
|
28
|
-
mockProcess = {
|
|
29
|
-
stdout,
|
|
30
|
-
stderr,
|
|
31
|
-
stdin,
|
|
32
|
-
kill: jest.fn(),
|
|
33
|
-
on: jest.fn(),
|
|
34
|
-
pid: 12345,
|
|
35
|
-
killed: false
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
|
|
39
|
-
mockSpawn.mockReturnValue(mockProcess as any);
|
|
40
|
-
|
|
41
|
-
const config = createMockConfig({
|
|
42
|
-
cascadePythonPath: '/usr/bin/python3',
|
|
43
|
-
cascadeGrpcEndpoint: 'localhost:50051'
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
client = new CascadeMcpClient(
|
|
47
|
-
config.cascadePythonPath!,
|
|
48
|
-
{
|
|
49
|
-
CASCADE_GRPC_ENDPOINT: config.cascadeGrpcEndpoint,
|
|
50
|
-
CASCADE_APP_ID: 'test-app',
|
|
51
|
-
CASCADE_USER_ID: 'test-user'
|
|
52
|
-
}
|
|
53
|
-
);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe('start', () => {
|
|
57
|
-
test('should spawn Python MCP server', async () => {
|
|
58
|
-
// Arrange
|
|
59
|
-
const startPromise = client.start();
|
|
60
|
-
|
|
61
|
-
// Emit initialize response
|
|
62
|
-
setTimeout(() => {
|
|
63
|
-
mockProcess.stdout.emit('data', JSON.stringify({
|
|
64
|
-
jsonrpc: '2.0',
|
|
65
|
-
id: 1,
|
|
66
|
-
result: {
|
|
67
|
-
protocolVersion: '2024-11-05',
|
|
68
|
-
capabilities: {},
|
|
69
|
-
serverInfo: { name: 'cascade-mcp', version: '1.0.0' }
|
|
70
|
-
}
|
|
71
|
-
}) + '\n');
|
|
72
|
-
}, 10);
|
|
73
|
-
|
|
74
|
-
// Act
|
|
75
|
-
await startPromise;
|
|
76
|
-
|
|
77
|
-
// Assert
|
|
78
|
-
expect(mockSpawn).toHaveBeenCalledWith(
|
|
79
|
-
'/usr/bin/python3',
|
|
80
|
-
['-m', 'mcp_server.cli'],
|
|
81
|
-
expect.objectContaining({
|
|
82
|
-
env: expect.objectContaining({
|
|
83
|
-
CASCADE_GRPC_ENDPOINT: 'localhost:50051',
|
|
84
|
-
CASCADE_APP_ID: 'test-app',
|
|
85
|
-
CASCADE_USER_ID: 'test-user'
|
|
86
|
-
}),
|
|
87
|
-
stdio: ['pipe', 'pipe', 'pipe']
|
|
88
|
-
})
|
|
89
|
-
);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('should send initialize request', async () => {
|
|
93
|
-
// Arrange
|
|
94
|
-
const startPromise = client.start();
|
|
95
|
-
|
|
96
|
-
// Emit initialize response
|
|
97
|
-
setTimeout(() => {
|
|
98
|
-
mockProcess.stdout.emit('data', JSON.stringify({
|
|
99
|
-
jsonrpc: '2.0',
|
|
100
|
-
id: 1,
|
|
101
|
-
result: {
|
|
102
|
-
protocolVersion: '2024-11-05',
|
|
103
|
-
capabilities: {},
|
|
104
|
-
serverInfo: { name: 'cascade-mcp', version: '1.0.0' }
|
|
105
|
-
}
|
|
106
|
-
}) + '\n');
|
|
107
|
-
}, 10);
|
|
108
|
-
|
|
109
|
-
// Act
|
|
110
|
-
await startPromise;
|
|
111
|
-
|
|
112
|
-
// Assert
|
|
113
|
-
const writeCalls = mockProcess.stdin.write.mock.calls;
|
|
114
|
-
const initRequest = JSON.parse(writeCalls[0][0]);
|
|
115
|
-
|
|
116
|
-
expect(initRequest).toMatchObject({
|
|
117
|
-
jsonrpc: '2.0',
|
|
118
|
-
id: 1,
|
|
119
|
-
method: 'initialize',
|
|
120
|
-
params: {
|
|
121
|
-
protocolVersion: '2024-11-05',
|
|
122
|
-
clientInfo: { name: 'openclaw-cascade', version: '1.0.0' }
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test('should throw when spawn fails', async () => {
|
|
128
|
-
// Arrange
|
|
129
|
-
mockSpawn.mockImplementation(() => {
|
|
130
|
-
throw new Error('Spawn failed');
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// Act & Assert
|
|
134
|
-
await expect(client.start()).rejects.toThrow('Spawn failed');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test('should timeout if no response', async () => {
|
|
138
|
-
// Arrange - don't emit any response
|
|
139
|
-
|
|
140
|
-
// Act & Assert
|
|
141
|
-
await expect(client.start()).rejects.toThrow('timed out');
|
|
142
|
-
}, 35000); // 35 second timeout for this test
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe('callTool', () => {
|
|
146
|
-
beforeEach(async () => {
|
|
147
|
-
// Start client first
|
|
148
|
-
const startPromise = client.start();
|
|
149
|
-
|
|
150
|
-
setTimeout(() => {
|
|
151
|
-
mockProcess.stdout.emit('data', JSON.stringify({
|
|
152
|
-
jsonrpc: '2.0',
|
|
153
|
-
id: 1,
|
|
154
|
-
result: {
|
|
155
|
-
protocolVersion: '2024-11-05',
|
|
156
|
-
capabilities: {},
|
|
157
|
-
serverInfo: { name: 'cascade-mcp', version: '1.0.0' }
|
|
158
|
-
}
|
|
159
|
-
}) + '\n');
|
|
160
|
-
}, 10);
|
|
161
|
-
|
|
162
|
-
await startPromise;
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('should send tool/call request', async () => {
|
|
166
|
-
// Arrange
|
|
167
|
-
const callPromise = client.callTool('click_element', { id: 'button1' });
|
|
168
|
-
|
|
169
|
-
setTimeout(() => {
|
|
170
|
-
mockProcess.stdout.emit('data', JSON.stringify({
|
|
171
|
-
jsonrpc: '2.0',
|
|
172
|
-
id: 2,
|
|
173
|
-
result: { success: true }
|
|
174
|
-
}) + '\n');
|
|
175
|
-
}, 10);
|
|
176
|
-
|
|
177
|
-
// Act
|
|
178
|
-
await callPromise;
|
|
179
|
-
|
|
180
|
-
// Assert
|
|
181
|
-
const writeCalls = mockProcess.stdin.write.mock.calls;
|
|
182
|
-
const lastCall = writeCalls[writeCalls.length - 1][0];
|
|
183
|
-
const request = JSON.parse(lastCall);
|
|
184
|
-
|
|
185
|
-
expect(request).toMatchObject({
|
|
186
|
-
jsonrpc: '2.0',
|
|
187
|
-
id: 2,
|
|
188
|
-
method: 'tools/call',
|
|
189
|
-
params: {
|
|
190
|
-
name: 'click_element',
|
|
191
|
-
arguments: { id: 'button1' }
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test('should return tool result', async () => {
|
|
197
|
-
// Arrange
|
|
198
|
-
const callPromise = client.callTool('get_screenshot', {});
|
|
199
|
-
|
|
200
|
-
setTimeout(() => {
|
|
201
|
-
mockProcess.stdout.emit('data', JSON.stringify({
|
|
202
|
-
jsonrpc: '2.0',
|
|
203
|
-
id: 2,
|
|
204
|
-
result: { image: 'base64data', format: 'PNG' }
|
|
205
|
-
}) + '\n');
|
|
206
|
-
}, 10);
|
|
207
|
-
|
|
208
|
-
// Act
|
|
209
|
-
const result = await callPromise;
|
|
210
|
-
|
|
211
|
-
// Assert
|
|
212
|
-
expect(result).toEqual({ image: 'base64data', format: 'PNG' });
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test('should handle tool errors', async () => {
|
|
216
|
-
// Arrange
|
|
217
|
-
const callPromise = client.callTool('click_element', { id: 'nonexistent' });
|
|
218
|
-
|
|
219
|
-
setTimeout(() => {
|
|
220
|
-
mockProcess.stdout.emit('data', JSON.stringify({
|
|
221
|
-
jsonrpc: '2.0',
|
|
222
|
-
id: 2,
|
|
223
|
-
error: { code: -32602, message: 'Element not found' }
|
|
224
|
-
}) + '\n');
|
|
225
|
-
}, 10);
|
|
226
|
-
|
|
227
|
-
// Act & Assert
|
|
228
|
-
await expect(callPromise).rejects.toThrow('Element not found');
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
test('should timeout after 30 seconds', async () => {
|
|
232
|
-
// Arrange - don't emit any response
|
|
233
|
-
|
|
234
|
-
// Act & Assert
|
|
235
|
-
await expect(client.callTool('click_element', {})).rejects.toThrow('timed out');
|
|
236
|
-
}, 35000); // 35 second timeout for this test
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
describe('listTools', () => {
|
|
240
|
-
beforeEach(async () => {
|
|
241
|
-
const startPromise = client.start();
|
|
242
|
-
|
|
243
|
-
setTimeout(() => {
|
|
244
|
-
mockProcess.stdout.emit('data', JSON.stringify({
|
|
245
|
-
jsonrpc: '2.0',
|
|
246
|
-
id: 1,
|
|
247
|
-
result: {
|
|
248
|
-
protocolVersion: '2024-11-05',
|
|
249
|
-
capabilities: {},
|
|
250
|
-
serverInfo: { name: 'cascade-mcp', version: '1.0.0' }
|
|
251
|
-
}
|
|
252
|
-
}) + '\n');
|
|
253
|
-
}, 10);
|
|
254
|
-
|
|
255
|
-
await startPromise;
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
test('should return list of available tools', async () => {
|
|
259
|
-
// Arrange
|
|
260
|
-
const listPromise = client.listTools();
|
|
261
|
-
|
|
262
|
-
setTimeout(() => {
|
|
263
|
-
mockProcess.stdout.emit('data', JSON.stringify({
|
|
264
|
-
jsonrpc: '2.0',
|
|
265
|
-
id: 2,
|
|
266
|
-
result: {
|
|
267
|
-
tools: [
|
|
268
|
-
{ name: 'click_element', description: 'Click element' },
|
|
269
|
-
{ name: 'get_screenshot', description: 'Get screenshot' }
|
|
270
|
-
]
|
|
271
|
-
}
|
|
272
|
-
}) + '\n');
|
|
273
|
-
}, 10);
|
|
274
|
-
|
|
275
|
-
// Act
|
|
276
|
-
const result = await listPromise;
|
|
277
|
-
|
|
278
|
-
// Assert
|
|
279
|
-
expect(result).toHaveLength(2);
|
|
280
|
-
expect(result[0].name).toBe('click_element');
|
|
281
|
-
expect(result[1].name).toBe('get_screenshot');
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test('should handle empty tool list', async () => {
|
|
285
|
-
// Arrange
|
|
286
|
-
const listPromise = client.listTools();
|
|
287
|
-
|
|
288
|
-
setTimeout(() => {
|
|
289
|
-
mockProcess.stdout.emit('data', JSON.stringify({
|
|
290
|
-
jsonrpc: '2.0',
|
|
291
|
-
id: 2,
|
|
292
|
-
result: { tools: [] }
|
|
293
|
-
}) + '\n');
|
|
294
|
-
}, 10);
|
|
295
|
-
|
|
296
|
-
// Act
|
|
297
|
-
const result = await listPromise;
|
|
298
|
-
|
|
299
|
-
// Assert
|
|
300
|
-
expect(result).toEqual([]);
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
describe('error handling', () => {
|
|
305
|
-
test('should handle JSON parse errors gracefully', async () => {
|
|
306
|
-
// Arrange
|
|
307
|
-
const startPromise = client.start();
|
|
308
|
-
|
|
309
|
-
setTimeout(() => {
|
|
310
|
-
// Emit invalid JSON first
|
|
311
|
-
mockProcess.stdout.emit('data', 'not valid json\n');
|
|
312
|
-
|
|
313
|
-
// Then emit valid response
|
|
314
|
-
mockProcess.stdout.emit('data', JSON.stringify({
|
|
315
|
-
jsonrpc: '2.0',
|
|
316
|
-
id: 1,
|
|
317
|
-
result: {
|
|
318
|
-
protocolVersion: '2024-11-05',
|
|
319
|
-
capabilities: {},
|
|
320
|
-
serverInfo: { name: 'cascade-mcp', version: '1.0.0' }
|
|
321
|
-
}
|
|
322
|
-
}) + '\n');
|
|
323
|
-
}, 10);
|
|
324
|
-
|
|
325
|
-
// Act - should not throw
|
|
326
|
-
await expect(startPromise).resolves.not.toThrow();
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
test('should handle process crash', async () => {
|
|
330
|
-
// Arrange
|
|
331
|
-
const startPromise = client.start();
|
|
332
|
-
|
|
333
|
-
setTimeout(() => {
|
|
334
|
-
const errorHandler = mockProcess.on.mock.calls.find(
|
|
335
|
-
(call: any) => call[0] === 'error'
|
|
336
|
-
)?.[1];
|
|
337
|
-
if (errorHandler) {
|
|
338
|
-
errorHandler(new Error('Process crashed'));
|
|
339
|
-
}
|
|
340
|
-
}, 10);
|
|
341
|
-
|
|
342
|
-
// Act & Assert
|
|
343
|
-
await expect(startPromise).rejects.toThrow('Process crashed');
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
test('should reject all pending on error', async () => {
|
|
347
|
-
// Arrange
|
|
348
|
-
const startPromise = client.start();
|
|
349
|
-
|
|
350
|
-
setTimeout(() => {
|
|
351
|
-
mockProcess.stdout.emit('data', JSON.stringify({
|
|
352
|
-
jsonrpc: '2.0',
|
|
353
|
-
id: 1,
|
|
354
|
-
result: { protocolVersion: '2024-11-05' }
|
|
355
|
-
}) + '\n');
|
|
356
|
-
}, 10);
|
|
357
|
-
|
|
358
|
-
await startPromise;
|
|
359
|
-
|
|
360
|
-
// Start a tool call that will hang
|
|
361
|
-
const toolPromise = client.callTool('click_element', {});
|
|
362
|
-
|
|
363
|
-
// Emit error
|
|
364
|
-
setTimeout(() => {
|
|
365
|
-
const errorHandler = mockProcess.on.mock.calls.find(
|
|
366
|
-
(call: any) => call[0] === 'error'
|
|
367
|
-
)?.[1];
|
|
368
|
-
if (errorHandler) {
|
|
369
|
-
errorHandler(new Error('Connection lost'));
|
|
370
|
-
}
|
|
371
|
-
}, 10);
|
|
372
|
-
|
|
373
|
-
// Act & Assert
|
|
374
|
-
await expect(toolPromise).rejects.toThrow('Connection lost');
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
|
|
378
|
-
describe('stop', () => {
|
|
379
|
-
test('should kill the process', async () => {
|
|
380
|
-
// Arrange
|
|
381
|
-
const startPromise = client.start();
|
|
382
|
-
|
|
383
|
-
setTimeout(() => {
|
|
384
|
-
mockProcess.stdout.emit('data', JSON.stringify({
|
|
385
|
-
jsonrpc: '2.0',
|
|
386
|
-
id: 1,
|
|
387
|
-
result: { protocolVersion: '2024-11-05' }
|
|
388
|
-
}) + '\n');
|
|
389
|
-
}, 10);
|
|
390
|
-
|
|
391
|
-
await startPromise;
|
|
392
|
-
|
|
393
|
-
// Act
|
|
394
|
-
client.stop();
|
|
395
|
-
|
|
396
|
-
// Assert
|
|
397
|
-
expect(mockProcess.kill).toHaveBeenCalled();
|
|
398
|
-
});
|
|
399
|
-
});
|
|
400
|
-
});
|