Skip to main content
Glama
crash.test.ts20.9 kB
/** * Unit tests for Crash Analysis tools * Tests crash log parsing, symbolication, and pattern detection */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { readFileSync } from 'fs'; import { join } from 'path'; import { CrashReport, CrashPattern, StackFrame, ThreadInfo, detectCrashPatterns, generateCrashSummary, generateCrashSuggestions, } from '../../../src/models/crash-report.js'; // Mock crash report for testing function createMockCrashReport(overrides: Partial<CrashReport> = {}): CrashReport { const crashedThread: ThreadInfo = { index: 0, name: 'main', crashed: true, frames: [ { index: 0, binary: 'TestApp', address: '0x100001250', symbol: '_$s7TestApp11ViewControllerC10loadViewyyF', isAppCode: true, }, { index: 1, binary: 'TestApp', address: '0x100001020', symbol: '_$s7TestApp11ViewControllerC11viewDidLoadyyF', isAppCode: true, }, { index: 2, binary: 'UIKitCore', address: '0x180100100', symbol: '-[UIViewController loadViewIfRequired]', isAppCode: false, }, ], }; return { timestamp: new Date('2025-01-15T14:30:00Z'), platform: 'ios', processName: 'TestApp', bundleId: 'com.example.testapp', appVersion: '1.0.0', deviceModel: 'iPhone14,5', osVersion: 'iOS 17.2', exception: { type: 'EXC_BAD_ACCESS', codes: 'KERN_INVALID_ADDRESS', signal: 'SIGSEGV', faultAddress: '0x0000000000000000', }, threads: [crashedThread], crashedThread, binaryImages: [ { name: 'TestApp', arch: 'arm64', uuid: 'A1B2C3D4-E5F6-7890-ABCD-EF1234567890', loadAddress: '0x100000000', path: '/var/containers/Bundle/Application/UUID/TestApp.app/TestApp', }, ], isSymbolicated: true, patterns: [], ...overrides, }; } describe('Crash Report Models', () => { describe('CrashReport structure', () => { it('should have required fields', () => { const report = createMockCrashReport(); expect(report.timestamp).toBeDefined(); expect(report.platform).toBe('ios'); expect(report.processName).toBe('TestApp'); expect(report.exception).toBeDefined(); expect(report.crashedThread).toBeDefined(); expect(report.threads).toHaveLength(1); }); it('should include exception details', () => { const report = createMockCrashReport(); expect(report.exception.type).toBe('EXC_BAD_ACCESS'); expect(report.exception.codes).toBe('KERN_INVALID_ADDRESS'); expect(report.exception.signal).toBe('SIGSEGV'); }); it('should include stack frames in crashed thread', () => { const report = createMockCrashReport(); expect(report.crashedThread.frames).toHaveLength(3); expect(report.crashedThread.crashed).toBe(true); }); it('should distinguish app code from system frames', () => { const report = createMockCrashReport(); const frames = report.crashedThread.frames; expect(frames[0].isAppCode).toBe(true); expect(frames[1].isAppCode).toBe(true); expect(frames[2].isAppCode).toBe(false); }); }); describe('StackFrame structure', () => { it('should contain frame information', () => { const frame: StackFrame = { index: 0, binary: 'TestApp', address: '0x100001250', symbol: 'myFunction', offset: 28, file: 'ViewController.swift', line: 42, isAppCode: true, }; expect(frame.index).toBe(0); expect(frame.binary).toBe('TestApp'); expect(frame.symbol).toBe('myFunction'); expect(frame.file).toBe('ViewController.swift'); expect(frame.line).toBe(42); }); it('should allow optional source location', () => { const frame: StackFrame = { index: 0, binary: 'TestApp', address: '0x100001250', symbol: '0x100001250', // Unsymbolicated isAppCode: true, }; expect(frame.file).toBeUndefined(); expect(frame.line).toBeUndefined(); }); }); }); describe('Pattern Detection', () => { describe('detectCrashPatterns', () => { it('should detect null pointer dereference', () => { const report = createMockCrashReport({ exception: { type: 'EXC_BAD_ACCESS', codes: 'KERN_INVALID_ADDRESS', faultAddress: '0x0', }, }); const patterns = detectCrashPatterns(report); expect(patterns.length).toBeGreaterThan(0); expect(patterns[0].id).toBe('exc_bad_access_null'); expect(patterns[0].name).toBe('Null Pointer Dereference'); expect(patterns[0].severity).toBe('critical'); }); it('should detect invalid memory access', () => { const report = createMockCrashReport({ exception: { type: 'EXC_BAD_ACCESS', codes: 'KERN_INVALID_ADDRESS at 0x1234', faultAddress: '0x1234', }, }); const patterns = detectCrashPatterns(report); const invalidAccess = patterns.find((p) => p.id === 'exc_bad_access_kern_invalid'); expect(invalidAccess).toBeDefined(); expect(invalidAccess?.severity).toBe('critical'); }); it('should detect SIGABRT with assertion', () => { const crashedThread: ThreadInfo = { index: 0, crashed: true, frames: [ { index: 0, binary: 'TestApp', address: '0x100001000', symbol: 'swift_fatalError', isAppCode: false, }, { index: 1, binary: 'TestApp', address: '0x100001100', symbol: 'assertionFailure', isAppCode: true, }, ], }; const report = createMockCrashReport({ exception: { type: 'SIGABRT' }, crashedThread, threads: [crashedThread], }); const patterns = detectCrashPatterns(report); const assertion = patterns.find((p) => p.id === 'sigabrt_assertion'); expect(assertion).toBeDefined(); expect(assertion?.name).toBe('Assertion Failure'); }); it('should detect uncaught exception', () => { const crashedThread: ThreadInfo = { index: 0, crashed: true, frames: [ { index: 0, binary: 'libobjc.A.dylib', address: '0x180001000', symbol: 'objc_exception_throw', isAppCode: false, }, { index: 1, binary: 'TestApp', address: '0x100001100', symbol: '-[TestClass performAction]', isAppCode: true, }, ], }; const report = createMockCrashReport({ exception: { type: 'SIGABRT' }, crashedThread, threads: [crashedThread], }); const patterns = detectCrashPatterns(report); const uncaught = patterns.find((p) => p.id === 'sigabrt_uncaught_exception'); expect(uncaught).toBeDefined(); expect(uncaught?.severity).toBe('high'); }); it('should detect watchdog timeout', () => { const report = createMockCrashReport({ exception: { type: 'EXC_CRASH' }, rawLog: 'Application terminated due to 8badf00d', }); const patterns = detectCrashPatterns(report); const watchdog = patterns.find((p) => p.id === 'watchdog_timeout'); expect(watchdog).toBeDefined(); expect(watchdog?.name).toBe('Watchdog Timeout'); expect(watchdog?.severity).toBe('critical'); }); it('should detect stack overflow', () => { // Create a thread with many repeating frames const frames: StackFrame[] = []; for (let i = 0; i < 100; i++) { frames.push({ index: i, binary: 'TestApp', address: `0x10000${1000 + i}`, symbol: 'recursiveFunction', isAppCode: true, }); } const crashedThread: ThreadInfo = { index: 0, crashed: true, frames, }; const report = createMockCrashReport({ exception: { type: 'EXC_BAD_ACCESS' }, crashedThread, threads: [crashedThread], }); const patterns = detectCrashPatterns(report); const overflow = patterns.find((p) => p.id === 'stack_overflow'); expect(overflow).toBeDefined(); expect(overflow?.name).toBe('Stack Overflow'); }); it('should detect Swift runtime failure', () => { const crashedThread: ThreadInfo = { index: 0, crashed: true, frames: [ { index: 0, binary: 'libswiftCore.dylib', address: '0x180001000', symbol: 'swift_fatalError', isAppCode: false, }, { index: 1, binary: 'TestApp', address: '0x100001100', symbol: 'ViewModel.loadData()', isAppCode: true, }, ], }; const report = createMockCrashReport({ exception: { type: 'SIGABRT' }, crashedThread, threads: [crashedThread], }); const patterns = detectCrashPatterns(report); const swift = patterns.find((p) => p.id === 'swift_runtime_failure'); expect(swift).toBeDefined(); }); it('should sort patterns by severity and confidence', () => { const crashedThread: ThreadInfo = { index: 0, crashed: true, frames: [ { index: 0, binary: 'libdispatch.dylib', address: '0x180001000', symbol: 'dispatch_sync', isAppCode: false, }, ], }; const report = createMockCrashReport({ exception: { type: 'EXC_BAD_ACCESS', codes: 'KERN_INVALID_ADDRESS', faultAddress: '0x0', }, crashedThread, threads: [crashedThread], }); const patterns = detectCrashPatterns(report); // Critical patterns should come first if (patterns.length >= 2) { const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 }; expect(severityOrder[patterns[0].severity]).toBeLessThanOrEqual( severityOrder[patterns[1].severity] ); } }); it('should return empty array for unrecognized crash', () => { const report = createMockCrashReport({ exception: { type: 'UNKNOWN_EXCEPTION' }, }); const patterns = detectCrashPatterns(report); // May or may not find patterns, but should not throw expect(Array.isArray(patterns)).toBe(true); }); it('should include confidence scores', () => { const report = createMockCrashReport({ exception: { type: 'EXC_BAD_ACCESS', faultAddress: '0x0', }, isSymbolicated: true, }); const patterns = detectCrashPatterns(report); if (patterns.length > 0) { expect(patterns[0].confidence).toBeGreaterThan(0); expect(patterns[0].confidence).toBeLessThanOrEqual(1); } }); }); }); describe('Crash Summary Generation', () => { describe('generateCrashSummary', () => { it('should generate markdown summary', () => { const report = createMockCrashReport(); report.patterns = detectCrashPatterns(report); const summary = generateCrashSummary(report); expect(summary).toContain('## Crash Summary'); expect(summary).toContain('TestApp'); expect(summary).toContain('EXC_BAD_ACCESS'); }); it('should include device and OS info', () => { const report = createMockCrashReport({ deviceModel: 'iPhone15,2', osVersion: 'iOS 18.0', }); const summary = generateCrashSummary(report); expect(summary).toContain('iPhone15,2'); expect(summary).toContain('iOS 18.0'); }); it('should include crashed thread info', () => { const report = createMockCrashReport(); const summary = generateCrashSummary(report); expect(summary).toContain('Crashed Thread'); expect(summary).toContain('App Code'); }); it('should include app code frames', () => { const report = createMockCrashReport(); const summary = generateCrashSummary(report); expect(summary).toContain('ViewControllerC10loadView'); }); it('should include detected patterns when present', () => { const report = createMockCrashReport({ exception: { type: 'EXC_BAD_ACCESS', faultAddress: '0x0', }, }); report.patterns = detectCrashPatterns(report); const summary = generateCrashSummary(report); if (report.patterns.length > 0) { expect(summary).toContain('Detected Patterns'); expect(summary).toContain(report.patterns[0].name); } }); }); }); describe('Suggestion Generation', () => { describe('generateCrashSuggestions', () => { it('should generate suggestions from patterns', () => { const patterns: CrashPattern[] = [ { id: 'exc_bad_access_null', name: 'Null Pointer Dereference', severity: 'critical', description: 'Null pointer access', likelyCause: 'Force unwrap', suggestion: 'Use optional binding', confidence: 0.9, }, ]; const suggestions = generateCrashSuggestions(patterns); expect(suggestions).toContain('Use optional binding'); }); it('should add default suggestions when no patterns', () => { const suggestions = generateCrashSuggestions([]); expect(suggestions.length).toBeGreaterThan(0); expect(suggestions.some((s) => s.includes('symbolication'))).toBe(true); }); it('should prioritize critical crashes', () => { const patterns: CrashPattern[] = [ { id: 'critical_crash', name: 'Critical Crash', severity: 'critical', description: 'Critical issue', likelyCause: 'Unknown', suggestion: 'Investigate immediately', confidence: 0.9, }, ]; const suggestions = generateCrashSuggestions(patterns); expect(suggestions.some((s) => s.toLowerCase().includes('critical'))).toBe(true); }); it('should deduplicate suggestions', () => { const patterns: CrashPattern[] = [ { id: 'pattern1', name: 'Pattern 1', severity: 'high', description: 'Issue 1', likelyCause: 'Cause', suggestion: 'Same suggestion', confidence: 0.8, }, { id: 'pattern2', name: 'Pattern 2', severity: 'high', description: 'Issue 2', likelyCause: 'Cause', suggestion: 'Same suggestion', confidence: 0.7, }, ]; const suggestions = generateCrashSuggestions(patterns); const sameCount = suggestions.filter((s) => s === 'Same suggestion').length; expect(sameCount).toBe(1); }); }); }); describe('Crash Log Parser', () => { describe('IPS format parsing', () => { it('should parse JSON IPS format', () => { // This will be implemented by the parser module const ipsPath = join(__dirname, '../../mocks/crash-reports/sample-crash.ips'); const content = readFileSync(ipsPath, 'utf-8'); const ipsData = JSON.parse(content); expect(ipsData.app_name).toBe('TestApp'); expect(ipsData.bundle_id).toBe('com.example.testapp'); expect(ipsData.exception.type).toBe('EXC_BAD_ACCESS'); expect(ipsData.threads).toHaveLength(2); }); it('should have thread frames', () => { const ipsPath = join(__dirname, '../../mocks/crash-reports/sample-crash.ips'); const content = readFileSync(ipsPath, 'utf-8'); const ipsData = JSON.parse(content); const crashedThread = ipsData.threads[0]; expect(crashedThread.crashed).toBe(true); expect(crashedThread.frames.length).toBeGreaterThan(0); }); it('should have binary images for symbolication', () => { const ipsPath = join(__dirname, '../../mocks/crash-reports/sample-crash.ips'); const content = readFileSync(ipsPath, 'utf-8'); const ipsData = JSON.parse(content); expect(ipsData.binary_images).toHaveLength(2); expect(ipsData.binary_images[0].uuid).toBeDefined(); }); }); describe('Classic crash format parsing', () => { it('should read classic crash format', () => { const crashPath = join(__dirname, '../../mocks/crash-reports/sample-crash-classic.crash'); const content = readFileSync(crashPath, 'utf-8'); expect(content).toContain('Incident Identifier:'); expect(content).toContain('Exception Type: EXC_BAD_ACCESS'); expect(content).toContain('Thread 0 Crashed:'); }); it('should contain stack trace', () => { const crashPath = join(__dirname, '../../mocks/crash-reports/sample-crash-classic.crash'); const content = readFileSync(crashPath, 'utf-8'); expect(content).toContain('0 TestApp'); expect(content).toContain('ViewControllerC10loadView'); }); it('should contain binary images section', () => { const crashPath = join(__dirname, '../../mocks/crash-reports/sample-crash-classic.crash'); const content = readFileSync(crashPath, 'utf-8'); expect(content).toContain('Binary Images:'); expect(content).toContain('arm64'); }); }); }); describe('Symbolication', () => { describe('atos wrapper', () => { it('should format atos command correctly', () => { // Test the command construction (actual atos won't run in tests) const dsymPath = '/path/to/TestApp.dSYM'; const arch = 'arm64'; const loadAddress = '0x100000000'; const addresses = ['0x100001250', '0x100001020']; const expectedCommand = `atos -arch ${arch} -o "${dsymPath}/Contents/Resources/DWARF/TestApp" -l ${loadAddress} ${addresses.join(' ')}`; expect(expectedCommand).toContain('-arch arm64'); expect(expectedCommand).toContain('-l 0x100000000'); expect(expectedCommand).toContain('0x100001250'); }); it('should mark frames as symbolicated when source info available', () => { const frame: StackFrame = { index: 0, binary: 'TestApp', address: '0x100001250', symbol: 'loadView()', file: 'ViewController.swift', line: 42, isAppCode: true, }; expect(frame.file).toBeDefined(); expect(frame.line).toBeDefined(); }); }); }); describe('analyze_crash tool', () => { describe('tool registration', () => { it('should define required input schema', () => { const expectedSchema = { crashLogPath: { type: 'string', description: expect.any(String) }, dsymPath: { type: 'string', description: expect.any(String) }, bundleId: { type: 'string', description: expect.any(String) }, }; // The actual schema validation will be done when the tool is implemented expect(expectedSchema.crashLogPath.type).toBe('string'); expect(expectedSchema.dsymPath.type).toBe('string'); }); }); describe('tool execution flow', () => { it('should handle missing crash log', async () => { // Mock the expected behavior const result = { success: false, error: 'Crash log not found', summary: '', patterns: [], suggestions: [], durationMs: 0, }; expect(result.success).toBe(false); expect(result.error).toContain('not found'); }); it('should handle missing dSYM (unsymbolicated analysis)', () => { // Even without dSYM, we should be able to analyze const report = createMockCrashReport({ isSymbolicated: false }); expect(report.isSymbolicated).toBe(false); // Pattern detection should still work const patterns = detectCrashPatterns(report); expect(Array.isArray(patterns)).toBe(true); }); it('should return structured analysis result', () => { const report = createMockCrashReport(); report.patterns = detectCrashPatterns(report); const result = { success: true, report, summary: generateCrashSummary(report), patterns: report.patterns, suggestions: generateCrashSuggestions(report.patterns), durationMs: 150, }; expect(result.success).toBe(true); expect(result.report).toBeDefined(); expect(result.summary).toContain('Crash Summary'); expect(Array.isArray(result.patterns)).toBe(true); expect(Array.isArray(result.suggestions)).toBe(true); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/abd3lraouf/specter-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server