Skip to content

PolyPilot Integration Test — PR #646 #63

PolyPilot Integration Test — PR #646

PolyPilot Integration Test — PR #646 #63

name: "PolyPilot Integration Test"
run-name: "PolyPilot Integration Test${{ inputs.pr_number && format(' — PR #{0}', inputs.pr_number) || '' }}"
on:
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to test (optional — posts results as comment)'
required: false
type: number
ref:
description: 'Git ref to build (branch or SHA). Defaults to main.'
required: false
type: string
default: ''
scenario:
description: 'Test scenario to run (smoke, full, scheduled-tasks)'
required: false
type: choice
options:
- smoke
- full
- scheduled-tasks
default: smoke
push:
branches:
- 'feature/polypilot-ci-integration'
paths:
- '.github/workflows/polypilot-integration.yml'
- 'PolyPilot.Gtk/**'
- 'PolyPilot/**'
jobs:
# ─── Linux/GTK ───────────────────────────────────────────────
integration-linux:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
- name: Setup .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
dotnet-quality: 'preview'
- name: Install GTK4 dependencies
run: |
sudo apt-get update
sudo apt-get install -y libgtk-4-dev libadwaita-1-dev libwebkitgtk-6.0-dev xvfb
- name: Install MAUI workload
run: dotnet workload install maui-android
- name: Install MauiDevFlow CLI
run: |
dotnet tool install --global Microsoft.Maui.Cli \
--add-source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10/nuget/v3/index.json \
--prerelease 2>&1 || {
echo "Install returned non-zero, checking if already installed..."
maui --version || { echo "❌ MauiDevFlow CLI not available"; exit 1; }
}
echo "$HOME/.dotnet/tools" >> $GITHUB_PATH
- name: Build PolyPilot.Gtk (Debug — includes MauiDevFlow)
run: |
dotnet build PolyPilot.Gtk/PolyPilot.Gtk.csproj \
-c Debug \
--disable-build-servers \
-p:UseSharedCompilation=false \
-nodeReuse:false
- name: Find built binary
id: find-binary
run: |
echo "=== Build output ==="
ls -la PolyPilot.Gtk/bin/Debug/net10.0/ 2>/dev/null | head -20 || true
# AssemblyName in PolyPilot.Gtk.csproj is "PolyPilot" (not "PolyPilot.Gtk")
BINARY=$(find PolyPilot.Gtk/bin/Debug -name "PolyPilot" -type f -executable 2>/dev/null | head -1)
if [ -z "$BINARY" ]; then
# On Linux, the native executable may not exist — use dotnet exec with the DLL
DLL=$(find PolyPilot.Gtk/bin/Debug -name "PolyPilot.dll" -type f 2>/dev/null | head -1)
if [ -n "$DLL" ]; then
echo "No native executable found, using dotnet exec: $DLL"
BINARY="dotnet $DLL"
fi
fi
if [ -z "$BINARY" ]; then
echo "❌ No executable binary found in build output"
find PolyPilot.Gtk/bin/Debug -type f 2>/dev/null | head -20
exit 1
fi
echo "binary=$BINARY" >> $GITHUB_OUTPUT
echo "Found binary: $BINARY"
- name: Launch PolyPilot under xvfb
run: |
echo "Starting PolyPilot.Gtk under virtual display..."
export DISPLAY=:99
Xvfb :99 -screen 0 1920x1080x24 &
XVFB_PID=$!
echo "XVFB_PID=$XVFB_PID" >> $GITHUB_ENV
sleep 2
# Launch PolyPilot in background, capture all output
${{ steps.find-binary.outputs.binary }} > /tmp/polypilot-stdout.log 2>/tmp/polypilot-stderr.log &
APP_PID=$!
echo "APP_PID=$APP_PID" >> $GITHUB_ENV
echo "PolyPilot launched with PID $APP_PID"
# Wait for app + DevFlow agent readiness (poll instead of fixed sleep)
echo "Waiting for PolyPilot to start..."
for i in $(seq 1 60); do
if ! kill -0 $APP_PID 2>/dev/null; then
echo "❌ PolyPilot crashed during startup"
echo "=== stdout ===" && cat /tmp/polypilot-stdout.log || true
echo "=== stderr ===" && cat /tmp/polypilot-stderr.log || true
exit 1
fi
if curl -s --max-time 2 http://localhost:9223/api/status > /dev/null 2>&1; then
echo "✅ PolyPilot ready after ${i}s"
break
fi
if [ "$i" = "60" ]; then
echo "⚠️ Timed out waiting for DevFlow agent after 60s — continuing with port scan"
fi
sleep 1
done
# Verify process is still running
if kill -0 $APP_PID 2>/dev/null; then
echo "✅ PolyPilot is running"
else
echo "❌ PolyPilot crashed on startup"
echo "=== stdout ===" && cat /tmp/polypilot-stdout.log || true
echo "=== stderr ===" && cat /tmp/polypilot-stderr.log || true
exit 1
fi
# Show app output so far
echo "=== App stdout (first 50 lines) ==="
head -50 /tmp/polypilot-stdout.log || true
echo "=== App stderr ==="
cat /tmp/polypilot-stderr.log || true
- name: Discover MauiDevFlow agent port
id: devflow
run: |
export DISPLAY=:99
echo "Waiting for MauiDevFlow agent..."
# Check for .mauidevflow file (agent writes port here when broker unavailable)
echo "=== Checking for .mauidevflow files ==="
find / -name '.mauidevflow' -maxdepth 5 2>/dev/null || true
find /home -name '.mauidevflow' 2>/dev/null || true
find /tmp -name '.mauidevflow' -o -name 'mauidevflow*' 2>/dev/null || true
# Try broker-based discovery
maui devflow wait --timeout 30 --json > /tmp/devflow-wait.json 2>&1 || true
echo "=== devflow wait output ===" && cat /tmp/devflow-wait.json || true
# Try list
echo "=== devflow list ==="
maui devflow list --json 2>&1 || true
maui devflow list 2>&1 || true
# Port scan
echo "=== Scanning all listening ports ==="
ss -tlnp 2>/dev/null || true
# Try common DevFlow ports
AGENT_PORT=""
for port in 9223 9224 9225 9226 9227 9228 9229 9230 5000 5001 8080; do
if curl -s --max-time 2 http://localhost:$port/api/status > /dev/null 2>&1; then
AGENT_PORT=$port
echo "✅ Found agent on port $port"
curl -s http://localhost:$port/api/status
break
fi
done
if [ -z "$AGENT_PORT" ]; then
# Try discovering from app stdout (agent may log its port)
AGENT_PORT=$(grep -oP 'listening on.*?:(\d+)' /tmp/polypilot-stdout.log 2>/dev/null | grep -oP '\d+$' | head -1 || true)
if [ -n "$AGENT_PORT" ]; then
echo "✅ Found agent port from stdout: $AGENT_PORT"
fi
fi
if [ -z "$AGENT_PORT" ]; then
echo "⚠️ Could not discover agent port"
echo "App is running: $(kill -0 $APP_PID 2>/dev/null && echo 'yes' || echo 'no')"
echo "agent_port=" >> $GITHUB_OUTPUT
else
echo "✅ MauiDevFlow agent on port $AGENT_PORT"
echo "agent_port=$AGENT_PORT" >> $GITHUB_OUTPUT
fi
- name: "Smoke Test: Discover API routes"
if: steps.devflow.outputs.agent_port != ''
run: |
PORT=${{ steps.devflow.outputs.agent_port }}
echo "=== Agent Status ==="
curl -s http://localhost:$PORT/api/status | python3 -m json.tool
echo "=== Trying common routes ==="
for route in \
/api/tree /api/visual-tree /api/visualtree \
/api/screenshot /api/capture \
/api/cdp /api/cdp/status /api/cdp/list \
/api/logs /api/log \
/api/maui /api/maui/tree /api/maui/status \
/api/elements /api/query \
/tree /screenshot /status \
/api/info /api/routes /api/help \
/ /api; do
RESULT=$(curl -s --max-time 2 -o /dev/null -w "%{http_code}" http://localhost:$PORT$route 2>/dev/null)
if [ "$RESULT" != "000" ] && [ "$RESULT" != "404" ]; then
echo " $route → HTTP $RESULT"
if [ "$RESULT" = "200" ]; then
echo " $(curl -s --max-time 2 http://localhost:$PORT$route | head -c 500)"
fi
fi
done
- name: "Smoke Test: Screenshot via status endpoint"
if: steps.devflow.outputs.agent_port != ''
run: |
PORT=${{ steps.devflow.outputs.agent_port }}
# The screenshot endpoint might return binary data
curl -s http://localhost:$PORT/api/screenshot -o /tmp/polypilot-ci-screenshot.png 2>&1 || true
if [ -f /tmp/polypilot-ci-screenshot.png ] && [ $(wc -c < /tmp/polypilot-ci-screenshot.png) -gt 100 ]; then
echo "✅ Screenshot saved ($(wc -c < /tmp/polypilot-ci-screenshot.png) bytes)"
else
echo "Screenshot too small or not found, trying other paths..."
curl -s http://localhost:$PORT/screenshot -o /tmp/polypilot-ci-screenshot.png 2>&1 || true
curl -s http://localhost:$PORT/api/capture -o /tmp/polypilot-ci-screenshot2.png 2>&1 || true
ls -la /tmp/polypilot-ci-screenshot*.png 2>/dev/null || echo "No screenshots captured"
fi
- name: "Smoke Test: CLI with verbose"
if: steps.devflow.outputs.agent_port != ''
run: |
export DISPLAY=:99
PORT=${{ steps.devflow.outputs.agent_port }}
echo "=== Trying CLI with verbose ==="
maui devflow MAUI status --agent-port $PORT --verbose 2>&1 || echo "CLI status failed"
echo "=== Trying CLI tree ==="
maui devflow MAUI tree --depth 3 --agent-port $PORT --verbose 2>&1 || echo "CLI tree failed"
- name: "Diagnostics (if agent not found)"
if: steps.devflow.outputs.agent_port == ''
run: |
echo "=== Process list ==="
ps aux | grep -i polypilot || true
echo "=== Listening ports ==="
ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null || true
echo "=== App stderr (if captured) ==="
cat /tmp/polypilot-stderr.log 2>/dev/null || echo "No stderr log"
- name: "Feature Test: Scheduled Tasks"
if: steps.devflow.outputs.agent_port != '' && (inputs.scenario == 'scheduled-tasks' || inputs.scenario == 'full')
run: |
PORT=${{ steps.devflow.outputs.agent_port }}
echo "Running scheduled tasks integration tests via dotnet test"
if [ ! -d "PolyPilot.IntegrationTests" ]; then
echo "Fetching integration tests from main..."
git fetch origin main --depth=1
git checkout origin/main -- PolyPilot.IntegrationTests/
fi
POLYPILOT_AGENT_PORT=$PORT dotnet test PolyPilot.IntegrationTests \
--filter "Category=ScheduledTasks" \
--nologo --verbosity normal 2>&1 || true
- name: Upload screenshot
if: always()
uses: actions/upload-artifact@v4
with:
name: polypilot-screenshot-linux
path: /tmp/polypilot-ci-screenshot.png
if-no-files-found: ignore
- name: Upload app logs
if: always()
uses: actions/upload-artifact@v4
with:
name: polypilot-logs-linux
path: |
/tmp/polypilot-stdout.log
/tmp/polypilot-stderr.log
/tmp/devflow-wait.json
if-no-files-found: ignore
- name: Cleanup
if: always()
run: |
kill $APP_PID 2>/dev/null || true
kill $XVFB_PID 2>/dev/null || true
# ─── Mac Catalyst ────────────────────────────────────────────
integration-maccatalyst:
runs-on: macos-15
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
- name: Setup .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
dotnet-quality: 'preview'
- name: Select Xcode
run: sudo xcode-select -p
- name: Install MAUI workload
run: dotnet workload install maui
- name: Install MauiDevFlow CLI
run: |
dotnet tool install --global Microsoft.Maui.Cli \
--add-source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10/nuget/v3/index.json \
--prerelease 2>&1 || {
echo "Install returned non-zero, checking if already installed..."
maui --version || { echo "❌ MauiDevFlow CLI not available"; exit 1; }
}
echo "$HOME/.dotnet/tools" >> $GITHUB_PATH
- name: Build PolyPilot (Mac Catalyst Debug)
run: |
dotnet build PolyPilot/PolyPilot.csproj \
-f net10.0-maccatalyst \
-c Debug \
--disable-build-servers \
-p:UseSharedCompilation=false \
-p:ValidateXcodeVersion=false \
-p:MtouchLink=SdkOnly \
-nodeReuse:false
- name: Find app bundle
id: find-app
run: |
APP=$(find PolyPilot/bin/Debug/net10.0-maccatalyst -name "*.app" -type d | head -1)
echo "app=$APP" >> $GITHUB_OUTPUT
echo "Found app: $APP"
ls -la "$APP/Contents/MacOS/" || true
- name: Launch PolyPilot (Mac Catalyst)
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
run: |
BINARY="${{ steps.find-app.outputs.app }}/Contents/MacOS/PolyPilot"
# Pass token via env — PolyPilot's ServerManager reads COPILOT_GITHUB_TOKEN
# and forwards it to the headless copilot process.
# The token is masked by GitHub Actions (never appears in logs).
"$BINARY" > /tmp/polypilot-stdout.log 2>/tmp/polypilot-stderr.log &
APP_PID=$!
echo "APP_PID=$APP_PID" >> $GITHUB_ENV
echo "PolyPilot launched with PID $APP_PID"
# Wait for app + DevFlow agent readiness (poll instead of fixed sleep)
echo "Waiting for PolyPilot to start..."
for i in $(seq 1 60); do
if ! kill -0 $APP_PID 2>/dev/null; then
echo "❌ PolyPilot crashed during startup"
cat /tmp/polypilot-stderr.log || true
exit 1
fi
if curl -s --max-time 2 http://localhost:9223/api/status > /dev/null 2>&1; then
echo "✅ PolyPilot ready after ${i}s"
break
fi
if [ "$i" = "60" ]; then
echo "⚠️ Timed out waiting for DevFlow agent after 60s"
echo "App is still running — continuing with port scan"
fi
sleep 1
done
if kill -0 $APP_PID 2>/dev/null; then
echo "✅ PolyPilot is running"
else
echo "❌ PolyPilot crashed"
cat /tmp/polypilot-stderr.log || true
exit 1
fi
- name: Discover DevFlow agent
id: devflow
run: |
AGENT_PORT=""
for port in 9223 9224 9225 9226 9227 9228 9229 9230; do
if curl -s --max-time 2 http://localhost:$port/api/status > /dev/null 2>&1; then
AGENT_PORT=$port
echo "✅ Found agent on port $port"
curl -s http://localhost:$port/api/status | python3 -m json.tool
break
fi
done
echo "agent_port=${AGENT_PORT}" >> $GITHUB_OUTPUT
- name: "Smoke: Status + Tree + Screenshot"
if: steps.devflow.outputs.agent_port != ''
run: |
PORT=${{ steps.devflow.outputs.agent_port }}
echo "=== Status ===" && curl -s http://localhost:$PORT/api/status | python3 -m json.tool
echo "=== Tree ===" && curl -s http://localhost:$PORT/api/tree | python3 -c "import sys,json; d=json.load(sys.stdin); print(json.dumps(d[:3], indent=2))" 2>/dev/null || curl -s http://localhost:$PORT/api/tree | head -50
echo "=== Screenshot ===" && curl -s http://localhost:$PORT/api/screenshot -o /tmp/polypilot-ci-screenshot.png && echo "✅ $(wc -c < /tmp/polypilot-ci-screenshot.png) bytes"
- name: "Copilot: Check headless server"
run: |
echo "=== Checking for copilot headless server ==="
# PolyPilot in Persistent mode auto-starts copilot on port 4321
for port in 4321 4322; do
if curl -s --max-time 3 http://127.0.0.1:$port/ > /dev/null 2>&1; then
echo "✅ Copilot server responding on port $port"
else
echo "⚠️ Port $port not responding (may use TCP not HTTP)"
fi
done
# Check for copilot process
echo "=== Copilot processes ==="
ps aux | grep -i copilot | grep -v grep || echo "No copilot processes found"
# Check PID file
echo "=== PID file ==="
cat ~/.polypilot/server.pid 2>/dev/null || echo "No PID file"
# Check app logs for server start
echo "=== Server startup in logs ==="
grep -i 'server\|headless\|persistent\|port\|4321\|copilot' /tmp/polypilot-stdout.log 2>/dev/null | head -20 || true
- name: "Copilot: Verify session via DevFlow"
if: steps.devflow.outputs.agent_port != ''
run: |
PORT=${{ steps.devflow.outputs.agent_port }}
echo "=== Visual tree ==="
curl -s http://localhost:$PORT/api/tree | python3 -c "
import sys, json
tree = json.load(sys.stdin)
def walk(nodes, depth=0):
for n in nodes:
prefix = ' ' * depth
ntype = n.get('type','?')
aid = n.get('automationId','')
text = n.get('text','')[:50]
nid = n.get('id','')
print(f'{prefix}{ntype} [{nid}]' + (f' aid={aid}' if aid else '') + (f' text={text}' if text else ''))
if 'children' in n: walk(n['children'], depth+1)
walk(tree)
" 2>/dev/null | head -30 || echo "Tree parse failed"
- name: "Copilot: Create session via PolyPilot UI"
if: steps.devflow.outputs.agent_port != ''
run: |
PORT=${{ steps.devflow.outputs.agent_port }}
CDP="http://localhost:$PORT/api/cdp"
cdp_eval() {
local payload
payload=$(python3 -c "import json,sys; print(json.dumps({'method':'Runtime.evaluate','params':{'expression':sys.argv[1]}}))" "$1")
curl -s -X POST "$CDP" -H "Content-Type: application/json" -d "$payload" 2>/dev/null
}
echo "=== Step 1: Page state ==="
cdp_eval "JSON.stringify({title: document.title, buttons: [...document.querySelectorAll('button')].map(b => b.title || b.textContent?.trim()?.substring(0,30)).filter(Boolean)})"
echo ""
echo "=== Step 2: Click 'Create something new' ==="
cdp_eval "const btn = [...document.querySelectorAll('button')].find(b => b.textContent?.includes('+')); btn?.click(); btn ? 'clicked +' : 'no + btn'"
sleep 2
echo "=== Step 3: Click 'Session' in the popover ==="
cdp_eval "const items = [...document.querySelectorAll('.sidebar-new-menu-item')]; const sessionBtn = items.find(b => b.textContent?.includes('Session')); sessionBtn?.click(); sessionBtn ? 'clicked Session' : 'no Session btn. Items: ' + items.map(i => i.textContent?.trim()?.substring(0,20)).join('|')"
sleep 3
echo ""
echo "=== Step 4: Check create form state ==="
curl -s $CDP -X POST -H "Content-Type: application/json" \
-d '{"method":"Runtime.evaluate","params":{"expression":"JSON.stringify({inputs: [...document.querySelectorAll(\"input,textarea\")].map(i => ({tag: i.tagName, id: i.id, class: i.className, placeholder: i.placeholder})), pageText: document.body?.innerText?.substring(0,500)})"}}' | python3 -c "import sys,json; r=json.load(sys.stdin); v=r.get('result',{}).get('result',{}).get('value',''); print(json.dumps(json.loads(v), indent=2) if v.startswith('{') else v)" 2>/dev/null || echo "parse failed"
curl -s http://localhost:$PORT/api/screenshot -o /tmp/polypilot-after-create-form.png
echo "Screenshot: $(wc -c < /tmp/polypilot-after-create-form.png) bytes"
echo ""
echo "=== Step 5: Fill session name and submit ==="
# The CreateSessionForm has input fields. Find and fill them.
cdp_eval "const nameInput = document.querySelector('input[placeholder*=\"session\"], input[placeholder*=\"name\"], .session-name-input, input.create-session-input'); nameInput ? 'found name input: ' + nameInput.placeholder : 'no name input. All inputs: ' + [...document.querySelectorAll('input')].map(i => i.placeholder || i.className || i.id).join('|')"
# Try filling any visible input
cdp_eval "const inp = [...document.querySelectorAll('input:not([type=hidden])')].find(i => i.offsetParent !== null); if (inp) { inp.value = 'CI Test Session'; inp.dispatchEvent(new Event('input', {bubbles:true})); inp.dispatchEvent(new Event('change', {bubbles:true})); 'filled: ' + inp.placeholder; } else { 'no visible input'; }"
sleep 1
# Click the create/submit button
cdp_eval "const btn = document.querySelector('.create-btn, .submit-btn, button[type=submit]') || [...document.querySelectorAll('button')].find(b => b.textContent?.includes('Create') && !b.textContent?.includes('something')); btn?.click(); btn ? 'submitted: ' + btn.textContent?.trim()?.substring(0,30) : 'no create btn'"
sleep 5
echo ""
echo "=== Step 6: Check if session was created ==="
cdp_eval "JSON.stringify({sessionItems: document.querySelectorAll('.session-item, .session-list-item, .sidebar-session')?.length || 0, cards: document.querySelectorAll('.session-card, .card')?.length || 0, pageText: document.body?.innerText?.substring(0,300)})"
curl -s http://localhost:$PORT/api/screenshot -o /tmp/polypilot-after-create.png
echo "Screenshot: $(wc -c < /tmp/polypilot-after-create.png) bytes"
echo ""
echo "=== Step 7: Click on the session to expand it ==="
# Click the first session item in the sidebar to open it
cdp_eval "const item = document.querySelector('.session-item, .session-list-item, .sidebar-session, [data-session]'); if (item) { item.click(); 'clicked session: ' + (item.dataset?.session || item.textContent?.trim()?.substring(0,30)); } else { 'no session item found'; }"
sleep 3
echo ""
echo "=== Step 8: Find and fill the chat input ==="
# The session card should now have an input
curl -s -X POST "$CDP" -H "Content-Type: application/json" \
-d '{"method":"Runtime.evaluate","params":{"expression":"JSON.stringify({inputs: [...document.querySelectorAll(\"input:not([type=hidden]), textarea\")].filter(i => i.offsetParent !== null).map(i => ({tag: i.tagName, id: i.id, class: i.className, placeholder: i.placeholder?.substring(0,50), parent: i.parentElement?.className?.substring(0,30)}))})"}}' | python3 -c "import sys,json; r=json.load(sys.stdin); v=r.get('result',{}).get('result',{}).get('value',''); print(json.dumps(json.loads(v), indent=2) if v.startswith('{') else v)" 2>/dev/null || echo "parse failed"
# Fill the card input or textarea
curl -s -X POST "$CDP" -H "Content-Type: application/json" \
-d '{"method":"Runtime.evaluate","params":{"expression":"const sel = \".card-input input, .card-input textarea, .input-row textarea, .expanded-card .input-area textarea, textarea\"; const input = [...document.querySelectorAll(sel)].find(i => i.offsetParent !== null); if (input) { input.value = \"Hello from CI! Testing PolyPilot integration.\"; input.dispatchEvent(new Event(\"input\", {bubbles:true})); input.dispatchEvent(new Event(\"change\", {bubbles:true})); \"filled: \" + input.tagName + \" \" + input.id + \" \" + input.className; } else { \"no visible input\"; }"}}' 2>/dev/null
sleep 1
curl -s http://localhost:$PORT/api/screenshot -o /tmp/polypilot-after-fill.png
echo "After fill: $(wc -c < /tmp/polypilot-after-fill.png) bytes"
echo ""
echo "=== Step 9: Send the message ==="
# Simulate Enter key to send, or click the send button
curl -s -X POST "$CDP" -H "Content-Type: application/json" \
-d '{"method":"Runtime.evaluate","params":{"expression":"const sel = \".card-input input, .card-input textarea, .input-row textarea, .expanded-card .input-area textarea, textarea\"; const input = [...document.querySelectorAll(sel)].find(i => i.offsetParent !== null); if (input) { const container = input.closest(\".card-input\") || input.closest(\".input-row\"); const sendBtn = container?.querySelector(\".send-btn:not(.stop-btn)\") || container?.querySelectorAll(\"button\")?.[container?.querySelectorAll(\"button\")?.length - 1]; if (sendBtn) { sendBtn.click(); \"clicked send: \" + sendBtn.className; } else { input.dispatchEvent(new KeyboardEvent(\"keydown\", {key: \"Enter\", bubbles: true})); \"dispatched Enter\"; } } else { \"no input to send from\"; }"}}' 2>/dev/null
echo ""
echo "=== Step 10: Wait for Copilot response ==="
sleep 20
# Check for messages
curl -s -X POST "$CDP" -H "Content-Type: application/json" \
-d '{"method":"Runtime.evaluate","params":{"expression":"JSON.stringify({processingIndicators: document.querySelectorAll(\".thinking, .processing, .spinner, .loading\")?.length || 0, messages: [...document.querySelectorAll(\".message-content, .chat-bubble, .response-text, .markdown-body\")].map(m => m.textContent?.substring(0,100)).filter(Boolean), bodySnippet: document.body?.innerText?.substring(0,500)})"}}' | python3 -c "import sys,json; r=json.load(sys.stdin); v=r.get('result',{}).get('result',{}).get('value',''); print(json.dumps(json.loads(v), indent=2) if v.startswith('{') else v)" 2>/dev/null || echo "parse failed"
curl -s http://localhost:$PORT/api/screenshot -o /tmp/polypilot-after-session.png
echo "✅ Final screenshot: $(wc -c < /tmp/polypilot-after-session.png) bytes"
- name: "Feature Test: Scheduled Tasks (Mac Catalyst)"
if: steps.devflow.outputs.agent_port != '' && (inputs.scenario == 'scheduled-tasks' || inputs.scenario == 'full')
run: |
PORT=${{ steps.devflow.outputs.agent_port }}
echo "Running integration tests via dotnet test"
# The integration test project lives on main — fetch it if not present
if [ ! -d "PolyPilot.IntegrationTests" ]; then
echo "Fetching integration tests from main..."
git fetch origin main --depth=1
git checkout origin/main -- PolyPilot.IntegrationTests/
fi
# Run ALL integration tests (all categories)
POLYPILOT_AGENT_PORT=$PORT dotnet test PolyPilot.IntegrationTests \
--nologo --verbosity normal 2>&1 || true
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: polypilot-maccatalyst
path: |
/tmp/polypilot-ci-screenshot.png
/tmp/polypilot-after-new-session.png
/tmp/polypilot-after-create-form.png
/tmp/polypilot-after-create.png
/tmp/polypilot-after-fill.png
/tmp/polypilot-after-session.png
/tmp/polypilot-scheduled-tasks-*.png
/tmp/polypilot-stdout.log
/tmp/polypilot-stderr.log
if-no-files-found: ignore
- name: Cleanup
if: always()
run: |
kill $APP_PID 2>/dev/null || true
# Also kill any copilot headless server
PID_FILE=~/.polypilot/server.pid
if [ -f "$PID_FILE" ]; then
COPILOT_PID=$(head -1 "$PID_FILE" 2>/dev/null)
kill "$COPILOT_PID" 2>/dev/null || true
fi
# ─── Windows ─────────────────────────────────────────────────
integration-windows:
runs-on: windows-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.sha }}
- name: Setup .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: '10.0.x'
dotnet-quality: 'preview'
- name: Install MAUI workload
run: dotnet workload install maui
- name: Install MauiDevFlow CLI
shell: pwsh
run: |
dotnet tool install --global Microsoft.Maui.Cli `
--add-source https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet10/nuget/v3/index.json `
--prerelease 2>&1 | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Host "Install returned non-zero, checking if already installed..."
& maui --version
if ($LASTEXITCODE -ne 0) { Write-Host "❌ MauiDevFlow CLI not available"; exit 1 }
}
$toolsPath = Join-Path $env:USERPROFILE ".dotnet\tools"
"$toolsPath" | Out-File -FilePath $env:GITHUB_PATH -Append
- name: Build PolyPilot (Windows Debug)
run: |
dotnet build PolyPilot/PolyPilot.csproj `
-f net10.0-windows10.0.19041.0 `
-c Debug `
--disable-build-servers `
-p:UseSharedCompilation=false `
-nodeReuse:false
shell: pwsh
- name: Launch PolyPilot (Windows)
shell: pwsh
run: |
$exe = Get-ChildItem -Path "PolyPilot\bin\Debug\net10.0-windows*" -Filter "PolyPilot.exe" -Recurse | Select-Object -First 1
if (-not $exe) {
$exe = Get-ChildItem -Path "PolyPilot\bin\Debug\net10.0-windows*" -Filter "*.exe" -Recurse | Select-Object -First 1
}
Write-Host "Found: $($exe.FullName)"
$proc = Start-Process -FilePath $exe.FullName -PassThru -RedirectStandardOutput "$env:RUNNER_TEMP\polypilot-stdout.log" -RedirectStandardError "$env:RUNNER_TEMP\polypilot-stderr.log"
"APP_PID=$($proc.Id)" | Out-File -FilePath $env:GITHUB_ENV -Append
Write-Host "PolyPilot launched with PID $($proc.Id)"
Start-Sleep -Seconds 20
if (-not $proc.HasExited) {
Write-Host "✅ PolyPilot is running"
} else {
Write-Host "❌ PolyPilot crashed (exit code: $($proc.ExitCode))"
Get-Content "$env:RUNNER_TEMP\polypilot-stderr.log" -ErrorAction SilentlyContinue
exit 1
}
- name: Discover DevFlow agent
id: devflow
shell: pwsh
run: |
$agentPort = ""
foreach ($port in 9223..9230) {
try {
$r = Invoke-WebRequest -Uri "http://localhost:$port/api/status" -TimeoutSec 2 -ErrorAction Stop
if ($r.StatusCode -eq 200) {
$agentPort = $port
Write-Host "✅ Found agent on port $port"
$r.Content | ConvertFrom-Json | ConvertTo-Json -Depth 5
break
}
} catch { }
}
"agent_port=$agentPort" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
- name: "Smoke: Status + Tree + Screenshot"
if: steps.devflow.outputs.agent_port != ''
shell: pwsh
run: |
$port = "${{ steps.devflow.outputs.agent_port }}"
Write-Host "=== Status ==="
(Invoke-WebRequest "http://localhost:$port/api/status").Content | ConvertFrom-Json | ConvertTo-Json
Write-Host "=== Tree ==="
(Invoke-WebRequest "http://localhost:$port/api/tree").Content | Out-String | Select-Object -First 50
Write-Host "=== Screenshot ==="
Invoke-WebRequest "http://localhost:$port/api/screenshot" -OutFile "$env:RUNNER_TEMP\polypilot-ci-screenshot.png"
$size = (Get-Item "$env:RUNNER_TEMP\polypilot-ci-screenshot.png").Length
Write-Host "✅ Screenshot saved ($size bytes)"
- name: "Feature Test: Scheduled Tasks"
if: steps.devflow.outputs.agent_port != '' && (inputs.scenario == 'scheduled-tasks' || inputs.scenario == 'full')
shell: bash
run: |
PORT=${{ steps.devflow.outputs.agent_port }}
echo "Running scheduled tasks integration tests via dotnet test"
if [ ! -d "PolyPilot.IntegrationTests" ]; then
echo "Fetching integration tests from main..."
git fetch origin main --depth=1
git checkout origin/main -- PolyPilot.IntegrationTests/
fi
POLYPILOT_AGENT_PORT=$PORT dotnet test PolyPilot.IntegrationTests \
--filter "Category=ScheduledTasks" \
--nologo --verbosity normal 2>&1 || true
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: polypilot-windows
path: |
${{ runner.temp }}/polypilot-ci-screenshot.png
${{ runner.temp }}/polypilot-stdout.log
${{ runner.temp }}/polypilot-stderr.log
if-no-files-found: ignore
- name: Cleanup
if: always()
shell: pwsh
run: |
$appPid = $env:APP_PID
if ($appPid) { Stop-Process -Id $appPid -Force -ErrorAction SilentlyContinue }
# ─── Report Results to PR ────────────────────────────────────
report:
name: Report Integration Results
runs-on: ubuntu-latest
needs: [integration-linux, integration-maccatalyst, integration-windows]
if: always() && inputs.pr_number
permissions:
pull-requests: write
steps:
- name: Post results to PR
env:
GH_TOKEN: ${{ github.token }}
LINUX: ${{ needs.integration-linux.result }}
CATALYST: ${{ needs.integration-maccatalyst.result }}
WINDOWS: ${{ needs.integration-windows.result }}
PR: ${{ inputs.pr_number }}
run: |
if [ "$LINUX" = "success" ] && [ "$CATALYST" = "success" ] && [ "$WINDOWS" = "success" ]; then
STATUS="✅ All platforms passed"
else
STATUS="❌ Integration test failures"
fi
cat > /tmp/report.md << EOF
## 🧪 Integration Test Report — PR #${PR}
| Platform | Build | Launch | DevFlow | Smoke |
|----------|-------|--------|---------|-------|
| Linux/GTK (xvfb) | $([ "$LINUX" = "success" ] && echo "✅" || echo "❌") | $([ "$LINUX" = "success" ] && echo "✅" || echo "❌") | $([ "$LINUX" = "success" ] && echo "✅" || echo "⚠️") | $([ "$LINUX" = "success" ] && echo "✅" || echo "❌") |
| Mac Catalyst | $([ "$CATALYST" = "success" ] && echo "✅" || echo "❌") | — | — | — |
| Windows | $([ "$WINDOWS" = "success" ] && echo "✅" || echo "❌") | $([ "$WINDOWS" = "success" ] && echo "✅" || echo "❌") | $([ "$WINDOWS" = "success" ] && echo "⚠️" || echo "❌") | $([ "$WINDOWS" = "success" ] && echo "⚠️" || echo "❌") |
**${STATUS}**
[View full run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})
EOF
gh pr comment "$PR" --repo "${{ github.repository }}" --body "$(cat /tmp/report.md)"