@kortix/agent-tunnel 0.1.3 → 0.12.7
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/README.md +65 -0
- package/dist/agent-cli.js +4676 -3174
- package/dist/client-cli.js +203 -494
- package/package.json +24 -29
- package/src/agent/agent.ts +67 -17
- package/src/agent/capabilities/desktop/cua-driver.ts +284 -0
- package/src/agent/capabilities/desktop.ts +100 -167
- package/src/agent/capabilities/enabled-registry.ts +24 -0
- package/src/agent/capabilities/filesystem.ts +136 -32
- package/src/agent/capabilities/index.ts +1 -1
- package/src/agent/capabilities/security.test.ts +204 -0
- package/src/agent/capabilities/shell.ts +37 -3
- package/src/agent/cli-device-auth.test.ts +179 -0
- package/src/agent/cli-help.test.ts +25 -0
- package/src/agent/cli.ts +475 -38
- package/src/agent/config.test.ts +53 -0
- package/src/agent/config.ts +169 -7
- package/src/agent/index.ts +1 -0
- package/src/agent/security/command-validator.ts +4 -2
- package/src/agent/security/path-validator.ts +73 -18
- package/src/agent/security/permission-guard.test.ts +52 -0
- package/src/agent/security/permission-guard.ts +35 -8
- package/src/agent/service.test.ts +63 -0
- package/src/agent/service.ts +410 -0
- package/src/client/cli.test.ts +150 -547
- package/src/client/cli.ts +116 -537
- package/src/client/index.ts +1 -1
- package/src/client/tools.ts +95 -356
- package/src/client/tunnel-client.ts +50 -80
- package/src/index.ts +7 -1
- package/src/node-ws-polyfill.test.ts +18 -0
- package/src/node-ws-polyfill.ts +5 -3
- package/src/server/heartbeat.ts +13 -6
- package/src/server/relay.test.ts +72 -0
- package/src/server/relay.ts +50 -9
- package/src/server/server.test.ts +33 -0
- package/src/server/server.ts +26 -6
- package/src/server/ws-handler.test.ts +158 -0
- package/src/server/ws-handler.ts +94 -36
- package/src/shared/crypto.ts +2 -3
- package/src/shared/index.ts +8 -0
- package/src/shared/permissions.ts +292 -0
- package/src/shared/types.ts +70 -41
- package/dist/agent/index.d.ts +0 -140
- package/dist/agent/index.js +0 -21
- package/dist/agent/index.js.map +0 -1
- package/dist/chunk-7N7GSU6K.js +0 -34
- package/dist/client/index.d.ts +0 -183
- package/dist/client/index.js +0 -8
- package/dist/client/index.js.map +0 -1
- package/dist/index.d.ts +0 -7
- package/dist/index.js +0 -55
- package/dist/index.js.map +0 -1
- package/dist/server/index.d.ts +0 -89
- package/dist/server/index.js +0 -14
- package/dist/server/index.js.map +0 -1
- package/dist/shared/index.d.ts +0 -10
- package/dist/shared/index.js +0 -20
- package/dist/shared/index.js.map +0 -1
- package/dist/types-Dpwrd8Ai.d.ts +0 -194
- package/src/agent/capabilities/desktop/atspi-helper.ts +0 -345
- package/src/agent/capabilities/desktop/csharp-helper.ts +0 -914
- package/src/agent/capabilities/desktop/linux-driver.ts +0 -368
- package/src/agent/capabilities/desktop/macos-driver.ts +0 -601
- package/src/agent/capabilities/desktop/swift-helper.ts +0 -736
- package/src/agent/capabilities/desktop/types.ts +0 -201
- package/src/agent/capabilities/desktop/windows-driver.ts +0 -220
package/src/client/cli.test.ts
CHANGED
|
@@ -1,191 +1,58 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
* Run: bun test src/client/cli.test.ts
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
13
|
-
import { spawn } from "child_process";
|
|
14
|
-
import { resolve, dirname } from "path";
|
|
15
|
-
import { tmpdir } from "os";
|
|
16
|
-
import { existsSync, unlinkSync } from "fs";
|
|
17
|
-
|
|
18
|
-
// ── Mock tunnel server ────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
const TUNNEL_ID = "test-tunnel-001";
|
|
21
|
-
|
|
22
|
-
/** Builds JSON-RPC–style responses for each method. */
|
|
23
|
-
function mockRpcResponse(
|
|
24
|
-
method: string,
|
|
25
|
-
params: Record<string, unknown>
|
|
26
|
-
): unknown {
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { resolve, dirname } from 'path';
|
|
4
|
+
|
|
5
|
+
const TUNNEL_ID = 'test-tunnel-001';
|
|
6
|
+
|
|
7
|
+
function mockRpcResponse(method: string, params: Record<string, unknown>): unknown {
|
|
27
8
|
switch (method) {
|
|
28
|
-
case
|
|
29
|
-
return {
|
|
30
|
-
|
|
31
|
-
size: 28,
|
|
32
|
-
path: params.path,
|
|
33
|
-
};
|
|
34
|
-
case "fs.write":
|
|
9
|
+
case 'fs.read':
|
|
10
|
+
return { content: `mock content of ${params.path}`, size: 28, path: params.path };
|
|
11
|
+
case 'fs.write':
|
|
35
12
|
return { path: params.path, size: (params.content as string).length };
|
|
36
|
-
case
|
|
13
|
+
case 'fs.list':
|
|
37
14
|
return {
|
|
38
15
|
entries: [
|
|
39
|
-
{
|
|
40
|
-
|
|
41
|
-
path: `${params.path}/file.txt`,
|
|
42
|
-
isDirectory: false,
|
|
43
|
-
isFile: true,
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
name: "subdir",
|
|
47
|
-
path: `${params.path}/subdir`,
|
|
48
|
-
isDirectory: true,
|
|
49
|
-
isFile: false,
|
|
50
|
-
},
|
|
16
|
+
{ name: 'file.txt', path: `${params.path}/file.txt`, isDirectory: false, isFile: true },
|
|
17
|
+
{ name: 'subdir', path: `${params.path}/subdir`, isDirectory: true, isFile: false },
|
|
51
18
|
],
|
|
52
19
|
count: 2,
|
|
53
20
|
};
|
|
54
|
-
case
|
|
21
|
+
case 'shell.exec':
|
|
55
22
|
return {
|
|
56
23
|
exitCode: 0,
|
|
57
24
|
signal: null,
|
|
58
|
-
stdout: `ran ${params.command} ${((params.args as string[]) || []).join(
|
|
59
|
-
stderr:
|
|
25
|
+
stdout: `ran ${params.command} ${((params.args as string[]) || []).join(' ')}`,
|
|
26
|
+
stderr: '',
|
|
60
27
|
stdoutTruncated: false,
|
|
61
28
|
stderrTruncated: false,
|
|
62
29
|
};
|
|
63
|
-
case
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
app: "Finder",
|
|
90
|
-
title: "Desktop",
|
|
91
|
-
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
|
|
92
|
-
minimized: false,
|
|
93
|
-
},
|
|
94
|
-
],
|
|
95
|
-
};
|
|
96
|
-
case "desktop.window.focus":
|
|
97
|
-
return { ok: true };
|
|
98
|
-
case "desktop.app.launch":
|
|
99
|
-
return { ok: true };
|
|
100
|
-
case "desktop.app.quit":
|
|
101
|
-
return { ok: true };
|
|
102
|
-
case "desktop.clipboard.read":
|
|
103
|
-
return { text: "clipboard mock content" };
|
|
104
|
-
case "desktop.clipboard.write":
|
|
105
|
-
return { ok: true };
|
|
106
|
-
case "desktop.screen.info":
|
|
107
|
-
return { width: 1920, height: 1080, scaleFactor: 2 };
|
|
108
|
-
case "desktop.cursor.image":
|
|
109
|
-
return {
|
|
110
|
-
image:
|
|
111
|
-
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
|
112
|
-
width: 100,
|
|
113
|
-
height: 100,
|
|
114
|
-
format: "png",
|
|
115
|
-
};
|
|
116
|
-
case "desktop.ax.tree":
|
|
117
|
-
return {
|
|
118
|
-
root: {
|
|
119
|
-
id: "0",
|
|
120
|
-
role: "AXApplication",
|
|
121
|
-
title: "TestApp",
|
|
122
|
-
value: "",
|
|
123
|
-
description: "",
|
|
124
|
-
bounds: { x: 0, y: 0, width: 800, height: 600 },
|
|
125
|
-
children: [
|
|
126
|
-
{
|
|
127
|
-
id: "0.0",
|
|
128
|
-
role: "AXButton",
|
|
129
|
-
title: "OK",
|
|
130
|
-
value: "",
|
|
131
|
-
description: "",
|
|
132
|
-
bounds: { x: 100, y: 100, width: 80, height: 30 },
|
|
133
|
-
children: [],
|
|
134
|
-
actions: ["AXPress"],
|
|
135
|
-
enabled: true,
|
|
136
|
-
focused: false,
|
|
137
|
-
},
|
|
138
|
-
],
|
|
139
|
-
actions: [],
|
|
140
|
-
enabled: true,
|
|
141
|
-
focused: false,
|
|
142
|
-
},
|
|
143
|
-
elementCount: 2,
|
|
144
|
-
};
|
|
145
|
-
case "desktop.ax.action":
|
|
146
|
-
return {
|
|
147
|
-
ok: true,
|
|
148
|
-
action: params.action,
|
|
149
|
-
elementId: params.elementId,
|
|
150
|
-
before: { focused: false, value: "" },
|
|
151
|
-
after: { focused: true, value: "" },
|
|
152
|
-
stateChanged: true,
|
|
153
|
-
role: "AXButton",
|
|
154
|
-
title: "OK",
|
|
155
|
-
};
|
|
156
|
-
case "desktop.ax.set_value":
|
|
157
|
-
return {
|
|
158
|
-
ok: true,
|
|
159
|
-
elementId: params.elementId,
|
|
160
|
-
requestedValue: params.value,
|
|
161
|
-
actualValue: params.value,
|
|
162
|
-
};
|
|
163
|
-
case "desktop.ax.focus":
|
|
164
|
-
return {
|
|
165
|
-
ok: true,
|
|
166
|
-
elementId: params.elementId,
|
|
167
|
-
role: "AXTextField",
|
|
168
|
-
title: "Search",
|
|
169
|
-
before: { focused: false },
|
|
170
|
-
after: { focused: true },
|
|
171
|
-
};
|
|
172
|
-
case "desktop.ax.search":
|
|
173
|
-
return {
|
|
174
|
-
elements: [
|
|
175
|
-
{
|
|
176
|
-
id: "0.1",
|
|
177
|
-
role: "AXButton",
|
|
178
|
-
title: params.query as string,
|
|
179
|
-
value: "",
|
|
180
|
-
description: "",
|
|
181
|
-
bounds: { x: 200, y: 200, width: 60, height: 30 },
|
|
182
|
-
children: [],
|
|
183
|
-
actions: ["AXPress"],
|
|
184
|
-
enabled: true,
|
|
185
|
-
focused: false,
|
|
186
|
-
},
|
|
187
|
-
],
|
|
188
|
-
};
|
|
30
|
+
case 'desktop.cua.ensure':
|
|
31
|
+
return { ok: true, binary: '/Users/me/.local/bin/cua-driver', version: 'cua-driver 0.5.7' };
|
|
32
|
+
case 'desktop.cua.start_daemon':
|
|
33
|
+
return { ok: true, status: 'ok' };
|
|
34
|
+
case 'desktop.cua.status':
|
|
35
|
+
return { status: 'ok' };
|
|
36
|
+
case 'desktop.cua.version':
|
|
37
|
+
return { version: 'cua-driver 0.5.7' };
|
|
38
|
+
case 'desktop.cua.list_tools':
|
|
39
|
+
return { tools: 'list_apps: List apps\nget_window_state: Get window state' };
|
|
40
|
+
case 'desktop.cua.describe':
|
|
41
|
+
return { description: `${params.tool}: mocked description` };
|
|
42
|
+
case 'desktop.cua.call':
|
|
43
|
+
return { tool: params.tool, args: params.args ?? {} };
|
|
44
|
+
case 'desktop.cua.list_apps':
|
|
45
|
+
return { applications: [{ name: 'TextEdit', pid: 123, bundle_id: 'com.apple.TextEdit' }] };
|
|
46
|
+
case 'desktop.cua.list_windows':
|
|
47
|
+
return { windows: [{ window_id: 77, pid: 123, title: 'Untitled', app: 'TextEdit' }] };
|
|
48
|
+
case 'desktop.cua.get_window_state':
|
|
49
|
+
return { pid: params.pid, window_id: params.window_id, accessibility_tree: '[element_index 1] text area' };
|
|
50
|
+
case 'desktop.cua.click':
|
|
51
|
+
return { ok: true, element_index: params.element_index };
|
|
52
|
+
case 'desktop.cua.type_text':
|
|
53
|
+
return { ok: true, text: params.text };
|
|
54
|
+
case 'desktop.cua.hotkey':
|
|
55
|
+
return { ok: true, keys: params.keys };
|
|
189
56
|
default:
|
|
190
57
|
return { error: `Unknown method: ${method}` };
|
|
191
58
|
}
|
|
@@ -196,44 +63,29 @@ let mockPort = 0;
|
|
|
196
63
|
|
|
197
64
|
beforeAll(() => {
|
|
198
65
|
mockServer = Bun.serve({
|
|
199
|
-
port: 0,
|
|
66
|
+
port: 0,
|
|
200
67
|
async fetch(req) {
|
|
201
68
|
const url = new URL(req.url);
|
|
202
69
|
|
|
203
|
-
|
|
204
|
-
if (
|
|
205
|
-
url.pathname === "/v1/tunnel/connections" &&
|
|
206
|
-
req.method === "GET"
|
|
207
|
-
) {
|
|
70
|
+
if (url.pathname === '/v1/tunnel/connections' && req.method === 'GET') {
|
|
208
71
|
return Response.json([
|
|
209
72
|
{
|
|
210
73
|
tunnelId: TUNNEL_ID,
|
|
211
|
-
name:
|
|
74
|
+
name: 'Test Tunnel',
|
|
212
75
|
isLive: true,
|
|
213
|
-
capabilities: [
|
|
214
|
-
machineInfo: {
|
|
215
|
-
hostname: "test-machine",
|
|
216
|
-
platform: "darwin",
|
|
217
|
-
arch: "arm64",
|
|
218
|
-
},
|
|
76
|
+
capabilities: ['filesystem', 'shell', 'desktop'],
|
|
77
|
+
machineInfo: { hostname: 'test-machine', platform: 'darwin', arch: 'arm64' },
|
|
219
78
|
},
|
|
220
79
|
]);
|
|
221
80
|
}
|
|
222
81
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (rpcMatch && req.method === "POST") {
|
|
228
|
-
const body = (await req.json()) as {
|
|
229
|
-
method: string;
|
|
230
|
-
params: Record<string, unknown>;
|
|
231
|
-
};
|
|
232
|
-
const result = mockRpcResponse(body.method, body.params || {});
|
|
233
|
-
return Response.json({ result });
|
|
82
|
+
const rpcMatch = url.pathname.match(/^\/v1\/tunnel\/rpc\/(.+)$/);
|
|
83
|
+
if (rpcMatch && req.method === 'POST') {
|
|
84
|
+
const body = (await req.json()) as { method: string; params: Record<string, unknown> };
|
|
85
|
+
return Response.json({ result: mockRpcResponse(body.method, body.params || {}) });
|
|
234
86
|
}
|
|
235
87
|
|
|
236
|
-
return new Response(
|
|
88
|
+
return new Response('Not Found', { status: 404 });
|
|
237
89
|
},
|
|
238
90
|
});
|
|
239
91
|
mockPort = mockServer.port!;
|
|
@@ -243,9 +95,7 @@ afterAll(() => {
|
|
|
243
95
|
mockServer?.stop(true);
|
|
244
96
|
});
|
|
245
97
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const CLI_PATH = resolve(dirname(import.meta.dir), "client/cli.ts");
|
|
98
|
+
const CLI_PATH = resolve(dirname(import.meta.dir), 'client/cli.ts');
|
|
249
99
|
|
|
250
100
|
interface CliResult {
|
|
251
101
|
stdout: string;
|
|
@@ -257,379 +107,173 @@ interface CliResult {
|
|
|
257
107
|
function runCli(
|
|
258
108
|
command: string,
|
|
259
109
|
argsJson?: string,
|
|
260
|
-
envOverrides?: Record<string, string
|
|
110
|
+
envOverrides?: Record<string, string>,
|
|
261
111
|
): Promise<CliResult> {
|
|
262
112
|
return new Promise((resolve) => {
|
|
263
|
-
const cliArgs = [
|
|
113
|
+
const cliArgs = ['run', CLI_PATH];
|
|
264
114
|
if (command) cliArgs.push(command);
|
|
265
115
|
if (argsJson) cliArgs.push(argsJson);
|
|
266
116
|
|
|
267
|
-
const child = spawn(
|
|
117
|
+
const child = spawn('bun', cliArgs, {
|
|
268
118
|
env: {
|
|
269
119
|
...process.env,
|
|
270
120
|
TUNNEL_API_URL: `http://localhost:${mockPort}`,
|
|
271
|
-
TUNNEL_TOKEN:
|
|
272
|
-
TUNNEL_ID:
|
|
121
|
+
TUNNEL_TOKEN: 'test-token',
|
|
122
|
+
TUNNEL_ID: '',
|
|
273
123
|
...envOverrides,
|
|
274
124
|
},
|
|
275
125
|
cwd: dirname(dirname(CLI_PATH)),
|
|
276
126
|
});
|
|
277
127
|
|
|
278
|
-
let stdout =
|
|
279
|
-
let stderr =
|
|
128
|
+
let stdout = '';
|
|
129
|
+
let stderr = '';
|
|
280
130
|
|
|
281
|
-
child.stdout.on(
|
|
282
|
-
child.stderr.on(
|
|
131
|
+
child.stdout.on('data', (d: Buffer) => (stdout += d.toString()));
|
|
132
|
+
child.stderr.on('data', (d: Buffer) => (stderr += d.toString()));
|
|
283
133
|
|
|
284
|
-
child.on(
|
|
134
|
+
child.on('close', (code: number | null) => {
|
|
285
135
|
let json: Record<string, unknown> | null = null;
|
|
286
136
|
try {
|
|
287
137
|
json = JSON.parse(stdout.trim());
|
|
288
138
|
} catch {}
|
|
289
|
-
resolve({
|
|
290
|
-
stdout: stdout.trim(),
|
|
291
|
-
stderr: stderr.trim(),
|
|
292
|
-
exitCode: code ?? 1,
|
|
293
|
-
json,
|
|
294
|
-
});
|
|
139
|
+
resolve({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode: code ?? 1, json });
|
|
295
140
|
});
|
|
296
141
|
});
|
|
297
142
|
}
|
|
298
143
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
// ── Dispatch & error handling ──
|
|
303
|
-
|
|
304
|
-
test("no command shows usage and exits 1", async () => {
|
|
305
|
-
const r = await runCli("");
|
|
144
|
+
describe('Agent Tunnel CLI', () => {
|
|
145
|
+
test('no command shows usage and exits 1', async () => {
|
|
146
|
+
const r = await runCli('');
|
|
306
147
|
expect(r.exitCode).toBe(1);
|
|
307
|
-
expect(r.stderr).toContain(
|
|
308
|
-
expect(r.stderr).toContain(
|
|
148
|
+
expect(r.stderr).toContain('Usage:');
|
|
149
|
+
expect(r.stderr).toContain('Available:');
|
|
309
150
|
});
|
|
310
151
|
|
|
311
|
-
test(
|
|
312
|
-
const r = await runCli(
|
|
152
|
+
test('unknown command shows error and exits 1', async () => {
|
|
153
|
+
const r = await runCli('nonexistent');
|
|
313
154
|
expect(r.exitCode).toBe(1);
|
|
314
|
-
expect(r.stderr).toContain(
|
|
315
|
-
expect(r.stderr).toContain("Available:");
|
|
155
|
+
expect(r.stderr).toContain('Unknown command: nonexistent');
|
|
316
156
|
});
|
|
317
157
|
|
|
318
|
-
test(
|
|
319
|
-
const r = await runCli(
|
|
158
|
+
test('invalid JSON arg exits 1 with JSON error', async () => {
|
|
159
|
+
const r = await runCli('fs_read', '{invalid json}');
|
|
320
160
|
expect(r.exitCode).toBe(1);
|
|
161
|
+
expect(r.json!.success).toBe(false);
|
|
321
162
|
});
|
|
322
163
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
test("status returns connections list", async () => {
|
|
326
|
-
const r = await runCli("status");
|
|
164
|
+
test('status returns connections list', async () => {
|
|
165
|
+
const r = await runCli('status');
|
|
327
166
|
expect(r.exitCode).toBe(0);
|
|
328
|
-
expect(r.json).not.toBeNull();
|
|
329
167
|
expect(r.json!.success).toBe(true);
|
|
330
|
-
expect(Array.isArray(r.json!.connections)).toBe(true);
|
|
331
168
|
const conns = r.json!.connections as Array<Record<string, unknown>>;
|
|
332
|
-
expect(conns.length).toBe(1);
|
|
333
169
|
expect(conns[0].tunnelId).toBe(TUNNEL_ID);
|
|
334
|
-
expect(conns[0].
|
|
335
|
-
expect(r.json!.hasOnline).toBe(true);
|
|
170
|
+
expect(conns[0].capabilities).toEqual(['filesystem', 'shell', 'desktop']);
|
|
336
171
|
});
|
|
337
172
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
test("fs_read returns file content", async () => {
|
|
341
|
-
const r = await runCli("fs_read", '{"path":"/tmp/test.txt"}');
|
|
173
|
+
test('fs_read returns file content', async () => {
|
|
174
|
+
const r = await runCli('fs_read', '{"path":"/tmp/test.txt"}');
|
|
342
175
|
expect(r.exitCode).toBe(0);
|
|
343
176
|
expect(r.json!.success).toBe(true);
|
|
344
|
-
expect(r.json!.
|
|
345
|
-
expect(r.json!.content).toContain("mock content");
|
|
346
|
-
expect(typeof r.json!.size).toBe("number");
|
|
177
|
+
expect(r.json!.content).toContain('mock content');
|
|
347
178
|
});
|
|
348
179
|
|
|
349
|
-
test(
|
|
350
|
-
const r = await runCli(
|
|
351
|
-
"fs_write",
|
|
352
|
-
'{"path":"/tmp/out.txt","content":"hello world"}'
|
|
353
|
-
);
|
|
180
|
+
test('fs_write returns path and size', async () => {
|
|
181
|
+
const r = await runCli('fs_write', '{"path":"/tmp/out.txt","content":"hello world"}');
|
|
354
182
|
expect(r.exitCode).toBe(0);
|
|
355
|
-
expect(r.json!.
|
|
356
|
-
expect(r.json!.path).toBe("/tmp/out.txt");
|
|
183
|
+
expect(r.json!.path).toBe('/tmp/out.txt');
|
|
357
184
|
expect(r.json!.size).toBe(11);
|
|
358
185
|
});
|
|
359
186
|
|
|
360
|
-
test(
|
|
361
|
-
const r = await runCli(
|
|
187
|
+
test('fs_list returns entries', async () => {
|
|
188
|
+
const r = await runCli('fs_list', '{"path":"/tmp"}');
|
|
362
189
|
expect(r.exitCode).toBe(0);
|
|
363
|
-
expect(r.json!.success).toBe(true);
|
|
364
190
|
expect(r.json!.count).toBe(2);
|
|
365
|
-
const entries = r.json!.entries as Array<Record<string, unknown>>;
|
|
366
|
-
expect(entries.length).toBe(2);
|
|
367
|
-
expect(entries[0].name).toBe("file.txt");
|
|
368
|
-
expect(entries[0].isFile).toBe(true);
|
|
369
|
-
expect(entries[1].name).toBe("subdir");
|
|
370
|
-
expect(entries[1].isDirectory).toBe(true);
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
// ── Shell ──
|
|
374
|
-
|
|
375
|
-
test("shell returns command output", async () => {
|
|
376
|
-
const r = await runCli(
|
|
377
|
-
"shell",
|
|
378
|
-
'{"command":"echo","args":["hello","world"]}'
|
|
379
|
-
);
|
|
380
|
-
expect(r.exitCode).toBe(0);
|
|
381
|
-
expect(r.json!.success).toBe(true);
|
|
382
|
-
expect(r.json!.exitCode).toBe(0);
|
|
383
|
-
expect(r.json!.stdout).toContain("echo hello world");
|
|
384
|
-
expect(r.json!.stderr).toBe("");
|
|
385
191
|
});
|
|
386
192
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
test("screenshot saves image and returns path", async () => {
|
|
390
|
-
const r = await runCli("screenshot");
|
|
193
|
+
test('shell returns command output', async () => {
|
|
194
|
+
const r = await runCli('shell', '{"command":"echo","args":["hello","world"]}');
|
|
391
195
|
expect(r.exitCode).toBe(0);
|
|
392
196
|
expect(r.json!.success).toBe(true);
|
|
393
|
-
expect(r.json!.
|
|
394
|
-
expect(r.json!.height).toBe(1);
|
|
395
|
-
expect(r.json!.format).toBe("png");
|
|
396
|
-
expect(typeof r.json!.path).toBe("string");
|
|
397
|
-
const imgPath = r.json!.path as string;
|
|
398
|
-
expect(imgPath).toContain("tunnel-screenshots");
|
|
399
|
-
expect(imgPath).toEndWith(".png");
|
|
400
|
-
// Verify the file was actually written
|
|
401
|
-
expect(existsSync(imgPath)).toBe(true);
|
|
402
|
-
// Clean up
|
|
403
|
-
try {
|
|
404
|
-
unlinkSync(imgPath);
|
|
405
|
-
} catch {}
|
|
197
|
+
expect(r.json!.stdout).toContain('echo hello world');
|
|
406
198
|
});
|
|
407
199
|
|
|
408
|
-
test(
|
|
409
|
-
const r = await runCli(
|
|
410
|
-
"screenshot",
|
|
411
|
-
'{"x":0,"y":0,"width":800,"height":600}'
|
|
412
|
-
);
|
|
200
|
+
test('cua_ensure returns binary and version', async () => {
|
|
201
|
+
const r = await runCli('cua_ensure');
|
|
413
202
|
expect(r.exitCode).toBe(0);
|
|
414
203
|
expect(r.json!.success).toBe(true);
|
|
204
|
+
expect((r.json!.result as Record<string, unknown>).version).toBe('cua-driver 0.5.7');
|
|
415
205
|
});
|
|
416
206
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
test("click returns success", async () => {
|
|
420
|
-
const r = await runCli("click", '{"x":100,"y":200}');
|
|
207
|
+
test('cua_start_daemon returns daemon status', async () => {
|
|
208
|
+
const r = await runCli('cua_start_daemon');
|
|
421
209
|
expect(r.exitCode).toBe(0);
|
|
422
|
-
expect(r.json!.
|
|
423
|
-
expect(r.json!.x).toBe(100);
|
|
424
|
-
expect(r.json!.y).toBe(200);
|
|
425
|
-
expect(r.json!.button).toBe("left");
|
|
210
|
+
expect((r.json!.result as Record<string, unknown>).ok).toBe(true);
|
|
426
211
|
});
|
|
427
212
|
|
|
428
|
-
test(
|
|
429
|
-
const r = await runCli(
|
|
430
|
-
"click",
|
|
431
|
-
'{"x":50,"y":50,"button":"right","clicks":2,"modifiers":["cmd"]}'
|
|
432
|
-
);
|
|
213
|
+
test('cua_list_tools returns CUA tool descriptions', async () => {
|
|
214
|
+
const r = await runCli('cua_list_tools');
|
|
433
215
|
expect(r.exitCode).toBe(0);
|
|
434
|
-
expect(r.json!.
|
|
435
|
-
expect(r.json!.button).toBe("right");
|
|
436
|
-
expect(r.json!.clicks).toBe(2);
|
|
216
|
+
expect((r.json!.result as Record<string, unknown>).tools).toContain('list_apps');
|
|
437
217
|
});
|
|
438
218
|
|
|
439
|
-
test(
|
|
440
|
-
const r = await runCli(
|
|
441
|
-
expect(r.exitCode).toBe(
|
|
442
|
-
expect(r.json!.success).toBe(
|
|
443
|
-
expect(r.json!.x).toBe(500);
|
|
444
|
-
expect(r.json!.y).toBe(300);
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
test("mouse_drag returns success", async () => {
|
|
448
|
-
const r = await runCli(
|
|
449
|
-
"mouse_drag",
|
|
450
|
-
'{"fromX":0,"fromY":0,"toX":100,"toY":100}'
|
|
451
|
-
);
|
|
452
|
-
expect(r.exitCode).toBe(0);
|
|
453
|
-
expect(r.json!.success).toBe(true);
|
|
454
|
-
expect(r.json!.fromX).toBe(0);
|
|
455
|
-
expect(r.json!.toX).toBe(100);
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
test("mouse_scroll returns success", async () => {
|
|
459
|
-
const r = await runCli(
|
|
460
|
-
"mouse_scroll",
|
|
461
|
-
'{"x":500,"y":500,"deltaY":3}'
|
|
462
|
-
);
|
|
463
|
-
expect(r.exitCode).toBe(0);
|
|
464
|
-
expect(r.json!.success).toBe(true);
|
|
465
|
-
expect(r.json!.deltaY).toBe(3);
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
// ── Keyboard ──
|
|
469
|
-
|
|
470
|
-
test("type returns char count", async () => {
|
|
471
|
-
const r = await runCli("type", '{"text":"hello world"}');
|
|
472
|
-
expect(r.exitCode).toBe(0);
|
|
473
|
-
expect(r.json!.success).toBe(true);
|
|
474
|
-
expect(r.json!.chars).toBe(11);
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
test("key returns pressed keys", async () => {
|
|
478
|
-
const r = await runCli("key", '{"keys":["cmd","s"]}');
|
|
479
|
-
expect(r.exitCode).toBe(0);
|
|
480
|
-
expect(r.json!.success).toBe(true);
|
|
481
|
-
const keys = r.json!.keys as string[];
|
|
482
|
-
expect(keys).toEqual(["cmd", "s"]);
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
// ── Windows ──
|
|
486
|
-
|
|
487
|
-
test("window_list returns windows", async () => {
|
|
488
|
-
const r = await runCli("window_list");
|
|
489
|
-
expect(r.exitCode).toBe(0);
|
|
490
|
-
expect(r.json!.success).toBe(true);
|
|
491
|
-
const windows = r.json!.windows as Array<Record<string, unknown>>;
|
|
492
|
-
expect(windows.length).toBe(1);
|
|
493
|
-
expect(windows[0].id).toBe(42);
|
|
494
|
-
expect(windows[0].app).toBe("Finder");
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
test("window_focus returns success", async () => {
|
|
498
|
-
const r = await runCli("window_focus", '{"windowId":42}');
|
|
499
|
-
expect(r.exitCode).toBe(0);
|
|
500
|
-
expect(r.json!.success).toBe(true);
|
|
501
|
-
expect(r.json!.windowId).toBe(42);
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
// ── Apps ──
|
|
505
|
-
|
|
506
|
-
test("app_launch returns success", async () => {
|
|
507
|
-
const r = await runCli("app_launch", '{"app":"Safari"}');
|
|
508
|
-
expect(r.exitCode).toBe(0);
|
|
509
|
-
expect(r.json!.success).toBe(true);
|
|
510
|
-
expect(r.json!.app).toBe("Safari");
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
test("app_quit returns success", async () => {
|
|
514
|
-
const r = await runCli("app_quit", '{"app":"Safari"}');
|
|
515
|
-
expect(r.exitCode).toBe(0);
|
|
516
|
-
expect(r.json!.success).toBe(true);
|
|
517
|
-
expect(r.json!.app).toBe("Safari");
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
// ── Clipboard ──
|
|
521
|
-
|
|
522
|
-
test("clipboard_read returns text", async () => {
|
|
523
|
-
const r = await runCli("clipboard_read");
|
|
524
|
-
expect(r.exitCode).toBe(0);
|
|
525
|
-
expect(r.json!.success).toBe(true);
|
|
526
|
-
expect(r.json!.text).toBe("clipboard mock content");
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
test("clipboard_write returns char count", async () => {
|
|
530
|
-
const r = await runCli("clipboard_write", '{"text":"test copy"}');
|
|
531
|
-
expect(r.exitCode).toBe(0);
|
|
532
|
-
expect(r.json!.success).toBe(true);
|
|
533
|
-
expect(r.json!.chars).toBe(9);
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
// ── Screen ──
|
|
537
|
-
|
|
538
|
-
test("screen_info returns dimensions", async () => {
|
|
539
|
-
const r = await runCli("screen_info");
|
|
540
|
-
expect(r.exitCode).toBe(0);
|
|
541
|
-
expect(r.json!.success).toBe(true);
|
|
542
|
-
expect(r.json!.width).toBe(1920);
|
|
543
|
-
expect(r.json!.height).toBe(1080);
|
|
544
|
-
expect(r.json!.scaleFactor).toBe(2);
|
|
219
|
+
test('cua_describe requires a tool', async () => {
|
|
220
|
+
const r = await runCli('cua_describe');
|
|
221
|
+
expect(r.exitCode).toBe(1);
|
|
222
|
+
expect(r.json!.success).toBe(false);
|
|
545
223
|
});
|
|
546
224
|
|
|
547
|
-
test(
|
|
548
|
-
const r = await runCli(
|
|
225
|
+
test('cua_describe calls desktop.cua.describe', async () => {
|
|
226
|
+
const r = await runCli('cua_describe', '{"tool":"get_window_state"}');
|
|
549
227
|
expect(r.exitCode).toBe(0);
|
|
550
|
-
expect(r.json!.
|
|
551
|
-
expect(r.json!.width).toBe(100);
|
|
552
|
-
expect(r.json!.height).toBe(100);
|
|
553
|
-
const imgPath = r.json!.path as string;
|
|
554
|
-
expect(existsSync(imgPath)).toBe(true);
|
|
555
|
-
try {
|
|
556
|
-
unlinkSync(imgPath);
|
|
557
|
-
} catch {}
|
|
228
|
+
expect((r.json!.result as Record<string, unknown>).description).toContain('get_window_state');
|
|
558
229
|
});
|
|
559
230
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
test("ax_tree returns formatted tree", async () => {
|
|
563
|
-
const r = await runCli("ax_tree");
|
|
231
|
+
test('cua_call calls arbitrary CUA tools', async () => {
|
|
232
|
+
const r = await runCli('cua_call', '{"tool":"check_permissions","args":{"format":"json"}}');
|
|
564
233
|
expect(r.exitCode).toBe(0);
|
|
565
|
-
expect(r.json!.
|
|
566
|
-
expect(r.json!.
|
|
567
|
-
expect(typeof r.json!.tree).toBe("string");
|
|
568
|
-
const tree = r.json!.tree as string;
|
|
569
|
-
expect(tree).toContain("[AXApplication]");
|
|
570
|
-
expect(tree).toContain("[AXButton] OK");
|
|
234
|
+
expect(r.json!.tool).toBe('check_permissions');
|
|
235
|
+
expect((r.json!.result as Record<string, unknown>).tool).toBe('check_permissions');
|
|
571
236
|
});
|
|
572
237
|
|
|
573
|
-
test(
|
|
574
|
-
const r = await runCli(
|
|
238
|
+
test('cua_list_apps returns app list', async () => {
|
|
239
|
+
const r = await runCli('cua_list_apps');
|
|
575
240
|
expect(r.exitCode).toBe(0);
|
|
576
|
-
|
|
241
|
+
const result = r.json!.result as Record<string, unknown>;
|
|
242
|
+
expect(Array.isArray(result.applications)).toBe(true);
|
|
577
243
|
});
|
|
578
244
|
|
|
579
|
-
test(
|
|
580
|
-
const r = await runCli(
|
|
581
|
-
"ax_action",
|
|
582
|
-
'{"elementId":"0.0","action":"AXPress"}'
|
|
583
|
-
);
|
|
245
|
+
test('cua_list_windows returns window list', async () => {
|
|
246
|
+
const r = await runCli('cua_list_windows', '{"pid":123}');
|
|
584
247
|
expect(r.exitCode).toBe(0);
|
|
585
|
-
|
|
586
|
-
expect(
|
|
587
|
-
expect(r.json!.elementId).toBe("0.0");
|
|
588
|
-
expect(r.json!.stateChanged).toBe(true);
|
|
589
|
-
expect(r.json!.role).toBe("AXButton");
|
|
248
|
+
const result = r.json!.result as Record<string, unknown>;
|
|
249
|
+
expect((result.windows as unknown[]).length).toBe(1);
|
|
590
250
|
});
|
|
591
251
|
|
|
592
|
-
test(
|
|
593
|
-
const r = await runCli(
|
|
594
|
-
"ax_set_value",
|
|
595
|
-
'{"elementId":"0.1","value":"search text"}'
|
|
596
|
-
);
|
|
252
|
+
test('cua_get_window_state returns accessibility markdown', async () => {
|
|
253
|
+
const r = await runCli('cua_get_window_state', '{"pid":123,"window_id":77}');
|
|
597
254
|
expect(r.exitCode).toBe(0);
|
|
598
|
-
expect(r.json!.
|
|
599
|
-
expect(r.json!.requestedValue).toBe("search text");
|
|
600
|
-
expect(r.json!.actualValue).toBe("search text");
|
|
255
|
+
expect((r.json!.result as Record<string, unknown>).accessibility_tree).toContain('element_index');
|
|
601
256
|
});
|
|
602
257
|
|
|
603
|
-
test(
|
|
604
|
-
const r = await runCli(
|
|
258
|
+
test('cua_click supports element-indexed input', async () => {
|
|
259
|
+
const r = await runCli('cua_click', '{"pid":123,"window_id":77,"element_index":1}');
|
|
605
260
|
expect(r.exitCode).toBe(0);
|
|
606
|
-
expect(r.json!.
|
|
607
|
-
expect(r.json!.role).toBe("AXTextField");
|
|
608
|
-
expect((r.json!.after as Record<string, unknown>).focused).toBe(true);
|
|
261
|
+
expect((r.json!.result as Record<string, unknown>).ok).toBe(true);
|
|
609
262
|
});
|
|
610
263
|
|
|
611
|
-
test(
|
|
612
|
-
const r = await runCli(
|
|
264
|
+
test('cua_type_text sends text', async () => {
|
|
265
|
+
const r = await runCli('cua_type_text', '{"pid":123,"text":"hello"}');
|
|
613
266
|
expect(r.exitCode).toBe(0);
|
|
614
|
-
expect(r.json!.
|
|
615
|
-
expect(r.json!.count).toBe(1);
|
|
616
|
-
const elements = r.json!.elements as Array<Record<string, unknown>>;
|
|
617
|
-
expect(elements[0].role).toBe("AXButton");
|
|
618
|
-
expect(elements[0].title).toBe("Submit");
|
|
267
|
+
expect((r.json!.result as Record<string, unknown>).text).toBe('hello');
|
|
619
268
|
});
|
|
620
269
|
|
|
621
|
-
test(
|
|
622
|
-
const r = await runCli(
|
|
623
|
-
"ax_search",
|
|
624
|
-
'{"query":"Submit","role":"button","maxResults":5}'
|
|
625
|
-
);
|
|
270
|
+
test('cua_hotkey sends keys', async () => {
|
|
271
|
+
const r = await runCli('cua_hotkey', '{"pid":123,"keys":["cmd","s"]}');
|
|
626
272
|
expect(r.exitCode).toBe(0);
|
|
627
|
-
expect(r.json!.
|
|
273
|
+
expect((r.json!.result as Record<string, unknown>).keys).toEqual(['cmd', 's']);
|
|
628
274
|
});
|
|
629
275
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
describe("permission handling", () => {
|
|
276
|
+
describe('permission handling', () => {
|
|
633
277
|
let permServer: ReturnType<typeof Bun.serve>;
|
|
634
278
|
let permPort = 0;
|
|
635
279
|
|
|
@@ -639,25 +283,18 @@ describe("Agent Tunnel CLI", () => {
|
|
|
639
283
|
async fetch(req) {
|
|
640
284
|
const url = new URL(req.url);
|
|
641
285
|
|
|
642
|
-
if (url.pathname ===
|
|
643
|
-
return Response.json([
|
|
644
|
-
{ tunnelId: "perm-tunnel", isLive: true },
|
|
645
|
-
]);
|
|
286
|
+
if (url.pathname === '/v1/tunnel/connections') {
|
|
287
|
+
return Response.json([{ tunnelId: 'perm-tunnel', isLive: true }]);
|
|
646
288
|
}
|
|
647
289
|
|
|
648
|
-
|
|
649
|
-
if (url.pathname.startsWith("/v1/tunnel/rpc/")) {
|
|
290
|
+
if (url.pathname.startsWith('/v1/tunnel/rpc/')) {
|
|
650
291
|
return Response.json(
|
|
651
|
-
{
|
|
652
|
-
|
|
653
|
-
error: "Permission denied",
|
|
654
|
-
requestId: "req-abc-123",
|
|
655
|
-
},
|
|
656
|
-
{ status: 403 }
|
|
292
|
+
{ code: -32003, error: 'Permission denied', requestId: 'req-abc-123' },
|
|
293
|
+
{ status: 403 },
|
|
657
294
|
);
|
|
658
295
|
}
|
|
659
296
|
|
|
660
|
-
return new Response(
|
|
297
|
+
return new Response('Not Found', { status: 404 });
|
|
661
298
|
},
|
|
662
299
|
});
|
|
663
300
|
permPort = permServer.port!;
|
|
@@ -667,54 +304,25 @@ describe("Agent Tunnel CLI", () => {
|
|
|
667
304
|
permServer?.stop(true);
|
|
668
305
|
});
|
|
669
306
|
|
|
670
|
-
test(
|
|
671
|
-
const r = await runCli(
|
|
307
|
+
test('permission denied returns structured response', async () => {
|
|
308
|
+
const r = await runCli('cua_list_apps', undefined, {
|
|
672
309
|
TUNNEL_API_URL: `http://localhost:${permPort}`,
|
|
673
310
|
});
|
|
674
|
-
// CLI should output permission-required JSON (not crash)
|
|
675
|
-
expect(r.json).not.toBeNull();
|
|
676
|
-
expect(r.json!.success).toBe(false);
|
|
677
|
-
expect(r.json!.permissionRequired).toBe(true);
|
|
678
|
-
expect(r.json!.requestId).toBe("req-abc-123");
|
|
679
|
-
expect(typeof r.json!.message).toBe("string");
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
test("shell permission denied returns structured response", async () => {
|
|
683
|
-
const r = await runCli(
|
|
684
|
-
"shell",
|
|
685
|
-
'{"command":"rm","args":["-rf","/"]}',
|
|
686
|
-
{ TUNNEL_API_URL: `http://localhost:${permPort}` }
|
|
687
|
-
);
|
|
688
311
|
expect(r.json!.success).toBe(false);
|
|
689
312
|
expect(r.json!.permissionRequired).toBe(true);
|
|
313
|
+
expect(r.json!.requestId).toBe('req-abc-123');
|
|
690
314
|
});
|
|
691
315
|
});
|
|
692
316
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
test("status with dead server returns error JSON", async () => {
|
|
697
|
-
const r = await runCli("status", undefined, {
|
|
698
|
-
TUNNEL_API_URL: "http://localhost:1",
|
|
699
|
-
});
|
|
700
|
-
expect(r.exitCode).toBe(1);
|
|
701
|
-
expect(r.json).not.toBeNull();
|
|
702
|
-
expect(r.json!.success).toBe(false);
|
|
703
|
-
expect(typeof r.json!.error).toBe("string");
|
|
704
|
-
});
|
|
705
|
-
|
|
706
|
-
test("fs_read with dead server returns error JSON", async () => {
|
|
707
|
-
const r = await runCli("fs_read", '{"path":"/tmp/x"}', {
|
|
708
|
-
TUNNEL_API_URL: "http://localhost:1",
|
|
709
|
-
});
|
|
317
|
+
describe('server unreachable', () => {
|
|
318
|
+
test('status with dead server returns error JSON', async () => {
|
|
319
|
+
const r = await runCli('status', undefined, { TUNNEL_API_URL: 'http://localhost:1' });
|
|
710
320
|
expect(r.exitCode).toBe(1);
|
|
711
321
|
expect(r.json!.success).toBe(false);
|
|
712
322
|
});
|
|
713
323
|
});
|
|
714
324
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
describe("no connections", () => {
|
|
325
|
+
describe('no connections', () => {
|
|
718
326
|
let emptyServer: ReturnType<typeof Bun.serve>;
|
|
719
327
|
let emptyPort = 0;
|
|
720
328
|
|
|
@@ -723,10 +331,8 @@ describe("Agent Tunnel CLI", () => {
|
|
|
723
331
|
port: 0,
|
|
724
332
|
async fetch(req) {
|
|
725
333
|
const url = new URL(req.url);
|
|
726
|
-
if (url.pathname ===
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
return new Response("Not Found", { status: 404 });
|
|
334
|
+
if (url.pathname === '/v1/tunnel/connections') return Response.json([]);
|
|
335
|
+
return new Response('Not Found', { status: 404 });
|
|
730
336
|
},
|
|
731
337
|
});
|
|
732
338
|
emptyPort = emptyServer.port!;
|
|
@@ -736,23 +342,20 @@ describe("Agent Tunnel CLI", () => {
|
|
|
736
342
|
emptyServer?.stop(true);
|
|
737
343
|
});
|
|
738
344
|
|
|
739
|
-
test(
|
|
740
|
-
const r = await runCli(
|
|
741
|
-
TUNNEL_API_URL: `http://localhost:${emptyPort}`,
|
|
742
|
-
});
|
|
345
|
+
test('status with no connections returns empty list', async () => {
|
|
346
|
+
const r = await runCli('status', undefined, { TUNNEL_API_URL: `http://localhost:${emptyPort}` });
|
|
743
347
|
expect(r.exitCode).toBe(0);
|
|
744
348
|
expect(r.json!.success).toBe(true);
|
|
745
349
|
expect((r.json!.connections as unknown[]).length).toBe(0);
|
|
746
|
-
expect(typeof r.json!.message).toBe("string");
|
|
747
350
|
});
|
|
748
351
|
|
|
749
|
-
test(
|
|
750
|
-
const r = await runCli(
|
|
352
|
+
test('fs_read with no connections returns error', async () => {
|
|
353
|
+
const r = await runCli('fs_read', '{"path":"/tmp/x"}', {
|
|
751
354
|
TUNNEL_API_URL: `http://localhost:${emptyPort}`,
|
|
752
355
|
});
|
|
753
356
|
expect(r.exitCode).toBe(1);
|
|
754
357
|
expect(r.json!.success).toBe(false);
|
|
755
|
-
expect(r.json!.error).toContain(
|
|
358
|
+
expect(r.json!.error).toContain('No tunnel connection found');
|
|
756
359
|
});
|
|
757
360
|
});
|
|
758
361
|
});
|