Skip to content

Commit b2e7516

Browse files
committed
refactor: extract CLI command routing into registry pattern with tests
- Create command-registry.ts with CommandDefinition type and COMMAND_REGISTRY - Add router-utils.ts with input parsing helpers (normalizeInput, parseCommand, isSlashCommand, isReferralCode) - Add referral.ts for handling ref-XXXX code redemption - Add comprehensive tests for router input parsing and command registry - Remove unused clearQueue and handleCtrlC params from chat.tsx - Referral codes now work with or without leading slash
1 parent b4d69f9 commit b2e7516

File tree

6 files changed

+653
-127
lines changed

6 files changed

+653
-127
lines changed

cli/src/chat.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -675,8 +675,6 @@ export const Chat = ({
675675
streamMessageIdRef,
676676
addToQueue,
677677
clearMessages,
678-
clearQueue,
679-
handleCtrlC,
680678
saveToHistory,
681679
scrollToLatest,
682680
sendMessage,
@@ -796,8 +794,6 @@ export const Chat = ({
796794
streamMessageIdRef,
797795
addToQueue,
798796
clearMessages,
799-
clearQueue,
800-
handleCtrlC,
801797
saveToHistory,
802798
scrollToLatest,
803799
sendMessage,
@@ -825,8 +821,6 @@ export const Chat = ({
825821
streamMessageIdRef,
826822
addToQueue,
827823
clearMessages,
828-
clearQueue,
829-
handleCtrlC,
830824
saveToHistory,
831825
scrollToLatest,
832826
sendMessage,
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import { describe, test, expect } from 'bun:test'
2+
3+
import {
4+
normalizeInput,
5+
parseCommand,
6+
isSlashCommand,
7+
isReferralCode,
8+
} from '../router-utils'
9+
import { findCommand, COMMAND_REGISTRY } from '../command-registry'
10+
11+
describe('router-utils', () => {
12+
describe('normalizeInput', () => {
13+
test('strips leading slash from input', () => {
14+
expect(normalizeInput('/help')).toBe('help')
15+
expect(normalizeInput('/logout')).toBe('logout')
16+
expect(normalizeInput('/ref-abc123')).toBe('ref-abc123')
17+
})
18+
19+
test('preserves input without leading slash', () => {
20+
expect(normalizeInput('help')).toBe('help')
21+
expect(normalizeInput('ref-abc123')).toBe('ref-abc123')
22+
expect(normalizeInput('some prompt text')).toBe('some prompt text')
23+
})
24+
25+
test('handles empty string', () => {
26+
expect(normalizeInput('')).toBe('')
27+
})
28+
29+
test('handles only slash', () => {
30+
expect(normalizeInput('/')).toBe('')
31+
})
32+
33+
test('handles multiple slashes', () => {
34+
expect(normalizeInput('//help')).toBe('/help')
35+
expect(normalizeInput('///test')).toBe('//test')
36+
})
37+
38+
test('preserves internal slashes', () => {
39+
expect(normalizeInput('/path/to/file')).toBe('path/to/file')
40+
expect(normalizeInput('path/to/file')).toBe('path/to/file')
41+
})
42+
43+
test('preserves whitespace in input', () => {
44+
expect(normalizeInput('/help me')).toBe('help me')
45+
expect(normalizeInput('help me')).toBe('help me')
46+
})
47+
})
48+
49+
describe('isSlashCommand', () => {
50+
test('returns true for input starting with /', () => {
51+
expect(isSlashCommand('/help')).toBe(true)
52+
expect(isSlashCommand('/logout')).toBe(true)
53+
expect(isSlashCommand('/ref-abc123')).toBe(true)
54+
expect(isSlashCommand('/')).toBe(true)
55+
})
56+
57+
test('returns false for input not starting with /', () => {
58+
expect(isSlashCommand('help')).toBe(false)
59+
expect(isSlashCommand('logout')).toBe(false)
60+
expect(isSlashCommand('ref-abc123')).toBe(false)
61+
expect(isSlashCommand('')).toBe(false)
62+
})
63+
64+
test('handles whitespace correctly', () => {
65+
expect(isSlashCommand(' /help')).toBe(true)
66+
expect(isSlashCommand(' help')).toBe(false)
67+
})
68+
})
69+
70+
describe('parseCommand', () => {
71+
test('extracts command from slashed input', () => {
72+
expect(parseCommand('/help')).toBe('help')
73+
expect(parseCommand('/logout')).toBe('logout')
74+
expect(parseCommand('/usage')).toBe('usage')
75+
})
76+
77+
test('returns empty string for unslashed input (not a slash command)', () => {
78+
expect(parseCommand('help')).toBe('')
79+
expect(parseCommand('logout')).toBe('')
80+
expect(parseCommand('usage')).toBe('')
81+
expect(parseCommand('login to my database')).toBe('')
82+
})
83+
84+
test('extracts first word as command when there are arguments', () => {
85+
expect(parseCommand('/help me')).toBe('help')
86+
expect(parseCommand('/usage stats')).toBe('usage')
87+
})
88+
89+
test('converts command to lowercase', () => {
90+
expect(parseCommand('/HELP')).toBe('help')
91+
expect(parseCommand('/LOGOUT')).toBe('logout')
92+
expect(parseCommand('/UsAgE')).toBe('usage')
93+
})
94+
95+
test('handles empty string', () => {
96+
expect(parseCommand('')).toBe('')
97+
})
98+
99+
test('handles whitespace-only input', () => {
100+
expect(parseCommand(' ')).toBe('')
101+
})
102+
103+
test('handles only slash', () => {
104+
expect(parseCommand('/')).toBe('')
105+
})
106+
107+
test('handles multiple spaces between words', () => {
108+
expect(parseCommand('/help me')).toBe('help')
109+
})
110+
})
111+
112+
describe('isReferralCode', () => {
113+
test('recognizes referral codes with slash prefix', () => {
114+
expect(isReferralCode('/ref-abc123')).toBe(true)
115+
expect(isReferralCode('/ref-XYZ')).toBe(true)
116+
expect(isReferralCode('/ref-')).toBe(true)
117+
})
118+
119+
test('recognizes referral codes without slash prefix', () => {
120+
expect(isReferralCode('ref-abc123')).toBe(true)
121+
expect(isReferralCode('ref-XYZ')).toBe(true)
122+
expect(isReferralCode('ref-')).toBe(true)
123+
})
124+
125+
test('rejects inputs that are not referral codes', () => {
126+
expect(isReferralCode('reference')).toBe(false)
127+
expect(isReferralCode('refund')).toBe(false)
128+
expect(isReferralCode('/reference')).toBe(false)
129+
expect(isReferralCode('ref abc')).toBe(false)
130+
expect(isReferralCode('')).toBe(false)
131+
})
132+
133+
test('is case-sensitive for ref- prefix', () => {
134+
expect(isReferralCode('REF-abc')).toBe(false)
135+
expect(isReferralCode('Ref-abc')).toBe(false)
136+
expect(isReferralCode('/REF-abc')).toBe(false)
137+
})
138+
})
139+
140+
describe('slash commands only work with / prefix', () => {
141+
const slashCommands = [
142+
'login',
143+
'logout',
144+
'usage',
145+
'credits',
146+
'exit',
147+
'clear',
148+
'new',
149+
'init',
150+
'bash',
151+
'feedback',
152+
]
153+
154+
for (const cmd of slashCommands) {
155+
test(`"/${cmd}" is recognized as slash command`, () => {
156+
expect(parseCommand(`/${cmd}`)).toBe(cmd)
157+
})
158+
159+
test(`"${cmd}" without slash is NOT a slash command (sent to agent)`, () => {
160+
expect(parseCommand(cmd)).toBe('')
161+
})
162+
}
163+
})
164+
165+
describe('words that look like commands but are not', () => {
166+
const nonCommands = [
167+
'login to my account',
168+
'I need help with logout functionality',
169+
'please help me',
170+
'usage of this function',
171+
'clear the database',
172+
]
173+
174+
for (const input of nonCommands) {
175+
test(`"${input}" is NOT a slash command`, () => {
176+
expect(parseCommand(input)).toBe('')
177+
})
178+
}
179+
})
180+
181+
describe('referral code detection with different input formats', () => {
182+
const validCodes = [
183+
'ref-abc123',
184+
'/ref-abc123',
185+
'ref-TEST',
186+
'/ref-TEST',
187+
'ref-12345',
188+
'/ref-12345',
189+
]
190+
191+
const invalidCodes = [
192+
'reference',
193+
'/reference',
194+
'refund-123',
195+
'/refund-123',
196+
'REF-abc',
197+
'/REF-abc',
198+
'ref abc',
199+
'/ref abc',
200+
'',
201+
'/',
202+
]
203+
204+
for (const code of validCodes) {
205+
test(`recognizes "${code}" as valid referral code`, () => {
206+
expect(isReferralCode(code)).toBe(true)
207+
})
208+
}
209+
210+
for (const code of invalidCodes) {
211+
test(`rejects "${code}" as referral code`, () => {
212+
expect(isReferralCode(code)).toBe(false)
213+
})
214+
}
215+
})
216+
})
217+
218+
describe('command-registry', () => {
219+
describe('findCommand', () => {
220+
test('finds command by name', () => {
221+
const login = findCommand('login')
222+
expect(login).toBeDefined()
223+
expect(login?.name).toBe('login')
224+
225+
const usage = findCommand('usage')
226+
expect(usage).toBeDefined()
227+
expect(usage?.name).toBe('usage')
228+
})
229+
230+
test('finds command by alias', () => {
231+
const credits = findCommand('credits')
232+
expect(credits).toBeDefined()
233+
expect(credits?.name).toBe('usage')
234+
235+
const quit = findCommand('quit')
236+
expect(quit).toBeDefined()
237+
expect(quit?.name).toBe('exit')
238+
239+
const signin = findCommand('signin')
240+
expect(signin).toBeDefined()
241+
expect(signin?.name).toBe('login')
242+
})
243+
244+
test('returns undefined for unknown command', () => {
245+
expect(findCommand('unknown')).toBeUndefined()
246+
expect(findCommand('notacommand')).toBeUndefined()
247+
})
248+
249+
test('is case insensitive', () => {
250+
expect(findCommand('LOGIN')?.name).toBe('login')
251+
expect(findCommand('UsAgE')?.name).toBe('usage')
252+
expect(findCommand('CREDITS')?.name).toBe('usage')
253+
})
254+
})
255+
256+
describe('COMMAND_REGISTRY', () => {
257+
test('all commands have unique names', () => {
258+
const names = COMMAND_REGISTRY.map((c) => c.name)
259+
const uniqueNames = new Set(names)
260+
expect(names.length).toBe(uniqueNames.size)
261+
})
262+
263+
test('all aliases are unique across all commands', () => {
264+
const allAliases = COMMAND_REGISTRY.flatMap((c) => c.aliases)
265+
const uniqueAliases = new Set(allAliases)
266+
expect(allAliases.length).toBe(uniqueAliases.size)
267+
})
268+
269+
test('no alias conflicts with command names', () => {
270+
const names = new Set(COMMAND_REGISTRY.map((c) => c.name))
271+
const allAliases = COMMAND_REGISTRY.flatMap((c) => c.aliases)
272+
for (const alias of allAliases) {
273+
expect(names.has(alias)).toBe(false)
274+
}
275+
})
276+
})
277+
})

0 commit comments

Comments
 (0)