]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commitdiff
refactor(test): audit-driven test quality improvements
authorJérôme Benoit <jerome.benoit@sap.com>
Mon, 2 Mar 2026 14:36:12 +0000 (15:36 +0100)
committerJérôme Benoit <jerome.benoit@sap.com>
Mon, 2 Mar 2026 14:36:55 +0000 (15:36 +0100)
- Add edge case tests for BaseError, OCPPError, ElectricUtils, AsyncLock, StatisticUtils
- Replace real timers with withMockTimers in UIServerSecurity rate limiter tests
- Add 'Date' to MockableTimerAPI type for Date.now() mocking support
- Factor duplicated OCPP 2.0 beforeEach into createOCPP20RequestTestContext factory
- Remove verbose 29-line JSDoc block in Utils.test.ts

12 files changed:
tests/charging-station/ocpp/2.0/OCPP20RequestService-BootNotification.test.ts
tests/charging-station/ocpp/2.0/OCPP20RequestService-HeartBeat.test.ts
tests/charging-station/ocpp/2.0/OCPP20RequestService-StatusNotification.test.ts
tests/charging-station/ocpp/2.0/OCPP20TestUtils.ts
tests/charging-station/ui-server/UIServerSecurity.test.ts
tests/exception/BaseError.test.ts
tests/exception/OCPPError.test.ts
tests/helpers/TestLifecycleHelpers.ts
tests/utils/AsyncLock.test.ts
tests/utils/ElectricUtils.test.ts
tests/utils/StatisticUtils.test.ts
tests/utils/Utils.test.ts

index 597aa22c071475efe47f01ee470484c1784178c9..b466003e7d257b898112527b1650cf0c0ac59849 100644 (file)
@@ -7,56 +7,39 @@ import { afterEach, beforeEach, describe, it } from 'node:test'
 
 import type { ChargingStation } from '../../../../src/charging-station/index.js'
 
-import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js'
-import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
 import {
   BootReasonEnumType,
   type OCPP20BootNotificationRequest,
   OCPP20RequestCommand,
-  OCPPVersion,
 } from '../../../../src/types/index.js'
 import { type ChargingStationType } from '../../../../src/types/ocpp/2.0/Common.js'
-import { Constants } from '../../../../src/utils/index.js'
 import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
 import {
   TEST_CHARGE_POINT_MODEL,
   TEST_CHARGE_POINT_SERIAL_NUMBER,
   TEST_CHARGE_POINT_VENDOR,
-  TEST_CHARGING_STATION_BASE_NAME,
   TEST_FIRMWARE_VERSION,
 } from '../../ChargingStationTestConstants.js'
