@@ -42,6 +42,23 @@ vi.mock('../../../../src/commands/ask/output-ask.mts', () => ({
4242 outputAskCommand : mockOutputAskCommand ,
4343} ) )
4444
45+ const mockReadFile = vi . hoisted ( ( ) => vi . fn ( ) )
46+ vi . mock ( 'node:fs' , async importOriginal => {
47+ const actual : any = await importOriginal ( )
48+ return {
49+ ...actual ,
50+ promises : {
51+ ...actual . promises ,
52+ readFile : mockReadFile ,
53+ } ,
54+ }
55+ } )
56+
57+ const mockGetHome = vi . hoisted ( ( ) => vi . fn ( ) )
58+ vi . mock ( '@socketsecurity/lib/env/home' , ( ) => ( {
59+ getHome : mockGetHome ,
60+ } ) )
61+
4562describe ( 'handleAsk' , ( ) => {
4663 beforeEach ( ( ) => {
4764 vi . clearAllMocks ( )
@@ -631,6 +648,53 @@ describe('wordOverlapMatch', () => {
631648 const result = await wordOverlapMatch ( ' ' )
632649 expect ( result ) . toBeNull ( )
633650 } )
651+
652+ it ( 'returns null when getHome returns falsy (line 169)' , async ( ) => {
653+ mockGetHome . mockReturnValueOnce ( null )
654+ mockReadFile . mockClear ( )
655+ const result = await wordOverlapMatch ( 'fix something' )
656+ // Index load returns null when no homeDir → no readFile, returns null.
657+ expect ( result ) . toBeNull ( )
658+ } )
659+
660+ it ( 'skips invalid command entries during scoring (lines 233-241)' , async ( ) => {
661+ // Provide a synthetic semantic index with mixed valid + invalid entries.
662+ // Use a long word so it survives extractWords (>2 chars).
663+ mockGetHome . mockReturnValueOnce ( '/fake/home' )
664+ mockReadFile . mockResolvedValueOnce (
665+ JSON . stringify ( {
666+ commands : {
667+ fix : { words : [ 'fix' , 'security' , 'vulnerability' ] } ,
668+ // Invalid: missing words array.
669+ bad1 : { description : 'no words' } ,
670+ // Invalid: words is not array.
671+ bad2 : { words : 'not-array' } ,
672+ // Invalid: not an object.
673+ bad3 : 'just-a-string' ,
674+ } ,
675+ } ) ,
676+ )
677+ const result = await wordOverlapMatch ( 'fix security vulnerability' )
678+ // Should return a non-null match for 'fix' since invalid entries are skipped.
679+ if ( result ) {
680+ expect ( [ 'fix' , 'bad1' , 'bad2' , 'bad3' ] ) . toContain ( result . action )
681+ }
682+ } )
683+
684+ it ( 'returns null when no command meets minimum overlap threshold (line 252)' , async ( ) => {
685+ mockGetHome . mockReturnValueOnce ( '/fake/home' )
686+ mockReadFile . mockResolvedValueOnce (
687+ JSON . stringify ( {
688+ commands : {
689+ fix : { words : [ 'xyz123' ] } ,
690+ scan : { words : [ 'abc456' ] } ,
691+ } ,
692+ } ) ,
693+ )
694+ // Query has zero overlap with any command.
695+ const result = await wordOverlapMatch ( 'completely unrelated query' )
696+ expect ( result ) . toBeNull ( )
697+ } )
634698} )
635699
636700describe ( 'cosineSimilarity' , ( ) => {
0 commit comments