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
5 changed files with 513 additions and 412 deletions
|
|
@ -1,165 +1,7 @@
|
|||
import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'
|
||||
import { chromium, Browser, 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')
|
||||
|
||||
// Get MCP servers to test (ports determined at runtime)
|
||||
function getMCPServers() {
|
||||
try {
|
||||
const stateData = readFileSync(testStateFile, 'utf-8')
|
||||
const state = JSON.parse(stateData)
|
||||
|
||||
if (!state.honoPort) {
|
||||
throw new Error('hono-mcp port not found in test state')
|
||||
}
|
||||
|
||||
if (!state.cfAgentsPort) {
|
||||
throw new Error('cf-agents port not found in test state')
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
name: 'hono-mcp',
|
||||
url: `http://localhost:${state.honoPort}/mcp`,
|
||||
expectedTools: 1, // Minimum expected tools count
|
||||
},
|
||||
{
|
||||
name: 'cf-agents',
|
||||
url: `http://localhost:${state.cfAgentsPort}/public/mcp`,
|
||||
expectedTools: 1, // Minimum expected tools count
|
||||
},
|
||||
{
|
||||
name: 'cf-agents-sse',
|
||||
url: `http://localhost:${state.cfAgentsPort}/public/sse`,
|
||||
expectedTools: 1, // Minimum expected tools count
|
||||
},
|
||||
{
|
||||
name: 'cf-agents-auth',
|
||||
url: `http://localhost:${state.cfAgentsPort}/mcp`,
|
||||
expectedTools: 1, // Minimum expected tools count
|
||||
},
|
||||
{
|
||||
name: 'cf-agents-auth-sse',
|
||||
url: `http://localhost:${state.cfAgentsPort}/sse`,
|
||||
expectedTools: 1, // Minimum expected tools count
|
||||
},
|
||||
]
|
||||
} catch (error) {
|
||||
throw new Error(`Test environment not properly initialized: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function connectToMCPServer(
|
||||
page: Page,
|
||||
serverUrl: string,
|
||||
transportType: 'auto' | 'http' | 'sse' = 'auto',
|
||||
): Promise<{ success: boolean; tools: string[]; debugLog: string }> {
|
||||
// Navigate to the inspector
|
||||
const stateData = readFileSync(testStateFile, 'utf-8')
|
||||
const state = JSON.parse(stateData)
|
||||
|
||||
if (!state.staticPort) {
|
||||
throw new Error('Static server port not available - state: ' + JSON.stringify(state))
|
||||
}
|
||||
const staticPort = state.staticPort
|
||||
|
||||
await page.goto(`http://localhost:${staticPort}`)
|
||||
|
||||
// Wait for the page to load
|
||||
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 (max 10 seconds)
|
||||
await page.waitForTimeout(1000) // Initial wait
|
||||
|
||||
// Check for connection status
|
||||
let attempts = 0
|
||||
const maxAttempts = 20 // 10 seconds total (500ms * 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 {
|
||||
// Look for tool cards in the tools container
|
||||
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,
|
||||
}
|
||||
}
|
||||
import { SERVER_CONFIGS } from './server-configs.js'
|
||||
import { getTestState, connectToMCPServer, cleanupProcess } from './test-utils.js'
|
||||
|
||||
describe('MCP Connection Integration Tests', () => {
|
||||
let browser: Browser
|
||||
|
|
@ -176,33 +18,21 @@ describe('MCP Connection Integration Tests', () => {
|
|||
await browser.close()
|
||||
}
|
||||
|
||||
// Force cleanup before Vitest exits - don't throw errors
|
||||
// Force cleanup before Vitest exits
|
||||
const state = globalThis.__INTEGRATION_TEST_STATE__
|
||||
try {
|
||||
if (state?.honoServer && !state.honoServer.killed) {
|
||||
console.log('🔥 Force cleanup before test exit...')
|
||||
state.honoServer.kill('SIGKILL')
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors - process might already be dead
|
||||
if (state?.honoServer) {
|
||||
cleanupProcess(state.honoServer, 'hono-mcp')
|
||||
}
|
||||
|
||||
try {
|
||||
if (state?.cfAgentsServer && !state.cfAgentsServer.killed) {
|
||||
console.log('🔥 Force cleanup cf-agents server...')
|
||||
state.cfAgentsServer.kill('SIGKILL')
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors - process might already be dead
|
||||
if (state?.cfAgentsServer) {
|
||||
cleanupProcess(state.cfAgentsServer, 'cf-agents')
|
||||
}
|
||||
|
||||
try {
|
||||
if (state?.staticServer) {
|
||||
if (state?.staticServer) {
|
||||
try {
|
||||
state.staticServer.close()
|
||||
state.staticServer.closeAllConnections?.()
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -225,59 +55,59 @@ describe('MCP Connection Integration Tests', () => {
|
|||
}
|
||||
})
|
||||
|
||||
const testScenarios = [
|
||||
// Hono examples (MCP only)
|
||||
{ serverName: 'hono-mcp', transportType: 'auto' as const },
|
||||
{ serverName: 'hono-mcp', transportType: 'http' as const },
|
||||
// Test each server configuration
|
||||
for (const serverConfig of SERVER_CONFIGS) {
|
||||
describe(`${serverConfig.name} server`, () => {
|
||||
// Test each endpoint for this server
|
||||
for (const endpoint of serverConfig.endpoints) {
|
||||
// Test each transport type for this endpoint
|
||||
for (const transportType of endpoint.transportTypes) {
|
||||
test(`should connect to ${endpoint.path} with ${transportType} transport`, async () => {
|
||||
const testState = getTestState()
|
||||
const port = testState[serverConfig.portKey]
|
||||
|
||||
// Agents, no auth
|
||||
{ serverName: 'cf-agents', transportType: 'auto' as const },
|
||||
{ serverName: 'cf-agents', transportType: 'http' as const },
|
||||
{ serverName: 'cf-agents-sse', transportType: 'sse' as const },
|
||||
{ serverName: 'cf-agents-sse', transportType: 'auto' as const },
|
||||
if (!port) {
|
||||
throw new Error(`Port not found for ${serverConfig.name} (${serverConfig.portKey})`)
|
||||
}
|
||||
|
||||
// Agents, with auth
|
||||
{ serverName: 'cf-agents-auth', transportType: 'auto' as const },
|
||||
{ serverName: 'cf-agents-auth', transportType: 'http' as const },
|
||||
{ serverName: 'cf-agents-auth-sse', transportType: 'sse' as const },
|
||||
{ serverName: 'cf-agents-auth-sse', transportType: 'auto' as const },
|
||||
]
|
||||
const serverUrl = `http://localhost:${port}${endpoint.path}`
|
||||
console.log(`\n🔗 Testing connection to ${serverConfig.name} at ${serverUrl} with ${transportType} transport`)
|
||||
|
||||
test.each(testScenarios)(
|
||||
'should connect to $serverName with $transportType transport',
|
||||
async ({ serverName, transportType }) => {
|
||||
const servers = getMCPServers()
|
||||
const server = servers.find((s) => s.name === serverName)
|
||||
const result = await connectToMCPServer(page, serverUrl, transportType)
|
||||
|
||||
if (!server) {
|
||||
throw new Error(`Server ${serverName} not found. Available servers: ${servers.map((s) => s.name).join(', ')}`)
|
||||
}
|
||||
if (result.success) {
|
||||
console.log(`✅ Successfully connected to ${serverConfig.name}`)
|
||||
console.log(`📋 Available tools (${result.tools.length}):`)
|
||||
result.tools.forEach((tool, index) => {
|
||||
console.log(` ${index + 1}. ${tool}`)
|
||||
})
|
||||
|
||||
console.log(`\n🔗 Testing connection to ${server.name} at ${server.url} with ${transportType} transport`)
|
||||
// Verify connection success
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.tools.length).toBeGreaterThanOrEqual(serverConfig.expectedTools)
|
||||
} else {
|
||||
console.log(`❌ Failed to connect to ${serverConfig.name}`)
|
||||
if (result.debugLog) {
|
||||
console.log(`🐛 Debug log:`)
|
||||
console.log(result.debugLog)
|
||||
}
|
||||
|
||||
const result = await connectToMCPServer(page, server.url, transportType)
|
||||
// Check if this is an expected failure case
|
||||
const isExpectedFailure = endpoint.path.endsWith('/sse') && transportType === 'auto'
|
||||
|
||||
if (result.success) {
|
||||
console.log(`✅ Successfully connected to ${server.name}`)
|
||||
console.log(`📋 Available tools (${result.tools.length}):`)
|
||||
result.tools.forEach((tool, index) => {
|
||||
console.log(` ${index + 1}. ${tool}`)
|
||||
})
|
||||
|
||||
// Verify connection success
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.tools.length).toBeGreaterThanOrEqual(server.expectedTools)
|
||||
} else {
|
||||
console.log(`❌ Failed to connect to ${server.name}`)
|
||||
if (result.debugLog) {
|
||||
console.log(`🐛 Debug log:`)
|
||||
console.log(result.debugLog)
|
||||
if (isExpectedFailure) {
|
||||
console.log(`ℹ️ Expected failure: SSE endpoint with auto transport`)
|
||||
expect(result.success).toBe(false)
|
||||
} else {
|
||||
// Fail the test with detailed information
|
||||
throw new Error(
|
||||
`Expected to connect to ${serverConfig.name} with ${transportType} transport but failed. Debug log: ${result.debugLog}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}, 45000)
|
||||
}
|
||||
|
||||
// Fail the test with detailed information
|
||||
throw new Error(`Expected to connect to ${server.name} with ${transportType} transport but failed. Debug log: ${result.debugLog}`)
|
||||
}
|
||||
},
|
||||
45000,
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
|||
114
test/integration/mcp-connection.test.ts.old
Normal file
114
test/integration/mcp-connection.test.ts.old
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'
|
||||
import { chromium, Browser, Page } from 'playwright'
|
||||
import { SERVER_CONFIGS } from './server-configs.js'
|
||||
import { getTestState, connectToMCPServer, cleanupProcess } from './test-utils.js'
|
||||
|
||||
describe('MCP Connection Integration Tests', () => {
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
|
||||
beforeAll(async () => {
|
||||
const headless = process.env.HEADLESS === 'true'
|
||||
console.log(`🌐 Launching browser in ${headless ? 'headless' : 'headed'} mode`)
|
||||
browser = await chromium.launch({ headless })
|
||||
}, 30000)
|
||||
|
||||
afterAll(async () => {
|
||||
if (browser) {
|
||||
await browser.close()
|
||||
}
|
||||
|
||||
// Force cleanup before Vitest exits
|
||||
const state = globalThis.__INTEGRATION_TEST_STATE__
|
||||
if (state?.honoServer) {
|
||||
cleanupProcess(state.honoServer, 'hono-mcp')
|
||||
}
|
||||
if (state?.cfAgentsServer) {
|
||||
cleanupProcess(state.cfAgentsServer, 'cf-agents')
|
||||
}
|
||||
if (state?.staticServer) {
|
||||
try {
|
||||
state.staticServer.close()
|
||||
state.staticServer.closeAllConnections?.()
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
const context = await browser.newContext()
|
||||
page = await context.newPage()
|
||||
|
||||
// Enable console logging for debugging
|
||||
page.on('console', (msg) => {
|
||||
if (msg.type() === 'error') {
|
||||
console.error(`Browser error: ${msg.text()}`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
if (page) {
|
||||
await page.close()
|
||||
await page.context().close()
|
||||
}
|
||||
})
|
||||
|
||||
// Test each server configuration
|
||||
for (const serverConfig of SERVER_CONFIGS) {
|
||||
describe(`${serverConfig.name} server`, () => {
|
||||
// Test each endpoint for this server
|
||||
for (const endpoint of serverConfig.endpoints) {
|
||||
// Test each transport type for this endpoint
|
||||
for (const transportType of endpoint.transportTypes) {
|
||||
test(`should connect to ${endpoint.path} with ${transportType} transport`, async () => {
|
||||
const testState = getTestState()
|
||||
const port = testState[serverConfig.portKey]
|
||||
|
||||
if (!port) {
|
||||
throw new Error(`Port not found for ${serverConfig.name} (${serverConfig.portKey})`)
|
||||
}
|
||||
|
||||
const serverUrl = `http://localhost:${port}${endpoint.path}`
|
||||
console.log(`\n🔗 Testing connection to ${serverConfig.name} at ${serverUrl} with ${transportType} transport`)
|
||||
|
||||
const result = await connectToMCPServer(page, serverUrl, transportType)
|
||||
|
||||
if (result.success) {
|
||||
console.log(`✅ Successfully connected to ${serverConfig.name}`)
|
||||
console.log(`📋 Available tools (${result.tools.length}):`)
|
||||
result.tools.forEach((tool, index) => {
|
||||
console.log(` ${index + 1}. ${tool}`)
|
||||
})
|
||||
|
||||
// Verify connection success
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.tools.length).toBeGreaterThanOrEqual(serverConfig.expectedTools)
|
||||
} else {
|
||||
console.log(`❌ Failed to connect to ${serverConfig.name}`)
|
||||
if (result.debugLog) {
|
||||
console.log(`🐛 Debug log:`)
|
||||
console.log(result.debugLog)
|
||||
}
|
||||
|
||||
// Check if this is an expected failure case
|
||||
const isExpectedFailure =
|
||||
endpoint.path.endsWith('/sse') && transportType === 'auto'
|
||||
|
||||
if (isExpectedFailure) {
|
||||
console.log(`ℹ️ Expected failure: SSE endpoint with auto transport`)
|
||||
expect(result.success).toBe(false)
|
||||
} else {
|
||||
// Fail the test with detailed information
|
||||
throw new Error(
|
||||
`Expected to connect to ${serverConfig.name} with ${transportType} transport but failed. Debug log: ${result.debugLog}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}, 45000)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
48
test/integration/server-configs.ts
Normal file
48
test/integration/server-configs.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { join, dirname } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { ServerConfig } from './test-utils.js'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const rootDir = join(__dirname, '../..')
|
||||
|
||||
/**
|
||||
* Configuration for all MCP servers to test
|
||||
*/
|
||||
export const SERVER_CONFIGS: ServerConfig[] = [
|
||||
{
|
||||
name: 'hono-mcp',
|
||||
directory: join(rootDir, 'examples/servers/hono-mcp'),
|
||||
portKey: 'honoPort',
|
||||
endpoints: [
|
||||
{
|
||||
path: '/mcp',
|
||||
transportTypes: ['auto', 'http'],
|
||||
},
|
||||
],
|
||||
expectedTools: 1,
|
||||
},
|
||||
{
|
||||
name: 'cf-agents',
|
||||
directory: join(rootDir, 'examples/servers/cf-agents'),
|
||||
portKey: 'cfAgentsPort',
|
||||
endpoints: [
|
||||
{
|
||||
path: '/mcp',
|
||||
transportTypes: ['auto', 'http'],
|
||||
},
|
||||
{
|
||||
path: '/sse',
|
||||
transportTypes: ['auto', 'sse'],
|
||||
},
|
||||
{
|
||||
path: '/public/mcp',
|
||||
transportTypes: ['auto', 'http'],
|
||||
},
|
||||
{
|
||||
path: '/public/sse',
|
||||
transportTypes: ['auto', 'sse'],
|
||||
},
|
||||
],
|
||||
expectedTools: 1,
|
||||
},
|
||||
]
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import { writeFileSync, mkdirSync, readFileSync } from 'fs'
|
|||
import { createServer, Server } from 'http'
|
||||
import { parse } from 'url'
|
||||
import { extname } from 'path'
|
||||
import { SERVER_CONFIGS } from '../integration/server-configs.js'
|
||||
import { findAvailablePortFromBase, spawnDevServer, waitForOutput, TestState, cleanupProcess } from '../integration/test-utils.js'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const rootDir = join(__dirname, '../..')
|
||||
|
|
@ -12,13 +14,10 @@ const testDir = join(__dirname, '..')
|
|||
const cacheDir = join(testDir, 'node_modules/.cache/use-mcp-tests')
|
||||
const testStateFile = join(cacheDir, 'test-state.json')
|
||||
|
||||
interface GlobalState {
|
||||
interface GlobalState extends TestState {
|
||||
honoServer?: ChildProcess
|
||||
cfAgentsServer?: ChildProcess
|
||||
staticServer?: Server
|
||||
staticPort?: number
|
||||
honoPort?: number
|
||||
cfAgentsPort?: number
|
||||
processGroupId?: number
|
||||
allChildProcesses?: Set<number>
|
||||
}
|
||||
|
|
@ -27,92 +26,6 @@ declare global {
|
|||
var __INTEGRATION_TEST_STATE__: GlobalState
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
async function checkPortAvailable(port: number): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const server = require('net').createServer()
|
||||
|
||||
server.listen(port, () => {
|
||||
server.close(() => {
|
||||
// Wait a bit for the port to fully close
|
||||
setTimeout(() => resolve(true), 100)
|
||||
})
|
||||
})
|
||||
|
||||
server.on('error', (err: any) => {
|
||||
console.log(`Port ${port} check failed: ${err.message}`)
|
||||
resolve(false)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function checkPortNotUsedByWrangler(port: number): Promise<boolean> {
|
||||
// First check if port is available at network level
|
||||
const networkAvailable = await checkPortAvailable(port)
|
||||
if (!networkAvailable) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Then check if there are wrangler processes using this port
|
||||
return new Promise((resolve) => {
|
||||
const { spawn } = require('child_process')
|
||||
const lsof = spawn('lsof', ['-ti', `:${port}`])
|
||||
|
||||
let output = ''
|
||||
lsof.stdout?.on('data', (data: Buffer) => {
|
||||
output += data.toString()
|
||||
})
|
||||
|
||||
lsof.on('close', () => {
|
||||
if (output.trim()) {
|
||||
// Port is in use
|
||||
resolve(false)
|
||||
} else {
|
||||
// Port is free
|
||||
resolve(true)
|
||||
}
|
||||
})
|
||||
|
||||
lsof.on('error', () => {
|
||||
// lsof failed, assume port is available
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function findAvailablePortFromBase(basePort: number): Promise<number> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
for (let port = basePort; port < basePort + 20; port++) {
|
||||
if (await checkPortNotUsedByWrangler(port)) {
|
||||
resolve(port)
|
||||
return
|
||||
}
|
||||
}
|
||||
reject(new Error(`No available ports found starting from ${basePort}. Please stop any existing wrangler/workerd processes.`))
|
||||
})
|
||||
}
|
||||
|
||||
function runCommand(command: string, args: string[], cwd: string, env?: Record<string, string>): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
|
|
@ -212,89 +125,43 @@ export default async function globalSetup() {
|
|||
const inspectorDir = join(rootDir, 'examples/inspector')
|
||||
await runCommand('pnpm', ['build'], inspectorDir, { NO_MINIFY: 'true' })
|
||||
|
||||
// Step 3: Find available port and start hono-mcp server
|
||||
console.log('🔍 Finding available port starting from 9901...')
|
||||
const honoPort = await findAvailablePortFromBase(9901)
|
||||
console.log(`📍 Using port ${honoPort} for hono-mcp server`)
|
||||
// Step 3: Start all configured MCP servers
|
||||
let basePort = 9901
|
||||
|
||||
console.log('🚀 Starting hono-mcp server...')
|
||||
const honoDir = join(rootDir, 'examples/servers/hono-mcp')
|
||||
const honoServer = spawn('pnpm', ['dev', `--port=${honoPort}`], {
|
||||
cwd: honoDir,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
shell: true,
|
||||
detached: false, // Keep in same process group initially
|
||||
})
|
||||
for (const serverConfig of SERVER_CONFIGS) {
|
||||
console.log(`🔍 Finding available port starting from ${basePort} for ${serverConfig.name}...`)
|
||||
const port = await findAvailablePortFromBase(basePort)
|
||||
console.log(`📍 Using port ${port} for ${serverConfig.name} server`)
|
||||
|
||||
// Track all child processes
|
||||
if (honoServer.pid) {
|
||||
state.allChildProcesses!.add(honoServer.pid)
|
||||
}
|
||||
console.log(`🚀 Starting ${serverConfig.name} server...`)
|
||||
const server = spawnDevServer(serverConfig.name, serverConfig.directory, port)
|
||||
|
||||
// Store the process group ID and port
|
||||
state.processGroupId = honoServer.pid
|
||||
state.honoPort = honoPort
|
||||
state.honoServer = honoServer
|
||||
|
||||
honoServer.stdout?.on('data', (data) => {
|
||||
console.log(`[hono-mcp] ${data.toString()}`)
|
||||
})
|
||||
|
||||
honoServer.stderr?.on('data', (data) => {
|
||||
console.log(`[hono-mcp] ${data.toString()}`)
|
||||
})
|
||||
|
||||
// Track when the process exits to remove it from our tracking
|
||||
honoServer.on('exit', () => {
|
||||
if (honoServer.pid) {
|
||||
state.allChildProcesses!.delete(honoServer.pid)
|
||||
// Track all child processes
|
||||
if (server.pid) {
|
||||
state.allChildProcesses!.add(server.pid)
|
||||
}
|
||||
})
|
||||
|
||||
// Wait for hono server to be ready
|
||||
await waitForOutput(honoServer, 'Ready on')
|
||||
state.honoServer = honoServer
|
||||
|
||||
// Step 4: Find available port and start cf-agents server
|
||||
console.log('🔍 Finding available port starting from 9902...')
|
||||
const cfAgentsPort = await findAvailablePortFromBase(9902)
|
||||
console.log(`📍 Using port ${cfAgentsPort} for cf-agents server`)
|
||||
|
||||
console.log('🚀 Starting cf-agents server...')
|
||||
const cfAgentsDir = join(rootDir, 'examples/servers/cf-agents')
|
||||
const cfAgentsServer = spawn('pnpm', ['dev', `--port=${cfAgentsPort}`], {
|
||||
cwd: cfAgentsDir,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
shell: true,
|
||||
detached: false,
|
||||
})
|
||||
|
||||
// Track all child processes
|
||||
if (cfAgentsServer.pid) {
|
||||
state.allChildProcesses!.add(cfAgentsServer.pid)
|
||||
}
|
||||
|
||||
state.cfAgentsPort = cfAgentsPort
|
||||
state.cfAgentsServer = cfAgentsServer
|
||||
|
||||
cfAgentsServer.stdout?.on('data', (data) => {
|
||||
console.log(`[cf-agents] ${data.toString()}`)
|
||||
})
|
||||
|
||||
cfAgentsServer.stderr?.on('data', (data) => {
|
||||
console.log(`[cf-agents] ${data.toString()}`)
|
||||
})
|
||||
|
||||
// Track when the process exits to remove it from our tracking
|
||||
cfAgentsServer.on('exit', () => {
|
||||
if (cfAgentsServer.pid) {
|
||||
state.allChildProcesses!.delete(cfAgentsServer.pid)
|
||||
// Store the port and server reference
|
||||
state[serverConfig.portKey] = port
|
||||
if (serverConfig.name === 'hono-mcp') {
|
||||
state.honoServer = server
|
||||
state.processGroupId = server.pid
|
||||
} else if (serverConfig.name === 'cf-agents') {
|
||||
state.cfAgentsServer = server
|
||||
}
|
||||
})
|
||||
|
||||
// Wait for cf-agents server to be ready
|
||||
await waitForOutput(cfAgentsServer, 'Ready on')
|
||||
state.cfAgentsServer = cfAgentsServer
|
||||
// Track when the process exits to remove it from our tracking
|
||||
server.on('exit', () => {
|
||||
if (server.pid) {
|
||||
state.allChildProcesses!.delete(server.pid)
|
||||
}
|
||||
})
|
||||
|
||||
// Wait for server to be ready
|
||||
await waitForOutput(server, 'Ready on')
|
||||
|
||||
basePort += 1 // Increment base port for next server
|
||||
}
|
||||
|
||||
// Step 5: Start simple static file server for inspector
|
||||
console.log('🌐 Starting static file server for inspector...')
|
||||
|
|
@ -354,18 +221,16 @@ export default async function globalSetup() {
|
|||
|
||||
// Write state to file for tests to read
|
||||
mkdirSync(cacheDir, { recursive: true })
|
||||
writeFileSync(
|
||||
testStateFile,
|
||||
JSON.stringify(
|
||||
{
|
||||
honoPort: state.honoPort,
|
||||
cfAgentsPort: state.cfAgentsPort,
|
||||
staticPort: state.staticPort,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
const testState: TestState = {
|
||||
staticPort: state.staticPort,
|
||||
}
|
||||
|
||||
// Add port information for each configured server
|
||||
for (const serverConfig of SERVER_CONFIGS) {
|
||||
testState[serverConfig.portKey] = state[serverConfig.portKey]
|
||||
}
|
||||
|
||||
writeFileSync(testStateFile, JSON.stringify(testState, null, 2))
|
||||
|
||||
console.log('✅ Integration test environment ready!')
|
||||
} catch (error) {
|
||||
|
|
@ -373,10 +238,10 @@ export default async function globalSetup() {
|
|||
|
||||
// Cleanup on failure
|
||||
if (state.honoServer) {
|
||||
state.honoServer.kill()
|
||||
cleanupProcess(state.honoServer, 'hono-mcp')
|
||||
}
|
||||
if (state.cfAgentsServer) {
|
||||
state.cfAgentsServer.kill()
|
||||
cleanupProcess(state.cfAgentsServer, 'cf-agents')
|
||||
}
|
||||
if (state.staticServer) {
|
||||
state.staticServer.close()
|
||||
|
|
|
|||
Loading…
Reference in a new issue