1- // Groq client tests — askGroq, askGroqJson, askGroqWithLogprobs, probeConsistency
2- // Mocks fetch() to test API interaction without real calls
1+ // Groq helper tests — askGroq, askGroqJson, askGroqWithLogprobs, probeConsistency
2+ // Mocks provider factory and fetch() to test API interaction without real calls
33
44import { describe , it , expect , vi , beforeEach } from 'vitest' ;
55
6+ const providerMocks = vi . hoisted ( ( ) => ( {
7+ createLLMProviderFactory : vi . fn ( ) ,
8+ generateResponse : vi . fn ( ) ,
9+ } ) ) ;
10+
11+ vi . mock ( '@stackbilt/llm-providers' , ( ) => ( {
12+ createLLMProviderFactory : providerMocks . createLLMProviderFactory ,
13+ } ) ) ;
14+
615// Mock tokenize/jaccardSimilarity before importing groq.ts
716vi . mock ( '../src/kernel/memory/index.js' , ( ) => ( {
817 tokenize : ( text : string ) => new Set ( text . toLowerCase ( ) . split ( / \s + / ) ) ,
@@ -19,11 +28,14 @@ vi.stubGlobal('fetch', mockFetch);
1928
2029const { askGroq, askGroqJson, askGroqWithLogprobs, probeConsistency } = await import ( '../src/groq.js' ) ;
2130
22- function groqResponse ( content : string , usage ?: { prompt_tokens : number ; completion_tokens : number } ) {
23- return new Response ( JSON . stringify ( {
24- choices : [ { message : { content } } ] ,
31+ function providerResponse ( content : unknown , usage = { inputTokens : 100 , outputTokens : 50 , totalTokens : 150 , cost : 0.001 } ) {
32+ return {
33+ message : content ,
2534 usage,
26- } ) , { status : 200 , headers : { 'Content-Type' : 'application/json' } } ) ;
35+ model : 'llama-test' ,
36+ provider : 'groq' ,
37+ responseTime : 10 ,
38+ } ;
2739}
2840
2941function groqLogprobResponse ( content : string , logprobs : Array < { token : string ; logprob : number } > ) {
@@ -36,52 +48,59 @@ function groqLogprobResponse(content: string, logprobs: Array<{ token: string; l
3648}
3749
3850describe ( 'askGroq' , ( ) => {
39- beforeEach ( ( ) => vi . clearAllMocks ( ) ) ;
51+ beforeEach ( ( ) => {
52+ vi . clearAllMocks ( ) ;
53+ providerMocks . createLLMProviderFactory . mockReturnValue ( { generateResponse : providerMocks . generateResponse } ) ;
54+ } ) ;
4055
4156 it ( 'returns content from Groq API' , async ( ) => {
42- mockFetch . mockResolvedValue ( groqResponse ( 'Hello!' ) ) ;
57+ providerMocks . generateResponse . mockResolvedValue ( providerResponse ( 'Hello!' ) ) ;
4358 const result = await askGroq ( 'key' , 'model' , 'system' , 'user' ) ;
4459 expect ( result ) . toBe ( 'Hello!' ) ;
45- expect ( mockFetch ) . toHaveBeenCalledOnce ( ) ;
60+ expect ( providerMocks . generateResponse ) . toHaveBeenCalledOnce ( ) ;
4661 } ) ;
4762
4863 it ( 'sends correct request shape' , async ( ) => {
49- mockFetch . mockResolvedValue ( groqResponse ( 'ok' ) ) ;
64+ providerMocks . generateResponse . mockResolvedValue ( providerResponse ( 'ok' ) ) ;
5065 await askGroq ( 'test-key' , 'llama-70b' , 'sys prompt' , 'user prompt' , 'https://custom.api' ) ;
5166
52- const [ url , opts ] = mockFetch . mock . calls [ 0 ] ;
53- expect ( url ) . toBe ( 'https://custom.api/openai/v1/chat/completions' ) ;
54- expect ( opts . method ) . toBe ( 'POST' ) ;
55- expect ( opts . headers [ 'Authorization' ] ) . toBe ( 'Bearer test-key' ) ;
56- const body = JSON . parse ( opts . body ) ;
57- expect ( body . model ) . toBe ( 'llama-70b' ) ;
58- expect ( body . messages ) . toHaveLength ( 2 ) ;
59- expect ( body . messages [ 0 ] . role ) . toBe ( 'system' ) ;
60- expect ( body . messages [ 1 ] . role ) . toBe ( 'user' ) ;
67+ expect ( providerMocks . createLLMProviderFactory ) . toHaveBeenCalledWith ( {
68+ groq : { apiKey : 'test-key' , baseUrl : 'https://custom.api' } ,
69+ fallbackRules : [ ] ,
70+ enableCircuitBreaker : true ,
71+ enableRetries : true ,
72+ } ) ;
73+ expect ( providerMocks . generateResponse ) . toHaveBeenCalledWith ( {
74+ model : 'llama-70b' ,
75+ systemPrompt : 'sys prompt' ,
76+ temperature : 0.3 ,
77+ maxTokens : 500 ,
78+ messages : [ { role : 'user' , content : 'user prompt' } ] ,
79+ } ) ;
6180 } ) ;
6281
6382 it ( 'throws on API error' , async ( ) => {
64- mockFetch . mockResolvedValue ( new Response ( 'rate limited' , { status : 429 } ) ) ;
65- await expect ( askGroq ( 'key' , 'model' , 'sys' , 'user' ) ) . rejects . toThrow ( 'Groq API error 429 ' ) ;
83+ providerMocks . generateResponse . mockRejectedValue ( new Error ( 'rate limited' ) ) ;
84+ await expect ( askGroq ( 'key' , 'model' , 'sys' , 'user' ) ) . rejects . toThrow ( 'Groq API error: rate limited ' ) ;
6685 } ) ;
6786
6887 it ( 'returns empty string when no content' , async ( ) => {
69- mockFetch . mockResolvedValue ( new Response (
70- JSON . stringify ( { choices : [ { message : { content : null } } ] } ) ,
71- { status : 200 , headers : { 'Content-Type' : 'application/json' } } ,
72- ) ) ;
88+ providerMocks . generateResponse . mockResolvedValue ( providerResponse ( null ) ) ;
7389 const result = await askGroq ( 'key' , 'model' , 'sys' , 'user' ) ;
7490 expect ( result ) . toBe ( '' ) ;
7591 } ) ;
7692} ) ;
7793
7894describe ( 'askGroqJson' , ( ) => {
79- beforeEach ( ( ) => vi . clearAllMocks ( ) ) ;
95+ beforeEach ( ( ) => {
96+ vi . clearAllMocks ( ) ;
97+ providerMocks . createLLMProviderFactory . mockReturnValue ( { generateResponse : providerMocks . generateResponse } ) ;
98+ } ) ;
8099
81100 it ( 'parses JSON response' , async ( ) => {
82- mockFetch . mockResolvedValue ( groqResponse (
101+ providerMocks . generateResponse . mockResolvedValue ( providerResponse (
83102 '{"name":"test","value":42}' ,
84- { prompt_tokens : 100 , completion_tokens : 50 } ,
103+ { inputTokens : 100 , outputTokens : 50 , totalTokens : 150 , cost : 0.001 } ,
85104 ) ) ;
86105 const { parsed, raw, usage } = await askGroqJson < { name : string ; value : number } > (
87106 'key' , 'model' , 'sys' , 'user' ,
@@ -93,9 +112,9 @@ describe('askGroqJson', () => {
93112 } ) ;
94113
95114 it ( 'handles prefill by concatenating' , async ( ) => {
96- mockFetch . mockResolvedValue ( groqResponse (
115+ providerMocks . generateResponse . mockResolvedValue ( providerResponse (
97116 'hello","done":true}' ,
98- { prompt_tokens : 50 , completion_tokens : 20 } ,
117+ { inputTokens : 50 , outputTokens : 20 , totalTokens : 70 , cost : 0.001 } ,
99118 ) ) ;
100119 const { parsed } = await askGroqJson < { greeting : string ; done : boolean } > (
101120 'key' , 'model' , 'sys' , 'user' , undefined ,
@@ -106,20 +125,24 @@ describe('askGroqJson', () => {
106125 } ) ;
107126
108127 it ( 'sends json_object response_format' , async ( ) => {
109- mockFetch . mockResolvedValue ( groqResponse ( '{}' ) ) ;
128+ providerMocks . generateResponse . mockResolvedValue ( providerResponse ( '{}' ) ) ;
110129 await askGroqJson ( 'key' , 'model' , 'sys' , 'user' ) ;
111- const body = JSON . parse ( mockFetch . mock . calls [ 0 ] [ 1 ] . body ) ;
112- expect ( body . response_format ) . toEqual ( { type : 'json_object' } ) ;
130+ expect ( providerMocks . generateResponse ) . toHaveBeenCalledWith ( expect . objectContaining ( {
131+ response_format : { type : 'json_object' } ,
132+ } ) ) ;
113133 } ) ;
114134
115135 it ( 'throws on API error' , async ( ) => {
116- mockFetch . mockResolvedValue ( new Response ( 'server error' , { status : 500 } ) ) ;
117- await expect ( askGroqJson ( 'key' , 'model' , 'sys' , 'user' ) ) . rejects . toThrow ( 'Groq API error 500 ' ) ;
136+ providerMocks . generateResponse . mockRejectedValue ( new Error ( 'server error' ) ) ;
137+ await expect ( askGroqJson ( 'key' , 'model' , 'sys' , 'user' ) ) . rejects . toThrow ( 'Groq API error: server error ' ) ;
118138 } ) ;
119139} ) ;
120140
121141describe ( 'askGroqWithLogprobs' , ( ) => {
122- beforeEach ( ( ) => vi . clearAllMocks ( ) ) ;
142+ beforeEach ( ( ) => {
143+ vi . clearAllMocks ( ) ;
144+ providerMocks . createLLMProviderFactory . mockReturnValue ( { generateResponse : providerMocks . generateResponse } ) ;
145+ } ) ;
123146
124147 it ( 'parses classification with token confidence' , async ( ) => {
125148 mockFetch . mockResolvedValue ( groqLogprobResponse (
@@ -161,30 +184,32 @@ describe('askGroqWithLogprobs', () => {
161184} ) ;
162185
163186describe ( 'probeConsistency' , ( ) => {
164- beforeEach ( ( ) => vi . clearAllMocks ( ) ) ;
187+ beforeEach ( ( ) => {
188+ vi . clearAllMocks ( ) ;
189+ providerMocks . createLLMProviderFactory . mockReturnValue ( { generateResponse : providerMocks . generateResponse } ) ;
190+ } ) ;
165191
166192 it ( 'returns sigma=0 when all responses agree' , async ( ) => {
167- // Each call needs a fresh Response (body can only be read once)
168- mockFetch . mockImplementation ( ( ) => Promise . resolve ( groqResponse ( 'The answer is 42' ) ) ) ;
193+ providerMocks . generateResponse . mockResolvedValue ( providerResponse ( 'The answer is 42' ) ) ;
169194 const result = await probeConsistency ( 'key' , 'model' , 'sys' , 'user' ) ;
170195 expect ( result . sigma ) . toBe ( 0 ) ;
171196 expect ( result . agreedText ) . toBe ( 'The answer is 42' ) ;
172197 expect ( result . responses ) . toHaveLength ( 3 ) ;
173198 } ) ;
174199
175200 it ( 'returns sigma=1.0 when responses completely disagree' , async ( ) => {
176- mockFetch
177- . mockResolvedValueOnce ( groqResponse ( 'alpha beta gamma delta epsilon' ) )
178- . mockResolvedValueOnce ( groqResponse ( 'one two three four five six seven' ) )
179- . mockResolvedValueOnce ( groqResponse ( 'red green blue purple orange yellow' ) ) ;
201+ providerMocks . generateResponse
202+ . mockResolvedValueOnce ( providerResponse ( 'alpha beta gamma delta epsilon' ) )
203+ . mockResolvedValueOnce ( providerResponse ( 'one two three four five six seven' ) )
204+ . mockResolvedValueOnce ( providerResponse ( 'red green blue purple orange yellow' ) ) ;
180205 const result = await probeConsistency ( 'key' , 'model' , 'sys' , 'user' ) ;
181206 expect ( result . sigma ) . toBe ( 1.0 ) ;
182207 expect ( result . agreedText ) . toBeNull ( ) ;
183208 } ) ;
184209
185210 it ( 'makes exactly 3 parallel calls' , async ( ) => {
186- mockFetch . mockImplementation ( ( ) => Promise . resolve ( groqResponse ( 'same' ) ) ) ;
211+ providerMocks . generateResponse . mockResolvedValue ( providerResponse ( 'same' ) ) ;
187212 await probeConsistency ( 'key' , 'model' , 'sys' , 'user' ) ;
188- expect ( mockFetch ) . toHaveBeenCalledTimes ( 3 ) ;
213+ expect ( providerMocks . generateResponse ) . toHaveBeenCalledTimes ( 3 ) ;
189214 } ) ;
190215} ) ;
0 commit comments