/ tests / slash-command-parser.test.ts
slash-command-parser.test.ts
  1  import { describe, expect, it } from 'vitest'
  2  
  3  import { parseSlashCommand } from '@/server/chat/slash-command-parser'
  4  
  5  describe('parseSlashCommand', () => {
  6    it('returns null for non-slash text or empty slash input', () => {
  7      expect(parseSlashCommand('hello')).toBeNull()
  8      expect(parseSlashCommand('/')).toBeNull()
  9      expect(parseSlashCommand('  /   ')).toBeNull()
 10    })
 11  
 12    it('parses help command topics and aliases', () => {
 13      expect(parseSlashCommand('/help model')).toEqual({ kind: 'help-model' })
 14      expect(parseSlashCommand('/help providers')).toEqual({
 15        kind: 'help-provider',
 16      })
 17      expect(parseSlashCommand('/help codexsend')).toEqual({
 18        kind: 'help-agents',
 19      })
 20      expect(parseSlashCommand('/help webtools')).toEqual({ kind: 'help-web' })
 21      expect(parseSlashCommand('/help listlocalsessions')).toEqual({
 22        kind: 'list-helper-sessions',
 23      })
 24      expect(parseSlashCommand('/help reasoning')).toEqual({
 25        kind: 'settings-show',
 26      })
 27      expect(parseSlashCommand('/help unknown-topic')).toBeNull()
 28    })
 29  
 30    it('parses agent command families and aliases', () => {
 31      expect(parseSlashCommand('/listagents')).toEqual({
 32        kind: 'list-agent-backends',
 33      })
 34      expect(parseSlashCommand('/agents probe codex')).toEqual({
 35        kind: 'agent-probe',
 36        backendId: 'codex-cli',
 37      })
 38      expect(parseSlashCommand('/agents probe nope')).toEqual({
 39        kind: 'help-agents',
 40      })
 41      expect(parseSlashCommand('/agentsessions oc')).toEqual({
 42        kind: 'list-agent-sessions',
 43        backendId: 'opencode',
 44      })
 45      expect(parseSlashCommand('/listcodexsessions')).toEqual({
 46        kind: 'list-agent-sessions',
 47        backendId: 'codex-cli',
 48      })
 49      expect(parseSlashCommand('/codexlatest abc123')).toEqual({
 50        kind: 'agent-latest',
 51        backendId: 'codex-cli',
 52        selector: 'abc123',
 53      })
 54      expect(
 55        parseSlashCommand('/agentsend codex-cli abc123 :: please continue'),
 56      ).toEqual({
 57        kind: 'agent-send',
 58        backendId: 'codex-cli',
 59        selector: 'abc123',
 60        prompt: 'please continue',
 61      })
 62      expect(
 63        parseSlashCommand('/agendsend codex-cli abc123 :: please continue'),
 64      ).toEqual({
 65        kind: 'agent-send',
 66        backendId: 'codex-cli',
 67        selector: 'abc123',
 68        prompt: 'please continue',
 69      })
 70      expect(
 71        parseSlashCommand(
 72          '/agentsend codex newsession --cwd /home/helper/repos/project :: investigate tool loops',
 73        ),
 74      ).toEqual({
 75        kind: 'agent-send',
 76        backendId: 'codex-cli',
 77        selector: 'newsession',
 78        cwd: '/home/helper/repos/project',
 79        prompt: 'investigate tool loops',
 80      })
 81      expect(parseSlashCommand('/codexsend abc123 please continue')).toEqual({
 82        kind: 'agent-send',
 83        backendId: 'codex-cli',
 84        selector: 'abc123',
 85        prompt: 'please continue',
 86      })
 87    })
 88  
 89    it('parses provider commands including endpoint and key clauses', () => {
 90      expect(parseSlashCommand('/provider')).toEqual({ kind: 'provider-current' })
 91      expect(parseSlashCommand('/provider clear')).toEqual({
 92        kind: 'provider-clear',
 93      })
 94      expect(parseSlashCommand('/provider probe example')).toEqual({
 95        kind: 'provider-probe',
 96        target: { kind: 'profile', id: 'example' },
 97      })
 98      expect(parseSlashCommand('/provider probe router')).toEqual({
 99        kind: 'provider-probe',
100        target: { kind: 'profile', id: 'router' },
101      })
102      expect(
103        parseSlashCommand('/provider use example endpoint:chat'),
104      ).toEqual({
105        kind: 'provider-use',
106        target: {
107          kind: 'profile',
108          id: 'example',
109        },
110        endpointMode: 'chat_completions',
111      })
112  
113      expect(parseSlashCommand('/provider use endpoint:nope')).toEqual({
114        kind: 'help-provider',
115      })
116    })
117  
118    it('preserves provider target parsing edge cases', () => {
119      expect(
120        parseSlashCommand('/provider use https://example.com key:after-url'),
121      ).toEqual({ kind: 'help-provider' })
122    })
123  
124    it('parses settings defaults updates and clear variants', () => {
125      expect(parseSlashCommand('/settings')).toEqual({ kind: 'settings-show' })
126      expect(parseSlashCommand('/settings defaults')).toEqual({
127        kind: 'settings-defaults-show',
128      })
129      expect(parseSlashCommand('/settings defaults clear provider')).toEqual({
130        kind: 'settings-defaults-update',
131        scopeLabel: 'default provider settings cleared',
132        patch: {
133          defaultProviderId: null,
134          providerEndpointMode: null,
135        },
136      })
137      expect(parseSlashCommand('/settings defaults model: gpt-5-mini')).toEqual({
138        kind: 'settings-defaults-update',
139        scopeLabel: 'default model → gpt-5-mini',
140        patch: { chatModel: 'gpt-5-mini' },
141      })
142      expect(parseSlashCommand('/settings defaults think: medium')).toEqual({
143        kind: 'settings-defaults-update',
144        scopeLabel: 'default thinking → medium',
145        patch: { thinkingLevel: 'medium' },
146      })
147      expect(
148        parseSlashCommand('/settings defaults endpoint_mode: chat-completions'),
149      ).toEqual({
150        kind: 'settings-defaults-update',
151        scopeLabel: 'default provider endpoint mode → chat_completions',
152        patch: { providerEndpointMode: 'chat_completions' },
153      })
154      expect(parseSlashCommand('/settings defaults nope: value')).toEqual({
155        kind: 'settings-defaults-show',
156      })
157    })
158  
159    it('preserves settings defaults clear/field edge cases', () => {
160      expect(parseSlashCommand('/settings defaults clear')).toEqual({
161        kind: 'settings-defaults-update',
162        scopeLabel: 'all defaults reset',
163        patch: {
164          chatModel: null,
165          thinkingLevel: null,
166          reasoningLevel: null,
167          defaultProviderId: null,
168          providerEndpointMode: null,
169        },
170      })
171  
172      expect(parseSlashCommand('/settings defaults provider: openai')).toEqual({
173        kind: 'settings-defaults-update',
174        scopeLabel: 'default provider → openai',
175        patch: { defaultProviderId: 'openai' },
176      })
177      // Dynamic provider IDs are now accepted
178      expect(parseSlashCommand('/settings defaults provider: ollama')).toEqual({
179        kind: 'settings-defaults-update',
180        scopeLabel: 'default provider → ollama',
181        patch: { defaultProviderId: 'ollama' },
182      })
183      expect(
184        parseSlashCommand('/settings defaults endpoint_mode: RESPONSES'),
185      ).toEqual({
186        kind: 'settings-defaults-update',
187        scopeLabel: 'default provider endpoint mode → responses',
188        patch: { providerEndpointMode: 'responses' },
189      })
190  
191    })
192  
193    it('parses web command aliases and search-type flags', () => {
194      expect(parseSlashCommand('/websearch pelicans')).toEqual({
195        kind: 'web-search',
196        query: 'pelicans',
197        searchType: undefined,
198      })
199      expect(parseSlashCommand('/search --image pelicans')).toEqual({
200        kind: 'web-search',
201        query: 'pelicans',
202        searchType: 'image',
203      })
204      expect(parseSlashCommand('/search --news technology')).toEqual({
205        kind: 'web-search',
206        query: 'technology',
207        searchType: 'news',
208      })
209      expect(parseSlashCommand('/fetch https://news.ycombinator.com')).toEqual({
210        kind: 'web-fetch',
211        url: 'https://news.ycombinator.com',
212      })
213      expect(parseSlashCommand('/webtools')).toEqual({ kind: 'help-web' })
214    })
215  
216    it('parses think, reasoning, and model commands', () => {
217      expect(parseSlashCommand('/think')).toEqual({ kind: 'think-show' })
218      expect(parseSlashCommand('/thinking clear')).toEqual({
219        kind: 'think-set',
220        thinkingLevel: null,
221      })
222      expect(parseSlashCommand('/think medium')).toEqual({
223        kind: 'think-set',
224        thinkingLevel: 'medium',
225      })
226      expect(parseSlashCommand('/reasoning stream')).toEqual({
227        kind: 'reasoning-set',
228        reasoningLevel: 'stream',
229      })
230      expect(parseSlashCommand('/models')).toEqual({ kind: 'model-list' })
231      expect(parseSlashCommand('/model current')).toEqual({
232        kind: 'model-current',
233      })
234      expect(parseSlashCommand('/model clear')).toEqual({ kind: 'model-clear' })
235      expect(parseSlashCommand('/model chat: gpt-5-mini')).toEqual({
236        kind: 'model-set',
237        model: 'gpt-5-mini',
238      })
239    })
240  })