From 9eea9dbe415284116810ad4f23b5e3aa7693b81f Mon Sep 17 00:00:00 2001 From: Raman Marozau Date: Tue, 14 Apr 2026 22:29:17 +0200 Subject: [PATCH 1/4] chore(examples): Add ESLint config and fix linting violations - Add comprehensive .eslintrc.json for examples directory with Node.js and ES2020 support - Configure ESLint overrides for Electron renderer and Next.js environments - Remove unused formatPlain import from CLI tool count command - Remove unused ORDER_STATUSES import from orders service - Rename unused passwordHash variables to _hash in users service to follow convention - Add ESLint disable comment for intentional unused next parameter in error handler - Expand CI matrix to include Node.js 24.x for broader version coverage --- .github/workflows/ci.yml | 2 +- examples/.eslintrc.json | 52 +++++++++++++++++++ examples/04-cli-tool/src/commands/count.js | 2 +- .../src/domains/orders/orders.service.js | 2 +- .../src/domains/users/users.service.js | 4 +- .../src/shared/middleware/error-handler.js | 3 +- 6 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 examples/.eslintrc.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0db5afd..cb69394 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - node-version: [18.x, 22.x] + node-version: [18.x, 22.x, 24.x] os: [ubuntu-latest, macos-latest] fail-fast: false diff --git a/examples/.eslintrc.json b/examples/.eslintrc.json new file mode 100644 index 0000000..d09ab1f --- /dev/null +++ b/examples/.eslintrc.json @@ -0,0 +1,52 @@ +{ + "env": { + "node": true, + "commonjs": true, + "es2020": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2020, + "sourceType": "module" + }, + "rules": { + "no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_" + } + ] + }, + "overrides": [ + { + "files": [ + "07-electron-desktop-app/src/renderer/**/*.js" + ], + "env": { + "browser": true, + "node": false + } + }, + { + "files": [ + "09-next-webapp/**/*.js", + "09-next-webapp/**/*.jsx" + ], + "parserOptions": { + "ecmaFeatures": { + "jsx": true + } + }, + "rules": { + "no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_|^[A-Z]" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/examples/04-cli-tool/src/commands/count.js b/examples/04-cli-tool/src/commands/count.js index d4c4ab8..42caf23 100644 --- a/examples/04-cli-tool/src/commands/count.js +++ b/examples/04-cli-tool/src/commands/count.js @@ -1,5 +1,5 @@ const fs = require('fs'); -const { formatTable, formatPlain } = require('../utils/format'); +const { formatTable } = require('../utils/format'); module.exports = { command: 'count ', diff --git a/examples/10-enterprise-platform/src/domains/orders/orders.service.js b/examples/10-enterprise-platform/src/domains/orders/orders.service.js index 5bc8daa..21e6f76 100644 --- a/examples/10-enterprise-platform/src/domains/orders/orders.service.js +++ b/examples/10-enterprise-platform/src/domains/orders/orders.service.js @@ -1,4 +1,4 @@ -const { createOrder, ORDER_STATUSES, STATUS_TRANSITIONS } = require('./orders.model'); +const { createOrder, STATUS_TRANSITIONS } = require('./orders.model'); const productsService = require('../products/products.service'); const orders = new Map(); diff --git a/examples/10-enterprise-platform/src/domains/users/users.service.js b/examples/10-enterprise-platform/src/domains/users/users.service.js index 6499e1a..6845e90 100644 --- a/examples/10-enterprise-platform/src/domains/users/users.service.js +++ b/examples/10-enterprise-platform/src/domains/users/users.service.js @@ -40,7 +40,7 @@ function register(data) { users.set(user.id, user); - const { passwordHash, ...safeUser } = user; + const { passwordHash: _hash, ...safeUser } = user; return safeUser; } @@ -58,7 +58,7 @@ function getProfile(id) { if (!user) { throw Object.assign(new Error('User not found'), { code: 'NOT_FOUND' }); } - const { passwordHash, ...safeUser } = user; + const { passwordHash: _hash, ...safeUser } = user; return safeUser; } diff --git a/examples/10-enterprise-platform/src/shared/middleware/error-handler.js b/examples/10-enterprise-platform/src/shared/middleware/error-handler.js index d02b2f7..47ae6b4 100644 --- a/examples/10-enterprise-platform/src/shared/middleware/error-handler.js +++ b/examples/10-enterprise-platform/src/shared/middleware/error-handler.js @@ -8,7 +8,8 @@ const ERROR_STATUS_MAP = { INVALID_TRANSITION: 422 }; -function errorHandler(err, req, res, _next) { +// eslint-disable-next-line no-unused-vars +function errorHandler(err, req, res, next) { const code = err.code || 'INTERNAL_ERROR'; const status = ERROR_STATUS_MAP[code] || 500; const message = status === 500 ? 'Internal server error' : err.message; From e4938ea126de2e2eb82f5bb983fb635493043079 Mon Sep 17 00:00:00 2001 From: Raman Marozau Date: Tue, 14 Apr 2026 22:39:26 +0200 Subject: [PATCH 2/4] test(lock.manager): Fix property test timing edge cases - Increase minimum lockTimeoutMs arbitrary from 1 to 100ms to avoid race conditions with very small timeout values - Replace lockTimeoutMs - delta calculation with Math.min(delta, Math.floor(lockTimeoutMs / 2)) to guarantee lock age stays well within timeout window - Add clarifying comments explaining why lockTimeoutMs / 2 is used to prevent Date.now() drift between test setup and isTimedOut() check from causing flaky failures - Apply timing fix consistently across both non-stale detection test cases (standard and CI environments) --- __tests__/properties/lock.manager.property.test.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/__tests__/properties/lock.manager.property.test.ts b/__tests__/properties/lock.manager.property.test.ts index 3dbd1f7..11ac125 100644 --- a/__tests__/properties/lock.manager.property.test.ts +++ b/__tests__/properties/lock.manager.property.test.ts @@ -112,7 +112,7 @@ const createLockManager = lockManagerMod.exports.createLockManager as typeof import('../../src/core/lock.manager').createLockManager; /** Arbitrary for lockTimeoutMs (positive integer, reasonable range) */ -const arbLockTimeoutMs = fc.integer({ min: 1, max: 600_000 }); +const arbLockTimeoutMs = fc.integer({ min: 100, max: 600_000 }); /** Arbitrary for a positive time delta in ms */ const arbPositiveDelta = fc.integer({ min: 50, max: 300_000 }); @@ -229,8 +229,11 @@ describe('Feature: operational-hardening, Property 8: Stale Detection по timeo arbPositiveDelta, (pid, operationId, command, hostname, lockTimeoutMs, delta) => { // createdAt is recent enough that timeout is NOT exceeded + // Use lockTimeoutMs / 2 as the age to guarantee the lock is well within the timeout + // window, avoiding race conditions with very small lockTimeoutMs values. const now = Date.now(); - const createdAtMs = now - Math.max(0, lockTimeoutMs - delta); // guarantees now - createdAt < lockTimeoutMs + const age = Math.min(delta, Math.floor(lockTimeoutMs / 2)); + const createdAtMs = now - age; const createdAt = new Date(createdAtMs).toISOString(); const lockData: LockData = { pid, operationId, command, createdAt, hostname, ci: false }; @@ -350,8 +353,12 @@ describe('Feature: operational-hardening, Property 8: Stale Detection по timeo arbPositiveDelta, (pid, operationId, command, hostname, lockTimeoutMs, delta) => { // Non-stale case in CI — timeout NOT exceeded + // Use lockTimeoutMs / 2 as the age to guarantee the lock is well within the timeout + // window, avoiding race conditions with very small lockTimeoutMs values where + // Date.now() drift between test setup and isTimedOut() check could push age past timeout. const now = Date.now(); - const createdAtMs = now - Math.max(0, lockTimeoutMs - delta); + const age = Math.min(delta, Math.floor(lockTimeoutMs / 2)); + const createdAtMs = now - age; const createdAt = new Date(createdAtMs).toISOString(); const lockData: LockData = { pid, operationId, command, createdAt, hostname, ci: true }; From fea2126e836df52d11ff86aa20e14870093cd73e Mon Sep 17 00:00:00 2001 From: Raman Marozau Date: Tue, 14 Apr 2026 22:47:36 +0200 Subject: [PATCH 3/4] test(e2e): Add stdout flush guard for semver platform tests - Add assertion to verify stdout is not empty before parsing JSON - Guard against child_process stdout not being fully flushed on CI - Prevents parsing errors on Node 18 + macOS ARM64 runners - Ensures test reliability across different CI environments --- __tests__/e2e/platforms-semver.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/__tests__/e2e/platforms-semver.test.ts b/__tests__/e2e/platforms-semver.test.ts index 89ab79e..37d8701 100644 --- a/__tests__/e2e/platforms-semver.test.ts +++ b/__tests__/e2e/platforms-semver.test.ts @@ -120,6 +120,9 @@ describe('E2E: SCM platform matrix', () => { ); expect(exitCode).toBe(0); + // On some CI runners (Node 18 + macOS ARM64), child_process stdout + // may not be fully flushed before exit. Guard against empty output. + expect(stdout.trim().length).toBeGreaterThan(0); const parsed = JSON.parse(stdout.trim()); expect(parsed.dryRun).toBe(true); expect(parsed.pullRequest).toBeDefined(); From 9c8accc4d35d802f9f9ee4584a486f3ecf0218b0 Mon Sep 17 00:00:00 2001 From: Raman Marozau Date: Tue, 14 Apr 2026 22:51:06 +0200 Subject: [PATCH 4/4] test(config.merger): Add scalar replacement guard for deep merge validation - Add early exit condition when B contains scalar value at merge key - Skip subtree traversal when B's scalar replaces A's entire object structure - Prevent false negatives in property test by handling scalar-over-object replacement - Ensures merge priority rules are correctly validated during deep object merging --- .../properties/config/config.merger.property.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/__tests__/properties/config/config.merger.property.test.ts b/__tests__/properties/config/config.merger.property.test.ts index 021ffc0..905566f 100644 --- a/__tests__/properties/config/config.merger.property.test.ts +++ b/__tests__/properties/config/config.merger.property.test.ts @@ -151,6 +151,15 @@ describe('Property 3: Priority and deep merge', () => { if (val === undefined) continue; const currentPath = [...path, key]; + // If B has a scalar at this key, it replaces A's entire subtree — skip + if (bObj && key in bObj && bObj[key] !== undefined) { + const bVal = bObj[key]; + if (typeof bVal !== 'object' || bVal === null || Array.isArray(bVal)) { + // B's scalar wins over A's value (object or scalar) — nothing to check + continue; + } + } + if (typeof val === 'object' && val !== null && !Array.isArray(val)) { const bSub = bObj && typeof bObj[key] === 'object' && bObj[key] !== null ? bObj[key] : {}; const mSub = mergedObj && typeof mergedObj[key] === 'object' ? mergedObj[key] : {};