fix(copyCommand): provide friendlier error messages for /copy command (#6723)

Co-authored-by: Ben Guo <hundunben@gmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
Ben Guo
2025-08-23 00:58:35 +08:00
committed by GitHub
parent c4a788b7b2
commit 9c1490e985
5 changed files with 114 additions and 16 deletions

View File

@@ -224,7 +224,9 @@ describe('commandUtils', () => {
setTimeout(() => {
if (callCount === 0) {
// First call (xclip) fails
child.stderr.emit('data', 'xclip not found');
const error = new Error('spawn xclip ENOENT');
(error as NodeJS.ErrnoException).code = 'ENOENT';
child.emit('error', error);
child.emit('close', 1);
callCount++;
} else {
@@ -253,9 +255,12 @@ describe('commandUtils', () => {
);
});
it('should throw error when both xclip and xsel fail', async () => {
it('should throw error when both xclip and xsel are not found', async () => {
const testText = 'Hello, world!';
let callCount = 0;
const linuxOptions: SpawnOptions = {
stdio: ['pipe', 'inherit', 'pipe'],
};
mockSpawn.mockImplementation(() => {
const child = Object.assign(new EventEmitter(), {
@@ -264,29 +269,99 @@ describe('commandUtils', () => {
end: vi.fn(),
}),
stderr: new EventEmitter(),
});
}) as MockChildProcess;
setTimeout(() => {
if (callCount === 0) {
// First call (xclip) fails
child.stderr.emit('data', 'xclip command not found');
// First call (xclip) fails with ENOENT
const error = new Error('spawn xclip ENOENT');
(error as NodeJS.ErrnoException).code = 'ENOENT';
child.emit('error', error);
child.emit('close', 1);
callCount++;
} else {
// Second call (xsel) fails
child.stderr.emit('data', 'xsel command not found');
// Second call (xsel) fails with ENOENT
const error = new Error('spawn xsel ENOENT');
(error as NodeJS.ErrnoException).code = 'ENOENT';
child.emit('error', error);
child.emit('close', 1);
}
}, 0);
return child as unknown as ReturnType<typeof spawn>;
});
await expect(copyToClipboard(testText)).rejects.toThrow(
/All copy commands failed/,
'Please ensure xclip or xsel is installed and configured.',
);
expect(mockSpawn).toHaveBeenCalledTimes(2);
expect(mockSpawn).toHaveBeenNthCalledWith(
1,
'xclip',
['-selection', 'clipboard'],
linuxOptions,
);
expect(mockSpawn).toHaveBeenNthCalledWith(
2,
'xsel',
['--clipboard', '--input'],
linuxOptions,
);
});
it('should emit error when xclip or xsel fail with stderr output (command installed)', async () => {
const testText = 'Hello, world!';
let callCount = 0;
const linuxOptions: SpawnOptions = {
stdio: ['pipe', 'inherit', 'pipe'],
};
const errorMsg = "Error: Can't open display:";
const exitCode = 1;
mockSpawn.mockImplementation(() => {
const child = Object.assign(new EventEmitter(), {
stdin: Object.assign(new EventEmitter(), {
write: vi.fn(),
end: vi.fn(),
}),
stderr: new EventEmitter(),
}) as MockChildProcess;
setTimeout(() => {
// e.g., cannot connect to X server
if (callCount === 0) {
child.stderr.emit('data', errorMsg);
child.emit('close', exitCode);
callCount++;
} else {
child.stderr.emit('data', errorMsg);
child.emit('close', exitCode);
}
}, 0);
return child as unknown as ReturnType<typeof spawn>;
});
const xclipErrorMsg = `'xclip' exited with code ${exitCode}${errorMsg ? `: ${errorMsg}` : ''}`;
const xselErrorMsg = `'xsel' exited with code ${exitCode}${errorMsg ? `: ${errorMsg}` : ''}`;
await expect(copyToClipboard(testText)).rejects.toThrow(
`All copy commands failed. "${xclipErrorMsg}", "${xselErrorMsg}". `,
);
expect(mockSpawn).toHaveBeenCalledTimes(2);
expect(mockSpawn).toHaveBeenNthCalledWith(
1,
'xclip',
['-selection', 'clipboard'],
linuxOptions,
);
expect(mockSpawn).toHaveBeenNthCalledWith(
2,
'xsel',
['--clipboard', '--input'],
linuxOptions,
);
});
});