294 lines
9.0 KiB
TypeScript
294 lines
9.0 KiB
TypeScript
import { APICall } from './api-interceptor';
|
|
|
|
export interface ComparisonResult {
|
|
matches: boolean;
|
|
totalCalls: number;
|
|
matchedCalls: number;
|
|
missingCalls: EndpointMismatch[];
|
|
extraCalls: EndpointMismatch[];
|
|
outOfOrderCalls: OrderMismatch[];
|
|
timingVariances: TimingVariance[];
|
|
report: string;
|
|
}
|
|
|
|
export interface EndpointMismatch {
|
|
endpoint: string;
|
|
expectedCount: number;
|
|
actualCount: number;
|
|
}
|
|
|
|
export interface OrderMismatch {
|
|
endpoint: string;
|
|
expectedPosition: number;
|
|
actualPosition: number;
|
|
deviation: number;
|
|
}
|
|
|
|
export interface TimingVariance {
|
|
endpoint: string;
|
|
expectedTiming: number;
|
|
actualTiming: number;
|
|
variance: number;
|
|
}
|
|
|
|
/**
|
|
* Compare two API call sequences
|
|
*/
|
|
export function compareAPISequences(
|
|
actual: APICall[],
|
|
expected: APICall[]
|
|
): ComparisonResult {
|
|
const missing: EndpointMismatch[] = [];
|
|
const extra: EndpointMismatch[] = [];
|
|
const outOfOrder: OrderMismatch[] = [];
|
|
const timingVariances: TimingVariance[] = [];
|
|
|
|
// Build endpoint maps
|
|
const actualEndpoints = buildEndpointMap(actual);
|
|
const expectedEndpoints = buildEndpointMap(expected);
|
|
|
|
// Find missing calls (in expected but not in actual)
|
|
for (const [endpoint, expectedCalls] of expectedEndpoints.entries()) {
|
|
const actualCalls = actualEndpoints.get(endpoint) || [];
|
|
if (actualCalls.length === 0) {
|
|
missing.push({
|
|
endpoint,
|
|
expectedCount: expectedCalls.length,
|
|
actualCount: 0,
|
|
});
|
|
} else if (actualCalls.length < expectedCalls.length) {
|
|
missing.push({
|
|
endpoint,
|
|
expectedCount: expectedCalls.length,
|
|
actualCount: actualCalls.length,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Find extra calls (in actual but not in expected)
|
|
for (const [endpoint, actualCalls] of actualEndpoints.entries()) {
|
|
const expectedCalls = expectedEndpoints.get(endpoint) || [];
|
|
if (expectedCalls.length === 0) {
|
|
extra.push({
|
|
endpoint,
|
|
expectedCount: 0,
|
|
actualCount: actualCalls.length,
|
|
});
|
|
} else if (actualCalls.length > expectedCalls.length) {
|
|
extra.push({
|
|
endpoint,
|
|
expectedCount: expectedCalls.length,
|
|
actualCount: actualCalls.length,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check order
|
|
const orderMismatches = checkOrder(actual, expected);
|
|
outOfOrder.push(...orderMismatches);
|
|
|
|
// Check timing
|
|
const timings = checkTimingVariance(actual, expected);
|
|
timingVariances.push(...timings);
|
|
|
|
// Calculate match percentage
|
|
const totalExpected = expected.length;
|
|
const matched = totalExpected - missing.reduce((sum, m) => sum + (m.expectedCount - m.actualCount), 0);
|
|
const matchPercentage = totalExpected > 0 ? (matched / totalExpected) * 100 : 0;
|
|
|
|
// Generate report
|
|
const report = generateReport({
|
|
matches: matchPercentage >= 95,
|
|
totalCalls: actual.length,
|
|
matchedCalls: matched,
|
|
missingCalls: missing,
|
|
extraCalls: extra,
|
|
outOfOrderCalls: outOfOrder,
|
|
timingVariances: timingVariances,
|
|
report: '',
|
|
});
|
|
|
|
return {
|
|
matches: matchPercentage >= 95,
|
|
totalCalls: actual.length,
|
|
matchedCalls: matched,
|
|
missingCalls: missing,
|
|
extraCalls: extra,
|
|
outOfOrderCalls: outOfOrder,
|
|
timingVariances: timingVariances,
|
|
report,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Build a map of endpoint -> calls
|
|
*/
|
|
function buildEndpointMap(calls: APICall[]): Map<string, APICall[]> {
|
|
const map = new Map<string, APICall[]>();
|
|
for (const call of calls) {
|
|
const endpoint = `${call.method} ${call.pathname}`;
|
|
if (!map.has(endpoint)) {
|
|
map.set(endpoint, []);
|
|
}
|
|
map.get(endpoint)!.push(call);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
/**
|
|
* Check if calls are in the same order
|
|
*/
|
|
function checkOrder(actual: APICall[], expected: APICall[]): OrderMismatch[] {
|
|
const mismatches: OrderMismatch[] = [];
|
|
|
|
// Create a sequence of endpoints for both
|
|
const actualSequence = actual.map(c => `${c.method} ${c.pathname}`);
|
|
const expectedSequence = expected.map(c => `${c.method} ${c.pathname}`);
|
|
|
|
// For each expected endpoint, find its position in actual
|
|
for (let i = 0; i < expectedSequence.length; i++) {
|
|
const endpoint = expectedSequence[i];
|
|
const actualIndex = actualSequence.indexOf(endpoint);
|
|
|
|
if (actualIndex !== -1) {
|
|
const deviation = Math.abs(actualIndex - i);
|
|
// Only report if deviation is significant (more than 3 positions)
|
|
if (deviation > 3) {
|
|
mismatches.push({
|
|
endpoint,
|
|
expectedPosition: i,
|
|
actualPosition: actualIndex,
|
|
deviation,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return mismatches;
|
|
}
|
|
|
|
/**
|
|
* Check timing variance between calls
|
|
*/
|
|
function checkTimingVariance(actual: APICall[], expected: APICall[]): TimingVariance[] {
|
|
const variances: TimingVariance[] = [];
|
|
|
|
if (actual.length === 0 || expected.length === 0) {
|
|
return variances;
|
|
}
|
|
|
|
// Calculate relative timings (time from first call)
|
|
const actualStartTime = actual[0].timestamp;
|
|
const expectedStartTime = expected[0].timestamp;
|
|
|
|
const actualTimings = new Map<string, number[]>();
|
|
const expectedTimings = new Map<string, number[]>();
|
|
|
|
for (const call of actual) {
|
|
const endpoint = `${call.method} ${call.pathname}`;
|
|
const relativeTime = call.timestamp - actualStartTime;
|
|
if (!actualTimings.has(endpoint)) {
|
|
actualTimings.set(endpoint, []);
|
|
}
|
|
actualTimings.get(endpoint)!.push(relativeTime);
|
|
}
|
|
|
|
for (const call of expected) {
|
|
const endpoint = `${call.method} ${call.pathname}`;
|
|
const relativeTime = call.timestamp - expectedStartTime;
|
|
if (!expectedTimings.has(endpoint)) {
|
|
expectedTimings.set(endpoint, []);
|
|
}
|
|
expectedTimings.get(endpoint)!.push(relativeTime);
|
|
}
|
|
|
|
// Compare average timings
|
|
for (const [endpoint, expectedTimes] of expectedTimings.entries()) {
|
|
const actualTimes = actualTimings.get(endpoint);
|
|
if (actualTimes && actualTimes.length > 0) {
|
|
const avgExpected = expectedTimes.reduce((a, b) => a + b, 0) / expectedTimes.length;
|
|
const avgActual = actualTimes.reduce((a, b) => a + b, 0) / actualTimes.length;
|
|
const variance = Math.abs(avgActual - avgExpected);
|
|
|
|
// Only report if variance is significant (more than 500ms)
|
|
if (variance > 500) {
|
|
variances.push({
|
|
endpoint,
|
|
expectedTiming: avgExpected,
|
|
actualTiming: avgActual,
|
|
variance,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return variances;
|
|
}
|
|
|
|
/**
|
|
* Generate a human-readable comparison report
|
|
*/
|
|
function generateReport(result: ComparisonResult): string {
|
|
let report = '# API Sequence Comparison Report\n\n';
|
|
|
|
report += `## Summary\n\n`;
|
|
report += `- **Match Status:** ${result.matches ? '✅ PASS' : '❌ FAIL'}\n`;
|
|
report += `- **Total Calls:** ${result.totalCalls}\n`;
|
|
report += `- **Matched Calls:** ${result.matchedCalls}\n`;
|
|
report += `- **Match Percentage:** ${((result.matchedCalls / result.totalCalls) * 100).toFixed(1)}%\n\n`;
|
|
|
|
if (result.missingCalls.length > 0) {
|
|
report += `## ❌ Missing API Calls\n\n`;
|
|
report += `The following expected API calls are missing or called fewer times:\n\n`;
|
|
for (const missing of result.missingCalls) {
|
|
report += `- **${missing.endpoint}**\n`;
|
|
report += ` - Expected: ${missing.expectedCount} call(s)\n`;
|
|
report += ` - Actual: ${missing.actualCount} call(s)\n`;
|
|
report += ` - Missing: ${missing.expectedCount - missing.actualCount} call(s)\n\n`;
|
|
}
|
|
}
|
|
|
|
if (result.extraCalls.length > 0) {
|
|
report += `## ⚠️ Extra API Calls\n\n`;
|
|
report += `The following API calls were made but not expected:\n\n`;
|
|
for (const extra of result.extraCalls) {
|
|
report += `- **${extra.endpoint}**\n`;
|
|
report += ` - Expected: ${extra.expectedCount} call(s)\n`;
|
|
report += ` - Actual: ${extra.actualCount} call(s)\n`;
|
|
report += ` - Extra: ${extra.actualCount - extra.expectedCount} call(s)\n\n`;
|
|
}
|
|
}
|
|
|
|
if (result.outOfOrderCalls.length > 0) {
|
|
report += `## ⚠️ Out of Order Calls\n\n`;
|
|
report += `The following API calls occurred in a different order:\n\n`;
|
|
for (const order of result.outOfOrderCalls) {
|
|
report += `- **${order.endpoint}**\n`;
|
|
report += ` - Expected position: ${order.expectedPosition}\n`;
|
|
report += ` - Actual position: ${order.actualPosition}\n`;
|
|
report += ` - Deviation: ${order.deviation} positions\n\n`;
|
|
}
|
|
}
|
|
|
|
if (result.timingVariances.length > 0) {
|
|
report += `## ⏱️ Timing Variances\n\n`;
|
|
report += `The following API calls have significant timing differences:\n\n`;
|
|
for (const timing of result.timingVariances) {
|
|
report += `- **${timing.endpoint}**\n`;
|
|
report += ` - Expected timing: ${timing.expectedTiming.toFixed(0)}ms\n`;
|
|
report += ` - Actual timing: ${timing.actualTiming.toFixed(0)}ms\n`;
|
|
report += ` - Variance: ${timing.variance.toFixed(0)}ms\n\n`;
|
|
}
|
|
}
|
|
|
|
if (result.matches) {
|
|
report += `## ✅ Conclusion\n\n`;
|
|
report += `The API sequence matches the expected baseline with acceptable variance.\n`;
|
|
} else {
|
|
report += `## ❌ Conclusion\n\n`;
|
|
report += `The API sequence does NOT match the expected baseline. Please review the mismatches above.\n`;
|
|
}
|
|
|
|
return report;
|
|
}
|