From c9cbecffbc63e03332522ddf7bccc1893effefb0 Mon Sep 17 00:00:00 2001 From: Aditya Raj Date: Wed, 18 Mar 2026 15:35:17 +0530 Subject: [PATCH 1/3] fix: add file-context to generator parse errors --- .../ast/__tests__/generate.test.mjs | 37 +++++++++++++ src/generators/ast/generate.mjs | 31 ++++++----- .../metadata/__tests__/generate.test.mjs | 55 +++++++++++++++++++ src/generators/metadata/generate.mjs | 10 +++- 4 files changed, 119 insertions(+), 14 deletions(-) create mode 100644 src/generators/ast/__tests__/generate.test.mjs create mode 100644 src/generators/metadata/__tests__/generate.test.mjs diff --git a/src/generators/ast/__tests__/generate.test.mjs b/src/generators/ast/__tests__/generate.test.mjs new file mode 100644 index 00000000..ece7683b --- /dev/null +++ b/src/generators/ast/__tests__/generate.test.mjs @@ -0,0 +1,37 @@ +import assert from 'node:assert/strict'; +import { describe, it, mock } from 'node:test'; + +// Mock dependencies +const mockReadFile = mock.fn(); +mock.module('node:fs/promises', { + namedExports: { readFile: mockReadFile }, +}); + +// Mock other internal modules as needed +// From src/generators/ast/__tests__/ to src/utils/configuration/index.mjs is ../../../utils/configuration/index.mjs +mock.module('../../../utils/configuration/index.mjs', { + defaultExport: () => ({ ast: { input: 'docs/*.md' } }), +}); + +const { processChunk } = await import('../generate.mjs'); + +describe('ast/generate.mjs error handling', () => { + it('should wrap readFile errors with filename, original message, and cause', async () => { + const error = new Error('FS_ERROR'); + mockReadFile.mock.mockImplementation(async () => { + throw error; + }); + + const inputSlice = ['test.md']; + const itemIndices = [0]; + + await assert.rejects( + async () => await processChunk(inputSlice, itemIndices), + { + name: 'Error', + message: 'Failed to process test.md: FS_ERROR', + cause: error, + } + ); + }); +}); diff --git a/src/generators/ast/generate.mjs b/src/generators/ast/generate.mjs index 65bcdb66..082528f5 100644 --- a/src/generators/ast/generate.mjs +++ b/src/generators/ast/generate.mjs @@ -25,19 +25,24 @@ export async function processChunk(inputSlice, itemIndices) { const results = []; for (const path of filePaths) { - const content = await readFile(path, 'utf-8'); - const vfile = new VFile({ - path, - value: content.replace( - QUERIES.stabilityIndexPrefix, - match => `[${match}](${STABILITY_INDEX_URL})` - ), - }); - - results.push({ - tree: remarkProcessor.parse(vfile), - file: { stem: vfile.stem, basename: vfile.basename }, - }); + try { + const content = await readFile(path, 'utf-8'); + const vfile = new VFile({ + path, + value: content.replace( + QUERIES.stabilityIndexPrefix, + match => `[${match}](${STABILITY_INDEX_URL})` + ), + }); + + results.push({ + tree: remarkProcessor.parse(vfile), + file: { stem: vfile.stem, basename: vfile.basename, path }, + }); + } catch (err) { + const message = `Failed to process ${path}: ${err.message ?? err}`; + throw new Error(message, { cause: err }); + } } return results; diff --git a/src/generators/metadata/__tests__/generate.test.mjs b/src/generators/metadata/__tests__/generate.test.mjs new file mode 100644 index 00000000..d157af7a --- /dev/null +++ b/src/generators/metadata/__tests__/generate.test.mjs @@ -0,0 +1,55 @@ +import assert from 'node:assert/strict'; +import { describe, it, mock } from 'node:test'; + +// Mock dependencies +const mockParseApiDoc = mock.fn(); +mock.module('../utils/parse.mjs', { + namedExports: { parseApiDoc: mockParseApiDoc }, +}); + +// Mock configuration and URL utils +// From src/generators/metadata/__tests__/ to src/utils/ is ../../../utils/ +mock.module('../../../utils/configuration/index.mjs', { + defaultExport: () => ({ metadata: { typeMap: 'typeMap.json' } }), +}); +mock.module('../../../utils/url.mjs', { + namedExports: { importFromURL: async () => ({}) }, +}); + +const { processChunk } = await import('../generate.mjs'); + +describe('metadata/generate.mjs error handling', () => { + it('should wrap parsing errors with filename, original message, and cause', async () => { + const error = new Error('PARSE_ERROR'); + mockParseApiDoc.mock.mockImplementation(() => { + throw error; + }); + + const fullInput = [{ file: { path: 'docs/api/fs.md', basename: 'fs.md' } }]; + const itemIndices = [0]; + + await assert.rejects( + async () => await processChunk(fullInput, itemIndices, {}), + { + name: 'Error', + message: 'Failed to parse metadata for docs/api/fs.md: PARSE_ERROR', + cause: error, + } + ); + }); + + it('should fallback to basename or unknown if path is missing', async () => { + const error = new Error('PARSE_ERROR'); + mockParseApiDoc.mock.mockImplementation(() => { + throw error; + }); + + const fullInput = [{ file: { basename: 'fs.md' } }]; + + await assert.rejects(async () => await processChunk(fullInput, [0], {}), { + name: 'Error', + message: 'Failed to parse metadata for fs.md: PARSE_ERROR', + cause: error, + }); + }); +}); diff --git a/src/generators/metadata/generate.mjs b/src/generators/metadata/generate.mjs index 149aacb6..06473ec8 100644 --- a/src/generators/metadata/generate.mjs +++ b/src/generators/metadata/generate.mjs @@ -14,7 +14,15 @@ export async function processChunk(fullInput, itemIndices, typeMap) { const results = []; for (const idx of itemIndices) { - results.push(...parseApiDoc(fullInput[idx], typeMap)); + const input = fullInput[idx]; + try { + results.push(...parseApiDoc(input, typeMap)); + } catch (err) { + const path = + input?.file?.path ?? input?.file?.basename ?? ''; + const message = `Failed to parse metadata for ${path}: ${err.message ?? err}`; + throw new Error(message, { cause: err }); + } } return results; From bbc3ecfb8e7bc964d9f48249fc9ad03d4aa5a32e Mon Sep 17 00:00:00 2001 From: Aditya Raj Date: Wed, 18 Mar 2026 17:40:27 +0530 Subject: [PATCH 2/3] fix: harden generator error wrapping and fallback coverage --- src/generators/ast/generate.mjs | 3 ++- .../metadata/__tests__/generate.test.mjs | 15 +++++++++++++++ src/generators/metadata/generate.mjs | 3 ++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/generators/ast/generate.mjs b/src/generators/ast/generate.mjs index 082528f5..015138bf 100644 --- a/src/generators/ast/generate.mjs +++ b/src/generators/ast/generate.mjs @@ -40,7 +40,8 @@ export async function processChunk(inputSlice, itemIndices) { file: { stem: vfile.stem, basename: vfile.basename, path }, }); } catch (err) { - const message = `Failed to process ${path}: ${err.message ?? err}`; + const errorText = err instanceof Error ? err.message : String(err); + const message = `Failed to process ${path}: ${errorText}`; throw new Error(message, { cause: err }); } } diff --git a/src/generators/metadata/__tests__/generate.test.mjs b/src/generators/metadata/__tests__/generate.test.mjs index d157af7a..ce1a7a60 100644 --- a/src/generators/metadata/__tests__/generate.test.mjs +++ b/src/generators/metadata/__tests__/generate.test.mjs @@ -52,4 +52,19 @@ describe('metadata/generate.mjs error handling', () => { cause: error, }); }); + + it('should fallback to when both path and basename are missing', async () => { + const error = new Error('PARSE_ERROR'); + mockParseApiDoc.mock.mockImplementation(() => { + throw error; + }); + + const fullInput = [{ file: {} }]; + + await assert.rejects(async () => await processChunk(fullInput, [0], {}), { + name: 'Error', + message: 'Failed to parse metadata for : PARSE_ERROR', + cause: error, + }); + }); }); diff --git a/src/generators/metadata/generate.mjs b/src/generators/metadata/generate.mjs index 06473ec8..1c37419d 100644 --- a/src/generators/metadata/generate.mjs +++ b/src/generators/metadata/generate.mjs @@ -20,7 +20,8 @@ export async function processChunk(fullInput, itemIndices, typeMap) { } catch (err) { const path = input?.file?.path ?? input?.file?.basename ?? ''; - const message = `Failed to parse metadata for ${path}: ${err.message ?? err}`; + const errorDetails = err instanceof Error ? err.message : String(err); + const message = `Failed to parse metadata for ${path}: ${errorDetails}`; throw new Error(message, { cause: err }); } } From ff10c83630310a6d329c73f981a9d0f555cfc5b6 Mon Sep 17 00:00:00 2001 From: Aditya Raj Date: Thu, 19 Mar 2026 11:49:43 +0530 Subject: [PATCH 3/3] refactor(generators): align worker error handling with centralized runner --- .../ast/__tests__/generate.test.mjs | 9 ++-- src/generators/ast/generate.mjs | 32 ++++++-------- .../metadata/__tests__/generate.test.mjs | 43 ++++++------------- src/generators/metadata/generate.mjs | 11 +---- 4 files changed, 31 insertions(+), 64 deletions(-) diff --git a/src/generators/ast/__tests__/generate.test.mjs b/src/generators/ast/__tests__/generate.test.mjs index ece7683b..ef416567 100644 --- a/src/generators/ast/__tests__/generate.test.mjs +++ b/src/generators/ast/__tests__/generate.test.mjs @@ -16,7 +16,7 @@ mock.module('../../../utils/configuration/index.mjs', { const { processChunk } = await import('../generate.mjs'); describe('ast/generate.mjs error handling', () => { - it('should wrap readFile errors with filename, original message, and cause', async () => { + it('should bubble readFile errors to the caller', async () => { const error = new Error('FS_ERROR'); mockReadFile.mock.mockImplementation(async () => { throw error; @@ -27,10 +27,9 @@ describe('ast/generate.mjs error handling', () => { await assert.rejects( async () => await processChunk(inputSlice, itemIndices), - { - name: 'Error', - message: 'Failed to process test.md: FS_ERROR', - cause: error, + err => { + assert.strictEqual(err, error); + return true; } ); }); diff --git a/src/generators/ast/generate.mjs b/src/generators/ast/generate.mjs index 015138bf..e8ccf8fa 100644 --- a/src/generators/ast/generate.mjs +++ b/src/generators/ast/generate.mjs @@ -25,25 +25,19 @@ export async function processChunk(inputSlice, itemIndices) { const results = []; for (const path of filePaths) { - try { - const content = await readFile(path, 'utf-8'); - const vfile = new VFile({ - path, - value: content.replace( - QUERIES.stabilityIndexPrefix, - match => `[${match}](${STABILITY_INDEX_URL})` - ), - }); - - results.push({ - tree: remarkProcessor.parse(vfile), - file: { stem: vfile.stem, basename: vfile.basename, path }, - }); - } catch (err) { - const errorText = err instanceof Error ? err.message : String(err); - const message = `Failed to process ${path}: ${errorText}`; - throw new Error(message, { cause: err }); - } + const content = await readFile(path, 'utf-8'); + const vfile = new VFile({ + path, + value: content.replace( + QUERIES.stabilityIndexPrefix, + match => `[${match}](${STABILITY_INDEX_URL})` + ), + }); + + results.push({ + tree: remarkProcessor.parse(vfile), + file: { stem: vfile.stem, basename: vfile.basename, path }, + }); } return results; diff --git a/src/generators/metadata/__tests__/generate.test.mjs b/src/generators/metadata/__tests__/generate.test.mjs index ce1a7a60..01511959 100644 --- a/src/generators/metadata/__tests__/generate.test.mjs +++ b/src/generators/metadata/__tests__/generate.test.mjs @@ -19,7 +19,7 @@ mock.module('../../../utils/url.mjs', { const { processChunk } = await import('../generate.mjs'); describe('metadata/generate.mjs error handling', () => { - it('should wrap parsing errors with filename, original message, and cause', async () => { + it('should bubble parsing errors to the caller', async () => { const error = new Error('PARSE_ERROR'); mockParseApiDoc.mock.mockImplementation(() => { throw error; @@ -30,41 +30,24 @@ describe('metadata/generate.mjs error handling', () => { await assert.rejects( async () => await processChunk(fullInput, itemIndices, {}), - { - name: 'Error', - message: 'Failed to parse metadata for docs/api/fs.md: PARSE_ERROR', - cause: error, + err => { + assert.strictEqual(err, error); + return true; } ); }); - it('should fallback to basename or unknown if path is missing', async () => { - const error = new Error('PARSE_ERROR'); - mockParseApiDoc.mock.mockImplementation(() => { - throw error; - }); - - const fullInput = [{ file: { basename: 'fs.md' } }]; - - await assert.rejects(async () => await processChunk(fullInput, [0], {}), { - name: 'Error', - message: 'Failed to parse metadata for fs.md: PARSE_ERROR', - cause: error, - }); - }); - - it('should fallback to when both path and basename are missing', async () => { - const error = new Error('PARSE_ERROR'); + it('should preserve non-Error throws from parseApiDoc', async () => { mockParseApiDoc.mock.mockImplementation(() => { - throw error; + throw 'PARSE_ERROR'; }); - const fullInput = [{ file: {} }]; - - await assert.rejects(async () => await processChunk(fullInput, [0], {}), { - name: 'Error', - message: 'Failed to parse metadata for : PARSE_ERROR', - cause: error, - }); + await assert.rejects( + async () => await processChunk([{}], [0], {}), + err => { + assert.strictEqual(err, 'PARSE_ERROR'); + return true; + } + ); }); }); diff --git a/src/generators/metadata/generate.mjs b/src/generators/metadata/generate.mjs index 1c37419d..149aacb6 100644 --- a/src/generators/metadata/generate.mjs +++ b/src/generators/metadata/generate.mjs @@ -14,16 +14,7 @@ export async function processChunk(fullInput, itemIndices, typeMap) { const results = []; for (const idx of itemIndices) { - const input = fullInput[idx]; - try { - results.push(...parseApiDoc(input, typeMap)); - } catch (err) { - const path = - input?.file?.path ?? input?.file?.basename ?? ''; - const errorDetails = err instanceof Error ? err.message : String(err); - const message = `Failed to parse metadata for ${path}: ${errorDetails}`; - throw new Error(message, { cause: err }); - } + results.push(...parseApiDoc(fullInput[idx], typeMap)); } return results;