11import { LDContext } from '@launchdarkly/js-server-sdk-common' ;
22
3- import { LDAIAgentDefaults } from '../src/api/agents' ;
4- import { LDAIDefaults } from '../src/api/config' ;
3+ import {
4+ LDAIAgentConfigDefault ,
5+ LDAIConversationConfigDefault ,
6+ LDAIJudgeConfigDefault ,
7+ } from '../src/api/config/types' ;
8+ import { Judge } from '../src/api/judge/Judge' ;
9+ import { AIProviderFactory } from '../src/api/providers/AIProviderFactory' ;
510import { LDAIClientImpl } from '../src/LDAIClientImpl' ;
611import { LDClientMin } from '../src/LDClientMin' ;
712
13+ // Mock Judge and AIProviderFactory
14+ jest . mock ( '../src/api/judge/Judge' ) ;
15+ jest . mock ( '../src/api/providers/AIProviderFactory' ) ;
16+
817const mockLdClient : jest . Mocked < LDClientMin > = {
918 variation : jest . fn ( ) ,
1019 track : jest . fn ( ) ,
1120} ;
1221
22+ // Reset mocks before each test
23+ beforeEach ( ( ) => {
24+ jest . clearAllMocks ( ) ;
25+ } ) ;
26+
1327const testContext : LDContext = { kind : 'user' , key : 'test-user' } ;
1428
1529it ( 'returns config with interpolated messages' , async ( ) => {
1630 const client = new LDAIClientImpl ( mockLdClient ) ;
1731 const key = 'test-flag' ;
18- const defaultValue : LDAIDefaults = {
32+ const defaultValue : LDAIConversationConfigDefault = {
1933 model : { name : 'test' , parameters : { name : 'test-model' } } ,
2034 messages : [ ] ,
2135 enabled : true ,
@@ -36,6 +50,7 @@ it('returns config with interpolated messages', async () => {
3650 _ldMeta : {
3751 variationKey : 'v1' ,
3852 enabled : true ,
53+ mode : 'completion' ,
3954 } ,
4055 } ;
4156
@@ -73,14 +88,14 @@ it('returns config with interpolated messages', async () => {
7388it ( 'includes context in variables for messages interpolation' , async ( ) => {
7489 const client = new LDAIClientImpl ( mockLdClient ) ;
7590 const key = 'test-flag' ;
76- const defaultValue : LDAIDefaults = {
91+ const defaultValue : LDAIConversationConfigDefault = {
7792 model : { name : 'test' , parameters : { name : 'test-model' } } ,
7893 messages : [ ] ,
7994 } ;
8095
8196 const mockVariation = {
8297 messages : [ { role : 'system' , content : 'User key: {{ldctx.key}}' } ] ,
83- _ldMeta : { variationKey : 'v1' , enabled : true } ,
98+ _ldMeta : { variationKey : 'v1' , enabled : true , mode : 'completion' } ,
8499 } ;
85100
86101 mockLdClient . variation . mockResolvedValue ( mockVariation ) ;
@@ -94,7 +109,7 @@ it('includes context in variables for messages interpolation', async () => {
94109it ( 'handles missing metadata in variation' , async ( ) => {
95110 const client = new LDAIClientImpl ( mockLdClient ) ;
96111 const key = 'test-flag' ;
97- const defaultValue : LDAIDefaults = {
112+ const defaultValue : LDAIConversationConfigDefault = {
98113 model : { name : 'test' , parameters : { name : 'test-model' } } ,
99114 messages : [ ] ,
100115 } ;
@@ -108,27 +123,26 @@ it('handles missing metadata in variation', async () => {
108123
109124 const result = await client . config ( key , testContext , defaultValue ) ;
110125
126+ // When metadata/mode is missing, a disabled config is returned
111127 expect ( result ) . toEqual ( {
112- model : { name : 'example-provider' , parameters : { name : 'imagination' } } ,
113- messages : [ { role : 'system' , content : 'Hello' } ] ,
114- tracker : expect . any ( Object ) ,
115128 enabled : false ,
129+ tracker : undefined ,
116130 toVercelAISDK : expect . any ( Function ) ,
117131 } ) ;
118132} ) ;
119133
120134it ( 'passes the default value to the underlying client' , async ( ) => {
121135 const client = new LDAIClientImpl ( mockLdClient ) ;
122136 const key = 'non-existent-flag' ;
123- const defaultValue : LDAIDefaults = {
137+ const defaultValue : LDAIConversationConfigDefault = {
124138 model : { name : 'default-model' , parameters : { name : 'default' } } ,
125139 provider : { name : 'default-provider' } ,
126140 messages : [ { role : 'system' , content : 'Default messages' } ] ,
127141 enabled : true ,
128142 } ;
129143
130144 const expectedLDFlagValue = {
131- _ldMeta : { enabled : true } ,
145+ _ldMeta : { enabled : true , mode : 'completion' , variationKey : '' } ,
132146 model : defaultValue . model ,
133147 messages : defaultValue . messages ,
134148 provider : defaultValue . provider ,
@@ -154,7 +168,7 @@ it('passes the default value to the underlying client', async () => {
154168it ( 'returns single agent config with interpolated instructions' , async ( ) => {
155169 const client = new LDAIClientImpl ( mockLdClient ) ;
156170 const key = 'test-agent' ;
157- const defaultValue : LDAIAgentDefaults = {
171+ const defaultValue : LDAIAgentConfigDefault = {
158172 model : { name : 'test' , parameters : { name : 'test-model' } } ,
159173 instructions : 'You are a helpful assistant.' ,
160174 enabled : true ,
@@ -206,7 +220,7 @@ it('returns single agent config with interpolated instructions', async () => {
206220it ( 'includes context in variables for agent instructions interpolation' , async ( ) => {
207221 const client = new LDAIClientImpl ( mockLdClient ) ;
208222 const key = 'test-agent' ;
209- const defaultValue : LDAIAgentDefaults = {
223+ const defaultValue : LDAIAgentConfigDefault = {
210224 model : { name : 'test' , parameters : { name : 'test-model' } } ,
211225 instructions : 'You are a helpful assistant.' ,
212226 enabled : true ,
@@ -227,7 +241,7 @@ it('includes context in variables for agent instructions interpolation', async (
227241it ( 'handles missing metadata in agent variation' , async ( ) => {
228242 const client = new LDAIClientImpl ( mockLdClient ) ;
229243 const key = 'test-agent' ;
230- const defaultValue : LDAIAgentDefaults = {
244+ const defaultValue : LDAIAgentConfigDefault = {
231245 model : { name : 'test' , parameters : { name : 'test-model' } } ,
232246 instructions : 'You are a helpful assistant.' ,
233247 enabled : true ,
@@ -242,26 +256,25 @@ it('handles missing metadata in agent variation', async () => {
242256
243257 const result = await client . agent ( key , testContext , defaultValue ) ;
244258
259+ // When metadata/mode is missing, a disabled config is returned
245260 expect ( result ) . toEqual ( {
246- model : { name : 'example-provider' , parameters : { name : 'imagination' } } ,
247- instructions : 'Hello.' ,
248- tracker : expect . any ( Object ) ,
249261 enabled : false ,
262+ tracker : undefined ,
250263 } ) ;
251264} ) ;
252265
253266it ( 'passes the default value to the underlying client for single agent' , async ( ) => {
254267 const client = new LDAIClientImpl ( mockLdClient ) ;
255268 const key = 'non-existent-agent' ;
256- const defaultValue : LDAIAgentDefaults = {
269+ const defaultValue : LDAIAgentConfigDefault = {
257270 model : { name : 'default-model' , parameters : { name : 'default' } } ,
258271 provider : { name : 'default-provider' } ,
259272 instructions : 'Default instructions' ,
260273 enabled : true ,
261274 } ;
262275
263276 const expectedLDFlagValue = {
264- _ldMeta : { enabled : defaultValue . enabled } ,
277+ _ldMeta : { enabled : defaultValue . enabled , mode : 'agent' , variationKey : '' } ,
265278 model : defaultValue . model ,
266279 provider : defaultValue . provider ,
267280 instructions : defaultValue . instructions ,
@@ -380,3 +393,224 @@ it('handles empty agent configs array', async () => {
380393 0 ,
381394 ) ;
382395} ) ;
396+
397+ // New judge-related tests
398+ describe ( 'judge method' , ( ) => {
399+ it ( 'retrieves judge configuration successfully' , async ( ) => {
400+ const client = new LDAIClientImpl ( mockLdClient ) ;
401+ const key = 'test-judge' ;
402+ const defaultValue : LDAIJudgeConfigDefault = {
403+ enabled : true ,
404+ model : { name : 'gpt-4' } ,
405+ provider : { name : 'openai' } ,
406+ evaluationMetricKeys : [ 'relevance' , 'accuracy' ] ,
407+ messages : [ { role : 'system' , content : 'You are a judge.' } ] ,
408+ } ;
409+
410+ const mockJudgeConfig = {
411+ enabled : true ,
412+ model : { name : 'gpt-4' } ,
413+ provider : { name : 'openai' } ,
414+ evaluationMetricKeys : [ 'relevance' , 'accuracy' ] ,
415+ messages : [ { role : 'system' as const , content : 'You are a judge.' } ] ,
416+ tracker : { } as any ,
417+ toVercelAISDK : jest . fn ( ) ,
418+ } ;
419+
420+ // Mock the _evaluate method
421+ const evaluateSpy = jest . spyOn ( client as any , '_evaluate' ) ;
422+ evaluateSpy . mockResolvedValue ( mockJudgeConfig ) ;
423+
424+ const result = await client . judge ( key , testContext , defaultValue ) ;
425+
426+ expect ( mockLdClient . track ) . toHaveBeenCalledWith (
427+ '$ld:ai:judge:function:single' ,
428+ testContext ,
429+ key ,
430+ 1 ,
431+ ) ;
432+ expect ( evaluateSpy ) . toHaveBeenCalledWith ( key , testContext , defaultValue , 'judge' , undefined ) ;
433+ expect ( result ) . toBe ( mockJudgeConfig ) ;
434+ } ) ;
435+
436+ it ( 'handles variables parameter' , async ( ) => {
437+ const client = new LDAIClientImpl ( mockLdClient ) ;
438+ const key = 'test-judge' ;
439+ const defaultValue : LDAIJudgeConfigDefault = {
440+ enabled : true ,
441+ model : { name : 'gpt-4' } ,
442+ provider : { name : 'openai' } ,
443+ evaluationMetricKeys : [ 'relevance' ] ,
444+ messages : [ { role : 'system' , content : 'You are a judge.' } ] ,
445+ } ;
446+ const variables = { metric : 'relevance' } ;
447+
448+ const mockJudgeConfig = {
449+ enabled : true ,
450+ model : { name : 'gpt-4' } ,
451+ provider : { name : 'openai' } ,
452+ evaluationMetricKeys : [ 'relevance' ] ,
453+ messages : [ { role : 'system' as const , content : 'You are a judge.' } ] ,
454+ tracker : { } as any ,
455+ toVercelAISDK : jest . fn ( ) ,
456+ } ;
457+
458+ const evaluateSpy = jest . spyOn ( client as any , '_evaluate' ) ;
459+ evaluateSpy . mockResolvedValue ( mockJudgeConfig ) ;
460+
461+ const result = await client . judge ( key , testContext , defaultValue , variables ) ;
462+
463+ expect ( evaluateSpy ) . toHaveBeenCalledWith ( key , testContext , defaultValue , 'judge' , variables ) ;
464+ expect ( result ) . toBe ( mockJudgeConfig ) ;
465+ } ) ;
466+ } ) ;
467+
468+ describe ( 'initJudge method' , ( ) => {
469+ let mockProvider : jest . Mocked < any > ;
470+ let mockJudge : jest . Mocked < Judge > ;
471+
472+ beforeEach ( ( ) => {
473+ mockProvider = {
474+ invokeStructuredModel : jest . fn ( ) ,
475+ } ;
476+
477+ mockJudge = {
478+ evaluate : jest . fn ( ) ,
479+ evaluateMessages : jest . fn ( ) ,
480+ } as any ;
481+
482+ // Mock AIProviderFactory.create
483+ ( AIProviderFactory . create as jest . Mock ) . mockResolvedValue ( mockProvider ) ;
484+
485+ // Mock Judge constructor
486+ ( Judge as jest . MockedClass < typeof Judge > ) . mockImplementation ( ( ) => mockJudge ) ;
487+ } ) ;
488+
489+ it ( 'initializes judge successfully' , async ( ) => {
490+ const client = new LDAIClientImpl ( mockLdClient ) ;
491+ const key = 'test-judge' ;
492+ const defaultValue : LDAIJudgeConfigDefault = {
493+ enabled : true ,
494+ model : { name : 'gpt-4' } ,
495+ provider : { name : 'openai' } ,
496+ evaluationMetricKeys : [ 'relevance' , 'accuracy' ] ,
497+ messages : [ { role : 'system' , content : 'You are a judge.' } ] ,
498+ } ;
499+
500+ const mockJudgeConfig = {
501+ enabled : true ,
502+ model : { name : 'gpt-4' } ,
503+ provider : { name : 'openai' } ,
504+ evaluationMetricKeys : [ 'relevance' , 'accuracy' ] ,
505+ messages : [ { role : 'system' as const , content : 'You are a judge.' } ] ,
506+ tracker : { } as any ,
507+ toVercelAISDK : jest . fn ( ) ,
508+ } ;
509+
510+ // Mock the judge method
511+ const judgeSpy = jest . spyOn ( client , 'judge' ) ;
512+ judgeSpy . mockResolvedValue ( mockJudgeConfig ) ;
513+
514+ const result = await client . initJudge ( key , testContext , defaultValue ) ;
515+
516+ expect ( mockLdClient . track ) . toHaveBeenCalledWith (
517+ '$ld:ai:judge:function:initJudge' ,
518+ testContext ,
519+ key ,
520+ 1 ,
521+ ) ;
522+ expect ( judgeSpy ) . toHaveBeenCalledWith ( key , testContext , defaultValue , undefined ) ;
523+ expect ( AIProviderFactory . create ) . toHaveBeenCalledWith ( mockJudgeConfig , undefined , undefined ) ;
524+ expect ( Judge ) . toHaveBeenCalledWith (
525+ mockJudgeConfig ,
526+ mockJudgeConfig . tracker ,
527+ mockProvider ,
528+ undefined ,
529+ ) ;
530+ expect ( result ) . toBe ( mockJudge ) ;
531+ } ) ;
532+
533+ it ( 'returns undefined when judge configuration is disabled' , async ( ) => {
534+ const client = new LDAIClientImpl ( mockLdClient ) ;
535+ const key = 'test-judge' ;
536+ const defaultValue : LDAIJudgeConfigDefault = {
537+ enabled : false ,
538+ model : { name : 'gpt-4' } ,
539+ provider : { name : 'openai' } ,
540+ evaluationMetricKeys : [ 'relevance' ] ,
541+ messages : [ { role : 'system' , content : 'You are a judge.' } ] ,
542+ } ;
543+
544+ const mockJudgeConfig = {
545+ enabled : false , // This should be false to test disabled case
546+ model : { name : 'gpt-4' } ,
547+ provider : { name : 'openai' } ,
548+ evaluationMetricKeys : [ 'relevance' ] ,
549+ messages : [ { role : 'system' as const , content : 'You are a judge.' } ] ,
550+ tracker : undefined , // No tracker for disabled config
551+ toVercelAISDK : jest . fn ( ) ,
552+ } ;
553+
554+ const judgeSpy = jest . spyOn ( client , 'judge' ) ;
555+ judgeSpy . mockResolvedValue ( mockJudgeConfig ) ;
556+
557+ const result = await client . initJudge ( key , testContext , defaultValue ) ;
558+
559+ expect ( result ) . toBeUndefined ( ) ;
560+ expect ( AIProviderFactory . create ) . not . toHaveBeenCalled ( ) ;
561+ expect ( Judge ) . not . toHaveBeenCalled ( ) ;
562+ } ) ;
563+
564+ it ( 'returns undefined when AIProviderFactory.create fails' , async ( ) => {
565+ const client = new LDAIClientImpl ( mockLdClient ) ;
566+ const key = 'test-judge' ;
567+ const defaultValue : LDAIJudgeConfigDefault = {
568+ enabled : true ,
569+ model : { name : 'gpt-4' } ,
570+ provider : { name : 'openai' } ,
571+ evaluationMetricKeys : [ 'relevance' ] ,
572+ messages : [ { role : 'system' , content : 'You are a judge.' } ] ,
573+ } ;
574+
575+ const mockJudgeConfig = {
576+ enabled : true ,
577+ model : { name : 'gpt-4' } ,
578+ provider : { name : 'openai' } ,
579+ evaluationMetricKeys : [ 'relevance' ] ,
580+ messages : [ { role : 'system' as const , content : 'You are a judge.' } ] ,
581+ tracker : { } as any ,
582+ toVercelAISDK : jest . fn ( ) ,
583+ } ;
584+
585+ const judgeSpy = jest . spyOn ( client , 'judge' ) ;
586+ judgeSpy . mockResolvedValue ( mockJudgeConfig ) ;
587+
588+ ( AIProviderFactory . create as jest . Mock ) . mockResolvedValue ( undefined ) ;
589+
590+ const result = await client . initJudge ( key , testContext , defaultValue ) ;
591+
592+ expect ( result ) . toBeUndefined ( ) ;
593+ expect ( AIProviderFactory . create ) . toHaveBeenCalledWith ( mockJudgeConfig , undefined , undefined ) ;
594+ expect ( Judge ) . not . toHaveBeenCalled ( ) ;
595+ } ) ;
596+
597+ it ( 'handles errors gracefully' , async ( ) => {
598+ const client = new LDAIClientImpl ( mockLdClient ) ;
599+ const key = 'test-judge' ;
600+ const defaultValue : LDAIJudgeConfigDefault = {
601+ enabled : true ,
602+ model : { name : 'gpt-4' } ,
603+ provider : { name : 'openai' } ,
604+ evaluationMetricKeys : [ 'relevance' ] ,
605+ messages : [ { role : 'system' , content : 'You are a judge.' } ] ,
606+ } ;
607+
608+ const error = new Error ( 'Judge configuration error' ) ;
609+ const judgeSpy = jest . spyOn ( client , 'judge' ) ;
610+ judgeSpy . mockRejectedValue ( error ) ;
611+
612+ const result = await client . initJudge ( key , testContext , defaultValue ) ;
613+
614+ expect ( result ) . toBeUndefined ( ) ;
615+ } ) ;
616+ } ) ;
0 commit comments