]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
test(utils): audit — overhaul existing, add 3 new test files, data-driven coverage
authorJérôme Benoit <jerome.benoit@sap.com>
Fri, 6 Mar 2026 22:05:35 +0000 (23:05 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Fri, 6 Mar 2026 22:05:35 +0000 (23:05 +0100)
tests/utils/AsyncLock.test.ts
tests/utils/ChargingStationConfigurationUtils.test.ts [new file with mode: 0644]
tests/utils/Configuration.test.ts
tests/utils/ConfigurationUtils.test.ts
tests/utils/ElectricUtils.test.ts
tests/utils/ErrorUtils.test.ts
tests/utils/FileUtils.test.ts [new file with mode: 0644]
tests/utils/MessageChannelUtils.test.ts [new file with mode: 0644]
tests/utils/Utils.test.ts

index 41cf1f204e23f02205a44b553b67d8f839085301..cabd1b2fee444d3e44fdcaf85fe2d7efd86d91c7 100644 (file)
@@ -3,7 +3,6 @@
  * @description Unit tests for asynchronous lock utilities
  */
 import { expect } from '@std/expect'
-import { randomInt } from 'node:crypto'
 import { afterEach, describe, it } from 'node:test'
 
 import { AsyncLock, AsyncLockType } from '../../src/utils/AsyncLock.js'
@@ -34,8 +33,9 @@ await describe('AsyncLock', async () => {
     const executed: number[] = []
     let count = 0
     const asyncFn = async () => {
-      await new Promise(resolve => {
-        setTimeout(resolve, randomInt(1, 101))
+      // Yield to event loop without real timers
+      await new Promise<void>(resolve => {
+        queueMicrotask(resolve)
       })
       executed.push(++count)
     }
@@ -72,16 +72,19 @@ await describe('AsyncLock', async () => {
 
   await it('should isolate locks across different lock types', async () => {
     const order: string[] = []
+    let resolveConfig!: () => void
     const configPromise = AsyncLock.runExclusive(AsyncLockType.configuration, async () => {
-      await new Promise(resolve => {
-        setTimeout(resolve, 50)
+      await new Promise<void>(resolve => {
+        resolveConfig = resolve
       })
       order.push('configuration')
     })
     const perfPromise = AsyncLock.runExclusive(AsyncLockType.performance, () => {
       order.push('performance')
     })
-    await Promise.all([configPromise, perfPromise])
+    await perfPromise
+    resolveConfig()
+    await configPromise
     expect(order[0]).toBe('performance')
     expect(order[1]).toBe('configuration')
   })
diff --git a/tests/utils/ChargingStationConfigurationUtils.test.ts b/tests/utils/ChargingStationConfigurationUtils.test.ts
new file mode 100644 (file)
index 0000000..5772acd
--- /dev/null
@@ -0,0 +1,301 @@
+/**
+ * @file Tests for ChargingStationConfigurationUtils
+ * @description Unit tests for charging station configuration utility functions including
+ *   buildConnectorsStatus, buildEvsesStatus, buildChargingStationAutomaticTransactionGeneratorConfiguration,
+ *   and the OutputFormat enum.
+ */
+import { expect } from '@std/expect'
+import { afterEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
+import type { ConnectorStatus, EvseStatus } from '../../src/types/index.js'
+
+import { AvailabilityType } from '../../src/types/index.js'
+import {
+  buildChargingStationAutomaticTransactionGeneratorConfiguration,
+  buildConnectorsStatus,
+  buildEvsesStatus,
+  OutputFormat,
+} from '../../src/utils/ChargingStationConfigurationUtils.js'
+import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
+
+/**
+ * Creates a minimal mock ChargingStation for configuration utility tests.
+ * @param options - Mock station properties.
+ * @param options.automaticTransactionGenerator - ATG instance stub.
+ * @param options.connectors - Connectors map.
+ * @param options.evses - EVSEs map.
+ * @param options.getAutomaticTransactionGeneratorConfiguration - ATG config getter.
+ * @returns Partial ChargingStation cast for test use.
+ */
+function createMockStationForConfigUtils (options: {
+  automaticTransactionGenerator?: undefined | { connectorsStatus?: Map<number, unknown> }
+  connectors?: Map<number, ConnectorStatus>
+  evses?: Map<number, EvseStatus>
+  getAutomaticTransactionGeneratorConfiguration?: () => unknown
+}): ChargingStation {
+  return {
+    automaticTransactionGenerator: options.automaticTransactionGenerator ?? undefined,
+    connectors: options.connectors ?? new Map(),
+    evses: options.evses ?? new Map(),
+    getAutomaticTransactionGeneratorConfiguration:
+      options.getAutomaticTransactionGeneratorConfiguration ?? (() => undefined),
+  } as unknown as ChargingStation
+}
+
+await describe('ChargingStationConfigurationUtils', async () => {
+  afterEach(() => {
+    standardCleanup()
+  })
+
+  await describe('OutputFormat', async () => {
+    await it('should have correct enum values', () => {
+      expect(OutputFormat.configuration).toBe('configuration')
+      expect(OutputFormat.worker).toBe('worker')
+    })
+  })
+
+  await describe('buildConnectorsStatus', async () => {
+    await it('should strip internal transaction fields from connectors', () => {
+      const noop = (): void => {
+        /* noop */
+      }
+      const interval1 = setInterval(noop, 1000)
+      const interval2 = setInterval(noop, 1000)
+      try {
+        const connectors = new Map<number, ConnectorStatus>()
+        connectors.set(0, {
+          availability: AvailabilityType.Operative,
+          MeterValues: [],
+        } as ConnectorStatus)
+        connectors.set(1, {
+          availability: AvailabilityType.Operative,
+          bootStatus: 'Available',
+          MeterValues: [],
+          transactionEventQueue: [],
+          transactionSetInterval: interval1 as unknown as NodeJS.Timeout,
+          transactionTxUpdatedSetInterval: interval2 as unknown as NodeJS.Timeout,
+        } as unknown as ConnectorStatus)
+
+        const station = createMockStationForConfigUtils({ connectors })
+        const result = buildConnectorsStatus(station)
+
+        expect(result.length).toBe(2)
+        for (const connector of result) {
+          expect('transactionSetInterval' in connector).toBe(false)
+          expect('transactionEventQueue' in connector).toBe(false)
+          expect('transactionTxUpdatedSetInterval' in connector).toBe(false)
+        }
+        expect(result[1].availability).toBe(AvailabilityType.Operative)
+      } finally {
+        clearInterval(interval1)
+        clearInterval(interval2)
+      }
+    })
+
+    await it('should handle empty connectors map', () => {
+      const station = createMockStationForConfigUtils({ connectors: new Map() })
+      const result = buildConnectorsStatus(station)
+      expect(result.length).toBe(0)
+    })
+
+    await it('should preserve non-internal fields', () => {
+      const connectors = new Map<number, ConnectorStatus>()
+      connectors.set(1, {
+        availability: AvailabilityType.Operative,
+        bootStatus: 'Available',
+        MeterValues: [],
+        transactionEventQueue: undefined,
+        transactionId: 42,
+        transactionSetInterval: undefined,
+        transactionStarted: true,
+        transactionTxUpdatedSetInterval: undefined,
+      } as unknown as ConnectorStatus)
+
+      const station = createMockStationForConfigUtils({ connectors })
+      const result = buildConnectorsStatus(station)
+
+      expect(result.length).toBe(1)
+      expect(result[0].availability).toBe(AvailabilityType.Operative)
+      expect(result[0].transactionId).toBe(42)
+      expect(result[0].transactionStarted).toBe(true)
+    })
+  })
+
+  await describe('buildEvsesStatus', async () => {
+    await it('should return configuration format with connectorsStatus and without connectors', () => {
+      const evseConnectors = new Map<number, ConnectorStatus>()
+      evseConnectors.set(1, {
+        availability: AvailabilityType.Operative,
+        MeterValues: [],
+        transactionEventQueue: [],
+        transactionSetInterval: undefined,
+        transactionTxUpdatedSetInterval: undefined,
+      } as unknown as ConnectorStatus)
+
+      const evses = new Map<number, EvseStatus>()
+      evses.set(0, {
+        availability: AvailabilityType.Operative,
+        connectors: new Map<number, ConnectorStatus>(),
+      })
+      evses.set(1, {
+        availability: AvailabilityType.Operative,
+        connectors: evseConnectors,
+      })
+
+      const station = createMockStationForConfigUtils({ evses })
+      const result = buildEvsesStatus(station, OutputFormat.configuration)
+
+      expect(result.length).toBe(2)
+      const evse1 = result[1] as Record<string, unknown>
+      expect('connectorsStatus' in evse1).toBe(true)
+      expect('connectors' in evse1).toBe(false)
+    })
+
+    await it('should strip internal fields from evse connectors in configuration format', () => {
+      const evseConnectors = new Map<number, ConnectorStatus>()
+      evseConnectors.set(1, {
+        availability: AvailabilityType.Operative,
+        MeterValues: [],
+        transactionEventQueue: [],
+        transactionSetInterval: undefined,
+        transactionTxUpdatedSetInterval: undefined,
+      } as unknown as ConnectorStatus)
+
+      const evses = new Map<number, EvseStatus>()
+      evses.set(1, {
+        availability: AvailabilityType.Operative,
+        connectors: evseConnectors,
+      })
+
+      const station = createMockStationForConfigUtils({ evses })
+      const result = buildEvsesStatus(station, OutputFormat.configuration)
+      const evse1 = result[0] as Record<string, unknown>
+      const connectorsStatus = evse1.connectorsStatus as ConnectorStatus[]
+
+      expect(connectorsStatus.length).toBe(1)
+      expect('transactionSetInterval' in connectorsStatus[0]).toBe(false)
+      expect('transactionEventQueue' in connectorsStatus[0]).toBe(false)
+      expect('transactionTxUpdatedSetInterval' in connectorsStatus[0]).toBe(false)
+    })
+
+    await it('should return worker format with connectors array', () => {
+      const evseConnectors = new Map<number, ConnectorStatus>()
+      evseConnectors.set(1, {
+        availability: AvailabilityType.Operative,
+        MeterValues: [],
+        transactionEventQueue: undefined,
+        transactionSetInterval: undefined,
+        transactionTxUpdatedSetInterval: undefined,
+      } as unknown as ConnectorStatus)
+
+      const evses = new Map<number, EvseStatus>()
+      evses.set(0, {
+        availability: AvailabilityType.Operative,
+        connectors: new Map<number, ConnectorStatus>(),
+      })
+      evses.set(1, {
+        availability: AvailabilityType.Operative,
+        connectors: evseConnectors,
+      })
+
+      const station = createMockStationForConfigUtils({ evses })
+      const result = buildEvsesStatus(station, OutputFormat.worker)
+
+      expect(result.length).toBe(2)
+      const evse1 = result[1] as Record<string, unknown>
+      expect('connectors' in evse1).toBe(true)
+      expect(Array.isArray(evse1.connectors)).toBe(true)
+    })
+
+    await it('should default to configuration format when no format specified', () => {
+      const evses = new Map<number, EvseStatus>()
+      evses.set(1, {
+        availability: AvailabilityType.Operative,
+        connectors: new Map<number, ConnectorStatus>(),
+      })
+
+      const station = createMockStationForConfigUtils({ evses })
+      const result = buildEvsesStatus(station)
+
+      expect(result.length).toBe(1)
+      const evse = result[0] as Record<string, unknown>
+      expect('connectorsStatus' in evse).toBe(true)
+      expect('connectors' in evse).toBe(false)
+    })
+
+    await it('should handle empty evses map', () => {
+      const station = createMockStationForConfigUtils({ evses: new Map() })
+      const result = buildEvsesStatus(station, OutputFormat.configuration)
+      expect(result.length).toBe(0)
+    })
+
+    await it('should throw RangeError for unknown output format', () => {
+      const evses = new Map<number, EvseStatus>()
+      evses.set(1, {
+        availability: AvailabilityType.Operative,
+        connectors: new Map<number, ConnectorStatus>(),
+      })
+      const station = createMockStationForConfigUtils({ evses })
+
+      expect(() => {
+        buildEvsesStatus(station, 'unknown' as OutputFormat)
+      }).toThrow(RangeError)
+    })
+  })
+
+  await describe('buildChargingStationAutomaticTransactionGeneratorConfiguration', async () => {
+    await it('should return ATG configuration when present', () => {
+      const atgConfig = { enable: true, maxDuration: 120, minDuration: 60 }
+      const station = createMockStationForConfigUtils({
+        automaticTransactionGenerator: {
+          connectorsStatus: new Map([[1, { start: false }]]),
+        },
+        getAutomaticTransactionGeneratorConfiguration: () => atgConfig,
+      })
+      const result = buildChargingStationAutomaticTransactionGeneratorConfiguration(station)
+
+      expect(result.automaticTransactionGenerator).toStrictEqual(atgConfig)
+      expect(result.automaticTransactionGeneratorStatuses).toBeDefined()
+      expect(Array.isArray(result.automaticTransactionGeneratorStatuses)).toBe(true)
+      expect(result.automaticTransactionGeneratorStatuses?.length).toBe(1)
+    })
+
+    await it('should return ATG configuration without statuses when no ATG instance', () => {
+      const atgConfig = { enable: false }
+      const station = createMockStationForConfigUtils({
+        automaticTransactionGenerator: undefined,
+        getAutomaticTransactionGeneratorConfiguration: () => atgConfig,
+      })
+      const result = buildChargingStationAutomaticTransactionGeneratorConfiguration(station)
+
+      expect(result.automaticTransactionGenerator).toStrictEqual(atgConfig)
+      expect(result.automaticTransactionGeneratorStatuses).toBeUndefined()
+    })
+
+    await it('should return undefined ATG config when not configured', () => {
+      const station = createMockStationForConfigUtils({
+        automaticTransactionGenerator: undefined,
+        getAutomaticTransactionGeneratorConfiguration: () => undefined,
+      })
+      const result = buildChargingStationAutomaticTransactionGeneratorConfiguration(station)
+
+      expect(result.automaticTransactionGenerator).toBeUndefined()
+      expect(result.automaticTransactionGeneratorStatuses).toBeUndefined()
+    })
+
+    await it('should return ATG configuration without statuses when connectorsStatus is null', () => {
+      const atgConfig = { enable: true }
+      const station = createMockStationForConfigUtils({
+        automaticTransactionGenerator: {
+          connectorsStatus: undefined,
+        },
+        getAutomaticTransactionGeneratorConfiguration: () => atgConfig,
+      })
+      const result = buildChargingStationAutomaticTransactionGeneratorConfiguration(station)
+
+      expect(result.automaticTransactionGenerator).toStrictEqual(atgConfig)
+      expect(result.automaticTransactionGeneratorStatuses).toBeUndefined()
+    })
+  })
+})
index 9f071f5ed3ec10c8234345a8cefeda4132dedd19..aba888a743a31ebc64efbabc5ade2ee2da3daccd 100644 (file)
@@ -21,7 +21,13 @@ import type {
   WorkerConfiguration,
 } from '../../src/types/index.js'
 
-import { ConfigurationSection, SupervisionUrlDistribution } from '../../src/types/index.js'
+import {
+  ApplicationProtocol,
+  ApplicationProtocolVersion,
+  ConfigurationSection,
+  StorageType,
+  SupervisionUrlDistribution,
+} from '../../src/types/index.js'
 import { Configuration } from '../../src/utils/index.js'
 import { WorkerProcessType } from '../../src/worker/WorkerTypes.js'
 import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
@@ -88,11 +94,11 @@ await describe('Configuration', async () => {
       ConfigurationSection.worker
     )
     expect(worker).toBeDefined()
-    expect(worker.processType).toBeDefined()
-    expect(worker.startDelay).toBeDefined()
-    expect(worker.poolMinSize).toBeDefined()
-    expect(worker.poolMaxSize).toBeDefined()
-    expect(worker.elementsPerWorker).toBeDefined()
+    expect(worker.processType).toBe(WorkerProcessType.workerSet)
+    expect(worker.startDelay).toBe(500)
+    expect(typeof worker.poolMinSize).toBe('number')
+    expect(typeof worker.poolMaxSize).toBe('number')
+    expect(worker.elementsPerWorker).toBe('auto')
   })
 
   await it('should include default worker process type', () => {
@@ -107,10 +113,12 @@ await describe('Configuration', async () => {
       ConfigurationSection.uiServer
     )
     expect(uiServer).toBeDefined()
-    expect(typeof uiServer.enabled).toBe('boolean')
-    expect(uiServer.type).toBeDefined()
-    expect(uiServer.version).toBeDefined()
+    expect(uiServer.enabled).toBe(false)
+    expect(uiServer.type).toBe(ApplicationProtocol.WS)
+    expect(uiServer.version).toBe(ApplicationProtocolVersion.VERSION_11)
     expect(uiServer.options).toBeDefined()
+    expect(typeof uiServer.options?.host).toBe('string')
+    expect(typeof uiServer.options?.port).toBe('number')
   })
 
   await it('should return performance storage configuration', () => {
@@ -118,8 +126,8 @@ await describe('Configuration', async () => {
       ConfigurationSection.performanceStorage
     )
     expect(storage).toBeDefined()
-    expect(typeof storage.enabled).toBe('boolean')
-    expect(storage.type).toBeDefined()
+    expect(storage.enabled).toBe(true)
+    expect(storage.type).toBe(StorageType.NONE)
   })
 
   await it('should return station template URLs', () => {
@@ -162,9 +170,7 @@ await describe('Configuration', async () => {
 
   await it('should return supervision URLs from configuration', () => {
     const urls = Configuration.getSupervisionUrls()
-    if (urls != null) {
-      expect(typeof urls === 'string' || Array.isArray(urls)).toBe(true)
-    }
+    expect(urls == null || typeof urls === 'string' || Array.isArray(urls)).toBe(true)
   })
 
   await it('should throw for unknown configuration section', () => {
index 983d87eaa9f222ce235e64abe92a7be3c43f4fe9..e33f405de242d4356da009819954c3a9f0ae7ba3 100644 (file)
@@ -5,14 +5,13 @@
 import { expect } from '@std/expect'
 import { afterEach, describe, it } from 'node:test'
 
-import { FileType, StorageType } from '../../src/types/index.js'
+import { StorageType } from '../../src/types/index.js'
 import {
   buildPerformanceUriFilePath,
   checkWorkerElementsPerWorker,
   getDefaultPerformanceStorageUri,
   logPrefix,
 } from '../../src/utils/ConfigurationUtils.js'
-import { handleFileException } from '../../src/utils/ErrorUtils.js'
 import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
 
 await describe('ConfigurationUtils', async () => {
@@ -47,18 +46,6 @@ await describe('ConfigurationUtils', async () => {
     }).toThrow(Error)
   })
 
-  await it('should throw and log error for file exceptions', t => {
-    const mockConsoleError = t.mock.method(console, 'error')
-    const error = new Error() as NodeJS.ErrnoException
-    error.code = 'ENOENT'
-    expect(() => {
-      handleFileException('path/to/module.js', FileType.Authorization, error, 'log prefix |', {
-        consoleOut: true,
-      })
-    }).toThrow(error)
-    expect(mockConsoleError.mock.calls.length).toBe(1)
-  })
-
   await it('should validate worker elements per worker configuration', () => {
     // These calls should not throw exceptions
     expect(() => {
index ddbe29c7a008c16b593efb20833f0d1fc942c346..3be19c3a2f33c2e3470204b8704b3a1252ef0b6c 100644 (file)
@@ -56,4 +56,42 @@ await describe('ElectricUtils', async () => {
     expect(DCElectricUtils.power(0, 10)).toBe(0)
     expect(DCElectricUtils.power(400, 0)).toBe(0)
   })
+
+  // Realistic EV charging scenarios
+
+  await it('should calculate 7.4 kW single-phase AC home charger values', () => {
+    // 230V × 32A × 1 phase × cosPhi=1 = 7360W
+    expect(ACElectricUtils.powerPerPhase(230, 32)).toBe(7360)
+    expect(ACElectricUtils.powerTotal(1, 230, 32)).toBe(7360)
+    expect(ACElectricUtils.amperageTotalFromPower(7360, 230)).toBe(32)
+    expect(ACElectricUtils.amperagePerPhaseFromPower(1, 7360, 230)).toBe(32)
+  })
+
+  await it('should calculate 22 kW three-phase AC wall box values', () => {
+    // 230V × 32A × 3 phases × cosPhi=1 = 22080W
+    expect(ACElectricUtils.powerPerPhase(230, 32)).toBe(7360)
+    expect(ACElectricUtils.powerTotal(3, 230, 32)).toBe(22080)
+    expect(ACElectricUtils.amperageTotalFromPower(22080, 230)).toBe(96)
+    expect(ACElectricUtils.amperagePerPhaseFromPower(3, 22080, 230)).toBe(32)
+  })
+
+  await it('should calculate 50 kW DC fast charger values', () => {
+    expect(DCElectricUtils.power(400, 125)).toBe(50000)
+    expect(DCElectricUtils.amperage(50000, 400)).toBe(125)
+  })
+
+  await it('should calculate 150 kW DC high-power charger values', () => {
+    expect(DCElectricUtils.power(500, 300)).toBe(150000)
+    expect(DCElectricUtils.amperage(150000, 500)).toBe(300)
+  })
+
+  await it('should handle industrial cosPhi values for AC calculations', () => {
+    // cosPhi = 0.95 (typical industrial)
+    expect(ACElectricUtils.powerPerPhase(230, 32, 0.95)).toBe(6992)
+    expect(ACElectricUtils.powerTotal(3, 230, 32, 0.95)).toBe(20976)
+    expect(ACElectricUtils.amperageTotalFromPower(6992, 230, 0.95)).toBe(32)
+    // cosPhi = 0.9
+    expect(ACElectricUtils.powerPerPhase(230, 32, 0.9)).toBe(6624)
+    expect(ACElectricUtils.amperageTotalFromPower(6624, 230, 0.9)).toBe(32)
+  })
 })
index 95b2e57706628de7b2bbd6e2fcba5a91b0ca6170..83228f59e53d6692509e477e7265ede28a230a6e 100644 (file)
@@ -3,7 +3,10 @@
  * @description Unit tests for error handling utilities
  */
 import { expect } from '@std/expect'
-import { afterEach, describe, it } from 'node:test'
+import process from 'node:process'
+import { afterEach, beforeEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
 
 import {
   FileType,
@@ -16,56 +19,124 @@ import {
   handleFileException,
   handleIncomingRequestError,
   handleSendMessageError,
+  handleUncaughtException,
+  handleUnhandledRejection,
 } from '../../src/utils/ErrorUtils.js'
 import { logger } from '../../src/utils/Logger.js'
 import { TEST_CHARGING_STATION_BASE_NAME } from '../charging-station/ChargingStationTestConstants.js'
 import { createMockChargingStation } from '../charging-station/ChargingStationTestUtils.js'
-import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
+import {
+  createConsoleMocks,
+  createLoggerMocks,
+  standardCleanup,
+} from '../helpers/TestLifecycleHelpers.js'
 
 await describe('ErrorUtils', async () => {
-  const { station: chargingStation } = createMockChargingStation({
-    baseName: TEST_CHARGING_STATION_BASE_NAME,
+  let chargingStation: ChargingStation
+
+  beforeEach(() => {
+    const { station } = createMockChargingStation({
+      baseName: TEST_CHARGING_STATION_BASE_NAME,
+    })
+    chargingStation = station
   })
 
   afterEach(() => {
     standardCleanup()
   })
 
-  await it('should throw or warn based on error code and options', t => {
-    const consoleWarnMock = t.mock.method(console, 'warn')
-    const consoleErrorMock = t.mock.method(console, 'error')
-    const warnMock = t.mock.method(logger, 'warn')
-    const errorMock = t.mock.method(logger, 'error')
+  await it('should throw error with logger output when throwError is true', t => {
+    const { errorMock } = createLoggerMocks(t, logger)
     const error = new Error() as NodeJS.ErrnoException
     error.code = 'ENOENT'
     expect(() => {
       handleFileException('path/to/module.js', FileType.Authorization, error, 'log prefix |', {})
     }).toThrow(error)
+    expect(errorMock.mock.calls.length).toBe(1)
+  })
+
+  await it('should log warning with logger when throwError is false', t => {
+    const { warnMock } = createLoggerMocks(t, logger)
+    const error = new Error() as NodeJS.ErrnoException
+    error.code = 'ENOENT'
     expect(() => {
       handleFileException('path/to/module.js', FileType.Authorization, error, 'log prefix |', {
         throwError: false,
       })
     }).not.toThrow()
     expect(warnMock.mock.calls.length).toBe(1)
-    expect(errorMock.mock.calls.length).toBe(1)
+  })
+
+  await it('should throw error with console output when consoleOut is true', t => {
+    const { errorMock } = createConsoleMocks(t, { error: true })
+    const error = new Error() as NodeJS.ErrnoException
+    error.code = 'ENOENT'
     expect(() => {
       handleFileException('path/to/module.js', FileType.Authorization, error, 'log prefix |', {
         consoleOut: true,
       })
     }).toThrow(error)
+    expect(errorMock?.mock.calls.length).toBe(1)
+  })
+
+  await it('should log console warning when consoleOut and throwError are false', t => {
+    const { warnMock } = createConsoleMocks(t, { error: true, warn: true })
+    const error = new Error() as NodeJS.ErrnoException
+    error.code = 'ENOENT'
     expect(() => {
       handleFileException('path/to/module.js', FileType.Authorization, error, 'log prefix |', {
         consoleOut: true,
         throwError: false,
       })
     }).not.toThrow()
-    expect(consoleWarnMock.mock.calls.length).toBe(1)
-    expect(consoleErrorMock.mock.calls.length).toBe(1)
+    expect(warnMock?.mock.calls.length).toBe(1)
   })
 
-  await it('should log error and optionally throw for send message errors', t => {
-    const errorMock = t.mock.method(logger, 'error')
-    const logPrefixMock = t.mock.method(chargingStation, 'logPrefix')
+  await it('should produce correct log message for each error code', t => {
+    const { warnMock } = createLoggerMocks(t, logger)
+    const errorCodes = [
+      { code: 'ENOENT', expectedSubstring: 'not found' },
+      { code: 'EACCES', expectedSubstring: 'access denied' },
+      { code: 'EPERM', expectedSubstring: 'permission denied' },
+      { code: 'EISDIR', expectedSubstring: 'is a directory' },
+      { code: 'ENOSPC', expectedSubstring: 'no space left' },
+      { code: 'ENOTDIR', expectedSubstring: 'not a directory' },
+      { code: 'EEXIST', expectedSubstring: 'already exists' },
+      { code: 'EROFS', expectedSubstring: 'read-only' },
+      { code: 'UNKNOWN_CODE', expectedSubstring: 'error' },
+    ]
+    for (const { code } of errorCodes) {
+      const error = new Error() as NodeJS.ErrnoException
+      error.code = code
+      handleFileException('path/to/module.js', FileType.Authorization, error, 'log prefix |', {
+        throwError: false,
+      })
+    }
+    expect(warnMock.mock.calls.length).toBe(errorCodes.length)
+    for (let i = 0; i < errorCodes.length; i++) {
+      const call = warnMock.mock.calls[i] as unknown as { arguments: unknown[] }
+      const logMessage = String(call.arguments[0]).toLowerCase()
+      expect(logMessage.includes(errorCodes[i].expectedSubstring)).toBe(true)
+    }
+  })
+
+  await it('should register uncaught exception handler on process', t => {
+    const onMock = t.mock.method(process, 'on')
+    handleUncaughtException()
+    expect(onMock.mock.calls.length).toBe(1)
+    expect(onMock.mock.calls[0].arguments[0]).toBe('uncaughtException')
+  })
+
+  await it('should register unhandled rejection handler on process', t => {
+    const onMock = t.mock.method(process, 'on')
+    handleUnhandledRejection()
+    expect(onMock.mock.calls.length).toBe(1)
+    expect(onMock.mock.calls[0].arguments[0]).toBe('unhandledRejection')
+  })
+
+  await it('should log error and not throw for send message errors by default', t => {
+    const { errorMock } = createLoggerMocks(t, logger)
+    t.mock.method(chargingStation, 'logPrefix')
     const error = new Error()
     expect(() => {
       handleSendMessageError(
@@ -75,6 +146,13 @@ await describe('ErrorUtils', async () => {
         error
       )
     }).not.toThrow()
+    expect(errorMock.mock.calls.length).toBe(1)
+  })
+
+  await it('should throw for send message errors when throwError is true', t => {
+    const { errorMock } = createLoggerMocks(t, logger)
+    t.mock.method(chargingStation, 'logPrefix')
+    const error = new Error()
     expect(() => {
       handleSendMessageError(
         chargingStation,
@@ -84,22 +162,34 @@ await describe('ErrorUtils', async () => {
         { throwError: true }
       )
     }).toThrow(error)
-    expect(logPrefixMock.mock.calls.length).toBe(2)
-    expect(errorMock.mock.calls.length).toBe(2)
+    expect(errorMock.mock.calls.length).toBe(1)
   })
 
-  await it('should log error and return error response for incoming requests', t => {
-    const errorMock = t.mock.method(logger, 'error')
-    const logPrefixMock = t.mock.method(chargingStation, 'logPrefix')
+  await it('should log error and not throw for incoming request errors by default', t => {
+    const { errorMock } = createLoggerMocks(t, logger)
+    t.mock.method(chargingStation, 'logPrefix')
     const error = new Error()
     expect(() => {
       handleIncomingRequestError(chargingStation, IncomingRequestCommand.CLEAR_CACHE, error)
     }).not.toThrow(error)
+    expect(errorMock.mock.calls.length).toBe(1)
+  })
+
+  await it('should throw for incoming request errors when throwError is true', t => {
+    createLoggerMocks(t, logger)
+    t.mock.method(chargingStation, 'logPrefix')
+    const error = new Error()
     expect(() => {
       handleIncomingRequestError(chargingStation, IncomingRequestCommand.CLEAR_CACHE, error, {
         throwError: true,
       })
     }).toThrow()
+  })
+
+  await it('should return error response for incoming request errors', t => {
+    const { errorMock } = createLoggerMocks(t, logger)
+    t.mock.method(chargingStation, 'logPrefix')
+    const error = new Error()
     const errorResponse = {
       status: GenericStatus.Rejected,
     }
@@ -108,7 +198,6 @@ await describe('ErrorUtils', async () => {
         errorResponse,
       })
     ).toStrictEqual(errorResponse)
-    expect(logPrefixMock.mock.calls.length).toBe(3)
-    expect(errorMock.mock.calls.length).toBe(3)
+    expect(errorMock.mock.calls.length).toBe(1)
   })
 })
diff --git a/tests/utils/FileUtils.test.ts b/tests/utils/FileUtils.test.ts
new file mode 100644 (file)
index 0000000..e3fdfef
--- /dev/null
@@ -0,0 +1,95 @@
+/**
+ * @file Tests for FileUtils
+ * @description Unit tests for file watching utility functions
+ */
+import { expect } from '@std/expect'
+import { mkdtempSync, rmSync, type WatchListener, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { join } from 'node:path'
+import { afterEach, describe, it } from 'node:test'
+
+import { FileType } from '../../src/types/index.js'
+import { watchJsonFile } from '../../src/utils/FileUtils.js'
+import { logger } from '../../src/utils/Logger.js'
+import { createLoggerMocks, standardCleanup } from '../helpers/TestLifecycleHelpers.js'
+
+const noop: WatchListener<string> = () => {
+  /* intentionally empty */
+}
+
+await describe('FileUtils', async () => {
+  afterEach(() => {
+    standardCleanup()
+  })
+
+  await it('should return undefined and log info for empty file path', t => {
+    const infoMock = t.mock.method(logger, 'info')
+
+    const result = watchJsonFile('', FileType.Authorization, 'test prefix |', noop)
+
+    expect(result).toBeUndefined()
+    expect(infoMock.mock.calls.length).toBe(1)
+  })
+
+  await it('should include file type and log prefix in info log message for empty path', t => {
+    const infoMock = t.mock.method(logger, 'info')
+
+    watchJsonFile('', FileType.ChargingStationConfiguration, 'CS-001 |', noop)
+
+    expect(infoMock.mock.calls.length).toBe(1)
+    const logMessage = infoMock.mock.calls[0].arguments[0] as unknown as string
+    expect(logMessage.includes(FileType.ChargingStationConfiguration)).toBe(true)
+    expect(logMessage.includes('CS-001 |')).toBe(true)
+  })
+
+  await it('should handle watch error and return undefined for nonexistent file', t => {
+    const { warnMock } = createLoggerMocks(t, logger)
+
+    const result = watchJsonFile(
+      '/nonexistent/path/to/file.json',
+      FileType.Authorization,
+      'test prefix |',
+      noop
+    )
+
+    expect(result).toBeUndefined()
+    expect(warnMock.mock.calls.length).toBe(1)
+  })
+
+  await it('should return FSWatcher for valid file path', () => {
+    const tmpDir = mkdtempSync(join(tmpdir(), 'fileutils-test-'))
+    const tmpFile = join(tmpDir, 'test.json')
+    writeFileSync(tmpFile, '{}')
+
+    try {
+      const result = watchJsonFile(tmpFile, FileType.Authorization, 'test |', noop)
+
+      expect(result).toBeDefined()
+      result?.close()
+    } finally {
+      rmSync(tmpDir, { recursive: true })
+    }
+  })
+
+  await it('should call watch with file and listener arguments', () => {
+    const tmpDir = mkdtempSync(join(tmpdir(), 'fileutils-test-'))
+    const tmpFile = join(tmpDir, 'test.json')
+    writeFileSync(tmpFile, '{}')
+
+    try {
+      let receivedEvent = false
+      const listener: WatchListener<string> = () => {
+        receivedEvent = true
+      }
+
+      const result = watchJsonFile(tmpFile, FileType.Authorization, 'test |', listener)
+
+      expect(result).toBeDefined()
+      expect(typeof result?.close).toBe('function')
+      result?.close()
+      expect(receivedEvent).toBe(false)
+    } finally {
+      rmSync(tmpDir, { recursive: true })
+    }
+  })
+})
diff --git a/tests/utils/MessageChannelUtils.test.ts b/tests/utils/MessageChannelUtils.test.ts
new file mode 100644 (file)
index 0000000..a5e02b7
--- /dev/null
@@ -0,0 +1,212 @@
+/**
+ * @file Tests for MessageChannelUtils
+ * @description Unit tests for charging station worker message builders and performance statistics conversion
+ */
+import { expect } from '@std/expect'
+import { CircularBuffer } from 'mnemonist'
+import { afterEach, describe, it } from 'node:test'
+
+import type { ChargingStation } from '../../src/charging-station/ChargingStation.js'
+import type { Statistics, TimestampedData } from '../../src/types/index.js'
+
+import { ChargingStationWorkerMessageEvents } from '../../src/types/index.js'
+import {
+  buildAddedMessage,
+  buildDeletedMessage,
+  buildPerformanceStatisticsMessage,
+  buildStartedMessage,
+  buildStoppedMessage,
+  buildUpdatedMessage,
+} from '../../src/utils/MessageChannelUtils.js'
+import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
+
+/**
+ * Creates a minimal mock station with properties needed by MessageChannelUtils builders.
+ * @returns Mock charging station instance
+ */
+function createMockStationForMessages (): ChargingStation {
+  return {
+    automaticTransactionGenerator: undefined,
+    bootNotificationResponse: {
+      currentTime: new Date('2024-01-01T00:00:00Z'),
+      interval: 300,
+      status: 'Accepted',
+    },
+    connectors: new Map([
+      [
+        0,
+        {
+          availability: 'Operative',
+          MeterValues: [],
+        },
+      ],
+      [
+        1,
+        {
+          availability: 'Operative',
+          MeterValues: [],
+        },
+      ],
+    ]),
+    evses: new Map(),
+    getAutomaticTransactionGeneratorConfiguration: () => undefined,
+    ocppConfiguration: { configurationKey: [] },
+    started: true,
+    stationInfo: {
+      baseName: 'CS-TEST',
+      chargingStationId: 'CS-TEST-00001',
+      hashId: 'test-hash',
+      templateIndex: 1,
+      templateName: 'test-template.json',
+    },
+    wsConnection: { readyState: 1 },
+    wsConnectionUrl: new URL('ws://localhost:8080/CS-TEST-00001'),
+  } as unknown as ChargingStation
+}
+
+await describe('MessageChannelUtils', async () => {
+  afterEach(() => {
+    standardCleanup()
+  })
+
+  await it('should build added message with correct event and data', () => {
+    const station = createMockStationForMessages()
+    const message = buildAddedMessage(station)
+
+    expect(message.event).toBe(ChargingStationWorkerMessageEvents.added)
+    expect(message.data).toBeDefined()
+    expect(message.data.started).toBe(true)
+    expect(message.data.stationInfo.chargingStationId).toBe('CS-TEST-00001')
+    expect(message.data.supervisionUrl).toBe('ws://localhost:8080/CS-TEST-00001')
+    expect(typeof message.data.timestamp).toBe('number')
+  })
+
+  await it('should build deleted message with correct event', () => {
+    const station = createMockStationForMessages()
+    const message = buildDeletedMessage(station)
+
+    expect(message.event).toBe(ChargingStationWorkerMessageEvents.deleted)
+    expect(message.data).toBeDefined()
+    expect(message.data.stationInfo.chargingStationId).toBe('CS-TEST-00001')
+  })
+
+  await it('should build started message with correct event', () => {
+    const station = createMockStationForMessages()
+    const message = buildStartedMessage(station)
+
+    expect(message.event).toBe(ChargingStationWorkerMessageEvents.started)
+    expect(message.data.started).toBe(true)
+  })
+
+  await it('should build stopped message with correct event', () => {
+    const station = createMockStationForMessages()
+    const message = buildStoppedMessage(station)
+
+    expect(message.event).toBe(ChargingStationWorkerMessageEvents.stopped)
+    expect(message.data).toBeDefined()
+    expect(message.data.supervisionUrl).toBe('ws://localhost:8080/CS-TEST-00001')
+  })
+
+  await it('should build updated message with correct event', () => {
+    const station = createMockStationForMessages()
+    const message = buildUpdatedMessage(station)
+
+    expect(message.event).toBe(ChargingStationWorkerMessageEvents.updated)
+    expect(message.data).toBeDefined()
+    expect(message.data.stationInfo.chargingStationId).toBe('CS-TEST-00001')
+  })
+
+  await it('should include ws state in station messages', () => {
+    const station = createMockStationForMessages()
+    const message = buildAddedMessage(station)
+
+    expect(message.data.wsState).toBe(1)
+  })
+
+  await it('should include connectors status in station messages', () => {
+    const station = createMockStationForMessages()
+    const message = buildAddedMessage(station)
+
+    expect(Array.isArray(message.data.connectors)).toBe(true)
+    expect(message.data.connectors.length).toBe(2)
+  })
+
+  await it('should convert CircularBuffer to array in statistics data', () => {
+    const buffer = new CircularBuffer<TimestampedData>(Array, 10)
+    buffer.push({ timestamp: 1000, value: 42 })
+    buffer.push({ timestamp: 2000, value: 84 })
+
+    const statistics: Statistics = {
+      createdAt: new Date('2024-01-01T00:00:00Z'),
+      id: 'test-station-id',
+      name: 'test-station',
+      statisticsData: new Map([
+        [
+          'Heartbeat',
+          {
+            measurementTimeSeries: buffer,
+            requestCount: 5,
+            responseCount: 5,
+          },
+        ],
+      ]),
+      uri: 'ws://localhost:8080',
+    }
+
+    const message = buildPerformanceStatisticsMessage(statistics)
+
+    expect(message.event).toBe(ChargingStationWorkerMessageEvents.performanceStatistics)
+    expect(message.data.id).toBe('test-station-id')
+    expect(message.data.name).toBe('test-station')
+
+    const heartbeatStats = message.data.statisticsData.get('Heartbeat')
+    expect(heartbeatStats).toBeDefined()
+    expect(Array.isArray(heartbeatStats?.measurementTimeSeries)).toBe(true)
+    const timeSeries = heartbeatStats?.measurementTimeSeries as TimestampedData[]
+    expect(timeSeries.length).toBe(2)
+    expect(timeSeries[0].value).toBe(42)
+    expect(timeSeries[1].value).toBe(84)
+  })
+
+  await it('should preserve non-CircularBuffer measurement time series', () => {
+    const statistics: Statistics = {
+      createdAt: new Date('2024-01-01T00:00:00Z'),
+      id: 'test-id',
+      name: 'test-station',
+      statisticsData: new Map([
+        [
+          'Heartbeat',
+          {
+            measurementTimeSeries: [{ timestamp: 1000, value: 10 }],
+            requestCount: 3,
+          },
+        ],
+      ]),
+      uri: 'ws://localhost:8080',
+    }
+
+    const message = buildPerformanceStatisticsMessage(statistics)
+    const heartbeat = message.data.statisticsData.get('Heartbeat')
+    expect(Array.isArray(heartbeat?.measurementTimeSeries)).toBe(true)
+  })
+
+  await it('should preserve statistics metadata in performance message', () => {
+    const createdAt = new Date('2024-01-01T00:00:00Z')
+    const updatedAt = new Date('2024-01-02T00:00:00Z')
+
+    const statistics: Statistics = {
+      createdAt,
+      id: 'station-001',
+      name: 'station-name',
+      statisticsData: new Map(),
+      updatedAt,
+      uri: 'ws://localhost:8080',
+    }
+
+    const message = buildPerformanceStatisticsMessage(statistics)
+
+    expect(message.data.createdAt).toBe(createdAt)
+    expect(message.data.updatedAt).toBe(updatedAt)
+    expect(message.data.uri).toBe('ws://localhost:8080')
+  })
+})
index 38d0c691e0b727e7b495f40708522e911be1b0d4..ad95f5661b4f6ad93802c12a8ab7f0a79c9a0f84 100644 (file)
@@ -6,6 +6,7 @@ import { expect } from '@std/expect'
 import { hoursToMilliseconds, hoursToSeconds } from 'date-fns'
 import { CircularBuffer } from 'mnemonist'
 import { randomInt } from 'node:crypto'
+import process from 'node:process'
 import { version } from 'node:process'
 import { afterEach, describe, it } from 'node:test'
 import { satisfies } from 'semver'
@@ -13,6 +14,7 @@ import { satisfies } from 'semver'
 import type { TimestampedData } from '../../src/types/index.js'
 
 import { JSRuntime, runtime } from '../../scripts/runtime.js'
+import { MapStringifyFormat } from '../../src/types/index.js'
 import { Constants } from '../../src/utils/Constants.js'
 import {
   clampToSafeTimerValue,
@@ -28,15 +30,23 @@ import {
   formatDurationSeconds,
   generateUUID,
   getRandomFloat,
+  getRandomFloatFluctuatedRounded,
+  getRandomFloatRounded,
+  getWebSocketCloseEventStatusString,
   has,
   insertAt,
   isArraySorted,
   isAsyncFunction,
+  isCFEnvironment,
   isEmpty,
   isNotEmptyArray,
   isNotEmptyString,
   isValidDate,
+  JSONStringify,
+  logPrefix,
+  mergeDeepRight,
   once,
+  queueMicrotaskErrorThrowing,
   roundTo,
   secureRandom,
   sleep,
@@ -52,7 +62,7 @@ await describe('Utils', async () => {
   await it('should generate valid UUIDs and validate them correctly', () => {
     const uuid = generateUUID()
     expect(uuid).toBeDefined()
-    expect(uuid.length).toStrictEqual(36)
+    expect(uuid.length).toBe(36)
     expect(validateUUID(uuid)).toBe(true)
     expect(validateUUID('abcdef00-0000-4000-9000-000000000000')).toBe(true)
     expect(validateUUID('abcdef00-0000-4000-a000-000000000000')).toBe(true)
@@ -142,7 +152,7 @@ await describe('Utils', async () => {
     expect(convertToInt(null)).toBe(0)
     expect(convertToInt(0)).toBe(0)
     const randomInteger = randomInt(Constants.MAX_RANDOM_INTEGER)
-    expect(convertToInt(randomInteger)).toStrictEqual(randomInteger)
+    expect(convertToInt(randomInteger)).toBe(randomInteger)
     expect(convertToInt('-1')).toBe(-1)
     expect(convertToInt('1')).toBe(1)
     expect(convertToInt('1.1')).toBe(1)
@@ -163,7 +173,7 @@ await describe('Utils', async () => {
     expect(convertToFloat(null)).toBe(0)
     expect(convertToFloat(0)).toBe(0)
     const randomFloat = getRandomFloat()
-    expect(convertToFloat(randomFloat)).toStrictEqual(randomFloat)
+    expect(convertToFloat(randomFloat)).toBe(randomFloat)
     expect(convertToFloat('-1')).toBe(-1)
     expect(convertToFloat('1')).toBe(1)
     expect(convertToFloat('1.1')).toBe(1.1)
@@ -250,47 +260,55 @@ await describe('Utils', async () => {
   })
 
   await it('should correctly identify async functions from other types', () => {
-    expect(isAsyncFunction(null)).toBe(false)
-    expect(isAsyncFunction(undefined)).toBe(false)
-    expect(isAsyncFunction(true)).toBe(false)
-    expect(isAsyncFunction(false)).toBe(false)
-    expect(isAsyncFunction(0)).toBe(false)
-    expect(isAsyncFunction('')).toBe(false)
-    expect(isAsyncFunction([])).toBe(false)
-    expect(isAsyncFunction(new Date())).toBe(false)
-    expect(isAsyncFunction(/[a-z]/i)).toBe(false)
-    expect(isAsyncFunction(new Error())).toBe(false)
-    expect(isAsyncFunction(new Map())).toBe(false)
-    expect(isAsyncFunction(new Set())).toBe(false)
-    expect(isAsyncFunction(new WeakMap())).toBe(false)
-    expect(isAsyncFunction(new WeakSet())).toBe(false)
-    expect(isAsyncFunction(new Int8Array())).toBe(false)
-    expect(isAsyncFunction(new Uint8Array())).toBe(false)
-    expect(isAsyncFunction(new Uint8ClampedArray())).toBe(false)
-    expect(isAsyncFunction(new Int16Array())).toBe(false)
-    expect(isAsyncFunction(new Uint16Array())).toBe(false)
-    expect(isAsyncFunction(new Int32Array())).toBe(false)
-    expect(isAsyncFunction(new Uint32Array())).toBe(false)
-    expect(isAsyncFunction(new Float32Array())).toBe(false)
-    expect(isAsyncFunction(new Float64Array())).toBe(false)
-    expect(isAsyncFunction(new BigInt64Array())).toBe(false)
-    expect(isAsyncFunction(new BigUint64Array())).toBe(false)
     /* eslint-disable @typescript-eslint/no-empty-function -- Testing with empty functions to verify isAsyncFunction correctly identifies async vs sync */
-    expect(isAsyncFunction(new Promise(() => {}))).toBe(false)
-    expect(isAsyncFunction(new WeakRef({}))).toBe(false)
-    expect(isAsyncFunction(new FinalizationRegistry(() => {}))).toBe(false)
-    expect(isAsyncFunction(new ArrayBuffer(16))).toBe(false)
-    expect(isAsyncFunction(new SharedArrayBuffer(16))).toBe(false)
-    expect(isAsyncFunction(new DataView(new ArrayBuffer(16)))).toBe(false)
-    expect(isAsyncFunction({})).toBe(false)
-    expect(isAsyncFunction({ a: 1 })).toBe(false)
-    expect(isAsyncFunction(() => {})).toBe(false)
-    expect(isAsyncFunction(function () {})).toBe(false)
-    expect(isAsyncFunction(function named () {})).toBe(false)
-    expect(isAsyncFunction(async () => {})).toBe(true)
-    expect(isAsyncFunction(async function () {})).toBe(true)
-    expect(isAsyncFunction(async function named () {})).toBe(true)
+    const nonAsyncValues: unknown[] = [
+      null,
+      undefined,
+      true,
+      false,
+      0,
+      '',
+      [],
+      new Date(),
+      /[a-z]/i,
+      new Error(),
+      new Map(),
+      new Set(),
+      new WeakMap(),
+      new WeakSet(),
+      new Int8Array(),
+      new Uint8Array(),
+      new Uint8ClampedArray(),
+      new Int16Array(),
+      new Uint16Array(),
+      new Int32Array(),
+      new Uint32Array(),
+      new Float32Array(),
+      new Float64Array(),
+      new BigInt64Array(),
+      new BigUint64Array(),
+      new Promise(() => {}),
+      new WeakRef({}),
+      new FinalizationRegistry(() => {}),
+      new ArrayBuffer(16),
+      new SharedArrayBuffer(16),
+      new DataView(new ArrayBuffer(16)),
+      {},
+      { a: 1 },
+      () => {},
+      function () {},
+      function named () {},
+    ]
+    for (const value of nonAsyncValues) {
+      expect(isAsyncFunction(value)).toBe(false)
+    }
+
+    const asyncValues: unknown[] = [async () => {}, async function () {}, async function named () {}]
+    for (const value of asyncValues) {
+      expect(isAsyncFunction(value)).toBe(true)
+    }
     /* eslint-enable @typescript-eslint/no-empty-function */
+
     class TestClass {
       /* eslint-disable @typescript-eslint/no-empty-function -- Testing class methods and properties */
       public static async testStaticAsync (): Promise<void> {}
@@ -605,4 +623,127 @@ await describe('Utils', async () => {
     expect(maxDelay).toBeGreaterThanOrEqual(102400) // ~102 seconds
     expect(maxDelay).toBeLessThanOrEqual(122880)
   })
+
+  await it('should return timestamped log prefix with optional string', () => {
+    const result = logPrefix()
+    expect(typeof result).toBe('string')
+    expect(result.length).toBeGreaterThan(0)
+    const withPrefix = logPrefix(' Test |')
+    expect(withPrefix).toContain(' Test |')
+  })
+
+  await it('should deep merge objects with source overriding target', () => {
+    // Simple merge
+    expect(mergeDeepRight({ a: 1 }, { b: 2 })).toStrictEqual({ a: 1, b: 2 })
+    // Source overrides target
+    expect(mergeDeepRight({ a: 1 }, { a: 2 })).toStrictEqual({ a: 2 })
+    // Nested merge
+    expect(mergeDeepRight({ a: { b: 1, c: 2 } }, { a: { c: 3, d: 4 } })).toStrictEqual({
+      a: { b: 1, c: 3, d: 4 },
+    })
+    // Deeply nested
+    expect(mergeDeepRight({ a: { b: { c: 1 } } }, { a: { b: { d: 2 } } })).toStrictEqual({
+      a: { b: { c: 1, d: 2 } },
+    })
+    // Non-object source value replaces target object
+    expect(mergeDeepRight({ a: { b: 1 } }, { a: 'string' })).toStrictEqual({ a: 'string' })
+    // Empty objects
+    expect(mergeDeepRight({}, { a: 1 })).toStrictEqual({ a: 1 })
+    expect(mergeDeepRight({ a: 1 }, {})).toStrictEqual({ a: 1 })
+  })
+
+  await it('should stringify objects with Map and Set support', () => {
+    // Basic object
+    expect(JSONStringify({ a: 1 })).toBe('{"a":1}')
+    // Map as array (default)
+    const map = new Map([['key', { value: 1 }]])
+    expect(JSONStringify(map)).toBe('[["key",{"value":1}]]')
+    // Map as object
+    expect(JSONStringify(map, undefined, MapStringifyFormat.object)).toBe('{"key":{"value":1}}')
+    // Set
+    const set = new Set([{ a: 1 }])
+    expect(JSONStringify(set)).toBe('[{"a":1}]')
+    // With space formatting
+    expect(JSONStringify({ a: 1 }, 2)).toBe('{\n  "a": 1\n}')
+  })
+
+  await it('should return human readable string for websocket close codes', () => {
+    // Known codes
+    expect(getWebSocketCloseEventStatusString(1000)).toBe('Normal Closure')
+    expect(getWebSocketCloseEventStatusString(1001)).toBe('Going Away')
+    expect(getWebSocketCloseEventStatusString(1006)).toBe('Abnormal Closure')
+    expect(getWebSocketCloseEventStatusString(1011)).toBe('Server Internal Error')
+    // Ranges
+    expect(getWebSocketCloseEventStatusString(0)).toBe('(Unused)')
+    expect(getWebSocketCloseEventStatusString(999)).toBe('(Unused)')
+    expect(getWebSocketCloseEventStatusString(1016)).toBe('(For WebSocket standard)')
+    expect(getWebSocketCloseEventStatusString(1999)).toBe('(For WebSocket standard)')
+    expect(getWebSocketCloseEventStatusString(2000)).toBe('(For WebSocket extensions)')
+    expect(getWebSocketCloseEventStatusString(2999)).toBe('(For WebSocket extensions)')
+    expect(getWebSocketCloseEventStatusString(3000)).toBe('(For libraries and frameworks)')
+    expect(getWebSocketCloseEventStatusString(3999)).toBe('(For libraries and frameworks)')
+    expect(getWebSocketCloseEventStatusString(4000)).toBe('(For applications)')
+    expect(getWebSocketCloseEventStatusString(4999)).toBe('(For applications)')
+    // Unknown
+    expect(getWebSocketCloseEventStatusString(5000)).toBe('(Unknown)')
+  })
+
+  await it('should generate random float rounded to specified scale', () => {
+    const result = getRandomFloatRounded(10, 0, 2)
+    expect(result).toBeGreaterThanOrEqual(0)
+    expect(result).toBeLessThanOrEqual(10)
+    // Check rounding to 2 decimal places
+    const decimalStr = result.toString()
+    if (decimalStr.includes('.')) {
+      expect(decimalStr.split('.')[1].length).toBeLessThanOrEqual(2)
+    }
+    // Default scale
+    const defaultScale = getRandomFloatRounded(10, 0)
+    expect(defaultScale).toBeGreaterThanOrEqual(0)
+    expect(defaultScale).toBeLessThanOrEqual(10)
+  })
+
+  await it('should generate fluctuated random float within percentage range', () => {
+    // 0% fluctuation returns static value rounded
+    expect(getRandomFloatFluctuatedRounded(100, 0)).toBe(100)
+    // 10% fluctuation: 100 ± 10
+    const result = getRandomFloatFluctuatedRounded(100, 10)
+    expect(result).toBeGreaterThanOrEqual(90)
+    expect(result).toBeLessThanOrEqual(110)
+    // Invalid fluctuation percent
+    expect(() => getRandomFloatFluctuatedRounded(100, -1)).toThrow(RangeError)
+    expect(() => getRandomFloatFluctuatedRounded(100, 101)).toThrow(RangeError)
+    // Negative static value with fluctuation
+    const negResult = getRandomFloatFluctuatedRounded(-100, 10)
+    expect(negResult).toBeGreaterThanOrEqual(-110)
+    expect(negResult).toBeLessThanOrEqual(-90)
+  })
+
+  await it('should detect Cloud Foundry environment from VCAP_APPLICATION', () => {
+    const originalVcap = process.env.VCAP_APPLICATION
+    try {
+      delete process.env.VCAP_APPLICATION
+      expect(isCFEnvironment()).toBe(false)
+      process.env.VCAP_APPLICATION = '{}'
+      expect(isCFEnvironment()).toBe(true)
+    } finally {
+      if (originalVcap != null) {
+        process.env.VCAP_APPLICATION = originalVcap
+      } else {
+        delete process.env.VCAP_APPLICATION
+      }
+    }
+  })
+
+  await it('should queue microtask that throws the given error', t => {
+    const error = new Error('test microtask error')
+    // eslint-disable-next-line @typescript-eslint/no-empty-function -- Mock queueMicrotask with no-op to prevent actual throw
+    const mockFn = t.mock.method(globalThis, 'queueMicrotask', () => {})
+    queueMicrotaskErrorThrowing(error)
+    expect(mockFn.mock.callCount()).toBe(1)
+    const callback = mockFn.mock.calls[0].arguments[0] as () => void
+    expect(() => {
+      callback()
+    }).toThrow(error)
+  })
 })