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 })