@@ -12,13 +12,15 @@ vi.mock("vscode", () => {
1212 constructor (
1313 public callId : string ,
1414 public name : string ,
15+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1516 public input : any ,
1617 ) { }
1718 }
1819
1920 return {
2021 workspace : {
2122 getConfiguration : vi . fn ( ( ) => ( {
23+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2224 get : vi . fn ( ( key : string , defaultValue : any ) => defaultValue ) ,
2325 } ) ) ,
2426 onDidChangeConfiguration : vi . fn ( ( _callback ) => ( {
@@ -106,6 +108,14 @@ describe("VsCodeLmHandler", () => {
106108 // Should reset client when config changes
107109 expect ( handler [ "client" ] ) . toBeNull ( )
108110 } )
111+
112+ it ( "should call initializeClient during construction" , ( ) => {
113+ // Constructor calls initializeClient() without await, so it starts async initialization.
114+ // Verify the handler is created and initializeClient was triggered.
115+ expect ( handler ) . toBeDefined ( )
116+ // The constructor triggers initializeClient which calls selectChatModels
117+ expect ( vscode . lm . selectChatModels ) . toHaveBeenCalled ( )
118+ } )
109119 } )
110120
111121 describe ( "createClient" , ( ) => {
@@ -135,6 +145,14 @@ describe("VsCodeLmHandler", () => {
135145 expect ( client . id ) . toBe ( "default-lm" )
136146 expect ( client . vendor ) . toBe ( "vscode" )
137147 } )
148+
149+ it ( "should throw a Zoo Code branded error when selectChatModels fails" , async ( ) => {
150+ ; ( vscode . lm . selectChatModels as Mock ) . mockRejectedValueOnce ( new Error ( "network down" ) )
151+
152+ await expect ( handler [ "createClient" ] ( { vendor : "test" } ) ) . rejects . toThrow (
153+ "Zoo Code <Language Model API>: Failed to select model: network down" ,
154+ )
155+ } )
138156 } )
139157
140158 describe ( "createMessage" , ( ) => {
@@ -471,18 +489,264 @@ describe("VsCodeLmHandler", () => {
471489 expect . stringContaining ( "STREAM_ERROR" ) ,
472490 )
473491
492+ consoleErrorSpy . mockRestore ( )
493+ } )
494+ it ( "should log Zoo Code branded warning for unknown chunk type in stream" , async ( ) => {
495+ const systemPrompt = "You are a helpful assistant"
496+ const messages : Anthropic . Messages . MessageParam [ ] = [ { role : "user" as const , content : "Hello" } ]
497+
498+ const consoleWarnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
499+
500+ mockLanguageModelChat . sendRequest . mockResolvedValueOnce ( {
501+ stream : ( async function * ( ) {
502+ // Yield an unknown chunk type (not TextPart, not ToolCallPart)
503+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
504+ yield { type : "unknown" , foo : "bar" } as any
505+ return
506+ } ) ( ) ,
507+ text : ( async function * ( ) {
508+ yield ""
509+ return
510+ } ) ( ) ,
511+ } )
512+
513+ const stream = handler . createMessage ( systemPrompt , messages )
514+ for await ( const _chunk of stream ) {
515+ // drain
516+ }
517+
518+ expect ( consoleWarnSpy ) . toHaveBeenCalledWith (
519+ "Zoo Code <Language Model API>: Unknown chunk type received:" ,
520+ expect . objectContaining ( { type : "unknown" } ) ,
521+ )
522+
523+ consoleWarnSpy . mockRestore ( )
524+ } )
525+
526+ it ( "should log Zoo Code branded warning for invalid text part value" , async ( ) => {
527+ const systemPrompt = "You are a helpful assistant"
528+ const messages : Anthropic . Messages . MessageParam [ ] = [ { role : "user" as const , content : "Hello" } ]
529+
530+ const consoleWarnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
531+
532+ // Create a TextPart with a non-string value (number)
533+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
534+ const badTextPart = new vscode . LanguageModelTextPart ( 42 as any )
535+ mockLanguageModelChat . sendRequest . mockResolvedValueOnce ( {
536+ stream : ( async function * ( ) {
537+ yield badTextPart
538+ return
539+ } ) ( ) ,
540+ text : ( async function * ( ) {
541+ yield ""
542+ return
543+ } ) ( ) ,
544+ } )
545+
546+ const stream = handler . createMessage ( systemPrompt , messages )
547+ for await ( const _chunk of stream ) {
548+ // drain
549+ }
550+
551+ expect ( consoleWarnSpy ) . toHaveBeenCalledWith (
552+ "Zoo Code <Language Model API>: Invalid text part value received:" ,
553+ 42 ,
554+ )
555+
556+ consoleWarnSpy . mockRestore ( )
557+ } )
558+
559+ it ( "should log Zoo Code branded warning for invalid tool callId" , async ( ) => {
560+ const systemPrompt = "You are a helpful assistant"
561+ const messages : Anthropic . Messages . MessageParam [ ] = [ { role : "user" as const , content : "Hello" } ]
562+
563+ const consoleWarnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
564+
565+ // Create a ToolCallPart with a non-string callId
566+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
567+ const badToolCall = new vscode . LanguageModelToolCallPart ( 123 as any , "valid-name" , { } )
568+ mockLanguageModelChat . sendRequest . mockResolvedValueOnce ( {
569+ stream : ( async function * ( ) {
570+ yield badToolCall
571+ return
572+ } ) ( ) ,
573+ text : ( async function * ( ) {
574+ yield ""
575+ return
576+ } ) ( ) ,
577+ } )
578+
579+ const stream = handler . createMessage ( systemPrompt , messages )
580+ for await ( const _chunk of stream ) {
581+ // drain
582+ }
583+
584+ expect ( consoleWarnSpy ) . toHaveBeenCalledWith (
585+ "Zoo Code <Language Model API>: Invalid tool callId received:" ,
586+ 123 ,
587+ )
588+
589+ consoleWarnSpy . mockRestore ( )
590+ } )
591+
592+ it ( "should log Zoo Code branded warning for invalid tool input" , async ( ) => {
593+ const systemPrompt = "You are a helpful assistant"
594+ const messages : Anthropic . Messages . MessageParam [ ] = [ { role : "user" as const , content : "Hello" } ]
595+
596+ const consoleWarnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
597+
598+ // Create a ToolCallPart with a string input (not an object)
599+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
600+ const badToolCall = new vscode . LanguageModelToolCallPart ( "call-1" , "valid-name" , "not-an-object" as any )
601+ mockLanguageModelChat . sendRequest . mockResolvedValueOnce ( {
602+ stream : ( async function * ( ) {
603+ yield badToolCall
604+ return
605+ } ) ( ) ,
606+ text : ( async function * ( ) {
607+ yield ""
608+ return
609+ } ) ( ) ,
610+ } )
611+
612+ const stream = handler . createMessage ( systemPrompt , messages )
613+ for await ( const _chunk of stream ) {
614+ // drain
615+ }
616+
617+ expect ( consoleWarnSpy ) . toHaveBeenCalledWith (
618+ "Zoo Code <Language Model API>: Invalid tool input received:" ,
619+ "not-an-object" ,
620+ )
621+
622+ consoleWarnSpy . mockRestore ( )
623+ } )
624+
625+ it ( "should log Zoo Code branded error when tool call processing fails" , async ( ) => {
626+ const systemPrompt = "You are a helpful assistant"
627+ const messages : Anthropic . Messages . MessageParam [ ] = [ { role : "user" as const , content : "Hello" } ]
628+
629+ const consoleErrorSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
630+
631+ // Create a ToolCallPart with circular input that will throw on JSON.stringify
632+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
633+ const circularInput : any = { name : "circular" }
634+ circularInput . self = circularInput
635+
636+ const badToolCall = new vscode . LanguageModelToolCallPart ( "call-1" , "valid-name" , circularInput )
637+ mockLanguageModelChat . sendRequest . mockResolvedValueOnce ( {
638+ stream : ( async function * ( ) {
639+ yield badToolCall
640+ return
641+ } ) ( ) ,
642+ text : ( async function * ( ) {
643+ yield ""
644+ return
645+ } ) ( ) ,
646+ } )
647+
648+ const stream = handler . createMessage ( systemPrompt , messages , {
649+ taskId : "test-task" ,
650+ tools : [
651+ {
652+ type : "function" as const ,
653+ function : { name : "test" , description : "" , parameters : { type : "object" , properties : { } } } ,
654+ } ,
655+ ] ,
656+ } )
657+ for await ( const _chunk of stream ) {
658+ // drain
659+ }
660+
661+ expect ( consoleErrorSpy ) . toHaveBeenCalledWith (
662+ "Zoo Code <Language Model API>: Failed to process tool call:" ,
663+ expect . any ( Error ) ,
664+ )
665+
666+ consoleErrorSpy . mockRestore ( )
667+ } )
668+ } )
669+
670+ describe ( "getClient" , ( ) => {
671+ it ( "should log Zoo Code branded debug when creating client with selector" , async ( ) => {
672+ const consoleDebugSpy = vi . spyOn ( console , "debug" ) . mockImplementation ( ( ) => { } )
673+ const mockModel = { ...mockLanguageModelChat }
674+ ; ( vscode . lm . selectChatModels as Mock ) . mockResolvedValue ( [ mockModel ] )
675+ handler [ "client" ] = null
676+
677+ // @ts -ignore – access private method for coverage
678+ await handler [ "getClient" ] ( )
679+
680+ expect ( consoleDebugSpy ) . toHaveBeenCalledWith (
681+ "Zoo Code <Language Model API>: Creating client with selector:" ,
682+ expect . any ( Object ) ,
683+ )
684+
685+ consoleDebugSpy . mockRestore ( )
686+ } )
687+
688+ it ( "should throw a Zoo Code branded error when getClient fails to create client" , async ( ) => {
689+ const consoleErrorSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
690+ ; ( vscode . lm . selectChatModels as Mock ) . mockRejectedValueOnce ( new Error ( "network error" ) )
691+ handler [ "client" ] = null
692+
693+ // @ts -ignore – access private method for coverage
694+ await expect ( handler [ "getClient" ] ( ) ) . rejects . toThrow (
695+ "Zoo Code <Language Model API>: Failed to create client:" ,
696+ )
697+
698+ expect ( consoleErrorSpy ) . toHaveBeenCalledWith (
699+ "Zoo Code <Language Model API>: Client creation failed:" ,
700+ expect . stringContaining ( "network error" ) ,
701+ )
702+
474703 consoleErrorSpy . mockRestore ( )
475704 } )
476705 } )
477706
478707 describe ( "initializeClient" , ( ) => {
708+ it ( "should log when client is already initialized" , async ( ) => {
709+ const consoleDebugSpy = vi . spyOn ( console , "debug" ) . mockImplementation ( ( ) => { } )
710+
711+ handler [ "client" ] = mockLanguageModelChat
712+ await handler . initializeClient ( )
713+
714+ expect ( consoleDebugSpy ) . toHaveBeenCalledWith ( "Zoo Code <Language Model API>: Client already initialized" )
715+
716+ consoleDebugSpy . mockRestore ( )
717+ } )
718+
719+ it ( "should log success when client is initialized" , async ( ) => {
720+ const consoleDebugSpy = vi . spyOn ( console , "debug" ) . mockImplementation ( ( ) => { } )
721+ const mockModel = { ...mockLanguageModelChat }
722+ ; ( vscode . lm . selectChatModels as Mock ) . mockResolvedValue ( [ mockModel ] )
723+ handler [ "client" ] = null
724+
725+ await handler . initializeClient ( )
726+
727+ expect ( consoleDebugSpy ) . toHaveBeenCalledWith (
728+ "Zoo Code <Language Model API>: Client initialized successfully" ,
729+ )
730+
731+ consoleDebugSpy . mockRestore ( )
732+ } )
733+
479734 it ( "should throw a Zoo Code branded error when client initialization fails" , async ( ) => {
480- ; ( vscode . lm . selectChatModels as Mock ) . mockRejectedValueOnce ( new Error ( "select failed" ) )
735+ const consoleErrorSpy = vi . spyOn ( console , "error" ) . mockImplementation ( ( ) => { } )
736+ // Use mockRejectedValue (not Once) because beforeEach already called selectChatModels once
737+ ; ( vscode . lm . selectChatModels as Mock ) . mockRejectedValue ( new Error ( "select failed" ) )
481738 handler [ "client" ] = null
482739
483740 await expect ( handler . initializeClient ( ) ) . rejects . toThrow (
484- "Zoo Code <Language Model API>: Failed to initialize client: Zoo Code <Language Model API>: Failed to select model: select failed " ,
741+ "Zoo Code <Language Model API>: Failed to initialize client:" ,
485742 )
743+
744+ expect ( consoleErrorSpy ) . toHaveBeenCalledWith (
745+ "Zoo Code <Language Model API>: Client initialization failed:" ,
746+ expect . stringContaining ( "select failed" ) ,
747+ )
748+
749+ consoleErrorSpy . mockRestore ( )
486750 } )
487751 } )
488752
@@ -642,6 +906,7 @@ describe("VsCodeLmHandler", () => {
642906 cancel : vi . fn ( ) ,
643907 dispose : vi . fn ( ) ,
644908 }
909+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
645910 handler [ "currentRequestCancellation" ] = mockCancellation as any
646911
647912 mockLanguageModelChat . countTokens . mockResolvedValueOnce ( 50 )
@@ -682,6 +947,58 @@ describe("VsCodeLmHandler", () => {
682947 expect ( result ) . toBe ( 5 )
683948 expect ( mockLanguageModelChat . countTokens ) . toHaveBeenCalledWith ( "[IMAGE]" , expect . any ( Object ) )
684949 } )
950+
951+ it ( "should return 0 and log when empty text is provided to internalCountTokens" , async ( ) => {
952+ handler [ "currentRequestCancellation" ] = null
953+ const consoleDebugSpy = vi . spyOn ( console , "debug" ) . mockImplementation ( ( ) => { } )
954+
955+ // @ts -ignore – access private method for coverage of line 234
956+ const result = await handler [ "internalCountTokens" ] ( "" )
957+
958+ expect ( result ) . toBe ( 0 )
959+ expect ( consoleDebugSpy ) . toHaveBeenCalledWith (
960+ "Zoo Code <Language Model API>: Empty text provided for token counting" ,
961+ )
962+
963+ consoleDebugSpy . mockRestore ( )
964+ } )
965+
966+ it ( "should return 0 and log when non-numeric token count is received" , async ( ) => {
967+ handler [ "currentRequestCancellation" ] = null
968+ const consoleWarnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
969+
970+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
971+ mockLanguageModelChat . countTokens . mockResolvedValueOnce ( "not-a-number" as any )
972+
973+ const content : Anthropic . Messages . ContentBlockParam [ ] = [ { type : "text" , text : "test" } ]
974+ const result = await handler . countTokens ( content )
975+
976+ expect ( result ) . toBe ( 0 )
977+ expect ( consoleWarnSpy ) . toHaveBeenCalledWith (
978+ "Zoo Code <Language Model API>: Non-numeric token count received:" ,
979+ "not-a-number" ,
980+ )
981+
982+ consoleWarnSpy . mockRestore ( )
983+ } )
984+
985+ it ( "should return 0 and log when negative token count is received" , async ( ) => {
986+ handler [ "currentRequestCancellation" ] = null
987+ const consoleWarnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
988+
989+ mockLanguageModelChat . countTokens . mockResolvedValueOnce ( - 5 )
990+
991+ const content : Anthropic . Messages . ContentBlockParam [ ] = [ { type : "text" , text : "test" } ]
992+ const result = await handler . countTokens ( content )
993+
994+ expect ( result ) . toBe ( 0 )
995+ expect ( consoleWarnSpy ) . toHaveBeenCalledWith (
996+ "Zoo Code <Language Model API>: Negative token count received:" ,
997+ - 5 ,
998+ )
999+
1000+ consoleWarnSpy . mockRestore ( )
1001+ } )
6851002 } )
6861003
6871004 describe ( "completePrompt" , ( ) => {
0 commit comments