@@ -3,11 +3,18 @@ import { createStore } from "jotai"
33import { shellModeActiveAtom , toggleShellModeAtom , executeShellCommandAtom , keyboardHandlerAtom } from "../keyboard.js"
44import { inputModeAtom } from "../ui.js"
55import type { Key } from "../../../types/keyboard.js"
6- import { shellHistoryAtom , shellHistoryIndexAtom } from "../shell.js"
6+ import {
7+ shellHistoryAtom ,
8+ shellHistoryIndexAtom ,
9+ navigateShellHistoryUpAtom ,
10+ navigateShellHistoryDownAtom ,
11+ addToShellHistoryAtom ,
12+ } from "../shell.js"
13+ import { textBufferStringAtom } from "../textBuffer.js"
714
815// Mock child_process to avoid actual command execution
916vi . mock ( "child_process" , ( ) => ( {
10- exec : vi . fn ( ( command , callback ) => {
17+ exec : vi . fn ( ( command ) => {
1118 // Simulate successful command execution
1219 const stdout = `Mock output for: ${ command } `
1320 const stderr = ""
@@ -32,14 +39,11 @@ vi.mock("child_process", () => ({
3239 }
3340 } ) ,
3441 }
35- if ( callback ) {
36- callback ( process )
37- }
3842 return process
3943 } ) ,
4044} ) )
4145
42- describe ( "shell mode - essential tests" , ( ) => {
46+ describe ( "shell mode - comprehensive tests" , ( ) => {
4347 let store : ReturnType < typeof createStore >
4448
4549 beforeEach ( ( ) => {
@@ -48,6 +52,7 @@ describe("shell mode - essential tests", () => {
4852 store . set ( shellHistoryAtom , [ ] )
4953 store . set ( shellModeActiveAtom , false )
5054 store . set ( inputModeAtom , "normal" as const )
55+ store . set ( shellHistoryIndexAtom , - 1 )
5156 } )
5257
5358 describe ( "shell mode activation" , ( ) => {
@@ -67,6 +72,50 @@ describe("shell mode - essential tests", () => {
6772 expect ( store . get ( inputModeAtom ) ) . toBe ( "normal" )
6873 } )
6974
75+ it ( "should reset history index when toggling on" , ( ) => {
76+ // Set a non-default history index
77+ store . set ( shellHistoryIndexAtom , 5 )
78+
79+ // Toggle on
80+ store . set ( toggleShellModeAtom )
81+
82+ // Index should be reset
83+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( - 1 )
84+ } )
85+
86+ it ( "should reset history index when toggling off" , ( ) => {
87+ // Activate shell mode and set history index
88+ store . set ( toggleShellModeAtom )
89+ store . set ( shellHistoryIndexAtom , 3 )
90+
91+ // Toggle off
92+ store . set ( toggleShellModeAtom )
93+
94+ // Index should be reset
95+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( - 1 )
96+ } )
97+
98+ it ( "should handle multiple rapid toggles" , ( ) => {
99+ // Toggle multiple times
100+ store . set ( toggleShellModeAtom )
101+ expect ( store . get ( shellModeActiveAtom ) ) . toBe ( true )
102+
103+ store . set ( toggleShellModeAtom )
104+ expect ( store . get ( shellModeActiveAtom ) ) . toBe ( false )
105+
106+ store . set ( toggleShellModeAtom )
107+ expect ( store . get ( shellModeActiveAtom ) ) . toBe ( true )
108+
109+ store . set ( toggleShellModeAtom )
110+ expect ( store . get ( shellModeActiveAtom ) ) . toBe ( false )
111+
112+ // Final state should be consistent
113+ expect ( store . get ( inputModeAtom ) ) . toBe ( "normal" )
114+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( - 1 )
115+ } )
116+ } )
117+
118+ describe ( "shell command execution" , ( ) => {
70119 it ( "should add commands to history" , async ( ) => {
71120 const command = "echo 'test'"
72121 await store . set ( executeShellCommandAtom , command )
@@ -84,6 +133,23 @@ describe("shell mode - essential tests", () => {
84133 expect ( history ) . toHaveLength ( 0 )
85134 } )
86135
136+ it ( "should trim whitespace from commands before adding to history" , async ( ) => {
137+ const command = " echo 'test' "
138+ await store . set ( executeShellCommandAtom , command )
139+
140+ const history = store . get ( shellHistoryAtom )
141+ expect ( history [ 0 ] ) . toBe ( "echo 'test'" )
142+ } )
143+
144+ it ( "should add multiple unique commands to history" , async ( ) => {
145+ await store . set ( executeShellCommandAtom , "ls" )
146+ await store . set ( executeShellCommandAtom , "pwd" )
147+ await store . set ( executeShellCommandAtom , "echo test" )
148+
149+ const history = store . get ( shellHistoryAtom )
150+ expect ( history ) . toEqual ( [ "ls" , "pwd" , "echo test" ] )
151+ } )
152+
87153 it ( "should reset history navigation index after command execution" , async ( ) => {
88154 // Add a few commands to history
89155 await store . set ( executeShellCommandAtom , "echo 'test1'" )
@@ -99,6 +165,189 @@ describe("shell mode - essential tests", () => {
99165 // History index should be reset to -1
100166 expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( - 1 )
101167 } )
168+
169+ it ( "should allow duplicate commands in history" , async ( ) => {
170+ await store . set ( executeShellCommandAtom , "echo test" )
171+ await store . set ( executeShellCommandAtom , "ls" )
172+ await store . set ( executeShellCommandAtom , "echo test" )
173+
174+ const history = store . get ( shellHistoryAtom )
175+ expect ( history ) . toEqual ( [ "echo test" , "ls" , "echo test" ] )
176+ } )
177+ } )
178+
179+ describe ( "shell history management" , ( ) => {
180+ it ( "should limit history to 100 commands" , async ( ) => {
181+ // Add 105 commands
182+ for ( let i = 0 ; i < 105 ; i ++ ) {
183+ await store . set ( executeShellCommandAtom , `command${ i } ` )
184+ }
185+
186+ const history = store . get ( shellHistoryAtom )
187+ expect ( history ) . toHaveLength ( 100 )
188+ // Should keep the most recent 100
189+ expect ( history [ 0 ] ) . toBe ( "command5" )
190+ expect ( history [ 99 ] ) . toBe ( "command104" )
191+ } )
192+
193+ it ( "should add command to history with addToShellHistoryAtom" , ( ) => {
194+ store . set ( addToShellHistoryAtom , "test command" )
195+ const history = store . get ( shellHistoryAtom )
196+ expect ( history ) . toContain ( "test command" )
197+ } )
198+
199+ it ( "should maintain history order (newest last)" , async ( ) => {
200+ await store . set ( executeShellCommandAtom , "first" )
201+ await store . set ( executeShellCommandAtom , "second" )
202+ await store . set ( executeShellCommandAtom , "third" )
203+
204+ const history = store . get ( shellHistoryAtom )
205+ expect ( history ) . toEqual ( [ "first" , "second" , "third" ] )
206+ } )
207+ } )
208+
209+ describe ( "history navigation - up" , ( ) => {
210+ it ( "should navigate to most recent command on first up" , ( ) => {
211+ // Add commands to history
212+ store . set ( shellHistoryAtom , [ "cmd1" , "cmd2" , "cmd3" ] )
213+
214+ // Navigate up
215+ store . set ( navigateShellHistoryUpAtom )
216+
217+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( 2 )
218+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "cmd3" )
219+ } )
220+
221+ it ( "should navigate to older commands with successive up presses" , ( ) => {
222+ store . set ( shellHistoryAtom , [ "cmd1" , "cmd2" , "cmd3" ] )
223+
224+ // First up - most recent
225+ store . set ( navigateShellHistoryUpAtom )
226+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( 2 )
227+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "cmd3" )
228+
229+ // Second up - older
230+ store . set ( navigateShellHistoryUpAtom )
231+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( 1 )
232+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "cmd2" )
233+
234+ // Third up - oldest
235+ store . set ( navigateShellHistoryUpAtom )
236+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( 0 )
237+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "cmd1" )
238+ } )
239+
240+ it ( "should stop at oldest command" , ( ) => {
241+ store . set ( shellHistoryAtom , [ "cmd1" , "cmd2" ] )
242+
243+ // Navigate to oldest
244+ store . set ( navigateShellHistoryUpAtom )
245+ store . set ( navigateShellHistoryUpAtom )
246+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( 0 )
247+
248+ // Try to go further up
249+ store . set ( navigateShellHistoryUpAtom )
250+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( 0 )
251+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "cmd1" )
252+ } )
253+
254+ it ( "should do nothing when history is empty" , ( ) => {
255+ store . set ( shellHistoryAtom , [ ] )
256+
257+ store . set ( navigateShellHistoryUpAtom )
258+
259+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( - 1 )
260+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "" )
261+ } )
262+
263+ it ( "should handle single command history" , ( ) => {
264+ store . set ( shellHistoryAtom , [ "only-cmd" ] )
265+
266+ store . set ( navigateShellHistoryUpAtom )
267+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( 0 )
268+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "only-cmd" )
269+
270+ // Try to go up again
271+ store . set ( navigateShellHistoryUpAtom )
272+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( 0 )
273+ } )
274+ } )
275+
276+ describe ( "history navigation - down" , ( ) => {
277+ it ( "should do nothing when at default index" , ( ) => {
278+ store . set ( shellHistoryAtom , [ "cmd1" , "cmd2" , "cmd3" ] )
279+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( - 1 )
280+
281+ store . set ( navigateShellHistoryDownAtom )
282+
283+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( - 1 )
284+ } )
285+
286+ it ( "should navigate to newer commands" , ( ) => {
287+ store . set ( shellHistoryAtom , [ "cmd1" , "cmd2" , "cmd3" , "cmd4" ] )
288+
289+ // Go to oldest
290+ store . set ( shellHistoryIndexAtom , 0 )
291+
292+ // Navigate down to newer
293+ store . set ( navigateShellHistoryDownAtom )
294+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( 1 )
295+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "cmd2" )
296+
297+ store . set ( navigateShellHistoryDownAtom )
298+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( 2 )
299+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "cmd3" )
300+ } )
301+
302+ it ( "should clear input when reaching most recent" , ( ) => {
303+ store . set ( shellHistoryAtom , [ "cmd1" , "cmd2" ] )
304+
305+ // Navigate up and then back down
306+ store . set ( navigateShellHistoryUpAtom ) // index 1 (cmd2)
307+ store . set ( navigateShellHistoryUpAtom ) // index 0 (cmd1)
308+ store . set ( navigateShellHistoryDownAtom ) // index 1 (cmd2)
309+ store . set ( navigateShellHistoryDownAtom ) // index -1 (clear)
310+
311+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( - 1 )
312+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "" )
313+ } )
314+
315+ it ( "should handle navigation cycle: up then all the way down" , ( ) => {
316+ store . set ( shellHistoryAtom , [ "cmd1" , "cmd2" , "cmd3" ] )
317+
318+ // Go up to recent
319+ store . set ( navigateShellHistoryUpAtom )
320+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "cmd3" )
321+
322+ // Go all the way down to clear
323+ store . set ( navigateShellHistoryDownAtom )
324+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( - 1 )
325+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "" )
326+ } )
327+ } )
328+
329+ describe ( "history navigation - combined up/down" , ( ) => {
330+ it ( "should handle mixed up/down navigation" , ( ) => {
331+ store . set ( shellHistoryAtom , [ "cmd1" , "cmd2" , "cmd3" , "cmd4" ] )
332+
333+ // Up twice
334+ store . set ( navigateShellHistoryUpAtom ) // cmd4
335+ store . set ( navigateShellHistoryUpAtom ) // cmd3
336+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "cmd3" )
337+
338+ // Down once
339+ store . set ( navigateShellHistoryDownAtom ) // cmd4
340+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "cmd4" )
341+
342+ // Up once
343+ store . set ( navigateShellHistoryUpAtom ) // cmd3
344+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "cmd3" )
345+
346+ // Up to oldest
347+ store . set ( navigateShellHistoryUpAtom ) // cmd2
348+ store . set ( navigateShellHistoryUpAtom ) // cmd1
349+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "cmd1" )
350+ } )
102351 } )
103352
104353 describe ( "Shift+1 key detection" , ( ) => {
@@ -119,5 +368,66 @@ describe("shell mode - essential tests", () => {
119368 expect ( store . get ( shellModeActiveAtom ) ) . toBe ( true )
120369 expect ( store . get ( inputModeAtom ) ) . toBe ( "shell" )
121370 } )
371+
372+ it ( "should toggle shell mode off with second Shift+1" , async ( ) => {
373+ const shift1Key : Key = {
374+ name : "shift-1" ,
375+ sequence : "!" ,
376+ ctrl : false ,
377+ meta : false ,
378+ shift : true ,
379+ paste : false ,
380+ }
381+
382+ // Activate
383+ await store . set ( keyboardHandlerAtom , shift1Key )
384+ expect ( store . get ( shellModeActiveAtom ) ) . toBe ( true )
385+
386+ // Deactivate
387+ await store . set ( keyboardHandlerAtom , shift1Key )
388+ expect ( store . get ( shellModeActiveAtom ) ) . toBe ( false )
389+ expect ( store . get ( inputModeAtom ) ) . toBe ( "normal" )
390+ } )
391+ } )
392+
393+ describe ( "edge cases" , ( ) => {
394+ it ( "should handle empty string command gracefully" , async ( ) => {
395+ await store . set ( executeShellCommandAtom , "" )
396+ const history = store . get ( shellHistoryAtom )
397+ expect ( history ) . toHaveLength ( 0 )
398+ } )
399+
400+ it ( "should handle only whitespace command" , async ( ) => {
401+ await store . set ( executeShellCommandAtom , " \t\n " )
402+ const history = store . get ( shellHistoryAtom )
403+ expect ( history ) . toHaveLength ( 0 )
404+ } )
405+
406+ it ( "should preserve history when toggling shell mode" , ( ) => {
407+ store . set ( shellHistoryAtom , [ "cmd1" , "cmd2" ] )
408+
409+ // Toggle on and off
410+ store . set ( toggleShellModeAtom )
411+ store . set ( toggleShellModeAtom )
412+
413+ // History should be preserved
414+ const history = store . get ( shellHistoryAtom )
415+ expect ( history ) . toEqual ( [ "cmd1" , "cmd2" ] )
416+ } )
417+
418+ it ( "should handle history navigation after clearing history" , ( ) => {
419+ store . set ( shellHistoryAtom , [ "cmd1" , "cmd2" ] )
420+ store . set ( navigateShellHistoryUpAtom )
421+ expect ( store . get ( textBufferStringAtom ) ) . toBe ( "cmd2" )
422+ const indexBeforeClear = store . get ( shellHistoryIndexAtom )
423+
424+ // Clear history
425+ store . set ( shellHistoryAtom , [ ] )
426+
427+ // Try to navigate - should return early and not change index
428+ store . set ( navigateShellHistoryUpAtom )
429+ // Index should remain unchanged when history is empty
430+ expect ( store . get ( shellHistoryIndexAtom ) ) . toBe ( indexBeforeClear )
431+ } )
122432 } )
123433} )
0 commit comments