From e22c6286e07db5ce384d8a94efd2ba51495188eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:25:11 +0000 Subject: [PATCH 1/6] Initial plan From f5447f6bdc3668d3e063f9bdeaa36fa352f9c974 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:30:06 +0000 Subject: [PATCH 2/6] Add unit tests with node:test framework Co-authored-by: taylortom <1059083+taylortom@users.noreply.github.com> --- .github/workflows/tests.yml | 13 +++ .gitignore | 3 + package.json | 3 + tests/AdaptError.spec.js | 163 +++++++++++++++++++++++++++++++++ tests/ErrorsModule.spec.js | 175 ++++++++++++++++++++++++++++++++++++ tests/data/test-errors.json | 21 +++++ 6 files changed, 378 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 tests/AdaptError.spec.js create mode 100644 tests/ErrorsModule.spec.js create mode 100644 tests/data/test-errors.json diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..550e58c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,13 @@ +name: Tests +on: push +jobs: + default: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: actions/setup-node@master + with: + node-version: 'lts/*' + cache: 'npm' + - run: npm ci + - run: npm test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c45938 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +*.log +.DS_Store diff --git a/package.json b/package.json index 72fb6d2..21a1695 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "type": "module", "main": "index.js", "repository": "github:adapt-security/adapt-authoring-errors", + "scripts": { + "test": "node --test 'tests/**/*.spec.js'" + }, "devDependencies": { "@semantic-release/git": "^10.0.1", "conventional-changelog-eslint": "^6.0.0", diff --git a/tests/AdaptError.spec.js b/tests/AdaptError.spec.js new file mode 100644 index 0000000..8f121fb --- /dev/null +++ b/tests/AdaptError.spec.js @@ -0,0 +1,163 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import AdaptError from '../lib/AdaptError.js' + +describe('AdaptError', () => { + describe('constructor', () => { + it('should create an error with code only', () => { + const error = new AdaptError('TEST_ERROR') + assert.equal(error.code, 'TEST_ERROR') + assert.equal(error.statusCode, 500) + assert.deepEqual(error.meta, {}) + assert.ok(error instanceof Error) + assert.ok(error instanceof AdaptError) + }) + + it('should create an error with code and statusCode', () => { + const error = new AdaptError('NOT_FOUND', 404) + assert.equal(error.code, 'NOT_FOUND') + assert.equal(error.statusCode, 404) + assert.deepEqual(error.meta, {}) + }) + + it('should create an error with code, statusCode, and metadata', () => { + const metadata = { description: 'Test error', data: { id: '123' } } + const error = new AdaptError('CUSTOM_ERROR', 400, metadata) + assert.equal(error.code, 'CUSTOM_ERROR') + assert.equal(error.statusCode, 400) + assert.deepEqual(error.meta, metadata) + }) + + it('should use default statusCode of 500 when not provided', () => { + const error = new AdaptError('SERVER_ERROR') + assert.equal(error.statusCode, 500) + }) + + it('should use empty object as default metadata when not provided', () => { + const error = new AdaptError('TEST_ERROR', 400) + assert.deepEqual(error.meta, {}) + }) + + it('should set the message property to the error code', () => { + const error = new AdaptError('MY_ERROR') + assert.equal(error.message, 'MY_ERROR') + }) + }) + + describe('#setData()', () => { + it('should set data on the error', () => { + const error = new AdaptError('TEST_ERROR') + const data = { userId: '123', action: 'delete' } + error.setData(data) + assert.deepEqual(error.data, data) + }) + + it('should return the error instance for chaining', () => { + const error = new AdaptError('TEST_ERROR') + const returnValue = error.setData({ test: 'value' }) + assert.equal(returnValue, error) + }) + + it('should allow method chaining', () => { + const error = new AdaptError('TEST_ERROR') + const data = { key: 'value' } + const result = error.setData(data) + assert.equal(result.data, data) + assert.ok(result instanceof AdaptError) + }) + + it('should overwrite existing data', () => { + const error = new AdaptError('TEST_ERROR') + error.setData({ first: 'data' }) + error.setData({ second: 'data' }) + assert.deepEqual(error.data, { second: 'data' }) + }) + }) + + describe('#toString()', () => { + it('should return formatted string without data', () => { + const error = new AdaptError('TEST_ERROR') + const result = error.toString() + assert.equal(result, 'AdaptError: TEST_ERROR ') + }) + + it('should return formatted string with data', () => { + const error = new AdaptError('TEST_ERROR') + error.setData({ userId: '123' }) + const result = error.toString() + assert.equal(result, 'AdaptError: TEST_ERROR {"userId":"123"}') + }) + + it('should include class name in output', () => { + const error = new AdaptError('MY_ERROR') + const result = error.toString() + assert.ok(result.startsWith('AdaptError:')) + }) + + it('should handle complex data objects', () => { + const error = new AdaptError('COMPLEX_ERROR') + const complexData = { nested: { key: 'value' }, array: [1, 2, 3] } + error.setData(complexData) + const result = error.toString() + assert.ok(result.includes(JSON.stringify(complexData))) + }) + }) + + describe('Error properties', () => { + it('should have code property', () => { + const error = new AdaptError('TEST_CODE') + assert.equal(typeof error.code, 'string') + assert.equal(error.code, 'TEST_CODE') + }) + + it('should have statusCode property', () => { + const error = new AdaptError('TEST_ERROR', 404) + assert.equal(typeof error.statusCode, 'number') + assert.equal(error.statusCode, 404) + }) + + it('should have meta property', () => { + const meta = { description: 'Test' } + const error = new AdaptError('TEST_ERROR', 500, meta) + assert.equal(typeof error.meta, 'object') + assert.deepEqual(error.meta, meta) + }) + + it('should have data property after setData is called', () => { + const error = new AdaptError('TEST_ERROR') + error.setData({ test: 'data' }) + assert.equal(typeof error.data, 'object') + assert.deepEqual(error.data, { test: 'data' }) + }) + }) + + describe('Edge cases', () => { + it('should handle empty string as error code', () => { + const error = new AdaptError('') + assert.equal(error.code, '') + assert.equal(error.message, '') + }) + + it('should handle null metadata', () => { + const error = new AdaptError('TEST_ERROR', 500, null) + assert.equal(error.meta, null) + }) + + it('should handle zero statusCode', () => { + const error = new AdaptError('TEST_ERROR', 0) + assert.equal(error.statusCode, 0) + }) + + it('should handle setData with null', () => { + const error = new AdaptError('TEST_ERROR') + error.setData(null) + assert.equal(error.data, null) + }) + + it('should handle setData with undefined', () => { + const error = new AdaptError('TEST_ERROR') + error.setData(undefined) + assert.equal(error.data, undefined) + }) + }) +}) diff --git a/tests/ErrorsModule.spec.js b/tests/ErrorsModule.spec.js new file mode 100644 index 0000000..261bd60 --- /dev/null +++ b/tests/ErrorsModule.spec.js @@ -0,0 +1,175 @@ +import { describe, it, beforeEach, afterEach } from 'node:test' +import assert from 'node:assert/strict' +import AdaptError from '../lib/AdaptError.js' +import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs' +import { join, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +// Note: ErrorsModule depends on adapt-authoring-core which is not available, +// so we test the core logic that can be tested independently +describe('ErrorsModule', () => { + let tempDir + + beforeEach(() => { + // Create a temporary directory for test errors + tempDir = join(__dirname, 'temp-test-errors') + mkdirSync(tempDir, { recursive: true }) + mkdirSync(join(tempDir, 'errors'), { recursive: true }) + }) + + afterEach(() => { + // Cleanup temp directory + if (tempDir) { + rmSync(tempDir, { recursive: true, force: true }) + } + }) + + describe('Error definition structure', () => { + it('should validate error definition format', () => { + // Test that our error definitions follow the expected structure + const errorDef = { + description: 'Test error description', + statusCode: 404, + data: { + id: 'Item identifier' + } + } + + assert.ok(errorDef.description) + assert.equal(typeof errorDef.description, 'string') + assert.ok(errorDef.statusCode) + assert.equal(typeof errorDef.statusCode, 'number') + if (errorDef.data) { + assert.equal(typeof errorDef.data, 'object') + } + }) + + it('should create AdaptError from definition', () => { + const errorDef = { + description: 'Test error', + statusCode: 500, + data: { + field: 'Test field' + } + } + + const metadata = { description: errorDef.description } + if (errorDef.data) { + metadata.data = errorDef.data + } + + const error = new AdaptError('TEST_CODE', errorDef.statusCode, metadata) + + assert.ok(error instanceof AdaptError) + assert.equal(error.code, 'TEST_CODE') + assert.equal(error.statusCode, 500) + assert.equal(error.meta.description, 'Test error') + assert.deepEqual(error.meta.data, { field: 'Test field' }) + }) + + it('should handle error definition without data field', () => { + const errorDef = { + description: 'Simple error', + statusCode: 400 + } + + const metadata = { description: errorDef.description } + const error = new AdaptError('SIMPLE_ERROR', errorDef.statusCode, metadata) + + assert.ok(error.meta.description) + assert.equal(error.meta.data, undefined) + }) + }) + + describe('Error JSON file format', () => { + it('should parse valid error JSON file', () => { + const errorDefs = { + ERROR_ONE: { + description: 'First error', + statusCode: 400 + }, + ERROR_TWO: { + description: 'Second error', + statusCode: 404, + data: { + id: 'Identifier' + } + } + } + + writeFileSync( + join(tempDir, 'errors', 'test.json'), + JSON.stringify(errorDefs) + ) + + const content = JSON.parse( + readFileSync(join(tempDir, 'errors', 'test.json'), 'utf8') + ) + + assert.ok(content.ERROR_ONE) + assert.ok(content.ERROR_TWO) + assert.equal(content.ERROR_ONE.statusCode, 400) + assert.equal(content.ERROR_TWO.statusCode, 404) + }) + + it('should validate error codes are uppercase with underscores', () => { + const validCodes = ['TEST_ERROR', 'NOT_FOUND', 'SERVER_ERROR', 'MY_CUSTOM_ERROR'] + + validCodes.forEach(code => { + assert.ok(/^[A-Z_]+$/.test(code), `${code} should be uppercase with underscores`) + }) + }) + }) + + describe('Error definition examples', () => { + it('should demonstrate typical error patterns', () => { + // Common HTTP error patterns + const errorPatterns = [ + { code: 'NOT_FOUND', statusCode: 404, description: 'Resource not found' }, + { code: 'UNAUTHORIZED', statusCode: 401, description: 'Authentication required' }, + { code: 'FORBIDDEN', statusCode: 403, description: 'Access denied' }, + { code: 'BAD_REQUEST', statusCode: 400, description: 'Invalid request' }, + { code: 'SERVER_ERROR', statusCode: 500, description: 'Internal server error' } + ] + + errorPatterns.forEach(pattern => { + const error = new AdaptError(pattern.code, pattern.statusCode, { description: pattern.description }) + assert.equal(error.code, pattern.code) + assert.equal(error.statusCode, pattern.statusCode) + assert.equal(error.meta.description, pattern.description) + }) + }) + }) + + describe('Error sorting', () => { + it('should sort error codes alphabetically', () => { + const unsortedCodes = ['ZEBRA_ERROR', 'ALPHA_ERROR', 'MIDDLE_ERROR', 'BETA_ERROR'] + const sortedCodes = [...unsortedCodes].sort() + + assert.deepEqual(sortedCodes, ['ALPHA_ERROR', 'BETA_ERROR', 'MIDDLE_ERROR', 'ZEBRA_ERROR']) + }) + }) + + describe('Error metadata handling', () => { + it('should preserve description in metadata', () => { + const description = 'A detailed error description' + const metadata = { description } + const error = new AdaptError('TEST', 500, metadata) + + assert.equal(error.meta.description, description) + }) + + it('should preserve data schema in metadata', () => { + const data = { + userId: 'User identifier', + action: 'The action being performed' + } + const metadata = { description: 'Test', data } + const error = new AdaptError('TEST', 500, metadata) + + assert.deepEqual(error.meta.data, data) + }) + }) +}) diff --git a/tests/data/test-errors.json b/tests/data/test-errors.json new file mode 100644 index 0000000..4cd6f38 --- /dev/null +++ b/tests/data/test-errors.json @@ -0,0 +1,21 @@ +{ + "TEST_ERROR": { + "description": "A test error", + "statusCode": 500 + }, + "TEST_NOT_FOUND": { + "description": "Test item not found", + "statusCode": 404, + "data": { + "id": "Item identifier" + } + }, + "TEST_VALIDATION_ERROR": { + "description": "Test validation failed", + "statusCode": 400, + "data": { + "field": "The invalid field", + "value": "The invalid value" + } + } +} From 5c07f41e371362611c049123d5120f5a28dd5c7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:31:01 +0000 Subject: [PATCH 3/6] Fix GitHub Actions to use pinned versions Co-authored-by: taylortom <1059083+taylortom@users.noreply.github.com> --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 550e58c..6a4749d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,8 +4,8 @@ jobs: default: runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - uses: actions/setup-node@master + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 'lts/*' cache: 'npm' From cc599b0f24a441f8b7b111d631cc83dea236163a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:31:37 +0000 Subject: [PATCH 4/6] Add comment explaining trailing space in toString test Co-authored-by: taylortom <1059083+taylortom@users.noreply.github.com> --- tests/AdaptError.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/AdaptError.spec.js b/tests/AdaptError.spec.js index 8f121fb..85c6c26 100644 --- a/tests/AdaptError.spec.js +++ b/tests/AdaptError.spec.js @@ -78,6 +78,7 @@ describe('AdaptError', () => { it('should return formatted string without data', () => { const error = new AdaptError('TEST_ERROR') const result = error.toString() + // Note: trailing space after code is part of the current implementation assert.equal(result, 'AdaptError: TEST_ERROR ') }) From 3a8684d7f1645801f49e3a5ba4772567dab46ee6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:33:42 +0000 Subject: [PATCH 5/6] Add explicit permissions to GitHub Actions workflow Co-authored-by: taylortom <1059083+taylortom@users.noreply.github.com> --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6a4749d..8a979af 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,6 +3,8 @@ on: push jobs: default: runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 From d27eaf82d95d91d701d47c89830b38da9e5878ec Mon Sep 17 00:00:00 2001 From: Thomas Taylor Date: Thu, 19 Feb 2026 23:13:59 +0000 Subject: [PATCH 6/6] Update tests --- tests/AdaptError.spec.js | 58 +++++++++ tests/ErrorsModule.spec.js | 253 ++++++++++++++++++++++++++++++++++++- 2 files changed, 309 insertions(+), 2 deletions(-) diff --git a/tests/AdaptError.spec.js b/tests/AdaptError.spec.js index 85c6c26..70c549c 100644 --- a/tests/AdaptError.spec.js +++ b/tests/AdaptError.spec.js @@ -132,6 +132,47 @@ describe('AdaptError', () => { }) }) + describe('Error inheritance', () => { + it('should have a stack trace', () => { + const error = new AdaptError('TEST_ERROR') + assert.equal(typeof error.stack, 'string') + assert.ok(error.stack.length > 0) + }) + + it('should be catchable as a generic Error', () => { + let caught = false + try { + throw new AdaptError('THROWN_ERROR', 400) + } catch (e) { + caught = true + assert.ok(e instanceof Error) + assert.equal(e.code, 'THROWN_ERROR') + assert.equal(e.statusCode, 400) + } + assert.ok(caught) + }) + + it('should inherit name from Error', () => { + const error = new AdaptError('TEST_ERROR') + assert.equal(error.name, 'Error') + }) + }) + + describe('setData and throw pattern', () => { + it('should support throw with chained setData', () => { + const data = { userId: '456' } + let caught + try { + throw new AdaptError('AUTH_ERROR', 401).setData(data) + } catch (e) { + caught = e + } + assert.ok(caught instanceof AdaptError) + assert.deepEqual(caught.data, data) + assert.equal(caught.code, 'AUTH_ERROR') + }) + }) + describe('Edge cases', () => { it('should handle empty string as error code', () => { const error = new AdaptError('') @@ -160,5 +201,22 @@ describe('AdaptError', () => { error.setData(undefined) assert.equal(error.data, undefined) }) + + it('should handle setData with a string value', () => { + const error = new AdaptError('TEST_ERROR') + error.setData('simple string') + assert.equal(error.data, 'simple string') + }) + + it('should handle toString with empty data object', () => { + const error = new AdaptError('TEST_ERROR') + error.setData({}) + assert.equal(error.toString(), 'AdaptError: TEST_ERROR {}') + }) + + it('should not have data property before setData is called', () => { + const error = new AdaptError('TEST_ERROR') + assert.equal(error.data, undefined) + }) }) }) diff --git a/tests/ErrorsModule.spec.js b/tests/ErrorsModule.spec.js index 261bd60..8a75d85 100644 --- a/tests/ErrorsModule.spec.js +++ b/tests/ErrorsModule.spec.js @@ -28,7 +28,6 @@ describe('ErrorsModule', () => { describe('Error definition structure', () => { it('should validate error definition format', () => { - // Test that our error definitions follow the expected structure const errorDef = { description: 'Test error description', statusCode: 404, @@ -121,11 +120,33 @@ describe('ErrorsModule', () => { assert.ok(/^[A-Z_]+$/.test(code), `${code} should be uppercase with underscores`) }) }) + + it('should handle multiple error JSON files', () => { + writeFileSync( + join(tempDir, 'errors', 'set-a.json'), + JSON.stringify({ ERR_A: { description: 'Error A', statusCode: 400 } }) + ) + writeFileSync( + join(tempDir, 'errors', 'set-b.json'), + JSON.stringify({ ERR_B: { description: 'Error B', statusCode: 404 } }) + ) + + const contentA = JSON.parse( + readFileSync(join(tempDir, 'errors', 'set-a.json'), 'utf8') + ) + const contentB = JSON.parse( + readFileSync(join(tempDir, 'errors', 'set-b.json'), 'utf8') + ) + + const merged = { ...contentA, ...contentB } + assert.ok(merged.ERR_A) + assert.ok(merged.ERR_B) + assert.equal(Object.keys(merged).length, 2) + }) }) describe('Error definition examples', () => { it('should demonstrate typical error patterns', () => { - // Common HTTP error patterns const errorPatterns = [ { code: 'NOT_FOUND', statusCode: 404, description: 'Resource not found' }, { code: 'UNAUTHORIZED', statusCode: 401, description: 'Authentication required' }, @@ -172,4 +193,232 @@ describe('ErrorsModule', () => { assert.deepEqual(error.meta.data, data) }) }) + + describe('loadErrors reduction logic', () => { + it('should create getter properties that return AdaptError instances', () => { + const errorDefs = { + TEST_ERR: { description: 'A test', statusCode: 500 } + } + + const result = Object.entries(errorDefs) + .sort() + .reduce((m, [k, { description, statusCode, data }]) => { + return Object.defineProperty(m, k, { + get: () => { + const metadata = { description } + if (data) metadata.data = data + return new AdaptError(k, statusCode, metadata) + }, + enumerable: true + }) + }, {}) + + const error = result.TEST_ERR + assert.ok(error instanceof AdaptError) + assert.equal(error.code, 'TEST_ERR') + assert.equal(error.statusCode, 500) + assert.equal(error.meta.description, 'A test') + }) + + it('should return a new instance on each property access', () => { + const errorDefs = { + MY_ERROR: { description: 'Repeated', statusCode: 400 } + } + + const result = Object.entries(errorDefs) + .sort() + .reduce((m, [k, { description, statusCode, data }]) => { + return Object.defineProperty(m, k, { + get: () => { + const metadata = { description } + if (data) metadata.data = data + return new AdaptError(k, statusCode, metadata) + }, + enumerable: true + }) + }, {}) + + const first = result.MY_ERROR + const second = result.MY_ERROR + assert.notEqual(first, second) + assert.equal(first.code, second.code) + }) + + it('should include data in metadata when defined', () => { + const errorDefs = { + DATA_ERR: { description: 'Has data', statusCode: 404, data: { id: 'Item ID' } } + } + + const result = Object.entries(errorDefs) + .sort() + .reduce((m, [k, { description, statusCode, data }]) => { + return Object.defineProperty(m, k, { + get: () => { + const metadata = { description } + if (data) metadata.data = data + return new AdaptError(k, statusCode, metadata) + }, + enumerable: true + }) + }, {}) + + const error = result.DATA_ERR + assert.deepEqual(error.meta.data, { id: 'Item ID' }) + }) + + it('should omit data from metadata when not defined', () => { + const errorDefs = { + NO_DATA_ERR: { description: 'No data', statusCode: 500 } + } + + const result = Object.entries(errorDefs) + .sort() + .reduce((m, [k, { description, statusCode, data }]) => { + return Object.defineProperty(m, k, { + get: () => { + const metadata = { description } + if (data) metadata.data = data + return new AdaptError(k, statusCode, metadata) + }, + enumerable: true + }) + }, {}) + + const error = result.NO_DATA_ERR + assert.equal(error.meta.data, undefined) + assert.ok(!('data' in error.meta)) + }) + + it('should sort error codes alphabetically in reduced result', () => { + const errorDefs = { + ZEBRA: { description: 'Z', statusCode: 500 }, + ALPHA: { description: 'A', statusCode: 500 }, + MIDDLE: { description: 'M', statusCode: 500 } + } + + const result = Object.entries(errorDefs) + .sort() + .reduce((m, [k, { description, statusCode, data }]) => { + return Object.defineProperty(m, k, { + get: () => { + const metadata = { description } + if (data) metadata.data = data + return new AdaptError(k, statusCode, metadata) + }, + enumerable: true + }) + }, {}) + + const keys = Object.keys(result) + assert.deepEqual(keys, ['ALPHA', 'MIDDLE', 'ZEBRA']) + }) + + it('should make error properties enumerable', () => { + const errorDefs = { + ENUM_ERR: { description: 'Enumerable', statusCode: 500 } + } + + const result = Object.entries(errorDefs) + .sort() + .reduce((m, [k, { description, statusCode, data }]) => { + return Object.defineProperty(m, k, { + get: () => { + const metadata = { description } + if (data) metadata.data = data + return new AdaptError(k, statusCode, metadata) + }, + enumerable: true + }) + }, {}) + + const descriptor = Object.getOwnPropertyDescriptor(result, 'ENUM_ERR') + assert.equal(descriptor.enumerable, true) + assert.equal(typeof descriptor.get, 'function') + }) + }) + + describe('Shipped error definitions', () => { + it('should have valid node-core error definitions', () => { + const nodeCoreErrors = JSON.parse( + readFileSync(join(__dirname, '..', 'errors', 'node-core.json'), 'utf8') + ) + + const expectedCodes = ['EACCES', 'EADDRINUSE', 'ECONNREFUSED', 'EEXIST', 'ENOENT', 'ENOTEMPTY', 'MODULE_NOT_FOUND'] + expectedCodes.forEach(code => { + assert.ok(nodeCoreErrors[code], `Missing expected error code: ${code}`) + assert.equal(typeof nodeCoreErrors[code].description, 'string') + assert.equal(typeof nodeCoreErrors[code].statusCode, 'number') + }) + }) + + it('should have valid adapt error definitions', () => { + const adaptErrors = JSON.parse( + readFileSync(join(__dirname, '..', 'errors', 'adapt-errors.json'), 'utf8') + ) + + const expectedCodes = ['FUNC_NOT_OVERRIDDEN', 'FUNC_DISABLED', 'SERVER_ERROR', 'INVALID_PARAMS', 'NOT_FOUND'] + expectedCodes.forEach(code => { + assert.ok(adaptErrors[code], `Missing expected error code: ${code}`) + assert.equal(typeof adaptErrors[code].description, 'string') + assert.equal(typeof adaptErrors[code].statusCode, 'number') + }) + }) + + it('should have valid test error definitions', () => { + const testErrors = JSON.parse( + readFileSync(join(__dirname, 'data', 'test-errors.json'), 'utf8') + ) + + assert.ok(testErrors.TEST_ERROR) + assert.ok(testErrors.TEST_NOT_FOUND) + assert.ok(testErrors.TEST_VALIDATION_ERROR) + assert.equal(testErrors.TEST_NOT_FOUND.statusCode, 404) + assert.equal(testErrors.TEST_VALIDATION_ERROR.statusCode, 400) + }) + + it('should use uppercase with underscores for all error codes', () => { + const files = ['node-core.json', 'adapt-errors.json'] + files.forEach(file => { + const errors = JSON.parse( + readFileSync(join(__dirname, '..', 'errors', file), 'utf8') + ) + Object.keys(errors).forEach(code => { + assert.ok(/^[A-Z][A-Z0-9_]*$/.test(code), `Invalid error code format: ${code} in ${file}`) + }) + }) + }) + }) + + describe('Duplicate error detection', () => { + it('should detect duplicate error codes across files', () => { + writeFileSync( + join(tempDir, 'errors', 'first.json'), + JSON.stringify({ DUPLICATE: { description: 'First', statusCode: 500 } }) + ) + writeFileSync( + join(tempDir, 'errors', 'second.json'), + JSON.stringify({ DUPLICATE: { description: 'Second', statusCode: 400 } }) + ) + + const allDefs = {} + const duplicates = [] + + const files = ['first.json', 'second.json'] + files.forEach(file => { + const contents = JSON.parse( + readFileSync(join(tempDir, 'errors', file), 'utf8') + ) + Object.entries(contents).forEach(([k, v]) => { + if (allDefs[k]) { + duplicates.push(k) + } else { + allDefs[k] = v + } + }) + }) + + assert.equal(duplicates.length, 1) + assert.equal(duplicates[0], 'DUPLICATE') + }) + }) })