mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
Remove the nested v3/ subdirectory by moving all active source code, tests, configs, and assets directly to the project root. Archive v2/ (dead code, 847MB) to v2-archive.tar.gz. Key changes: - Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/ - Move v3/tsconfig.json, v3/vitest.config.ts to root - Merge v3/package.json into root package.json (ESM, scripts, deps, exports) - Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/ - Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes - Fix all init installer path resolution (3-level-up → 2-level-up) - Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe" - Update settings.json ADR/DDD directories to docs/implementation/ - Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks) - Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces - Update 15 guidance shards: v3/src/ → src/ path references - Fix sync interfaces: remove v3/ and v2/ path references - Fix test path calculations (process.cwd() + '..' no longer needed) - Fix flaky perf test threshold (500ms → 1000ms for CI environments) - Fix complexity-analyzer test to match ADR-051 keyword-based eligibility Build passes, 17,901 tests pass, CLI and MCP bundles verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
295 lines
9.7 KiB
TypeScript
295 lines
9.7 KiB
TypeScript
/**
|
|
* Agentic QE v3 - Real Provider Integration Tests
|
|
* ADR-043: Vendor-Independent LLM Support
|
|
*
|
|
* REAL integration tests using MSW (Mock Service Worker) to intercept
|
|
* actual HTTP requests. Unlike other tests that mock at the module level,
|
|
* these tests exercise the full HTTP stack.
|
|
*
|
|
* @see https://mswjs.io/
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
import { http, HttpResponse } from 'msw';
|
|
import { setupServer } from 'msw/node';
|
|
import { HybridRouter, createHybridRouter, createQERouter } from '../../../src/shared/llm/router/hybrid-router';
|
|
import { ProviderManager } from '../../../src/shared/llm/provider-manager';
|
|
import type { ChatParams } from '../../../src/shared/llm/router/types';
|
|
|
|
// ============================================================================
|
|
// MSW Server Setup - Real HTTP Interception
|
|
// ============================================================================
|
|
|
|
// Mock Claude API responses
|
|
const handlers = [
|
|
// Claude Messages API
|
|
http.post('https://api.anthropic.com/v1/messages', async ({ request }) => {
|
|
const body = await request.json() as Record<string, unknown>;
|
|
const model = body.model as string;
|
|
const messages = body.messages as Array<{ role: string; content: string }>;
|
|
const inputTokens = messages.reduce((acc, m) => acc + m.content.length / 4, 0);
|
|
|
|
return HttpResponse.json({
|
|
id: `msg_${Date.now()}`,
|
|
type: 'message',
|
|
role: 'assistant',
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: `MSW intercepted response for model ${model}. This is a real integration test.`,
|
|
},
|
|
],
|
|
model: model,
|
|
stop_reason: 'end_turn',
|
|
usage: {
|
|
input_tokens: Math.ceil(inputTokens),
|
|
output_tokens: 50,
|
|
},
|
|
}, {
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'x-request-id': `req_${Date.now()}`,
|
|
},
|
|
});
|
|
}),
|
|
|
|
// OpenAI Chat Completions API
|
|
http.post('https://api.openai.com/v1/chat/completions', async ({ request }) => {
|
|
const body = await request.json() as Record<string, unknown>;
|
|
const model = body.model as string;
|
|
const messages = body.messages as Array<{ role: string; content: string }>;
|
|
const inputTokens = messages.reduce((acc, m) => acc + m.content.length / 4, 0);
|
|
|
|
return HttpResponse.json({
|
|
id: `chatcmpl-${Date.now()}`,
|
|
object: 'chat.completion',
|
|
created: Math.floor(Date.now() / 1000),
|
|
model: model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
message: {
|
|
role: 'assistant',
|
|
content: `OpenAI MSW response for model ${model}.`,
|
|
},
|
|
finish_reason: 'stop',
|
|
},
|
|
],
|
|
usage: {
|
|
prompt_tokens: Math.ceil(inputTokens),
|
|
completion_tokens: 30,
|
|
total_tokens: Math.ceil(inputTokens) + 30,
|
|
},
|
|
});
|
|
}),
|
|
|
|
// Ollama API (local)
|
|
http.post('http://localhost:11434/api/chat', async ({ request }) => {
|
|
const body = await request.json() as Record<string, unknown>;
|
|
const model = body.model as string;
|
|
|
|
return HttpResponse.json({
|
|
model: model,
|
|
message: {
|
|
role: 'assistant',
|
|
content: `Ollama MSW response for model ${model}.`,
|
|
},
|
|
done: true,
|
|
total_duration: 1000000000,
|
|
load_duration: 100000000,
|
|
prompt_eval_count: 10,
|
|
eval_count: 20,
|
|
});
|
|
}),
|
|
];
|
|
|
|
const server = setupServer(...handlers);
|
|
|
|
// ============================================================================
|
|
// Test Suite
|
|
// ============================================================================
|
|
|
|
describe('Real Provider Integration Tests (MSW)', () => {
|
|
let providerManager: ProviderManager;
|
|
let router: HybridRouter;
|
|
|
|
beforeAll(() => {
|
|
server.listen({ onUnhandledRequest: 'bypass' });
|
|
});
|
|
|
|
afterAll(() => {
|
|
server.close();
|
|
});
|
|
|
|
afterEach(() => {
|
|
server.resetHandlers();
|
|
});
|
|
|
|
beforeAll(async () => {
|
|
providerManager = new ProviderManager({
|
|
defaultProvider: 'claude',
|
|
providers: {
|
|
claude: {
|
|
apiKey: 'test-api-key-for-msw',
|
|
model: 'claude-sonnet-4-20250514',
|
|
},
|
|
openai: {
|
|
apiKey: 'test-openai-key-for-msw',
|
|
model: 'gpt-4o',
|
|
},
|
|
ollama: {
|
|
baseUrl: 'http://localhost:11434',
|
|
model: 'llama2',
|
|
},
|
|
},
|
|
});
|
|
await providerManager.initialize();
|
|
router = createHybridRouter(providerManager);
|
|
});
|
|
|
|
describe('Claude Provider via MSW', () => {
|
|
it('should make real HTTP request to Claude API (intercepted by MSW)', async () => {
|
|
await router.initialize();
|
|
router.setMode('manual');
|
|
|
|
const params: ChatParams = {
|
|
messages: [{ role: 'user', content: 'Hello from MSW integration test' }],
|
|
preferredProvider: 'claude',
|
|
};
|
|
|
|
const response = await router.chat(params);
|
|
|
|
// Verify response came through the full HTTP stack
|
|
expect(response.content).toContain('MSW intercepted response');
|
|
expect(response.provider).toBe('claude');
|
|
expect(response.usage.promptTokens).toBeGreaterThan(0);
|
|
expect(response.usage.completionTokens).toBe(50);
|
|
expect(response.requestId).toBeDefined();
|
|
});
|
|
|
|
it('should handle model ID normalization through real request', async () => {
|
|
await router.initialize();
|
|
router.setMode('manual');
|
|
|
|
const params: ChatParams = {
|
|
messages: [{ role: 'user', content: 'Test model normalization' }],
|
|
preferredProvider: 'claude',
|
|
model: 'claude-sonnet-4', // Canonical model ID
|
|
};
|
|
|
|
const response = await router.chat(params);
|
|
|
|
// Verify the provider received the provider-specific model ID (mapped from canonical)
|
|
// The MSW mock echoes back the model it received
|
|
expect(response.content).toContain('claude-sonnet-4-20250514');
|
|
expect(response.provider).toBe('claude');
|
|
// The routing decision stores canonical in model, provider-specific in response
|
|
expect(response.routingDecision?.model).toBe('claude-sonnet-4');
|
|
expect(response.routingDecision?.providerModelId).toBe('claude-sonnet-4-20250514');
|
|
});
|
|
});
|
|
|
|
describe('Provider Fallback via MSW', () => {
|
|
it('should fallback to next provider when primary fails', async () => {
|
|
// Override Claude handler to return error
|
|
server.use(
|
|
http.post('https://api.anthropic.com/v1/messages', () => {
|
|
return HttpResponse.json(
|
|
{ error: { message: 'Rate limited' } },
|
|
{ status: 429 }
|
|
);
|
|
})
|
|
);
|
|
|
|
await router.initialize();
|
|
router.setMode('manual');
|
|
|
|
const params: ChatParams = {
|
|
messages: [{ role: 'user', content: 'Test fallback' }],
|
|
preferredProvider: 'claude',
|
|
};
|
|
|
|
// This should trigger fallback to another provider
|
|
const response = await router.chat(params);
|
|
|
|
// Either succeeded with fallback or got a proper error
|
|
expect(response.content).toBeDefined();
|
|
expect(['claude', 'openai', 'ollama']).toContain(response.provider);
|
|
});
|
|
});
|
|
|
|
describe('Cost Tracking via MSW', () => {
|
|
it('should track costs from real responses', async () => {
|
|
await router.initialize();
|
|
router.setMode('manual');
|
|
|
|
const params: ChatParams = {
|
|
messages: [{ role: 'user', content: 'Test cost tracking with MSW' }],
|
|
preferredProvider: 'claude',
|
|
};
|
|
|
|
const response = await router.chat(params);
|
|
|
|
// Verify cost object is calculated based on actual token usage
|
|
expect(response.cost).toBeDefined();
|
|
expect(response.cost.inputCost).toBeDefined();
|
|
expect(response.cost.outputCost).toBeDefined();
|
|
expect(response.cost.totalCost).toBeDefined();
|
|
// Cost should be non-negative (could be 0 for test configs without pricing)
|
|
expect(response.cost.totalCost).toBeGreaterThanOrEqual(0);
|
|
|
|
// Verify token usage is tracked
|
|
expect(response.usage).toBeDefined();
|
|
expect(response.usage.promptTokens).toBeGreaterThan(0);
|
|
expect(response.usage.completionTokens).toBe(50);
|
|
expect(response.usage.totalTokens).toBe(response.usage.promptTokens + 50);
|
|
});
|
|
});
|
|
|
|
describe('QE Router Integration', () => {
|
|
it('should use QE-specific router with MSW', async () => {
|
|
const qeRouter = createQERouter(providerManager);
|
|
await qeRouter.initialize();
|
|
|
|
const config = qeRouter.getConfig();
|
|
expect(config.mode).toBe('rule-based');
|
|
expect(config.enableMetrics).toBe(true);
|
|
|
|
const params: ChatParams = {
|
|
messages: [{ role: 'user', content: 'Generate a test' }],
|
|
agentType: 'v3-qe-test-generator',
|
|
};
|
|
|
|
const decision = await qeRouter.selectProvider(params);
|
|
|
|
expect(decision.providerType).toBeDefined();
|
|
expect(decision.model).toBeDefined();
|
|
expect(decision.providerModelId).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe('Multi-Provider Routing', () => {
|
|
it('should route to different providers based on rules', async () => {
|
|
await router.initialize();
|
|
router.setMode('rule-based');
|
|
|
|
// Security agent should route to high-capability model
|
|
const securityParams: ChatParams = {
|
|
messages: [{ role: 'user', content: 'Security analysis' }],
|
|
agentType: 'security-auditor',
|
|
};
|
|
const securityDecision = await router.selectProvider(securityParams);
|
|
expect(securityDecision.providerType).toBe('claude');
|
|
|
|
// Low complexity should route to efficient model
|
|
const simpleParams: ChatParams = {
|
|
messages: [{ role: 'user', content: 'Simple task' }],
|
|
complexity: 'low',
|
|
};
|
|
const simpleDecision = await router.selectProvider(simpleParams);
|
|
expect(simpleDecision.providerType).toBe('claude');
|
|
expect(simpleDecision.model).toBe('claude-haiku-3-5');
|
|
});
|
|
});
|
|
});
|