]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/commit
feat(config): add Zod-based simulator configuration syntax validation (#1874)
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Wed, 27 May 2026 21:30:34 +0000 (23:30 +0200)
committerGitHub <noreply@github.com>
Wed, 27 May 2026 21:30:34 +0000 (23:30 +0200)
commitc1c823329d8c7241811abc14ce59703ec5197e31
tree5d861f89075cf93fd641817304807d7a3a6c1e9b
parent2be3c0f989232db3328719d334d01ac04ce1179a
feat(config): add Zod-based simulator configuration syntax validation (#1874)

* feat(config): scaffold simulator configuration schema and validator skeleton

- Add ConfigurationMigrations.ts: CURRENT_CONFIGURATION_SCHEMA_VERSION=1,
  coerceConfigurationVersion, applyConfigurationMigration, migrateV0ToV1
  with DEPRECATED_KEY_REMAPPINGS (~25 legacy keys)
- Add ConfigurationSchema.ts: strict Zod v4 schema for all config sections
  (log, worker, performanceStorage, uiServer, stationTemplateUrls) with
  deprecated keys as .optional()
- Add ConfigurationValidation.ts: validateConfiguration pipeline
  (guard→clone→coerce→migrate→safeParse→transform) + ConfigurationValidationError
  extends BaseError with structured fieldErrors
- Add $schemaVersion: 1 to config-template.json
- Add ConfigurationFixtures.ts test helpers
- Add ConfigurationSchema.test.ts (63 tests), ConfigurationMigrations.test.ts (44 tests)
- Wire validateConfiguration into Configuration.getConfigurationData() with
  hard-throw at boot (console.error+chalk+process.exit(1))
- Hot-reload snapshot rollback: pre-clear snapshot, restore on failure
- Replace ConfigurationData interface with z.infer<typeof ConfigurationSchema>
- Migrate all consumers atomically (SimulatorState, Configuration, types barrel)
- Delete checkWorkerProcessType and checkWorkerElementsPerWorker (subsumed by schema)
- Delete ConfigurationMigration.ts (logic moved to ConfigurationMigrations.ts)
- Add ConfigurationValidation.test.ts (39 tests), Configuration-hot-reload.test.ts
- Add ConfigurationValidation-perf.test.ts (p99 < 50ms budget)
- Add error message snapshot test
- Update README.md with $schemaVersion documentation

* feat(config): remove legacy ConfigurationMigration.ts and stale call sites

- Delete src/utils/ConfigurationMigration.ts (deprecated-key logic now
  owned by ConfigurationMigrations.ts v0→v1 migration step)
- Remove checkDeprecatedConfigurationKeys import and call site from
  Configuration.getStationTemplateUrls() — validation pipeline in
  getConfigurationData() already handles deprecated key remapping

* chore(config): silence lint warnings on configuration migrations

- Add JSDoc descriptions for setAtPath() params
- Add JSDoc descriptions and @returns for migrateV0ToV1
- Whitelist 'emerg' (syslog level) and 'REMAPPINGS' in cspell dictionary

* refactor(config): rebuild configuration validation pipeline

Address audit findings on PR #1874:

- Extract deprecated-key sweep into pure remapDeprecatedKeys() that
  reports warnings and field errors via return value instead of side
  effects. Runs unconditionally regardless of $schemaVersion so v1
  configs still containing deprecated keys never silently drop user
  values (B3).
- Replace silent-drop setAtPath with collision- and intermediate-aware
  variant: equal-value writes are idempotent no-ops, unequal values
  produce a typed field error (B4), non-object intermediates are
  reported and stop traversal instead of overwriting user data (N7).
- Type DEPRECATED_KEY_REMAPPINGS as Record<string, string | null>;
  null marks deprecated keys with no canonical destination, replacing
  the autoReconnectMaxRetries self-mapping that silently dropped the
  user value (B2). Add 'worker.elementStartDelay' as a dotted source
  key so nested deprecations live in the same single source of truth.
- Refactor ConfigurationValidationError: primary constructor takes
  FieldError[] + a context with explicit phase ('migration' | 'schema');
  static fromZodError() factory wraps Zod failures. Error messages now
  carry the phase in their tag for clearer diagnostics.
- Switch deprecation-warning channel from logger.warn to console.warn
  to break a re-entrant boot path where the Logger proxy lazily
  resolved Configuration.getConfigurationSection('log'), recursing into
  the validation pipeline (B1). transformConfiguration is now a pure
  deep clone — its previous warning loop moved upstream to the sweep.
- Delete dead post-migration remapping in Configuration.ts:
  deprecatedLogKeyMap, deprecatedWorkerKeyMap, the
  delete configurationData.workerPoolStrategy mutation, the cast hiding
  the legacy 'supervisionURLs' read, and the worker.elementStartDelay
  fallback (B6). All deprecation handling now flows through the
  migration's single source of truth.
- Make hot-reload reload loop async end-to-end. Awaits the change
  callback inside the same lock so subsequent reloads cannot interleave
  with an in-flight callback. Adds configurationFileReloadPending so
  events arriving during a reload coalesce into exactly one drain
  reload after the current one completes (N8).
- Polish schema: use z.enum(NativeEnum) directly for ApplicationProtocol
  and ApplicationProtocolVersion to preserve literal-type narrowing on
  UIServerConfiguration (N4); drop the BaseConfigurationSchema alias
  (N5).

* test(config): cover migration edge cases and hot-reload rollback

Tests aligned with the rebuilt validation pipeline:

- Split migration tests into a remapDeprecatedKeys block (per-key sweep,
  null-destination removal for autoReconnectMaxRetries, equal-value
  collision idempotency, unequal-value collision field error, non-object
  intermediate field error, nested worker.elementStartDelay) and a lean
  applyConfigurationMigration block (version-bump only, immutability).
- Replace the empty self-mapping branch with explicit hasOwnProperty
  removal assertions to defeat the previously vacuous test.
- ConfigurationValidation tests: switch deprecation-warning channel
  spies from logger.warn to console.warn (B1 regression also asserts
  logger.warn is never called from the pipeline), tolerate
  schema-incompatible remap targets so the warning fires before the
  downstream throw, add B3 sweep / B6 SSOT / future-version pipeline /
  ConfigurationValidationError shape (FieldError[] + phase, fromZodError
  factory, schema-phase aggregation) tests, and update the error-message
  snapshot to include the new [phase] tag.
- transformConfiguration immutability test verifies a fresh validation
  is unaffected by mutating a prior return value.
- ConfigurationSchema tests: strict parity for performanceStorage and
  uiServer.authentication; StationTemplateUrl entry constraints
  (empty file, negative numberOfStations, deprecated numberOfStation,
  unknown key); worker.elementsPerWorker and poolMaxSize/MinSize
  positive-integer constraints; log.statisticsInterval non-negative
  integer constraints; bidirectional schema/DEPRECATED_KEY_REMAPPINGS
  sync meta-test that walks both top-level and nested @deprecated
  describe markers.
- Hot-reload tests rebuilt around the async runReloadLoop: validation
  failure asserts the logged error is a ConfigurationValidationError;
  JSON parse error path asserts configurationData and section cache are
  fully restored (Gap 7); a sentinel watcher survives a failed reload;
  N8 rapid double-save coalesces into exactly one drain reload reflecting
  the latest content; flag reset is exercised on both paths.
- Configuration validation perf test: relative p99 budget (20× median
  with a 1ms floor) plus an absolute 500ms catastrophic ceiling,
  replacing the flaky absolute 50ms threshold (N1).
- Fixtures: new buildInvalidJsonString, buildV1WithDeprecatedKey
  (handles top-level and dotted source keys), and
  buildV0WithDeprecatedKeyCollision builders for the new test cases.

* docs(config): trim verbose JSDocs and harmonize with Template* conventions

Polish pass after PR audit: bring comments and docstrings in line with
the existing TemplateMigrations / TemplateValidation / TemplateSchema
patterns, drop ceremonial AI-generated narration, and fix two precision
defects.

- ConfigurationMigrations.ts: trim getAtPath / setAtPath / remapDeprecatedKeys /
  migrateV0ToV1 / applyConfigurationMigration JSDocs to match Template*
  density; drop the historical "now handled unconditionally upstream"
  paragraph (AGENTS.md "exclude historical evolution").
- ConfigurationValidation.ts: collapse the duplicated pipeline narration
  (the function-level JSDoc already enumerates stages, the inline
  // Stage N — markers were removing them); shorten the
  ConfigurationValidationError class docstring; tighten transformConfiguration
  JSDoc and drop the aspirational "future cross-field invariants" sentence;
  collapse the 5-line re-entrancy comment to a 2-line rationale.
- ConfigurationSchema.ts: fix two precision defects in JSDoc — stale
  module name (ConfigurationMigration → ConfigurationMigrations) and wrong
  subject (numberOfStation is not in the remap table; nothing auto-migrates
  it). Disambiguate WorkerConfigurationSchema description (resourceLimits
  is bridged, not deprecated; elementStartDelay is the deprecated alias).
- Configuration.ts: shrink runReloadLoop and performReload JSDocs to two-
  line summaries matching the surrounding private-method conventions in
  the same file; collapse the 4-line ESLint-disable rationale to a single
  -- suffix on the disable directive.
- Test fixtures and headers: drop internal review codes (B1/B3/B4/B6/N7/N8/
  Gap-7/RG-4) from JSDocs and test names — they had no lookup table and
  would be opaque to future maintainers; trim test-file headers to the
  one-line Template* shape.

* refactor(config): address post-audit findings on validation pipeline

* fix(config): drop incompatible v0 remaps and tighten validation invariants

Addresses 4 PR review findings cross-validated by 2 oracles.

- ConfigurationMigrations: drop `useWorkerPool` (boolean→enum),
  `distributeStationToTenantEqually` and `distributeStationsToTenantsEqually`
  (boolean→enum), and `uiWebSocketServer` (legacy shape→strict object) to
  `null` (warn-and-delete) — same pattern as `workerPoolStrategy` and
  `autoReconnectMaxRetries`. v0 configurations carrying any of these
  legacy keys would otherwise auto-write a value the strict schema
  rejects, killing the simulator at boot. Migration now warns and asks
  the user to set the canonical key explicitly. Fixes the BLOCKING
  finding from the PR review (regression on 'v0 configs remain valid').
- ConfigurationMigrations: remove redundant `out.$schemaVersion = 1`
  from `migrateV0ToV1`. The `applyConfigurationMigration` loop is the
  single owner of version stamping; per-step writes are silently
  overwritten and become misleading once a second migration is added.
- ConfigurationSchema: tighten `StationTemplateUrlSchema.numberOfStations`
  from `.nonnegative()` to `.positive()` — `0` was schema-valid but
  semantically meaningless (a template entry that spawns no station)
  and conflicts with `isValidNumberOfStations()` which rejects `<= 0`.
  Deprecated `numberOfStation` and `provisionedNumberOfStations` keep
  `.nonnegative()` (back-compat / 'none provisioned' is meaningful).
- ConfigurationValidation: import `isEmpty` directly from
  `utils/Utils.js` instead of the `utils/index.js` barrel, which
  re-exports `Configuration` and creates the cycle
  `ConfigurationValidation → utils/index → Configuration → ConfigurationValidation`.
  Mirrors the existing direct-path `BaseError` import in
  `ConfigurationMigrations.ts` for the same TDZ-cycle-avoidance reason.

* docs(config): fix precision defects in schema JSDocs and README

- ConfigurationSchema.ts: correct StorageConfigurationSchema JSDoc —
  'URI' is accepted but not auto-migrated (not in DEPRECATED_KEY_REMAPPINGS);
  narrow ConfigurationSchema JSDoc meta-test scope claim to 'top-level
  and worker.* keys' to match actual test coverage.
- README.md: clarify that deprecated-key remapping is unconditional
  (runs on every load, not only on v0 migration).

* fix(config): keep numberOfStations as .nonnegative() to preserve disabled-entry convention

Revert the .positive() tightening from 95cd26dd: docker/config.json ships
5 stationTemplateUrls entries with `numberOfStations: 0` (used as a
'keep on disk but disabled' convention), and this file is copied to
src/assets/config.json by docker/Dockerfile at image build time. With
.positive(), Docker images built from this branch would fail to start
on validateConfiguration's strict parse → process.exit(1).

Bootstrap loops already tolerate 0 (no-op, no crash). The original
.nonnegative() correctly models the on-disk convention; isValidNumberOfStations()
in UIServerSecurity.ts is for runtime UI add-station requests, a
separate concern from config-file parsing.

* docs(config): trim re-entrancy and clone rationales to one-liners
21 files changed:
README.md
cspell.config.yaml
src/assets/config-template.json
src/charging-station/ConfigurationMigrations.ts [new file with mode: 0644]
src/charging-station/ConfigurationSchema.ts [new file with mode: 0644]
src/charging-station/ConfigurationValidation.ts [new file with mode: 0644]
src/types/ConfigurationData.ts
src/utils/Configuration.ts
src/utils/ConfigurationMigration.ts [deleted file]
src/utils/ConfigurationUtils.ts
src/worker/WorkerUtils.ts
src/worker/index.ts
tests/charging-station/ConfigurationMigrations.test.ts [new file with mode: 0644]
tests/charging-station/ConfigurationSchema.test.ts [new file with mode: 0644]
tests/charging-station/ConfigurationValidation-perf.test.ts [new file with mode: 0644]
tests/charging-station/ConfigurationValidation.test.ts [new file with mode: 0644]
tests/charging-station/helpers/ConfigurationFixtures.ts [new file with mode: 0644]
tests/utils/Configuration-hot-reload.test.ts [new file with mode: 0644]
tests/utils/Configuration.test.ts
tests/utils/ConfigurationUtils.test.ts
tests/worker/WorkerUtils.test.ts