8bea170f3f
- Changed WAProto.proto syntax from proto3 to proto2 to support required fields - Regenerated WAProto static files (index.d.ts, index.js) with correct proto2 syntax - Fixed type annotations in test files (circuit-breaker, sync-action-utils, trace-context) - Fixed Mock type errors in baileys-event-stream and cache-utils tests - Fixed LID mapping test expectations to match array type - All 78 TypeScript errors resolved, 0 remaining https://claude.ai/code/session_015R3U3kiprQiNTTNNt31Sg6
487 lines
12 KiB
TypeScript
487 lines
12 KiB
TypeScript
/**
|
|
* Testes unitários para trace-context.ts
|
|
*/
|
|
|
|
import { describe, it, expect, beforeEach } from '@jest/globals'
|
|
import {
|
|
createTraceContext,
|
|
getCurrentContext,
|
|
getOrCreateContext,
|
|
runWithContext,
|
|
runWithNewContext,
|
|
createSpan,
|
|
startSpan,
|
|
endSpan,
|
|
addSpanEvent,
|
|
setSpanAttributes,
|
|
setSpanError,
|
|
withSpan,
|
|
withSpanSync,
|
|
setBaggage,
|
|
getBaggage,
|
|
getAllBaggage,
|
|
removeBaggage,
|
|
injectTraceHeaders,
|
|
extractTraceHeaders,
|
|
exportContext,
|
|
importContext,
|
|
createPrecisionTimer,
|
|
generateTraceId,
|
|
generateSpanId,
|
|
generateCorrelationId,
|
|
TRACE_HEADERS,
|
|
type TraceContext,
|
|
type Span,
|
|
} from '../../Utils/trace-context.js'
|
|
|
|
describe('ID generation', () => {
|
|
describe('generateTraceId', () => {
|
|
it('should generate 32 character hex string', () => {
|
|
const id = generateTraceId()
|
|
expect(id).toHaveLength(32)
|
|
expect(id).toMatch(/^[0-9a-f]+$/)
|
|
})
|
|
|
|
it('should generate unique IDs', () => {
|
|
const ids = new Set()
|
|
for (let i = 0; i < 100; i++) {
|
|
ids.add(generateTraceId())
|
|
}
|
|
expect(ids.size).toBe(100)
|
|
})
|
|
})
|
|
|
|
describe('generateSpanId', () => {
|
|
it('should generate 16 character hex string', () => {
|
|
const id = generateSpanId()
|
|
expect(id).toHaveLength(16)
|
|
expect(id).toMatch(/^[0-9a-f]+$/)
|
|
})
|
|
})
|
|
|
|
describe('generateCorrelationId', () => {
|
|
it('should generate readable correlation ID', () => {
|
|
const id = generateCorrelationId()
|
|
expect(id).toMatch(/^[a-z0-9]+-[a-z0-9]+$/)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('TraceContext', () => {
|
|
describe('createTraceContext', () => {
|
|
it('should create context with generated IDs', () => {
|
|
const context = createTraceContext()
|
|
|
|
expect(context.traceIds.traceId).toHaveLength(32)
|
|
expect(context.traceIds.spanId).toHaveLength(16)
|
|
expect(context.traceIds.correlationId).toBeTruthy()
|
|
expect(context.baggage).toEqual({})
|
|
expect(context.spanStack).toEqual([])
|
|
})
|
|
|
|
it('should use provided options', () => {
|
|
const context = createTraceContext({
|
|
traceId: 'custom-trace-id',
|
|
correlationId: 'custom-corr-id',
|
|
parentSpanId: 'parent-span',
|
|
baggage: { key: 'value' },
|
|
metadata: { env: 'test' },
|
|
})
|
|
|
|
expect(context.traceIds.traceId).toBe('custom-trace-id')
|
|
expect(context.traceIds.correlationId).toBe('custom-corr-id')
|
|
expect(context.traceIds.parentSpanId).toBe('parent-span')
|
|
expect(context.baggage).toEqual({ key: 'value' })
|
|
expect(context.metadata).toEqual({ env: 'test' })
|
|
})
|
|
})
|
|
|
|
describe('runWithContext', () => {
|
|
it('should make context available within scope', () => {
|
|
const context = createTraceContext()
|
|
let captured: TraceContext | undefined
|
|
|
|
runWithContext(context, () => {
|
|
captured = getCurrentContext()
|
|
})
|
|
|
|
expect(captured).toBe(context)
|
|
})
|
|
|
|
it('should not leak context outside scope', () => {
|
|
const context = createTraceContext()
|
|
|
|
runWithContext(context, () => {
|
|
// Context available here
|
|
})
|
|
|
|
// Context should be undefined outside
|
|
expect(getCurrentContext()).toBeUndefined()
|
|
})
|
|
|
|
it('should return value from function', () => {
|
|
const context = createTraceContext()
|
|
|
|
const result = runWithContext(context, () => 'result')
|
|
|
|
expect(result).toBe('result')
|
|
})
|
|
})
|
|
|
|
describe('runWithNewContext', () => {
|
|
it('should create and run with new context', () => {
|
|
let captured: TraceContext | undefined
|
|
|
|
runWithNewContext({ baggage: { test: 'value' } }, () => {
|
|
captured = getCurrentContext()
|
|
})
|
|
|
|
expect(captured).toBeDefined()
|
|
expect(captured!.baggage.test).toBe('value')
|
|
})
|
|
})
|
|
|
|
describe('getOrCreateContext', () => {
|
|
it('should return existing context if available', () => {
|
|
const context = createTraceContext()
|
|
|
|
runWithContext(context, () => {
|
|
const retrieved = getOrCreateContext()
|
|
expect(retrieved).toBe(context)
|
|
})
|
|
})
|
|
|
|
it('should create new context if none exists', () => {
|
|
const context = getOrCreateContext()
|
|
expect(context).toBeDefined()
|
|
expect(context.traceIds.traceId).toHaveLength(32)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Spans', () => {
|
|
describe('createSpan', () => {
|
|
it('should create span with provided name', () => {
|
|
const span = createSpan({ name: 'test-span' })
|
|
|
|
expect(span.name).toBe('test-span')
|
|
expect(span.traceIds.spanId).toHaveLength(16)
|
|
expect(span.startTime).toBeGreaterThan(0)
|
|
expect(span.status).toBe('unset')
|
|
expect(span.ended).toBe(false)
|
|
})
|
|
|
|
it('should include provided attributes', () => {
|
|
const span = createSpan({
|
|
name: 'test',
|
|
attributes: { key: 'value', count: 42 },
|
|
})
|
|
|
|
expect(span.attributes).toEqual({ key: 'value', count: 42 })
|
|
})
|
|
})
|
|
|
|
describe('startSpan', () => {
|
|
it('should start span and set as current in context', () => {
|
|
const context = createTraceContext()
|
|
|
|
runWithContext(context, () => {
|
|
const span = startSpan({ name: 'operation' })
|
|
|
|
expect(getCurrentContext()!.currentSpan).toBe(span)
|
|
})
|
|
})
|
|
|
|
it('should support nested spans', () => {
|
|
const context = createTraceContext()
|
|
|
|
runWithContext(context, () => {
|
|
const parent = startSpan({ name: 'parent' })
|
|
const child = startSpan({ name: 'child' })
|
|
|
|
expect(child.traceIds.parentSpanId).toBe(parent.traceIds.spanId)
|
|
expect(getCurrentContext()!.spanStack).toContain(parent)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('endSpan', () => {
|
|
it('should mark span as ended', () => {
|
|
const span = createSpan({ name: 'test' })
|
|
endSpan(span)
|
|
|
|
expect(span.ended).toBe(true)
|
|
expect(span.endTime).toBeDefined()
|
|
expect(span.duration).toBeDefined()
|
|
expect(span.status).toBe('ok')
|
|
})
|
|
|
|
it('should set custom status', () => {
|
|
const span = createSpan({ name: 'test' })
|
|
endSpan(span, 'error')
|
|
|
|
expect(span.status).toBe('error')
|
|
})
|
|
|
|
it('should not modify already ended span', () => {
|
|
const span = createSpan({ name: 'test' })
|
|
endSpan(span, 'ok')
|
|
|
|
const endTime = span.endTime
|
|
|
|
endSpan(span, 'error')
|
|
|
|
expect(span.endTime).toBe(endTime)
|
|
expect(span.status).toBe('ok')
|
|
})
|
|
|
|
it('should pop span from stack', () => {
|
|
const context = createTraceContext()
|
|
|
|
runWithContext(context, () => {
|
|
const parent = startSpan({ name: 'parent' })
|
|
const child = startSpan({ name: 'child' })
|
|
|
|
endSpan(child)
|
|
|
|
expect(getCurrentContext()!.currentSpan).toBe(parent)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('addSpanEvent', () => {
|
|
it('should add event to span', () => {
|
|
const span = createSpan({ name: 'test' })
|
|
addSpanEvent(span, 'event-occurred', { detail: 'value' })
|
|
|
|
expect(span.events).toHaveLength(1)
|
|
expect(span.events[0]!.name).toBe('event-occurred')
|
|
expect(span.events[0]!.attributes).toEqual({ detail: 'value' })
|
|
})
|
|
|
|
it('should not add event to ended span', () => {
|
|
const span = createSpan({ name: 'test' })
|
|
endSpan(span)
|
|
addSpanEvent(span, 'late-event')
|
|
|
|
expect(span.events).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe('setSpanAttributes', () => {
|
|
it('should set attributes on span', () => {
|
|
const span = createSpan({ name: 'test' })
|
|
setSpanAttributes(span, { key1: 'value1', key2: 'value2' })
|
|
|
|
expect(span.attributes.key1).toBe('value1')
|
|
expect(span.attributes.key2).toBe('value2')
|
|
})
|
|
})
|
|
|
|
describe('setSpanError', () => {
|
|
it('should mark span as error with details', () => {
|
|
const span = createSpan({ name: 'test' })
|
|
const error = new Error('Something went wrong')
|
|
setSpanError(span, error)
|
|
|
|
expect(span.status).toBe('error')
|
|
expect(span.attributes.error).toBe(true)
|
|
expect(span.attributes.errorMessage).toBe('Something went wrong')
|
|
expect(span.events.some((e) => e.name === 'exception')).toBe(true)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('withSpan helpers', () => {
|
|
describe('withSpan', () => {
|
|
it('should execute async operation within span', async () => {
|
|
const result = await withSpan('async-op', async (span) => {
|
|
expect(span.name).toBe('async-op')
|
|
return 'async-result'
|
|
})
|
|
|
|
expect(result).toBe('async-result')
|
|
})
|
|
|
|
it('should handle errors and mark span as error', async () => {
|
|
await expect(
|
|
withSpan('failing-op', async () => {
|
|
throw new Error('Failure')
|
|
})
|
|
).rejects.toThrow('Failure')
|
|
})
|
|
})
|
|
|
|
describe('withSpanSync', () => {
|
|
it('should execute sync operation within span', () => {
|
|
const result = withSpanSync('sync-op', (span) => {
|
|
expect(span.name).toBe('sync-op')
|
|
return 'sync-result'
|
|
})
|
|
|
|
expect(result).toBe('sync-result')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Baggage', () => {
|
|
it('should set and get baggage', () => {
|
|
const context = createTraceContext()
|
|
|
|
runWithContext(context, () => {
|
|
setBaggage('userId', 'user-123')
|
|
setBaggage('requestType', 'api')
|
|
|
|
expect(getBaggage('userId')).toBe('user-123')
|
|
expect(getBaggage('requestType')).toBe('api')
|
|
})
|
|
})
|
|
|
|
it('should get all baggage', () => {
|
|
const context = createTraceContext({ baggage: { existing: 'value' } })
|
|
|
|
runWithContext(context, () => {
|
|
setBaggage('new', 'item')
|
|
|
|
const all = getAllBaggage()
|
|
|
|
expect(all).toEqual({ existing: 'value', new: 'item' })
|
|
})
|
|
})
|
|
|
|
it('should remove baggage', () => {
|
|
const context = createTraceContext({ baggage: { toRemove: 'value' } })
|
|
|
|
runWithContext(context, () => {
|
|
removeBaggage('toRemove')
|
|
|
|
expect(getBaggage('toRemove')).toBeUndefined()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Header injection/extraction', () => {
|
|
describe('injectTraceHeaders', () => {
|
|
it('should inject trace headers', () => {
|
|
const context = createTraceContext({
|
|
baggage: { user: 'test' },
|
|
})
|
|
|
|
const headers = runWithContext(context, () => {
|
|
return injectTraceHeaders({})
|
|
})
|
|
|
|
expect(headers[TRACE_HEADERS.TRACE_ID]).toBe(context.traceIds.traceId)
|
|
expect(headers[TRACE_HEADERS.SPAN_ID]).toBe(context.traceIds.spanId)
|
|
expect(headers[TRACE_HEADERS.CORRELATION_ID]).toBe(context.traceIds.correlationId)
|
|
expect(headers[TRACE_HEADERS.BAGGAGE]).toContain('user=test')
|
|
})
|
|
|
|
it('should preserve existing headers', () => {
|
|
const context = createTraceContext()
|
|
|
|
const headers = runWithContext(context, () => {
|
|
return injectTraceHeaders({ 'Content-Type': 'application/json' })
|
|
})
|
|
|
|
expect(headers['Content-Type']).toBe('application/json')
|
|
})
|
|
})
|
|
|
|
describe('extractTraceHeaders', () => {
|
|
it('should extract trace context from headers', () => {
|
|
const headers = {
|
|
[TRACE_HEADERS.TRACE_ID]: 'trace-123',
|
|
[TRACE_HEADERS.PARENT_SPAN_ID]: 'parent-456',
|
|
[TRACE_HEADERS.CORRELATION_ID]: 'corr-789',
|
|
[TRACE_HEADERS.BAGGAGE]: 'key1=value1,key2=value2',
|
|
}
|
|
|
|
const options = extractTraceHeaders(headers)
|
|
|
|
expect(options.traceId).toBe('trace-123')
|
|
expect(options.parentSpanId).toBe('parent-456')
|
|
expect(options.correlationId).toBe('corr-789')
|
|
expect(options.baggage).toEqual({ key1: 'value1', key2: 'value2' })
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Context serialization', () => {
|
|
describe('exportContext', () => {
|
|
it('should serialize context to JSON string', () => {
|
|
const context = createTraceContext({
|
|
baggage: { key: 'value' },
|
|
metadata: { env: 'test' },
|
|
})
|
|
|
|
const exported = exportContext(context)
|
|
const parsed = JSON.parse(exported)
|
|
|
|
expect(parsed.traceIds).toEqual(context.traceIds)
|
|
expect(parsed.baggage).toEqual(context.baggage)
|
|
expect(parsed.metadata).toEqual(context.metadata)
|
|
})
|
|
})
|
|
|
|
describe('importContext', () => {
|
|
it('should deserialize context from JSON string', () => {
|
|
const serialized = JSON.stringify({
|
|
traceIds: {
|
|
traceId: 'trace-123',
|
|
spanId: 'span-456',
|
|
correlationId: 'corr-789',
|
|
},
|
|
baggage: { imported: 'value' },
|
|
metadata: { source: 'external' },
|
|
})
|
|
|
|
const options = importContext(serialized)
|
|
|
|
expect(options.traceId).toBe('trace-123')
|
|
expect(options.parentSpanId).toBe('span-456')
|
|
expect(options.correlationId).toBe('corr-789')
|
|
expect(options.baggage).toEqual({ imported: 'value' })
|
|
})
|
|
|
|
it('should handle invalid JSON gracefully', () => {
|
|
const options = importContext('invalid json')
|
|
expect(options).toEqual({})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('PrecisionTimer', () => {
|
|
it('should measure elapsed time', async () => {
|
|
const timer = createPrecisionTimer()
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
|
|
const elapsed = timer.elapsed()
|
|
expect(elapsed).toBeGreaterThan(10)
|
|
})
|
|
|
|
it('should format elapsed time', async () => {
|
|
const timer = createPrecisionTimer()
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
|
|
const formatted = timer.elapsedFormatted()
|
|
expect(formatted).toMatch(/\d+(\.\d+)?(µs|ms|s)/)
|
|
})
|
|
|
|
it('should stop timer', async () => {
|
|
const timer = createPrecisionTimer()
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
|
|
const stopped = timer.stop()
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
|
|
const afterStop = timer.elapsed()
|
|
|
|
expect(afterStop).toBe(stopped)
|
|
})
|
|
})
|