Files
wehub-resource-sync 719032b19f
Update Schema / Update configuration json schema (push) Has been cancelled
Memory Benchmark / Memory Test (Full Analysis) (push) Has been cancelled
CI Quality / Lint GitHub Actions (actionlint) (push) Has been cancelled
CI Quality / Lint GitHub Actions (zizmor) (push) Has been cancelled
CI Quality / Check typos (push) Has been cancelled
CI / Test coverage (push) Has been cancelled
CI / Test (22.x, windows-latest) (push) Has been cancelled
CI / Test (24.x, macos-latest) (push) Has been cancelled
CI / Test (24.x, ubuntu-latest) (push) Has been cancelled
CI / Test (24.x, windows-latest) (push) Has been cancelled
CI / Test (26.x, macos-latest) (push) Has been cancelled
CI / Test (26.x, ubuntu-latest) (push) Has been cancelled
CI / Test (26.x, windows-latest) (push) Has been cancelled
CI / Test with Bun (latest, macos-latest) (push) Has been cancelled
CI / Test with Bun (latest, ubuntu-latest) (push) Has been cancelled
CI / Test with Bun (latest, windows-latest) (push) Has been cancelled
CI / Build and run (22.x, macos-latest) (push) Has been cancelled
CI / Build and run (22.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (22.x, windows-latest) (push) Has been cancelled
autofix.ci / autofix (push) Has been cancelled
CI Browser Extension / Lint Browser Extension (push) Has been cancelled
CI Browser Extension / Test Browser Extension (push) Has been cancelled
Memory Benchmark / Memory Test (push) Has been cancelled
CI Website / Lint Website Client (push) Has been cancelled
CI Website / Lint Website Server (push) Has been cancelled
CI Website / Test Website Server (push) Has been cancelled
CI Website / Bundle Website Server (push) Has been cancelled
CI / Lint Biome (push) Has been cancelled
CI / Lint oxlint (push) Has been cancelled
CI / Lint TypeScript (push) Has been cancelled
CI / Lint Secretlint (push) Has been cancelled
CI / Test (22.x, macos-latest) (push) Has been cancelled
CI / Test (22.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (24.x, macos-latest) (push) Has been cancelled
CI / Build and run (24.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (24.x, windows-latest) (push) Has been cancelled
CI / Build and run (26.x, macos-latest) (push) Has been cancelled
CI / Build and run (26.x, ubuntu-latest) (push) Has been cancelled
CI / Build and run (26.x, windows-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, macos-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, ubuntu-latest) (push) Has been cancelled
CI / Build and run with Bun (latest, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Docker / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Docker / build (linux/arm/v7, ubuntu-24.04-arm) (push) Has been cancelled
Docker / build (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker / merge (push) Has been cancelled
Pack repository with Repomix / pack-repo (push) Has been cancelled
Performance Benchmark History / Benchmark (macos-latest) (push) Has been cancelled
Performance Benchmark History / Benchmark (ubuntu-latest) (push) Has been cancelled
Performance Benchmark History / Benchmark (windows-latest) (push) Has been cancelled
Performance Benchmark History / Store Results (push) Has been cancelled
Test Repomix Action / Test Node.js 22 (push) Has been cancelled
Test Repomix Action / Test Node.js 24 (push) Has been cancelled
Test Repomix Action / Test Node.js 26 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:37 +08:00

259 lines
9.3 KiB
TypeScript

import * as v from 'valibot';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
handleError,
OperationCancelledError,
RepomixConfigValidationError,
RepomixError,
rethrowValidationErrorIfSchemaError,
} from '../../src/shared/errorHandle.js';
import { logger, repomixLogLevels } from '../../src/shared/logger.js';
describe('rethrowValidationErrorIfSchemaError', () => {
it('rethrows ValiError as RepomixConfigValidationError with formatted message', () => {
const schema = v.object({ foo: v.string() });
let caught: unknown;
try {
v.parse(schema, { foo: 42 });
} catch (error) {
caught = error;
}
expect(() => rethrowValidationErrorIfSchemaError(caught, 'Invalid config')).toThrow(RepomixConfigValidationError);
try {
rethrowValidationErrorIfSchemaError(caught, 'Invalid config');
} catch (error) {
expect(error).toBeInstanceOf(RepomixConfigValidationError);
expect((error as Error).message).toContain('Invalid config');
// Valibot path segments are { key } objects — the helper should unwrap them.
expect((error as Error).message).toContain('[foo]');
}
});
it('rethrows ZodError-shaped duck-typed errors as RepomixConfigValidationError', () => {
const zodLike = {
name: 'ZodError',
message: 'Validation failed',
issues: [{ path: ['output', 'style'], message: 'Invalid enum value' }],
};
expect(() => rethrowValidationErrorIfSchemaError(zodLike, 'Invalid cli arguments')).toThrow(
RepomixConfigValidationError,
);
try {
rethrowValidationErrorIfSchemaError(zodLike, 'Invalid cli arguments');
} catch (error) {
expect((error as Error).message).toContain('[output.style]');
expect((error as Error).message).toContain('Invalid enum value');
}
});
it('handles serialized ValiError across worker boundaries (no instanceof Error)', () => {
// Simulate a structured-clone copy — no Error prototype, plain object.
const workerError = {
name: 'ValiError',
message: 'Invalid type',
issues: [{ path: [{ key: 'input' }, { key: 'maxFileSize' }], message: 'Invalid type' }],
};
expect(() => rethrowValidationErrorIfSchemaError(workerError, 'Invalid config')).toThrow(
RepomixConfigValidationError,
);
});
it('does nothing for non-schema errors', () => {
expect(() => rethrowValidationErrorIfSchemaError(new Error('boom'), 'msg')).not.toThrow();
expect(() => rethrowValidationErrorIfSchemaError(null, 'msg')).not.toThrow();
expect(() => rethrowValidationErrorIfSchemaError('string', 'msg')).not.toThrow();
expect(() => rethrowValidationErrorIfSchemaError({ name: 'Other', issues: [] }, 'msg')).not.toThrow();
});
it('filters out empty path segments so the joined path stays clean', () => {
// A malformed path item (object without `key`) should drop out instead of
// producing a double-dot like `[output..style]`.
const malformed = {
name: 'ValiError',
message: 'Invalid type',
issues: [
{
path: [{ key: 'output' }, { type: 'object' }, { key: 'style' }],
message: 'Invalid type',
},
],
};
try {
rethrowValidationErrorIfSchemaError(malformed, 'Invalid config');
expect.fail('Expected RepomixConfigValidationError to be thrown');
} catch (error) {
expect((error as Error).message).toContain('[output.style]');
expect((error as Error).message).not.toContain('..');
}
});
it('omits the bracketed path when a schema issue has no path', () => {
// Root-level issues carry no path; the formatted message should read as
// `message` rather than `[] message`.
const rootIssue = {
name: 'ValiError',
message: 'Root-level failure',
issues: [{ path: [] as unknown[], message: 'Expected object' }],
};
try {
rethrowValidationErrorIfSchemaError(rootIssue, 'Invalid config');
expect.fail('Expected RepomixConfigValidationError to be thrown');
} catch (error) {
const msg = (error as Error).message;
expect(msg).toContain('Expected object');
expect(msg).not.toContain('[]');
}
});
});
describe('error classes', () => {
it('RepomixError sets name and preserves cause', () => {
const cause = new Error('underlying');
const err = new RepomixError('boom', { cause });
expect(err.name).toBe('RepomixError');
expect(err.message).toBe('boom');
expect(err.cause).toBe(cause);
});
it('RepomixConfigValidationError extends RepomixError', () => {
const err = new RepomixConfigValidationError('invalid');
expect(err).toBeInstanceOf(RepomixError);
expect(err.name).toBe('RepomixConfigValidationError');
});
it('OperationCancelledError uses default message', () => {
const err = new OperationCancelledError();
expect(err).toBeInstanceOf(RepomixError);
expect(err.name).toBe('OperationCancelledError');
expect(err.message).toBe('Operation cancelled');
});
});
describe('handleError', () => {
let errorSpy: ReturnType<typeof vi.spyOn>;
let noteSpy: ReturnType<typeof vi.spyOn>;
let infoSpy: ReturnType<typeof vi.spyOn>;
let debugSpy: ReturnType<typeof vi.spyOn>;
const originalLevel = logger.getLogLevel();
beforeEach(() => {
errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {});
noteSpy = vi.spyOn(logger, 'note').mockImplementation(() => {});
vi.spyOn(logger, 'log').mockImplementation(() => {});
infoSpy = vi.spyOn(logger, 'info').mockImplementation(() => {});
debugSpy = vi.spyOn(logger, 'debug').mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
logger.setLogLevel(originalLevel);
});
it('handles RepomixError with verbose hint at non-debug level', () => {
logger.setLogLevel(repomixLogLevels.INFO);
const err = new RepomixError('config invalid');
handleError(err);
expect(errorSpy).toHaveBeenCalledWith('✖ config invalid');
expect(noteSpy).toHaveBeenCalledWith(expect.stringContaining('--verbose'));
expect(infoSpy).toHaveBeenCalledWith('Need help?');
});
it('handles RepomixError without verbose hint at debug level', () => {
logger.setLogLevel(repomixLogLevels.DEBUG);
const err = new RepomixError('config invalid');
handleError(err);
expect(errorSpy).toHaveBeenCalledWith('✖ config invalid');
// Verbose hint is suppressed at DEBUG level
expect(noteSpy).not.toHaveBeenCalledWith(expect.stringContaining('--verbose'));
});
it('logs cause when RepomixError has one', () => {
logger.setLogLevel(repomixLogLevels.DEBUG);
const cause = new Error('root cause');
const err = new RepomixError('outer', { cause });
handleError(err);
expect(debugSpy).toHaveBeenCalledWith('Caused by:', cause);
});
it('handles unexpected (non-Repomix) Error with stack trace', () => {
logger.setLogLevel(repomixLogLevels.INFO);
const err = new Error('something broke');
handleError(err);
expect(errorSpy).toHaveBeenCalledWith('✖ Unexpected error: something broke');
expect(noteSpy).toHaveBeenCalledWith('Stack trace:', err.stack);
expect(noteSpy).toHaveBeenCalledWith(expect.stringContaining('--verbose'));
});
it('handles duck-typed Error from worker boundary', () => {
logger.setLogLevel(repomixLogLevels.INFO);
// Plain object that quacks like an Error (e.g. structured-clone copy)
const workerError = { name: 'TypeError', message: 'invalid arg', stack: 'stack here' };
handleError(workerError);
expect(errorSpy).toHaveBeenCalledWith('✖ Unexpected error: invalid arg');
});
it('handles duck-typed RepomixError from worker boundary', () => {
logger.setLogLevel(repomixLogLevels.INFO);
const workerError = { name: 'RepomixError', message: 'pack failed' };
handleError(workerError);
expect(errorSpy).toHaveBeenCalledWith('✖ pack failed');
});
it('handles duck-typed RepomixConfigValidationError from worker boundary', () => {
logger.setLogLevel(repomixLogLevels.INFO);
const workerError = { name: 'RepomixConfigValidationError', message: 'bad config' };
handleError(workerError);
expect(errorSpy).toHaveBeenCalledWith('✖ bad config');
});
it('handles duck-typed OperationCancelledError from worker boundary', () => {
// OperationCancelledError extends RepomixError. Without the name being
// listed in isRepomixError's duck-typed comparison, a structured-clone
// copy from a worker would fall into the "Unexpected error" branch
// and surface a noisy stack trace for what is actually a user cancel.
logger.setLogLevel(repomixLogLevels.INFO);
const workerError = { name: 'OperationCancelledError', message: 'Operation cancelled' };
handleError(workerError);
expect(errorSpy).toHaveBeenCalledWith('✖ Operation cancelled');
expect(errorSpy).not.toHaveBeenCalledWith(expect.stringContaining('Unexpected error'));
});
it('handles unknown non-Error values via inspect()', () => {
logger.setLogLevel(repomixLogLevels.INFO);
handleError('a thrown string');
expect(errorSpy).toHaveBeenCalledWith('✖ An unknown error occurred');
expect(noteSpy).toHaveBeenCalledWith('Error details:', expect.stringContaining('a thrown string'));
});
it('handles null as unknown error', () => {
logger.setLogLevel(repomixLogLevels.INFO);
handleError(null);
expect(errorSpy).toHaveBeenCalledWith('✖ An unknown error occurred');
});
});