/**
* Ask-Multiple-Choice Tool E2E Test
*
* Isolated test for the ask-multiple-choice tool functionality.
* Tests complex multi-question scenarios with priority and comments.
*/
import { spawn, ChildProcess } from 'child_process';
import { join } from 'path';
import fetch from 'node-fetch';
describe('Ask-Multiple-Choice Tool E2E', () => {
let serverProcess: ChildProcess | null = null;
const serverPath = join(__dirname, '../../../dist/askme-server/main.js');
const testPort = 3001; // Use different port to avoid conflicts
let requestId: string | null = null;
beforeEach(async () => {
serverProcess = spawn('node', [serverPath, '--port', testPort.toString(), '--exit-after-command'], {
stdio: ['pipe', 'pipe', 'pipe'],
env: {
...process.env,
ASK_ME_MCP_DEBUG: '1'
}
});
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Server failed to start within 10 seconds'));
}, 10000);
serverProcess!.stderr!.on('data', (data) => {
const message = data.toString();
if (message.includes('Server ready for connections')) {
clearTimeout(timeout);
resolve();
} else if (message.includes('EADDRINUSE')) {
clearTimeout(timeout);
reject(new Error(`Port ${testPort} already in use`));
}
});
serverProcess!.on('error', (error) => {
clearTimeout(timeout);
reject(error);
});
});
});
afterEach(async () => {
if (serverProcess) {
if (!serverProcess.killed) {
serverProcess.kill('SIGTERM');
}
await new Promise<void>((resolve) => {
const timeout = setTimeout(() => {
if (serverProcess && !serverProcess.killed) {
serverProcess.kill('SIGKILL');
}
resolve();
}, 3000);
serverProcess!.once('exit', () => {
clearTimeout(timeout);
resolve();
});
});
serverProcess = null;
}
});
test('should execute ask-multiple-choice tool with multiple questions', async () => {
// Initialize MCP connection
const initRequest = {
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {
roots: { listChanged: true },
sampling: {}
},
clientInfo: {
name: 'test-client',
version: '1.0.0'
}
}
};
const initPromise = new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Initialize timeout')), 5000);
serverProcess!.stdout!.on('data', (data) => {
const lines = data.toString().split('\n').filter(line => line.trim());
for (const line of lines) {
try {
const response = JSON.parse(line);
if (response.id === 1) {
clearTimeout(timeout);
resolve(response);
return;
}
} catch (e) { /* ignore */ }
}
});
});
serverProcess!.stdin!.write(JSON.stringify(initRequest) + '\n');
await initPromise;
// Call ask-multiple-choice tool with complex scenario
const toolRequest = {
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'ask-multiple-choice',
arguments: {
questions: [
{
text: 'Which deployment environments should we target for the next release?',
options: ['Development', 'Staging', 'Production', 'Beta Testing']
},
{
text: 'Which features should be included in this release?',
options: [
'User Authentication System',
'Dashboard Analytics',
'Mobile App Support',
'API Rate Limiting',
'Advanced Search'
]
},
{
text: 'What is the priority level for this release?',
options: ['Low', 'Medium', 'High', 'Critical']
}
]
}
}
};
const responsePromise = new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Tool call timeout')), 30000);
serverProcess!.stdout!.on('data', (data) => {
const lines = data.toString().split('\n').filter(line => line.trim());
for (const line of lines) {
try {
const response = JSON.parse(line);
if (response.id === 2) {
clearTimeout(timeout);
resolve(response);
return;
}
} catch (e) { /* ignore */ }
}
});
});
// Set up browser mock for complex multiple choice response
const browserMockPromise = (async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
try {
// Use EventSource-style polling instead of streaming for Node.js compatibility
let requestFound = false;
let attempts = 0;
const maxAttempts = 10;
while (!requestFound && attempts < maxAttempts) {
attempts++;
try {
const response = await fetch(`http://localhost:${testPort}/mcp/browser-events`);
const responseText = await response.text();
const lines = responseText.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const eventData = JSON.parse(line.slice(6));
if (eventData.type === 'new_request' && eventData.data.type === 'ask-multiple-choice') {
requestId = eventData.data.id;
requestFound = true;
break;
}
} catch (e) { /* ignore malformed JSON */ }
}
}
} catch (e) {
// Ignore connection errors and retry
}
if (!requestFound) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
if (requestFound && requestId) {
// Simulate complex human response with selections, priorities, and comments
const humanResponse = {
responses: [
{
questionIndex: 0,
selections: [
{
text: 'Staging',
selected: true,
priority: 'high',
comment: 'We need staging environment for proper testing before production'
},
{
text: 'Production',
selected: true,
priority: 'medium',
comment: 'Production deployment should happen after staging validation'
},
{
text: 'Development',
selected: false,
priority: 'low',
comment: 'Dev environment is already updated'
},
{
text: 'Beta Testing',
selected: true,
priority: 'high',
comment: 'Beta testing is crucial for user feedback'
}
]
},
{
questionIndex: 1,
selections: [
{
text: 'User Authentication System',
selected: true,
priority: 'high',
comment: 'Critical security feature, must be included'
},
{
text: 'Dashboard Analytics',
selected: true,
priority: 'medium',
comment: 'Important for user insights but not blocking'
},
{
text: 'Mobile App Support',
selected: false,
priority: 'low',
comment: 'Can be delayed to next release'
},
{
text: 'API Rate Limiting',
selected: true,
priority: 'high',
comment: 'Essential for system stability'
},
{
text: 'Advanced Search',
selected: false,
priority: 'low',
comment: 'Nice to have but not essential'
}
]
},
{
questionIndex: 2,
selections: [
{
text: 'High',
selected: true,
priority: 'high',
comment: 'This release contains critical security features'
}
]
}
],
completionStatus: 'drill-deeper',
generalComment: 'Overall, this release should focus on security and stability features. The authentication system and rate limiting are non-negotiable.'
};
await fetch(`http://localhost:${testPort}/mcp/response`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
requestId: requestId,
sessionId: 'demo',
response: humanResponse
})
});
}
} catch (error) {
console.error('Browser mock error:', error);
}
})();
// Execute both tool call and browser simulation
serverProcess!.stdin!.write(JSON.stringify(toolRequest) + '\n');
await Promise.all([browserMockPromise, new Promise(resolve => setTimeout(resolve, 2000))]);
const toolResponse = await responsePromise;
// Validate response structure
expect(toolResponse).toHaveProperty('result');
const result = (toolResponse as any).result;
expect(result).toHaveProperty('content');
expect(Array.isArray(result.content)).toBe(true);
expect(result.content.length).toBeGreaterThan(0);
const content = result.content[0];
expect(content).toHaveProperty('type', 'text');
expect(content).toHaveProperty('text');
expect(typeof content.text).toBe('string');
// Validate content includes human selections and comments
expect(content.text).toContain('Staging');
expect(content.text).toContain('Production');
expect(content.text).toContain('User Authentication System');
expect(content.text).toContain('API Rate Limiting');
expect(content.text).toContain('High');
// Validate comments are included
expect(content.text).toContain('proper testing before production');
expect(content.text).toContain('Critical security feature');
expect(content.text).toContain('critical security features');
// Validate completion status instruction
expect(content.text).toContain('follow-up questions');
expect(content.text).toContain('drill deeper');
// Validate priority information
expect(content.text).toContain('high');
expect(content.text).toContain('medium');
}, 35000);
test('should handle single question multiple choice', async () => {
// Initialize
const initRequest = {
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: { roots: { listChanged: true }, sampling: {} },
clientInfo: { name: 'test-client', version: '1.0.0' }
}
};
const initPromise = new Promise((resolve) => {
serverProcess!.stdout!.on('data', (data) => {
const lines = data.toString().split('\n').filter(line => line.trim());
for (const line of lines) {
try {
const response = JSON.parse(line);
if (response.id === 1) resolve(response);
} catch (e) { /* ignore */ }
}
});
});
serverProcess!.stdin!.write(JSON.stringify(initRequest) + '\n');
await initPromise;
// Simple single question
const toolRequest = {
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'ask-multiple-choice',
arguments: {
questions: [
{
text: 'Which programming language should we use?',
options: ['TypeScript', 'JavaScript', 'Python', 'Go']
}
]
}
}
};
const responsePromise = new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Timeout')), 20000);
serverProcess!.stdout!.on('data', (data) => {
const lines = data.toString().split('\n').filter(line => line.trim());
for (const line of lines) {
try {
const response = JSON.parse(line);
if (response.id === 2) {
clearTimeout(timeout);
resolve(response);
}
} catch (e) { /* ignore */ }
}
});
});
// Simple browser mock using polling approach
setTimeout(async () => {
try {
await new Promise(resolve => setTimeout(resolve, 1000));
let requestFound = false;
let attempts = 0;
const maxAttempts = 10;
while (!requestFound && attempts < maxAttempts) {
attempts++;
try {
const response = await fetch(`http://localhost:${testPort}/mcp/browser-events`);
const responseText = await response.text();
const lines = responseText.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const eventData = JSON.parse(line.slice(6));
if (eventData.type === 'new_request') {
await fetch(`http://localhost:${testPort}/mcp/response`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
requestId: eventData.data.id,
sessionId: 'demo',
response: {
responses: [{
questionIndex: 0,
selections: [{
text: 'TypeScript',
selected: true,
priority: 'high',
comment: 'Better type safety and developer experience'
}]
}],
completionStatus: 'done'
}
})
});
requestFound = true;
return;
}
} catch (e) { /* ignore */ }
}
}
} catch (e) {
// Ignore connection errors and retry
}
if (!requestFound) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
} catch (error) {
console.error('Mock error:', error);
}
}, 500);
serverProcess!.stdin!.write(JSON.stringify(toolRequest) + '\n');
const toolResponse = await responsePromise;
expect(toolResponse).toHaveProperty('result');
const result = (toolResponse as any).result;
expect(result.content[0].text).toContain('TypeScript');
expect(result.content[0].text).toContain('type safety');
expect(result.content[0].text).toContain('Do not ask additional questions');
}, 25000);
test('should reject invalid multiple choice arguments', async () => {
// Initialize
const initRequest = {
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: { roots: { listChanged: true }, sampling: {} },
clientInfo: { name: 'test-client', version: '1.0.0' }
}
};
const initPromise = new Promise((resolve) => {
serverProcess!.stdout!.on('data', (data) => {
const lines = data.toString().split('\n').filter(line => line.trim());
for (const line of lines) {
try {
const response = JSON.parse(line);
if (response.id === 1) resolve(response);
} catch (e) { /* ignore */ }
}
});
});
serverProcess!.stdin!.write(JSON.stringify(initRequest) + '\n');
await initPromise;
// Invalid arguments - missing questions array
const toolRequest = {
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'ask-multiple-choice',
arguments: {
invalidField: 'test'
}
}
};
const responsePromise = new Promise((resolve) => {
serverProcess!.stdout!.on('data', (data) => {
const lines = data.toString().split('\n').filter(line => line.trim());
for (const line of lines) {
try {
const response = JSON.parse(line);
if (response.id === 2) resolve(response);
} catch (e) { /* ignore */ }
}
});
});
serverProcess!.stdin!.write(JSON.stringify(toolRequest) + '\n');
const toolResponse = await responsePromise;
expect(toolResponse).toHaveProperty('error');
const error = (toolResponse as any).error;
expect(error.code).toBe(-32602); // Invalid params
expect(error.message).toContain('questions');
}, 15000);
});