import { beforeEach, describe, expect, it, mock } from 'bun:test'; import { OperationContext } from '../src/operation-context'; import type { OperationContextOptions } from '../src/operation-context'; describe('OperationContext', () => { const mockLogger = { info: mock(() => {}), error: mock(() => {}), warn: mock(() => {}), debug: mock(() => {}), trace: mock(() => {}), child: mock(() => mockLogger), }; const mockContainer = { resolve: mock((name: string) => ({ name })), resolveAsync: mock(async (name: string) => ({ name })), }; beforeEach(() => { // Reset mocks Object.keys(mockLogger).forEach(key => { if (typeof mockLogger[key as keyof typeof mockLogger] === 'function') { (mockLogger as any)[key] = mock(() => (key === 'child' ? mockLogger : undefined)); } }); mockContainer.resolve = mock((name: string) => ({ name })); mockContainer.resolveAsync = mock(async (name: string) => ({ name })); }); describe('constructor', () => { it('should create context with required options', () => { const options: OperationContextOptions = { handlerName: 'test-handler', operationName: 'test-op', }; const context = new OperationContext(options); expect(context).toBeDefined(); expect(context.traceId).toBeDefined(); expect(context.metadata).toEqual({}); expect(context.logger).toBeDefined(); }); it('should create context with all options', () => { const options: OperationContextOptions = { handlerName: 'test-handler', operationName: 'test-op', parentLogger: mockLogger, container: mockContainer, metadata: { key: 'value' }, traceId: 'custom-trace-id', }; const context = new OperationContext(options); expect(context.traceId).toBe('custom-trace-id'); expect(context.metadata).toEqual({ key: 'value' }); expect(context.logger).toBe(mockLogger); }); }); describe('static create', () => { it('should create context using static method', () => { const context = OperationContext.create('handler', 'operation', { metadata: { foo: 'bar' }, }); expect(context).toBeDefined(); expect(context.metadata).toEqual({ foo: 'bar' }); }); }); describe('service resolution', () => { it('should resolve services from container', () => { const context = new OperationContext({ handlerName: 'test', operationName: 'test-op', container: mockContainer, }); const service = context.resolve('myService'); expect(service).toEqual({ name: 'myService' }); expect(mockContainer.resolve).toHaveBeenCalledWith('myService'); }); it('should resolve services asynchronously', async () => { const context = new OperationContext({ handlerName: 'test', operationName: 'test-op', container: mockContainer, }); const service = await context.resolveAsync('myService'); expect(service).toEqual({ name: 'myService' }); expect(mockContainer.resolveAsync).toHaveBeenCalledWith('myService'); }); it('should throw error when no container available', () => { const context = new OperationContext({ handlerName: 'test', operationName: 'test-op', }); expect(() => context.resolve('service')).toThrow('No service container available'); }); it('should throw error when no container available for async', async () => { const context = new OperationContext({ handlerName: 'test', operationName: 'test-op', }); await expect(context.resolveAsync('service')).rejects.toThrow( 'No service container available' ); }); }); describe('metadata', () => { it('should add metadata', () => { const context = new OperationContext({ handlerName: 'test', operationName: 'test-op', }); context.addMetadata('userId', '12345'); context.addMetadata('correlationId', 'corr-456'); expect(context.metadata.userId).toBe('12345'); expect(context.metadata.correlationId).toBe('corr-456'); }); it('should preserve initial metadata', () => { const context = new OperationContext({ handlerName: 'test', operationName: 'test-op', metadata: { initial: 'value' }, }); context.addMetadata('added', 'new-value'); expect(context.metadata.initial).toBe('value'); expect(context.metadata.added).toBe('new-value'); }); }); describe('execution time', () => { it('should track execution time', async () => { const context = new OperationContext({ handlerName: 'test', operationName: 'test-op', }); // Wait a bit await new Promise(resolve => setTimeout(resolve, 50)); const executionTime = context.getExecutionTime(); expect(executionTime).toBeGreaterThan(40); expect(executionTime).toBeLessThan(100); }); }); describe('logging', () => { it('should log successful completion', () => { const context = new OperationContext({ handlerName: 'test', operationName: 'test-op', parentLogger: mockLogger, }); context.logCompletion(true); expect(mockLogger.info).toHaveBeenCalledWith( 'Operation completed successfully', expect.objectContaining({ executionTime: expect.any(Number), metadata: {}, }) ); }); it('should log failed completion with error', () => { const context = new OperationContext({ handlerName: 'test', operationName: 'test-op', parentLogger: mockLogger, }); const error = new Error('Test error'); context.logCompletion(false, error); expect(mockLogger.error).toHaveBeenCalledWith( 'Operation failed', expect.objectContaining({ executionTime: expect.any(Number), error: 'Test error', stack: expect.any(String), metadata: {}, }) ); }); }); describe('child context', () => { it('should create child context', () => { const parent = new OperationContext({ handlerName: 'parent', operationName: 'parent-op', parentLogger: mockLogger, container: mockContainer, traceId: 'parent-trace', metadata: { parentKey: 'parentValue' }, }); const child = parent.createChild('child-op', { childKey: 'childValue' }); expect(child.traceId).toBe('parent-trace'); // Inherits trace ID expect(child.metadata).toEqual({ parentKey: 'parentValue', childKey: 'childValue', }); expect(child.logger).toBe(mockLogger); // Inherits logger }); it('should create child without additional metadata', () => { const parent = new OperationContext({ handlerName: 'parent', operationName: 'parent-op', metadata: { key: 'value' }, }); const child = parent.createChild('child-op'); expect(child.metadata).toEqual({ key: 'value' }); }); }); describe('dispose', () => { it('should log completion on dispose', async () => { const context = new OperationContext({ handlerName: 'test', operationName: 'test-op', parentLogger: mockLogger, }); await context.dispose(); expect(mockLogger.info).toHaveBeenCalledWith( 'Operation completed successfully', expect.any(Object) ); }); }); describe('trace ID generation', () => { it('should generate unique trace IDs', () => { const context1 = new OperationContext({ handlerName: 'test', operationName: 'test-op', }); const context2 = new OperationContext({ handlerName: 'test', operationName: 'test-op', }); expect(context1.traceId).not.toBe(context2.traceId); expect(context1.traceId).toMatch(/^\d+-[a-z0-9]+$/); }); }); });