mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
00e4d2cfa6
125 test files had beforeEach/beforeAll but no afterEach/afterAll, leaking state between tests and causing order-dependent failures. Added cleanup to 104 files (skipped 4 archived, 16 import-only): - 2 critical DB handle leaks (witness-chain): added db.close() - 7 high-risk service files: added vi.restoreAllMocks() - 7 beforeAll files: added matching afterAll - 88 medium/low risk: added afterEach with appropriate cleanup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
341 lines
9.5 KiB
TypeScript
341 lines
9.5 KiB
TypeScript
/**
|
|
* Agentic QE v3 - MCP Tool Base Tests
|
|
* Tests for the base MCP tool infrastructure per ADR-010
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
|
|
import { MCPToolBase, MCPToolConfig, MCPToolContext, MCPToolSchema } from '../../../../src/mcp/tools/base';
|
|
import { ToolResult } from '../../../../src/mcp/types';
|
|
|
|
// ============================================================================
|
|
// Test Implementation
|
|
// ============================================================================
|
|
|
|
interface TestParams {
|
|
input: string;
|
|
count?: number;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
interface TestResult {
|
|
output: string;
|
|
processed: number;
|
|
}
|
|
|
|
class TestTool extends MCPToolBase<TestParams, TestResult> {
|
|
readonly config: MCPToolConfig = {
|
|
name: 'test/tool',
|
|
description: 'A test tool',
|
|
domain: 'test-generation',
|
|
schema: {
|
|
type: 'object',
|
|
properties: {
|
|
input: {
|
|
type: 'string',
|
|
description: 'Input text',
|
|
},
|
|
count: {
|
|
type: 'number',
|
|
description: 'Count',
|
|
default: 1,
|
|
},
|
|
},
|
|
required: ['input'],
|
|
},
|
|
streaming: true,
|
|
timeout: 5000,
|
|
};
|
|
|
|
async execute(
|
|
params: TestParams,
|
|
context: MCPToolContext
|
|
): Promise<ToolResult<TestResult>> {
|
|
this.emitStream(context, { status: 'processing', message: 'Starting' });
|
|
|
|
if (this.isAborted(context)) {
|
|
return { success: false, error: 'Aborted' };
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
output: params.input.toUpperCase(),
|
|
processed: params.count || 1,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
class FailingTool extends MCPToolBase<TestParams, TestResult> {
|
|
readonly config: MCPToolConfig = {
|
|
name: 'test/failing',
|
|
description: 'A failing test tool',
|
|
domain: 'test-generation',
|
|
schema: {
|
|
type: 'object',
|
|
properties: {
|
|
input: { type: 'string', description: 'Input' },
|
|
},
|
|
required: ['input'],
|
|
},
|
|
streaming: false,
|
|
timeout: 5000,
|
|
};
|
|
|
|
async execute(): Promise<ToolResult<TestResult>> {
|
|
throw new Error('Intentional failure');
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests
|
|
// ============================================================================
|
|
|
|
describe('MCPToolBase', () => {
|
|
let tool: TestTool;
|
|
let failingTool: FailingTool;
|
|
|
|
beforeEach(() => {
|
|
tool = new TestTool();
|
|
failingTool = new FailingTool();
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Reset state to prevent leaks between tests
|
|
});
|
|
|
|
describe('properties', () => {
|
|
it('should expose tool name', () => {
|
|
expect(tool.name).toBe('test/tool');
|
|
});
|
|
|
|
it('should expose tool description', () => {
|
|
expect(tool.description).toBe('A test tool');
|
|
});
|
|
|
|
it('should expose tool domain', () => {
|
|
expect(tool.domain).toBe('test-generation');
|
|
});
|
|
|
|
it('should expose timeout', () => {
|
|
expect(tool.timeout).toBe(5000);
|
|
});
|
|
|
|
it('should indicate streaming support', () => {
|
|
expect(tool.supportsStreaming).toBe(true);
|
|
expect(failingTool.supportsStreaming).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getSchema', () => {
|
|
it('should return the tool schema', () => {
|
|
const schema = tool.getSchema();
|
|
expect(schema.type).toBe('object');
|
|
expect(schema.properties.input).toBeDefined();
|
|
expect(schema.properties.count).toBeDefined();
|
|
expect(schema.required).toContain('input');
|
|
});
|
|
|
|
it('should include property descriptions', () => {
|
|
const schema = tool.getSchema();
|
|
expect(schema.properties.input.description).toBe('Input text');
|
|
});
|
|
|
|
it('should include default values', () => {
|
|
const schema = tool.getSchema();
|
|
expect(schema.properties.count.default).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('validate', () => {
|
|
it('should validate correct params', () => {
|
|
const result = tool.validate({ input: 'test' });
|
|
expect(result.valid).toBe(true);
|
|
expect(result.errors).toHaveLength(0);
|
|
});
|
|
|
|
it('should fail when required param is missing', () => {
|
|
const result = tool.validate({});
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors).toContain('Missing required field: input');
|
|
});
|
|
|
|
it('should validate param types', () => {
|
|
const result = tool.validate({ input: 123 });
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors[0]).toContain('input');
|
|
expect(result.errors[0]).toContain('string');
|
|
});
|
|
|
|
it('should accept optional params', () => {
|
|
const result = tool.validate({ input: 'test', count: 5 });
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should validate optional param types', () => {
|
|
const result = tool.validate({ input: 'test', count: 'not a number' });
|
|
expect(result.valid).toBe(false);
|
|
expect(result.errors[0]).toContain('count');
|
|
});
|
|
});
|
|
|
|
describe('invoke', () => {
|
|
it('should execute tool and return result', async () => {
|
|
const result = await tool.invoke({ input: 'hello' });
|
|
expect(result.success).toBe(true);
|
|
expect(result.data?.output).toBe('HELLO');
|
|
expect(result.data?.processed).toBe(1);
|
|
});
|
|
|
|
it('should pass count parameter', async () => {
|
|
const result = await tool.invoke({ input: 'test', count: 5 });
|
|
expect(result.success).toBe(true);
|
|
expect(result.data?.processed).toBe(5);
|
|
});
|
|
|
|
it('should include metadata', async () => {
|
|
const result = await tool.invoke({ input: 'test' });
|
|
expect(result.metadata).toBeDefined();
|
|
expect(result.metadata?.requestId).toBeDefined();
|
|
expect(result.metadata?.executionTime).toBeGreaterThanOrEqual(0);
|
|
expect(result.metadata?.toolName).toBe('test/tool');
|
|
});
|
|
|
|
it('should fail validation for invalid params', async () => {
|
|
const result = await tool.invoke({} as TestParams);
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Validation failed');
|
|
});
|
|
|
|
it('should catch errors and return failure', async () => {
|
|
const result = await failingTool.invoke({ input: 'test' });
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Intentional failure');
|
|
});
|
|
});
|
|
|
|
describe('streaming', () => {
|
|
it('should call onStream callback', async () => {
|
|
const streamData: unknown[] = [];
|
|
|
|
await tool.invoke({ input: 'test' }, {
|
|
streaming: true,
|
|
onStream: (data) => streamData.push(data),
|
|
});
|
|
|
|
expect(streamData.length).toBeGreaterThan(0);
|
|
expect(streamData[0]).toMatchObject({
|
|
status: 'processing',
|
|
message: 'Starting',
|
|
});
|
|
});
|
|
|
|
it('should not fail if onStream is not provided', async () => {
|
|
const result = await tool.invoke({ input: 'test' });
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('abort signal', () => {
|
|
it('should handle abort signal', async () => {
|
|
const controller = new AbortController();
|
|
controller.abort();
|
|
|
|
const result = await tool.invoke({ input: 'test' }, {
|
|
abortSignal: controller.signal,
|
|
});
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toBe('Aborted');
|
|
});
|
|
});
|
|
|
|
describe('metadata', () => {
|
|
it('should include metadata in result', async () => {
|
|
const result = await tool.invoke({ input: 'test' });
|
|
expect(result.success).toBe(true);
|
|
expect(result.metadata?.requestId).toBeDefined();
|
|
expect(result.metadata?.executionTime).toBeGreaterThanOrEqual(0);
|
|
expect(result.metadata?.domain).toBe('test-generation');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('MCPToolSchema', () => {
|
|
it('should support string properties', () => {
|
|
const schema: MCPToolSchema = {
|
|
type: 'object',
|
|
properties: {
|
|
name: { type: 'string', description: 'Name' },
|
|
},
|
|
required: ['name'],
|
|
};
|
|
expect(schema.properties.name.type).toBe('string');
|
|
});
|
|
|
|
it('should support number properties', () => {
|
|
const schema: MCPToolSchema = {
|
|
type: 'object',
|
|
properties: {
|
|
count: { type: 'number', description: 'Count', minimum: 0, maximum: 100 },
|
|
},
|
|
};
|
|
expect(schema.properties.count.minimum).toBe(0);
|
|
expect(schema.properties.count.maximum).toBe(100);
|
|
});
|
|
|
|
it('should support boolean properties', () => {
|
|
const schema: MCPToolSchema = {
|
|
type: 'object',
|
|
properties: {
|
|
enabled: { type: 'boolean', description: 'Enabled', default: false },
|
|
},
|
|
};
|
|
expect(schema.properties.enabled.default).toBe(false);
|
|
});
|
|
|
|
it('should support array properties', () => {
|
|
const schema: MCPToolSchema = {
|
|
type: 'object',
|
|
properties: {
|
|
items: {
|
|
type: 'array',
|
|
description: 'Items',
|
|
items: { type: 'string', description: 'Item' },
|
|
},
|
|
},
|
|
};
|
|
expect(schema.properties.items.type).toBe('array');
|
|
expect(schema.properties.items.items?.type).toBe('string');
|
|
});
|
|
|
|
it('should support enum properties', () => {
|
|
const schema: MCPToolSchema = {
|
|
type: 'object',
|
|
properties: {
|
|
level: {
|
|
type: 'string',
|
|
description: 'Level',
|
|
enum: ['low', 'medium', 'high'],
|
|
},
|
|
},
|
|
};
|
|
expect(schema.properties.level.enum).toContain('medium');
|
|
});
|
|
|
|
it('should support object properties', () => {
|
|
const schema: MCPToolSchema = {
|
|
type: 'object',
|
|
properties: {
|
|
config: {
|
|
type: 'object',
|
|
description: 'Configuration',
|
|
properties: {
|
|
timeout: { type: 'number', description: 'Timeout' },
|
|
},
|
|
},
|
|
},
|
|
};
|
|
expect(schema.properties.config.type).toBe('object');
|
|
});
|
|
});
|