diff --git a/src/cli/actions/workers/defaultActionWorker.ts b/src/cli/actions/workers/defaultActionWorker.ts index 323ebaa84..8c3613543 100644 --- a/src/cli/actions/workers/defaultActionWorker.ts +++ b/src/cli/actions/workers/defaultActionWorker.ts @@ -103,8 +103,14 @@ async function defaultActionWorker( spinner.succeed('Packing completed successfully!'); + // Strip file content from IPC response to reduce structured clone overhead. + // The main process only uses processedFiles[].path (for token count tree), + // not the content (~4MB savings for typical repos). return { - packResult, + packResult: { + ...packResult, + processedFiles: packResult.processedFiles.map((file) => ({ ...file, content: '' })), + }, config, }; } catch (error) { diff --git a/src/cli/cliRun.ts b/src/cli/cliRun.ts index 837de53ca..766fbe547 100644 --- a/src/cli/cliRun.ts +++ b/src/cli/cliRun.ts @@ -2,15 +2,10 @@ import process from 'node:process'; import { Option, program } from 'commander'; import pc from 'picocolors'; import { getVersion } from '../core/file/packageJsonParse.js'; -import { isExplicitRemoteUrl } from '../core/git/gitRemoteParse.js'; import { handleError, RepomixError } from '../shared/errorHandle.js'; import { logger, repomixLogLevels } from '../shared/logger.js'; import { parseHumanSizeToBytes } from '../shared/sizeParse.js'; import { runDefaultAction } from './actions/defaultAction.js'; -import { runInitAction } from './actions/initAction.js'; -import { runMcpAction } from './actions/mcpAction.js'; -import { runRemoteAction } from './actions/remoteAction.js'; -import { runVersionAction } from './actions/versionAction.js'; import type { CliOptions } from './types.js'; // Semantic mapping for CLI suggestions @@ -257,10 +252,12 @@ export const runCli = async (directories: string[], cwd: string, options: CliOpt logger.trace('options:', options); if (options.mcp) { + const { runMcpAction } = await import('./actions/mcpAction.js'); return await runMcpAction(); } if (options.version) { + const { runVersionAction } = await import('./actions/versionAction.js'); await runVersionAction(); return; } @@ -272,17 +269,21 @@ export const runCli = async (directories: string[], cwd: string, options: CliOpt } if (options.init) { + const { runInitAction } = await import('./actions/initAction.js'); await runInitAction(cwd, options.global || false); return; } if (options.remote) { + const { runRemoteAction } = await import('./actions/remoteAction.js'); return await runRemoteAction(options.remote, options); } // Auto-detect explicit remote URLs (https://, git@, ssh://, git://) in positional arguments - if (directories.length === 1 && isExplicitRemoteUrl(directories[0])) { + // Inline prefix check to avoid loading git-url-parse module for non-remote runs + if (directories.length === 1 && ['https://', 'git@', 'ssh://', 'git://'].some((p) => directories[0].startsWith(p))) { logger.trace(`Auto-detected remote URL from positional argument: ${directories[0]}`); + const { runRemoteAction } = await import('./actions/remoteAction.js'); return await runRemoteAction(directories[0], options); } diff --git a/src/cli/cliSpinner.ts b/src/cli/cliSpinner.ts index a499965d3..2143602fb 100644 --- a/src/cli/cliSpinner.ts +++ b/src/cli/cliSpinner.ts @@ -1,4 +1,3 @@ -import logUpdate from 'log-update'; import pc from 'picocolors'; import type { CliOptions } from './types.js'; @@ -6,6 +5,9 @@ import type { CliOptions } from './types.js'; const dotsFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; const dotsInterval = 80; +// ANSI escape: erase current line + carriage return (replaces log-update dependency) +const CLEAR_LINE = '\x1B[2K\r'; + export class Spinner { private message: string; private currentFrame = 0; @@ -28,7 +30,7 @@ export class Spinner { this.interval = setInterval(() => { this.currentFrame++; const frame = dotsFrames[this.currentFrame % framesLength]; - logUpdate(`${pc.cyan(frame)} ${this.message}`); + process.stderr.write(`${CLEAR_LINE}${pc.cyan(frame)} ${this.message}`); }, dotsInterval); } @@ -49,8 +51,7 @@ export class Spinner { clearInterval(this.interval); this.interval = null; } - logUpdate(finalMessage); - logUpdate.done(); + process.stderr.write(`${CLEAR_LINE}${finalMessage}\n`); } succeed(message: string): void { diff --git a/src/core/file/fileTreeGenerate.ts b/src/core/file/fileTreeGenerate.ts index a79223b38..532181e8c 100644 --- a/src/core/file/fileTreeGenerate.ts +++ b/src/core/file/fileTreeGenerate.ts @@ -32,6 +32,9 @@ export const generateFileTree = (files: string[], emptyDirPaths: string[] = []): addPathToTree(root, dir, true); } + // Sort once after tree construction instead of on every treeToString call + sortTreeNodes(root); + return root; }; @@ -68,20 +71,43 @@ const sortTreeNodes = (node: TreeNode) => { } }; +const treeToStringInner = (node: TreeNode, prefix: string, parts: string[]): void => { + for (const child of node.children) { + parts.push(prefix, child.name, child.isDirectory ? '/\n' : '\n'); + if (child.isDirectory) { + treeToStringInner(child, `${prefix} `, parts); + } + } +}; + export const treeToString = (node: TreeNode, prefix = '', _isRoot = true): string => { if (_isRoot) { sortTreeNodes(node); } - let result = ''; + const parts: string[] = []; + treeToStringInner(node, prefix, parts); + return parts.join(''); +}; +const treeToStringWithLineCountsInner = ( + node: TreeNode, + lineCounts: Record, + prefix: string, + currentPath: string, + parts: string[], +): void => { for (const child of node.children) { - result += `${prefix}${child.name}${child.isDirectory ? '/' : ''}\n`; + const childPath = currentPath ? `${currentPath}/${child.name}` : child.name; + if (child.isDirectory) { - result += treeToString(child, `${prefix} `, false); + parts.push(prefix, child.name, '/\n'); + treeToStringWithLineCountsInner(child, lineCounts, `${prefix} `, childPath, parts); + } else { + const lineCount = lineCounts[childPath]; + const lineCountSuffix = lineCount !== undefined ? ` (${lineCount} lines)` : ''; + parts.push(prefix, child.name, lineCountSuffix, '\n'); } } - - return result; }; /** @@ -101,22 +127,9 @@ export const treeToStringWithLineCounts = ( if (_isRoot) { sortTreeNodes(node); } - let result = ''; - - for (const child of node.children) { - const childPath = currentPath ? `${currentPath}/${child.name}` : child.name; - - if (child.isDirectory) { - result += `${prefix}${child.name}/\n`; - result += treeToStringWithLineCounts(child, lineCounts, `${prefix} `, childPath, false); - } else { - const lineCount = lineCounts[childPath]; - const lineCountSuffix = lineCount !== undefined ? ` (${lineCount} lines)` : ''; - result += `${prefix}${child.name}${lineCountSuffix}\n`; - } - } - - return result; + const parts: string[] = []; + treeToStringWithLineCountsInner(node, lineCounts, prefix, currentPath, parts); + return parts.join(''); }; export const generateTreeString = (files: string[], emptyDirPaths: string[] = []): string => { diff --git a/src/core/file/truncateBase64.ts b/src/core/file/truncateBase64.ts index 212eb2e9d..15a57b0af 100644 --- a/src/core/file/truncateBase64.ts +++ b/src/core/file/truncateBase64.ts @@ -5,6 +5,18 @@ const TRUNCATION_LENGTH = 32; const MIN_CHAR_DIVERSITY = 10; const MIN_CHAR_TYPE_COUNT = 3; +// Pre-compiled regex patterns (hoisted to module scope to avoid recompilation per file) +const dataUriPattern = new RegExp( + `data:([a-zA-Z0-9\\/\\-\\+]+)(;[a-zA-Z0-9\\-=]+)*;base64,([A-Za-z0-9+/=]{${MIN_BASE64_LENGTH_DATA_URI},})`, + 'g', +); +const standaloneBase64Pattern = new RegExp(`([A-Za-z0-9+/]{${MIN_BASE64_LENGTH_STANDALONE},}={0,2})`, 'g'); +const base64ValidCharsPattern = /^[A-Za-z0-9+/]+=*$/; +const hasNumbersPattern = /[0-9]/; +const hasUpperCasePattern = /[A-Z]/; +const hasLowerCasePattern = /[a-z]/; +const hasSpecialCharsPattern = /[+/]/; + /** * Truncates base64 encoded data in content to reduce file size * Detects common base64 patterns like data URIs and standalone base64 strings @@ -13,25 +25,16 @@ const MIN_CHAR_TYPE_COUNT = 3; * @returns Content with base64 data truncated */ export const truncateBase64Content = (content: string): string => { - // Pattern to match data URIs (e.g., data:image/png;base64,...) - const dataUriPattern = new RegExp( - `data:([a-zA-Z0-9\\/\\-\\+]+)(;[a-zA-Z0-9\\-=]+)*;base64,([A-Za-z0-9+/=]{${MIN_BASE64_LENGTH_DATA_URI},})`, - 'g', - ); - - // Pattern to match standalone base64 strings - // This matches base64 strings that are likely encoded binary data - const standaloneBase64Pattern = new RegExp(`([A-Za-z0-9+/]{${MIN_BASE64_LENGTH_STANDALONE},}={0,2})`, 'g'); - let processedContent = content; - // Replace data URIs + // Reset lastIndex for global regexes before each use + dataUriPattern.lastIndex = 0; processedContent = processedContent.replace(dataUriPattern, (_match, mimeType, params, base64Data) => { const preview = base64Data.substring(0, TRUNCATION_LENGTH); return `data:${mimeType}${params || ''};base64,${preview}...`; }); - // Replace standalone base64 strings + standaloneBase64Pattern.lastIndex = 0; processedContent = processedContent.replace(standaloneBase64Pattern, (match, base64String) => { // Check if this looks like actual base64 (not just a long string) if (isLikelyBase64(base64String)) { @@ -52,7 +55,7 @@ export const truncateBase64Content = (content: string): string => { */ function isLikelyBase64(str: string): boolean { // Check for valid base64 characters only - if (!/^[A-Za-z0-9+/]+=*$/.test(str)) { + if (!base64ValidCharsPattern.test(str)) { return false; } @@ -64,10 +67,10 @@ function isLikelyBase64(str: string): boolean { // Additional check: base64 encoded binary data typically has good character distribution // Must have at least MIN_CHAR_TYPE_COUNT of the 4 character types (numbers, uppercase, lowercase, special) - const hasNumbers = /[0-9]/.test(str); - const hasUpperCase = /[A-Z]/.test(str); - const hasLowerCase = /[a-z]/.test(str); - const hasSpecialChars = /[+/]/.test(str); + const hasNumbers = hasNumbersPattern.test(str); + const hasUpperCase = hasUpperCasePattern.test(str); + const hasLowerCase = hasLowerCasePattern.test(str); + const hasSpecialChars = hasSpecialCharsPattern.test(str); const charTypeCount = [hasNumbers, hasUpperCase, hasLowerCase, hasSpecialChars].filter(Boolean).length; diff --git a/src/core/git/gitRepositoryHandle.ts b/src/core/git/gitRepositoryHandle.ts index b3a159066..e4708df41 100644 --- a/src/core/git/gitRepositoryHandle.ts +++ b/src/core/git/gitRepositoryHandle.ts @@ -24,18 +24,36 @@ export const getFileChangeCount = async ( } }; +// Promise-based cache to deduplicate concurrent isGitRepository calls +// (e.g., when getGitDiffs and getGitLogs run in parallel via Promise.all) +const isGitRepoCache = new Map>(); + export const isGitRepository = async ( directory: string, deps = { execGitRevParse, }, ): Promise => { - try { - await deps.execGitRevParse(directory); - return true; - } catch { - return false; + // Only use cache with default deps (skip for test mocks) + const useCache = deps.execGitRevParse === execGitRevParse; + + if (useCache) { + const cached = isGitRepoCache.get(directory); + if (cached) { + return cached; + } } + + const promise = deps.execGitRevParse(directory).then( + () => true, + () => false, + ); + + if (useCache) { + isGitRepoCache.set(directory, promise); + } + + return promise; }; export const isGitInstalled = async ( diff --git a/src/core/output/outputGenerate.ts b/src/core/output/outputGenerate.ts index 949e0267f..383b860e7 100644 --- a/src/core/output/outputGenerate.ts +++ b/src/core/output/outputGenerate.ts @@ -59,17 +59,24 @@ const calculateMarkdownDelimiter = (files: ReadonlyArray): string return '`'.repeat(Math.max(3, maxBackticks + 1)); }; +const countNewlines = (str: string): number => { + let count = 0; + let pos = str.indexOf('\n'); + while (pos !== -1) { + count++; + pos = str.indexOf('\n', pos + 1); + } + return count; +}; + const calculateFileLineCounts = (processedFiles: ProcessedFile[]): Record => { const lineCounts: Record = {}; for (const file of processedFiles) { - // Count lines: empty files have 0 lines, otherwise count newlines + 1 - // (unless the content ends with a newline, in which case the last "line" is empty) const content = file.content; if (content.length === 0) { lineCounts[file.path] = 0; } else { - // Count actual lines (text editor style: number of \n + 1, but trailing \n doesn't add extra line) - const newlineCount = (content.match(/\n/g) || []).length; + const newlineCount = countNewlines(content); lineCounts[file.path] = content.endsWith('\n') ? newlineCount : newlineCount + 1; } } diff --git a/src/core/packager.ts b/src/core/packager.ts index 2bf3c01e2..501840ad3 100644 --- a/src/core/packager.ts +++ b/src/core/packager.ts @@ -105,13 +105,12 @@ export const pack = async ( const rawFiles = collectResults.flatMap((curr) => curr.rawFiles); const allSkippedFiles = collectResults.flatMap((curr) => curr.skippedFiles); - // Get git diffs if enabled - run this before security check - progressCallback('Getting git diffs...'); - const gitDiffResult = await deps.getGitDiffs(rootDirs, config); - - // Get git logs if enabled - run this before security check - progressCallback('Getting git logs...'); - const gitLogResult = await deps.getGitLogs(rootDirs, config); + // Get git diffs and logs in parallel - both are independent subprocess calls + progressCallback('Getting git information...'); + const [gitDiffResult, gitLogResult] = await Promise.all([ + deps.getGitDiffs(rootDirs, config), + deps.getGitLogs(rootDirs, config), + ]); // Run security check and get filtered safe files const { safeFilePaths, safeRawFiles, suspiciousFilesResults, suspiciousGitDiffResults, suspiciousGitLogResults } = diff --git a/src/core/security/filterOutUntrustedFiles.ts b/src/core/security/filterOutUntrustedFiles.ts index 06fcfa0d6..ea2fe6ac7 100644 --- a/src/core/security/filterOutUntrustedFiles.ts +++ b/src/core/security/filterOutUntrustedFiles.ts @@ -4,5 +4,7 @@ import type { SuspiciousFileResult } from './securityCheck.js'; export const filterOutUntrustedFiles = ( rawFiles: RawFile[], suspiciousFilesResults: SuspiciousFileResult[], -): RawFile[] => - rawFiles.filter((rawFile) => !suspiciousFilesResults.some((result) => result.filePath === rawFile.path)); +): RawFile[] => { + const suspiciousPaths = new Set(suspiciousFilesResults.map((result) => result.filePath)); + return rawFiles.filter((rawFile) => !suspiciousPaths.has(rawFile.path)); +}; diff --git a/src/core/security/validateFileSafety.ts b/src/core/security/validateFileSafety.ts index ee61d0c9a..aa309fb54 100644 --- a/src/core/security/validateFileSafety.ts +++ b/src/core/security/validateFileSafety.ts @@ -21,24 +21,36 @@ export const validateFileSafety = async ( filterOutUntrustedFiles, }, ) => { - let suspiciousFilesResults: SuspiciousFileResult[] = []; - let suspiciousGitDiffResults: SuspiciousFileResult[] = []; - let suspiciousGitLogResults: SuspiciousFileResult[] = []; + const suspiciousFilesResults: SuspiciousFileResult[] = []; + const suspiciousGitDiffResults: SuspiciousFileResult[] = []; + const suspiciousGitLogResults: SuspiciousFileResult[] = []; if (config.security.enableSecurityCheck) { progressCallback('Running security check...'); const allResults = await deps.runSecurityCheck(rawFiles, progressCallback, gitDiffResult, gitLogResult); - // Separate Git diff and Git log results from regular file results - suspiciousFilesResults = allResults.filter((result) => result.type === 'file'); - suspiciousGitDiffResults = allResults.filter((result) => result.type === 'gitDiff'); - suspiciousGitLogResults = allResults.filter((result) => result.type === 'gitLog'); + // Single-pass partitioning instead of three separate .filter() calls + for (const result of allResults) { + switch (result.type) { + case 'file': + suspiciousFilesResults.push(result); + break; + case 'gitDiff': + suspiciousGitDiffResults.push(result); + break; + case 'gitLog': + suspiciousGitLogResults.push(result); + break; + } + } logSuspiciousContentWarning('Git diffs', suspiciousGitDiffResults); logSuspiciousContentWarning('Git logs', suspiciousGitLogResults); } - const safeRawFiles = deps.filterOutUntrustedFiles(rawFiles, suspiciousFilesResults); + // Short-circuit: skip filtering when no suspicious files found (common case) + const safeRawFiles = + suspiciousFilesResults.length > 0 ? deps.filterOutUntrustedFiles(rawFiles, suspiciousFilesResults) : rawFiles; const safeFilePaths = safeRawFiles.map((file) => file.path); logger.trace('Safe files count:', safeRawFiles.length); diff --git a/tests/cli/actions/workers/defaultActionWorker.test.ts b/tests/cli/actions/workers/defaultActionWorker.test.ts index 7f4df3102..1877d4425 100644 --- a/tests/cli/actions/workers/defaultActionWorker.test.ts +++ b/tests/cli/actions/workers/defaultActionWorker.test.ts @@ -191,7 +191,10 @@ describe('defaultActionWorker', () => { {}, ); expect(result).toEqual({ - packResult: mockPackResult, + packResult: { + ...mockPackResult, + processedFiles: mockPackResult.processedFiles.map((f) => ({ ...f, content: '' })), + }, config: mockConfig, }); }); @@ -217,7 +220,10 @@ describe('defaultActionWorker', () => { {}, ); expect(result).toEqual({ - packResult: mockPackResult, + packResult: { + ...mockPackResult, + processedFiles: mockPackResult.processedFiles.map((f) => ({ ...f, content: '' })), + }, config: mockConfig, }); }); @@ -261,7 +267,10 @@ describe('defaultActionWorker', () => { {}, ); expect(result).toEqual({ - packResult: mockPackResult, + packResult: { + ...mockPackResult, + processedFiles: mockPackResult.processedFiles.map((f) => ({ ...f, content: '' })), + }, config: mockConfig, }); }); diff --git a/tests/cli/cliSpinner.test.ts b/tests/cli/cliSpinner.test.ts index 468089975..bff088ef1 100644 --- a/tests/cli/cliSpinner.test.ts +++ b/tests/cli/cliSpinner.test.ts @@ -2,15 +2,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { Spinner } from '../../src/cli/cliSpinner.js'; import type { CliOptions } from '../../src/cli/types.js'; -// Mock log-update and picocolors -vi.mock('log-update', () => { - const mockFn = vi.fn() as ReturnType & { - done: ReturnType; - }; - mockFn.done = vi.fn(); - return { default: mockFn }; -}); - vi.mock('picocolors', () => ({ default: { cyan: (text: string) => `cyan(${text})`, @@ -20,16 +11,12 @@ vi.mock('picocolors', () => ({ })); describe('cliSpinner', () => { - let mockLogUpdateFn: ReturnType & { done: ReturnType }; - let mockLogUpdateDone: ReturnType; + let stderrWriteSpy: ReturnType; - beforeEach(async () => { + beforeEach(() => { vi.clearAllMocks(); vi.useFakeTimers(); - // Get the mocked module - const logUpdateModule = await import('log-update'); - mockLogUpdateFn = logUpdateModule.default as unknown as typeof mockLogUpdateFn; - mockLogUpdateDone = mockLogUpdateFn.done; + stderrWriteSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); }); afterEach(() => { @@ -45,10 +32,9 @@ describe('cliSpinner', () => { // Advance time to trigger frame updates vi.advanceTimersByTime(80); - expect(mockLogUpdateFn).toHaveBeenCalled(); + expect(stderrWriteSpy).toHaveBeenCalled(); spinner.stop('Done'); - expect(mockLogUpdateDone).toHaveBeenCalled(); }); it('should update spinner message', () => { @@ -58,7 +44,7 @@ describe('cliSpinner', () => { spinner.update('Updated message'); vi.advanceTimersByTime(80); - expect(mockLogUpdateFn).toHaveBeenCalled(); + expect(stderrWriteSpy).toHaveBeenCalled(); spinner.stop('Done'); }); @@ -69,8 +55,7 @@ describe('cliSpinner', () => { spinner.stop('Final message'); - expect(mockLogUpdateFn).toHaveBeenCalledWith('Final message'); - expect(mockLogUpdateDone).toHaveBeenCalled(); + expect(stderrWriteSpy).toHaveBeenCalledWith('\x1B[2K\rFinal message\n'); }); it('should succeed with success message', () => { @@ -79,8 +64,7 @@ describe('cliSpinner', () => { spinner.succeed('Success!'); - expect(mockLogUpdateFn).toHaveBeenCalledWith('green(✔) Success!'); - expect(mockLogUpdateDone).toHaveBeenCalled(); + expect(stderrWriteSpy).toHaveBeenCalledWith('\x1B[2K\rgreen(✔) Success!\n'); }); it('should fail with error message', () => { @@ -89,8 +73,7 @@ describe('cliSpinner', () => { spinner.fail('Failed!'); - expect(mockLogUpdateFn).toHaveBeenCalledWith('red(✖) Failed!'); - expect(mockLogUpdateDone).toHaveBeenCalled(); + expect(stderrWriteSpy).toHaveBeenCalledWith('\x1B[2K\rred(✖) Failed!\n'); }); it('should cycle through animation frames', () => { @@ -102,8 +85,8 @@ describe('cliSpinner', () => { vi.advanceTimersByTime(80); } - expect(mockLogUpdateFn).toHaveBeenCalled(); - expect(mockLogUpdateFn.mock.calls.length).toBeGreaterThan(1); + expect(stderrWriteSpy).toHaveBeenCalled(); + expect(stderrWriteSpy.mock.calls.length).toBeGreaterThan(1); spinner.stop('Complete'); }); @@ -115,7 +98,7 @@ describe('cliSpinner', () => { spinner.start(); vi.advanceTimersByTime(80); - expect(mockLogUpdateFn).not.toHaveBeenCalled(); + expect(stderrWriteSpy).not.toHaveBeenCalled(); spinner.stop('Done'); }); @@ -125,7 +108,7 @@ describe('cliSpinner', () => { spinner.start(); vi.advanceTimersByTime(80); - expect(mockLogUpdateFn).not.toHaveBeenCalled(); + expect(stderrWriteSpy).not.toHaveBeenCalled(); spinner.stop('Done'); }); @@ -135,7 +118,7 @@ describe('cliSpinner', () => { spinner.start(); vi.advanceTimersByTime(80); - expect(mockLogUpdateFn).not.toHaveBeenCalled(); + expect(stderrWriteSpy).not.toHaveBeenCalled(); spinner.stop('Done'); }); @@ -146,7 +129,7 @@ describe('cliSpinner', () => { spinner.update('Updated'); vi.advanceTimersByTime(80); - expect(mockLogUpdateFn).not.toHaveBeenCalled(); + expect(stderrWriteSpy).not.toHaveBeenCalled(); spinner.stop('Done'); }); @@ -156,8 +139,7 @@ describe('cliSpinner', () => { spinner.start(); spinner.stop('Done'); - expect(mockLogUpdateFn).not.toHaveBeenCalled(); - expect(mockLogUpdateDone).not.toHaveBeenCalled(); + expect(stderrWriteSpy).not.toHaveBeenCalled(); }); it('should not show succeed message in quiet mode', () => { @@ -165,8 +147,7 @@ describe('cliSpinner', () => { spinner.start(); spinner.succeed('Success!'); - expect(mockLogUpdateFn).not.toHaveBeenCalled(); - expect(mockLogUpdateDone).not.toHaveBeenCalled(); + expect(stderrWriteSpy).not.toHaveBeenCalled(); }); it('should not show fail message in quiet mode', () => { @@ -174,8 +155,7 @@ describe('cliSpinner', () => { spinner.start(); spinner.fail('Failed!'); - expect(mockLogUpdateFn).not.toHaveBeenCalled(); - expect(mockLogUpdateDone).not.toHaveBeenCalled(); + expect(stderrWriteSpy).not.toHaveBeenCalled(); }); });