Files
Profa b91627f282 fix(quality): implement GOAP Issue #149 code quality remediation
Phase 1: Test Determinism
- Create SeededRandom utility with Mulberry32 PRNG (src/utils/SeededRandom.ts)
- Migrate 258+ Math.random() calls to SeededRandom across 65+ test files
- Add timer test utilities (tests/helpers/timerTestUtils.ts)
- Add jest.useFakeTimers() pattern to 13 test files

Phase 2: Type Safety (63.5% reduction)
- Reduce `any` types from ~1,800 to 656
- Add 50+ new interfaces across agents, core, mcp, cli modules
- Fix all TypeScript errors (0 errors, build passes)
- Replace Record<string, any> with Record<string, unknown>
- Add proper type guards for hook methods (PostTaskData, TaskErrorData)

Key improvements:
- agents/: 105 → 0 any types (100% reduction)
- mcp/: 226 → 102 any types (55% reduction)
- cli/: 277 → 168 any types (39% reduction)
- core/: 199 → 175 any types (12% reduction)

Closes #149

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 17:08:12 +00:00

484 lines
14 KiB
TypeScript

/**
* REST API Tests for Phase 3 Visualization
* Tests all REST endpoints, pagination, filtering, caching, and error handling
*/
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
import { createSeededRandom } from '../../../src/utils/SeededRandom';
interface APIResponse<T> {
success: boolean;
data?: T;
error?: { code: string; message: string };
pagination?: {
page: number;
pageSize: number;
total: number;
hasNext: boolean;
};
etag?: string;
cacheControl?: string;
}
interface TelemetryEvent {
id: string;
timestamp: number;
type: string;
agentId: string;
data: Record<string, unknown>;
}
interface MetricData {
id: string;
name: string;
value: number;
timestamp: number;
tags: Record<string, string>;
}
// Mock REST API Client
class MockRESTClient {
private baseURL: string = 'http://localhost:3000/api/v1';
private cache: Map<string, { data: unknown; etag: string; timestamp: number }> = new Map();
private cacheMaxAge: number = 60000; // 1 minute
async get<T>(endpoint: string, params?: Record<string, unknown>): Promise<APIResponse<T>> {
const url = this.buildURL(endpoint, params);
const cacheKey = url;
// Check cache
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.cacheMaxAge) {
return {
success: true,
data: cached.data as T,
etag: cached.etag,
cacheControl: `max-age=${Math.floor((this.cacheMaxAge - (Date.now() - cached.timestamp)) / 1000)}`
};
}
// Simulate API call
const response = await this.mockFetch<T>(endpoint, params);
// Cache successful responses
if (response.success && response.data) {
const etag = this.generateETag(response.data);
this.cache.set(cacheKey, {
data: response.data,
etag,
timestamp: Date.now()
});
response.etag = etag;
response.cacheControl = `max-age=${this.cacheMaxAge / 1000}`;
}
return response;
}
async post<T>(endpoint: string, data: unknown): Promise<APIResponse<T>> {
// Invalidate cache
this.cache.clear();
return this.mockFetch<T>(endpoint, { method: 'POST', body: data });
}
private buildURL(endpoint: string, params?: Record<string, unknown>): string {
const url = new URL(endpoint, this.baseURL);
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.append(key, String(value));
}
});
}
return url.toString();
}
private async mockFetch<T>(endpoint: string, params?: Record<string, unknown>): Promise<APIResponse<T>> {
// Simulate different endpoints
if (endpoint.includes('/events')) {
return this.mockEventsEndpoint(params) as APIResponse<T>;
} else if (endpoint.includes('/metrics')) {
return this.mockMetricsEndpoint(params) as APIResponse<T>;
} else if (endpoint.includes('/reasoning-chains')) {
return this.mockReasoningChainsEndpoint(params) as APIResponse<T>;
}
return {
success: false,
error: { code: 'NOT_FOUND', message: 'Endpoint not found' }
};
}
private mockEventsEndpoint(params?: Record<string, unknown>): APIResponse<TelemetryEvent[]> {
const page = Number(params?.page) || 1;
const pageSize = Number(params?.pageSize) || 20;
const total = 100;
const events: TelemetryEvent[] = [];
const start = (page - 1) * pageSize;
const end = Math.min(start + pageSize, total);
for (let i = start; i < end; i++) {
events.push({
id: `event-${i}`,
timestamp: Date.now() - i * 1000,
type: 'test.event',
agentId: `agent-${i % 5}`,
data: { index: i }
});
}
return {
success: true,
data: events,
pagination: {
page,
pageSize,
total,
hasNext: end < total
}
};
}
private mockMetricsEndpoint(params?: Record<string, unknown>): APIResponse<MetricData[]> {
const page = Number(params?.page) || 1;
const pageSize = Number(params?.pageSize) || 20;
const total = 50;
const rng = createSeededRandom(21002);
const metrics: MetricData[] = [];
const start = (page - 1) * pageSize;
const end = Math.min(start + pageSize, total);
for (let i = start; i < end; i++) {
metrics.push({
id: `metric-${i}`,
name: 'test_metric',
value: rng.random() * 100,
timestamp: Date.now() - i * 1000,
tags: { agent: `agent-${i % 5}` }
});
}
return {
success: true,
data: metrics,
pagination: {
page,
pageSize,
total,
hasNext: end < total
}
};
}
private mockReasoningChainsEndpoint(params?: Record<string, unknown>): APIResponse<unknown[]> {
const page = Number(params?.page) || 1;
const pageSize = Number(params?.pageSize) || 10;
const total = 30;
const chains: unknown[] = [];
const start = (page - 1) * pageSize;
const end = Math.min(start + pageSize, total);
for (let i = start; i < end; i++) {
chains.push({
id: `chain-${i}`,
agentId: `agent-${i % 3}`,
steps: [{ step: 1, action: 'think' }, { step: 2, action: 'act' }],
timestamp: Date.now() - i * 1000
});
}
return {
success: true,
data: chains,
pagination: {
page,
pageSize,
total,
hasNext: end < total
}
};
}
private generateETag(data: unknown): string {
return `"${Buffer.from(JSON.stringify(data)).toString('base64').slice(0, 16)}"`;
}
clearCache(): void {
this.cache.clear();
}
}
describe('REST API Tests', () => {
let client: MockRESTClient;
beforeEach(() => {
client = new MockRESTClient();
});
afterEach(() => {
client.clearCache();
});
describe('Events Endpoint', () => {
it('should fetch telemetry events successfully', async () => {
const response = await client.get<TelemetryEvent[]>('/events');
expect(response.success).toBe(true);
expect(response.data).toBeDefined();
expect(Array.isArray(response.data)).toBe(true);
expect(response.data!.length).toBeGreaterThan(0);
});
it('should support pagination', async () => {
const page1 = await client.get<TelemetryEvent[]>('/events', { page: 1, pageSize: 10 });
const page2 = await client.get<TelemetryEvent[]>('/events', { page: 2, pageSize: 10 });
expect(page1.success).toBe(true);
expect(page2.success).toBe(true);
expect(page1.data).toHaveLength(10);
expect(page2.data).toHaveLength(10);
expect(page1.data![0].id).not.toBe(page2.data![0].id);
});
it('should include pagination metadata', async () => {
const response = await client.get<TelemetryEvent[]>('/events', { page: 1, pageSize: 20 });
expect(response.pagination).toBeDefined();
expect(response.pagination!.page).toBe(1);
expect(response.pagination!.pageSize).toBe(20);
expect(response.pagination!.total).toBeGreaterThan(0);
expect(typeof response.pagination!.hasNext).toBe('boolean');
});
it('should handle last page correctly', async () => {
const response = await client.get<TelemetryEvent[]>('/events', { page: 5, pageSize: 20 });
expect(response.success).toBe(true);
expect(response.pagination!.hasNext).toBe(false);
});
it('should filter events by agent ID', async () => {
const response = await client.get<TelemetryEvent[]>('/events', { agentId: 'agent-1' });
expect(response.success).toBe(true);
// Note: Filtering logic would be implemented in real API
});
it('should filter events by time range', async () => {
const now = Date.now();
const oneHourAgo = now - 3600000;
const response = await client.get<TelemetryEvent[]>('/events', {
startTime: oneHourAgo,
endTime: now
});
expect(response.success).toBe(true);
});
});
describe('Metrics Endpoint', () => {
it('should fetch metrics successfully', async () => {
const response = await client.get<MetricData[]>('/metrics');
expect(response.success).toBe(true);
expect(response.data).toBeDefined();
expect(Array.isArray(response.data)).toBe(true);
});
it('should paginate metrics', async () => {
const response = await client.get<MetricData[]>('/metrics', { page: 1, pageSize: 10 });
expect(response.success).toBe(true);
expect(response.data).toHaveLength(10);
expect(response.pagination).toBeDefined();
});
it('should filter metrics by name', async () => {
const response = await client.get<MetricData[]>('/metrics', { name: 'test_metric' });
expect(response.success).toBe(true);
});
it('should filter metrics by tags', async () => {
const response = await client.get<MetricData[]>('/metrics', { tags: JSON.stringify({ agent: 'agent-1' }) });
expect(response.success).toBe(true);
});
it('should aggregate metrics', async () => {
const response = await client.get<MetricData[]>('/metrics', {
aggregate: 'avg',
interval: '5m'
});
expect(response.success).toBe(true);
});
});
describe('Reasoning Chains Endpoint', () => {
it('should fetch reasoning chains successfully', async () => {
const response = await client.get<unknown[]>('/reasoning-chains');
expect(response.success).toBe(true);
expect(response.data).toBeDefined();
expect(Array.isArray(response.data)).toBe(true);
});
it('should paginate reasoning chains', async () => {
const response = await client.get<unknown[]>('/reasoning-chains', { page: 1, pageSize: 5 });
expect(response.success).toBe(true);
expect(response.data).toHaveLength(5);
});
it('should filter chains by agent ID', async () => {
const response = await client.get<unknown[]>('/reasoning-chains', { agentId: 'agent-1' });
expect(response.success).toBe(true);
});
});
describe('Response Caching', () => {
it('should cache GET responses', async () => {
const response1 = await client.get<TelemetryEvent[]>('/events');
const response2 = await client.get<TelemetryEvent[]>('/events');
expect(response1.success).toBe(true);
expect(response2.success).toBe(true);
expect(response2.etag).toBeDefined();
expect(response2.cacheControl).toBeDefined();
});
it('should include ETag in cached responses', async () => {
const response = await client.get<TelemetryEvent[]>('/events');
expect(response.etag).toBeDefined();
expect(response.etag).toMatch(/^"/); // ETags are quoted
});
it('should include Cache-Control headers', async () => {
const response = await client.get<TelemetryEvent[]>('/events');
expect(response.cacheControl).toBeDefined();
expect(response.cacheControl).toContain('max-age');
});
it('should invalidate cache on POST', async () => {
await client.get<TelemetryEvent[]>('/events');
await client.post('/events', { type: 'new.event' });
const response = await client.get<TelemetryEvent[]>('/events');
expect(response.success).toBe(true);
});
it('should respect cache max-age', async () => {
const response1 = await client.get<TelemetryEvent[]>('/events');
// Wait for cache to expire (simulated)
await new Promise(resolve => setTimeout(resolve, 100));
const response2 = await client.get<TelemetryEvent[]>('/events');
expect(response1.etag).toBeDefined();
expect(response2.etag).toBeDefined();
});
});
describe('Error Handling', () => {
it('should handle 404 not found', async () => {
const response = await client.get('/nonexistent');
expect(response.success).toBe(false);
expect(response.error).toBeDefined();
expect(response.error!.code).toBe('NOT_FOUND');
});
it('should validate required query parameters', async () => {
// This would be implemented with proper validation
const response = await client.get('/events', { page: -1 });
expect(response.success).toBe(true); // Mock doesn't validate yet
});
it('should handle malformed query parameters', async () => {
const response = await client.get('/events', { page: 'invalid' });
expect(response.success).toBe(true); // Coerced to number
});
});
describe('Performance', () => {
it('should respond within 200ms', async () => {
const start = Date.now();
await client.get<TelemetryEvent[]>('/events');
const duration = Date.now() - start;
expect(duration).toBeLessThan(200);
});
it('should handle concurrent requests', async () => {
const requests = Array(10).fill(null).map(() =>
client.get<TelemetryEvent[]>('/events')
);
const responses = await Promise.all(requests);
expect(responses).toHaveLength(10);
responses.forEach(response => {
expect(response.success).toBe(true);
});
});
it('should efficiently paginate large datasets', async () => {
const pageSize = 100;
const start = Date.now();
await client.get<TelemetryEvent[]>('/events', { page: 1, pageSize });
const duration = Date.now() - start;
expect(duration).toBeLessThan(300);
});
});
describe('Query Parameter Validation', () => {
it('should handle various page sizes', async () => {
const pageSizes = [10, 20, 50, 100];
for (const pageSize of pageSizes) {
const response = await client.get<TelemetryEvent[]>('/events', { pageSize });
expect(response.success).toBe(true);
expect(response.data!.length).toBeLessThanOrEqual(pageSize);
}
});
it('should handle default pagination values', async () => {
const response = await client.get<TelemetryEvent[]>('/events');
expect(response.pagination).toBeDefined();
expect(response.pagination!.page).toBe(1);
expect(response.pagination!.pageSize).toBe(20); // Default
});
it('should support sorting parameters', async () => {
const response = await client.get<TelemetryEvent[]>('/events', {
sort: 'timestamp',
order: 'desc'
});
expect(response.success).toBe(true);
});
it('should support field selection', async () => {
const response = await client.get<TelemetryEvent[]>('/events', {
fields: 'id,timestamp,type'
});
expect(response.success).toBe(true);
});
});
});