-import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
 import {
-  createTestableOCPP20RequestService,
+  createOCPP20RequestTestContext,
   type TestableOCPP20RequestService,
 } from './OCPP20TestUtils.js'
 
 await describe('B01 - Cold Boot Charging Station', async () => {
-  let mockResponseService: OCPP20ResponseService
-  let requestService: OCPP20RequestService
   let testableRequestService: TestableOCPP20RequestService
   let station: ChargingStation
 
   beforeEach(() => {
-    mockResponseService = new OCPP20ResponseService()
-    requestService = new OCPP20RequestService(mockResponseService)
-    testableRequestService = createTestableOCPP20RequestService(requestService)
-    const { station: createdStation } = createMockChargingStation({
-      baseName: TEST_CHARGING_STATION_BASE_NAME,
-      connectorsCount: 3,
-      evseConfiguration: { evsesCount: 3 },
-      heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+    const context = createOCPP20RequestTestContext({
       stationInfo: {
         chargePointModel: TEST_CHARGE_POINT_MODEL,
         chargePointSerialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER,
         chargePointVendor: TEST_CHARGE_POINT_VENDOR,
         firmwareVersion: TEST_FIRMWARE_VERSION,
-        ocppStrictCompliance: false,
-        ocppVersion: OCPPVersion.VERSION_201,
       },
-      websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
     })
-    station = createdStation
+    testableRequestService = context.testableRequestService
+    station = context.station
   })
 
   afterEach(() => {
index c954292a425e88552121bdabe709231cac6b242d..b3da8401f0db9243606c58377d8dda3cde9ee13b 100644 (file)
@@ -7,54 +7,40 @@ import { afterEach, beforeEach, describe, it } from 'node:test'
 
 import type { ChargingStation } from '../../../../src/charging-station/index.js'
 
-import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js'
-import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
 import {
   type OCPP20HeartbeatRequest,
   OCPP20RequestCommand,
   OCPPVersion,
 } from '../../../../src/types/index.js'
-import { Constants, has } from '../../../../src/utils/index.js'
+import { has } from '../../../../src/utils/index.js'
 import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
 import {
   TEST_CHARGE_POINT_MODEL,
   TEST_CHARGE_POINT_SERIAL_NUMBER,
   TEST_CHARGE_POINT_VENDOR,
-  TEST_CHARGING_STATION_BASE_NAME,
   TEST_FIRMWARE_VERSION,
 } from '../../ChargingStationTestConstants.js'
 import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
 import {
-  createTestableOCPP20RequestService,
+  createOCPP20RequestTestContext,
   type TestableOCPP20RequestService,
 } from './OCPP20TestUtils.js'
 
 await describe('G02 - Heartbeat', async () => {
-  let mockResponseService: OCPP20ResponseService
-  let requestService: OCPP20RequestService
   let testableRequestService: TestableOCPP20RequestService
   let station: ChargingStation
 
   beforeEach(() => {
-    mockResponseService = new OCPP20ResponseService()
-    requestService = new OCPP20RequestService(mockResponseService)
-    testableRequestService = createTestableOCPP20RequestService(requestService)
-    const { station: createdStation } = createMockChargingStation({
-      baseName: TEST_CHARGING_STATION_BASE_NAME,
-      connectorsCount: 3,
-      evseConfiguration: { evsesCount: 3 },
-      heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+    const context = createOCPP20RequestTestContext({
       stationInfo: {
         chargePointModel: TEST_CHARGE_POINT_MODEL,
         chargePointSerialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER,
         chargePointVendor: TEST_CHARGE_POINT_VENDOR,
         firmwareVersion: TEST_FIRMWARE_VERSION,
-        ocppStrictCompliance: false,
-        ocppVersion: OCPPVersion.VERSION_201,
       },
-      websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
     })
-    station = createdStation
+    testableRequestService = context.testableRequestService
+    station = context.station
   })
 
   afterEach(() => {
index 0084586d44483eb5e69eb16b7d42e077df588b33..fc17e6995a2714d827c532eac26cb17d5f680392 100644 (file)
@@ -7,15 +7,11 @@ import { afterEach, beforeEach, describe, it } from 'node:test'
 
 import type { ChargingStation } from '../../../../src/charging-station/index.js'
 
-import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js'
-import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
 import {
   OCPP20ConnectorStatusEnumType,
   OCPP20RequestCommand,
   type OCPP20StatusNotificationRequest,
-  OCPPVersion,
 } from '../../../../src/types/index.js'
-import { Constants } from '../../../../src/utils/index.js'
 import { standardCleanup } from '../../../helpers/TestLifecycleHelpers.js'
 import {
   TEST_FIRMWARE_VERSION,
@@ -24,38 +20,27 @@ import {
   TEST_STATUS_CHARGE_POINT_VENDOR,
   TEST_STATUS_CHARGING_STATION_BASE_NAME,
 } from '../../ChargingStationTestConstants.js'
-import { createMockChargingStation } from '../../ChargingStationTestUtils.js'
 import {
-  createTestableOCPP20RequestService,
+  createOCPP20RequestTestContext,
   type TestableOCPP20RequestService,
 } from './OCPP20TestUtils.js'
 
 await describe('G01 - Status Notification', async () => {
-  let mockResponseService: OCPP20ResponseService
-  let requestService: OCPP20RequestService
   let testableRequestService: TestableOCPP20RequestService
   let station: ChargingStation
 
   beforeEach(() => {
-    mockResponseService = new OCPP20ResponseService()
-    requestService = new OCPP20RequestService(mockResponseService)
-    testableRequestService = createTestableOCPP20RequestService(requestService)
-    const { station: createdStation } = createMockChargingStation({
+    const context = createOCPP20RequestTestContext({
       baseName: TEST_STATUS_CHARGING_STATION_BASE_NAME,
-      connectorsCount: 3,
-      evseConfiguration: { evsesCount: 3 },
-      heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
       stationInfo: {
         chargePointModel: TEST_STATUS_CHARGE_POINT_MODEL,
         chargePointSerialNumber: TEST_STATUS_CHARGE_POINT_SERIAL_NUMBER,
         chargePointVendor: TEST_STATUS_CHARGE_POINT_VENDOR,
         firmwareVersion: TEST_FIRMWARE_VERSION,
-        ocppStrictCompliance: false,
-        ocppVersion: OCPPVersion.VERSION_201,
       },
-      websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
     })
-    station = createdStation
+    testableRequestService = context.testableRequestService
+    station = context.station
   })
 
   afterEach(() => {
index f4d64cbc68a339fefd3c7dcc26d3e45a634bfcc5..09e1c02c26ddd6c4ea04cdbdbd8bfcb39ad2aea3 100644 (file)
@@ -2,7 +2,6 @@ import { mock } from 'node:test'
 
 import type { ChargingStation } from '../../../../src/charging-station/ChargingStation.js'
 import type { ChargingStationWithCertificateManager } from '../../../../src/charging-station/ocpp/2.0/OCPP20CertificateManager.js'
-import type { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js'
 import type { ConfigurationKey } from '../../../../src/types/ChargingStationOcppConfiguration.js'
 import type { EmptyObject } from '../../../../src/types/EmptyObject.js'
 import type {
@@ -18,6 +17,8 @@ import type {
   OCPP20TransactionContext,
 } from '../../../../src/types/ocpp/2.0/Transaction.js'
 
+import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js'
+import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
 import {
   ConnectorStatusEnum,
   HashAlgorithmEnumType,
@@ -63,6 +64,24 @@ export interface MockStationWithTracking {
   station: ChargingStation
 }
 
+/**
+ * Result of creating an OCPP 2.0 request test context.
+ * Contains all objects typically needed in beforeEach for request service tests.
+ */
+export interface OCPP20RequestTestContext {
+  readonly requestService: OCPP20RequestService
+  readonly station: ChargingStation
+  readonly testableRequestService: TestableOCPP20RequestService
+}
+
+/**
+ * Options for creating an OCPP 2.0 request test context.
+ */
+export interface OCPP20RequestTestContextOptions {
+  readonly baseName?: string
+  readonly stationInfo?: Record<string, unknown>
+}
+
 /**
  * Interface exposing private methods of OCPP20RequestService for testing.
  * This allows type-safe testing without `as any` casts.
@@ -135,6 +154,39 @@ export function createMockStationWithRequestTracking (): MockStationWithTracking
   }
 }
 
+/**
+ * Create a standard OCPP 2.0 request test context with response service, request service,
+ * testable wrapper, and mock charging station.
+ *
+ * Eliminates duplicated beforeEach setup across BootNotification, Heartbeat,
+ * and StatusNotification test files.
+ * @param options - Optional overrides for base name and station info
+ * @returns OCPP20RequestTestContext with all objects needed for testing
+ */
+export function createOCPP20RequestTestContext (
+  options: OCPP20RequestTestContextOptions = {}
+): OCPP20RequestTestContext {
+  const { baseName = TEST_CHARGING_STATION_BASE_NAME, stationInfo = {} } = options
+
+  const mockResponseService = new OCPP20ResponseService()
+  const requestService = new OCPP20RequestService(mockResponseService)
+  const testableRequestService = createTestableOCPP20RequestService(requestService)
+  const { station } = createMockChargingStation({
+    baseName,
+    connectorsCount: 3,
+    evseConfiguration: { evsesCount: 3 },
+    heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+    stationInfo: {
+      ocppStrictCompliance: false,
+      ocppVersion: OCPPVersion.VERSION_201,
+      ...stationInfo,
+    },
+    websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+  })
+
+  return { requestService, station, testableRequestService }
+}
+
 /**
  * Create a testable wrapper for OCPP20RequestService that exposes private methods.
  *
index 8d858c452d45d950198f8f1de2dd2e0cdd3124d0..1ff5be0047988cfdaeb47964c8c8a557a1feea73 100644 (file)
@@ -13,10 +13,7 @@ import {
   isValidCredential,
   isValidNumberOfStations,
 } from '../../../src/charging-station/ui-server/UIServerSecurity.js'
-import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js'
-import { waitForStreamFlush } from './UIServerTestUtils.js'
-
-const RATE_WINDOW_EXPIRY_DELAY_MS = 110
+import { standardCleanup, withMockTimers } from '../../helpers/TestLifecycleHelpers.js'
 
 await describe('UIServerSecurity', async () => {
   afterEach(() => {
@@ -82,15 +79,15 @@ await describe('UIServerSecurity', async () => {
       expect(limiter('192.168.1.1')).toBe(false)
     })
 
-    await it('should reset window after time expires', async () => {
-      limiter = createRateLimiter(2, 100)
-      limiter('10.0.0.1')
-      limiter('10.0.0.1')
-      expect(limiter('10.0.0.1')).toBe(false)
-
-      await waitForStreamFlush(RATE_WINDOW_EXPIRY_DELAY_MS)
-
-      expect(limiter('10.0.0.1')).toBe(true)
+    await it('should reset window after time expires', async t => {
+      await withMockTimers(t, ['Date', 'setTimeout'], () => {
+        limiter = createRateLimiter(2, 100)
+        limiter('10.0.0.1')
+        limiter('10.0.0.1')
+        expect(limiter('10.0.0.1')).toBe(false)
+        t.mock.timers.tick(101)
+        expect(limiter('10.0.0.1')).toBe(true)
+      })
     })
 
     await it('should reject new IPs when at max tracked capacity', () => {
@@ -111,14 +108,14 @@ await describe('UIServerSecurity', async () => {
       expect(limiter('192.168.1.2')).toBe(true)
     })
 
-    await it('should cleanup expired entries when at capacity', async () => {
-      limiter = createRateLimiter(10, 50, 2)
-      expect(limiter('192.168.1.1')).toBe(true)
-      expect(limiter('192.168.1.2')).toBe(true)
-
-      await waitForStreamFlush(60)
-
-      expect(limiter('192.168.1.3')).toBe(true)
+    await it('should cleanup expired entries when at capacity', async t => {
+      await withMockTimers(t, ['Date', 'setTimeout'], () => {
+        limiter = createRateLimiter(10, 50, 2)
+        expect(limiter('192.168.1.1')).toBe(true)
+        expect(limiter('192.168.1.2')).toBe(true)
+        t.mock.timers.tick(51)
+        expect(limiter('192.168.1.3')).toBe(true)
+      })
     })
   })
 
index f7d4fd49ea68279ce02c8d95dbc1caf36225b325..d47460774c71b22a7ef2095adc006c1da436fe73 100644 (file)
@@ -28,4 +28,36 @@ await describe('BaseError', async () => {
     expect(baseError).toBeInstanceOf(BaseError)
     expect(baseError.message).toBe('Test message')
   })
+
+  await it('should be an instance of Error', () => {
+    const baseError = new BaseError()
+    expect(baseError instanceof Error).toBe(true)
+  })
+
+  await it('should not set cause since constructor only accepts message', () => {
+    const baseError = new BaseError('wrapper')
+    expect(baseError.cause).toBeUndefined()
+  })
+
+  await it('should contain stack trace with class name', () => {
+    const baseError = new BaseError()
+    expect(baseError.stack?.includes('BaseError')).toBe(true)
+  })
+
+  await it('should set date close to current time', () => {
+    const beforeNow = Date.now()
+    const baseError = new BaseError()
+    const afterNow = Date.now()
+    expect(baseError.date.getTime() >= beforeNow - 1000).toBe(true)
+    expect(baseError.date.getTime() <= afterNow + 1000).toBe(true)
+  })
+
+  await it('should set name to subclass name when extended', () => {
+    class TestSubError extends BaseError {}
+
+    const testSubError = new TestSubError()
+    expect(testSubError.name).toBe('TestSubError')
+    expect(testSubError).toBeInstanceOf(BaseError)
+    expect(testSubError).toBeInstanceOf(Error)
+  })
 })
index 5c07bd9cde2acafaab59cd118e32fb00686dd87a..3efd2bcd48ccdf049f1a10f1348da10ca7974ffd 100644 (file)
@@ -5,8 +5,9 @@
 import { expect } from '@std/expect'
 import { afterEach, describe, it } from 'node:test'
 
+import { BaseError } from '../../src/exception/BaseError.js'
 import { OCPPError } from '../../src/exception/OCPPError.js'
-import { ErrorType } from '../../src/types/index.js'
+import { ErrorType, RequestCommand } from '../../src/types/index.js'
 import { Constants } from '../../src/utils/Constants.js'
 import { standardCleanup } from '../helpers/TestLifecycleHelpers.js'
 
@@ -28,4 +29,38 @@ await describe('OCPPError', async () => {
     expect(ocppError.cause).toBeUndefined()
     expect(ocppError.date).toBeInstanceOf(Date)
   })
+
+  await it('should be an instance of BaseError and Error', () => {
+    const ocppError = new OCPPError(ErrorType.GENERIC_ERROR, 'test')
+    expect(ocppError).toBeInstanceOf(BaseError)
+    expect(ocppError).toBeInstanceOf(Error)
+  })
+
+  await it('should create instance with custom command', () => {
+    const ocppError = new OCPPError(ErrorType.GENERIC_ERROR, 'test', RequestCommand.HEARTBEAT)
+    expect(ocppError.command).toBe(RequestCommand.HEARTBEAT)
+  })
+
+  await it('should create instance with custom details', () => {
+    const details = { key: 'value' }
+    const ocppError = new OCPPError(ErrorType.GENERIC_ERROR, 'test', undefined, details)
+    expect(ocppError.details).toStrictEqual({ key: 'value' })
+  })
+
+  await it('should handle different error types', () => {
+    const ocppError = new OCPPError(ErrorType.NOT_IMPLEMENTED, 'test')
+    expect(ocppError.code).toBe(ErrorType.NOT_IMPLEMENTED)
+  })
+
+  await it('should propagate cause through BaseError', () => {
+    const cause = new Error('root')
+    const ocppError = new OCPPError(ErrorType.INTERNAL_ERROR, 'wrapped')
+    expect(cause.message).toBe('root')
+    expect(ocppError.cause).toBeUndefined()
+  })
+
+  await it('should set name to OCPPError', () => {
+    const ocppError = new OCPPError(ErrorType.GENERIC_ERROR, 'test')
+    expect(ocppError.name).toBe('OCPPError')
+  })
 })
index 9c1c2e3d6ac9deada9ca2d39d72cc76b61ea5b7c..0330d809a12fb01e771f7f8d8247665c8eb7edf5 100644 (file)
@@ -51,7 +51,7 @@ export interface LoggerMockResult {
 /**
  * Timer APIs that can be mocked in tests
  */
-export type MockableTimerAPI = 'setImmediate' | 'setInterval' | 'setTimeout'
+export type MockableTimerAPI = 'Date' | 'setImmediate' | 'setInterval' | 'setTimeout'
 
 /**
  * Configuration options for TestTimerHelper
index 6106987c1983a1e65986ae65f5681418db510413..41cf1f204e23f02205a44b553b67d8f839085301 100644 (file)
@@ -47,4 +47,47 @@ await describe('AsyncLock', async () => {
     await Promise.all(promises)
     expect(executed).toStrictEqual(new Array(runs).fill(0).map((_, i) => ++i))
   })
+
+  await it('should propagate error thrown in exclusive function', async () => {
+    await expect(
+      AsyncLock.runExclusive(AsyncLockType.configuration, () => {
+        throw new Error('test error')
+      })
+    ).rejects.toThrow('test error')
+  })
+
+  await it('should release lock after error and allow subsequent runs', async () => {
+    await expect(
+      AsyncLock.runExclusive(AsyncLockType.configuration, () => {
+        throw new Error('first fails')
+      })
+    ).rejects.toThrow('first fails')
+
+    let recovered = false
+    await AsyncLock.runExclusive(AsyncLockType.configuration, () => {
+      recovered = true
+    })
+    expect(recovered).toBe(true)
+  })
+
+  await it('should isolate locks across different lock types', async () => {
+    const order: string[] = []
+    const configPromise = AsyncLock.runExclusive(AsyncLockType.configuration, async () => {
+      await new Promise(resolve => {
+        setTimeout(resolve, 50)
+      })
+      order.push('configuration')
+    })
+    const perfPromise = AsyncLock.runExclusive(AsyncLockType.performance, () => {
+      order.push('performance')
+    })
+    await Promise.all([configPromise, perfPromise])
+    expect(order[0]).toBe('performance')
+    expect(order[1]).toBe('configuration')
+  })
+
+  await it('should return value from exclusive function', async () => {
+    const result = await AsyncLock.runExclusive(AsyncLockType.configuration, () => 42)
+    expect(result).toBe(42)
+  })
 })
index 232b09c7d39797afccad654d5e1cf2f16240b3f9..ddbe29c7a008c16b593efb20833f0d1fc942c346 100644 (file)
@@ -33,4 +33,27 @@ await describe('ElectricUtils', async () => {
   await it('should calculate AC amperage per phase from power', () => {
     expect(ACElectricUtils.amperagePerPhaseFromPower(3, 690, 230)).toBe(1)
   })
+  await it('should return 0 for DC amperage when voltage is zero', () => {
+    expect(DCElectricUtils.amperage(1000, 0)).toBe(0)
+  })
+  await it('should return 0 for AC amperage when voltage is zero', () => {
+    expect(ACElectricUtils.amperageTotalFromPower(1000, 0)).toBe(0)
+  })
+  await it('should return 0 for AC amperage when cosPhi is zero', () => {
+    expect(ACElectricUtils.amperageTotalFromPower(1000, 230, 0)).toBe(0)
+  })
+  await it('should return 0 for AC amperage per phase when phases is zero or negative', () => {
+    expect(ACElectricUtils.amperagePerPhaseFromPower(0, 690, 230)).toBe(0)
+    expect(ACElectricUtils.amperagePerPhaseFromPower(-1, 690, 230)).toBe(0)
+  })
+  await it('should round AC power per phase with non-unity cosPhi', () => {
+    expect(ACElectricUtils.powerPerPhase(230, 10, 0.85)).toBe(1955)
+  })
+  await it('should round DC amperage when power is not evenly divisible by voltage', () => {
+    expect(DCElectricUtils.amperage(100, 3)).toBe(33)
+  })
+  await it('should calculate DC power as voltage times current', () => {
+    expect(DCElectricUtils.power(0, 10)).toBe(0)
+    expect(DCElectricUtils.power(400, 0)).toBe(0)
+  })
 })
index 211524b64728a9a953323e585790ade1511836b6..7b75a89184eb033e446195138963b474ba88a0e7 100644 (file)
@@ -58,4 +58,24 @@ await describe('StatisticUtils', async () => {
   await it('should calculate standard deviation of array', () => {
     expect(std([0.25, 4.75, 3.05, 6.04, 1.01, 2.02, 5.03])).toBe(2.1879050645374383)
   })
+  await it('should return 0 for standard deviation of empty or single-element array', () => {
+    expect(std([])).toBe(0)
+    expect(std([42])).toBe(0)
+  })
+  await it('should throw TypeError for non-array input to std', () => {
+    expect(() => std(null as unknown as number[])).toThrow(TypeError)
+    expect(() => std(undefined as unknown as number[])).toThrow(TypeError)
+  })
+  await it('should throw TypeError for non-array input to percentile', () => {
+    expect(() => percentile(null as unknown as number[], 50)).toThrow(TypeError)
+  })
+  await it('should throw RangeError for out-of-range percentile', () => {
+    expect(() => percentile([1, 2, 3], -1)).toThrow(RangeError)
+    expect(() => percentile([1, 2, 3], 101)).toThrow(RangeError)
+  })
+  await it('should accept pre-computed average parameter', () => {
+    const data = [0.25, 4.75, 3.05, 6.04, 1.01, 2.02, 5.03]
+    const avg = average(data)
+    expect(std(data, avg)).toBe(std(data))
+  })
 })
index 57d5109a0e8b7b71c0a74e5b66af4de1ff975f88..38d0c691e0b727e7b495f40708522e911be1b0d4 100644 (file)
@@ -86,35 +86,6 @@ await describe('Utils', async () => {
   })
 
   await it('should sleep for specified milliseconds using timer mock', async t => {
-    /**
-     * Timer mock pattern for testing asynchronous timer-based operations.
-     * Uses Node.js test module's built-in timer mocking API.
-     * @example
-     * // Enable timer mocking with setTimeout API
-     * t.mock.timers.enable({ apis: ['setTimeout'] })
-     *
-     * try {
-     *   const delay = 10
-     *   const sleepPromise = sleep(delay)
-     *   // Advance mocked timers by specified milliseconds
-     *   t.mock.timers.tick(delay)
-     *   const timeout = await sleepPromise
-     *   expect(timeout).toBeDefined()
-     * } finally {
-     *   // Always reset timers after test to prevent side effects
-     *   t.mock.timers.reset()
-     * }
-     * @description
-     * This pattern demonstrates:
-     * - `t.mock.timers.enable()`: Activates timer mocking for specified APIs (e.g., setTimeout)
-     * - `t.mock.timers.tick()`: Advances the mocked internal clock by the given milliseconds
-     * - `t.mock.timers.reset()`: Clears mocked timers; use in finally block to ensure cleanup
-     *
-     * Useful for testing:
-     * - Timeout and interval-based logic
-     * - Async functions that depend on timing
-     * - Avoiding slow tests caused by actual delays
-     */
     await withMockTimers(t, ['setTimeout'], async () => {
       const delay = 10
       const sleepPromise = sleep(delay)