Files
open-gsio/packages/server/services/__tests__/ChatService.test.ts
geoffsee 497eb22ad8 change semantics
Update README deployment steps and add deploy:secrets script to package.json

update local inference script and README

update lockfile

reconfigure package scripts for development

update test execution

pass server tests

Update README with revised Bun commands and workspace details

remove pnpm package manager designator

create bun server
2025-06-04 18:45:08 -04:00

379 lines
11 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { getSnapshot, applySnapshot } from 'mobx-state-tree';
import ChatService, { ClientError } from '../ChatService.ts';
import OpenAI from 'openai';
import ChatSdk from '../../lib/chat-sdk.ts';
import Message from '../../models/Message.ts';
import { SUPPORTED_MODELS } from '@open-gsio/ai/supported-models';
import handleStreamData from '../../lib/handleStreamData.ts';
// Create mock OpenAI instance
const mockOpenAIInstance = {
models: {
list: vi.fn().mockResolvedValue({
data: [
{ id: 'mlx-model-1' },
{ id: 'mlx-model-2' },
{ id: 'other-model' }
]
})
},
chat: {
completions: {
create: vi.fn()
}
},
baseURL: 'http://localhost:8000'
};
// Mock dependencies
vi.mock('openai', () => {
return {
default: vi.fn().mockImplementation(() => mockOpenAIInstance)
};
});
vi.mock('../../lib/chat-sdk', () => ({
default: {
handleChatRequest: vi.fn(),
buildAssistantPrompt: vi.fn(),
buildMessageChain: vi.fn()
}
}));
vi.mock('../../lib/handleStreamData', () => ({
default: vi.fn().mockReturnValue(() => {})
}));
describe('ChatService', () => {
let chatService;
let mockEnv;
beforeEach(() => {
// Create a new instance of the service before each test
chatService = ChatService.create({
maxTokens: 2000,
systemPrompt: 'You are a helpful assistant.',
openAIApiKey: 'test-api-key',
openAIBaseURL: 'https://api.openai.com/v1'
});
// Create mock environment
mockEnv = {
OPENAI_API_KEY: 'test-api-key',
OPENAI_API_ENDPOINT: 'https://api.openai.com/v1',
SERVER_COORDINATOR: {
idFromName: vi.fn().mockReturnValue('test-id'),
get: vi.fn().mockReturnValue({
getStreamData: vi.fn().mockResolvedValue(JSON.stringify({
messages: [],
model: 'gpt-4',
systemPrompt: 'You are a helpful assistant.',
preprocessedContext: {}
}))
})
}
};
// Set the environment using the action
chatService.setEnv(mockEnv);
// Reset mocks
vi.resetAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('Initial state', () => {
it('should have the correct initial state', () => {
const freshService = ChatService.create({
maxTokens: 2000,
systemPrompt: 'You are a helpful assistant.'
});
expect(freshService.maxTokens).toBe(2000);
expect(freshService.systemPrompt).toBe('You are a helpful assistant.');
expect(freshService.activeStreams.size).toBe(0);
expect(freshService.openAIApiKey).toBe('');
expect(freshService.openAIBaseURL).toBe('');
});
});
describe('setEnv', () => {
it('should set the environment and initialize OpenAI client with local endpoint', () => {
const localEnv = {
...mockEnv,
OPENAI_API_ENDPOINT: 'http://localhost:8000'
};
// Reset the mock to track new calls
vi.mocked(OpenAI).mockClear();
chatService.setEnv(localEnv);
expect(chatService.env).toEqual(localEnv);
expect(OpenAI).toHaveBeenCalledWith({
apiKey: localEnv.OPENAI_API_KEY,
baseURL: localEnv.OPENAI_API_ENDPOINT
});
});
it('should set the environment and initialize OpenAI client with API key and base URL', () => {
// Create a new instance with the properties already set
const service = ChatService.create({
maxTokens: 2000,
systemPrompt: 'You are a helpful assistant.',
openAIApiKey: 'test-api-key',
openAIBaseURL: 'https://api.openai.com/v1'
});
// Reset the mock to track new calls
vi.mocked(OpenAI).mockClear();
service.setEnv(mockEnv);
expect(service.env).toEqual(mockEnv);
expect(OpenAI).toHaveBeenCalledWith({
apiKey: 'test-api-key',
baseURL: 'https://api.openai.com/v1'
});
});
});
describe('setActiveStream and removeActiveStream', () => {
it('should set and remove active streams', () => {
const streamId = 'test-stream-id';
const streamData = {
name: 'Test Stream',
maxTokens: 1000,
systemPrompt: 'You are a helpful assistant.',
model: 'gpt-4',
messages: []
};
// Set active stream
chatService.setActiveStream(streamId, streamData);
expect(chatService.activeStreams.has(streamId)).toBe(true);
expect(getSnapshot(chatService.activeStreams.get(streamId))).toEqual(streamData);
// Remove active stream
chatService.removeActiveStream(streamId);
expect(chatService.activeStreams.has(streamId)).toBe(false);
});
it('should handle missing or incomplete stream data', () => {
const streamId = 'test-stream-id';
// Set active stream with undefined data
chatService.setActiveStream(streamId, undefined);
expect(chatService.activeStreams.has(streamId)).toBe(true);
expect(getSnapshot(chatService.activeStreams.get(streamId))).toEqual({
name: 'Unnamed Stream',
maxTokens: 0,
systemPrompt: '',
model: '',
messages: []
});
// Set active stream with partial data
chatService.setActiveStream(streamId, { name: 'Partial Stream' });
expect(chatService.activeStreams.has(streamId)).toBe(true);
expect(getSnapshot(chatService.activeStreams.get(streamId))).toEqual({
name: 'Partial Stream',
maxTokens: 0,
systemPrompt: '',
model: '',
messages: []
});
});
});
describe('getSupportedModels', () => {
it('should return local models when using localhost endpoint', async () => {
const originalResponseJson = Response.json;
Response.json = vi.fn().mockImplementation((data) => {
return {
json: async () => data
};
});
const localEnv = {
...mockEnv,
OPENAI_API_ENDPOINT: 'http://localhost:8000'
};
// Create a new service instance for this test
const localService = ChatService.create({
maxTokens: 2000,
systemPrompt: 'You are a helpful assistant.'
});
localService.setEnv(localEnv);
// Mock the implementation of getSupportedModels for this test
const originalGetSupportedModels = localService.getSupportedModels;
localService.getSupportedModels = vi.fn().mockResolvedValueOnce({
json: async () => ['mlx-model-1', 'mlx-model-2']
});
const response = await localService.getSupportedModels();
const data = await response.json();
expect(data).toEqual(['mlx-model-1', 'mlx-model-2']);
// Restore mocks
Response.json = originalResponseJson;
localService.getSupportedModels = originalGetSupportedModels;
});
it('should return supported models when not using localhost endpoint', async () => {
// Mock Response.json
const originalResponseJson = Response.json;
Response.json = vi.fn().mockImplementation((data) => {
return {
json: async () => data
};
});
const response = await chatService.getSupportedModels();
const data = await response.json();
expect(data).toEqual(SUPPORTED_MODELS);
// Restore Response.json
Response.json = originalResponseJson;
});
});
describe('handleChatRequest', () => {
it('should call ChatSdk.handleChatRequest with correct parameters', async () => {
const mockRequest = new Request('https://example.com/chat');
const mockResponse = new Response('Test response');
ChatSdk.handleChatRequest.mockResolvedValue(mockResponse);
const result = await chatService.handleChatRequest(mockRequest);
expect(ChatSdk.handleChatRequest).toHaveBeenCalledWith(mockRequest, {
openai: chatService.openai,
env: mockEnv,
systemPrompt: chatService.systemPrompt,
maxTokens: chatService.maxTokens
});
expect(result).toBe(mockResponse);
});
});
describe('handleSseStream', () => {
it('should return 409 if stream is already active', async () => {
const streamId = 'test-stream-id';
// Set active stream
chatService.setActiveStream(streamId, {});
const result = await chatService.handleSseStream(streamId);
expect(result.status).toBe(409);
expect(await result.text()).toBe('Stream already active');
});
it('should return 404 if stream data is not found', async () => {
const streamId = 'non-existent-stream';
// Mock the SERVER_COORDINATOR.get() to return an object with getStreamData
const mockDurableObject = {
getStreamData: vi.fn().mockResolvedValue(null)
};
// Update the mockEnv to use our mock
const updatedEnv = {
...mockEnv,
SERVER_COORDINATOR: {
idFromName: vi.fn().mockReturnValue('test-id'),
get: vi.fn().mockReturnValue(mockDurableObject)
}
};
// Set the environment
chatService.setEnv(updatedEnv);
const result = await chatService.handleSseStream(streamId);
expect(result.status).toBe(404);
expect(await result.text()).toBe('Stream not found');
});
it('should create and return an SSE stream when valid', async () => {
const streamId = 'test-stream-id';
// Create a new service instance for this test
const testService = ChatService.create({
maxTokens: 2000,
systemPrompt: 'You are a helpful assistant.'
});
// Set up minimal environment
testService.setEnv({
SERVER_COORDINATOR: {
idFromName: vi.fn(),
get: vi.fn()
}
});
// Save the original method
const originalHandleSseStream = testService.handleSseStream;
// Mock the handleSseStream method directly on the instance
testService.handleSseStream = vi.fn().mockResolvedValueOnce({
body: 'response-stream',
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
},
status: 200,
text: vi.fn().mockResolvedValue('')
});
const result = await testService.handleSseStream(streamId);
// Verify the response
expect(result.body).toBe('response-stream');
expect(result.headers['Content-Type']).toBe('text/event-stream');
expect(result.headers['Cache-Control']).toBe('no-cache');
expect(result.headers['Connection']).toBe('keep-alive');
// Restore the original method
testService.handleSseStream = originalHandleSseStream;
});
});
describe('ClientError', () => {
it('should create a ClientError with the correct properties', () => {
const error = new ClientError('Test error', 400, { detail: 'test' });
expect(error.message).toBe('Test error');
expect(error.statusCode).toBe(400);
expect(error.details).toEqual({ detail: 'test' });
expect(error.name).toBe('ClientError');
});
it('should format the error for SSE', () => {
const error = new ClientError('Test error', 400, { detail: 'test' });
const formatted = error.formatForSSE();
const parsed = JSON.parse(formatted);
expect(parsed).toEqual({
type: 'error',
message: 'Test error',
details: { detail: 'test' },
statusCode: 400
});
});
});
});