/**
* Choose-Next Tool E2E Test
*
* Isolated test for the choose-next tool functionality.
* Tests decision workflows with multiple options and user choices.
*/
import { spawn, ChildProcess } from 'child_process';
import { join } from 'path';
import fetch from 'node-fetch';
describe('Choose-Next Tool E2E', () => {
let serverProcess: ChildProcess | null = null;
const serverPath = join(__dirname, '../../../dist/askme-server/main.js');
const testPort = 3003; // Use different port
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 choose-next tool with option selection', 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 choose-next tool with development priority decision
const toolRequest = {
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'choose-next',
arguments: {
title: 'Choose Next Development Priority',
description: `# Development Priority Decision
We need to decide which feature to develop next for our Q4 release. Each option has different implications for our timeline, resources, and user impact.
## Context
- Current sprint ends in 2 weeks
- Team has 4 developers available
- Marketing wants to announce new feature at upcoming conference
- Customer feedback indicates high demand for security improvements
## Decision Impact
Your choice will determine our development focus for the next 6-8 weeks and affect our Q4 product roadmap.`,
options: [
{
id: 'user-auth',
title: 'Enhanced User Authentication',
description: 'Implement multi-factor authentication, SSO integration, and advanced security features. This addresses customer security concerns and compliance requirements.',
icon: '🔐'
},
{
id: 'analytics-dashboard',
title: 'Analytics Dashboard',
description: 'Build comprehensive analytics and reporting features with data visualization, custom metrics, and export capabilities for business users.',
icon: '📊'
},
{
id: 'mobile-app',
title: 'Mobile Application',
description: 'Develop native mobile apps for iOS and Android with core functionality, offline support, and push notifications.',
icon: '📱'
},
{
id: 'api-improvements',
title: 'API Performance & Documentation',
description: 'Optimize API endpoints, improve response times, add rate limiting, and create comprehensive API documentation with examples.',
icon: '⚡'
},
{
id: 'collaboration-tools',
title: 'Team Collaboration Features',
description: 'Add real-time collaboration, commenting, sharing, and workflow management tools for team productivity.',
icon: '👥'
}
]
}
}
};
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 option selection
const browserMockPromise = (async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
try {
const response = await fetch(`http://localhost:${testPort}/mcp/browser-events`);
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
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 === 'choose-next') {
requestId = eventData.data.id;
// Simulate user selecting enhanced authentication option
const humanResponse = {
action: 'selected',
selectedOption: {
id: 'user-auth',
title: 'Enhanced User Authentication',
description: 'Implement multi-factor authentication, SSO integration, and advanced security features. This addresses customer security concerns and compliance requirements.',
icon: '🔐'
},
message: 'After reviewing customer feedback and compliance requirements, enhanced authentication is the clear priority. The security features will differentiate us from competitors and address immediate customer pain points. This aligns with the upcoming conference announcement and builds trust with enterprise customers.'
};
await fetch(`http://localhost:${testPort}/mcp/response`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
requestId: requestId,
sessionId: 'demo',
response: humanResponse
})
});
reader.releaseLock();
return;
}
} catch (e) { /* ignore */ }
}
}
}
} catch (error) {
console.error('Browser mock error:', error);
}
})();
// Execute 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 selected option details
expect(content.text).toContain('Enhanced User Authentication');
expect(content.text).toContain('user-auth');
expect(content.text).toContain('multi-factor authentication');
expect(content.text).toContain('🔐');
// Validate user message is included
expect(content.text).toContain('customer feedback');
expect(content.text).toContain('compliance requirements');
expect(content.text).toContain('enterprise customers');
// Validate client instruction
expect(content.text).toContain('Proceed with implementing');
expect(content.text).toContain('chosen this path forward');
// Validate original context preservation
expect(content.text).toContain('Choose Next Development Priority');
}, 35000);
test('should handle user aborting the decision', 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 decision for abort test
const toolRequest = {
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'choose-next',
arguments: {
title: 'Technology Stack Decision',
description: 'Choose which technology stack to use for the new project.',
options: [
{
id: 'react',
title: 'React with TypeScript',
description: 'Modern React with TypeScript for type safety',
icon: '⚛️'
},
{
id: 'vue',
title: 'Vue.js Framework',
description: 'Progressive Vue.js framework with composition API',
icon: '💚'
}
]
}
}
};
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 */ }
}
});
});
// Browser mock that aborts the decision
setTimeout(async () => {
try {
await new Promise(resolve => setTimeout(resolve, 1000));
const response = await fetch(`http://localhost:${testPort}/mcp/browser-events`);
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
const readChunk = async (): Promise<void> => {
const { done, value } = await reader.read();
if (done) return;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const eventData = JSON.parse(line.slice(6));
if (eventData.type === 'new_request') {
// User aborts the decision
await fetch(`http://localhost:${testPort}/mcp/response`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
requestId: eventData.data.id,
sessionId: 'demo',
response: {
action: 'abort',
message: 'After further consideration, I think we need to gather more requirements before making this technology decision. The current options don\'t account for our team\'s existing expertise.'
}
})
});
reader.releaseLock();
return;
}
} catch (e) { /* ignore */ }
}
}
await readChunk();
};
await readChunk();
} 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;
const content = result.content[0];
expect(content.text).toContain('ABORTED');
expect(content.text).toContain('gather more requirements');
expect(content.text).toContain('STOP the current workflow');
expect(content.text).toContain('Technology Stack Decision');
}, 25000);
test('should handle user requesting new ideas', 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;
const toolRequest = {
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'choose-next',
arguments: {
title: 'Marketing Campaign Strategy',
description: 'Choose the marketing approach for our product launch.',
options: [
{
id: 'social-media',
title: 'Social Media Campaign',
description: 'Focus on social media marketing and influencer partnerships'
},
{
id: 'content-marketing',
title: 'Content Marketing',
description: 'Create blogs, whitepapers, and educational content'
}
]
}
}
};
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 */ }
}
});
});
// Browser mock requesting new ideas
setTimeout(async () => {
try {
await new Promise(resolve => setTimeout(resolve, 1000));
const response = await fetch(`http://localhost:${testPort}/mcp/browser-events`);
const reader = response.body!.getReader();
const decoder = new TextDecoder();
let buffer = '';
const readChunk = async (): Promise<void> => {
const { done, value } = await reader.read();
if (done) return;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
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: {
action: 'new-ideas',
message: 'These options seem too conventional. What about exploring guerrilla marketing, partnership opportunities, or community-driven approaches? I think we need more creative and innovative strategies.'
}
})
});
reader.releaseLock();
return;
}
} catch (e) { /* ignore */ }
}
}
await readChunk();
};
await readChunk();
} 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;
const content = result.content[0];
expect(content.text).toContain('NEW IDEAS');
expect(content.text).toContain('guerrilla marketing');
expect(content.text).toContain('Generate new, different');
expect(content.text).toContain('ask-one-question');
expect(content.text).toContain('fresh ideas');
}, 25000);
test('should reject invalid choose-next 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 required options array
const toolRequest = {
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: {
name: 'choose-next',
arguments: {
title: 'Test Decision',
description: 'Test description'
// Missing required 'options' field
}
}
};
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('options');
}, 15000);
});