Files
repomix-mirror/tests/shared/logger.test.ts
Kazuki Yamada e5f7a1f311 fix(shared): Address PR review feedback
- shared/errorHandle: recognize duck-typed OperationCancelledError from
  worker boundaries in isRepomixError (it extends RepomixError but the
  name was missing from the structured-clone fallback comparison).
  Add a regression test for the worker-boundary case.

Test improvements per coderabbit / claude review:
- cliReport: assert skill-directory + relative path on the same log line.
- processConcurrency: restore process.versions.bun by removing the property
  when it didn't originally exist, instead of leaving it defined-as-undefined.
- logger: drop the no-op `process.env.REPOMIX_LOG_LEVEL = undefined` (it
  coerces to the string "undefined" and is overwritten by the next delete).
- unifiedWorker: replace the tautological cache test with one that proves
  cache uniqueness via onWorkerTermination cleanup count; add a test for
  task-based inference overriding workerData (bundled-env reuse).
- calculateMetricsWorker: new direct test for the default export's items
  vs. single-mode dispatch — unifiedWorker mocks this module so the branch
  was otherwise untested.
- packRemoteRepositoryTool: hard-code the expected output path instead of
  expect.any(String) to catch arg-swap regressions.
- memoryUtils: tighten getMemoryStats assertions with sanity bounds
  (heapUsed <= heapTotal, rss > 0, heapUsagePercent <= 100) so a
  unit-conversion regression (bytes vs MB) would fail the test.
2026-04-26 22:20:42 +09:00

202 lines
6.5 KiB
TypeScript

