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>
647 lines
18 KiB
TypeScript
647 lines
18 KiB
TypeScript
/**
|
||
* Browser Integration - Parallel Viewports Integration Tests
|
||
*
|
||
* Tests parallel viewport capture:
|
||
* - 5 viewports × 3 URLs (15 captures total)
|
||
* - Performance: must complete in < 30s
|
||
* - All viewports get consistent results
|
||
* - Proper cleanup after completion
|
||
*
|
||
* Uses mocked browser sessions for testing.
|
||
*/
|
||
|
||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||
import type { MemoryBackend } from '../../../src/kernel/interfaces';
|
||
import { Result, ok, err } from '../../../src/shared/types';
|
||
|
||
// ============================================================================
|
||
// Types
|
||
// ============================================================================
|
||
|
||
interface Viewport {
|
||
width: number;
|
||
height: number;
|
||
deviceScaleFactor: number;
|
||
isMobile: boolean;
|
||
hasTouch: boolean;
|
||
name?: string;
|
||
}
|
||
|
||
interface CaptureResult {
|
||
viewport: Viewport;
|
||
url: string;
|
||
success: boolean;
|
||
screenshotPath?: string;
|
||
error?: string;
|
||
captureTimeMs: number;
|
||
timestamp: Date;
|
||
}
|
||
|
||
interface ParallelCaptureResult {
|
||
totalCaptures: number;
|
||
successCount: number;
|
||
failedCount: number;
|
||
captures: CaptureResult[];
|
||
totalTimeMs: number;
|
||
avgCaptureTimeMs: number;
|
||
startedAt: Date;
|
||
completedAt: Date;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Test Doubles
|
||
// ============================================================================
|
||
|
||
/**
|
||
* In-memory backend for testing
|
||
*/
|
||
class TestMemoryBackend implements MemoryBackend {
|
||
private store = new Map<string, unknown>();
|
||
|
||
async initialize(): Promise<void> {}
|
||
async dispose(): Promise<void> {}
|
||
|
||
async set<T>(key: string, value: T): Promise<void> {
|
||
this.store.set(key, value);
|
||
}
|
||
|
||
async get<T>(key: string): Promise<T | undefined> {
|
||
return this.store.get(key) as T | undefined;
|
||
}
|
||
|
||
async delete(key: string): Promise<boolean> {
|
||
return this.store.delete(key);
|
||
}
|
||
|
||
async has(key: string): Promise<boolean> {
|
||
return this.store.has(key);
|
||
}
|
||
|
||
async search(pattern: string): Promise<string[]> {
|
||
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
||
return Array.from(this.store.keys()).filter((k) => regex.test(k));
|
||
}
|
||
|
||
async vectorSearch(): Promise<any[]> {
|
||
return [];
|
||
}
|
||
|
||
async storeVector(): Promise<void> {}
|
||
|
||
async count(namespace: string): Promise<number> {
|
||
const keys = await this.search(`${namespace}:*`);
|
||
return keys.length;
|
||
}
|
||
|
||
async hasCodeIntelligenceIndex(): Promise<boolean> {
|
||
const count = await this.count('code-intelligence:kg');
|
||
return count > 0;
|
||
}
|
||
|
||
clear(): void {
|
||
this.store.clear();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Mock viewport capture service
|
||
*/
|
||
class MockViewportCaptureService {
|
||
private captureDelayMs = 200; // Simulate capture time
|
||
|
||
async captureAtViewport(
|
||
url: string,
|
||
viewport: Viewport
|
||
): Promise<Result<CaptureResult, Error>> {
|
||
const startTime = Date.now();
|
||
|
||
// Simulate capture delay
|
||
await new Promise((resolve) => setTimeout(resolve, this.captureDelayMs));
|
||
|
||
const captureTimeMs = Date.now() - startTime;
|
||
|
||
// Simulate 95% success rate
|
||
const success = Math.random() > 0.05;
|
||
|
||
if (!success) {
|
||
return ok({
|
||
viewport,
|
||
url,
|
||
success: false,
|
||
error: 'Simulated capture failure',
|
||
captureTimeMs,
|
||
timestamp: new Date(),
|
||
});
|
||
}
|
||
|
||
return ok({
|
||
viewport,
|
||
url,
|
||
success: true,
|
||
screenshotPath: `/tmp/screenshot-${viewport.width}x${viewport.height}-${Date.now()}.png`,
|
||
captureTimeMs,
|
||
timestamp: new Date(),
|
||
});
|
||
}
|
||
|
||
async captureAllViewports(
|
||
urls: string[],
|
||
viewports: Viewport[]
|
||
): Promise<Result<ParallelCaptureResult, Error>> {
|
||
const startedAt = new Date();
|
||
const captures: CaptureResult[] = [];
|
||
|
||
// Capture all combinations in parallel
|
||
const capturePromises: Promise<CaptureResult>[] = [];
|
||
|
||
for (const url of urls) {
|
||
for (const viewport of viewports) {
|
||
const promise = this.captureAtViewport(url, viewport).then((result) => {
|
||
if (result.success) {
|
||
return result.value;
|
||
}
|
||
// Return failed capture result
|
||
const errorMsg = result.success === false ? result.error.message : 'Unknown error';
|
||
return {
|
||
viewport,
|
||
url,
|
||
success: false,
|
||
error: errorMsg,
|
||
captureTimeMs: 0,
|
||
timestamp: new Date(),
|
||
};
|
||
});
|
||
capturePromises.push(promise);
|
||
}
|
||
}
|
||
|
||
// Wait for all captures
|
||
const results = await Promise.all(capturePromises);
|
||
captures.push(...results);
|
||
|
||
const completedAt = new Date();
|
||
const totalTimeMs = completedAt.getTime() - startedAt.getTime();
|
||
|
||
const successCount = captures.filter((c) => c.success).length;
|
||
const failedCount = captures.length - successCount;
|
||
|
||
const totalCaptureTime = captures.reduce((sum, c) => sum + c.captureTimeMs, 0);
|
||
const avgCaptureTimeMs = totalCaptureTime / captures.length;
|
||
|
||
return ok({
|
||
totalCaptures: captures.length,
|
||
successCount,
|
||
failedCount,
|
||
captures,
|
||
totalTimeMs,
|
||
avgCaptureTimeMs,
|
||
startedAt,
|
||
completedAt,
|
||
});
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Standard Viewports
|
||
// ============================================================================
|
||
|
||
const STANDARD_VIEWPORTS: Viewport[] = [
|
||
{
|
||
name: 'mobile-s',
|
||
width: 320,
|
||
height: 568,
|
||
deviceScaleFactor: 2,
|
||
isMobile: true,
|
||
hasTouch: true,
|
||
},
|
||
{
|
||
name: 'mobile-m',
|
||
width: 375,
|
||
height: 667,
|
||
deviceScaleFactor: 2,
|
||
isMobile: true,
|
||
hasTouch: true,
|
||
},
|
||
{
|
||
name: 'tablet',
|
||
width: 768,
|
||
height: 1024,
|
||
deviceScaleFactor: 2,
|
||
isMobile: true,
|
||
hasTouch: true,
|
||
},
|
||
{
|
||
name: 'laptop',
|
||
width: 1024,
|
||
height: 768,
|
||
deviceScaleFactor: 1,
|
||
isMobile: false,
|
||
hasTouch: false,
|
||
},
|
||
{
|
||
name: 'desktop',
|
||
width: 1440,
|
||
height: 900,
|
||
deviceScaleFactor: 1,
|
||
isMobile: false,
|
||
hasTouch: false,
|
||
},
|
||
];
|
||
|
||
const TEST_URLS = [
|
||
'https://example.com',
|
||
'https://example.com/about',
|
||
'https://example.com/contact',
|
||
];
|
||
|
||
// ============================================================================
|
||
// Integration Tests
|
||
// ============================================================================
|
||
|
||
describe('Parallel Viewports - Multi-URL Multi-Viewport Capture', () => {
|
||
let memory: TestMemoryBackend;
|
||
let captureService: MockViewportCaptureService;
|
||
|
||
beforeEach(() => {
|
||
memory = new TestMemoryBackend();
|
||
captureService = new MockViewportCaptureService();
|
||
});
|
||
|
||
afterEach(() => {
|
||
memory.clear();
|
||
});
|
||
|
||
it('should capture 5 viewports × 3 URLs (15 captures total)', async () => {
|
||
const result = await captureService.captureAllViewports(
|
||
TEST_URLS,
|
||
STANDARD_VIEWPORTS
|
||
);
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(result.value?.totalCaptures).toBe(15);
|
||
expect(result.value?.captures).toHaveLength(15);
|
||
|
||
// Verify all combinations are captured
|
||
const combinations = new Set<string>();
|
||
for (const capture of result.value?.captures ?? []) {
|
||
const key = `${capture.url}:${capture.viewport.width}x${capture.viewport.height}`;
|
||
combinations.add(key);
|
||
}
|
||
|
||
expect(combinations.size).toBe(15);
|
||
});
|
||
|
||
it('should complete all captures in < 30s', async () => {
|
||
const startTime = Date.now();
|
||
|
||
const result = await captureService.captureAllViewports(
|
||
TEST_URLS,
|
||
STANDARD_VIEWPORTS
|
||
);
|
||
|
||
const totalTime = Date.now() - startTime;
|
||
|
||
expect(result.success).toBe(true);
|
||
expect(totalTime).toBeLessThan(30000); // 30 seconds
|
||
|
||
// With parallel execution and 200ms per capture, should complete much faster
|
||
// Sequential would take 15 × 200ms = 3000ms
|
||
// Parallel should take ~200-300ms
|
||
expect(result.value?.totalTimeMs).toBeLessThan(1000);
|
||
});
|
||
|
||
it('should produce consistent results across all viewports', async () => {
|
||
const result = await captureService.captureAllViewports(
|
||
TEST_URLS,
|
||
STANDARD_VIEWPORTS
|
||
);
|
||
|
||
expect(result.success).toBe(true);
|
||
|
||
// Group by URL
|
||
const capturesByUrl = new Map<string, CaptureResult[]>();
|
||
for (const capture of result.value?.captures ?? []) {
|
||
const existing = capturesByUrl.get(capture.url) ?? [];
|
||
existing.push(capture);
|
||
capturesByUrl.set(capture.url, existing);
|
||
}
|
||
|
||
// Each URL should have 5 viewport captures
|
||
for (const [url, captures] of capturesByUrl.entries()) {
|
||
expect(captures).toHaveLength(5);
|
||
|
||
// All captures for same URL should have consistent success pattern
|
||
const successCount = captures.filter((c) => c.success).length;
|
||
expect(successCount).toBeGreaterThan(0); // At least some should succeed
|
||
}
|
||
});
|
||
|
||
it('should cleanup resources after completion', async () => {
|
||
const result = await captureService.captureAllViewports(
|
||
TEST_URLS,
|
||
STANDARD_VIEWPORTS
|
||
);
|
||
|
||
expect(result.success).toBe(true);
|
||
|
||
// Store results in memory
|
||
await memory.set('parallel-capture:results', result.value);
|
||
|
||
// Verify results are stored
|
||
const stored = await memory.get<ParallelCaptureResult>(
|
||
'parallel-capture:results'
|
||
);
|
||
expect(stored).toBeDefined();
|
||
expect(stored?.totalCaptures).toBe(15);
|
||
|
||
// Simulate cleanup
|
||
memory.clear();
|
||
|
||
// Verify cleanup
|
||
const afterCleanup = await memory.get('parallel-capture:results');
|
||
expect(afterCleanup).toBeUndefined();
|
||
});
|
||
});
|
||
|
||
describe('Parallel Viewports - Performance Metrics', () => {
|
||
let memory: TestMemoryBackend;
|
||
let captureService: MockViewportCaptureService;
|
||
|
||
beforeEach(() => {
|
||
memory = new TestMemoryBackend();
|
||
captureService = new MockViewportCaptureService();
|
||
});
|
||
|
||
afterEach(() => {
|
||
memory.clear();
|
||
});
|
||
|
||
it('should track capture time per viewport', async () => {
|
||
const result = await captureService.captureAllViewports(
|
||
TEST_URLS,
|
||
STANDARD_VIEWPORTS
|
||
);
|
||
|
||
expect(result.success).toBe(true);
|
||
|
||
// All captures should have timing data
|
||
for (const capture of result.value?.captures ?? []) {
|
||
expect(capture.captureTimeMs).toBeGreaterThan(0);
|
||
expect(capture.captureTimeMs).toBeLessThan(1000); // Should be fast
|
||
}
|
||
|
||
// Average should be reasonable
|
||
expect(result.value?.avgCaptureTimeMs).toBeGreaterThan(0);
|
||
expect(result.value?.avgCaptureTimeMs).toBeLessThan(500);
|
||
});
|
||
|
||
it('should track success rate across all captures', async () => {
|
||
const result = await captureService.captureAllViewports(
|
||
TEST_URLS,
|
||
STANDARD_VIEWPORTS
|
||
);
|
||
|
||
expect(result.success).toBe(true);
|
||
|
||
const successRate = result.value
|
||
? result.value.successCount / result.value.totalCaptures
|
||
: 0;
|
||
|
||
// With 95% success rate simulation, should be reasonably high
|
||
// Using 0.75 threshold to account for statistical variance
|
||
expect(successRate).toBeGreaterThan(0.75);
|
||
|
||
// Store metrics
|
||
await memory.set('metrics:success-rate', {
|
||
successRate,
|
||
successCount: result.value?.successCount,
|
||
failedCount: result.value?.failedCount,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
const metrics = await memory.get<any>('metrics:success-rate');
|
||
expect(metrics?.successRate).toBe(successRate);
|
||
});
|
||
|
||
it('should identify slow captures', async () => {
|
||
const result = await captureService.captureAllViewports(
|
||
TEST_URLS,
|
||
STANDARD_VIEWPORTS
|
||
);
|
||
|
||
expect(result.success).toBe(true);
|
||
|
||
// Identify captures that took longer than average
|
||
const avgTime = result.value?.avgCaptureTimeMs ?? 0;
|
||
const slowCaptures = (result.value?.captures ?? []).filter(
|
||
(c) => c.captureTimeMs > avgTime * 1.5
|
||
);
|
||
|
||
// Store slow captures for analysis
|
||
if (slowCaptures.length > 0) {
|
||
await memory.set('metrics:slow-captures', {
|
||
count: slowCaptures.length,
|
||
avgTime,
|
||
slowCaptures: slowCaptures.map((c) => ({
|
||
url: c.url,
|
||
viewport: `${c.viewport.width}x${c.viewport.height}`,
|
||
timeMs: c.captureTimeMs,
|
||
})),
|
||
});
|
||
}
|
||
|
||
// Should have timing data even if no slow captures
|
||
expect(avgTime).toBeGreaterThan(0);
|
||
});
|
||
});
|
||
|
||
describe('Parallel Viewports - Error Handling', () => {
|
||
let memory: TestMemoryBackend;
|
||
let captureService: MockViewportCaptureService;
|
||
|
||
beforeEach(() => {
|
||
memory = new TestMemoryBackend();
|
||
captureService = new MockViewportCaptureService();
|
||
});
|
||
|
||
afterEach(() => {
|
||
memory.clear();
|
||
});
|
||
|
||
it('should handle partial failures gracefully', async () => {
|
||
const result = await captureService.captureAllViewports(
|
||
TEST_URLS,
|
||
STANDARD_VIEWPORTS
|
||
);
|
||
|
||
expect(result.success).toBe(true);
|
||
|
||
// Should have both successes and failures
|
||
const successCount = result.value?.successCount ?? 0;
|
||
const failedCount = result.value?.failedCount ?? 0;
|
||
|
||
expect(successCount).toBeGreaterThan(0);
|
||
// Failures are expected with 5% failure rate
|
||
// With 15 captures, we might have 0-3 failures
|
||
|
||
// Failed captures should have error messages
|
||
const failedCaptures = (result.value?.captures ?? []).filter(
|
||
(c) => !c.success
|
||
);
|
||
|
||
for (const failed of failedCaptures) {
|
||
expect(failed.error).toBeDefined();
|
||
}
|
||
});
|
||
|
||
it('should retry failed captures', async () => {
|
||
// First attempt
|
||
const firstResult = await captureService.captureAllViewports(
|
||
TEST_URLS,
|
||
STANDARD_VIEWPORTS
|
||
);
|
||
|
||
const failedCaptures = (firstResult.value?.captures ?? []).filter(
|
||
(c) => !c.success
|
||
);
|
||
|
||
if (failedCaptures.length > 0) {
|
||
// Retry failed captures
|
||
const retryPromises = failedCaptures.map((failed) =>
|
||
captureService.captureAtViewport(failed.url, failed.viewport)
|
||
);
|
||
|
||
const retryResults = await Promise.all(retryPromises);
|
||
|
||
// Some retries should succeed
|
||
const retriedSuccesses = retryResults.filter((r) => r.success && r.value?.success);
|
||
|
||
// With random failures, retries should improve success rate
|
||
expect(retriedSuccesses.length).toBeGreaterThanOrEqual(0);
|
||
}
|
||
});
|
||
|
||
it('should aggregate errors for reporting', async () => {
|
||
const result = await captureService.captureAllViewports(
|
||
TEST_URLS,
|
||
STANDARD_VIEWPORTS
|
||
);
|
||
|
||
expect(result.success).toBe(true);
|
||
|
||
const failedCaptures = (result.value?.captures ?? []).filter(
|
||
(c) => !c.success
|
||
);
|
||
|
||
// Aggregate errors by type
|
||
const errorCounts = new Map<string, number>();
|
||
for (const failed of failedCaptures) {
|
||
const error = failed.error ?? 'Unknown error';
|
||
const count = errorCounts.get(error) ?? 0;
|
||
errorCounts.set(error, count + 1);
|
||
}
|
||
|
||
// Store error report
|
||
await memory.set('error-report', {
|
||
totalErrors: failedCaptures.length,
|
||
errorsByType: Array.from(errorCounts.entries()).map(([error, count]) => ({
|
||
error,
|
||
count,
|
||
})),
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
const report = await memory.get<any>('error-report');
|
||
expect(report?.totalErrors).toBe(failedCaptures.length);
|
||
});
|
||
});
|
||
|
||
describe('Parallel Viewports - Resource Management', () => {
|
||
let memory: TestMemoryBackend;
|
||
let captureService: MockViewportCaptureService;
|
||
|
||
beforeEach(() => {
|
||
memory = new TestMemoryBackend();
|
||
captureService = new MockViewportCaptureService();
|
||
});
|
||
|
||
afterEach(() => {
|
||
memory.clear();
|
||
});
|
||
|
||
it('should track memory usage during parallel captures', async () => {
|
||
const initialMemory = process.memoryUsage();
|
||
|
||
const result = await captureService.captureAllViewports(
|
||
TEST_URLS,
|
||
STANDARD_VIEWPORTS
|
||
);
|
||
|
||
const finalMemory = process.memoryUsage();
|
||
|
||
expect(result.success).toBe(true);
|
||
|
||
// Calculate memory delta
|
||
const heapDelta = finalMemory.heapUsed - initialMemory.heapUsed;
|
||
|
||
// Store memory metrics
|
||
await memory.set('metrics:memory', {
|
||
initialHeap: initialMemory.heapUsed,
|
||
finalHeap: finalMemory.heapUsed,
|
||
heapDelta,
|
||
totalCaptures: result.value?.totalCaptures,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
const metrics = await memory.get<any>('metrics:memory');
|
||
expect(metrics?.totalCaptures).toBe(15);
|
||
});
|
||
|
||
it('should cleanup resources even on error', async () => {
|
||
let cleanupCalled = false;
|
||
|
||
try {
|
||
const result = await captureService.captureAllViewports(
|
||
TEST_URLS,
|
||
STANDARD_VIEWPORTS
|
||
);
|
||
|
||
// Simulate processing
|
||
if (result.success) {
|
||
await memory.set('results', result.value);
|
||
}
|
||
} finally {
|
||
// Cleanup should always happen
|
||
memory.clear();
|
||
cleanupCalled = true;
|
||
}
|
||
|
||
expect(cleanupCalled).toBe(true);
|
||
});
|
||
|
||
it('should limit concurrent captures', async () => {
|
||
// In production, we'd limit parallelism to avoid overwhelming resources
|
||
const maxConcurrent = 5;
|
||
let activeCaptures = 0;
|
||
let maxActiveCaptures = 0;
|
||
|
||
// Track active captures
|
||
const originalCapture = captureService.captureAtViewport.bind(captureService);
|
||
captureService.captureAtViewport = async (url: string, viewport: Viewport) => {
|
||
activeCaptures++;
|
||
maxActiveCaptures = Math.max(maxActiveCaptures, activeCaptures);
|
||
|
||
const result = await originalCapture(url, viewport);
|
||
|
||
activeCaptures--;
|
||
return result;
|
||
};
|
||
|
||
await captureService.captureAllViewports(TEST_URLS, STANDARD_VIEWPORTS);
|
||
|
||
// With Promise.all, all 15 would be concurrent
|
||
// In production, we'd use a semaphore or pool
|
||
expect(maxActiveCaptures).toBeLessThanOrEqual(15);
|
||
});
|
||
});
|