Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/test/node/testRemoteAgentHost.sh
13399 views
1
#!/usr/bin/env bash
2
# --------------------------------------------------------------------------------------------
3
# Copyright (c) Microsoft Corporation. All rights reserved.
4
# Licensed under the MIT License. See License.txt in the project root for license information.
5
# --------------------------------------------------------------------------------------------
6
7
# End-to-end smoke test for the remote agent host feature.
8
#
9
# Launches a standalone agent host server, starts the Sessions app with
10
# `chat.remoteAgentHosts` pre-configured to connect to it, validates that
11
# the Sessions app discovers the remote, and sends a chat message via the
12
# remote session target.
13
#
14
# Usage:
15
# ./testRemoteAgentHost.sh
16
# ./testRemoteAgentHost.sh "Hello, what can you do?"
17
# ./testRemoteAgentHost.sh --server-port 9090 --cdp-port 9225 "Explain this"
18
#
19
# Options:
20
# --server-port <N> Agent host WebSocket port (default: 8081)
21
# --cdp-port <N> CDP debugging port for Sessions app (default: 9224)
22
# --timeout <N> Seconds to wait for response (default: 60)
23
# --no-kill Don't kill processes after the test
24
# --skip-message Only validate connection, don't send a message
25
#
26
# Requires: @playwright/cli (npm install -g @playwright/cli, or use npx)
27
28
set -e
29
30
ROOT="$(cd "$(dirname "$0")/../../../../../.." && pwd)"
31
SERVER_PORT=8081
32
CDP_PORT=9224
33
RESPONSE_TIMEOUT=60
34
KILL_AFTER=true
35
SKIP_MESSAGE=false
36
MESSAGE=""
37
38
# Parse arguments
39
while [[ $# -gt 0 ]]; do
40
case "$1" in
41
--server-port)
42
SERVER_PORT="$2"
43
shift 2
44
;;
45
--cdp-port)
46
CDP_PORT="$2"
47
shift 2
48
;;
49
--timeout)
50
RESPONSE_TIMEOUT="$2"
51
shift 2
52
;;
53
--no-kill)
54
KILL_AFTER=false
55
shift
56
;;
57
--skip-message)
58
SKIP_MESSAGE=true
59
shift
60
;;
61
-*)
62
echo "Unknown option: $1" >&2
63
exit 1
64
;;
65
*)
66
MESSAGE="$1"
67
shift
68
;;
69
esac
70
done
71
72
if [ -z "$MESSAGE" ] && [ "$SKIP_MESSAGE" = false ]; then
73
MESSAGE="Hello, what can you do?"
74
fi
75
76
AB="npx @playwright/cli"
77
SERVER_PID=""
78
USERDATA_DIR=""
79
80
cleanup() {
81
echo "" >&2
82
echo "=== Cleanup ===" >&2
83
84
$AB close 2>/dev/null || true
85
86
if [ "$KILL_AFTER" = true ]; then
87
# Kill Sessions app
88
local CDP_PIDS
89
CDP_PIDS=$(lsof -t -i :"$CDP_PORT" 2>/dev/null || true)
90
if [ -n "$CDP_PIDS" ]; then
91
echo "Killing Sessions app (CDP port $CDP_PORT)..." >&2
92
kill $CDP_PIDS 2>/dev/null || true
93
fi
94
95
# Kill agent host server
96
if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
97
echo "Killing agent host server (PID $SERVER_PID)..." >&2
98
kill "$SERVER_PID" 2>/dev/null || true
99
# Give it a moment, then force-kill if still alive
100
sleep 0.5
101
if kill -0 "$SERVER_PID" 2>/dev/null; then
102
kill -9 "$SERVER_PID" 2>/dev/null || true
103
fi
104
fi
105
106
# Kill the sleep process that was keeping the server's stdin open.
107
# It was started via process substitution and is a child of this shell.
108
local SLEEP_PIDS
109
SLEEP_PIDS=$(pgrep -P $$ -f "sleep 86400" 2>/dev/null || true)
110
if [ -n "$SLEEP_PIDS" ]; then
111
kill $SLEEP_PIDS 2>/dev/null || true
112
fi
113
114
# Also kill by port in case PID tracking missed it
115
local SERVER_PIDS
116
SERVER_PIDS=$(lsof -t -i :"$SERVER_PORT" 2>/dev/null || true)
117
if [ -n "$SERVER_PIDS" ]; then
118
kill $SERVER_PIDS 2>/dev/null || true
119
fi
120
fi
121
122
# Clean up temp user data dir
123
if [ -n "$USERDATA_DIR" ] && [ -d "$USERDATA_DIR" ]; then
124
echo "Cleaning up temp user data dir: $USERDATA_DIR" >&2
125
rm -rf "$USERDATA_DIR"
126
fi
127
}
128
trap cleanup EXIT
129
130
# ---- Step 1: Start the agent host server ------------------------------------
131
132
echo "=== Step 1: Starting agent host server on port $SERVER_PORT ===" >&2
133
134
# Ensure port is free
135
if lsof -i :"$SERVER_PORT" >/dev/null 2>&1; then
136
echo "ERROR: Port $SERVER_PORT already in use" >&2
137
exit 1
138
fi
139
if lsof -i :"$CDP_PORT" >/dev/null 2>&1; then
140
echo "ERROR: CDP port $CDP_PORT already in use" >&2
141
exit 1
142
fi
143
144
cd "$ROOT"
145
146
# Start the server directly using Node (not via code-agent-host.sh which
147
# spawns a subprocess tree that's harder to manage in background mode).
148
# Use system node rather than the VS Code-managed node binary which may
149
# not have been downloaded yet.
150
SERVER_ENTRY="$ROOT/out/vs/platform/agentHost/node/agentHostServerMain.js"
151
152
if [ ! -f "$SERVER_ENTRY" ]; then
153
echo "ERROR: Server entry point not found: $SERVER_ENTRY" >&2
154
echo " Run the build first (npm run compile or the watch task)" >&2
155
exit 1
156
fi
157
158
# Use a temp file for output and poll for READY.
159
# The server stays alive until stdin closes (process.stdin.on('end', shutdown)),
160
# so we keep stdin open using a process substitution with a long sleep.
161
# This avoids FIFOs and leaked file descriptors that caused cleanup hangs.
162
SERVER_READY_FILE=$(mktemp)
163
164
node "$SERVER_ENTRY" --port "$SERVER_PORT" --quiet --enable-mock-agent \
165
< <(sleep 86400) > "$SERVER_READY_FILE" 2>/dev/null &
166
SERVER_PID=$!
167
168
echo "Server PID: $SERVER_PID" >&2
169
170
# Poll the output file for the READY line
171
echo "Waiting for server to start..." >&2
172
SERVER_ADDR=""
173
for i in $(seq 1 30); do
174
READY_MATCH=$(grep -o 'READY:[0-9]*' "$SERVER_READY_FILE" 2>/dev/null || true)
175
if [ -n "$READY_MATCH" ]; then
176
READY_PORT=$(echo "$READY_MATCH" | cut -d: -f2)
177
SERVER_ADDR="ws://127.0.0.1:${READY_PORT}"
178
break
179
fi
180
sleep 1
181
done
182
rm -f "$SERVER_READY_FILE"
183
184
if [ -z "$SERVER_ADDR" ]; then
185
echo "ERROR: Server did not start within 30 seconds" >&2
186
exit 1
187
fi
188
189
echo "Agent host server ready at $SERVER_ADDR" >&2
190
191
# ---- Step 2: Prepare user data with remote agent host setting ---------------
192
193
echo "=== Step 2: Configuring Sessions app settings ===" >&2
194
195
# We use 127.0.0.1:<PORT> as the address (strip ws:// prefix)
196
REMOTE_ADDR=$(echo "$SERVER_ADDR" | sed 's|^ws://||')
197
198
USERDATA_DIR=$(mktemp -d)
199
SETTINGS_DIR="$USERDATA_DIR/User"
200
mkdir -p "$SETTINGS_DIR"
201
202
cat > "$SETTINGS_DIR/settings.json" << EOF
203
{
204
"chat.remoteAgentHosts": [
205
{
206
"address": "$REMOTE_ADDR",
207
"name": "Test Remote Agent"
208
}
209
],
210
"window.titleBarStyle": "custom"
211
}
212
EOF
213
214
echo "Settings configured: $SETTINGS_DIR/settings.json" >&2
215
echo " Remote address: $REMOTE_ADDR" >&2
216
217
# ---- Step 3: Launch Sessions app --------------------------------------------
218
219
echo "=== Step 3: Launching Sessions app ===" >&2
220
221
cd "$ROOT"
222
# Unset ELECTRON_RUN_AS_NODE to ensure the app launches as Electron, not Node.
223
VSCODE_SKIP_PRELAUNCH=1 ELECTRON_RUN_AS_NODE= ./scripts/code.sh \
224
--agents \
225
--skip-sessions-welcome \
226
--remote-debugging-port="$CDP_PORT" \
227
--user-data-dir="$USERDATA_DIR" \
228
&>/dev/null &
229
230
echo "Waiting for Sessions app to start..." >&2
231
for i in $(seq 1 30); do
232
if $AB attach --cdp=http://127.0.0.1:$CDP_PORT 2>/dev/null; then
233
break
234
fi
235
sleep 2
236
if [ "$i" -eq 30 ]; then
237
echo "ERROR: Sessions app did not start within 60 seconds" >&2
238
exit 1
239
fi
240
done
241
242
echo "Connected to Sessions app via CDP" >&2
243
244
# Give the app a moment to initialize fully
245
sleep 3
246
247
# ---- Step 4: Validate the remote connection appeared -------------------------
248
249
echo "=== Step 4: Validating remote agent host connection ===" >&2
250
251
# Wait for the remote to appear as a session target
252
REMOTE_FOUND=false
253
for i in $(seq 1 20); do
254
SNAPSHOT=$($AB snapshot 2>&1 || true)
255
256
# Look for the remote in the session target picker or any UI element
257
if echo "$SNAPSHOT" | grep -qi "Test Remote Agent\|remote.*agent"; then
258
REMOTE_FOUND=true
259
break
260
fi
261
262
# Also check via DOM for the session target radio containing our remote name
263
REMOTE_CHECK=$($AB eval '
264
(() => {
265
const text = document.body.innerText || "";
266
if (text.includes("Test Remote Agent")) return "found";
267
// Check radio buttons in the session target picker
268
const buttons = document.querySelectorAll(".monaco-custom-radio > .monaco-button");
269
for (const btn of buttons) {
270
if (btn.textContent?.includes("Test Remote Agent")) return "found";
271
}
272
return "not found";
273
})()' 2>&1 || true)
274
275
if echo "$REMOTE_CHECK" | grep -q "found"; then
276
REMOTE_FOUND=true
277
break
278
fi
279
280
sleep 2
281
done
282
283
if [ "$REMOTE_FOUND" = true ]; then
284
echo "SUCCESS: Remote agent host 'Test Remote Agent' is visible in the Sessions app" >&2
285
else
286
echo "ERROR: Could not find remote agent host 'Test Remote Agent' in the Sessions app UI" >&2
287
echo "Snapshot excerpt:" >&2
288
echo "$SNAPSHOT" | head -30 >&2
289
exit 1
290
fi
291
292
# ---- Step 5: Send a message (optional) --------------------------------------
293
294
if [ "$SKIP_MESSAGE" = true ]; then
295
echo "=== Skipping message send (--skip-message) ===" >&2
296
echo "Remote agent host test completed successfully." >&2
297
exit 0
298
fi
299
300
echo "=== Step 5: Switching to remote session target and sending message ===" >&2
301
302
# Take a screenshot before interaction
303
SCREENSHOT_DIR="/tmp/remote-agent-test-$(date +%Y-%m-%dT%H-%M-%S)"
304
mkdir -p "$SCREENSHOT_DIR"
305
$AB screenshot --filename="$SCREENSHOT_DIR/01-before-interaction.png" 2>/dev/null || true
306
307
# Click the session target radio button for the remote agent host
308
CLICK_RESULT=$($AB eval '
309
(() => {
310
const buttons = document.querySelectorAll(".monaco-custom-radio > .monaco-button");
311
for (const btn of buttons) {
312
if (btn.textContent?.includes("Test Remote Agent")) {
313
btn.click();
314
return "clicked";
315
}
316
}
317
return "not found";
318
})()' 2>&1 || true)
319
320
if echo "$CLICK_RESULT" | grep -q "not found"; then
321
echo "ERROR: Could not find 'Test Remote Agent' radio button to click" >&2
322
$AB screenshot --filename="$SCREENSHOT_DIR/02-click-failed.png" 2>/dev/null || true
323
exit 1
324
fi
325
echo "Switched to remote session target" >&2
326
327
sleep 1
328
329
$AB screenshot --filename="$SCREENSHOT_DIR/02-after-target-switch.png" 2>/dev/null || true
330
331
# Fill in the remote folder path input (required for remote sessions)
332
echo "Setting remote folder path..." >&2
333
FOLDER_SET=$($AB eval '
334
(() => {
335
const input = document.querySelector("input.sessions-chat-remote-folder-text");
336
if (!input) return "no input";
337
input.focus();
338
return "focused";
339
})()' 2>&1 || true)
340
341
if echo "$FOLDER_SET" | grep -q "no input"; then
342
echo "WARNING: Could not find remote folder input, continuing anyway..." >&2
343
else
344
# Type a folder path using clipboard paste for speed
345
echo "/tmp" | pbcopy
346
$AB press Meta+a 2>/dev/null || true
347
$AB press Meta+v 2>/dev/null || true
348
sleep 0.3
349
# Press Enter to confirm the folder path
350
$AB press Enter 2>/dev/null || true
351
sleep 0.5
352
echo "Remote folder path set to /tmp" >&2
353
fi
354
355
$AB screenshot --filename="$SCREENSHOT_DIR/03-after-folder.png" 2>/dev/null || true
356
357
# Type the message into the chat editor using clipboard paste for speed
358
echo "Typing message: $MESSAGE" >&2
359
$AB eval '
360
(() => {
361
// Focus the chat editor textarea
362
const textarea = document.querySelector(".new-chat-widget .monaco-editor textarea");
363
if (textarea) { textarea.focus(); return "focused editor"; }
364
return "editor not found";
365
})()' 2>/dev/null || true
366
367
sleep 0.3
368
echo -n "$MESSAGE" | pbcopy
369
$AB press Meta+v 2>/dev/null || true
370
sleep 0.5
371
372
$AB screenshot --filename="$SCREENSHOT_DIR/04-after-type.png" 2>/dev/null || true
373
374
# Send the message via the send button or keyboard
375
$AB eval '
376
(() => {
377
// Try clicking the send button directly
378
const sendBtn = document.querySelector(".new-chat-widget .codicon-send");
379
if (sendBtn) {
380
const btn = sendBtn.closest("a, button, .monaco-button");
381
if (btn) { btn.click(); return "clicked send"; }
382
}
383
return "send button not found";
384
})()' 2>/dev/null || true
385
386
$AB screenshot --filename="$SCREENSHOT_DIR/05-after-send.png" 2>/dev/null || true
387
388
# ---- Step 6: Wait for response ----------------------------------------------
389
390
echo "Waiting for response (timeout: ${RESPONSE_TIMEOUT}s)..." >&2
391
392
RESPONSE=""
393
for i in $(seq 1 "$RESPONSE_TIMEOUT"); do
394
sleep 1
395
396
# Check for response content in the chat area
397
RESPONSE=$($AB eval '
398
(() => {
399
// Sessions app uses the main chat area (not sidebar)
400
const items = document.querySelectorAll(".interactive-item-container");
401
if (items.length < 2) return "";
402
const lastItem = items[items.length - 1];
403
const text = lastItem.textContent || "";
404
if (text.length > 20) return text;
405
return "";
406
})()' 2>&1 | sed 's/^"//;s/"$//')
407
408
if [ -n "$RESPONSE" ]; then
409
break
410
fi
411
412
# Progress indicator
413
if (( i % 10 == 0 )); then
414
echo " Still waiting... (${i}s)" >&2
415
fi
416
done
417
418
$AB screenshot --filename="$SCREENSHOT_DIR/04-response.png" 2>/dev/null || true
419
420
if [ -z "$RESPONSE" ]; then
421
echo "WARNING: No response received within ${RESPONSE_TIMEOUT}s" >&2
422
echo "Screenshots saved to: $SCREENSHOT_DIR" >&2
423
exit 1
424
fi
425
426
echo "=== Response ===" >&2
427
echo "$RESPONSE"
428
429
echo "" >&2
430
echo "Screenshots saved to: $SCREENSHOT_DIR" >&2
431
echo "Remote agent host test completed successfully." >&2
432
433