diff --git a/.agents/skills/lora-manager-e2e/SKILL.md b/.agents/skills/lora-manager-e2e/SKILL.md new file mode 100644 index 00000000..316212d6 --- /dev/null +++ b/.agents/skills/lora-manager-e2e/SKILL.md @@ -0,0 +1,201 @@ +--- +name: lora-manager-e2e +description: End-to-end testing and validation for LoRa Manager features. Use when performing automated E2E validation of LoRa Manager standalone mode, including starting/restarting the server, using Chrome DevTools MCP to interact with the web UI at http://127.0.0.1:8188/loras, and verifying frontend-to-backend functionality. Covers workflow validation, UI interaction testing, and integration testing between the standalone Python backend and the browser frontend. +--- + +# LoRa Manager E2E Testing + +This skill provides workflows and utilities for end-to-end testing of LoRa Manager using Chrome DevTools MCP. + +## Prerequisites + +- LoRa Manager project cloned and dependencies installed (`pip install -r requirements.txt`) +- Chrome browser available for debugging +- Chrome DevTools MCP connected + +## Quick Start Workflow + +### 1. Start LoRa Manager Standalone + +```python +# Use the provided script to start the server +python .agents/skills/lora-manager-e2e/scripts/start_server.py --port 8188 +``` + +Or manually: +```bash +cd /home/miao/workspace/ComfyUI/custom_nodes/ComfyUI-Lora-Manager +python standalone.py --port 8188 +``` + +Wait for server ready message before proceeding. + +### 2. Open Chrome Debug Mode + +```bash +# Chrome with remote debugging on port 9222 +google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-lora-manager http://127.0.0.1:8188/loras +``` + +### 3. Connect Chrome DevTools MCP + +Ensure the MCP server is connected to Chrome at `http://localhost:9222`. + +### 4. Navigate and Interact + +Use Chrome DevTools MCP tools to: +- Take snapshots: `take_snapshot` +- Click elements: `click` +- Fill forms: `fill` or `fill_form` +- Evaluate scripts: `evaluate_script` +- Wait for elements: `wait_for` + +## Common E2E Test Patterns + +### Pattern: Full Page Load Verification + +```python +# Navigate to LoRA list page +navigate_page(type="url", url="http://127.0.0.1:8188/loras") + +# Wait for page to load +wait_for(text="LoRAs", timeout=10000) + +# Take snapshot to verify UI state +snapshot = take_snapshot() +``` + +### Pattern: Restart Server for Configuration Changes + +```python +# Stop current server (if running) +# Start with new configuration +python .agents/skills/lora-manager-e2e/scripts/start_server.py --port 8188 --restart + +# Wait and refresh browser +navigate_page(type="reload", ignoreCache=True) +wait_for(text="LoRAs", timeout=15000) +``` + +### Pattern: Verify Backend API via Frontend + +```python +# Execute script in browser to call backend API +result = evaluate_script(function=""" +async () => { + const response = await fetch('/loras/api/list'); + const data = await response.json(); + return { count: data.length, firstItem: data[0]?.name }; +} +""") +``` + +### Pattern: Form Submission Flow + +```python +# Fill a form (e.g., search or filter) +fill_form(elements=[ + {"uid": "search-input", "value": "character"}, +]) + +# Click submit button +click(uid="search-button") + +# Wait for results +wait_for(text="Results", timeout=5000) + +# Verify results via snapshot +snapshot = take_snapshot() +``` + +### Pattern: Modal Dialog Interaction + +```python +# Open modal (e.g., add LoRA) +click(uid="add-lora-button") + +# Wait for modal to appear +wait_for(text="Add LoRA", timeout=3000) + +# Fill modal form +fill_form(elements=[ + {"uid": "lora-name", "value": "Test LoRA"}, + {"uid": "lora-path", "value": "/path/to/lora.safetensors"}, +]) + +# Submit +click(uid="modal-submit-button") + +# Wait for success message or close +wait_for(text="Success", timeout=5000) +``` + +## Available Scripts + +### scripts/start_server.py + +Starts or restarts the LoRa Manager standalone server. + +```bash +python scripts/start_server.py [--port PORT] [--restart] [--wait] +``` + +Options: +- `--port`: Server port (default: 8188) +- `--restart`: Kill existing server before starting +- `--wait`: Wait for server to be ready before exiting + +### scripts/wait_for_server.py + +Polls server until ready or timeout. + +```bash +python scripts/wait_for_server.py [--port PORT] [--timeout SECONDS] +``` + +## Test Scenarios Reference + +See [references/test-scenarios.md](references/test-scenarios.md) for detailed test scenarios including: +- LoRA list display and filtering +- Model metadata editing +- Recipe creation and management +- Settings configuration +- Import/export functionality + +## Network Request Verification + +Use `list_network_requests` and `get_network_request` to verify API calls: + +```python +# List recent XHR/fetch requests +requests = list_network_requests(resourceTypes=["xhr", "fetch"]) + +# Get details of specific request +details = get_network_request(reqid=123) +``` + +## Console Message Monitoring + +```python +# Check for errors or warnings +messages = list_console_messages(types=["error", "warn"]) +``` + +## Performance Testing + +```python +# Start performance trace +performance_start_trace(reload=True, autoStop=False) + +# Perform actions... + +# Stop and analyze +results = performance_stop_trace() +``` + +## Cleanup + +Always ensure proper cleanup after tests: +1. Stop the standalone server +2. Close browser pages (keep at least one open) +3. Clear temporary data if needed diff --git a/.agents/skills/lora-manager-e2e/references/mcp-cheatsheet.md b/.agents/skills/lora-manager-e2e/references/mcp-cheatsheet.md new file mode 100644 index 00000000..ff534ea1 --- /dev/null +++ b/.agents/skills/lora-manager-e2e/references/mcp-cheatsheet.md @@ -0,0 +1,324 @@ +# Chrome DevTools MCP Cheatsheet for LoRa Manager + +Quick reference for common MCP commands used in LoRa Manager E2E testing. + +## Navigation + +```python +# Navigate to LoRA list page +navigate_page(type="url", url="http://127.0.0.1:8188/loras") + +# Reload page with cache clear +navigate_page(type="reload", ignoreCache=True) + +# Go back/forward +navigate_page(type="back") +navigate_page(type="forward") +``` + +## Waiting + +```python +# Wait for text to appear +wait_for(text="LoRAs", timeout=10000) + +# Wait for specific element (via evaluate_script) +evaluate_script(function=""" +() => { + return new Promise((resolve) => { + const check = () => { + if (document.querySelector('.lora-card')) { + resolve(true); + } else { + setTimeout(check, 100); + } + }; + check(); + }); +} +""") +``` + +## Taking Snapshots + +```python +# Full page snapshot +snapshot = take_snapshot() + +# Verbose snapshot (more details) +snapshot = take_snapshot(verbose=True) + +# Save to file +take_snapshot(filePath="test-snapshots/page-load.json") +``` + +## Element Interaction + +```python +# Click element +click(uid="element-uid-from-snapshot") + +# Double click +click(uid="element-uid", dblClick=True) + +# Fill input +fill(uid="search-input", value="test query") + +# Fill multiple inputs +fill_form(elements=[ + {"uid": "input-1", "value": "value 1"}, + {"uid": "input-2", "value": "value 2"}, +]) + +# Hover +hover(uid="lora-card-1") + +# Upload file +upload_file(uid="file-input", filePath="/path/to/file.safetensors") +``` + +## Keyboard Input + +```python +# Press key +press_key(key="Enter") +press_key(key="Escape") +press_key(key="Tab") + +# Keyboard shortcuts +press_key(key="Control+A") # Select all +press_key(key="Control+F") # Find +``` + +## JavaScript Evaluation + +```python +# Simple evaluation +result = evaluate_script(function="() => document.title") + +# Async evaluation +result = evaluate_script(function=""" +async () => { + const response = await fetch('/loras/api/list'); + return await response.json(); +} +""") + +# Check element existence +exists = evaluate_script(function=""" +() => document.querySelector('.lora-card') !== null +""") + +# Get element count +count = evaluate_script(function=""" +() => document.querySelectorAll('.lora-card').length +""") +``` + +## Network Monitoring + +```python +# List all network requests +requests = list_network_requests() + +# Filter by resource type +xhr_requests = list_network_requests(resourceTypes=["xhr", "fetch"]) + +# Get specific request details +details = get_network_request(reqid=123) + +# Include preserved requests from previous navigations +all_requests = list_network_requests(includePreservedRequests=True) +``` + +## Console Monitoring + +```python +# List all console messages +messages = list_console_messages() + +# Filter by type +errors = list_console_messages(types=["error", "warn"]) + +# Include preserved messages +all_messages = list_console_messages(includePreservedMessages=True) + +# Get specific message +details = get_console_message(msgid=1) +``` + +## Performance Testing + +```python +# Start trace with page reload +performance_start_trace(reload=True, autoStop=False) + +# Start trace without reload +performance_start_trace(reload=False, autoStop=True, filePath="trace.json.gz") + +# Stop trace +results = performance_stop_trace() + +# Stop and save +performance_stop_trace(filePath="trace-results.json.gz") + +# Analyze specific insight +insight = performance_analyze_insight( + insightSetId="results.insightSets[0].id", + insightName="LCPBreakdown" +) +``` + +## Page Management + +```python +# List open pages +pages = list_pages() + +# Select a page +select_page(pageId=0, bringToFront=True) + +# Create new page +new_page(url="http://127.0.0.1:8188/loras") + +# Close page (keep at least one open!) +close_page(pageId=1) + +# Resize page +resize_page(width=1920, height=1080) +``` + +## Screenshots + +```python +# Full page screenshot +take_screenshot(fullPage=True) + +# Viewport screenshot +take_screenshot() + +# Element screenshot +take_screenshot(uid="lora-card-1") + +# Save to file +take_screenshot(filePath="screenshots/page.png", format="png") + +# JPEG with quality +take_screenshot(filePath="screenshots/page.jpg", format="jpeg", quality=90) +``` + +## Dialog Handling + +```python +# Accept dialog +handle_dialog(action="accept") + +# Accept with text input +handle_dialog(action="accept", promptText="user input") + +# Dismiss dialog +handle_dialog(action="dismiss") +``` + +## Device Emulation + +```python +# Mobile viewport +emulate(viewport={"width": 375, "height": 667, "isMobile": True, "hasTouch": True}) + +# Tablet viewport +emulate(viewport={"width": 768, "height": 1024, "isMobile": True, "hasTouch": True}) + +# Desktop viewport +emulate(viewport={"width": 1920, "height": 1080}) + +# Network throttling +emulate(networkConditions="Slow 3G") +emulate(networkConditions="Fast 4G") + +# CPU throttling +emulate(cpuThrottlingRate=4) # 4x slowdown + +# Geolocation +emulate(geolocation={"latitude": 37.7749, "longitude": -122.4194}) + +# User agent +emulate(userAgent="Mozilla/5.0 (Custom)") + +# Reset emulation +emulate(viewport=None, networkConditions="No emulation", userAgent=None) +``` + +## Drag and Drop + +```python +# Drag element to another +drag(from_uid="draggable-item", to_uid="drop-zone") +``` + +## Common LoRa Manager Test Patterns + +### Verify LoRA Cards Loaded + +```python +navigate_page(type="url", url="http://127.0.0.1:8188/loras") +wait_for(text="LoRAs", timeout=10000) + +# Check if cards loaded +result = evaluate_script(function=""" +() => { + const cards = document.querySelectorAll('.lora-card'); + return { + count: cards.length, + hasData: cards.length > 0 + }; +} +""") +``` + +### Search and Verify Results + +```python +fill(uid="search-input", value="character") +press_key(key="Enter") +wait_for(timeout=2000) # Wait for debounce + +# Check results +result = evaluate_script(function=""" +() => { + const cards = document.querySelectorAll('.lora-card'); + const names = Array.from(cards).map(c => c.dataset.name || c.textContent); + return { count: cards.length, names }; +} +""") +``` + +### Check API Response + +```python +# Trigger API call +evaluate_script(function=""" +() => window.loraApiCallPromise = fetch('/loras/api/list').then(r => r.json()) +""") + +# Wait and get result +import time +time.sleep(1) + +result = evaluate_script(function=""" +async () => await window.loraApiCallPromise +""") +``` + +### Monitor Console for Errors + +```python +# Before test: clear console (navigate reloads) +navigate_page(type="reload") + +# ... perform actions ... + +# Check for errors +errors = list_console_messages(types=["error"]) +assert len(errors) == 0, f"Console errors: {errors}" +``` diff --git a/.agents/skills/lora-manager-e2e/references/test-scenarios.md b/.agents/skills/lora-manager-e2e/references/test-scenarios.md new file mode 100644 index 00000000..54dbb497 --- /dev/null +++ b/.agents/skills/lora-manager-e2e/references/test-scenarios.md @@ -0,0 +1,272 @@ +# LoRa Manager E2E Test Scenarios + +This document provides detailed test scenarios for end-to-end validation of LoRa Manager features. + +## Table of Contents + +1. [LoRA List Page](#lora-list-page) +2. [Model Details](#model-details) +3. [Recipes](#recipes) +4. [Settings](#settings) +5. [Import/Export](#importexport) + +--- + +## LoRA List Page + +### Scenario: Page Load and Display + +**Objective**: Verify the LoRA list page loads correctly and displays models. + +**Steps**: +1. Navigate to `http://127.0.0.1:8188/loras` +2. Wait for page title "LoRAs" to appear +3. Take snapshot to verify: + - Header with "LoRAs" title is visible + - Search/filter controls are present + - Grid/list view toggle exists + - LoRA cards are displayed (if models exist) + - Pagination controls (if applicable) + +**Expected Result**: Page loads without errors, UI elements are present. + +### Scenario: Search Functionality + +**Objective**: Verify search filters LoRA models correctly. + +**Steps**: +1. Ensure at least one LoRA exists with known name (e.g., "test-character") +2. Navigate to LoRA list page +3. Enter search term in search box: "test" +4. Press Enter or click search button +5. Wait for results to update + +**Expected Result**: Only LoRAs matching search term are displayed. + +**Verification Script**: +```python +# After search, verify filtered results +evaluate_script(function=""" +() => { + const cards = document.querySelectorAll('.lora-card'); + const names = Array.from(cards).map(c => c.dataset.name); + return { count: cards.length, names }; +} +""") +``` + +### Scenario: Filter by Tags + +**Objective**: Verify tag filtering works correctly. + +**Steps**: +1. Navigate to LoRA list page +2. Click on a tag (e.g., "character", "style") +3. Wait for filtered results + +**Expected Result**: Only LoRAs with selected tag are displayed. + +### Scenario: View Mode Toggle + +**Objective**: Verify grid/list view toggle works. + +**Steps**: +1. Navigate to LoRA list page +2. Click list view button +3. Verify list layout +4. Click grid view button +5. Verify grid layout + +**Expected Result**: View mode changes correctly, layout updates. + +--- + +## Model Details + +### Scenario: Open Model Details + +**Objective**: Verify clicking a LoRA opens its details. + +**Steps**: +1. Navigate to LoRA list page +2. Click on a LoRA card +3. Wait for details panel/modal to open + +**Expected Result**: Details panel shows: +- Model name +- Preview image +- Metadata (trigger words, tags, etc.) +- Action buttons (edit, delete, etc.) + +### Scenario: Edit Model Metadata + +**Objective**: Verify metadata editing works end-to-end. + +**Steps**: +1. Open a LoRA's details +2. Click "Edit" button +3. Modify trigger words field +4. Add/remove tags +5. Save changes +6. Refresh page +7. Reopen the same LoRA + +**Expected Result**: Changes persist after refresh. + +### Scenario: Delete Model + +**Objective**: Verify model deletion works. + +**Steps**: +1. Open a LoRA's details +2. Click "Delete" button +3. Confirm deletion in dialog +4. Wait for removal + +**Expected Result**: Model removed from list, success message shown. + +--- + +## Recipes + +### Scenario: Recipe List Display + +**Objective**: Verify recipes page loads and displays recipes. + +**Steps**: +1. Navigate to `http://127.0.0.1:8188/recipes` +2. Wait for "Recipes" title +3. Take snapshot + +**Expected Result**: Recipe list displayed with cards/items. + +### Scenario: Create New Recipe + +**Objective**: Verify recipe creation workflow. + +**Steps**: +1. Navigate to recipes page +2. Click "New Recipe" button +3. Fill recipe form: + - Name: "Test Recipe" + - Description: "E2E test recipe" + - Add LoRA models +4. Save recipe +5. Verify recipe appears in list + +**Expected Result**: New recipe created and displayed. + +### Scenario: Apply Recipe + +**Objective**: Verify applying a recipe to ComfyUI. + +**Steps**: +1. Open a recipe +2. Click "Apply" or "Load in ComfyUI" +3. Verify action completes + +**Expected Result**: Recipe applied successfully. + +--- + +## Settings + +### Scenario: Settings Page Load + +**Objective**: Verify settings page displays correctly. + +**Steps**: +1. Navigate to `http://127.0.0.1:8188/settings` +2. Wait for "Settings" title +3. Take snapshot + +**Expected Result**: Settings form with various options displayed. + +### Scenario: Change Setting and Restart + +**Objective**: Verify settings persist after restart. + +**Steps**: +1. Navigate to settings page +2. Change a setting (e.g., default view mode) +3. Save settings +4. Restart server: `python scripts/start_server.py --restart --wait` +5. Refresh browser page +6. Navigate to settings + +**Expected Result**: Changed setting value persists. + +--- + +## Import/Export + +### Scenario: Export Models List + +**Objective**: Verify export functionality. + +**Steps**: +1. Navigate to LoRA list +2. Click "Export" button +3. Select format (JSON/CSV) +4. Download file + +**Expected Result**: File downloaded with correct data. + +### Scenario: Import Models + +**Objective**: Verify import functionality. + +**Steps**: +1. Prepare import file +2. Navigate to import page +3. Upload file +4. Verify import results + +**Expected Result**: Models imported successfully, confirmation shown. + +--- + +## API Integration Tests + +### Scenario: Verify API Endpoints + +**Objective**: Verify backend API responds correctly. + +**Test via browser console**: +```javascript +// List LoRAs +fetch('/loras/api/list').then(r => r.json()).then(console.log) + +// Get LoRA details +fetch('/loras/api/detail/').then(r => r.json()).then(console.log) + +// Search LoRAs +fetch('/loras/api/search?q=test').then(r => r.json()).then(console.log) +``` + +**Expected Result**: APIs return valid JSON with expected structure. + +--- + +## Console Error Monitoring + +During all tests, monitor browser console for errors: + +```python +# Check for JavaScript errors +messages = list_console_messages(types=["error"]) +assert len(messages) == 0, f"Console errors found: {messages}" +``` + +## Network Request Verification + +Verify key API calls are made: + +```python +# List XHR requests +requests = list_network_requests(resourceTypes=["xhr", "fetch"]) + +# Look for specific endpoints +lora_list_requests = [r for r in requests if "/api/list" in r.get("url", "")] +assert len(lora_list_requests) > 0, "LoRA list API not called" +``` diff --git a/.agents/skills/lora-manager-e2e/scripts/example_e2e_test.py b/.agents/skills/lora-manager-e2e/scripts/example_e2e_test.py new file mode 100755 index 00000000..6b2c15bd --- /dev/null +++ b/.agents/skills/lora-manager-e2e/scripts/example_e2e_test.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +""" +Example E2E test demonstrating LoRa Manager testing workflow. + +This script shows how to: +1. Start the standalone server +2. Use Chrome DevTools MCP to interact with the UI +3. Verify functionality end-to-end + +Note: This is a template. Actual execution requires Chrome DevTools MCP. +""" + +import subprocess +import sys +import time + + +def run_test(): + """Run example E2E test flow.""" + + print("=" * 60) + print("LoRa Manager E2E Test Example") + print("=" * 60) + + # Step 1: Start server + print("\n[1/5] Starting LoRa Manager standalone server...") + result = subprocess.run( + [sys.executable, "start_server.py", "--port", "8188", "--wait", "--timeout", "30"], + capture_output=True, + text=True + ) + if result.returncode != 0: + print(f"Failed to start server: {result.stderr}") + return 1 + print("Server ready!") + + # Step 2: Open Chrome (manual step - show command) + print("\n[2/5] Open Chrome with debug mode:") + print("google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-lora-manager http://127.0.0.1:8188/loras") + print("(In actual test, this would be automated via MCP)") + + # Step 3: Navigate and verify page load + print("\n[3/5] Page Load Verification:") + print(""" + MCP Commands to execute: + 1. navigate_page(type="url", url="http://127.0.0.1:8188/loras") + 2. wait_for(text="LoRAs", timeout=10000) + 3. snapshot = take_snapshot() + """) + + # Step 4: Test search functionality + print("\n[4/5] Search Functionality Test:") + print(""" + MCP Commands to execute: + 1. fill(uid="search-input", value="test") + 2. press_key(key="Enter") + 3. wait_for(text="Results", timeout=5000) + 4. result = evaluate_script(function=""" + () => { + const cards = document.querySelectorAll('.lora-card'); + return { count: cards.length }; + } + """) + """) + + # Step 5: Verify API + print("\n[5/5] API Verification:") + print(""" + MCP Commands to execute: + 1. api_result = evaluate_script(function=""" + async () => { + const response = await fetch('/loras/api/list'); + const data = await response.json(); + return { count: data.length, status: response.status }; + } + """) + 2. Verify api_result['status'] == 200 + """) + + print("\n" + "=" * 60) + print("Test flow completed!") + print("=" * 60) + + return 0 + + +def example_restart_flow(): + """Example: Testing configuration change that requires restart.""" + + print("\n" + "=" * 60) + print("Example: Server Restart Flow") + print("=" * 60) + + print(""" + Scenario: Change setting and verify after restart + + Steps: + 1. Navigate to settings page + - navigate_page(type="url", url="http://127.0.0.1:8188/settings") + + 2. Change a setting (e.g., theme) + - fill(uid="theme-select", value="dark") + - click(uid="save-settings-button") + + 3. Restart server + - subprocess.run([python, "start_server.py", "--restart", "--wait"]) + + 4. Refresh browser + - navigate_page(type="reload", ignoreCache=True) + - wait_for(text="LoRAs", timeout=15000) + + 5. Verify setting persisted + - navigate_page(type="url", url="http://127.0.0.1:8188/settings") + - theme = evaluate_script(function="() => document.querySelector('#theme-select').value") + - assert theme == "dark" + """) + + +def example_modal_interaction(): + """Example: Testing modal dialog interaction.""" + + print("\n" + "=" * 60) + print("Example: Modal Dialog Interaction") + print("=" * 60) + + print(""" + Scenario: Add new LoRA via modal + + Steps: + 1. Open modal + - click(uid="add-lora-button") + - wait_for(text="Add LoRA", timeout=3000) + + 2. Fill form + - fill_form(elements=[ + {"uid": "lora-name", "value": "Test Character"}, + {"uid": "lora-path", "value": "/models/test.safetensors"}, + ]) + + 3. Submit + - click(uid="modal-submit-button") + + 4. Verify success + - wait_for(text="Successfully added", timeout=5000) + - snapshot = take_snapshot() + """) + + +def example_network_monitoring(): + """Example: Network request monitoring.""" + + print("\n" + "=" * 60) + print("Example: Network Request Monitoring") + print("=" * 60) + + print(""" + Scenario: Verify API calls during user interaction + + Steps: + 1. Clear network log (implicit on navigation) + - navigate_page(type="url", url="http://127.0.0.1:8188/loras") + + 2. Perform action that triggers API call + - fill(uid="search-input", value="character") + - press_key(key="Enter") + + 3. List network requests + - requests = list_network_requests(resourceTypes=["xhr", "fetch"]) + + 4. Find search API call + - search_requests = [r for r in requests if "/api/search" in r.get("url", "")] + - assert len(search_requests) > 0, "Search API was not called" + + 5. Get request details + - if search_requests: + details = get_network_request(reqid=search_requests[0]["reqid"]) + - Verify request method, response status, etc. + """) + + +if __name__ == "__main__": + print("LoRa Manager E2E Test Examples\n") + print("This script demonstrates E2E testing patterns.\n") + print("Note: Actual execution requires Chrome DevTools MCP connection.\n") + + run_test() + example_restart_flow() + example_modal_interaction() + example_network_monitoring() + + print("\n" + "=" * 60) + print("All examples shown!") + print("=" * 60) diff --git a/.agents/skills/lora-manager-e2e/scripts/start_server.py b/.agents/skills/lora-manager-e2e/scripts/start_server.py new file mode 100755 index 00000000..bb1716f7 --- /dev/null +++ b/.agents/skills/lora-manager-e2e/scripts/start_server.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +""" +Start or restart LoRa Manager standalone server for E2E testing. +""" + +import argparse +import subprocess +import sys +import time +import socket +import signal +import os + + +def find_server_process(port: int) -> list[int]: + """Find PIDs of processes listening on the given port.""" + try: + result = subprocess.run( + ["lsof", "-ti", f":{port}"], + capture_output=True, + text=True, + check=False + ) + if result.returncode == 0 and result.stdout.strip(): + return [int(pid) for pid in result.stdout.strip().split("\n") if pid] + except FileNotFoundError: + # lsof not available, try netstat + try: + result = subprocess.run( + ["netstat", "-tlnp"], + capture_output=True, + text=True, + check=False + ) + pids = [] + for line in result.stdout.split("\n"): + if f":{port}" in line: + parts = line.split() + for part in parts: + if "/" in part: + try: + pid = int(part.split("/")[0]) + pids.append(pid) + except ValueError: + pass + return pids + except FileNotFoundError: + pass + return [] + + +def kill_server(port: int) -> None: + """Kill processes using the specified port.""" + pids = find_server_process(port) + for pid in pids: + try: + os.kill(pid, signal.SIGTERM) + print(f"Sent SIGTERM to process {pid}") + except ProcessLookupError: + pass + + # Wait for processes to terminate + time.sleep(1) + + # Force kill if still running + pids = find_server_process(port) + for pid in pids: + try: + os.kill(pid, signal.SIGKILL) + print(f"Sent SIGKILL to process {pid}") + except ProcessLookupError: + pass + + +def is_server_ready(port: int, timeout: float = 0.5) -> bool: + """Check if server is accepting connections.""" + try: + with socket.create_connection(("127.0.0.1", port), timeout=timeout): + return True + except (socket.timeout, ConnectionRefusedError, OSError): + return False + + +def wait_for_server(port: int, timeout: int = 30) -> bool: + """Wait for server to become ready.""" + start = time.time() + while time.time() - start < timeout: + if is_server_ready(port): + return True + time.sleep(0.5) + return False + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Start LoRa Manager standalone server for E2E testing" + ) + parser.add_argument( + "--port", + type=int, + default=8188, + help="Server port (default: 8188)" + ) + parser.add_argument( + "--restart", + action="store_true", + help="Kill existing server before starting" + ) + parser.add_argument( + "--wait", + action="store_true", + help="Wait for server to be ready before exiting" + ) + parser.add_argument( + "--timeout", + type=int, + default=30, + help="Timeout for waiting (default: 30)" + ) + + args = parser.parse_args() + + # Get project root (parent of .agents directory) + script_dir = os.path.dirname(os.path.abspath(__file__)) + skill_dir = os.path.dirname(script_dir) + project_root = os.path.dirname(os.path.dirname(os.path.dirname(skill_dir))) + + # Restart if requested + if args.restart: + print(f"Killing existing server on port {args.port}...") + kill_server(args.port) + time.sleep(1) + + # Check if already running + if is_server_ready(args.port): + print(f"Server already running on port {args.port}") + return 0 + + # Start server + print(f"Starting LoRa Manager standalone server on port {args.port}...") + cmd = [sys.executable, "standalone.py", "--port", str(args.port)] + + # Start in background + process = subprocess.Popen( + cmd, + cwd=project_root, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + start_new_session=True + ) + + print(f"Server process started with PID {process.pid}") + + # Wait for ready if requested + if args.wait: + print(f"Waiting for server to be ready (timeout: {args.timeout}s)...") + if wait_for_server(args.port, args.timeout): + print(f"Server ready at http://127.0.0.1:{args.port}/loras") + return 0 + else: + print(f"Timeout waiting for server") + return 1 + + print(f"Server starting at http://127.0.0.1:{args.port}/loras") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.agents/skills/lora-manager-e2e/scripts/wait_for_server.py b/.agents/skills/lora-manager-e2e/scripts/wait_for_server.py new file mode 100755 index 00000000..170ff684 --- /dev/null +++ b/.agents/skills/lora-manager-e2e/scripts/wait_for_server.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +""" +Wait for LoRa Manager server to become ready. +""" + +import argparse +import socket +import sys +import time + + +def is_server_ready(port: int, timeout: float = 0.5) -> bool: + """Check if server is accepting connections.""" + try: + with socket.create_connection(("127.0.0.1", port), timeout=timeout): + return True + except (socket.timeout, ConnectionRefusedError, OSError): + return False + + +def wait_for_server(port: int, timeout: int = 30) -> bool: + """Wait for server to become ready.""" + start = time.time() + while time.time() - start < timeout: + if is_server_ready(port): + return True + time.sleep(0.5) + return False + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Wait for LoRa Manager server to become ready" + ) + parser.add_argument( + "--port", + type=int, + default=8188, + help="Server port (default: 8188)" + ) + parser.add_argument( + "--timeout", + type=int, + default=30, + help="Timeout in seconds (default: 30)" + ) + + args = parser.parse_args() + + print(f"Waiting for server on port {args.port} (timeout: {args.timeout}s)...") + + if wait_for_server(args.port, args.timeout): + print(f"Server ready at http://127.0.0.1:{args.port}/loras") + return 0 + else: + print(f"Timeout: Server not ready after {args.timeout}s") + return 1 + + +if __name__ == "__main__": + sys.exit(main())