import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; import { trace } from '@opentelemetry/api'; import { GrpcMetadataBuilder } from './grpc-metadata.builder'; import type { AiServiceConfig } from '../../config/check-ai-service-config'; /** * Locks the metadata-injection contract per ADR-0024 §"Metadata contract": * * - x-client-id ← AI_SERVICE_CLIENT_ID env (via AI_CONFIG). * - x-correlation-id ← active OTel span's trace-id when present; * explicit `correlationId` override wins over * the span; otherwise a fresh UUID is minted * so the AI service can still join its audit * row to a unique id even when the portal * request is outside a trace. */ const CONFIG: AiServiceConfig = { endpoint: 'apf-ai-service:8080', clientId: 'apf-portal-test', useTls: false, }; let activeSpan: ReturnType = undefined; let getActiveSpanSpy: ReturnType; beforeEach(() => { activeSpan = undefined; getActiveSpanSpy = jest.spyOn(trace, 'getActiveSpan').mockImplementation(() => activeSpan); }); afterEach(() => { getActiveSpanSpy.mockRestore(); }); describe('GrpcMetadataBuilder', () => { it('always emits x-client-id from the config', () => { const builder = new GrpcMetadataBuilder(CONFIG); const meta = builder.build(); expect(meta.get('x-client-id')).toEqual(['apf-portal-test']); }); it('uses the active OTel trace-id as x-correlation-id when a span is active', () => { activeSpan = { spanContext: () => ({ traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', spanId: 'bbbbbbbbbbbbbbbb', traceFlags: 1, }), } as ReturnType; const builder = new GrpcMetadataBuilder(CONFIG); const meta = builder.build(); expect(meta.get('x-correlation-id')).toEqual(['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa']); }); it('prefers the explicit options.correlationId over the active span', () => { activeSpan = { spanContext: () => ({ traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', spanId: 'bbbbbbbbbbbbbbbb', traceFlags: 1, }), } as ReturnType; const builder = new GrpcMetadataBuilder(CONFIG); const meta = builder.build({ correlationId: 'caller-supplied-id' }); expect(meta.get('x-correlation-id')).toEqual(['caller-supplied-id']); }); it('falls back to a UUID v4 when no span is active and no override is passed', () => { const builder = new GrpcMetadataBuilder(CONFIG); const meta = builder.build(); const correlationIds = meta.get('x-correlation-id') as string[]; expect(correlationIds).toHaveLength(1); expect(correlationIds[0]).toMatch( /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i, ); }); it('returns a fresh Metadata instance on every call (no shared mutation)', () => { const builder = new GrpcMetadataBuilder(CONFIG); const a = builder.build(); const b = builder.build(); expect(a).not.toBe(b); }); });