Skip to content

Commit 5d48342

Browse files
committed
additional tests and code review feedback for ai sdk
1 parent 4bf4c02 commit 5d48342

File tree

9 files changed

+863
-121
lines changed

9 files changed

+863
-121
lines changed

packages/sdk/server-ai/__tests__/Judge.test.ts

Lines changed: 497 additions & 0 deletions
Large diffs are not rendered by default.

packages/sdk/server-ai/__tests__/LDAIClientImpl.test.ts

Lines changed: 253 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
11
import { 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';
510
import { LDAIClientImpl } from '../src/LDAIClientImpl';
611
import { 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+
817
const 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+
1327
const testContext: LDContext = { kind: 'user', key: 'test-user' };
1428

1529
it('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 () => {
7388
it('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 () => {
94109
it('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

120134
it('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 () => {
154168
it('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 () => {
206220
it('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 (
227241
it('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

253266
it('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

Comments
 (0)