import { describe, expect, it } from 'bun:test'; import { validateEmittedFacet, type CompiledValidator, type AdvisoryLogger } from './advisory-validation'; /** A validator that always passes (mirrors Ajv's compiled-fn shape). */ const passing: CompiledValidator = () => true; /** A validator that always fails and exposes Ajv-shaped errors. */ const failing: CompiledValidator = Object.assign(() => false, { errors: [{ instancePath: '/temperature', message: 'must be number' }], }); /** A validator that throws (e.g. a broken/incompatible schema at runtime). */ const throwing: CompiledValidator = () => { throw new Error('boom'); }; function makeLogger() { const warnings: Array<{ message: string; detail?: unknown }> = []; const logger: AdvisoryLogger = { warn: (message, detail) => warnings.push({ message, detail }), }; return { logger, warnings }; } describe('validateEmittedFacet', () => { it('returns true and does not warn when the validator is undefined (no schema → skip)', () => { const { logger, warnings } = makeLogger(); const result = validateEmittedFacet( undefined, { anything: true }, { descriptorLabel: 'twin-1', facet: 'reported', logger, }, ); expect(result).toBe(true); expect(warnings).toHaveLength(0); }); it('returns true and does not warn for conforming data', () => { const { logger, warnings } = makeLogger(); const result = validateEmittedFacet( passing, { temperature: 21 }, { descriptorLabel: 'twin-1', facet: 'reported', logger, }, ); expect(result).toBe(true); expect(warnings).toHaveLength(0); }); it('returns false and logs a single advisory warning for non-conforming data', () => { const { logger, warnings } = makeLogger(); const result = validateEmittedFacet( failing, { temperature: 'hot' }, { descriptorLabel: 'scanner-1', facet: 'event', facetKey: 'scan', logger, }, ); expect(result).toBe(false); expect(warnings).toHaveLength(1); expect(warnings[0]?.message).toContain('mismatch'); expect(warnings[0]?.message).toContain("event 'scan'"); expect(warnings[0]?.message).toContain('scanner-1'); // The Ajv errors are forwarded as the warning detail. expect(warnings[0]?.detail).toBe(failing.errors); }); it('fails open (returns true, warns, never throws) when the validator throws', () => { const { logger, warnings } = makeLogger(); let result: boolean | undefined; expect(() => { result = validateEmittedFacet( throwing, { temperature: 21 }, { descriptorLabel: 'twin-1', facet: 'actionReturns', facetKey: 'calibrate', logger, }, ); }).not.toThrow(); expect(result).toBe(true); expect(warnings).toHaveLength(1); expect(warnings[0]?.message).toContain('Failed to run advisory descriptor validation'); expect(warnings[0]?.message).toContain("actionReturns 'calibrate'"); }); });