import { spawn } from 'child_process';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
// Test utilities
class TestRunner {
constructor() {
this.tests = [];
this.passed = 0;
this.failed = 0;
}
test(name, fn) {
this.tests.push({ name, fn });
}
async run() {
console.log('Running Loop MCP Server Tests...\n');
for (const test of this.tests) {
try {
await test.fn();
this.passed++;
console.log(`✅ ${test.name}`);
} catch (error) {
this.failed++;
console.log(`❌ ${test.name}`);
console.log(` Error: ${error.message}`);
}
}
console.log(`\nTest Summary: ${this.passed} passed, ${this.failed} failed\n`);
return this.failed === 0;
}
}
// Test helper to create and connect client
async function createTestClient() {
const transport = new StdioClientTransport({
command: 'node',
args: ['server.js'],
});
const client = new Client(
{
name: 'test-client',
version: '1.0.0',
},
{
capabilities: {},
}
);
await client.connect(transport);
return client;
}
// Assertion helpers
function assertEqual(actual, expected, message = '') {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(`Assertion failed: ${message}\nExpected: ${JSON.stringify(expected)}\nActual: ${JSON.stringify(actual)}`);
}
}
function assertContains(text, substring, message = '') {
if (!text.includes(substring)) {
throw new Error(`Assertion failed: ${message}\nExpected "${text}" to contain "${substring}"`);
}
}
function assertThrows(fn, message = '') {
try {
fn();
throw new Error(`Expected function to throw: ${message}`);
} catch (e) {
// Expected
}
}
// Main test suite
async function runTests() {
const runner = new TestRunner();
let client;
// Setup before each test
async function setup() {
client = await createTestClient();
await client.callTool({ name: 'reset', arguments: {} });
}
// Cleanup after each test
async function cleanup() {
if (client) {
await client.close();
}
}
// Test 1: Initialize array with default batch size
runner.test('Initialize array with default batch size', async () => {
await setup();
const result = await client.callTool({
name: 'initialize_array',
arguments: {
array: [1, 2, 3, 4, 5],
task: 'Test task',
},
});
assertContains(result.content[0].text, 'Array initialized with 5 items');
assertContains(result.content[0].text, 'Batch size: 1');
await cleanup();
});
// Test 2: Initialize array with custom batch size
runner.test('Initialize array with custom batch size', async () => {
await setup();
const result = await client.callTool({
name: 'initialize_array',
arguments: {
array: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
task: 'Test task',
batchSize: 3,
},
});
assertContains(result.content[0].text, 'Array initialized with 10 items');
assertContains(result.content[0].text, 'Batch size: 3');
await cleanup();
});
// Test 3: Get next item still works
runner.test('Get next item works with batch size 1', async () => {
await setup();
await client.callTool({
name: 'initialize_array',
arguments: {
array: ['a', 'b', 'c'],
task: 'Process letters',
},
});
const result = await client.callTool({
name: 'get_next_item',
arguments: {},
});
const data = JSON.parse(result.content[0].text);
assertEqual(data.item, 'a');
assertEqual(data.index, 0);
assertEqual(data.total, 3);
await cleanup();
});
// Test 4: Get next batch with default size
runner.test('Get next batch with default batch size', async () => {
await setup();
await client.callTool({
name: 'initialize_array',
arguments: {
array: [1, 2, 3, 4, 5],
task: 'Double numbers',
},
});
const result = await client.callTool({
name: 'get_next_batch',
arguments: {},
});
const data = JSON.parse(result.content[0].text);
assertEqual(data.items, [1]);
assertEqual(data.startIndex, 0);
assertEqual(data.endIndex, 0);
assertEqual(data.batchSize, 1);
await cleanup();
});
// Test 5: Get next batch with custom batch size
runner.test('Get next batch with custom batch size', async () => {
await setup();
await client.callTool({
name: 'initialize_array',
arguments: {
array: [10, 20, 30, 40, 50, 60, 70],
task: 'Process numbers',
batchSize: 3,
},
});
// First batch
const batch1 = await client.callTool({
name: 'get_next_batch',
arguments: {},
});
const data1 = JSON.parse(batch1.content[0].text);
assertEqual(data1.items, [10, 20, 30]);
assertEqual(data1.startIndex, 0);
assertEqual(data1.endIndex, 2);
assertEqual(data1.batchSize, 3);
await cleanup();
});
// Test 6: Store single result
runner.test('Store single result still works', async () => {
await setup();
await client.callTool({
name: 'initialize_array',
arguments: {
array: ['test'],
task: 'Process string',
},
});
await client.callTool({
name: 'get_next_item',
arguments: {},
});
const result = await client.callTool({
name: 'store_result',
arguments: {
result: 'PROCESSED',
},
});
assertContains(result.content[0].text, 'Result stored');
assertContains(result.content[0].text, 'All items processed');
await cleanup();
});
// Test 7: Store batch results
runner.test('Store batch results', async () => {
await setup();
await client.callTool({
name: 'initialize_array',
arguments: {
array: [1, 2, 3, 4, 5, 6],
task: 'Square numbers',
batchSize: 3,
},
});
// Get first batch
await client.callTool({
name: 'get_next_batch',
arguments: {},
});
// Store batch results
const result = await client.callTool({
name: 'store_result',
arguments: {
result: [1, 4, 9], // squared values
},
});
assertContains(result.content[0].text, 'Result stored');
assertContains(result.content[0].text, '3 items remaining');
await cleanup();
});
// Test 8: Complete batch processing flow
runner.test('Complete batch processing flow', async () => {
await setup();
await client.callTool({
name: 'initialize_array',
arguments: {
array: [2, 4, 6, 8, 10],
task: 'Halve numbers',
batchSize: 2,
},
});
// Process first batch (2, 4)
const batch1 = await client.callTool({
name: 'get_next_batch',
arguments: {},
});
const data1 = JSON.parse(batch1.content[0].text);
assertEqual(data1.items, [2, 4]);
await client.callTool({
name: 'store_result',
arguments: {
result: [1, 2],
},
});
// Process second batch (6, 8)
const batch2 = await client.callTool({
name: 'get_next_batch',
arguments: {},
});
const data2 = JSON.parse(batch2.content[0].text);
assertEqual(data2.items, [6, 8]);
await client.callTool({
name: 'store_result',
arguments: {
result: [3, 4],
},
});
// Process final batch (10)
const batch3 = await client.callTool({
name: 'get_next_batch',
arguments: {},
});
const data3 = JSON.parse(batch3.content[0].text);
assertEqual(data3.items, [10]);
assertEqual(data3.batchSize, 1); // Last batch is partial
await client.callTool({
name: 'store_result',
arguments: {
result: [5],
},
});
// Verify all processed
const batch4 = await client.callTool({
name: 'get_next_batch',
arguments: {},
});
assertEqual(batch4.content[0].text, 'All items have been processed.');
// Get all results
const allResults = await client.callTool({
name: 'get_all_results',
arguments: { summarize: true },
});
const results = JSON.parse(allResults.content[0].text);
assertEqual(results.processed, 5);
assertEqual(results.results.length, 5);
assertEqual(results.results.map(r => r.result), [1, 2, 3, 4, 5]);
await cleanup();
});
// Test 9: Mixing single and batch operations
runner.test('Mixing single and batch operations', async () => {
await setup();
await client.callTool({
name: 'initialize_array',
arguments: {
array: ['a', 'b', 'c', 'd'],
task: 'Process letters',
batchSize: 2,
},
});
// Use get_next_item (should get one item)
const single = await client.callTool({
name: 'get_next_item',
arguments: {},
});
const singleData = JSON.parse(single.content[0].text);
assertEqual(singleData.item, 'a');
// Store single result
await client.callTool({
name: 'store_result',
arguments: {
result: 'A',
},
});
// Use get_next_batch (should get remaining items based on batch size)
const batch = await client.callTool({
name: 'get_next_batch',
arguments: {},
});
const batchData = JSON.parse(batch.content[0].text);
assertEqual(batchData.items, ['b', 'c']); // batch size is 2
await cleanup();
});
// Test 10: Error handling for batch operations
runner.test('Error handling for uninitialized batch operations', async () => {
await setup();
const result = await client.callTool({
name: 'get_next_batch',
arguments: {},
});
assertContains(result.content[0].text, 'Error: Array not initialized');
await cleanup();
});
// Test 11: Batch size validation
runner.test('Batch size edge cases', async () => {
await setup();
// Test with batch size larger than array
await client.callTool({
name: 'initialize_array',
arguments: {
array: [1, 2, 3],
task: 'Test task',
batchSize: 10,
},
});
const result = await client.callTool({
name: 'get_next_batch',
arguments: {},
});
const data = JSON.parse(result.content[0].text);
assertEqual(data.items, [1, 2, 3]);
assertEqual(data.batchSize, 3); // Should be capped at array length
await cleanup();
});
// Test 12: Store result limits to remaining items
runner.test('Store result limits to remaining items', async () => {
await setup();
await client.callTool({
name: 'initialize_array',
arguments: {
array: [1, 2, 3, 4, 5, 6, 7],
task: 'Test task',
batchSize: 3,
},
});
// Get first batch (should be 3 items: 1, 2, 3)
const batch1 = await client.callTool({
name: 'get_next_batch',
arguments: {},
});
const batchData1 = JSON.parse(batch1.content[0].text);
assertEqual(batchData1.items, [1, 2, 3]);
// Store exactly 3 results for the batch
await client.callTool({
name: 'store_result',
arguments: {
result: [10, 20, 30],
},
});
// Get second batch (should be 3 items: 4, 5, 6)
const batch2 = await client.callTool({
name: 'get_next_batch',
arguments: {},
});
const batchData2 = JSON.parse(batch2.content[0].text);
assertEqual(batchData2.items, [4, 5, 6]);
// Try to store more results than remaining items
// We have items 4, 5, 6, 7 remaining (4 items), but we'll try to store 10 results
const result = await client.callTool({
name: 'store_result',
arguments: {
result: [40, 50, 60, 70, 80, 90, 100, 110, 120, 130], // 10 results for 4 remaining items
},
});
// Should only process the 4 remaining items
assertContains(result.content[0].text, 'Result stored');
assertContains(result.content[0].text, 'All items processed');
// Verify all results
const allResults = await client.callTool({
name: 'get_all_results',
arguments: {},
});
const finalData = JSON.parse(allResults.content[0].text);
assertEqual(finalData.results.length, 7); // Should have exactly 7 results
assertEqual(finalData.results.map(r => r.result), [10, 20, 30, 40, 50, 60, 70]);
await cleanup();
});
// Run all tests
const success = await runner.run();
process.exit(success ? 0 : 1);
}
// Run the test suite
runTests().catch(error => {
console.error('Test suite failed:', error);
process.exit(1);
});