@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
569 lines (501 loc) • 17.1 kB
text/typescript
import { AsyncTaskStatus } from '@/types/asyncTask';
import { Generation, GenerationBatch } from '@/types/generation';
import { GenerationBatchDispatch, generationBatchReducer } from './reducer';
// Test data factory functions
const createMockGeneration = (overrides: Partial<Generation> = {}): Generation => ({
id: 'gen1',
asset: {
type: 'image',
originalUrl: 'https://example.com/image1.jpg',
url: 'files/image1.jpg',
thumbnailUrl: 'files/image1_thumb.jpg',
width: 1024,
height: 1024,
},
seed: 12345,
asyncTaskId: 'task1',
createdAt: new Date('2024-01-01'),
task: {
id: 'task1',
status: AsyncTaskStatus.Success,
},
...overrides,
});
const createMockGenerationBatch = (overrides: Partial<GenerationBatch> = {}): GenerationBatch => ({
id: 'batch1',
provider: 'openai',
model: 'dall-e-3',
prompt: 'A beautiful sunset',
width: 1024,
height: 1024,
config: {
prompt: 'A beautiful sunset',
width: 1024,
height: 1024,
},
generations: [],
createdAt: new Date('2024-01-01'),
...overrides,
});
describe('generationBatchReducer', () => {
let initialState: GenerationBatch[];
beforeEach(() => {
initialState = [
createMockGenerationBatch({
id: 'batch1',
generations: [
createMockGeneration({
id: 'gen1',
}),
createMockGeneration({
id: 'gen2',
asset: {
type: 'image',
originalUrl: 'https://example.com/image2.jpg',
url: 'files/image2.jpg',
thumbnailUrl: 'files/image2_thumb.jpg',
width: 1024,
height: 1024,
},
seed: 67890,
asyncTaskId: 'task2',
task: {
id: 'task2',
status: AsyncTaskStatus.Success,
},
}),
],
}),
createMockGenerationBatch({
id: 'batch2',
provider: 'midjourney',
model: 'mj-v6',
prompt: 'A futuristic city',
config: {
prompt: 'A futuristic city',
width: 1024,
height: 1024,
},
generations: [
createMockGeneration({
id: 'gen3',
asset: {
type: 'image',
originalUrl: 'https://example.com/image3.jpg',
url: 'files/image3.jpg',
thumbnailUrl: 'files/image3_thumb.jpg',
width: 1024,
height: 1024,
},
seed: 11111,
asyncTaskId: 'task3',
createdAt: new Date('2024-01-02'),
task: {
id: 'task3',
status: AsyncTaskStatus.Success,
},
}),
],
createdAt: new Date('2024-01-02'),
}),
];
});
describe('updateGenerationInBatch', () => {
it('should update the specified generation within a batch', () => {
const payload: GenerationBatchDispatch = {
type: 'updateGenerationInBatch',
batchId: 'batch1',
generationId: 'gen1',
value: {
asset: {
type: 'image',
url: 'files/newFile1.jpg',
width: 2048,
height: 2048,
},
seed: 99999,
},
};
const newState = generationBatchReducer(initialState, payload);
const updatedBatch = newState.find((batch) => batch.id === 'batch1');
const updatedGeneration = updatedBatch?.generations.find((gen) => gen.id === 'gen1');
expect(updatedGeneration).toBeDefined();
expect(updatedGeneration?.asset?.url).toBe('files/newFile1.jpg');
expect(updatedGeneration?.asset?.width).toBe(2048);
expect(updatedGeneration?.seed).toBe(99999);
// Ensure other properties remain unchanged
expect(updatedGeneration?.asyncTaskId).toBe('task1');
});
it('should not modify the state if the batch is not found', () => {
const payload: GenerationBatchDispatch = {
type: 'updateGenerationInBatch',
batchId: 'nonexistentBatch',
generationId: 'gen1',
value: { seed: 99999 },
};
const newState = generationBatchReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
it('should not modify the state if the generation is not found', () => {
const payload: GenerationBatchDispatch = {
type: 'updateGenerationInBatch',
batchId: 'batch1',
generationId: 'nonexistentGen',
value: { seed: 99999 },
};
const newState = generationBatchReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
it('should only update the specified generation and not affect others', () => {
const payload: GenerationBatchDispatch = {
type: 'updateGenerationInBatch',
batchId: 'batch1',
generationId: 'gen1',
value: { seed: 99999 },
};
const newState = generationBatchReducer(initialState, payload);
const batch = newState.find((b) => b.id === 'batch1');
const gen1 = batch?.generations.find((g) => g.id === 'gen1');
const gen2 = batch?.generations.find((g) => g.id === 'gen2');
expect(gen1?.seed).toBe(99999);
expect(gen2?.seed).toBe(67890); // Unchanged
});
it('should update asset properties', () => {
const payload: GenerationBatchDispatch = {
type: 'updateGenerationInBatch',
batchId: 'batch1',
generationId: 'gen1',
value: {
asset: {
type: 'image',
originalUrl: 'https://example.com/new-image.jpg',
url: 'files/new-image.jpg',
thumbnailUrl: 'files/new-image_thumb.jpg',
width: 2048,
height: 2048,
},
},
};
const newState = generationBatchReducer(initialState, payload);
const updatedGeneration = newState
.find((batch) => batch.id === 'batch1')
?.generations.find((gen) => gen.id === 'gen1');
expect(updatedGeneration?.asset).toEqual({
type: 'image',
originalUrl: 'https://example.com/new-image.jpg',
url: 'files/new-image.jpg',
thumbnailUrl: 'files/new-image_thumb.jpg',
width: 2048,
height: 2048,
});
});
});
describe('updateBatch', () => {
it('should update the specified batch with the provided value', () => {
const payload: GenerationBatchDispatch = {
type: 'updateBatch',
id: 'batch1',
value: { prompt: 'Updated prompt', width: 2048 },
};
const newState = generationBatchReducer(initialState, payload);
const updatedBatch = newState.find((batch) => batch.id === 'batch1');
expect(updatedBatch).toBeDefined();
expect(updatedBatch?.prompt).toBe('Updated prompt');
expect(updatedBatch?.width).toBe(2048);
// Ensure other properties remain unchanged
expect(updatedBatch?.height).toBe(1024);
expect(updatedBatch?.generations.length).toBe(2);
});
it('should not modify the state if the batch is not found', () => {
const payload: GenerationBatchDispatch = {
type: 'updateBatch',
id: 'nonexistentBatch',
value: { prompt: 'Updated prompt' },
};
const newState = generationBatchReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
it('should only update the specified batch and not affect others', () => {
const payload: GenerationBatchDispatch = {
type: 'updateBatch',
id: 'batch1',
value: { prompt: 'Updated prompt' },
};
const newState = generationBatchReducer(initialState, payload);
const batch1 = newState.find((b) => b.id === 'batch1');
const batch2 = newState.find((b) => b.id === 'batch2');
expect(batch1?.prompt).toBe('Updated prompt');
expect(batch2?.prompt).toBe('A futuristic city'); // Unchanged
});
it('should update config properties', () => {
const payload: GenerationBatchDispatch = {
type: 'updateBatch',
id: 'batch1',
value: {
config: {
prompt: 'Updated prompt with config',
width: 2048,
height: 2048,
steps: 50,
cfg: 7.5,
},
},
};
const newState = generationBatchReducer(initialState, payload);
const updatedBatch = newState.find((batch) => batch.id === 'batch1');
expect(updatedBatch?.config).toEqual({
prompt: 'Updated prompt with config',
width: 2048,
height: 2048,
steps: 50,
cfg: 7.5,
});
});
});
describe('deleteBatch', () => {
it('should remove the specified batch from the state', () => {
const payload: GenerationBatchDispatch = {
type: 'deleteBatch',
id: 'batch1',
};
const newState = generationBatchReducer(initialState, payload);
expect(newState.length).toBe(1);
expect(newState.find((batch) => batch.id === 'batch1')).toBeUndefined();
expect(newState.find((batch) => batch.id === 'batch2')).toBeDefined();
});
it('should not modify the state if the batch to delete is not found', () => {
const payload: GenerationBatchDispatch = {
type: 'deleteBatch',
id: 'nonexistentBatch',
};
const newState = generationBatchReducer(initialState, payload);
expect(newState).toEqual(initialState);
expect(newState.length).toBe(2);
});
it('should handle deleting all batches', () => {
const state1 = generationBatchReducer(initialState, {
type: 'deleteBatch',
id: 'batch1',
});
const state2 = generationBatchReducer(state1, {
type: 'deleteBatch',
id: 'batch2',
});
expect(state2.length).toBe(0);
});
});
describe('deleteGenerationInBatch', () => {
it('should remove the specified generation from a batch', () => {
const payload: GenerationBatchDispatch = {
type: 'deleteGenerationInBatch',
batchId: 'batch1',
generationId: 'gen1',
};
const newState = generationBatchReducer(initialState, payload);
const updatedBatch = newState.find((batch) => batch.id === 'batch1');
expect(updatedBatch?.generations.length).toBe(1);
expect(updatedBatch?.generations.find((gen) => gen.id === 'gen1')).toBeUndefined();
expect(updatedBatch?.generations.find((gen) => gen.id === 'gen2')).toBeDefined();
});
it('should not modify the state if the batch is not found', () => {
const payload: GenerationBatchDispatch = {
type: 'deleteGenerationInBatch',
batchId: 'nonexistentBatch',
generationId: 'gen1',
};
const newState = generationBatchReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
it('should not modify the state if the generation is not found', () => {
const payload: GenerationBatchDispatch = {
type: 'deleteGenerationInBatch',
batchId: 'batch1',
generationId: 'nonexistentGen',
};
const newState = generationBatchReducer(initialState, payload);
const batch = newState.find((b) => b.id === 'batch1');
expect(batch?.generations.length).toBe(2); // Still 2 generations
});
it('should handle deleting all generations from a batch', () => {
const state1 = generationBatchReducer(initialState, {
type: 'deleteGenerationInBatch',
batchId: 'batch1',
generationId: 'gen1',
});
const state2 = generationBatchReducer(state1, {
type: 'deleteGenerationInBatch',
batchId: 'batch1',
generationId: 'gen2',
});
const batch = state2.find((b) => b.id === 'batch1');
expect(batch?.generations.length).toBe(0);
});
});
describe('addBatch', () => {
it('should add a new batch to the state at the beginning', () => {
const newBatch = createMockGenerationBatch({
id: 'batch3',
provider: 'anthropic',
model: 'claude-3',
prompt: 'A new generation',
width: 512,
height: 512,
config: {
prompt: 'A new generation',
width: 512,
height: 512,
},
createdAt: new Date('2024-01-03'),
});
const payload: GenerationBatchDispatch = {
type: 'addBatch',
value: newBatch,
};
const newState = generationBatchReducer(initialState, payload);
expect(newState.length).toBe(3);
expect(newState[0]).toEqual(newBatch); // New batch is at the beginning
expect(newState[1]).toEqual(initialState[0]); // Original first batch is now second
});
it('should handle adding batch with generations', () => {
const newBatch: GenerationBatch = {
id: 'batch3',
provider: 'midjourney',
model: 'mj-v6',
prompt: 'Complex scene',
width: 1024,
height: 1024,
config: {
prompt: 'Complex scene',
width: 1024,
height: 1024,
},
generations: [
{
id: 'gen4',
asset: {
type: 'image',
originalUrl: 'https://example.com/image4.jpg',
url: 'files/image4.jpg',
thumbnailUrl: 'files/image4_thumb.jpg',
width: 1024,
height: 1024,
},
seed: 22222,
asyncTaskId: 'task4',
createdAt: new Date('2024-01-03'),
task: {
id: 'task4',
status: AsyncTaskStatus.Pending,
},
},
],
createdAt: new Date('2024-01-03'),
};
const payload: GenerationBatchDispatch = {
type: 'addBatch',
value: newBatch,
};
const newState = generationBatchReducer(initialState, payload);
expect(newState.length).toBe(3);
expect(newState[0].generations.length).toBe(1);
expect(newState[0].generations[0].id).toBe('gen4');
});
it('should handle adding batch to empty state', () => {
const newBatch: GenerationBatch = {
id: 'batch1',
provider: 'openai',
model: 'dall-e-3',
prompt: 'First batch',
width: 1024,
height: 1024,
config: {
prompt: 'First batch',
width: 1024,
height: 1024,
},
generations: [],
createdAt: new Date('2024-01-01'),
};
const payload: GenerationBatchDispatch = {
type: 'addBatch',
value: newBatch,
};
const newState = generationBatchReducer([], payload);
expect(newState.length).toBe(1);
expect(newState[0]).toEqual(newBatch);
});
it('should handle adding batch with null values', () => {
const newBatch: GenerationBatch = {
id: 'batch4',
provider: 'openai',
model: 'dall-e-3',
prompt: 'Test with nulls',
width: null,
height: null,
config: {
prompt: 'Test with nulls',
},
generations: [
{
id: 'gen5',
asset: null,
seed: null,
asyncTaskId: null,
createdAt: new Date('2024-01-04'),
task: {
id: 'task5',
status: AsyncTaskStatus.Error,
error: {
name: 'GEN_ERROR',
body: { detail: 'Generation failed' },
},
},
},
],
createdAt: new Date('2024-01-04'),
};
const payload: GenerationBatchDispatch = {
type: 'addBatch',
value: newBatch,
};
const newState = generationBatchReducer(initialState, payload);
expect(newState.length).toBe(3);
expect(newState[0].width).toBeNull();
expect(newState[0].height).toBeNull();
expect(newState[0].generations[0].asset).toBeNull();
expect(newState[0].generations[0].seed).toBeNull();
});
});
describe('default case', () => {
it('should return the current state for unknown action types', () => {
// @ts-expect-error - Testing unknown action type
const payload: GenerationBatchDispatch = { type: 'unknownType' };
const newState = generationBatchReducer(initialState, payload);
expect(newState).toEqual(initialState);
});
});
describe('edge cases', () => {
it('should handle undefined state gracefully', () => {
const payload: GenerationBatchDispatch = {
type: 'addBatch',
value: {
id: 'batch1',
provider: 'openai',
model: 'dall-e-3',
prompt: 'Test',
width: 1024,
height: 1024,
config: {
prompt: 'Test',
width: 1024,
height: 1024,
},
generations: [],
createdAt: new Date(),
},
};
const newState = generationBatchReducer(undefined, payload);
expect(newState.length).toBe(1);
});
});
});