told amp to make my tests better (#14)
Co-authored-by: Glen Maddern <gmaddern@gmaddern.c.groq-143208.internal>
This commit is contained in:
parent
0ea0bc48c6
commit
d80a7e68b0
1 changed files with 513 additions and 412 deletions
244
test/integration/test-utils.ts
Normal file
244
test/integration/test-utils.ts
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
import { spawn, ChildProcess } from 'child_process'
|
||||
import { Page } from 'playwright'
|
||||
import { readFileSync } from 'fs'
|
||||
import { join, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const testDir = join(__dirname, '..')
|
||||
const testStateFile = join(testDir, 'node_modules/.cache/use-mcp-tests/test-state.json')
|
||||
|
||||
export interface TestState {
|
||||
honoPort?: number
|
||||
cfAgentsPort?: number
|
||||
staticPort?: number
|
||||
}
|
||||
|
||||
export interface ServerEndpoint {
|
||||
path: string
|
||||
transportTypes: ('auto' | 'http' | 'sse')[]
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
name: string
|
||||
directory: string
|
||||
portKey: keyof TestState
|
||||
endpoints: ServerEndpoint[]
|
||||
expectedTools: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the test state file containing server ports
|
||||
*/
|
||||
export function getTestState(): TestState {
|
||||
try {
|
||||
const stateData = readFileSync(testStateFile, 'utf-8')
|
||||
return JSON.parse(stateData)
|
||||
} catch (error) {
|
||||
throw new Error(`Test environment not properly initialized: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a process to output a specific string
|
||||
*/
|
||||
export function waitForOutput(process: ChildProcess, targetOutput: string, timeout = 30000): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error(`Timeout waiting for: ${targetOutput}`))
|
||||
}, timeout)
|
||||
|
||||
const onData = (data: Buffer) => {
|
||||
const output = data.toString()
|
||||
console.log(`[server output] ${output}`)
|
||||
if (output.includes(targetOutput)) {
|
||||
clearTimeout(timer)
|
||||
process.stdout?.off('data', onData)
|
||||
process.stderr?.off('data', onData)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout?.on('data', onData)
|
||||
process.stderr?.on('data', onData)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a port is available
|
||||
*/
|
||||
export async function checkPortAvailable(port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const server = require('net').createServer()
|
||||
|
||||
server.listen(port, () => {
|
||||
server.close(() => {
|
||||
setTimeout(() => resolve(true), 100)
|
||||
})
|
||||
})
|
||||
|
||||
server.on('error', (err: any) => {
|
||||
console.log(`Port ${port} check failed: ${err.message}`)
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an available port starting from a base port
|
||||
*/
|
||||
export function findAvailablePortFromBase(basePort: number): Promise<number> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
for (let port = basePort; port < basePort + 20; port++) {
|
||||
if (await checkPortAvailable(port)) {
|
||||
resolve(port)
|
||||
return
|
||||
}
|
||||
}
|
||||
reject(new Error(`No available ports found starting from ${basePort}`))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a server process in development mode
|
||||
*/
|
||||
export function spawnDevServer(serverName: string, directory: string, port: number, onOutput?: (data: string) => void): ChildProcess {
|
||||
const server = spawn('pnpm', ['dev', `--port=${port}`], {
|
||||
cwd: directory,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
shell: true,
|
||||
detached: false,
|
||||
})
|
||||
|
||||
server.stdout?.on('data', (data) => {
|
||||
const output = `[${serverName}] ${data.toString()}`
|
||||
console.log(output)
|
||||
onOutput?.(output)
|
||||
})
|
||||
|
||||
server.stderr?.on('data', (data) => {
|
||||
const output = `[${serverName}] ${data.toString()}`
|
||||
console.log(output)
|
||||
onOutput?.(output)
|
||||
})
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to an MCP server through the inspector UI
|
||||
*/
|
||||
export async function connectToMCPServer(
|
||||
page: Page,
|
||||
serverUrl: string,
|
||||
transportType: 'auto' | 'http' | 'sse' = 'auto',
|
||||
): Promise<{ success: boolean; tools: string[]; debugLog: string }> {
|
||||
const state = getTestState()
|
||||
|
||||
if (!state.staticPort) {
|
||||
throw new Error('Static server port not available - state: ' + JSON.stringify(state))
|
||||
}
|
||||
|
||||
await page.goto(`http://localhost:${state.staticPort}`)
|
||||
await page.waitForSelector('input[placeholder="Enter MCP server URL"]', { timeout: 10000 })
|
||||
|
||||
// Enter the server URL
|
||||
const urlInput = page.locator('input[placeholder="Enter MCP server URL"]')
|
||||
await urlInput.fill(serverUrl)
|
||||
|
||||
// Set transport type
|
||||
const transportSelect = page.locator('select')
|
||||
await transportSelect.selectOption(transportType)
|
||||
|
||||
// Click connect button
|
||||
const connectButton = page.locator('button:has-text("Connect")')
|
||||
await connectButton.click()
|
||||
|
||||
// Wait for connection attempt to complete
|
||||
await page.waitForTimeout(1000)
|
||||
|
||||
// Check for connection status
|
||||
let attempts = 0
|
||||
const maxAttempts = 20
|
||||
let isConnected = false
|
||||
|
||||
while (attempts < maxAttempts && !isConnected) {
|
||||
try {
|
||||
// Check if status badge shows "Connected"
|
||||
const statusBadge = page.locator('.px-2.py-1.rounded-full')
|
||||
if ((await statusBadge.count()) > 0) {
|
||||
const statusText = await statusBadge.textContent({ timeout: 500 })
|
||||
if (statusText?.toLowerCase().includes('connected')) {
|
||||
isConnected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Also check if tools count is > 0
|
||||
const toolsHeader = page.locator('h3:has-text("Available Tools")')
|
||||
if ((await toolsHeader.count()) > 0) {
|
||||
const toolsText = await toolsHeader.textContent()
|
||||
if (toolsText && /\d+/.test(toolsText)) {
|
||||
const toolsCount = parseInt(toolsText.match(/\d+/)?.[0] || '0')
|
||||
if (toolsCount > 0) {
|
||||
isConnected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Continue waiting
|
||||
}
|
||||
|
||||
await page.waitForTimeout(500)
|
||||
attempts++
|
||||
}
|
||||
|
||||
// Extract available tools
|
||||
const tools: string[] = []
|
||||
try {
|
||||
const toolCards = page.locator('.bg-white.rounded.border')
|
||||
const toolCount = await toolCards.count()
|
||||
|
||||
for (let i = 0; i < toolCount; i++) {
|
||||
const toolNameElement = toolCards.nth(i).locator('h4.font-bold.text-base.text-black')
|
||||
const toolName = await toolNameElement.textContent()
|
||||
if (toolName?.trim()) {
|
||||
tools.push(toolName.trim())
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not extract tools list:', e)
|
||||
}
|
||||
|
||||
// Extract debug log
|
||||
let debugLog = ''
|
||||
try {
|
||||
const debugContainer = page.locator('.h-32.overflow-y-auto.font-mono.text-xs')
|
||||
if ((await debugContainer.count()) > 0) {
|
||||
debugLog = (await debugContainer.first().textContent()) || ''
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not extract debug log:', e)
|
||||
}
|
||||
|
||||
return {
|
||||
success: isConnected,
|
||||
tools,
|
||||
debugLog,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up a child process safely
|
||||
*/
|
||||
export function cleanupProcess(process: ChildProcess, name: string): void {
|
||||
try {
|
||||
if (process && !process.killed) {
|
||||
console.log(`🔥 Cleaning up ${name} server...`)
|
||||
process.kill('SIGKILL')
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors - process might already be dead
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue