hermes-agent/ui-tui/src/__tests__/completionApply.test.ts
xxxigm 58ad6942d9
fix(tui): don't make Enter swallow trailing-space-only slash completions (#48425)
* fix(tui): don't make Enter swallow trailing-space-only slash completions

Submitting a slash command in the TUI took three Enter presses: one to
complete the name (/ex → /exit), a second that only appended the trailing
space the gateway adds to keep the classic-CLI prompt_toolkit dropdown open
(/exit → "/exit "), and a third to actually submit.

The composer's submit handler accepted the highlighted completion whenever
applying it changed the input at all, so the whitespace-only delta ate an
extra keypress. Treat a completion whose only change is trailing whitespace
on an already-complete token as "already complete" and fall through to
submit. Partial-name and argument completions (a real token change) still
accept on Enter as before.

The replace/accept logic is extracted into pure helpers (applyCompletion,
completionToApplyOnSubmit) in domain/slash.ts.

* test(tui): cover Enter/completion trailing-space behavior and isolate poller queue

- completionApply.test.ts asserts completionToApplyOnSubmit accepts real
  token completions (partial command name, argument) but returns null for a
  trailing-space-only delta on an already-complete command, so Enter submits
  instead of needing extra presses.
- test_notification_poller_delivers_completion / _skips_consumed previously
  shared the process-global process_registry.completion_queue. Their events
  carry no session_key, so a leaked/concurrent poller could dequeue and
  dispatch them to a fixture agent without run_conversation, flaking CI
  ("AttributeError: '_FakeAgent' object has no attribute 'run_conversation'").
  Isolate the queue per test (fresh queue.Queue via monkeypatch), matching the
  sibling poller tests that already do this.
2026-06-18 11:04:59 -05:00

51 lines
2.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { applyCompletion, completionToApplyOnSubmit } from '../domain/slash.js'
describe('applyCompletion', () => {
it('replaces from compReplace and drops the leading slash from the row', () => {
// The gateway's slash completer returns bare command names with
// replace_from = 1 (after the leading "/").
expect(applyCompletion('/ex', 'exit', 1)).toBe('/exit')
})
it('keeps the leading slash when the row carries one and input does not', () => {
expect(applyCompletion('ex', '/exit', 0)).toBe('/exit')
})
it('replaces an argument token after a space (subcommand completion)', () => {
expect(applyCompletion('/cron ad', 'add', 6)).toBe('/cron add')
})
})
describe('completionToApplyOnSubmit', () => {
it('accepts a completion that finishes a partial command name', () => {
// "/ex" -> "/exit": a real token change, so Enter accepts it.
expect(completionToApplyOnSubmit('/ex', 'exit', 1)).toBe('/exit')
})
it('does NOT swallow Enter when the completion only adds a trailing space', () => {
// This is the bug: once "/exit" is fully typed, the gateway returns the
// command with a trailing space ("exit ") so the classic-CLI dropdown
// stays open. In the TUI that must NOT eat the Enter — the command is
// already complete, so Enter should submit.
expect(completionToApplyOnSubmit('/exit', 'exit ', 1)).toBeNull()
})
it('does not swallow Enter when applying the row is a no-op', () => {
expect(completionToApplyOnSubmit('/exit', 'exit', 1)).toBeNull()
})
it('still accepts a real argument completion (no trailing-space false positive)', () => {
expect(completionToApplyOnSubmit('/cron ad', 'add', 6)).toBe('/cron add')
})
it('submits (no accept) once an argument is fully typed and only a space is added', () => {
expect(completionToApplyOnSubmit('/cron add', 'add ', 6)).toBeNull()
})
it('returns null when there is no row text', () => {
expect(completionToApplyOnSubmit('/exit', undefined, 1)).toBeNull()
expect(completionToApplyOnSubmit('/exit', '', 1)).toBeNull()
})
})