From ae68e51a05c5a503b688bedeeee16f20d2508112 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Sat, 19 Jul 2025 01:21:02 +0900 Subject: [PATCH 01/13] fix(core): optimize worker thread allocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Set TASKS_PER_THREAD to 100 for better balance between performance and resource usage - Add comment explaining that worker initialization is expensive - Update tests to match new thread allocation logic 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/shared/processConcurrency.ts | 5 ++++- tests/shared/processConcurrency.test.ts | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/shared/processConcurrency.ts b/src/shared/processConcurrency.ts index b31d36f87..7df926f38 100644 --- a/src/shared/processConcurrency.ts +++ b/src/shared/processConcurrency.ts @@ -2,6 +2,9 @@ import os from 'node:os'; import { Tinypool } from 'tinypool'; import { logger } from './logger.js'; +// Worker initialization is expensive, so we prefer fewer threads unless there are many files +const TASKS_PER_THREAD = 100; + export const getProcessConcurrency = (): number => { return typeof os.availableParallelism === 'function' ? os.availableParallelism() : os.cpus().length; }; @@ -12,7 +15,7 @@ export const getWorkerThreadCount = (numOfTasks: number): { minThreads: number; const minThreads = 1; // Limit max threads based on number of tasks - const maxThreads = Math.max(minThreads, Math.min(processConcurrency, Math.ceil(numOfTasks / 100))); + const maxThreads = Math.max(minThreads, Math.min(processConcurrency, Math.ceil(numOfTasks / TASKS_PER_THREAD))); return { minThreads, diff --git a/tests/shared/processConcurrency.test.ts b/tests/shared/processConcurrency.test.ts index 50c635a6f..16632f929 100644 --- a/tests/shared/processConcurrency.test.ts +++ b/tests/shared/processConcurrency.test.ts @@ -35,7 +35,7 @@ describe('processConcurrency', () => { const { minThreads, maxThreads } = getWorkerThreadCount(1000); expect(minThreads).toBe(1); - expect(maxThreads).toBe(8); // Limited by CPU count + expect(maxThreads).toBe(8); // Limited by CPU count: Math.min(8, 1000/100) = 8 }); it('should scale max threads based on task count', () => { @@ -49,7 +49,7 @@ describe('processConcurrency', () => { const { minThreads, maxThreads } = getWorkerThreadCount(10000); expect(minThreads).toBe(1); - expect(maxThreads).toBe(8); // Limited by CPU count + expect(maxThreads).toBe(8); // Limited by CPU count: Math.min(8, 10000/100) = 8 }); it('should handle zero tasks', () => { @@ -73,7 +73,7 @@ describe('processConcurrency', () => { expect(Tinypool).toHaveBeenCalledWith({ filename: workerPath, minThreads: 1, - maxThreads: 4, + maxThreads: 4, // Math.min(4, 500/100) = 4 idleTimeout: 5000, env: expect.objectContaining({ ...process.env, From 47d8a79663d8b2eeaaf86ba98fca9e71af505978 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Sat, 19 Jul 2025 15:33:07 +0900 Subject: [PATCH 02/13] perf(core): Add timing measurement for Tinypool initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add performance monitoring to track how long it takes to initialize the Tinypool worker pool. This helps identify potential bottlenecks during startup. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/shared/processConcurrency.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/shared/processConcurrency.ts b/src/shared/processConcurrency.ts index 7df926f38..96019fd61 100644 --- a/src/shared/processConcurrency.ts +++ b/src/shared/processConcurrency.ts @@ -30,7 +30,9 @@ export const initWorker = (numOfTasks: number, workerPath: string): Tinypool => `Initializing worker pool with min=${minThreads}, max=${maxThreads} threads. Worker path: ${workerPath}`, ); - return new Tinypool({ + const startTime = process.hrtime.bigint(); + + const pool = new Tinypool({ filename: workerPath, minThreads, maxThreads, @@ -40,4 +42,11 @@ export const initWorker = (numOfTasks: number, workerPath: string): Tinypool => REPOMIX_LOGLEVEL: logger.getLogLevel().toString(), }, }); + + const endTime = process.hrtime.bigint(); + const initTime = Number(endTime - startTime) / 1e6; // Convert to milliseconds + + logger.debug(`Tinypool initialization took ${initTime.toFixed(2)}ms`); + + return pool; }; From 6eb8f27fd5f3ef4d131e68be050a7df34b416866 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Sat, 19 Jul 2025 16:05:12 +0900 Subject: [PATCH 03/13] refactor(core): Replace environment variable with workerData for worker log level MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the environment variable approach for passing log levels to workers with Tinypool's workerData mechanism, which is more idiomatic for worker thread configuration. Changes: - Add setLogLevelByWorkerData() method to handle workerData-based log level setting - Update Tinypool configuration to use workerData instead of env variables - Update all 5 worker files to use setLogLevelByWorkerData() - Remove unused setLogLevelByEnv function and related test mocks - Update tests to reflect new workerData configuration This provides better isolation and follows Node.js worker thread best practices. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/core/file/workers/fileCollectWorker.ts | 6 ++--- src/core/file/workers/fileProcessWorker.ts | 6 ++--- src/core/metrics/workers/fileMetricsWorker.ts | 6 ++--- .../metrics/workers/outputMetricsWorker.ts | 6 ++--- .../security/workers/securityCheckWorker.ts | 6 ++--- src/shared/logger.ts | 27 ++++++++++--------- src/shared/processConcurrency.ts | 5 ++-- tests/cli/cliRun.test.ts | 2 +- tests/shared/processConcurrency.test.ts | 7 +++-- 9 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/core/file/workers/fileCollectWorker.ts b/src/core/file/workers/fileCollectWorker.ts index 2efdd227a..9bf5ce524 100644 --- a/src/core/file/workers/fileCollectWorker.ts +++ b/src/core/file/workers/fileCollectWorker.ts @@ -1,5 +1,5 @@ import path from 'node:path'; -import { logger, setLogLevelByEnv } from '../../../shared/logger.js'; +import { logger, setLogLevelByWorkerData } from '../../../shared/logger.js'; import { readRawFile } from '../fileRead.js'; export interface FileCollectTask { @@ -8,8 +8,8 @@ export interface FileCollectTask { maxFileSize: number; } -// Set logger log level from environment variable if provided -setLogLevelByEnv(); +// Set logger log level from workerData if provided +setLogLevelByWorkerData(); export default async ({ filePath, rootDir, maxFileSize }: FileCollectTask) => { const fullPath = path.resolve(rootDir, filePath); diff --git a/src/core/file/workers/fileProcessWorker.ts b/src/core/file/workers/fileProcessWorker.ts index 5f77012f5..f5bd19e30 100644 --- a/src/core/file/workers/fileProcessWorker.ts +++ b/src/core/file/workers/fileProcessWorker.ts @@ -1,5 +1,5 @@ import type { RepomixConfigMerged } from '../../../config/configSchema.js'; -import { logger, setLogLevelByEnv } from '../../../shared/logger.js'; +import { logger, setLogLevelByWorkerData } from '../../../shared/logger.js'; import { processContent } from '../fileProcessContent.js'; import type { ProcessedFile, RawFile } from '../fileTypes.js'; @@ -8,8 +8,8 @@ export interface FileProcessTask { config: RepomixConfigMerged; } -// Set logger log level from environment variable if provided -setLogLevelByEnv(); +// Set logger log level from workerData if provided +setLogLevelByWorkerData(); export default async ({ rawFile, config }: FileProcessTask): Promise => { const processedContent = await processContent(rawFile, config); diff --git a/src/core/metrics/workers/fileMetricsWorker.ts b/src/core/metrics/workers/fileMetricsWorker.ts index 212b42712..2e8c3124d 100644 --- a/src/core/metrics/workers/fileMetricsWorker.ts +++ b/src/core/metrics/workers/fileMetricsWorker.ts @@ -1,5 +1,5 @@ import type { TiktokenEncoding } from 'tiktoken'; -import { logger, setLogLevelByEnv } from '../../../shared/logger.js'; +import { logger, setLogLevelByWorkerData } from '../../../shared/logger.js'; import type { ProcessedFile } from '../../file/fileTypes.js'; import { TokenCounter } from '../TokenCounter.js'; import type { FileMetrics } from './types.js'; @@ -21,8 +21,8 @@ const getTokenCounter = (encoding: TiktokenEncoding): TokenCounter => { return tokenCounter; }; -// Set logger log level from environment variable if provided -setLogLevelByEnv(); +// Set logger log level from workerData if provided +setLogLevelByWorkerData(); export default async ({ file, encoding }: FileMetricsTask): Promise => { const processStartAt = process.hrtime.bigint(); diff --git a/src/core/metrics/workers/outputMetricsWorker.ts b/src/core/metrics/workers/outputMetricsWorker.ts index 1fb70e8f3..4afa96c67 100644 --- a/src/core/metrics/workers/outputMetricsWorker.ts +++ b/src/core/metrics/workers/outputMetricsWorker.ts @@ -1,5 +1,5 @@ import type { TiktokenEncoding } from 'tiktoken'; -import { logger, setLogLevelByEnv } from '../../../shared/logger.js'; +import { logger, setLogLevelByWorkerData } from '../../../shared/logger.js'; import { TokenCounter } from '../TokenCounter.js'; export interface OutputMetricsTask { @@ -18,8 +18,8 @@ const getTokenCounter = (encoding: TiktokenEncoding): TokenCounter => { return tokenCounter; }; -// Set logger log level from environment variable if provided -setLogLevelByEnv(); +// Set logger log level from workerData if provided +setLogLevelByWorkerData(); export default async ({ content, encoding, path }: OutputMetricsTask): Promise => { const processStartAt = process.hrtime.bigint(); diff --git a/src/core/security/workers/securityCheckWorker.ts b/src/core/security/workers/securityCheckWorker.ts index 295dab348..426cc83f0 100644 --- a/src/core/security/workers/securityCheckWorker.ts +++ b/src/core/security/workers/securityCheckWorker.ts @@ -1,7 +1,7 @@ import { lintSource } from '@secretlint/core'; import { creator } from '@secretlint/secretlint-rule-preset-recommend'; import type { SecretLintCoreConfig } from '@secretlint/types'; -import { logger, setLogLevelByEnv } from '../../../shared/logger.js'; +import { logger, setLogLevelByWorkerData } from '../../../shared/logger.js'; // Security check type to distinguish between regular files and git diffs export type SecurityCheckType = 'file' | 'gitDiff'; @@ -18,8 +18,8 @@ export interface SuspiciousFileResult { type: SecurityCheckType; } -// Set logger log level from environment variable if provided -setLogLevelByEnv(); +// Set logger log level from workerData if provided +setLogLevelByWorkerData(); export default async ({ filePath, content, type }: SecurityCheckTask) => { const config = createSecretLintConfig(); diff --git a/src/shared/logger.ts b/src/shared/logger.ts index 4d29d45da..111d941fc 100644 --- a/src/shared/logger.ts +++ b/src/shared/logger.ts @@ -1,4 +1,5 @@ import util from 'node:util'; +import { workerData } from 'node:worker_threads'; import pc from 'picocolors'; export const repomixLogLevels = { @@ -92,18 +93,20 @@ export const setLogLevel = (level: RepomixLogLevel) => { }; /** - * Set logger log level from REPOMIX_LOGLEVEL environment variable if valid. + * Set logger log level from workerData if valid. + * This is used in worker threads where configuration is passed via workerData. */ -export const setLogLevelByEnv = () => { - const logLevelStr = process.env.REPOMIX_LOGLEVEL; - const logLevelNum = Number(logLevelStr); - if ( - logLevelNum === repomixLogLevels.SILENT || - logLevelNum === repomixLogLevels.ERROR || - logLevelNum === repomixLogLevels.WARN || - logLevelNum === repomixLogLevels.INFO || - logLevelNum === repomixLogLevels.DEBUG - ) { - setLogLevel(logLevelNum); +export const setLogLevelByWorkerData = () => { + if (workerData?.logLevel !== undefined) { + const logLevel = workerData.logLevel; + if ( + logLevel === repomixLogLevels.SILENT || + logLevel === repomixLogLevels.ERROR || + logLevel === repomixLogLevels.WARN || + logLevel === repomixLogLevels.INFO || + logLevel === repomixLogLevels.DEBUG + ) { + setLogLevel(logLevel); + } } }; diff --git a/src/shared/processConcurrency.ts b/src/shared/processConcurrency.ts index 96019fd61..baa97699d 100644 --- a/src/shared/processConcurrency.ts +++ b/src/shared/processConcurrency.ts @@ -37,9 +37,8 @@ export const initWorker = (numOfTasks: number, workerPath: string): Tinypool => minThreads, maxThreads, idleTimeout: 5000, - env: { - ...process.env, - REPOMIX_LOGLEVEL: logger.getLogLevel().toString(), + workerData: { + logLevel: logger.getLogLevel(), }, }); diff --git a/tests/cli/cliRun.test.ts b/tests/cli/cliRun.test.ts index d89390c77..18d797693 100644 --- a/tests/cli/cliRun.test.ts +++ b/tests/cli/cliRun.test.ts @@ -35,7 +35,7 @@ vi.mock('../../src/shared/logger', () => ({ }), getLogLevel: vi.fn(() => logLevel), }, - setLogLevelByEnv: vi.fn(), + setLogLevelByWorkerData: vi.fn(), })); vi.mock('../../src/cli/actions/defaultAction'); diff --git a/tests/shared/processConcurrency.test.ts b/tests/shared/processConcurrency.test.ts index 16632f929..bcd9fa075 100644 --- a/tests/shared/processConcurrency.test.ts +++ b/tests/shared/processConcurrency.test.ts @@ -75,10 +75,9 @@ describe('processConcurrency', () => { minThreads: 1, maxThreads: 4, // Math.min(4, 500/100) = 4 idleTimeout: 5000, - env: expect.objectContaining({ - ...process.env, - REPOMIX_LOGLEVEL: '2', - }), + workerData: { + logLevel: 2, + }, }); expect(tinypool).toBeDefined(); }); From 7f7d4877fcac8546fc306a2e4f97a37b0817af6b Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Sat, 19 Jul 2025 16:09:01 +0900 Subject: [PATCH 04/13] fix(core): Handle workerData as array in setLogLevelByWorkerData MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix setLogLevelByWorkerData to properly handle workerData when it comes as an array format like [{ workerId: 1 }, { logLevel: 3 }]. The logLevel is in the second element of the array. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/shared/logger.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/logger.ts b/src/shared/logger.ts index 111d941fc..68c929a36 100644 --- a/src/shared/logger.ts +++ b/src/shared/logger.ts @@ -97,8 +97,8 @@ export const setLogLevel = (level: RepomixLogLevel) => { * This is used in worker threads where configuration is passed via workerData. */ export const setLogLevelByWorkerData = () => { - if (workerData?.logLevel !== undefined) { - const logLevel = workerData.logLevel; + if (Array.isArray(workerData) && workerData.length > 1 && workerData[1]?.logLevel !== undefined) { + const logLevel = workerData[1].logLevel; if ( logLevel === repomixLogLevels.SILENT || logLevel === repomixLogLevels.ERROR || From f51bf409574c9483db80cb8c7db993198eca31c1 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Sat, 19 Jul 2025 16:16:58 +0900 Subject: [PATCH 05/13] refactor(core): Improve worker logger initialization positioning and comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move setLogLevelByWorkerData() calls to the top of all worker files (immediately after imports) with clear documentation. This ensures logger configuration is set up before any other code execution in worker threads. - Move logger initialization to module load time in all 5 worker files - Add standardized comment explaining the importance of early initialization - Ensure consistent pattern across all worker implementations This improves debugging capabilities and ensures proper logging from worker startup. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/core/file/workers/fileCollectWorker.ts | 7 ++++--- src/core/file/workers/fileProcessWorker.ts | 7 ++++--- src/core/metrics/workers/fileMetricsWorker.ts | 7 ++++--- src/core/metrics/workers/outputMetricsWorker.ts | 7 ++++--- src/core/security/workers/securityCheckWorker.ts | 7 ++++--- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/core/file/workers/fileCollectWorker.ts b/src/core/file/workers/fileCollectWorker.ts index 9bf5ce524..c20cf98da 100644 --- a/src/core/file/workers/fileCollectWorker.ts +++ b/src/core/file/workers/fileCollectWorker.ts @@ -2,15 +2,16 @@ import path from 'node:path'; import { logger, setLogLevelByWorkerData } from '../../../shared/logger.js'; import { readRawFile } from '../fileRead.js'; +// Initialize logger configuration from workerData at module load time +// This must be called before any logging operations in the worker +setLogLevelByWorkerData(); + export interface FileCollectTask { filePath: string; rootDir: string; maxFileSize: number; } -// Set logger log level from workerData if provided -setLogLevelByWorkerData(); - export default async ({ filePath, rootDir, maxFileSize }: FileCollectTask) => { const fullPath = path.resolve(rootDir, filePath); const content = await readRawFile(fullPath, maxFileSize); diff --git a/src/core/file/workers/fileProcessWorker.ts b/src/core/file/workers/fileProcessWorker.ts index f5bd19e30..1b84cb741 100644 --- a/src/core/file/workers/fileProcessWorker.ts +++ b/src/core/file/workers/fileProcessWorker.ts @@ -3,14 +3,15 @@ import { logger, setLogLevelByWorkerData } from '../../../shared/logger.js'; import { processContent } from '../fileProcessContent.js'; import type { ProcessedFile, RawFile } from '../fileTypes.js'; +// Initialize logger configuration from workerData at module load time +// This must be called before any logging operations in the worker +setLogLevelByWorkerData(); + export interface FileProcessTask { rawFile: RawFile; config: RepomixConfigMerged; } -// Set logger log level from workerData if provided -setLogLevelByWorkerData(); - export default async ({ rawFile, config }: FileProcessTask): Promise => { const processedContent = await processContent(rawFile, config); return { diff --git a/src/core/metrics/workers/fileMetricsWorker.ts b/src/core/metrics/workers/fileMetricsWorker.ts index 2e8c3124d..63b52e356 100644 --- a/src/core/metrics/workers/fileMetricsWorker.ts +++ b/src/core/metrics/workers/fileMetricsWorker.ts @@ -4,6 +4,10 @@ import type { ProcessedFile } from '../../file/fileTypes.js'; import { TokenCounter } from '../TokenCounter.js'; import type { FileMetrics } from './types.js'; +// Initialize logger configuration from workerData at module load time +// This must be called before any logging operations in the worker +setLogLevelByWorkerData(); + export interface FileMetricsTask { file: ProcessedFile; index: number; @@ -21,9 +25,6 @@ const getTokenCounter = (encoding: TiktokenEncoding): TokenCounter => { return tokenCounter; }; -// Set logger log level from workerData if provided -setLogLevelByWorkerData(); - export default async ({ file, encoding }: FileMetricsTask): Promise => { const processStartAt = process.hrtime.bigint(); const metrics = await calculateIndividualFileMetrics(file, encoding); diff --git a/src/core/metrics/workers/outputMetricsWorker.ts b/src/core/metrics/workers/outputMetricsWorker.ts index 4afa96c67..936541a1d 100644 --- a/src/core/metrics/workers/outputMetricsWorker.ts +++ b/src/core/metrics/workers/outputMetricsWorker.ts @@ -2,6 +2,10 @@ import type { TiktokenEncoding } from 'tiktoken'; import { logger, setLogLevelByWorkerData } from '../../../shared/logger.js'; import { TokenCounter } from '../TokenCounter.js'; +// Initialize logger configuration from workerData at module load time +// This must be called before any logging operations in the worker +setLogLevelByWorkerData(); + export interface OutputMetricsTask { content: string; encoding: TiktokenEncoding; @@ -18,9 +22,6 @@ const getTokenCounter = (encoding: TiktokenEncoding): TokenCounter => { return tokenCounter; }; -// Set logger log level from workerData if provided -setLogLevelByWorkerData(); - export default async ({ content, encoding, path }: OutputMetricsTask): Promise => { const processStartAt = process.hrtime.bigint(); const counter = getTokenCounter(encoding); diff --git a/src/core/security/workers/securityCheckWorker.ts b/src/core/security/workers/securityCheckWorker.ts index 426cc83f0..2140c6ee5 100644 --- a/src/core/security/workers/securityCheckWorker.ts +++ b/src/core/security/workers/securityCheckWorker.ts @@ -3,6 +3,10 @@ import { creator } from '@secretlint/secretlint-rule-preset-recommend'; import type { SecretLintCoreConfig } from '@secretlint/types'; import { logger, setLogLevelByWorkerData } from '../../../shared/logger.js'; +// Initialize logger configuration from workerData at module load time +// This must be called before any logging operations in the worker +setLogLevelByWorkerData(); + // Security check type to distinguish between regular files and git diffs export type SecurityCheckType = 'file' | 'gitDiff'; @@ -18,9 +22,6 @@ export interface SuspiciousFileResult { type: SecurityCheckType; } -// Set logger log level from workerData if provided -setLogLevelByWorkerData(); - export default async ({ filePath, content, type }: SecurityCheckTask) => { const config = createSecretLintConfig(); From f09ca5228a1d9c635bd2163b03ee3afe0ff8f707 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Sat, 19 Jul 2025 16:22:20 +0900 Subject: [PATCH 06/13] refactor(core): Extract TokenCounter management to factory with multi-encoding support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create tokenCounterFactory.ts to centralize TokenCounter singleton management and add support for multiple encodings using a Map-based cache. Changes: - Add tokenCounterFactory.ts with Map cache - Support multiple encodings simultaneously in worker threads - Add getTokenCounter() and freeTokenCounter() functions for lifecycle management - Update fileMetricsWorker.ts and outputMetricsWorker.ts to use factory - Remove duplicate singleton implementations from worker files - Add timing measurement to TokenCounter initialization for performance monitoring This improves code organization, reduces duplication, and enables proper handling of multiple encoding types while maintaining memory efficiency. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/core/metrics/TokenCounter.ts | 7 +++++ src/core/metrics/tokenCounterFactory.ts | 29 +++++++++++++++++++ src/core/metrics/workers/fileMetricsWorker.ts | 17 ++--------- .../metrics/workers/outputMetricsWorker.ts | 17 ++--------- 4 files changed, 40 insertions(+), 30 deletions(-) create mode 100644 src/core/metrics/tokenCounterFactory.ts diff --git a/src/core/metrics/TokenCounter.ts b/src/core/metrics/TokenCounter.ts index a8f719d31..d823bf54a 100644 --- a/src/core/metrics/TokenCounter.ts +++ b/src/core/metrics/TokenCounter.ts @@ -5,8 +5,15 @@ export class TokenCounter { private encoding: Tiktoken; constructor(encodingName: TiktokenEncoding) { + const startTime = process.hrtime.bigint(); + // Setup encoding with the specified model this.encoding = get_encoding(encodingName); + + const endTime = process.hrtime.bigint(); + const initTime = Number(endTime - startTime) / 1e6; // Convert to milliseconds + + logger.debug(`TokenCounter initialization took ${initTime.toFixed(2)}ms`); } public countTokens(content: string, filePath?: string): number { diff --git a/src/core/metrics/tokenCounterFactory.ts b/src/core/metrics/tokenCounterFactory.ts new file mode 100644 index 000000000..4f9ae1577 --- /dev/null +++ b/src/core/metrics/tokenCounterFactory.ts @@ -0,0 +1,29 @@ +import type { TiktokenEncoding } from 'tiktoken'; +import { TokenCounter } from './TokenCounter.js'; + +// Worker-level cache for TokenCounter instances by encoding +const tokenCounters = new Map(); + +/** + * Get or create a TokenCounter instance for the given encoding. + * This ensures only one TokenCounter exists per encoding per worker thread to optimize memory usage. + */ +export const getTokenCounter = (encoding: TiktokenEncoding): TokenCounter => { + let tokenCounter = tokenCounters.get(encoding); + if (!tokenCounter) { + tokenCounter = new TokenCounter(encoding); + tokenCounters.set(encoding, tokenCounter); + } + return tokenCounter; +}; + +/** + * Free all TokenCounter resources and clear the cache. + * This should be called when the worker is terminating. + */ +export const freeTokenCounter = (): void => { + for (const tokenCounter of tokenCounters.values()) { + tokenCounter.free(); + } + tokenCounters.clear(); +}; diff --git a/src/core/metrics/workers/fileMetricsWorker.ts b/src/core/metrics/workers/fileMetricsWorker.ts index 63b52e356..f18db7bee 100644 --- a/src/core/metrics/workers/fileMetricsWorker.ts +++ b/src/core/metrics/workers/fileMetricsWorker.ts @@ -1,7 +1,7 @@ import type { TiktokenEncoding } from 'tiktoken'; import { logger, setLogLevelByWorkerData } from '../../../shared/logger.js'; import type { ProcessedFile } from '../../file/fileTypes.js'; -import { TokenCounter } from '../TokenCounter.js'; +import { freeTokenCounter, getTokenCounter } from '../tokenCounterFactory.js'; import type { FileMetrics } from './types.js'; // Initialize logger configuration from workerData at module load time @@ -15,16 +15,6 @@ export interface FileMetricsTask { encoding: TiktokenEncoding; } -// Worker-level singleton for TokenCounter -let tokenCounter: TokenCounter | null = null; - -const getTokenCounter = (encoding: TiktokenEncoding): TokenCounter => { - if (!tokenCounter) { - tokenCounter = new TokenCounter(encoding); - } - return tokenCounter; -}; - export default async ({ file, encoding }: FileMetricsTask): Promise => { const processStartAt = process.hrtime.bigint(); const metrics = await calculateIndividualFileMetrics(file, encoding); @@ -49,8 +39,5 @@ export const calculateIndividualFileMetrics = async ( // Cleanup when worker is terminated process.on('exit', () => { - if (tokenCounter) { - tokenCounter.free(); - tokenCounter = null; - } + freeTokenCounter(); }); diff --git a/src/core/metrics/workers/outputMetricsWorker.ts b/src/core/metrics/workers/outputMetricsWorker.ts index 936541a1d..22638ca73 100644 --- a/src/core/metrics/workers/outputMetricsWorker.ts +++ b/src/core/metrics/workers/outputMetricsWorker.ts @@ -1,6 +1,6 @@ import type { TiktokenEncoding } from 'tiktoken'; import { logger, setLogLevelByWorkerData } from '../../../shared/logger.js'; -import { TokenCounter } from '../TokenCounter.js'; +import { freeTokenCounter, getTokenCounter } from '../tokenCounterFactory.js'; // Initialize logger configuration from workerData at module load time // This must be called before any logging operations in the worker @@ -12,16 +12,6 @@ export interface OutputMetricsTask { path?: string; } -// Worker-level singleton for TokenCounter -let tokenCounter: TokenCounter | null = null; - -const getTokenCounter = (encoding: TiktokenEncoding): TokenCounter => { - if (!tokenCounter) { - tokenCounter = new TokenCounter(encoding); - } - return tokenCounter; -}; - export default async ({ content, encoding, path }: OutputMetricsTask): Promise => { const processStartAt = process.hrtime.bigint(); const counter = getTokenCounter(encoding); @@ -37,8 +27,5 @@ export default async ({ content, encoding, path }: OutputMetricsTask): Promise { - if (tokenCounter) { - tokenCounter.free(); - tokenCounter = null; - } + freeTokenCounter(); }); From 002bea3d615451c02e5075a7466c01ff8c6605b7 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Wed, 23 Jul 2025 00:05:13 +0900 Subject: [PATCH 07/13] refactor(core): Rename handleOutput to writeOutputToDisk for clarity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update dependency injection parameter names to be more descriptive of the actual functionality. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/core/packager.ts | 5 +++-- tests/core/packager.test.ts | 6 +++--- tests/core/packager/diffsFunctionality.test.ts | 4 ++-- tests/integration-tests/packager.test.ts | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/core/packager.ts b/src/core/packager.ts index a229c4635..103c0f15e 100644 --- a/src/core/packager.ts +++ b/src/core/packager.ts @@ -1,4 +1,5 @@ import type { RepomixConfigMerged } from '../config/configSchema.js'; +import { RepomixError } from '../shared/errorHandle.js'; import type { RepomixProgressCallback } from '../shared/types.js'; import { collectFiles } from './file/fileCollect.js'; import { sortPaths } from './file/filePathSort.js'; @@ -32,7 +33,7 @@ const defaultDeps = { processFiles, generateOutput, validateFileSafety, - handleOutput: writeOutputToDisk, + writeOutputToDisk, copyToClipboardIfEnabled, calculateMetrics, sortPaths, @@ -97,7 +98,7 @@ export const pack = async ( const output = await deps.generateOutput(rootDirs, config, processedFiles, safeFilePaths, gitDiffResult); progressCallback('Writing output file...'); - await deps.handleOutput(output, config); + await deps.writeOutputToDisk(output, config); await deps.copyToClipboardIfEnabled(output, progressCallback, config); diff --git a/tests/core/packager.test.ts b/tests/core/packager.test.ts index 5e154ecb1..0985e5876 100644 --- a/tests/core/packager.test.ts +++ b/tests/core/packager.test.ts @@ -50,7 +50,7 @@ describe('packager', () => { suspiciousFilesResults: [], }), generateOutput: vi.fn().mockResolvedValue(mockOutput), - handleOutput: vi.fn().mockResolvedValue(undefined), + writeOutputToDisk: vi.fn().mockResolvedValue(undefined), copyToClipboardIfEnabled: vi.fn().mockResolvedValue(undefined), calculateMetrics: vi.fn().mockResolvedValue({ totalFiles: 2, @@ -75,7 +75,7 @@ describe('packager', () => { expect(mockDeps.collectFiles).toHaveBeenCalledWith(mockFilePaths, 'root', mockConfig, progressCallback); expect(mockDeps.validateFileSafety).toHaveBeenCalled(); expect(mockDeps.processFiles).toHaveBeenCalled(); - expect(mockDeps.handleOutput).toHaveBeenCalled(); + expect(mockDeps.writeOutputToDisk).toHaveBeenCalled(); expect(mockDeps.generateOutput).toHaveBeenCalled(); expect(mockDeps.calculateMetrics).toHaveBeenCalled(); @@ -88,7 +88,7 @@ describe('packager', () => { mockFilePaths, undefined, ); - expect(mockDeps.handleOutput).toHaveBeenCalledWith(mockOutput, mockConfig); + expect(mockDeps.writeOutputToDisk).toHaveBeenCalledWith(mockOutput, mockConfig); expect(mockDeps.copyToClipboardIfEnabled).toHaveBeenCalledWith(mockOutput, progressCallback, mockConfig); expect(mockDeps.calculateMetrics).toHaveBeenCalledWith( mockProcessedFiles, diff --git a/tests/core/packager/diffsFunctionality.test.ts b/tests/core/packager/diffsFunctionality.test.ts index 03a91d9c5..173ab4dbb 100644 --- a/tests/core/packager/diffsFunctionality.test.ts +++ b/tests/core/packager/diffsFunctionality.test.ts @@ -83,7 +83,7 @@ index 123..456 100644 processFiles: mockProcessFiles, generateOutput: mockGenerateOutput, validateFileSafety: mockValidateFileSafety, - handleOutput: mockHandleOutput, + writeOutputToDisk: mockHandleOutput, copyToClipboardIfEnabled: mockCopyToClipboard, calculateMetrics: mockCalculateMetrics, sortPaths: mockSortPaths, @@ -135,7 +135,7 @@ index 123..456 100644 processFiles: mockProcessFiles, generateOutput: mockGenerateOutput, validateFileSafety: mockValidateFileSafety, - handleOutput: mockHandleOutput, + writeOutputToDisk: mockHandleOutput, copyToClipboardIfEnabled: mockCopyToClipboard, calculateMetrics: mockCalculateMetrics, sortPaths: mockSortPaths, diff --git a/tests/integration-tests/packager.test.ts b/tests/integration-tests/packager.test.ts index b19c5d080..a07b03b71 100644 --- a/tests/integration-tests/packager.test.ts +++ b/tests/integration-tests/packager.test.ts @@ -112,7 +112,7 @@ describe.runIf(!isWindows)('packager integration', () => { filterOutUntrustedFiles, }); }, - handleOutput: writeOutputToDisk, + writeOutputToDisk, copyToClipboardIfEnabled, calculateMetrics: async (processedFiles, output, progressCallback, config, gitDiffResult) => { return { From de3e18d6710d93557ebf0b02a214df33ff9faa55 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Wed, 23 Jul 2025 00:05:23 +0900 Subject: [PATCH 08/13] feat(shared): Add ErrorOptions support to RepomixError classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable error chaining by accepting ErrorOptions parameter in RepomixError and RepomixConfigValidationError constructors. Enhanced error handler to display cause information during debugging. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/shared/errorHandle.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/shared/errorHandle.ts b/src/shared/errorHandle.ts index 922f060d2..c41c10425 100644 --- a/src/shared/errorHandle.ts +++ b/src/shared/errorHandle.ts @@ -3,15 +3,15 @@ import { REPOMIX_DISCORD_URL, REPOMIX_ISSUES_URL } from './constants.js'; import { logger, repomixLogLevels } from './logger.js'; export class RepomixError extends Error { - constructor(message: string) { - super(message); + constructor(message: string, options?: ErrorOptions) { + super(message, options); this.name = 'RepomixError'; } } export class RepomixConfigValidationError extends RepomixError { - constructor(message: string) { - super(message); + constructor(message: string, options?: ErrorOptions) { + super(message, options); this.name = 'RepomixConfigValidationError'; } } @@ -21,8 +21,16 @@ export const handleError = (error: unknown): void => { if (error instanceof RepomixError) { logger.error(`✖ ${error.message}`); + if (logger.getLogLevel() < repomixLogLevels.DEBUG) { + logger.log(''); + logger.note('For detailed debug information, use the --verbose flag'); + } // If expected error, show stack trace for debugging - logger.debug('Stack trace:', error.stack); + logger.note('Stack trace:', error.stack); + // Show cause if available + if (error.cause) { + logger.note('Caused by:', error.cause); + } } else if (error instanceof Error) { logger.error(`✖ Unexpected error: ${error.message}`); // If unexpected error, show stack trace by default @@ -37,6 +45,7 @@ export const handleError = (error: unknown): void => { logger.error('✖ An unknown error occurred'); if (logger.getLogLevel() < repomixLogLevels.DEBUG) { + logger.log(''); logger.note('For detailed debug information, use the --verbose flag'); } } From 2f3c89175871f32201f34058446799ea1ee3eb67 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Wed, 23 Jul 2025 00:05:33 +0900 Subject: [PATCH 09/13] fix(core): Handle 'Invalid string length' error with user-friendly message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add specific error handling for JavaScript string size limit (~512MB) in Handlebars template compilation. Provides actionable guidance to use --include flag for processing specific directories. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/core/output/outputGenerate.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/core/output/outputGenerate.ts b/src/core/output/outputGenerate.ts index 14aed0cf8..8bbea1e31 100644 --- a/src/core/output/outputGenerate.ts +++ b/src/core/output/outputGenerate.ts @@ -94,6 +94,7 @@ const generateParsableXmlOutput = async (renderContext: RenderContext): Promise< } catch (error) { throw new RepomixError( `Failed to generate XML output: ${error instanceof Error ? error.message : 'Unknown error'}`, + error instanceof Error ? { cause: error } : undefined, ); } }; @@ -118,7 +119,16 @@ const generateHandlebarOutput = async (config: RepomixConfigMerged, renderContex const compiledTemplate = Handlebars.compile(template); return `${compiledTemplate(renderContext).trim()}\n`; } catch (error) { - throw new RepomixError(`Failed to compile template: ${error instanceof Error ? error.message : 'Unknown error'}`); + if (error instanceof RangeError && error.message === 'Invalid string length') { + throw new RepomixError( + 'Output size exceeds JavaScript string limit (~512MB). Consider using --include to process specific directories or files.', + { cause: error }, + ); + } + throw new RepomixError( + `Failed to compile template: ${error instanceof Error ? error.message : 'Unknown error'}`, + error instanceof Error ? { cause: error } : undefined, + ); } }; From 6290ec9f952ba0327d0a930559b445c58ebcf083 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Wed, 23 Jul 2025 00:07:03 +0900 Subject: [PATCH 10/13] fix(core): Improve error message to mention both --include and --ignore options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update 'Invalid string length' error message to suggest both --include and --ignore flags as solutions for handling large repositories. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/core/output/outputGenerate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/output/outputGenerate.ts b/src/core/output/outputGenerate.ts index 8bbea1e31..94d225859 100644 --- a/src/core/output/outputGenerate.ts +++ b/src/core/output/outputGenerate.ts @@ -121,7 +121,7 @@ const generateHandlebarOutput = async (config: RepomixConfigMerged, renderContex } catch (error) { if (error instanceof RangeError && error.message === 'Invalid string length') { throw new RepomixError( - 'Output size exceeds JavaScript string limit (~512MB). Consider using --include to process specific directories or files.', + 'Output size exceeds JavaScript string limit (~512MB). Consider using --include to process specific directories or --ignore to exclude unnecessary files.', { cause: error }, ); } From 4b4fb4ec447e35d504828c43ac77fec7302d1199 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Wed, 23 Jul 2025 00:09:59 +0900 Subject: [PATCH 11/13] fix(core): Improve 'Invalid string length' error message with clearer guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enhanced error message to be more user-friendly: - Clearly explain the issue (repository contains files too large to process) - Provide concrete examples for --ignore usage - Structure solutions in easy-to-follow bullet points - Add option to process smaller portions of the repository 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/core/output/outputGenerate.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/output/outputGenerate.ts b/src/core/output/outputGenerate.ts index 94d225859..4f1727e39 100644 --- a/src/core/output/outputGenerate.ts +++ b/src/core/output/outputGenerate.ts @@ -121,7 +121,11 @@ const generateHandlebarOutput = async (config: RepomixConfigMerged, renderContex } catch (error) { if (error instanceof RangeError && error.message === 'Invalid string length') { throw new RepomixError( - 'Output size exceeds JavaScript string limit (~512MB). Consider using --include to process specific directories or --ignore to exclude unnecessary files.', + 'Output size exceeds JavaScript string limit. The repository contains files that are too large to process.\n' + + 'Please try:\n' + + ' - Use --ignore to exclude large files (e.g., --ignore "docs/**" or --ignore "*.html")\n' + + ' - Use --include to process only specific files\n' + + ' - Process smaller portions of the repository at a time', { cause: error }, ); } From 287cceaaa6429cd270d315e64aa983db1a79e505 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Wed, 23 Jul 2025 00:12:17 +0900 Subject: [PATCH 12/13] fix(shared): Show stack trace only in debug mode for RepomixError MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed RepomixError stack trace output from logger.note to logger.debug to reduce noise in standard error output. Stack traces are now only shown when verbose flag is used. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/shared/errorHandle.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/errorHandle.ts b/src/shared/errorHandle.ts index c41c10425..7ed3d0f11 100644 --- a/src/shared/errorHandle.ts +++ b/src/shared/errorHandle.ts @@ -26,10 +26,10 @@ export const handleError = (error: unknown): void => { logger.note('For detailed debug information, use the --verbose flag'); } // If expected error, show stack trace for debugging - logger.note('Stack trace:', error.stack); + logger.debug('Stack trace:', error.stack); // Show cause if available if (error.cause) { - logger.note('Caused by:', error.cause); + logger.debug('Caused by:', error.cause); } } else if (error instanceof Error) { logger.error(`✖ Unexpected error: ${error.message}`); From 34685042be178661b1448b90f95333573c4b54b6 Mon Sep 17 00:00:00 2001 From: Kazuki Yamada Date: Wed, 23 Jul 2025 00:29:21 +0900 Subject: [PATCH 13/13] feat(core): Show largest files in 'Invalid string length' error message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When output size exceeds JavaScript string limit, now displays the top 5 largest files with their sizes to help users identify which files to exclude. This makes it easier to decide which --ignore patterns to use. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/core/output/outputGenerate.ts | 32 +++++++++++++------ tests/core/output/outputGenerateDiffs.test.ts | 8 ++--- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/core/output/outputGenerate.ts b/src/core/output/outputGenerate.ts index 4f1727e39..38fd35f11 100644 --- a/src/core/output/outputGenerate.ts +++ b/src/core/output/outputGenerate.ts @@ -99,7 +99,11 @@ const generateParsableXmlOutput = async (renderContext: RenderContext): Promise< } }; -const generateHandlebarOutput = async (config: RepomixConfigMerged, renderContext: RenderContext): Promise => { +const generateHandlebarOutput = async ( + config: RepomixConfigMerged, + renderContext: RenderContext, + processedFiles?: ProcessedFile[], +): Promise => { let template: string; switch (config.output.style) { case 'xml': @@ -120,12 +124,22 @@ const generateHandlebarOutput = async (config: RepomixConfigMerged, renderContex return `${compiledTemplate(renderContext).trim()}\n`; } catch (error) { if (error instanceof RangeError && error.message === 'Invalid string length') { + let largeFilesInfo = ''; + if (processedFiles && processedFiles.length > 0) { + const topFiles = processedFiles + .sort((a, b) => b.content.length - a.content.length) + .slice(0, 5) + .map((f) => ` - ${f.path} (${(f.content.length / 1024 / 1024).toFixed(1)} MB)`) + .join('\n'); + largeFilesInfo = `\n\nLargest files in this repository:\n${topFiles}`; + } + throw new RepomixError( - 'Output size exceeds JavaScript string limit. The repository contains files that are too large to process.\n' + - 'Please try:\n' + - ' - Use --ignore to exclude large files (e.g., --ignore "docs/**" or --ignore "*.html")\n' + - ' - Use --include to process only specific files\n' + - ' - Process smaller portions of the repository at a time', + `Output size exceeds JavaScript string limit. The repository contains files that are too large to process. +Please try: + - Use --ignore to exclude large files (e.g., --ignore "docs/**" or --ignore "*.html") + - Use --include to process only specific files + - Process smaller portions of the repository at a time${largeFilesInfo}`, { cause: error }, ); } @@ -161,14 +175,14 @@ export const generateOutput = async ( ); const renderContext = createRenderContext(outputGeneratorContext); - if (!config.output.parsableStyle) return deps.generateHandlebarOutput(config, renderContext); + if (!config.output.parsableStyle) return deps.generateHandlebarOutput(config, renderContext, sortedProcessedFiles); switch (config.output.style) { case 'xml': return deps.generateParsableXmlOutput(renderContext); case 'markdown': - return deps.generateHandlebarOutput(config, renderContext); + return deps.generateHandlebarOutput(config, renderContext, sortedProcessedFiles); default: - return deps.generateHandlebarOutput(config, renderContext); + return deps.generateHandlebarOutput(config, renderContext, sortedProcessedFiles); } }; diff --git a/tests/core/output/outputGenerateDiffs.test.ts b/tests/core/output/outputGenerateDiffs.test.ts index 890e0cb53..987fc348b 100644 --- a/tests/core/output/outputGenerateDiffs.test.ts +++ b/tests/core/output/outputGenerateDiffs.test.ts @@ -70,7 +70,7 @@ describe('Output Generation with Diffs', () => { mockConfig.output.parsableStyle = false; // Mock the Handlebars output function to check for diffs in the template - mockDeps.generateHandlebarOutput.mockImplementation((config, renderContext: RenderContext) => { + mockDeps.generateHandlebarOutput.mockImplementation((config, renderContext: RenderContext, processedFiles) => { // Verify that the renderContext has the gitDiffs property expect(renderContext.gitDiffWorkTree).toBe(sampleDiff); @@ -129,7 +129,7 @@ describe('Output Generation with Diffs', () => { mockConfig.output.parsableStyle = false; // Mock the Handlebars output function for markdown - mockDeps.generateHandlebarOutput.mockImplementation((config, renderContext: RenderContext) => { + mockDeps.generateHandlebarOutput.mockImplementation((config, renderContext: RenderContext, processedFiles) => { // Verify that the renderContext has the gitDiffs property expect(renderContext.gitDiffWorkTree).toBe(sampleDiff); @@ -156,7 +156,7 @@ describe('Output Generation with Diffs', () => { mockConfig.output.parsableStyle = false; // Mock the Handlebars output function for plain text - mockDeps.generateHandlebarOutput.mockImplementation((config, renderContext: RenderContext) => { + mockDeps.generateHandlebarOutput.mockImplementation((config, renderContext: RenderContext, processedFiles) => { expect(renderContext.gitDiffWorkTree).toBe(sampleDiff); // Simulate the plain text output @@ -191,7 +191,7 @@ describe('Output Generation with Diffs', () => { })); // Mock the Handlebars output function - mockDeps.generateHandlebarOutput.mockImplementation((config, renderContext: RenderContext) => { + mockDeps.generateHandlebarOutput.mockImplementation((config, renderContext: RenderContext, processedFiles) => { // Verify that the renderContext does not have the gitDiffs property expect(renderContext.gitDiffWorkTree).toBeUndefined();