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/__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(); 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] : {}; 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 }; 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;