Path: blob/main/src/vs/platform/agentHost/test/node/testRemoteAgentHost.sh
13399 views
#!/usr/bin/env bash1# --------------------------------------------------------------------------------------------2# Copyright (c) Microsoft Corporation. All rights reserved.3# Licensed under the MIT License. See License.txt in the project root for license information.4# --------------------------------------------------------------------------------------------56# End-to-end smoke test for the remote agent host feature.7#8# Launches a standalone agent host server, starts the Sessions app with9# `chat.remoteAgentHosts` pre-configured to connect to it, validates that10# the Sessions app discovers the remote, and sends a chat message via the11# remote session target.12#13# Usage:14# ./testRemoteAgentHost.sh15# ./testRemoteAgentHost.sh "Hello, what can you do?"16# ./testRemoteAgentHost.sh --server-port 9090 --cdp-port 9225 "Explain this"17#18# Options:19# --server-port <N> Agent host WebSocket port (default: 8081)20# --cdp-port <N> CDP debugging port for Sessions app (default: 9224)21# --timeout <N> Seconds to wait for response (default: 60)22# --no-kill Don't kill processes after the test23# --skip-message Only validate connection, don't send a message24#25# Requires: @playwright/cli (npm install -g @playwright/cli, or use npx)2627set -e2829ROOT="$(cd "$(dirname "$0")/../../../../../.." && pwd)"30SERVER_PORT=808131CDP_PORT=922432RESPONSE_TIMEOUT=6033KILL_AFTER=true34SKIP_MESSAGE=false35MESSAGE=""3637# Parse arguments38while [[ $# -gt 0 ]]; do39case "$1" in40--server-port)41SERVER_PORT="$2"42shift 243;;44--cdp-port)45CDP_PORT="$2"46shift 247;;48--timeout)49RESPONSE_TIMEOUT="$2"50shift 251;;52--no-kill)53KILL_AFTER=false54shift55;;56--skip-message)57SKIP_MESSAGE=true58shift59;;60-*)61echo "Unknown option: $1" >&262exit 163;;64*)65MESSAGE="$1"66shift67;;68esac69done7071if [ -z "$MESSAGE" ] && [ "$SKIP_MESSAGE" = false ]; then72MESSAGE="Hello, what can you do?"73fi7475AB="npx @playwright/cli"76SERVER_PID=""77USERDATA_DIR=""7879cleanup() {80echo "" >&281echo "=== Cleanup ===" >&28283$AB close 2>/dev/null || true8485if [ "$KILL_AFTER" = true ]; then86# Kill Sessions app87local CDP_PIDS88CDP_PIDS=$(lsof -t -i :"$CDP_PORT" 2>/dev/null || true)89if [ -n "$CDP_PIDS" ]; then90echo "Killing Sessions app (CDP port $CDP_PORT)..." >&291kill $CDP_PIDS 2>/dev/null || true92fi9394# Kill agent host server95if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then96echo "Killing agent host server (PID $SERVER_PID)..." >&297kill "$SERVER_PID" 2>/dev/null || true98# Give it a moment, then force-kill if still alive99sleep 0.5100if kill -0 "$SERVER_PID" 2>/dev/null; then101kill -9 "$SERVER_PID" 2>/dev/null || true102fi103fi104105# Kill the sleep process that was keeping the server's stdin open.106# It was started via process substitution and is a child of this shell.107local SLEEP_PIDS108SLEEP_PIDS=$(pgrep -P $$ -f "sleep 86400" 2>/dev/null || true)109if [ -n "$SLEEP_PIDS" ]; then110kill $SLEEP_PIDS 2>/dev/null || true111fi112113# Also kill by port in case PID tracking missed it114local SERVER_PIDS115SERVER_PIDS=$(lsof -t -i :"$SERVER_PORT" 2>/dev/null || true)116if [ -n "$SERVER_PIDS" ]; then117kill $SERVER_PIDS 2>/dev/null || true118fi119fi120121# Clean up temp user data dir122if [ -n "$USERDATA_DIR" ] && [ -d "$USERDATA_DIR" ]; then123echo "Cleaning up temp user data dir: $USERDATA_DIR" >&2124rm -rf "$USERDATA_DIR"125fi126}127trap cleanup EXIT128129# ---- Step 1: Start the agent host server ------------------------------------130131echo "=== Step 1: Starting agent host server on port $SERVER_PORT ===" >&2132133# Ensure port is free134if lsof -i :"$SERVER_PORT" >/dev/null 2>&1; then135echo "ERROR: Port $SERVER_PORT already in use" >&2136exit 1137fi138if lsof -i :"$CDP_PORT" >/dev/null 2>&1; then139echo "ERROR: CDP port $CDP_PORT already in use" >&2140exit 1141fi142143cd "$ROOT"144145# Start the server directly using Node (not via code-agent-host.sh which146# spawns a subprocess tree that's harder to manage in background mode).147# Use system node rather than the VS Code-managed node binary which may148# not have been downloaded yet.149SERVER_ENTRY="$ROOT/out/vs/platform/agentHost/node/agentHostServerMain.js"150151if [ ! -f "$SERVER_ENTRY" ]; then152echo "ERROR: Server entry point not found: $SERVER_ENTRY" >&2153echo " Run the build first (npm run compile or the watch task)" >&2154exit 1155fi156157# Use a temp file for output and poll for READY.158# The server stays alive until stdin closes (process.stdin.on('end', shutdown)),159# so we keep stdin open using a process substitution with a long sleep.160# This avoids FIFOs and leaked file descriptors that caused cleanup hangs.161SERVER_READY_FILE=$(mktemp)162163node "$SERVER_ENTRY" --port "$SERVER_PORT" --quiet --enable-mock-agent \164< <(sleep 86400) > "$SERVER_READY_FILE" 2>/dev/null &165SERVER_PID=$!166167echo "Server PID: $SERVER_PID" >&2168169# Poll the output file for the READY line170echo "Waiting for server to start..." >&2171SERVER_ADDR=""172for i in $(seq 1 30); do173READY_MATCH=$(grep -o 'READY:[0-9]*' "$SERVER_READY_FILE" 2>/dev/null || true)174if [ -n "$READY_MATCH" ]; then175READY_PORT=$(echo "$READY_MATCH" | cut -d: -f2)176SERVER_ADDR="ws://127.0.0.1:${READY_PORT}"177break178fi179sleep 1180done181rm -f "$SERVER_READY_FILE"182183if [ -z "$SERVER_ADDR" ]; then184echo "ERROR: Server did not start within 30 seconds" >&2185exit 1186fi187188echo "Agent host server ready at $SERVER_ADDR" >&2189190# ---- Step 2: Prepare user data with remote agent host setting ---------------191192echo "=== Step 2: Configuring Sessions app settings ===" >&2193194# We use 127.0.0.1:<PORT> as the address (strip ws:// prefix)195REMOTE_ADDR=$(echo "$SERVER_ADDR" | sed 's|^ws://||')196197USERDATA_DIR=$(mktemp -d)198SETTINGS_DIR="$USERDATA_DIR/User"199mkdir -p "$SETTINGS_DIR"200201cat > "$SETTINGS_DIR/settings.json" << EOF202{203"chat.remoteAgentHosts": [204{205"address": "$REMOTE_ADDR",206"name": "Test Remote Agent"207}208],209"window.titleBarStyle": "custom"210}211EOF212213echo "Settings configured: $SETTINGS_DIR/settings.json" >&2214echo " Remote address: $REMOTE_ADDR" >&2215216# ---- Step 3: Launch Sessions app --------------------------------------------217218echo "=== Step 3: Launching Sessions app ===" >&2219220cd "$ROOT"221# Unset ELECTRON_RUN_AS_NODE to ensure the app launches as Electron, not Node.222VSCODE_SKIP_PRELAUNCH=1 ELECTRON_RUN_AS_NODE= ./scripts/code.sh \223--agents \224--skip-sessions-welcome \225--remote-debugging-port="$CDP_PORT" \226--user-data-dir="$USERDATA_DIR" \227&>/dev/null &228229echo "Waiting for Sessions app to start..." >&2230for i in $(seq 1 30); do231if $AB attach --cdp=http://127.0.0.1:$CDP_PORT 2>/dev/null; then232break233fi234sleep 2235if [ "$i" -eq 30 ]; then236echo "ERROR: Sessions app did not start within 60 seconds" >&2237exit 1238fi239done240241echo "Connected to Sessions app via CDP" >&2242243# Give the app a moment to initialize fully244sleep 3245246# ---- Step 4: Validate the remote connection appeared -------------------------247248echo "=== Step 4: Validating remote agent host connection ===" >&2249250# Wait for the remote to appear as a session target251REMOTE_FOUND=false252for i in $(seq 1 20); do253SNAPSHOT=$($AB snapshot 2>&1 || true)254255# Look for the remote in the session target picker or any UI element256if echo "$SNAPSHOT" | grep -qi "Test Remote Agent\|remote.*agent"; then257REMOTE_FOUND=true258break259fi260261# Also check via DOM for the session target radio containing our remote name262REMOTE_CHECK=$($AB eval '263(() => {264const text = document.body.innerText || "";265if (text.includes("Test Remote Agent")) return "found";266// Check radio buttons in the session target picker267const buttons = document.querySelectorAll(".monaco-custom-radio > .monaco-button");268for (const btn of buttons) {269if (btn.textContent?.includes("Test Remote Agent")) return "found";270}271return "not found";272})()' 2>&1 || true)273274if echo "$REMOTE_CHECK" | grep -q "found"; then275REMOTE_FOUND=true276break277fi278279sleep 2280done281282if [ "$REMOTE_FOUND" = true ]; then283echo "SUCCESS: Remote agent host 'Test Remote Agent' is visible in the Sessions app" >&2284else285echo "ERROR: Could not find remote agent host 'Test Remote Agent' in the Sessions app UI" >&2286echo "Snapshot excerpt:" >&2287echo "$SNAPSHOT" | head -30 >&2288exit 1289fi290291# ---- Step 5: Send a message (optional) --------------------------------------292293if [ "$SKIP_MESSAGE" = true ]; then294echo "=== Skipping message send (--skip-message) ===" >&2295echo "Remote agent host test completed successfully." >&2296exit 0297fi298299echo "=== Step 5: Switching to remote session target and sending message ===" >&2300301# Take a screenshot before interaction302SCREENSHOT_DIR="/tmp/remote-agent-test-$(date +%Y-%m-%dT%H-%M-%S)"303mkdir -p "$SCREENSHOT_DIR"304$AB screenshot --filename="$SCREENSHOT_DIR/01-before-interaction.png" 2>/dev/null || true305306# Click the session target radio button for the remote agent host307CLICK_RESULT=$($AB eval '308(() => {309const buttons = document.querySelectorAll(".monaco-custom-radio > .monaco-button");310for (const btn of buttons) {311if (btn.textContent?.includes("Test Remote Agent")) {312btn.click();313return "clicked";314}315}316return "not found";317})()' 2>&1 || true)318319if echo "$CLICK_RESULT" | grep -q "not found"; then320echo "ERROR: Could not find 'Test Remote Agent' radio button to click" >&2321$AB screenshot --filename="$SCREENSHOT_DIR/02-click-failed.png" 2>/dev/null || true322exit 1323fi324echo "Switched to remote session target" >&2325326sleep 1327328$AB screenshot --filename="$SCREENSHOT_DIR/02-after-target-switch.png" 2>/dev/null || true329330# Fill in the remote folder path input (required for remote sessions)331echo "Setting remote folder path..." >&2332FOLDER_SET=$($AB eval '333(() => {334const input = document.querySelector("input.sessions-chat-remote-folder-text");335if (!input) return "no input";336input.focus();337return "focused";338})()' 2>&1 || true)339340if echo "$FOLDER_SET" | grep -q "no input"; then341echo "WARNING: Could not find remote folder input, continuing anyway..." >&2342else343# Type a folder path using clipboard paste for speed344echo "/tmp" | pbcopy345$AB press Meta+a 2>/dev/null || true346$AB press Meta+v 2>/dev/null || true347sleep 0.3348# Press Enter to confirm the folder path349$AB press Enter 2>/dev/null || true350sleep 0.5351echo "Remote folder path set to /tmp" >&2352fi353354$AB screenshot --filename="$SCREENSHOT_DIR/03-after-folder.png" 2>/dev/null || true355356# Type the message into the chat editor using clipboard paste for speed357echo "Typing message: $MESSAGE" >&2358$AB eval '359(() => {360// Focus the chat editor textarea361const textarea = document.querySelector(".new-chat-widget .monaco-editor textarea");362if (textarea) { textarea.focus(); return "focused editor"; }363return "editor not found";364})()' 2>/dev/null || true365366sleep 0.3367echo -n "$MESSAGE" | pbcopy368$AB press Meta+v 2>/dev/null || true369sleep 0.5370371$AB screenshot --filename="$SCREENSHOT_DIR/04-after-type.png" 2>/dev/null || true372373# Send the message via the send button or keyboard374$AB eval '375(() => {376// Try clicking the send button directly377const sendBtn = document.querySelector(".new-chat-widget .codicon-send");378if (sendBtn) {379const btn = sendBtn.closest("a, button, .monaco-button");380if (btn) { btn.click(); return "clicked send"; }381}382return "send button not found";383})()' 2>/dev/null || true384385$AB screenshot --filename="$SCREENSHOT_DIR/05-after-send.png" 2>/dev/null || true386387# ---- Step 6: Wait for response ----------------------------------------------388389echo "Waiting for response (timeout: ${RESPONSE_TIMEOUT}s)..." >&2390391RESPONSE=""392for i in $(seq 1 "$RESPONSE_TIMEOUT"); do393sleep 1394395# Check for response content in the chat area396RESPONSE=$($AB eval '397(() => {398// Sessions app uses the main chat area (not sidebar)399const items = document.querySelectorAll(".interactive-item-container");400if (items.length < 2) return "";401const lastItem = items[items.length - 1];402const text = lastItem.textContent || "";403if (text.length > 20) return text;404return "";405})()' 2>&1 | sed 's/^"//;s/"$//')406407if [ -n "$RESPONSE" ]; then408break409fi410411# Progress indicator412if (( i % 10 == 0 )); then413echo " Still waiting... (${i}s)" >&2414fi415done416417$AB screenshot --filename="$SCREENSHOT_DIR/04-response.png" 2>/dev/null || true418419if [ -z "$RESPONSE" ]; then420echo "WARNING: No response received within ${RESPONSE_TIMEOUT}s" >&2421echo "Screenshots saved to: $SCREENSHOT_DIR" >&2422exit 1423fi424425echo "=== Response ===" >&2426echo "$RESPONSE"427428echo "" >&2429echo "Screenshots saved to: $SCREENSHOT_DIR" >&2430echo "Remote agent host test completed successfully." >&2431432433