From ec6fa9bdb255ec71a2ed809f4df323bd67f3b03a Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 25 Jul 2026 21:19:20 -0500 Subject: [PATCH] test(tui): cover inline skill references end to end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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'`. --- ui-tui/src/__tests__/completionApply.test.ts | 12 ++ ui-tui/src/__tests__/inlineSlashSkill.test.ts | 112 ++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 ui-tui/src/__tests__/inlineSlashSkill.test.ts diff --git a/ui-tui/src/__tests__/completionApply.test.ts b/ui-tui/src/__tests__/completionApply.test.ts index 5b26f8810d3e..b8870a942783 100644 --- a/ui-tui/src/__tests__/completionApply.test.ts +++ b/ui-tui/src/__tests__/completionApply.test.ts @@ -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', () => { diff --git a/ui-tui/src/__tests__/inlineSlashSkill.test.ts b/ui-tui/src/__tests__/inlineSlashSkill.test.ts new file mode 100644 index 000000000000..d2f81b7059f0 --- /dev/null +++ b/ui-tui/src/__tests__/inlineSlashSkill.test.ts @@ -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: '' }]) + }) +})