mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-12 03:42:08 +00:00
329 lines
9.5 KiB
TypeScript
329 lines
9.5 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
import { isUsableClipboardText, readClipboardText, writeClipboardText } from '../lib/clipboard.js'
|
|
|
|
describe('readClipboardText', () => {
|
|
it('reads text from pbpaste on macOS', async () => {
|
|
const run = vi.fn().mockResolvedValue({ stdout: 'hello world\n' })
|
|
|
|
await expect(readClipboardText('darwin', run)).resolves.toBe('hello world\n')
|
|
expect(run).toHaveBeenCalledWith(
|
|
'pbpaste',
|
|
[],
|
|
expect.objectContaining({ encoding: 'utf8', maxBuffer: 4 * 1024 * 1024, windowsHide: true })
|
|
)
|
|
})
|
|
|
|
it('reads text from PowerShell on Windows', async () => {
|
|
const run = vi.fn().mockResolvedValue({ stdout: 'from windows\r\n' })
|
|
|
|
await expect(readClipboardText('win32', run)).resolves.toBe('from windows\r\n')
|
|
expect(run).toHaveBeenCalledWith(
|
|
'powershell',
|
|
['-NoProfile', '-NonInteractive', '-Command', 'Get-Clipboard -Raw'],
|
|
expect.objectContaining({ encoding: 'utf8', maxBuffer: 4 * 1024 * 1024, windowsHide: true })
|
|
)
|
|
})
|
|
|
|
it('tries powershell.exe first on WSL', async () => {
|
|
const run = vi.fn().mockResolvedValue({ stdout: 'from wsl\n' })
|
|
|
|
await expect(readClipboardText('linux', run, { WSL_INTEROP: '/tmp/socket' } as NodeJS.ProcessEnv)).resolves.toBe(
|
|
'from wsl\n'
|
|
)
|
|
expect(run).toHaveBeenCalledWith(
|
|
'powershell.exe',
|
|
['-NoProfile', '-NonInteractive', '-Command', 'Get-Clipboard -Raw'],
|
|
expect.objectContaining({ encoding: 'utf8', maxBuffer: 4 * 1024 * 1024, windowsHide: true })
|
|
)
|
|
})
|
|
|
|
it('uses wl-paste on Wayland Linux', async () => {
|
|
const run = vi.fn().mockResolvedValue({ stdout: 'from wayland\n' })
|
|
|
|
await expect(readClipboardText('linux', run, { WAYLAND_DISPLAY: 'wayland-1' } as NodeJS.ProcessEnv)).resolves.toBe(
|
|
'from wayland\n'
|
|
)
|
|
expect(run).toHaveBeenCalledWith(
|
|
'wl-paste',
|
|
['--type', 'text'],
|
|
expect.objectContaining({ encoding: 'utf8', maxBuffer: 4 * 1024 * 1024, windowsHide: true })
|
|
)
|
|
})
|
|
|
|
it('falls back to xclip on Linux when wl-paste fails', async () => {
|
|
const run = vi
|
|
.fn()
|
|
.mockRejectedValueOnce(new Error('wl-paste missing'))
|
|
.mockResolvedValueOnce({ stdout: 'from xclip\n' })
|
|
|
|
await expect(readClipboardText('linux', run, { WAYLAND_DISPLAY: 'wayland-1' } as NodeJS.ProcessEnv)).resolves.toBe(
|
|
'from xclip\n'
|
|
)
|
|
expect(run).toHaveBeenNthCalledWith(
|
|
1,
|
|
'wl-paste',
|
|
['--type', 'text'],
|
|
expect.objectContaining({ encoding: 'utf8', maxBuffer: 4 * 1024 * 1024, windowsHide: true })
|
|
)
|
|
expect(run).toHaveBeenNthCalledWith(
|
|
2,
|
|
'xclip',
|
|
['-selection', 'clipboard', '-out'],
|
|
expect.objectContaining({ encoding: 'utf8', maxBuffer: 4 * 1024 * 1024, windowsHide: true })
|
|
)
|
|
})
|
|
|
|
it('returns null when every clipboard backend fails', async () => {
|
|
const run = vi.fn().mockRejectedValue(new Error('clipboard failed'))
|
|
|
|
await expect(
|
|
readClipboardText('linux', run, { WAYLAND_DISPLAY: 'wayland-1' } as NodeJS.ProcessEnv)
|
|
).resolves.toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('isUsableClipboardText', () => {
|
|
it('accepts normal text', () => {
|
|
expect(isUsableClipboardText('hello world\n')).toBe(true)
|
|
})
|
|
|
|
it('rejects empty or whitespace-only content', () => {
|
|
expect(isUsableClipboardText('')).toBe(false)
|
|
expect(isUsableClipboardText(' \n\t')).toBe(false)
|
|
})
|
|
|
|
it('rejects binary-looking clipboard payloads', () => {
|
|
expect(isUsableClipboardText('PNG\u0000\u0001\u0002\u0003IHDR')).toBe(false)
|
|
expect(isUsableClipboardText('TIFF\ufffd\ufffd\ufffdmetadata')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('writeClipboardText', () => {
|
|
it('does nothing off macOS when no tools are available', async () => {
|
|
const child = {
|
|
once: vi.fn((event: string, cb: (code?: number) => void) => {
|
|
if (event === 'close') {
|
|
cb(1) // non-zero exit = failure
|
|
}
|
|
|
|
return child
|
|
}),
|
|
stdin: { end: vi.fn() }
|
|
}
|
|
|
|
const start = vi.fn().mockReturnValue(child)
|
|
|
|
// Linux with no WAYLAND_DISPLAY / no WSL_INTEROP — falls through xclip then xsel, both fail
|
|
await expect(writeClipboardText('hello', 'linux', start, {})).resolves.toBe(false)
|
|
})
|
|
|
|
it('writes text to pbcopy on macOS', async () => {
|
|
const stdin = { end: vi.fn() }
|
|
|
|
const child = {
|
|
once: vi.fn((event: string, cb: (code?: number) => void) => {
|
|
if (event === 'close') {
|
|
cb(0)
|
|
}
|
|
|
|
return child
|
|
}),
|
|
stdin
|
|
}
|
|
|
|
const start = vi.fn().mockReturnValue(child)
|
|
|
|
await expect(writeClipboardText('hello world', 'darwin', start as any)).resolves.toBe(true)
|
|
expect(start).toHaveBeenCalledWith(
|
|
'pbcopy',
|
|
[],
|
|
expect.objectContaining({ stdio: ['pipe', 'ignore', 'ignore'], windowsHide: true })
|
|
)
|
|
expect(stdin.end).toHaveBeenCalledWith('hello world')
|
|
})
|
|
|
|
it('returns false when pbcopy fails', async () => {
|
|
const child = {
|
|
once: vi.fn((event: string, cb: () => void) => {
|
|
if (event === 'error') {
|
|
cb()
|
|
}
|
|
|
|
return child
|
|
}),
|
|
stdin: { end: vi.fn() }
|
|
}
|
|
|
|
const start = vi.fn().mockReturnValue(child)
|
|
|
|
await expect(writeClipboardText('hello world', 'darwin', start as any)).resolves.toBe(false)
|
|
})
|
|
|
|
it('uses wl-copy on Wayland Linux', async () => {
|
|
const stdin = { end: vi.fn() }
|
|
|
|
const child = {
|
|
once: vi.fn((event: string, cb: (code?: number) => void) => {
|
|
if (event === 'close') {
|
|
cb(0)
|
|
}
|
|
|
|
return child
|
|
}),
|
|
stdin
|
|
}
|
|
|
|
const start = vi.fn().mockReturnValue(child)
|
|
|
|
await expect(
|
|
writeClipboardText('wayland text', 'linux', start as any, { WAYLAND_DISPLAY: 'wayland-1' })
|
|
).resolves.toBe(true)
|
|
expect(start).toHaveBeenCalledWith(
|
|
'wl-copy',
|
|
['--type', 'text/plain'],
|
|
expect.objectContaining({ stdio: ['pipe', 'ignore', 'ignore'], windowsHide: true })
|
|
)
|
|
expect(stdin.end).toHaveBeenCalledWith('wayland text')
|
|
})
|
|
|
|
it('falls back to xclip when wl-copy fails on Wayland', async () => {
|
|
let callCount = 0
|
|
const stdin = { end: vi.fn() }
|
|
|
|
const child = {
|
|
once: vi.fn((event: string, cb: (code?: number) => void) => {
|
|
if (event === 'close') {
|
|
callCount++
|
|
// wl-copy fails, xclip succeeds
|
|
cb(callCount === 1 ? 1 : 0)
|
|
}
|
|
|
|
return child
|
|
}),
|
|
stdin
|
|
}
|
|
|
|
const start = vi.fn().mockReturnValue(child)
|
|
|
|
await expect(
|
|
writeClipboardText('x11 text', 'linux', start as any, { WAYLAND_DISPLAY: 'wayland-1' })
|
|
).resolves.toBe(true)
|
|
expect(start).toHaveBeenNthCalledWith(
|
|
1,
|
|
'wl-copy',
|
|
['--type', 'text/plain'],
|
|
expect.anything()
|
|
)
|
|
expect(start).toHaveBeenNthCalledWith(
|
|
2,
|
|
'xclip',
|
|
['-selection', 'clipboard', '-in'],
|
|
expect.anything()
|
|
)
|
|
})
|
|
|
|
it('falls back to xsel when both wl-copy and xclip fail', async () => {
|
|
let callCount = 0
|
|
const stdin = { end: vi.fn() }
|
|
|
|
const child = {
|
|
once: vi.fn((event: string, cb: (code?: number) => void) => {
|
|
if (event === 'close') {
|
|
callCount++
|
|
cb(callCount < 3 ? 1 : 0) // first two fail, third (xsel) succeeds
|
|
}
|
|
|
|
return child
|
|
}),
|
|
stdin
|
|
}
|
|
|
|
const start = vi.fn().mockReturnValue(child)
|
|
|
|
await expect(
|
|
writeClipboardText('xsel text', 'linux', start as any, { WAYLAND_DISPLAY: 'wayland-1' })
|
|
).resolves.toBe(true)
|
|
expect(start).toHaveBeenNthCalledWith(3, 'xsel', ['--clipboard', '--input'], expect.anything())
|
|
})
|
|
|
|
it('uses PowerShell on WSL2 when WSL_DISTRO_NAME is set', async () => {
|
|
const stdin = { end: vi.fn() }
|
|
|
|
const child = {
|
|
once: vi.fn((event: string, cb: (code?: number) => void) => {
|
|
if (event === 'close') {
|
|
cb(0)
|
|
}
|
|
|
|
return child
|
|
}),
|
|
stdin
|
|
}
|
|
|
|
const start = vi.fn().mockReturnValue(child)
|
|
|
|
await expect(writeClipboardText('wsl text', 'linux', start as any, { WSL_DISTRO_NAME: 'Ubuntu' })).resolves.toBe(true)
|
|
expect(start).toHaveBeenCalledWith(
|
|
'powershell.exe',
|
|
expect.arrayContaining(['-NoProfile', '-NonInteractive']),
|
|
expect.anything()
|
|
)
|
|
expect(stdin.end).toHaveBeenCalledWith('wsl text')
|
|
})
|
|
|
|
it('prefers the Windows clipboard path over wl-copy inside WSLg', async () => {
|
|
const stdin = { end: vi.fn() }
|
|
|
|
const child = {
|
|
once: vi.fn((event: string, cb: (code?: number) => void) => {
|
|
if (event === 'close') {
|
|
cb(0)
|
|
}
|
|
|
|
return child
|
|
}),
|
|
stdin
|
|
}
|
|
|
|
const start = vi.fn().mockReturnValue(child)
|
|
|
|
await expect(
|
|
writeClipboardText('wslg text', 'linux', start as any, {
|
|
WAYLAND_DISPLAY: 'wayland-0',
|
|
WSL_DISTRO_NAME: 'Ubuntu'
|
|
})
|
|
).resolves.toBe(true)
|
|
expect(start).toHaveBeenNthCalledWith(
|
|
1,
|
|
'powershell.exe',
|
|
expect.arrayContaining(['-NoProfile', '-NonInteractive']),
|
|
expect.anything()
|
|
)
|
|
expect(stdin.end).toHaveBeenCalledWith('wslg text')
|
|
})
|
|
|
|
it('uses PowerShell on Windows', async () => {
|
|
const stdin = { end: vi.fn() }
|
|
|
|
const child = {
|
|
once: vi.fn((event: string, cb: (code?: number) => void) => {
|
|
if (event === 'close') {
|
|
cb(0)
|
|
}
|
|
|
|
return child
|
|
}),
|
|
stdin
|
|
}
|
|
|
|
const start = vi.fn().mockReturnValue(child)
|
|
|
|
await expect(writeClipboardText('windows text', 'win32', start as any)).resolves.toBe(true)
|
|
expect(start).toHaveBeenCalledWith(
|
|
'powershell',
|
|
expect.arrayContaining(['-NoProfile', '-NonInteractive']),
|
|
expect.anything()
|
|
)
|
|
})
|
|
})
|