Files
hermes-agent/ui-tui/src/__tests__/textInputCut.test.ts
T
null-runner 24af6685b0 fix(tui): word-delete forward and transactional cut
The dashboard maps Ctrl+Delete to ESC d for delete-word-forward, but the
composer had no binding for it: hermes-ink decodes ESC d as meta+'d', which
fell through to the printable path and typed a literal "d" instead of
deleting the next word. Add a meta+d branch that mirrors the existing
Ctrl+W delete-word-backward, sharing a deleteWordForward helper with the
Delete+word path.

The new cut() wrote the clipboard fire-and-forget and removed the selected
text immediately, so on a headless/SSH box with no clipboard backend the
write fails and the text is lost with no copy to paste back. Make cut
transactional via cutSelection(): await the write and only remove the
selection when it succeeds; on failure the selection stays intact. The
removal also re-checks the selection to avoid slicing with stale offsets
after the awaited write.
2026-08-15 01:05:39 -07:00

54 lines
1.6 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import { cutSelection } from '../components/textInput.js'
describe('cutSelection (transactional cut)', () => {
it('removes the selection only after the clipboard write succeeds', async () => {
const write = vi.fn().mockResolvedValue(true)
const removeSelection = vi.fn()
const ok = await cutSelection('hello', write, removeSelection)
expect(ok).toBe(true)
expect(write).toHaveBeenCalledWith('hello')
expect(removeSelection).toHaveBeenCalledOnce()
})
it('keeps the text intact when the clipboard write fails (headless/SSH)', async () => {
const write = vi.fn().mockResolvedValue(false)
const removeSelection = vi.fn()
const ok = await cutSelection('hello', write, removeSelection)
expect(ok).toBe(false)
expect(write).toHaveBeenCalledWith('hello')
// Text must NOT be removed. A failed write would otherwise destroy it with no
// clipboard copy to paste back.
expect(removeSelection).not.toHaveBeenCalled()
})
it('awaits the write before removing (no fire-and-forget removal)', async () => {
let resolveWrite: (value: boolean) => void = () => {}
const write = vi.fn(
() =>
new Promise<boolean>(resolve => {
resolveWrite = resolve
})
)
const removeSelection = vi.fn()
const pending = cutSelection('hello', write, removeSelection)
// While the write is still pending the selection must remain untouched.
await Promise.resolve()
expect(removeSelection).not.toHaveBeenCalled()
resolveWrite(true)
await pending
expect(removeSelection).toHaveBeenCalledOnce()
})
})