From 85e40406f9c3510029b3c665934077150023619a Mon Sep 17 00:00:00 2001 From: Bryant Austin Date: Wed, 22 Apr 2026 13:51:02 -0600 Subject: [PATCH 1/5] added skip and message to tests with Long in R4 or earlier --- src/services/test-runner.ts | 127 +++++++++++++++++++++++++++++++----- 1 file changed, 112 insertions(+), 15 deletions(-) diff --git a/src/services/test-runner.ts b/src/services/test-runner.ts index b68627d..d05de66 100644 --- a/src/services/test-runner.ts +++ b/src/services/test-runner.ts @@ -16,7 +16,15 @@ export interface TestRunnerOptions { useAxios?: boolean; // For backward compatibility with run-tests-command } + + +interface CapabilityStatementMetadata { + resourceType?: string; + fhirVersion?: string; +} + export class TestRunner { + private readonly fhirVersionCache = new Map(); public async runTests( configData: any, options: TestRunnerOptions = {} @@ -29,20 +37,20 @@ export class TestRunner { // Verify server connectivity before proceeding await ServerConnectivity.verifyServerConnectivity(serverBaseUrl); - const build = config.Build; - const cqlEngine = new CQLEngine( - serverBaseUrl, - cqlEndpoint, - build.cqlTranslator ?? '', - build.cqlTranslatorVersion ?? '', - build.cqlEngine ?? '', - build.cqlEngineVersion ?? '' - ); - cqlEngine.cqlVersion = '1.5'; //default value - const cqlVersion = config.Build?.CqlVersion; - if (typeof cqlVersion === 'string' && cqlVersion.trim() !== '') { - cqlEngine.cqlVersion = cqlVersion; - } + const build = config.Build; + const cqlEngine = new CQLEngine( + serverBaseUrl, + cqlEndpoint, + build.cqlTranslator ?? '', + build.cqlTranslatorVersion ?? '', + build.cqlEngine ?? '', + build.cqlEngineVersion ?? '' + ); + cqlEngine.cqlVersion = '1.5'; //default value + const cqlVersion = config.Build?.CqlVersion; + if (typeof cqlVersion === 'string' && cqlVersion.trim() !== '') { + cqlEngine.cqlVersion = cqlVersion; + } // Load CVL using dynamic import // @ts-ignore @@ -78,6 +86,21 @@ export class TestRunner { skipReason ); } + if ( + await this.shouldSkipInteger64R4OrEarlierExpressionTest( + serverBaseUrl, + result, + options.useAxios + ) + ) { + this.addToSkipList( + skipMap, + result.testsName, + result.groupName, + result.testName, + 'FHIR R4 and earlier do not support Long/Integer64 values' + ); + } await this.runTest( result, cqlEngine.apiUrl!, @@ -130,7 +153,7 @@ export class TestRunner { ); return result; } else if (onlySet.size > 0 && !onlySet.has(key)) { - result.SkipMessage = 'Skipped by OnlyList filter'; + result.skipMessage = 'Skipped by OnlyList filter'; result.testStatus = 'skip'; return result; } else if (skipMap.has(key)) { @@ -249,6 +272,80 @@ export class TestRunner { return 0; // versions are equal } + private async getServerFhirVersion( + serverBaseUrl: string | undefined, + useAxios: boolean = false + ): Promise { + if (!serverBaseUrl) return undefined; + + const normalizedBaseUrl = serverBaseUrl.replace(/\/+$/, ''); + if (this.fhirVersionCache.has(normalizedBaseUrl)) { + return this.fhirVersionCache.get(normalizedBaseUrl); + } + + let fhirVersion: string | undefined; + + try { + const metadataUrl = `${normalizedBaseUrl}/metadata`; + + if (useAxios) { + const axios = await import('axios'); + const axiosResponse = await axios.default.get(metadataUrl, { + headers: { + Accept: 'application/fhir+json, application/json', + }, + }); + fhirVersion = axiosResponse.data?.fhirVersion; + } else { + const fetchResponse = await fetch(metadataUrl, { + method: 'GET', + headers: { + Accept: 'application/fhir+json, application/json', + }, + }); + + if (fetchResponse.ok) { + const metadata = + (await fetchResponse.json()) as CapabilityStatementMetadata; + fhirVersion = metadata.fhirVersion; + } + } + } catch (error) { + console.warn('Unable to determine FHIR version from metadata:', error); + } + + this.fhirVersionCache.set(normalizedBaseUrl, fhirVersion); + return fhirVersion; + } + + private async isR4OrEarlierServer( + serverBaseUrl: string | undefined, + useAxios: boolean = false + ): Promise { + const fhirVersion = await this.getServerFhirVersion(serverBaseUrl, useAxios); + if (typeof fhirVersion === 'string' && fhirVersion.trim() !== '') { + const major = parseInt(fhirVersion.split('.')[0], 10); + if (!Number.isNaN(major)) { + return major <= 4; + } + } + + // Fallback for URLs that explicitly include older FHIR version markers + return /\/(dstu2|stu3|r4)(\/|$)/i.test(serverBaseUrl ?? ''); + } + + private async shouldSkipInteger64R4OrEarlierExpressionTest( + serverBaseUrl: string | undefined, + result: InternalTestResult, + useAxios: boolean = false + ): Promise { + if (!(await this.isR4OrEarlierServer(serverBaseUrl, useAxios))) return false; + const expression = String(result.expression ?? ''); + return /(\bLong\b|\bInteger64\b|\bSystem\.Long\b|\bSystem\.Integer64\b|\d+[lL]\b)/.test( + expression + ); + } + private shouldSkipVersionTest(cqlEngine: CQLEngine, result: InternalTestResult): boolean { const engineVersion = cqlEngine?.cqlVersion; if (!engineVersion) return false; // no version to compare against From f300840584cd1d22a389086e60fbf1ff4625e442 Mon Sep 17 00:00:00 2001 From: Bryant Austin Date: Thu, 23 Apr 2026 13:38:28 -0600 Subject: [PATCH 2/5] adjusted to account for Long in capabilities --- src/services/test-runner.ts | 11 +++++------ src/shared/results-shared.ts | 8 ++++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/services/test-runner.ts b/src/services/test-runner.ts index d05de66..ec2b64a 100644 --- a/src/services/test-runner.ts +++ b/src/services/test-runner.ts @@ -87,7 +87,7 @@ export class TestRunner { ); } if ( - await this.shouldSkipInteger64R4OrEarlierExpressionTest( + await this.shouldSkipLongR4OrEarlierCapabilityTest( serverBaseUrl, result, options.useAxios @@ -334,16 +334,15 @@ export class TestRunner { return /\/(dstu2|stu3|r4)(\/|$)/i.test(serverBaseUrl ?? ''); } - private async shouldSkipInteger64R4OrEarlierExpressionTest( + private async shouldSkipLongR4OrEarlierCapabilityTest( serverBaseUrl: string | undefined, result: InternalTestResult, useAxios: boolean = false ): Promise { if (!(await this.isR4OrEarlierServer(serverBaseUrl, useAxios))) return false; - const expression = String(result.expression ?? ''); - return /(\bLong\b|\bInteger64\b|\bSystem\.Long\b|\bSystem\.Integer64\b|\d+[lL]\b)/.test( - expression - ); + + const capabilities = Array.isArray(result.capability) ? result.capability : []; + return capabilities.some(cap => String(cap.code ?? '').toLowerCase() === 'system.long'); } private shouldSkipVersionTest(cqlEngine: CQLEngine, result: InternalTestResult): boolean { diff --git a/src/shared/results-shared.ts b/src/shared/results-shared.ts index b910d55..f80cb33 100644 --- a/src/shared/results-shared.ts +++ b/src/shared/results-shared.ts @@ -53,9 +53,13 @@ export class Result implements InternalTestResult { this.skipMessage = 'No output specified'; } - this.capability = Array.isArray(test.capability) - ? test.capability.map(({ code, value }) => ({ code, value })) + const testCapabilities = Array.isArray(test.capability) + ? test.capability + : test.capability + ? [test.capability] : []; + + this.capability = testCapabilities.map(({ code, value }) => ({ code, value })); } } From 1f14c3b97b3e935117c5631e1f87677fd8b57226 Mon Sep 17 00:00:00 2001 From: Bryant Austin Date: Fri, 24 Apr 2026 12:37:13 -0600 Subject: [PATCH 3/5] adjustments to handle system.long testing --- .../value-type-extractors/long-extractor.ts | 37 ++++++++ src/server/extractor-builder.ts | 2 + src/services/test-runner.ts | 95 ------------------- src/shared/results-shared.ts | 2 +- test/extractResults-cql_operations.test.ts | 55 +++++++++++ 5 files changed, 95 insertions(+), 96 deletions(-) create mode 100644 src/extractors/value-type-extractors/long-extractor.ts diff --git a/src/extractors/value-type-extractors/long-extractor.ts b/src/extractors/value-type-extractors/long-extractor.ts new file mode 100644 index 0000000..960d6f0 --- /dev/null +++ b/src/extractors/value-type-extractors/long-extractor.ts @@ -0,0 +1,37 @@ +import { BaseExtractor } from '../base-extractor.js'; + +const CQL_TYPE_EXTENSION_URL = 'http://hl7.org/fhir/StructureDefinition/cqf-cqlType'; + +function hasCqlLongType(parameter: any): boolean { + const extensions = Array.isArray(parameter?.extension) ? parameter.extension : []; + return extensions.some( + (extension: any) => + extension?.url === CQL_TYPE_EXTENSION_URL && + String(extension?.valueString ?? '').toLowerCase() === 'system.long' + ); +} + +function parseLong(value: unknown): bigint | undefined { + if (value === undefined || value === null) return undefined; + + const text = String(value).trim(); + const normalized = text.endsWith('L') || text.endsWith('l') ? text.slice(0, -1) : text; + + if (!/^[+-]?\d+$/.test(normalized)) return undefined; + + return BigInt(normalized); +} + +export class LongExtractor extends BaseExtractor { + protected _process(parameter: any): any { + if (parameter.hasOwnProperty('valueInteger64')) { + return parseLong(parameter.valueInteger64); + } + + if (parameter.hasOwnProperty('valueString') && hasCqlLongType(parameter)) { + return parseLong(parameter.valueString); + } + + return undefined; + } +} diff --git a/src/server/extractor-builder.ts b/src/server/extractor-builder.ts index 41e125a..a55f658 100644 --- a/src/server/extractor-builder.ts +++ b/src/server/extractor-builder.ts @@ -2,6 +2,7 @@ import { EvaluationErrorExtractor } from '../extractors/evaluation-error-extract import { NullEmptyExtractor } from '../extractors/null-empty-extractor.js'; import { UndefinedExtractor } from '../extractors/undefined-extractor.js'; import { StringExtractor } from '../extractors/value-type-extractors/string-extractor.js'; +import { LongExtractor } from '../extractors/value-type-extractors/long-extractor.js'; import { BooleanExtractor } from '../extractors/value-type-extractors/boolean-extractor.js'; import { IntegerExtractor } from '../extractors/value-type-extractors/integer-extractor.js'; import { DecimalExtractor } from '../extractors/value-type-extractors/decimal-extractor.js'; @@ -24,6 +25,7 @@ export function buildExtractor(): ResultExtractor { extractors .setNextExtractor(new NullEmptyExtractor()) .setNextExtractor(new UndefinedExtractor()) + .setNextExtractor(new LongExtractor()) .setNextExtractor(new StringExtractor()) .setNextExtractor(new BooleanExtractor()) .setNextExtractor(new IntegerExtractor()) diff --git a/src/services/test-runner.ts b/src/services/test-runner.ts index ec2b64a..883ceb8 100644 --- a/src/services/test-runner.ts +++ b/src/services/test-runner.ts @@ -17,14 +17,7 @@ export interface TestRunnerOptions { } - -interface CapabilityStatementMetadata { - resourceType?: string; - fhirVersion?: string; -} - export class TestRunner { - private readonly fhirVersionCache = new Map(); public async runTests( configData: any, options: TestRunnerOptions = {} @@ -86,21 +79,6 @@ export class TestRunner { skipReason ); } - if ( - await this.shouldSkipLongR4OrEarlierCapabilityTest( - serverBaseUrl, - result, - options.useAxios - ) - ) { - this.addToSkipList( - skipMap, - result.testsName, - result.groupName, - result.testName, - 'FHIR R4 and earlier do not support Long/Integer64 values' - ); - } await this.runTest( result, cqlEngine.apiUrl!, @@ -272,79 +250,6 @@ export class TestRunner { return 0; // versions are equal } - private async getServerFhirVersion( - serverBaseUrl: string | undefined, - useAxios: boolean = false - ): Promise { - if (!serverBaseUrl) return undefined; - - const normalizedBaseUrl = serverBaseUrl.replace(/\/+$/, ''); - if (this.fhirVersionCache.has(normalizedBaseUrl)) { - return this.fhirVersionCache.get(normalizedBaseUrl); - } - - let fhirVersion: string | undefined; - - try { - const metadataUrl = `${normalizedBaseUrl}/metadata`; - - if (useAxios) { - const axios = await import('axios'); - const axiosResponse = await axios.default.get(metadataUrl, { - headers: { - Accept: 'application/fhir+json, application/json', - }, - }); - fhirVersion = axiosResponse.data?.fhirVersion; - } else { - const fetchResponse = await fetch(metadataUrl, { - method: 'GET', - headers: { - Accept: 'application/fhir+json, application/json', - }, - }); - - if (fetchResponse.ok) { - const metadata = - (await fetchResponse.json()) as CapabilityStatementMetadata; - fhirVersion = metadata.fhirVersion; - } - } - } catch (error) { - console.warn('Unable to determine FHIR version from metadata:', error); - } - - this.fhirVersionCache.set(normalizedBaseUrl, fhirVersion); - return fhirVersion; - } - - private async isR4OrEarlierServer( - serverBaseUrl: string | undefined, - useAxios: boolean = false - ): Promise { - const fhirVersion = await this.getServerFhirVersion(serverBaseUrl, useAxios); - if (typeof fhirVersion === 'string' && fhirVersion.trim() !== '') { - const major = parseInt(fhirVersion.split('.')[0], 10); - if (!Number.isNaN(major)) { - return major <= 4; - } - } - - // Fallback for URLs that explicitly include older FHIR version markers - return /\/(dstu2|stu3|r4)(\/|$)/i.test(serverBaseUrl ?? ''); - } - - private async shouldSkipLongR4OrEarlierCapabilityTest( - serverBaseUrl: string | undefined, - result: InternalTestResult, - useAxios: boolean = false - ): Promise { - if (!(await this.isR4OrEarlierServer(serverBaseUrl, useAxios))) return false; - - const capabilities = Array.isArray(result.capability) ? result.capability : []; - return capabilities.some(cap => String(cap.code ?? '').toLowerCase() === 'system.long'); - } - private shouldSkipVersionTest(cqlEngine: CQLEngine, result: InternalTestResult): boolean { const engineVersion = cqlEngine?.cqlVersion; if (!engineVersion) return false; // no version to compare against diff --git a/src/shared/results-shared.ts b/src/shared/results-shared.ts index f80cb33..dad12cb 100644 --- a/src/shared/results-shared.ts +++ b/src/shared/results-shared.ts @@ -57,7 +57,7 @@ export class Result implements InternalTestResult { ? test.capability : test.capability ? [test.capability] - : []; + : []; this.capability = testCapabilities.map(({ code, value }) => ({ code, value })); } diff --git a/test/extractResults-cql_operations.test.ts b/test/extractResults-cql_operations.test.ts index 3f751d5..2a80ab1 100644 --- a/test/extractResults-cql_operations.test.ts +++ b/test/extractResults-cql_operations.test.ts @@ -108,6 +108,61 @@ test('string response check', () => { ).toBe('abc'); }); + +test('R4 System.Long response check using valueString with L suffix', () => { + expect( + extractor!.extract({ + resourceType: 'Parameters', + parameter: [ + { + name: 'return', + extension: [ + { + url: 'http://hl7.org/fhir/StructureDefinition/cqf-cqlType', + valueString: 'System.Long', + }, + ], + valueString: '1L', + }, + ], + }) + ).toBe(1n); +}); + +test('R4 System.Long response check using valueString without L suffix', () => { + expect( + extractor!.extract({ + resourceType: 'Parameters', + parameter: [ + { + name: 'return', + extension: [ + { + url: 'http://hl7.org/fhir/StructureDefinition/cqf-cqlType', + valueString: 'System.Long', + }, + ], + valueString: '1', + }, + ], + }) + ).toBe(1n); +}); + +test('R5 System.Long response check using valueInteger64', () => { + expect( + extractor!.extract({ + resourceType: 'Parameters', + parameter: [ + { + name: 'return', + valueInteger64: '1', + }, + ], + }) + ).toBe(1n); +}); + test('singleton list-typed return stays array when singletonListKeys includes return (issue #82)', () => { expect( extractor!.extract( From 9d369e02e885c1430098d03c4c899c0ee44cc1fd Mon Sep 17 00:00:00 2001 From: Bryant Austin Date: Mon, 27 Apr 2026 14:23:52 -0600 Subject: [PATCH 4/5] file not uploaded as part of fixes --- src/shared/results-utils.ts | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/shared/results-utils.ts b/src/shared/results-utils.ts index 1fa30b5..c88096e 100644 --- a/src/shared/results-utils.ts +++ b/src/shared/results-utils.ts @@ -18,6 +18,10 @@ export function resultsEqual(expected: any, actual: any): boolean { return true; } + if (cqlDateTimesEqual(expected, actual)) { + return true; + } + if ( typeof expected !== 'object' || expected === null || @@ -40,3 +44,57 @@ export function resultsEqual(expected: any, actual: any): boolean { return true; } + +interface CqlDateTimeParts { + base: string; + timezone?: string; +} + +function cqlDateTimesEqual(expected: any, actual: any): boolean { + if (typeof expected !== 'string' || typeof actual !== 'string') { + return false; + } + + const expectedDateTime = parseCqlDateTime(expected); + const actualDateTime = parseCqlDateTime(actual); + + if (!expectedDateTime || !actualDateTime) { + return false; + } + + if (expectedDateTime.base !== actualDateTime.base) { + return false; + } + + // If either side omits timezone, compare only the local DateTime components. + // Some engines return the server default timezone for DateTimes that were + // authored without an explicit timezone, e.g. @2014-01-01T08 becomes + // @2014-01-01T08-06:00. For these conformance tests, that offset should not + // make the value fail comparison when the expected value also omits it. + if (!expectedDateTime.timezone || !actualDateTime.timezone) { + return true; + } + + return normalizeTimezone(expectedDateTime.timezone) === normalizeTimezone(actualDateTime.timezone); +} + +function parseCqlDateTime(value: string): CqlDateTimeParts | undefined { + // DateTime literals have a T component. Date-only literals do not. + if (!/^@\d{4}(?:-\d{2})?(?:-\d{2})?T/.test(value)) { + return undefined; + } + + const match = value.match(/^(.*?)(Z|[+-]\d{2}:\d{2})?$/); + if (!match) { + return undefined; + } + + return { + base: match[1], + timezone: match[2], + }; +} + +function normalizeTimezone(timezone: string): string { + return timezone === 'Z' ? '+00:00' : timezone; +} From bfa4cf9c832545239ce7651dff9fd1f9400b7a28 Mon Sep 17 00:00:00 2001 From: Bryant Austin Date: Tue, 28 Apr 2026 12:25:30 -0600 Subject: [PATCH 5/5] normalization of ucum to fix compares --- src/shared/results-utils.ts | 103 ++++++++++++++++++++++++++- src/test-results/cql-test-results.ts | 36 +++++++++- 2 files changed, 135 insertions(+), 4 deletions(-) diff --git a/src/shared/results-utils.ts b/src/shared/results-utils.ts index c88096e..3cef30a 100644 --- a/src/shared/results-utils.ts +++ b/src/shared/results-utils.ts @@ -11,7 +11,7 @@ export function resultsEqual(expected: any, actual: any): boolean { } if (typeof expected === 'number') { - return Math.abs(actual - expected) < 0.00000001; + return Math.abs(Number(actual) - expected) < 0.00000001; } if (expected === actual) { @@ -22,6 +22,10 @@ export function resultsEqual(expected: any, actual: any): boolean { return true; } + if (quantitiesEqual(expected, actual)) { + return true; + } + if ( typeof expected !== 'object' || expected === null || @@ -98,3 +102,100 @@ function parseCqlDateTime(value: string): CqlDateTimeParts | undefined { function normalizeTimezone(timezone: string): string { return timezone === 'Z' ? '+00:00' : timezone; } + +interface ParsedQuantity { + value: number; + unit: string; +} + +interface UcumConversion { + canonicalUnit: string; + factor: number; +} + +const UCUM_CONVERSIONS: Record = { + mm: { canonicalUnit: 'm', factor: 0.001 }, + cm: { canonicalUnit: 'm', factor: 0.01 }, + m: { canonicalUnit: 'm', factor: 1 }, + km: { canonicalUnit: 'm', factor: 1000 }, + mm2: { canonicalUnit: 'm2', factor: 0.000001 }, + cm2: { canonicalUnit: 'm2', factor: 0.0001 }, + m2: { canonicalUnit: 'm2', factor: 1 }, + km2: { canonicalUnit: 'm2', factor: 1000000 }, + mg: { canonicalUnit: 'g', factor: 0.001 }, + g: { canonicalUnit: 'g', factor: 1 }, + kg: { canonicalUnit: 'g', factor: 1000 }, + ms: { canonicalUnit: 's', factor: 0.001 }, + s: { canonicalUnit: 's', factor: 1 }, + min: { canonicalUnit: 's', factor: 60 }, + h: { canonicalUnit: 's', factor: 3600 }, + d: { canonicalUnit: 's', factor: 86400 }, +}; + +function quantitiesEqual(expected: any, actual: any): boolean { + const expectedQuantity = parseQuantity(expected); + const actualQuantity = parseQuantity(actual); + + if (!expectedQuantity || !actualQuantity) { + return false; + } + + // Same unit can be compared directly, even if we do not have a conversion rule. + if (expectedQuantity.unit === actualQuantity.unit) { + return numbersEqual(expectedQuantity.value, actualQuantity.value); + } + + const expectedNormalized = normalizeQuantity(expectedQuantity); + const actualNormalized = normalizeQuantity(actualQuantity); + + if (!expectedNormalized || !actualNormalized) { + return false; + } + + return ( + expectedNormalized.unit === actualNormalized.unit && + numbersEqual(expectedNormalized.value, actualNormalized.value) + ); +} + +function parseQuantity(value: any): ParsedQuantity | undefined { + if (typeof value === 'string') { + const quantityMatch = /^\s*(-?\d+(?:\.\d+)?)\s*'([^']+)'\s*$/.exec(value); + if (!quantityMatch) return undefined; + + return { + value: Number(quantityMatch[1]), + unit: quantityMatch[2], + }; + } + + if (value && typeof value === 'object' && 'value' in value) { + const quantityValue = Number(value.value); + const unit = value.unit ?? value.code; + + if (!Number.isFinite(quantityValue) || typeof unit !== 'string') { + return undefined; + } + + return { + value: quantityValue, + unit, + }; + } + + return undefined; +} + +function normalizeQuantity(quantity: ParsedQuantity): ParsedQuantity | undefined { + const conversion = UCUM_CONVERSIONS[quantity.unit]; + if (!conversion) return undefined; + + return { + value: quantity.value * conversion.factor, + unit: conversion.canonicalUnit, + }; +} + +function numbersEqual(a: number, b: number): boolean { + return Math.abs(a - b) < 0.00000001; +} diff --git a/src/test-results/cql-test-results.ts b/src/test-results/cql-test-results.ts index eb3b26d..c52f349 100644 --- a/src/test-results/cql-test-results.ts +++ b/src/test-results/cql-test-results.ts @@ -106,7 +106,7 @@ export class CQLTestResults { ...(result.responseStatus !== undefined && { responseStatus: result.responseStatus, }), - ...(result.actual !== undefined && { actual: String(result.actual) }), + ...(result.actual !== undefined && { actual: this.formatResultValue(result.actual) }), ...(result.expected && { expected: result.expected }), ...(result.error && { error: { @@ -172,6 +172,37 @@ export class CQLTestResults { return filePath; } + + /** + * Formats a value for JSON output without losing structured CQL values. + */ + private formatResultValue(value: any): string { + if (typeof value === 'bigint') { + return `${value.toString()}L`; + } + + if (value && typeof value === 'object' && 'value' in value) { + const unit = value.unit ?? value.code; + if (unit !== undefined) { + return `${this.formatNumber(value.value)}'${unit}'`; + } + } + + if (value && typeof value === 'object') { + return JSON.stringify(value); + } + + return String(value); + } + + private formatNumber(value: any): string { + const numericValue = Number(value); + if (!Number.isFinite(numericValue)) { + return String(value); + } + return Number(numericValue.toPrecision(15)).toString(); + } + /** * Equalizes value types for comparison */ @@ -188,8 +219,7 @@ export class CQLTestResults { } else if (typeof act === 'number' && typeof exp === 'string') { r.actual = String(act); } else if (act !== undefined && act !== null && typeof act !== 'string') { - // Convert any non-string value to string for schema compliance - r.actual = String(act); + r.actual = this.formatResultValue(act); } } }