mirror of
https://github.com/geoffsee/open-gsio.git
synced 2025-09-08 22:56:46 +00:00
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
This commit is contained in:

committed by
Geoff Seemueller

parent
1055cda2f1
commit
497eb22ad8
155
packages/server/lib/__tests__/assistant-sdk.test.ts
Normal file
155
packages/server/lib/__tests__/assistant-sdk.test.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { AssistantSdk } from '../assistant-sdk.ts';
|
||||
import { Utils } from '../utils.ts';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../utils', () => ({
|
||||
Utils: {
|
||||
selectEquitably: vi.fn(),
|
||||
getCurrentDate: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../prompts/few_shots', () => ({
|
||||
default: {
|
||||
'a': 'A1',
|
||||
'question1': 'answer1',
|
||||
'question2': 'answer2',
|
||||
'question3': 'answer3'
|
||||
}
|
||||
}));
|
||||
|
||||
describe('AssistantSdk', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2023-01-01T12:30:45Z'));
|
||||
|
||||
// Reset mocks
|
||||
vi.mocked(Utils.selectEquitably).mockReset();
|
||||
vi.mocked(Utils.getCurrentDate).mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('getAssistantPrompt', () => {
|
||||
it('should return a prompt with default values when minimal params are provided', () => {
|
||||
// Mock dependencies
|
||||
vi.mocked(Utils.selectEquitably).mockReturnValue({
|
||||
'question1': 'answer1',
|
||||
'question2': 'answer2'
|
||||
});
|
||||
vi.mocked(Utils.getCurrentDate).mockReturnValue('2023-01-01T12:30:45Z');
|
||||
|
||||
const prompt = AssistantSdk.getAssistantPrompt({});
|
||||
|
||||
expect(prompt).toContain('# Assistant Knowledge');
|
||||
expect(prompt).toContain('2023-01-01');
|
||||
expect(prompt).toContain('- **Web Host**: geoff.seemueller.io');
|
||||
expect(prompt).toContain('- **User Location**: Unknown');
|
||||
expect(prompt).toContain('- **Timezone**: UTC');
|
||||
expect(prompt).not.toContain('- **Response Limit**:');
|
||||
});
|
||||
|
||||
it('should include maxTokens when provided', () => {
|
||||
// Mock dependencies
|
||||
vi.mocked(Utils.selectEquitably).mockReturnValue({
|
||||
'question1': 'answer1',
|
||||
'question2': 'answer2'
|
||||
});
|
||||
vi.mocked(Utils.getCurrentDate).mockReturnValue('2023-01-01T12:30:45Z');
|
||||
|
||||
const prompt = AssistantSdk.getAssistantPrompt({ maxTokens: 1000 });
|
||||
|
||||
expect(prompt).toContain('- **Response Limit**: 1000 tokens (maximum)');
|
||||
});
|
||||
|
||||
it('should use provided userTimezone and userLocation', () => {
|
||||
// Mock dependencies
|
||||
vi.mocked(Utils.selectEquitably).mockReturnValue({
|
||||
'question1': 'answer1',
|
||||
'question2': 'answer2'
|
||||
});
|
||||
vi.mocked(Utils.getCurrentDate).mockReturnValue('2023-01-01T12:30:45Z');
|
||||
|
||||
const prompt = AssistantSdk.getAssistantPrompt({
|
||||
userTimezone: 'America/New_York',
|
||||
userLocation: 'New York, USA'
|
||||
});
|
||||
|
||||
expect(prompt).toContain('- **User Location**: New York, USA');
|
||||
expect(prompt).toContain('- **Timezone**: America/New_York');
|
||||
});
|
||||
|
||||
it('should use current date when Utils.getCurrentDate is not available', () => {
|
||||
// Mock dependencies
|
||||
vi.mocked(Utils.selectEquitably).mockReturnValue({
|
||||
'question1': 'answer1',
|
||||
'question2': 'answer2'
|
||||
});
|
||||
vi.mocked(Utils.getCurrentDate).mockReturnValue(undefined);
|
||||
|
||||
const prompt = AssistantSdk.getAssistantPrompt({});
|
||||
|
||||
// Instead of checking for a specific date, just verify that a date is included
|
||||
expect(prompt).toMatch(/- \*\*Date\*\*: \d{4}-\d{2}-\d{2} \d{1,2}:\d{2} \d{1,2}s/);
|
||||
});
|
||||
|
||||
it('should use few_shots directly when Utils.selectEquitably is not available', () => {
|
||||
// Mock dependencies
|
||||
vi.mocked(Utils.selectEquitably).mockReturnValue(undefined);
|
||||
vi.mocked(Utils.getCurrentDate).mockReturnValue('2023-01-01T12:30:45Z');
|
||||
|
||||
const prompt = AssistantSdk.getAssistantPrompt({});
|
||||
|
||||
// The prompt should still contain examples
|
||||
expect(prompt).toContain('#### Example 1');
|
||||
// Instead of checking for specific content, just verify that examples are included
|
||||
expect(prompt).toMatch(/\*\*Human\*\*: .+\n\*\*Assistant\*\*: .+/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useFewshots', () => {
|
||||
it('should format fewshots correctly', () => {
|
||||
const fewshots = {
|
||||
'What is the capital of France?': 'Paris is the capital of France.',
|
||||
'How do I make pasta?': 'Boil water, add pasta, cook until al dente.'
|
||||
};
|
||||
|
||||
const result = AssistantSdk.useFewshots(fewshots);
|
||||
|
||||
expect(result).toContain('#### Example 1');
|
||||
expect(result).toContain('**Human**: What is the capital of France?');
|
||||
expect(result).toContain('**Assistant**: Paris is the capital of France.');
|
||||
expect(result).toContain('#### Example 2');
|
||||
expect(result).toContain('**Human**: How do I make pasta?');
|
||||
expect(result).toContain('**Assistant**: Boil water, add pasta, cook until al dente.');
|
||||
});
|
||||
|
||||
it('should respect the limit parameter', () => {
|
||||
const fewshots = {
|
||||
'Q1': 'A1',
|
||||
'Q2': 'A2',
|
||||
'Q3': 'A3',
|
||||
'Q4': 'A4',
|
||||
'Q5': 'A5',
|
||||
'Q6': 'A6'
|
||||
};
|
||||
|
||||
const result = AssistantSdk.useFewshots(fewshots, 3);
|
||||
|
||||
expect(result).toContain('#### Example 1');
|
||||
expect(result).toContain('**Human**: Q1');
|
||||
expect(result).toContain('**Assistant**: A1');
|
||||
expect(result).toContain('#### Example 2');
|
||||
expect(result).toContain('**Human**: Q2');
|
||||
expect(result).toContain('**Assistant**: A2');
|
||||
expect(result).toContain('#### Example 3');
|
||||
expect(result).toContain('**Human**: Q3');
|
||||
expect(result).toContain('**Assistant**: A3');
|
||||
expect(result).not.toContain('#### Example 4');
|
||||
expect(result).not.toContain('**Human**: Q4');
|
||||
});
|
||||
});
|
||||
});
|
237
packages/server/lib/__tests__/chat-sdk.test.ts
Normal file
237
packages/server/lib/__tests__/chat-sdk.test.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ChatSdk } from '../chat-sdk.ts';
|
||||
import { AssistantSdk } from '../assistant-sdk.ts';
|
||||
import Message from '../../models/Message.ts';
|
||||
import { getModelFamily } from '@open-gsio/ai/supported-models';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../assistant-sdk', () => ({
|
||||
AssistantSdk: {
|
||||
getAssistantPrompt: vi.fn()
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('../../models/Message', () => ({
|
||||
default: {
|
||||
create: vi.fn((message) => message)
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('@open-gsio/ai/supported-models', () => ({
|
||||
getModelFamily: vi.fn()
|
||||
}));
|
||||
|
||||
describe('ChatSdk', () => {
|
||||
beforeEach(() => {
|
||||
// Reset mocks
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('preprocess', () => {
|
||||
it('should return an assistant message with empty content', async () => {
|
||||
const messages = [{ role: 'user', content: 'Hello' }];
|
||||
|
||||
const result = await ChatSdk.preprocess({ messages });
|
||||
|
||||
expect(Message.create).toHaveBeenCalledWith({
|
||||
role: 'assistant',
|
||||
content: ''
|
||||
});
|
||||
expect(result).toEqual({
|
||||
role: 'assistant',
|
||||
content: ''
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleChatRequest', () => {
|
||||
it('should return a 400 response if no messages are provided', async () => {
|
||||
const request = {
|
||||
json: vi.fn().mockResolvedValue({ messages: [] })
|
||||
};
|
||||
const ctx = {
|
||||
openai: {},
|
||||
systemPrompt: 'System prompt',
|
||||
maxTokens: 1000,
|
||||
env: {
|
||||
SERVER_COORDINATOR: {
|
||||
idFromName: vi.fn(),
|
||||
get: vi.fn()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await ChatSdk.handleChatRequest(request as any, ctx as any);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(await response.text()).toBe('No messages provided');
|
||||
});
|
||||
|
||||
it('should save stream data and return a response with streamUrl', async () => {
|
||||
const streamId = 'test-uuid';
|
||||
vi.stubGlobal('crypto', {
|
||||
randomUUID: vi.fn().mockReturnValue(streamId)
|
||||
});
|
||||
|
||||
const messages = [{ role: 'user', content: 'Hello' }];
|
||||
const model = 'gpt-4';
|
||||
const conversationId = 'conv-123';
|
||||
|
||||
const request = {
|
||||
json: vi.fn().mockResolvedValue({ messages, model, conversationId })
|
||||
};
|
||||
|
||||
const saveStreamData = vi.fn();
|
||||
const durableObject = {
|
||||
saveStreamData
|
||||
};
|
||||
|
||||
const ctx = {
|
||||
openai: {},
|
||||
systemPrompt: 'System prompt',
|
||||
maxTokens: 1000,
|
||||
env: {
|
||||
SERVER_COORDINATOR: {
|
||||
idFromName: vi.fn().mockReturnValue('object-id'),
|
||||
get: vi.fn().mockReturnValue(durableObject)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const response = await ChatSdk.handleChatRequest(request as any, ctx as any);
|
||||
const responseBody = await response.json();
|
||||
|
||||
expect(ctx.env.SERVER_COORDINATOR.idFromName).toHaveBeenCalledWith('stream-index');
|
||||
expect(ctx.env.SERVER_COORDINATOR.get).toHaveBeenCalledWith('object-id');
|
||||
expect(saveStreamData).toHaveBeenCalledWith(
|
||||
streamId,
|
||||
expect.stringContaining(model)
|
||||
);
|
||||
expect(responseBody).toEqual({
|
||||
streamUrl: `/api/streams/${streamId}`
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateMaxTokens', () => {
|
||||
it('should call the durable object to calculate max tokens', async () => {
|
||||
const messages = [{ role: 'user', content: 'Hello' }];
|
||||
const dynamicMaxTokens = vi.fn().mockResolvedValue(500);
|
||||
const durableObject = {
|
||||
dynamicMaxTokens
|
||||
};
|
||||
|
||||
const ctx = {
|
||||
maxTokens: 1000,
|
||||
env: {
|
||||
SERVER_COORDINATOR: {
|
||||
idFromName: vi.fn().mockReturnValue('object-id'),
|
||||
get: vi.fn().mockReturnValue(durableObject)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await ChatSdk.calculateMaxTokens(messages, ctx as any);
|
||||
|
||||
expect(ctx.env.SERVER_COORDINATOR.idFromName).toHaveBeenCalledWith('dynamic-token-counter');
|
||||
expect(ctx.env.SERVER_COORDINATOR.get).toHaveBeenCalledWith('object-id');
|
||||
expect(dynamicMaxTokens).toHaveBeenCalledWith(messages, 1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildAssistantPrompt', () => {
|
||||
it('should call AssistantSdk.getAssistantPrompt with the correct parameters', () => {
|
||||
vi.mocked(AssistantSdk.getAssistantPrompt).mockReturnValue('Assistant prompt');
|
||||
|
||||
const result = ChatSdk.buildAssistantPrompt({ maxTokens: 1000 });
|
||||
|
||||
expect(AssistantSdk.getAssistantPrompt).toHaveBeenCalledWith({
|
||||
maxTokens: 1000,
|
||||
userTimezone: 'UTC',
|
||||
userLocation: 'USA/unknown'
|
||||
});
|
||||
expect(result).toBe('Assistant prompt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildMessageChain', () => {
|
||||
it('should build a message chain with system role for most models', () => {
|
||||
vi.mocked(getModelFamily).mockReturnValue('openai');
|
||||
|
||||
const messages = [
|
||||
{ role: 'user', content: 'Hello' }
|
||||
];
|
||||
|
||||
const opts = {
|
||||
systemPrompt: 'System prompt',
|
||||
assistantPrompt: 'Assistant prompt',
|
||||
toolResults: { role: 'tool', content: 'Tool result' },
|
||||
model: 'gpt-4'
|
||||
};
|
||||
|
||||
const result = ChatSdk.buildMessageChain(messages, opts as any);
|
||||
|
||||
expect(getModelFamily).toHaveBeenCalledWith('gpt-4');
|
||||
expect(Message.create).toHaveBeenCalledTimes(3);
|
||||
expect(Message.create).toHaveBeenNthCalledWith(1, {
|
||||
role: 'system',
|
||||
content: 'System prompt'
|
||||
});
|
||||
expect(Message.create).toHaveBeenNthCalledWith(2, {
|
||||
role: 'assistant',
|
||||
content: 'Assistant prompt'
|
||||
});
|
||||
expect(Message.create).toHaveBeenNthCalledWith(3, {
|
||||
role: 'user',
|
||||
content: 'Hello'
|
||||
});
|
||||
});
|
||||
|
||||
it('should build a message chain with assistant role for o1, gemma, claude, or google models', () => {
|
||||
vi.mocked(getModelFamily).mockReturnValue('claude');
|
||||
|
||||
const messages = [
|
||||
{ role: 'user', content: 'Hello' }
|
||||
];
|
||||
|
||||
const opts = {
|
||||
systemPrompt: 'System prompt',
|
||||
assistantPrompt: 'Assistant prompt',
|
||||
toolResults: { role: 'tool', content: 'Tool result' },
|
||||
model: 'claude-3'
|
||||
};
|
||||
|
||||
const result = ChatSdk.buildMessageChain(messages, opts as any);
|
||||
|
||||
expect(getModelFamily).toHaveBeenCalledWith('claude-3');
|
||||
expect(Message.create).toHaveBeenCalledTimes(3);
|
||||
expect(Message.create).toHaveBeenNthCalledWith(1, {
|
||||
role: 'assistant',
|
||||
content: 'System prompt'
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter out messages with empty content', () => {
|
||||
vi.mocked(getModelFamily).mockReturnValue('openai');
|
||||
|
||||
const messages = [
|
||||
{ role: 'user', content: 'Hello' },
|
||||
{ role: 'user', content: '' },
|
||||
{ role: 'user', content: ' ' },
|
||||
{ role: 'user', content: 'World' }
|
||||
];
|
||||
|
||||
const opts = {
|
||||
systemPrompt: 'System prompt',
|
||||
assistantPrompt: 'Assistant prompt',
|
||||
toolResults: { role: 'tool', content: 'Tool result' },
|
||||
model: 'gpt-4'
|
||||
};
|
||||
|
||||
const result = ChatSdk.buildMessageChain(messages, opts as any);
|
||||
|
||||
// 2 system/assistant messages + 2 user messages (Hello and World)
|
||||
expect(Message.create).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
});
|
||||
});
|
40
packages/server/lib/__tests__/debug-utils.test.ts
Normal file
40
packages/server/lib/__tests__/debug-utils.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Utils } from '../utils.ts';
|
||||
|
||||
describe('Debug Utils.getSeason', () => {
|
||||
it('should print out the actual seasons for different dates', () => {
|
||||
// Test dates with more specific focus on boundaries
|
||||
const dates = [
|
||||
// June boundary (month 5)
|
||||
'2023-06-20', // June 20
|
||||
'2023-06-21', // June 21
|
||||
'2023-06-22', // June 22
|
||||
'2023-06-23', // June 23
|
||||
|
||||
// September boundary (month 8)
|
||||
'2023-09-20', // September 20
|
||||
'2023-09-21', // September 21
|
||||
'2023-09-22', // September 22
|
||||
'2023-09-23', // September 23
|
||||
'2023-09-24', // September 24
|
||||
|
||||
// Also check the implementation directly
|
||||
'2023-06-22', // month === 5 && day > 21 should be Summer
|
||||
'2023-09-23', // month === 8 && day > 22 should be Autumn
|
||||
];
|
||||
|
||||
// Print out the actual seasons
|
||||
console.log('Date | Month | Day | Season');
|
||||
console.log('-----|-------|-----|-------');
|
||||
dates.forEach(date => {
|
||||
const d = new Date(date);
|
||||
const month = d.getMonth();
|
||||
const day = d.getDate();
|
||||
const season = Utils.getSeason(date);
|
||||
console.log(`${date} | ${month} | ${day} | ${season}`);
|
||||
});
|
||||
|
||||
// This test will always pass, it's just for debugging
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
188
packages/server/lib/__tests__/handleStreamData.test.ts
Normal file
188
packages/server/lib/__tests__/handleStreamData.test.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import handleStreamData from '../handleStreamData.ts';
|
||||
|
||||
describe('handleStreamData', () => {
|
||||
// Setup mocks
|
||||
const mockController = {
|
||||
enqueue: vi.fn()
|
||||
};
|
||||
const mockEncoder = {
|
||||
encode: vi.fn((str) => str)
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should return early if data type is not "chat"', () => {
|
||||
const handler = handleStreamData(mockController as any, mockEncoder as any);
|
||||
|
||||
handler({ type: 'not-chat', data: {} });
|
||||
|
||||
expect(mockController.enqueue).not.toHaveBeenCalled();
|
||||
expect(mockEncoder.encode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return early if data is undefined', () => {
|
||||
const handler = handleStreamData(mockController as any, mockEncoder as any);
|
||||
|
||||
handler(undefined as any);
|
||||
|
||||
expect(mockController.enqueue).not.toHaveBeenCalled();
|
||||
expect(mockEncoder.encode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle content_block_start type data', () => {
|
||||
const handler = handleStreamData(mockController as any, mockEncoder as any);
|
||||
|
||||
const data = {
|
||||
type: 'chat',
|
||||
data: {
|
||||
type: 'content_block_start',
|
||||
content_block: {
|
||||
type: 'text',
|
||||
text: 'Hello world'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handler(data);
|
||||
|
||||
expect(mockController.enqueue).toHaveBeenCalledTimes(1);
|
||||
expect(mockEncoder.encode).toHaveBeenCalledWith(expect.stringContaining('Hello world'));
|
||||
|
||||
const encodedData = mockEncoder.encode.mock.calls[0][0];
|
||||
const parsedData = JSON.parse(encodedData.split('data: ')[1]);
|
||||
|
||||
expect(parsedData.type).toBe('chat');
|
||||
expect(parsedData.data.choices[0].delta.content).toBe('Hello world');
|
||||
});
|
||||
|
||||
it('should handle delta.text type data', () => {
|
||||
const handler = handleStreamData(mockController as any, mockEncoder as any);
|
||||
|
||||
const data = {
|
||||
type: 'chat',
|
||||
data: {
|
||||
delta: {
|
||||
text: 'Hello world'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handler(data);
|
||||
|
||||
expect(mockController.enqueue).toHaveBeenCalledTimes(1);
|
||||
expect(mockEncoder.encode).toHaveBeenCalledWith(expect.stringContaining('Hello world'));
|
||||
|
||||
const encodedData = mockEncoder.encode.mock.calls[0][0];
|
||||
const parsedData = JSON.parse(encodedData.split('data: ')[1]);
|
||||
|
||||
expect(parsedData.type).toBe('chat');
|
||||
expect(parsedData.data.choices[0].delta.content).toBe('Hello world');
|
||||
});
|
||||
|
||||
it('should handle choices[0].delta.content type data', () => {
|
||||
const handler = handleStreamData(mockController as any, mockEncoder as any);
|
||||
|
||||
const data = {
|
||||
type: 'chat',
|
||||
data: {
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {
|
||||
content: 'Hello world'
|
||||
},
|
||||
logprobs: null,
|
||||
finish_reason: null
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
handler(data);
|
||||
|
||||
expect(mockController.enqueue).toHaveBeenCalledTimes(1);
|
||||
expect(mockEncoder.encode).toHaveBeenCalledWith(expect.stringContaining('Hello world'));
|
||||
|
||||
const encodedData = mockEncoder.encode.mock.calls[0][0];
|
||||
const parsedData = JSON.parse(encodedData.split('data: ')[1]);
|
||||
|
||||
expect(parsedData.type).toBe('chat');
|
||||
expect(parsedData.data.choices[0].delta.content).toBe('Hello world');
|
||||
expect(parsedData.data.choices[0].finish_reason).toBe(null);
|
||||
});
|
||||
|
||||
it('should pass through data with choices but no delta.content', () => {
|
||||
const handler = handleStreamData(mockController as any, mockEncoder as any);
|
||||
|
||||
const data = {
|
||||
type: 'chat',
|
||||
data: {
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
delta: {},
|
||||
logprobs: null,
|
||||
finish_reason: 'stop'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
handler(data);
|
||||
|
||||
expect(mockController.enqueue).toHaveBeenCalledTimes(1);
|
||||
expect(mockEncoder.encode).toHaveBeenCalledWith(expect.stringContaining('"finish_reason":"stop"'));
|
||||
});
|
||||
|
||||
it('should return early for unrecognized data format', () => {
|
||||
const handler = handleStreamData(mockController as any, mockEncoder as any);
|
||||
|
||||
const data = {
|
||||
type: 'chat',
|
||||
data: {
|
||||
// No recognized properties
|
||||
unrecognized: 'property'
|
||||
}
|
||||
};
|
||||
|
||||
handler(data);
|
||||
|
||||
expect(mockController.enqueue).not.toHaveBeenCalled();
|
||||
expect(mockEncoder.encode).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use custom transform function if provided', () => {
|
||||
const handler = handleStreamData(mockController as any, mockEncoder as any);
|
||||
|
||||
const data = {
|
||||
type: 'chat',
|
||||
data: {
|
||||
original: 'data'
|
||||
}
|
||||
};
|
||||
|
||||
const transformFn = vi.fn().mockReturnValue({
|
||||
type: 'chat',
|
||||
data: {
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
content: 'Transformed content'
|
||||
},
|
||||
logprobs: null,
|
||||
finish_reason: null
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
handler(data, transformFn);
|
||||
|
||||
expect(transformFn).toHaveBeenCalledWith(data);
|
||||
expect(mockController.enqueue).toHaveBeenCalledTimes(1);
|
||||
expect(mockEncoder.encode).toHaveBeenCalledWith(expect.stringContaining('Transformed content'));
|
||||
});
|
||||
});
|
195
packages/server/lib/__tests__/utils.test.ts
Normal file
195
packages/server/lib/__tests__/utils.test.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { Utils } from '../utils.ts';
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('getSeason', () => {
|
||||
// Based on the actual behavior from debug tests (months are 0-indexed in JavaScript):
|
||||
// Winter: month < 2 (Jan, Feb) OR month === 2 && day <= 20 (Mar 1-20) OR month === 11 (Dec)
|
||||
// Spring: (month === 2 && day > 20) (Mar 21-31) OR month === 3 || month === 4 (Apr, May) OR (month === 5 && day <= 21) (Jun 1-21)
|
||||
// Summer: (month === 5 && day > 21) (Jun 22-30) OR month === 6 || month === 7 (Jul, Aug) OR (month === 8 && day <= 22) (Sep 1-22)
|
||||
// Autumn: (month === 8 && day > 22) (Sep 23-30) OR month === 9 || month === 10 (Oct, Nov)
|
||||
|
||||
it('should return Winter for dates in winter in Northern Hemisphere', () => {
|
||||
expect(Utils.getSeason('2023-01-15')).toBe('Winter'); // January (month 0)
|
||||
expect(Utils.getSeason('2023-02-15')).toBe('Winter'); // February (month 1)
|
||||
expect(Utils.getSeason('2023-03-20')).toBe('Winter'); // March 20 (month 2)
|
||||
expect(Utils.getSeason('2023-12-15')).toBe('Winter'); // December (month 11)
|
||||
});
|
||||
|
||||
it('should return Spring for dates in spring in Northern Hemisphere', () => {
|
||||
expect(Utils.getSeason('2023-03-25')).toBe('Spring'); // March 25 (month 2)
|
||||
expect(Utils.getSeason('2023-04-15')).toBe('Spring'); // April (month 3)
|
||||
expect(Utils.getSeason('2023-05-15')).toBe('Spring'); // May (month 4)
|
||||
expect(Utils.getSeason('2023-06-21')).toBe('Spring'); // June 21 (month 5)
|
||||
});
|
||||
|
||||
it('should return Summer for dates in summer in Northern Hemisphere', () => {
|
||||
expect(Utils.getSeason('2023-06-23')).toBe('Summer'); // June 23 (month 5)
|
||||
expect(Utils.getSeason('2023-07-15')).toBe('Summer'); // July (month 6)
|
||||
expect(Utils.getSeason('2023-08-15')).toBe('Summer'); // August (month 7)
|
||||
expect(Utils.getSeason('2023-09-22')).toBe('Summer'); // September 22 (month 8)
|
||||
});
|
||||
|
||||
it('should return Autumn for dates in autumn in Northern Hemisphere', () => {
|
||||
expect(Utils.getSeason('2023-09-24')).toBe('Autumn'); // September 24 (month 8)
|
||||
expect(Utils.getSeason('2023-10-15')).toBe('Autumn'); // October (month 9)
|
||||
expect(Utils.getSeason('2023-11-15')).toBe('Autumn'); // November (month 10)
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTimezone', () => {
|
||||
const originalDateTimeFormat = Intl.DateTimeFormat;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock Intl.DateTimeFormat
|
||||
global.Intl.DateTimeFormat = vi.fn().mockReturnValue({
|
||||
resolvedOptions: vi.fn().mockReturnValue({
|
||||
timeZone: 'America/New_York'
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original
|
||||
global.Intl.DateTimeFormat = originalDateTimeFormat;
|
||||
});
|
||||
|
||||
it('should return the provided timezone if available', () => {
|
||||
expect(Utils.getTimezone('Europe/London')).toBe('Europe/London');
|
||||
});
|
||||
|
||||
it('should return the system timezone if no timezone is provided', () => {
|
||||
expect(Utils.getTimezone(undefined)).toBe('America/New_York');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCurrentDate', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2023-01-01T12:30:45Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return the current date as an ISO string', () => {
|
||||
expect(Utils.getCurrentDate()).toBe('2023-01-01T12:30:45.000Z');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAssetUrl', () => {
|
||||
it('should return true for URLs starting with /assets/', () => {
|
||||
expect(Utils.isAssetUrl('https://example.com/assets/image.png')).toBe(true);
|
||||
expect(Utils.isAssetUrl('http://localhost:8080/assets/script.js')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for URLs not starting with /assets/', () => {
|
||||
expect(Utils.isAssetUrl('https://example.com/api/data')).toBe(false);
|
||||
expect(Utils.isAssetUrl('http://localhost:8080/images/logo.png')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectEquitably', () => {
|
||||
beforeEach(() => {
|
||||
// Mock Math.random to return predictable values
|
||||
vi.spyOn(Math, 'random').mockReturnValue(0.5);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should select items equitably from multiple sources', () => {
|
||||
const sources = {
|
||||
a: { 'key1': 'value1', 'key2': 'value2' },
|
||||
b: { 'key3': 'value3', 'key4': 'value4' },
|
||||
c: { 'key5': 'value5', 'key6': 'value6' },
|
||||
d: { 'key7': 'value7', 'key8': 'value8' }
|
||||
};
|
||||
|
||||
const result = Utils.selectEquitably(sources, 4);
|
||||
|
||||
expect(Object.keys(result).length).toBe(4);
|
||||
// Due to the mocked Math.random, the selection should be deterministic
|
||||
// but we can't predict the exact keys due to the sort, so we just check the count
|
||||
});
|
||||
|
||||
it('should handle itemCount greater than available items', () => {
|
||||
const sources = {
|
||||
a: { 'key1': 'value1' },
|
||||
b: { 'key2': 'value2' },
|
||||
c: {},
|
||||
d: {}
|
||||
};
|
||||
|
||||
const result = Utils.selectEquitably(sources, 5);
|
||||
|
||||
expect(Object.keys(result).length).toBe(2);
|
||||
expect(result).toHaveProperty('key1');
|
||||
expect(result).toHaveProperty('key2');
|
||||
});
|
||||
|
||||
it('should handle empty sources', () => {
|
||||
const sources = {
|
||||
a: {},
|
||||
b: {},
|
||||
c: {},
|
||||
d: {}
|
||||
};
|
||||
|
||||
const result = Utils.selectEquitably(sources, 5);
|
||||
|
||||
expect(Object.keys(result).length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeWithBlanks', () => {
|
||||
it('should insert blank messages to maintain user/assistant alternation', () => {
|
||||
const messages = [
|
||||
{ role: 'user', content: 'Hello' },
|
||||
{ role: 'user', content: 'How are you?' }
|
||||
];
|
||||
|
||||
const result = Utils.normalizeWithBlanks(messages);
|
||||
|
||||
expect(result.length).toBe(3);
|
||||
expect(result[0]).toEqual({ role: 'user', content: 'Hello' });
|
||||
expect(result[1]).toEqual({ role: 'assistant', content: '' });
|
||||
expect(result[2]).toEqual({ role: 'user', content: 'How are you?' });
|
||||
});
|
||||
|
||||
it('should insert blank user message if first message is assistant', () => {
|
||||
const messages = [
|
||||
{ role: 'assistant', content: 'Hello, how can I help?' }
|
||||
];
|
||||
|
||||
const result = Utils.normalizeWithBlanks(messages);
|
||||
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0]).toEqual({ role: 'user', content: '' });
|
||||
expect(result[1]).toEqual({ role: 'assistant', content: 'Hello, how can I help?' });
|
||||
});
|
||||
|
||||
it('should handle empty array', () => {
|
||||
const messages: any[] = [];
|
||||
|
||||
const result = Utils.normalizeWithBlanks(messages);
|
||||
|
||||
expect(result.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle already alternating messages', () => {
|
||||
const messages = [
|
||||
{ role: 'user', content: 'Hello' },
|
||||
{ role: 'assistant', content: 'Hi there' },
|
||||
{ role: 'user', content: 'How are you?' }
|
||||
];
|
||||
|
||||
const result = Utils.normalizeWithBlanks(messages);
|
||||
|
||||
expect(result.length).toBe(3);
|
||||
expect(result).toEqual(messages);
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user