diff --git a/toolkit-docs-generator/src/cli/index.ts b/toolkit-docs-generator/src/cli/index.ts index bb05939f6..da6a075db 100644 --- a/toolkit-docs-generator/src/cli/index.ts +++ b/toolkit-docs-generator/src/cli/index.ts @@ -1714,11 +1714,17 @@ program } } + const mergeFailures = allResults.filter((result) => result.error); + // Error results can still carry a last-known-good toolkit fallback from + // the merger. Keep them in the batch output so one failed merge cannot + // silently remove that toolkit from index.json. + const writableResults = allResults; + // Generate output files (batch mode if not incremental) - if (!useIncremental && allResults.length > 0) { + if (!useIncremental && writableResults.length > 0) { spinner.start("Writing output files..."); - const toolkits = allResults.map((r) => r.toolkit); + const toolkits = writableResults.map((result) => result.toolkit); const genResult = await generator.generateAll(toolkits); filesWritten.push(...genResult.filesWritten); @@ -1731,13 +1737,15 @@ program } } else if (useIncremental) { // Generate index file for incremental mode - if (allResults.length > 0 || skipToolkitIds.size > 0) { + if (writableResults.length > 0 || skipToolkitIds.size > 0) { spinner.start("Generating index file..."); try { const existingToolkits = await generator.getCompletedToolkitIds(); const allToolkitIds = new Set([ ...existingToolkits, - ...allResults.map((r) => r.toolkit.id.toLowerCase()), + ...writableResults.map((result) => + result.toolkit.id.toLowerCase() + ), ]); // Load all toolkits for index @@ -1797,7 +1805,11 @@ program } // Print summary - if (filesWritten.length > 0 || writeErrors.length > 0) { + if ( + filesWritten.length > 0 || + writeErrors.length > 0 || + mergeFailures.length > 0 + ) { console.log(chalk.green("\n✓ Generation complete\n")); if (options.verbose) { console.log(chalk.dim("Files:")); @@ -1814,6 +1826,14 @@ program console.log(chalk.yellow(` ${error}`)); } } + if (mergeFailures.length > 0) { + console.log(chalk.red("\nFailed toolkits:")); + for (const failure of mergeFailures) { + console.log( + chalk.red(` ${failure.toolkit.id}: ${failure.error}`) + ); + } + } } if (options.verifyOutput) { @@ -1846,13 +1866,7 @@ program (count, result) => count + result.warnings.length, 0 ); - const failedToolkits = allResults - .filter((result) => - result.warnings.some((warning) => - warning.startsWith("Error processing toolkit") - ) - ) - .map((result) => result.toolkit.id); + const failedToolkits = mergeFailures.map((result) => result.toolkit.id); const failedTools = allResults.flatMap((result) => result.failedTools); const failedToolkitsFromTools = Array.from( new Set(failedTools.map((tool) => tool.toolkitId)) @@ -1905,6 +1919,9 @@ program title: "generate", details: runDetails, }); + if (mergeFailures.length > 0 || writeErrors.length > 0) { + process.exitCode = 1; + } } catch (error) { spinner.fail( `Error: ${error instanceof Error ? error.message : String(error)}` @@ -2415,6 +2432,11 @@ program spinner.start(progressTracker.getProgressString()); const results = await merger.mergeAllToolkits(); + const mergeFailures = results.filter((result) => result.error); + // Error results can still carry a last-known-good toolkit fallback. + // Preserve it in batch output rather than letting --clear-output + // remove the prior JSON artifact. + const writableResults = results; const summary = progressTracker.getSummary(); spinner.succeed( `Processed ${summary.completed} toolkit(s) with ${summary.totalTools} tools in ${summary.elapsed}` @@ -2435,11 +2457,20 @@ program console.log(chalk.dim(` - ${warning}`)); } } + if (mergeFailures.length > 0) { + console.log(chalk.red("\nFailed toolkits:")); + for (const failure of mergeFailures) { + console.log( + chalk.red(` ${failure.toolkit.id}: ${failure.error}`) + ); + } + process.exitCode = 1; + } // Generate output (batch mode if not incremental) - if (!useIncremental && results.length > 0) { + if (!useIncremental && writableResults.length > 0) { spinner.start("Writing output files..."); - const toolkits = results.map((r) => r.toolkit); + const toolkits = writableResults.map((result) => result.toolkit); const genResult = await generator.generateAll(toolkits); filesWritten.push(...genResult.filesWritten); @@ -2461,7 +2492,9 @@ program const existingToolkits = await generator.getCompletedToolkitIds(); const allToolkitIds = new Set([ ...existingToolkits, - ...results.map((r) => r.toolkit.id.toLowerCase()), + ...writableResults.map((result) => + result.toolkit.id.toLowerCase() + ), ]); const toolkitsForIndex: MergedToolkit[] = []; @@ -2553,6 +2586,7 @@ program for (const warning of writeErrors) { console.log(chalk.yellow(` ${warning}`)); } + process.exitCode = 1; } } catch (error) { spinner.fail( diff --git a/toolkit-docs-generator/src/merger/data-merger.ts b/toolkit-docs-generator/src/merger/data-merger.ts index 6696dfd89..d278b18ca 100644 --- a/toolkit-docs-generator/src/merger/data-merger.ts +++ b/toolkit-docs-generator/src/merger/data-merger.ts @@ -93,6 +93,7 @@ export interface MergeResult { toolkit: MergedToolkit; warnings: string[]; failedTools: FailedTool[]; + error?: string; } export interface ToolExampleResult { @@ -987,6 +988,15 @@ export class DataMerger { message: string, previousToolkit?: MergedToolkit ): MergeResult { + if (previousToolkit) { + return { + toolkit: previousToolkit, + warnings: [`Error processing toolkit: ${message}`], + failedTools: [], + error: message, + }; + } + return { toolkit: { id: toolkitId, @@ -1005,13 +1015,14 @@ export class DataMerger { }, auth: null, tools: [], - documentationChunks: previousToolkit?.documentationChunks ?? [], - customImports: previousToolkit?.customImports ?? [], - subPages: previousToolkit?.subPages ?? [], + documentationChunks: [], + customImports: [], + subPages: [], generatedAt: new Date().toISOString(), }, warnings: [`Error processing toolkit: ${message}`], failedTools: [], + error: message, }; } @@ -1049,8 +1060,21 @@ export class DataMerger { return result; } catch (error) { const message = error instanceof Error ? error.message : String(error); + if (this.requireCompleteData) { + throw new Error(`Failed to process ${toolkitId}: ${message}`, { + cause: error, + }); + } const previousToolkit = this.getPreviousToolkit(toolkitId); - return this.buildMergeErrorResult(toolkitId, message, previousToolkit); + const result = this.buildMergeErrorResult( + toolkitId, + message, + previousToolkit + ); + if (this.onToolkitComplete && previousToolkit) { + await this.onToolkitComplete(result); + } + return result; } } diff --git a/toolkit-docs-generator/tests/merger/data-merger.test.ts b/toolkit-docs-generator/tests/merger/data-merger.test.ts index 70b0afe0c..e2277f399 100644 --- a/toolkit-docs-generator/tests/merger/data-merger.test.ts +++ b/toolkit-docs-generator/tests/merger/data-merger.test.ts @@ -1917,18 +1917,23 @@ describe("DataMerger", () => { }), createStubGenerator() ); + const completedToolkitIds: string[] = []; const merger = new DataMerger({ toolkitDataSource, customSectionsSource: makeFailingCustomSectionsSource(), toolExampleGenerator: createStubGenerator(), previousToolkits: new Map([["github", previousResult.toolkit]]), + onToolkitComplete: async (result) => { + completedToolkitIds.push(result.toolkit.id); + }, }); const results = await merger.mergeAllToolkits(); const result = results[0]; - // Error result must preserve previous custom sections, not return empty arrays + expect(result?.error).toBe("Custom sections source unavailable"); + expect(result?.toolkit).toEqual(previousResult.toolkit); expect(result?.toolkit.documentationChunks).toHaveLength(1); expect(result?.toolkit.documentationChunks[0]?.content).toBe( "Critical: GitHub Apps only." @@ -1938,6 +1943,7 @@ describe("DataMerger", () => { expect(result?.warnings[0]).toContain( "Custom sections source unavailable" ); + expect(completedToolkitIds).toEqual(["Github"]); }); it("returns empty custom sections in error result when no previous toolkit exists", async () => { @@ -1945,22 +1951,28 @@ describe("DataMerger", () => { toolSource: new InMemoryToolDataSource([githubTool1]), metadataSource: new InMemoryMetadataSource([githubMetadata]), }); + const completedToolkitIds: string[] = []; const merger = new DataMerger({ toolkitDataSource, customSectionsSource: makeFailingCustomSectionsSource(), toolExampleGenerator: createStubGenerator(), + onToolkitComplete: async (result) => { + completedToolkitIds.push(result.toolkit.id); + }, }); const results = await merger.mergeAllToolkits(); const result = results[0]; + expect(result?.error).toBe("Custom sections source unavailable"); expect(result?.toolkit.documentationChunks).toHaveLength(0); expect(result?.toolkit.customImports).toHaveLength(0); expect(result?.toolkit.subPages).toHaveLength(0); expect(result?.warnings[0]).toContain( "Custom sections source unavailable" ); + expect(completedToolkitIds).toEqual([]); }); }); @@ -2053,6 +2065,27 @@ describe("DataMerger", () => { expect(results[0]?.toolkit.id).toBe("Github"); }); + it("fails strict runs when a complete toolkit cannot be merged", async () => { + const toolkitDataSource = createCombinedToolkitDataSource({ + toolSource: new InMemoryToolDataSource([githubTool1]), + metadataSource: new InMemoryMetadataSource([githubMetadata]), + }); + const merger = new DataMerger({ + toolkitDataSource, + customSectionsSource: { + getCustomSections: async () => { + throw new Error("Custom sections source unavailable"); + }, + }, + toolExampleGenerator: createStubGenerator(), + requireCompleteData: true, + }); + + await expect(merger.mergeAllToolkits()).rejects.toThrow( + "Failed to process Github: Custom sections source unavailable" + ); + }); + it("should return empty array when no tools", async () => { const toolkitDataSource = createCombinedToolkitDataSource({ toolSource: new InMemoryToolDataSource([]),