import type { IDiffResult, IDiffChange } from '../dist_ts_tapbundle_protocol/index.js'; /** * Generate a diff between two values */ export function generateDiff(expected: any, actual: any, context: number = 3): IDiffResult | null { // Handle same values if (expected === actual) { return null; } // Determine diff type based on values if (typeof expected === 'string' && typeof actual === 'string') { return generateStringDiff(expected, actual, context); } else if (Array.isArray(expected) && Array.isArray(actual)) { return generateArrayDiff(expected, actual); } else if (expected && actual && typeof expected === 'object' && typeof actual === 'object') { return generateObjectDiff(expected, actual); } else { return generatePrimitiveDiff(expected, actual); } } /** * Generate diff for primitive values */ function generatePrimitiveDiff(expected: any, actual: any): IDiffResult { return { type: 'primitive', changes: [{ type: 'modify', oldValue: expected, newValue: actual }] }; } /** * Generate diff for strings (line-by-line) */ function generateStringDiff(expected: string, actual: string, context: number): IDiffResult { const expectedLines = expected.split('\n'); const actualLines = actual.split('\n'); const changes: IDiffChange[] = []; // Simple line-by-line diff const maxLines = Math.max(expectedLines.length, actualLines.length); for (let i = 0; i < maxLines; i++) { const expectedLine = expectedLines[i]; const actualLine = actualLines[i]; if (expectedLine === undefined) { changes.push({ type: 'add', line: i, content: actualLine }); } else if (actualLine === undefined) { changes.push({ type: 'remove', line: i, content: expectedLine }); } else if (expectedLine !== actualLine) { changes.push({ type: 'remove', line: i, content: expectedLine }); changes.push({ type: 'add', line: i, content: actualLine }); } } return { type: 'string', changes, context }; } /** * Generate diff for arrays */ function generateArrayDiff(expected: any[], actual: any[]): IDiffResult { const changes: IDiffChange[] = []; const maxLength = Math.max(expected.length, actual.length); for (let i = 0; i < maxLength; i++) { const expectedItem = expected[i]; const actualItem = actual[i]; if (i >= expected.length) { changes.push({ type: 'add', path: [String(i)], newValue: actualItem }); } else if (i >= actual.length) { changes.push({ type: 'remove', path: [String(i)], oldValue: expectedItem }); } else if (!deepEqual(expectedItem, actualItem)) { changes.push({ type: 'modify', path: [String(i)], oldValue: expectedItem, newValue: actualItem }); } } return { type: 'array', changes }; } /** * Generate diff for objects */ function generateObjectDiff(expected: any, actual: any): IDiffResult { const changes: IDiffChange[] = []; const allKeys = new Set([...Object.keys(expected), ...Object.keys(actual)]); for (const key of allKeys) { const expectedValue = expected[key]; const actualValue = actual[key]; if (!(key in expected)) { changes.push({ type: 'add', path: [key], newValue: actualValue }); } else if (!(key in actual)) { changes.push({ type: 'remove', path: [key], oldValue: expectedValue }); } else if (!deepEqual(expectedValue, actualValue)) { changes.push({ type: 'modify', path: [key], oldValue: expectedValue, newValue: actualValue }); } } return { type: 'object', changes }; } /** * Deep equality check */ function deepEqual(a: any, b: any): boolean { if (a === b) return true; if (a === null || b === null) return false; if (typeof a !== typeof b) return false; if (typeof a === 'object') { if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; return a.every((item, index) => deepEqual(item, b[index])); } const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; return keysA.every(key => deepEqual(a[key], b[key])); } return false; }