import pc from 'picocolors';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { logger, repomixLogLevels } from '../../src/shared/logger.js';
vi.mock('picocolors', () => ({
default: {
red: vi.fn((str) => `RED:${str}`),
yellow: vi.fn((str) => `YELLOW:${str}`),
green: vi.fn((str) => `GREEN:${str}`),
cyan: vi.fn((str) => `CYAN:${str}`),
dim: vi.fn((str) => `DIM:${str}`),
blue: vi.fn((str) => `BLUE:${str}`),
gray: vi.fn((str) => `GRAY:${str}`),
},
}));
describe('logger', () => {
beforeEach(() => {
vi.spyOn(console, 'error').mockImplementation(vi.fn());
vi.spyOn(console, 'log').mockImplementation(vi.fn());
logger.init();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('log levels', () => {
it('should not log anything in SILENT mode', () => {
logger.setLogLevel(repomixLogLevels.SILENT);
logger.error('Error message');
logger.warn('Warning message');
logger.success('Success message');
logger.info('Info message');
logger.note('Note message');
logger.debug('Debug message');
logger.trace('Trace message');
logger.log('Log message');
expect(console.error).not.toHaveBeenCalled();
expect(console.log).not.toHaveBeenCalled();
});
it('should only log errors in ERROR mode', () => {
logger.setLogLevel(repomixLogLevels.ERROR);
logger.error('Error message');
logger.warn('Warning message');
expect(console.error).toHaveBeenCalledWith('RED:Error message');
expect(console.log).not.toHaveBeenCalled();
});
});
it('should log error messages', () => {
logger.error('Error message');
expect(console.error).toHaveBeenCalledWith('RED:Error message');
});
it('should log warning messages', () => {
logger.warn('Warning message');
expect(console.log).toHaveBeenCalledWith('YELLOW:Warning message');
});
it('should log success messages', () => {
logger.success('Success message');
expect(console.log).toHaveBeenCalledWith('GREEN:Success message');
});
it('should log info messages', () => {
logger.info('Info message');
expect(console.log).toHaveBeenCalledWith('CYAN:Info message');
});
it('should log log messages', () => {
logger.log('Note message');
expect(console.log).toHaveBeenCalledWith('Note message');
});
it('should not log debug messages when verbose is false', () => {
logger.debug('Debug message');
expect(console.log).not.toHaveBeenCalled();
});
it('should log debug messages when verbose is true', () => {
logger.setLogLevel(repomixLogLevels.DEBUG);
logger.debug('Debug message');
expect(console.log).toHaveBeenCalledWith('BLUE:Debug message');
});
it('should not log trace messages when verbose is false', () => {
logger.trace('Trace message');
expect(console.log).not.toHaveBeenCalled();
});
it('should log trace messages when verbose is true', () => {
logger.setLogLevel(repomixLogLevels.DEBUG);
logger.trace('Trace message');
expect(console.log).toHaveBeenCalledWith(pc.gray('Trace message'));
});
it('should format object arguments correctly', () => {
const obj = { key: 'value' };
logger.info('Object:', obj);
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('CYAN:Object: '));
});
it('should handle multiple arguments', () => {
logger.info('Multiple', 'arguments', 123);
expect(console.log).toHaveBeenCalledWith('CYAN:Multiple arguments 123');
});
});
describe('setLogLevelByWorkerData', () => {
// setLogLevelByWorkerData reads `workerData` (captured at module load) and
// process.env at call time. Re-import per case via vi.resetModules so each
// test sees a fresh module with its own mocked workerData.
const originalLevel = logger.getLogLevel();
const originalEnvLogLevel = process.env.REPOMIX_LOG_LEVEL;
beforeEach(() => {
delete process.env.REPOMIX_LOG_LEVEL;
});
afterEach(() => {
vi.doUnmock('node:worker_threads');
vi.resetModules();
logger.setLogLevel(originalLevel);
if (originalEnvLogLevel === undefined) {
delete process.env.REPOMIX_LOG_LEVEL;
} else {
process.env.REPOMIX_LOG_LEVEL = originalEnvLogLevel;
}
});
it('sets log level from REPOMIX_LOG_LEVEL env var', async () => {
process.env.REPOMIX_LOG_LEVEL = String(repomixLogLevels.DEBUG);
const { setLogLevelByWorkerData, logger: freshLogger } = await import('../../src/shared/logger.js');
setLogLevelByWorkerData();
expect(freshLogger.getLogLevel()).toBe(repomixLogLevels.DEBUG);
});
it('ignores non-numeric REPOMIX_LOG_LEVEL', async () => {
process.env.REPOMIX_LOG_LEVEL = 'not-a-number';
const { setLogLevelByWorkerData, logger: freshLogger } = await import('../../src/shared/logger.js');
const before = freshLogger.getLogLevel();
setLogLevelByWorkerData();
expect(freshLogger.getLogLevel()).toBe(before);
});
it('ignores out-of-range REPOMIX_LOG_LEVEL', async () => {
process.env.REPOMIX_LOG_LEVEL = '999';
const { setLogLevelByWorkerData, logger: freshLogger } = await import('../../src/shared/logger.js');
const before = freshLogger.getLogLevel();
setLogLevelByWorkerData();
expect(freshLogger.getLogLevel()).toBe(before);
});
it('falls back to workerData when env var is unset', async () => {
vi.resetModules();
vi.doMock('node:worker_threads', () => ({
workerData: ['some-task', { logLevel: repomixLogLevels.ERROR }],
}));
const { setLogLevelByWorkerData, logger: freshLogger } = await import('../../src/shared/logger.js');
setLogLevelByWorkerData();
expect(freshLogger.getLogLevel()).toBe(repomixLogLevels.ERROR);
});
it('ignores invalid logLevel in workerData', async () => {
vi.resetModules();
vi.doMock('node:worker_threads', () => ({
workerData: ['some-task', { logLevel: 42 }],
}));
const { setLogLevelByWorkerData, logger: freshLogger } = await import('../../src/shared/logger.js');
const before = freshLogger.getLogLevel();
setLogLevelByWorkerData();
expect(freshLogger.getLogLevel()).toBe(before);
});
it('does nothing when workerData is null and env var is unset', async () => {
vi.resetModules();
vi.doMock('node:worker_threads', () => ({ workerData: null }));
const { setLogLevelByWorkerData, logger: freshLogger } = await import('../../src/shared/logger.js');
const before = freshLogger.getLogLevel();
setLogLevelByWorkerData();
expect(freshLogger.getLogLevel()).toBe(before);
});
});