diff --git a/.gitignore b/.gitignore index 4f9cbf5..6f2ddc2 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ samples/rust/target/ # Beads workspace state .beads/ +.runtime/ diff --git a/src/adapters/adapters.test.ts b/src/adapters/adapters.test.ts new file mode 100644 index 0000000..7f15446 --- /dev/null +++ b/src/adapters/adapters.test.ts @@ -0,0 +1,386 @@ +/** + * Unit tests for debug adapter configurations + */ + +import * as path from 'node:path'; +import { describe, it, expect } from 'vitest'; +import { nodeAdapter } from './node.js'; +import { debugpyAdapter } from './debugpy.js'; +import { netcoredbgAdapter } from './netcoredbg.js'; +import type { LaunchOptions, AttachOptions } from './base.js'; + +describe('Node.js Adapter', () => { + describe('properties', () => { + it('has correct adapter ID', () => { + expect(nodeAdapter.id).toBe('pwa-node'); + }); + + it('has correct adapter name', () => { + expect(nodeAdapter.name).toBe('node'); + }); + + it('uses socket transport', () => { + expect(nodeAdapter.transport).toBe('socket'); + }); + + it('has exception filters defined', () => { + expect(nodeAdapter.exceptionFilters).toBeDefined(); + expect(nodeAdapter.exceptionFilters).toContain('all'); + expect(nodeAdapter.exceptionFilters).toContain('uncaught'); + }); + }); + + describe('launchConfig', () => { + it('creates basic launch configuration', () => { + const options: LaunchOptions = { + program: 'test.js', + }; + + const config = nodeAdapter.launchConfig(options); + + expect(config.type).toBe('pwa-node'); + expect(config.request).toBe('launch'); + expect(config.program).toContain('test.js'); + }); + + it('resolves program path to absolute', () => { + const options: LaunchOptions = { + program: 'src/index.ts', + }; + + const config = nodeAdapter.launchConfig(options); + + expect(path.isAbsolute(config.program as string)).toBe(true); + }); + + it('includes args when provided', () => { + const options: LaunchOptions = { + program: 'test.js', + args: ['--port', '3000'], + }; + + const config = nodeAdapter.launchConfig(options); + + expect(config.args).toEqual(['--port', '3000']); + }); + + it('uses cwd when provided', () => { + const options: LaunchOptions = { + program: 'test.js', + cwd: '/custom/working/dir', + }; + + const config = nodeAdapter.launchConfig(options); + + expect(config.cwd).toBe('/custom/working/dir'); + }); + + it('defaults cwd to program directory', () => { + const options: LaunchOptions = { + program: '/app/src/index.js', + }; + + const config = nodeAdapter.launchConfig(options); + + expect(config.cwd).toBe('/app/src'); + }); + + it('includes environment variables', () => { + const options: LaunchOptions = { + program: 'test.js', + env: { NODE_ENV: 'development', DEBUG: 'true' }, + }; + + const config = nodeAdapter.launchConfig(options); + + expect(config.env).toEqual({ NODE_ENV: 'development', DEBUG: 'true' }); + }); + + it('sets stopOnEntry when requested', () => { + const options: LaunchOptions = { + program: 'test.js', + stopAtEntry: true, + }; + + const config = nodeAdapter.launchConfig(options); + + expect(config.stopOnEntry).toBe(true); + }); + + it('includes js-debug specific options', () => { + const options: LaunchOptions = { + program: 'test.js', + }; + + const config = nodeAdapter.launchConfig(options); + + expect(config.skipFiles).toBeDefined(); + expect(config.resolveSourceMapLocations).toBeDefined(); + expect(config.autoAttachChildProcesses).toBe(false); + }); + }); + + describe('attachConfig', () => { + it('creates basic attach configuration', () => { + const options: AttachOptions = { + pid: 12345, + }; + + const config = nodeAdapter.attachConfig(options); + + expect(config.type).toBe('pwa-node'); + expect(config.request).toBe('attach'); + expect(config.processId).toBe(12345); + }); + + it('uses default port when not specified', () => { + const options: AttachOptions = {}; + + const config = nodeAdapter.attachConfig(options); + + expect(config.port).toBe(9229); + }); + + it('uses custom port when provided', () => { + const options: AttachOptions = { + port: 9230, + }; + + const config = nodeAdapter.attachConfig(options); + + expect(config.port).toBe(9230); + }); + + it('uses default host when not specified', () => { + const options: AttachOptions = {}; + + const config = nodeAdapter.attachConfig(options); + + expect(config.host).toBe('localhost'); + }); + + it('uses custom host when provided', () => { + const options: AttachOptions = { + host: '192.168.1.100', + }; + + const config = nodeAdapter.attachConfig(options); + + expect(config.host).toBe('192.168.1.100'); + }); + }); +}); + +describe('Python (debugpy) Adapter', () => { + describe('properties', () => { + it('has correct adapter ID', () => { + expect(debugpyAdapter.id).toBe('debugpy'); + }); + + it('has correct adapter name', () => { + expect(debugpyAdapter.name).toBe('debugpy'); + }); + + it('requires launch first (for DAP flow)', () => { + expect(debugpyAdapter.requiresLaunchFirst).toBe(true); + }); + + it('has exception filters defined', () => { + expect(debugpyAdapter.exceptionFilters).toBeDefined(); + expect(debugpyAdapter.exceptionFilters).toContain('raised'); + expect(debugpyAdapter.exceptionFilters).toContain('uncaught'); + expect(debugpyAdapter.exceptionFilters).toContain('userUnhandled'); + }); + }); + + describe('launchConfig', () => { + it('creates basic launch configuration', () => { + const options: LaunchOptions = { + program: 'test.py', + }; + + const config = debugpyAdapter.launchConfig(options); + + expect(config.type).toBe('debugpy'); + expect(config.request).toBe('launch'); + expect(config.program).toContain('test.py'); + }); + + it('resolves program path to absolute', () => { + const options: LaunchOptions = { + program: 'src/main.py', + }; + + const config = debugpyAdapter.launchConfig(options); + + expect(path.isAbsolute(config.program as string)).toBe(true); + }); + + it('includes args when provided', () => { + const options: LaunchOptions = { + program: 'test.py', + args: ['--config', 'settings.json'], + }; + + const config = debugpyAdapter.launchConfig(options); + + expect(config.args).toEqual(['--config', 'settings.json']); + }); + + it('sets justMyCode to false by default', () => { + const options: LaunchOptions = { + program: 'test.py', + }; + + const config = debugpyAdapter.launchConfig(options); + + expect(config.justMyCode).toBe(false); + }); + + it('uses cwd when provided', () => { + const options: LaunchOptions = { + program: 'test.py', + cwd: '/project/src', + }; + + const config = debugpyAdapter.launchConfig(options); + + expect(config.cwd).toBe('/project/src'); + }); + }); + + describe('attachConfig', () => { + it('creates basic attach configuration', () => { + const options: AttachOptions = { + pid: 54321, + }; + + const config = debugpyAdapter.attachConfig(options); + + expect(config.type).toBe('debugpy'); + expect(config.request).toBe('attach'); + expect(config.processId).toBe(54321); + }); + + it('uses default port when not specified', () => { + const options: AttachOptions = {}; + + const config = debugpyAdapter.attachConfig(options); + + expect(config.port).toBe(5678); + }); + + it('uses custom port when provided', () => { + const options: AttachOptions = { + port: 5679, + }; + + const config = debugpyAdapter.attachConfig(options); + + expect(config.port).toBe(5679); + }); + }); +}); + +describe('.NET (netcoredbg) Adapter', () => { + describe('properties', () => { + it('has correct adapter ID', () => { + expect(netcoredbgAdapter.id).toBe('coreclr'); + }); + + it('has correct adapter name', () => { + expect(netcoredbgAdapter.name).toBe('netcoredbg'); + }); + + it('uses vscode interpreter mode', () => { + expect(netcoredbgAdapter.args).toContain('--interpreter=vscode'); + }); + + it('has exception filters defined', () => { + expect(netcoredbgAdapter.exceptionFilters).toBeDefined(); + expect(netcoredbgAdapter.exceptionFilters).toContain('all'); + expect(netcoredbgAdapter.exceptionFilters).toContain('user-unhandled'); + }); + }); + + describe('launchConfig', () => { + it('creates basic launch configuration', () => { + const options: LaunchOptions = { + program: 'bin/Debug/net8.0/MyApp.dll', + }; + + const config = netcoredbgAdapter.launchConfig(options); + + expect(config.type).toBe('coreclr'); + expect(config.request).toBe('launch'); + expect(config.program).toContain('MyApp.dll'); + }); + + it('resolves program path to absolute', () => { + const options: LaunchOptions = { + program: 'bin/Debug/MyApp.dll', + }; + + const config = netcoredbgAdapter.launchConfig(options); + + expect(path.isAbsolute(config.program as string)).toBe(true); + }); + + it('includes args when provided', () => { + const options: LaunchOptions = { + program: 'MyApp.dll', + args: ['--environment', 'Development'], + }; + + const config = netcoredbgAdapter.launchConfig(options); + + expect(config.args).toEqual(['--environment', 'Development']); + }); + + it('uses cwd when provided', () => { + const options: LaunchOptions = { + program: 'MyApp.dll', + cwd: '/app/bin', + }; + + const config = netcoredbgAdapter.launchConfig(options); + + expect(config.cwd).toBe('/app/bin'); + }); + + it('defaults cwd to program directory', () => { + const options: LaunchOptions = { + program: '/app/bin/Debug/MyApp.dll', + }; + + const config = netcoredbgAdapter.launchConfig(options); + + expect(config.cwd).toBe('/app/bin/Debug'); + }); + + it('sets stopAtEntry when requested', () => { + const options: LaunchOptions = { + program: 'MyApp.dll', + stopAtEntry: true, + }; + + const config = netcoredbgAdapter.launchConfig(options); + + expect(config.stopAtEntry).toBe(true); + }); + }); + + describe('attachConfig', () => { + it('creates basic attach configuration', () => { + const options: AttachOptions = { + pid: 99999, + }; + + const config = netcoredbgAdapter.attachConfig(options); + + expect(config.type).toBe('coreclr'); + expect(config.request).toBe('attach'); + expect(config.processId).toBe(99999); + }); + }); +}); diff --git a/src/output/formatter.test.ts b/src/output/formatter.test.ts index 4011696..bb38f58 100644 --- a/src/output/formatter.test.ts +++ b/src/output/formatter.test.ts @@ -218,9 +218,7 @@ describe('OutputFormatter compact mode', () => { timestamp: '2025-01-15T10:00:00Z', threadId: 1, location: { file: '/app/src/order.ts', line: 45, function: 'processOrder' }, - stackTrace: [ - { frameId: 1, function: 'processOrder', file: '/app/src/order.ts', line: 45 }, - ], + stackTrace: [{ frameId: 1, function: 'processOrder', file: '/app/src/order.ts', line: 45 }], locals, }; @@ -244,9 +242,7 @@ describe('OutputFormatter compact mode', () => { timestamp: '2025-01-15T10:00:00Z', threadId: 1, location: { file: '/app/src/order.ts', line: 45, function: 'processOrder' }, - stackTrace: [ - { frameId: 1, function: 'processOrder', file: '/app/src/order.ts', line: 45 }, - ], + stackTrace: [{ frameId: 1, function: 'processOrder', file: '/app/src/order.ts', line: 45 }], locals: { order: { type: 'Order', value: { id: 1, total: 100 } }, customer: { type: 'Customer', value: { name: 'John' } }, @@ -260,9 +256,7 @@ describe('OutputFormatter compact mode', () => { timestamp: '2025-01-15T10:00:01Z', threadId: 1, location: { file: '/app/src/order.ts', line: 50, function: 'processOrder' }, - stackTrace: [ - { frameId: 1, function: 'processOrder', file: '/app/src/order.ts', line: 50 }, - ], + stackTrace: [{ frameId: 1, function: 'processOrder', file: '/app/src/order.ts', line: 50 }], locals: { order: { type: 'Order', value: { id: 1, total: 150 } }, // Changed! customer: { type: 'Customer', value: { name: 'John' } }, // Same diff --git a/src/session/exceptions.test.ts b/src/session/exceptions.test.ts new file mode 100644 index 0000000..93a6bba --- /dev/null +++ b/src/session/exceptions.test.ts @@ -0,0 +1,475 @@ +/** + * Unit tests for exception chain flattening functionality + */ + +import { describe, it, expect } from 'vitest'; +import { flattenExceptionChainFromLocals } from './exceptions.js'; +import type { VariableValue } from '../output/events.js'; + +describe('flattenExceptionChainFromLocals', () => { + describe('basic exception parsing', () => { + it('extracts single exception with no inner exception', () => { + const locals: Record = { + $exception: { + type: 'System.InvalidOperationException', + value: { + Message: { type: 'string', value: 'Operation is not valid' }, + Source: { type: 'string', value: 'TestAssembly' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result).not.toBeNull(); + expect(result!.chain).toHaveLength(1); + expect(result!.chain[0].type).toBe('System.InvalidOperationException'); + expect(result!.chain[0].message).toBe('Operation is not valid'); + expect(result!.chain[0].source).toBe('TestAssembly'); + expect(result!.chain[0].isRootCause).toBe(true); + }); + + it('returns null when no $exception in locals', () => { + const locals: Record = { + someVar: { type: 'int', value: 42 }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result).toBeNull(); + }); + + it('extracts exception chain with inner exceptions', () => { + const locals: Record = { + $exception: { + type: 'System.Exception {DbConnectionException}', + value: { + Message: { type: 'string', value: 'Database connection failed' }, + Source: { type: 'string', value: 'DataLayer' }, + InnerException: { + type: 'System.Net.Sockets.SocketException', + value: { + Message: { type: 'string', value: 'Connection refused' }, + Source: { type: 'string', value: 'System.Net.Sockets' }, + InnerException: { type: 'null', value: null }, + }, + }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result).not.toBeNull(); + expect(result!.chain).toHaveLength(2); + expect(result!.chain[0].type).toBe('DbConnectionException'); + expect(result!.chain[0].depth).toBe(0); + expect(result!.chain[0].isRootCause).toBeUndefined(); + expect(result!.chain[1].type).toBe('System.Net.Sockets.SocketException'); + expect(result!.chain[1].depth).toBe(1); + expect(result!.chain[1].isRootCause).toBe(true); + }); + + it('respects maxDepth parameter', () => { + // Create a deep exception chain + const innerException3: VariableValue = { + type: 'System.Exception', + value: { + Message: { type: 'string', value: 'Level 3' }, + InnerException: { type: 'null', value: null }, + }, + }; + const innerException2: VariableValue = { + type: 'System.Exception', + value: { + Message: { type: 'string', value: 'Level 2' }, + InnerException: innerException3, + }, + }; + const innerException1: VariableValue = { + type: 'System.Exception', + value: { + Message: { type: 'string', value: 'Level 1' }, + InnerException: innerException2, + }, + }; + const locals: Record = { + $exception: { + type: 'System.Exception', + value: { + Message: { type: 'string', value: 'Level 0' }, + InnerException: innerException1, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals, 2); + + expect(result).not.toBeNull(); + expect(result!.chain).toHaveLength(2); + expect(result!.chain[0].message).toBe('Level 0'); + expect(result!.chain[1].message).toBe('Level 1'); + }); + }); + + describe('exception type extraction', () => { + it('extracts actual type from "System.Exception {ActualType}" format', () => { + const locals: Record = { + $exception: { + type: 'System.Exception {CustomException}', + value: { + Message: { type: 'string', value: 'Custom error' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.chain[0].type).toBe('CustomException'); + }); + + it('uses full type when no curly brace format', () => { + const locals: Record = { + $exception: { + type: 'System.ArgumentNullException', + value: { + Message: { type: 'string', value: 'Value cannot be null' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.chain[0].type).toBe('System.ArgumentNullException'); + }); + }); + + describe('exception data extraction', () => { + it('extracts SQL error number from SqlException', () => { + const locals: Record = { + $exception: { + type: 'Microsoft.Data.SqlClient.SqlException', + value: { + Message: { type: 'string', value: 'Login failed' }, + Number: { type: 'int', value: 18456 }, + State: { type: 'int', value: 1 }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.chain[0].data).toBeDefined(); + expect(result!.chain[0].data!.sqlErrorNumber).toBe(18456); + expect(result!.chain[0].data!.sqlState).toBe(1); + }); + + it('extracts socket error code from SocketException', () => { + const locals: Record = { + $exception: { + type: 'System.Net.Sockets.SocketException', + value: { + Message: { type: 'string', value: 'Connection refused' }, + SocketErrorCode: { type: 'string', value: 'ConnectionRefused' }, + NativeErrorCode: { type: 'int', value: 10061 }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.chain[0].data).toBeDefined(); + expect(result!.chain[0].data!.socketErrorCode).toBe('ConnectionRefused'); + expect(result!.chain[0].data!.nativeErrorCode).toBe(10061); + }); + + it('extracts param name from ArgumentNullException', () => { + const locals: Record = { + $exception: { + type: 'System.ArgumentNullException', + value: { + Message: { type: 'string', value: 'Value cannot be null' }, + ParamName: { type: 'string', value: 'customerId' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.chain[0].data).toBeDefined(); + expect(result!.chain[0].data!.paramName).toBe('customerId'); + }); + + it('extracts file name from FileNotFoundException', () => { + const locals: Record = { + $exception: { + type: 'System.IO.FileNotFoundException', + value: { + Message: { type: 'string', value: 'Could not find file' }, + FileName: { type: 'string', value: '/path/to/missing.txt' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.chain[0].data).toBeDefined(); + expect(result!.chain[0].data!.fileName).toBe('/path/to/missing.txt'); + }); + }); + + describe('root cause classification', () => { + it('classifies network exceptions', () => { + const locals: Record = { + $exception: { + type: 'System.Net.Sockets.SocketException', + value: { + Message: { type: 'string', value: 'Connection refused' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.rootCause.category).toBe('network'); + }); + + it('classifies database exceptions', () => { + const locals: Record = { + $exception: { + type: 'Microsoft.Data.SqlClient.SqlException', + value: { + Message: { type: 'string', value: 'Database error' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.rootCause.category).toBe('database'); + }); + + it('classifies validation exceptions', () => { + const locals: Record = { + $exception: { + type: 'System.ArgumentNullException', + value: { + Message: { type: 'string', value: 'Value cannot be null' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.rootCause.category).toBe('validation'); + }); + + it('classifies timeout exceptions', () => { + const locals: Record = { + $exception: { + type: 'System.TimeoutException', + value: { + Message: { type: 'string', value: 'Operation timed out' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.rootCause.category).toBe('timeout'); + }); + + it('classifies file system exceptions', () => { + const locals: Record = { + $exception: { + type: 'System.IO.FileNotFoundException', + value: { + Message: { type: 'string', value: 'File not found' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.rootCause.category).toBe('file_system'); + }); + + it('classifies null reference exceptions', () => { + const locals: Record = { + $exception: { + type: 'System.NullReferenceException', + value: { + Message: { type: 'string', value: 'Object reference not set' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.rootCause.category).toBe('null_reference'); + }); + + it('returns unknown for unrecognized exceptions', () => { + const locals: Record = { + $exception: { + type: 'CustomNamespace.CustomException', + value: { + Message: { type: 'string', value: 'Some custom error' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.rootCause.category).toBe('unknown'); + }); + }); + + describe('actionable hints', () => { + it('provides actionable hint for SQL login failure', () => { + const locals: Record = { + $exception: { + type: 'Microsoft.Data.SqlClient.SqlException', + value: { + Message: { type: 'string', value: 'Login failed for user' }, + Number: { type: 'int', value: 18456 }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.rootCause.actionableHint).toContain('login'); + }); + + it('provides actionable hint for socket connection refused', () => { + const locals: Record = { + $exception: { + type: 'System.Net.Sockets.SocketException', + value: { + Message: { type: 'string', value: 'Connection refused' }, + NativeErrorCode: { type: 'int', value: 10061 }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.rootCause.actionableHint).toContain('refused'); + }); + + it('provides actionable hint for null reference', () => { + const locals: Record = { + $exception: { + type: 'System.NullReferenceException', + value: { + Message: { type: 'string', value: 'Object reference not set' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.rootCause.actionableHint).toContain('null'); + }); + + it('provides generic hint when no specific hint available', () => { + const locals: Record = { + $exception: { + type: 'CustomNamespace.VeryCustomException', + value: { + Message: { type: 'string', value: 'Something went wrong' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result!.rootCause.actionableHint).toBeDefined(); + }); + }); + + describe('edge cases', () => { + it('handles exception with string value instead of object', () => { + const locals: Record = { + $exception: { + type: 'System.Exception {NetworkException}', + value: '{NetworkException: Connection refused to host 10.0.0.1}', + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result).not.toBeNull(); + expect(result!.chain[0].type).toBe('NetworkException'); + expect(result!.chain[0].message).toContain('Connection refused'); + }); + + it('handles null Source gracefully', () => { + const locals: Record = { + $exception: { + type: 'System.Exception', + value: { + Message: { type: 'string', value: 'Error' }, + Source: { type: 'string', value: 'null' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result).not.toBeNull(); + expect(result!.chain[0].source).toBeUndefined(); + }); + + it('extracts TargetSite for throw location', () => { + const locals: Record = { + $exception: { + type: 'System.Exception', + value: { + Message: { type: 'string', value: 'Error' }, + TargetSite: { type: 'MethodBase', value: '{Void ValidateOrder(Order)}' }, + InnerException: { type: 'null', value: null }, + }, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result).not.toBeNull(); + expect(result!.chain[0].throwSite).toBe('Void ValidateOrder(Order)'); + }); + + it('returns null for empty exception variable', () => { + const locals: Record = { + $exception: { + type: '', + value: null, + }, + }; + + const result = flattenExceptionChainFromLocals(locals); + + expect(result).toBeNull(); + }); + }); +}); diff --git a/src/session/manager.test.ts b/src/session/manager.test.ts index fa2f161..338febf 100644 --- a/src/session/manager.test.ts +++ b/src/session/manager.test.ts @@ -2,7 +2,7 @@ * Unit tests for DebugSession manager */ -import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { DebugSession } from './manager.js'; import type { AdapterConfig } from '../adapters/base.js'; import { OutputFormatter } from '../output/formatter.js'; @@ -15,6 +15,8 @@ const mockAdapter: AdapterConfig = { args: [], launchConfig: () => ({}), attachConfig: () => ({}), + detect: async () => null, + installHint: 'Test adapter', }; describe('DebugSession', () => { @@ -29,7 +31,11 @@ describe('DebugSession', () => { sessionEndCalls.push(summary); }), emit: vi.fn(), - createEvent: vi.fn((type, data) => ({ type, timestamp: new Date().toISOString(), ...data })), + createEvent: vi.fn((type, data) => ({ + type, + timestamp: new Date().toISOString(), + ...data, + })), error: vi.fn(), programOutput: vi.fn(), } as unknown as OutputFormatter; @@ -68,7 +74,11 @@ describe('DebugSession', () => { sessionEndCalls.push(summary); }), emit: vi.fn(), - createEvent: vi.fn((type, data) => ({ type, timestamp: new Date().toISOString(), ...data })), + createEvent: vi.fn((type, data) => ({ + type, + timestamp: new Date().toISOString(), + ...data, + })), error: vi.fn(), programOutput: vi.fn(), } as unknown as OutputFormatter;