mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
test(tui): cover inline skill references end to end
inlineSlashTrigger and completionRequestForInput are driven through the mid-message shape, the position-0 shape, and the path cases that must not claim a slash (/usr/local/bin, src/foo/bar, and/or, 3 /4). splitSlashSkillRefs asserts the round-trip invariant — the segments always rejoin to the exact input — rather than freezing a segment list. The applyCompletion cases reproduce the reported bug rather than restating the implementation: reverting the fix fails with `expected 'please run //clean' to be 'please run /clean'`.
This commit is contained in:
parent
07244c5ead
commit
ec6fa9bdb2
2 changed files with 124 additions and 0 deletions
|
|
@ -16,6 +16,18 @@ describe('applyCompletion', () => {
|
|||
it('replaces an argument token after a space (subcommand completion)', () => {
|
||||
expect(applyCompletion('/cron ad', 'add', 6)).toBe('/cron add')
|
||||
})
|
||||
|
||||
it('applies an inline skill pick without disturbing the prose in front of it', () => {
|
||||
// The gateway returns bare names for `complete.slash`; a mid-message
|
||||
// reference replaces from just after its own `/`, so "please run " stays.
|
||||
expect(applyCompletion('please run /cle', 'clean', 12)).toBe('please run /clean')
|
||||
})
|
||||
|
||||
it('drops the row slash based on the character before the replace point, not the input start', () => {
|
||||
// Widget-app rows carry a leading slash. Mid-message the input does NOT
|
||||
// start with `/`, so a start-anchored check would double it up.
|
||||
expect(applyCompletion('please run /cle', '/clean', 12)).toBe('please run /clean')
|
||||
})
|
||||
})
|
||||
|
||||
describe('completionToApplyOnSubmit', () => {
|
||||
|
|
|
|||
112
ui-tui/src/__tests__/inlineSlashSkill.test.ts
Normal file
112
ui-tui/src/__tests__/inlineSlashSkill.test.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { inlineSlashTrigger, splitSlashSkillRefs } from '../domain/slash.js'
|
||||
import { completionRequestForInput } from '../hooks/useCompletion.js'
|
||||
|
||||
describe('inlineSlashTrigger', () => {
|
||||
it('detects a slash typed mid-message', () => {
|
||||
// The reported bug: only a position-0 slash offered anything, so
|
||||
// "please run /cle" completed nothing.
|
||||
expect(inlineSlashTrigger('please run /cle')).toEqual({ query: 'cle', start: 11 })
|
||||
})
|
||||
|
||||
it('detects a bare slash after whitespace, before any name is typed', () => {
|
||||
expect(inlineSlashTrigger('please run /')).toEqual({ query: '', start: 11 })
|
||||
})
|
||||
|
||||
it('fires after a newline, not just a space', () => {
|
||||
expect(inlineSlashTrigger('text\n/skill')).toEqual({ query: 'skill', start: 5 })
|
||||
})
|
||||
|
||||
it('does not fire at position 0 — that is a command invocation', () => {
|
||||
expect(inlineSlashTrigger('/clean')).toBeNull()
|
||||
expect(inlineSlashTrigger('/')).toBeNull()
|
||||
})
|
||||
|
||||
it('leaves file paths alone', () => {
|
||||
expect(inlineSlashTrigger('look at /usr/local/bin')).toBeNull()
|
||||
expect(inlineSlashTrigger('check src/foo/bar')).toBeNull()
|
||||
expect(inlineSlashTrigger('and/or')).toBeNull()
|
||||
})
|
||||
|
||||
it('stops at the command token — an inline reference takes no args', () => {
|
||||
// Only a position-0 slash is a real invocation, so `/personality alic`
|
||||
// mid-message is prose with a reference in it, already ended.
|
||||
expect(inlineSlashTrigger('hello there /personality alic')).toBeNull()
|
||||
})
|
||||
|
||||
it('reports a start index that replaces only the typed token', () => {
|
||||
const text = 'please run /cle'
|
||||
const trigger = inlineSlashTrigger(text)!
|
||||
|
||||
expect(text.slice(0, trigger.start)).toBe('please run ')
|
||||
expect(text.slice(trigger.start)).toBe('/cle')
|
||||
})
|
||||
})
|
||||
|
||||
describe('completionRequestForInput — inline skill references', () => {
|
||||
it('asks for skills only when the slash is mid-message', () => {
|
||||
const request = completionRequestForInput('please run /cle')
|
||||
|
||||
expect(request).toMatchObject({
|
||||
method: 'complete.slash',
|
||||
params: { text: '/cle' },
|
||||
replaceFrom: 12,
|
||||
skillsOnly: true
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the full command set at position 0', () => {
|
||||
expect(completionRequestForInput('/cle')).toEqual({
|
||||
method: 'complete.slash',
|
||||
params: { text: '/cle' },
|
||||
replaceFrom: 1
|
||||
})
|
||||
})
|
||||
|
||||
it('routes a real mid-message path to path completion, not skills', () => {
|
||||
expect(completionRequestForInput('open src/foo/ba')).toMatchObject({ method: 'complete.path' })
|
||||
expect(completionRequestForInput('open /usr/lo')).toMatchObject({ method: 'complete.path' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('splitSlashSkillRefs', () => {
|
||||
it('marks a skill referenced mid-prose', () => {
|
||||
expect(splitSlashSkillRefs('clean this up with /clean')).toEqual([
|
||||
{ ref: false, text: 'clean this up with ' },
|
||||
{ ref: true, text: '/clean' }
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps the prose on both sides of the reference', () => {
|
||||
expect(splitSlashSkillRefs('run /clean then ship')).toEqual([
|
||||
{ ref: false, text: 'run ' },
|
||||
{ ref: true, text: '/clean' },
|
||||
{ ref: false, text: ' then ship' }
|
||||
])
|
||||
})
|
||||
|
||||
it('does not mark paths', () => {
|
||||
for (const text of ['look at /usr/local/bin', 'check src/foo/bar', 'a 3 /4 b']) {
|
||||
expect(splitSlashSkillRefs(text)).toEqual([{ ref: false, text }])
|
||||
}
|
||||
})
|
||||
|
||||
it('does not mark a leading slash — that is a command, not a reference', () => {
|
||||
expect(splitSlashSkillRefs('/clean')).toEqual([{ ref: false, text: '/clean' }])
|
||||
})
|
||||
|
||||
it('round-trips the input exactly', () => {
|
||||
for (const text of ['run /clean then /work ok', 'plain text', '', 'look at /usr/local/bin']) {
|
||||
expect(
|
||||
splitSlashSkillRefs(text)
|
||||
.map(s => s.text)
|
||||
.join('')
|
||||
).toBe(text)
|
||||
}
|
||||
})
|
||||
|
||||
it('always returns at least one segment', () => {
|
||||
expect(splitSlashSkillRefs('')).toEqual([{ ref: false, text: '' }])
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue