mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-20 10:10:56 -07:00
207 lines
5.9 KiB
TypeScript
207 lines
5.9 KiB
TypeScript
|
|
/**
|
||
|
|
* @license
|
||
|
|
* Copyright 2025 Google LLC
|
||
|
|
* SPDX-License-Identifier: Apache-2.0
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
import { getFsErrorMessage } from './fsErrorMessages.js';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Helper to create a mock NodeJS.ErrnoException
|
||
|
|
*/
|
||
|
|
function createNodeError(
|
||
|
|
code: string,
|
||
|
|
message: string,
|
||
|
|
path?: string,
|
||
|
|
): NodeJS.ErrnoException {
|
||
|
|
const error = new Error(message) as NodeJS.ErrnoException;
|
||
|
|
error.code = code;
|
||
|
|
if (path) {
|
||
|
|
error.path = path;
|
||
|
|
}
|
||
|
|
return error;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface FsErrorCase {
|
||
|
|
code: string;
|
||
|
|
message: string;
|
||
|
|
path?: string;
|
||
|
|
expected: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface FallbackErrorCase {
|
||
|
|
value: unknown;
|
||
|
|
expected: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('getFsErrorMessage', () => {
|
||
|
|
describe('known filesystem error codes', () => {
|
||
|
|
const testCases: FsErrorCase[] = [
|
||
|
|
{
|
||
|
|
code: 'EACCES',
|
||
|
|
message: 'EACCES: permission denied',
|
||
|
|
path: '/etc/gemini-cli/settings.json',
|
||
|
|
expected:
|
||
|
|
"Permission denied: cannot access '/etc/gemini-cli/settings.json'. Check file permissions or run with elevated privileges.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'EACCES',
|
||
|
|
message: 'EACCES: permission denied',
|
||
|
|
expected:
|
||
|
|
'Permission denied. Check file permissions or run with elevated privileges.',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'ENOENT',
|
||
|
|
message: 'ENOENT: no such file or directory',
|
||
|
|
path: '/nonexistent/file.txt',
|
||
|
|
expected:
|
||
|
|
"File or directory not found: '/nonexistent/file.txt'. Check if the path exists and is spelled correctly.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'ENOENT',
|
||
|
|
message: 'ENOENT: no such file or directory',
|
||
|
|
expected:
|
||
|
|
'File or directory not found. Check if the path exists and is spelled correctly.',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'ENOSPC',
|
||
|
|
message: 'ENOSPC: no space left on device',
|
||
|
|
expected:
|
||
|
|
'No space left on device. Free up some disk space and try again.',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'EISDIR',
|
||
|
|
message: 'EISDIR: illegal operation on a directory',
|
||
|
|
path: '/some/directory',
|
||
|
|
expected:
|
||
|
|
"Path is a directory, not a file: '/some/directory'. Please provide a path to a file instead.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'EISDIR',
|
||
|
|
message: 'EISDIR: illegal operation on a directory',
|
||
|
|
expected:
|
||
|
|
'Path is a directory, not a file. Please provide a path to a file instead.',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'EROFS',
|
||
|
|
message: 'EROFS: read-only file system',
|
||
|
|
expected:
|
||
|
|
'Read-only file system. Ensure the file system allows write operations.',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'EPERM',
|
||
|
|
message: 'EPERM: operation not permitted',
|
||
|
|
path: '/protected/file',
|
||
|
|
expected:
|
||
|
|
"Operation not permitted: '/protected/file'. Ensure you have the required permissions for this action.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'EPERM',
|
||
|
|
message: 'EPERM: operation not permitted',
|
||
|
|
expected:
|
||
|
|
'Operation not permitted. Ensure you have the required permissions for this action.',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'EEXIST',
|
||
|
|
message: 'EEXIST: file already exists',
|
||
|
|
path: '/existing/file',
|
||
|
|
expected:
|
||
|
|
"File or directory already exists: '/existing/file'. Try using a different name or path.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'EEXIST',
|
||
|
|
message: 'EEXIST: file already exists',
|
||
|
|
expected:
|
||
|
|
'File or directory already exists. Try using a different name or path.',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'EBUSY',
|
||
|
|
message: 'EBUSY: resource busy or locked',
|
||
|
|
path: '/locked/file',
|
||
|
|
expected:
|
||
|
|
"Resource busy or locked: '/locked/file'. Close any programs that might be using the file.",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'EBUSY',
|
||
|
|
message: 'EBUSY: resource busy or locked',
|
||
|
|
expected:
|
||
|
|
'Resource busy or locked. Close any programs that might be using the file.',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'EMFILE',
|
||
|
|
message: 'EMFILE: too many open files',
|
||
|
|
expected:
|
||
|
|
'Too many open files. Close some unused files or applications.',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'ENFILE',
|
||
|
|
message: 'ENFILE: file table overflow',
|
||
|
|
expected:
|
||
|
|
'Too many open files in system. Close some unused files or applications.',
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
it.each(testCases)(
|
||
|
|
'returns friendly message for $code (path: $path)',
|
||
|
|
({ code, message, path, expected }) => {
|
||
|
|
const error = createNodeError(code, message, path);
|
||
|
|
expect(getFsErrorMessage(error)).toBe(expected);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('unknown node error codes', () => {
|
||
|
|
const testCases: FsErrorCase[] = [
|
||
|
|
{
|
||
|
|
code: 'EUNKNOWN',
|
||
|
|
message: 'Some unknown error occurred',
|
||
|
|
expected: 'Some unknown error occurred (EUNKNOWN)',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
code: 'toString',
|
||
|
|
message: 'Unexpected error',
|
||
|
|
path: '/some/path',
|
||
|
|
expected: 'Unexpected error (toString)',
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
it.each(testCases)(
|
||
|
|
'includes code in fallback message for $code',
|
||
|
|
({ code, message, path, expected }) => {
|
||
|
|
const error = createNodeError(code, message, path);
|
||
|
|
expect(getFsErrorMessage(error)).toBe(expected);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('non-node and nullish errors', () => {
|
||
|
|
const fallbackCases: FallbackErrorCase[] = [
|
||
|
|
{
|
||
|
|
value: new Error('Something went wrong'),
|
||
|
|
expected: 'Something went wrong',
|
||
|
|
},
|
||
|
|
{ value: 'string error', expected: 'string error' },
|
||
|
|
{ value: 12345, expected: '12345' },
|
||
|
|
{ value: null, expected: 'An unknown error occurred' },
|
||
|
|
{ value: undefined, expected: 'An unknown error occurred' },
|
||
|
|
];
|
||
|
|
|
||
|
|
it.each(fallbackCases)(
|
||
|
|
'returns a message for $value',
|
||
|
|
({ value, expected }) => {
|
||
|
|
expect(getFsErrorMessage(value)).toBe(expected);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
|
||
|
|
it.each([null, undefined] as const)(
|
||
|
|
'uses custom default for %s',
|
||
|
|
(value) => {
|
||
|
|
expect(getFsErrorMessage(value, 'Custom default')).toBe(
|
||
|
|
'Custom default',
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|