@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.
464 lines (356 loc) • 14.2 kB
text/typescript
import { describe, expect, it, vi } from 'vitest';
import { ContextEngine } from '../pipeline';
import type { ContextProcessor, PipelineContext } from '../types';
import { PipelineError } from '../types';
describe('ContextEngine', () => {
const createMockProcessor = (name: string, delay = 0): ContextProcessor => ({
name,
process: vi.fn(async (context) => {
if (delay > 0) await new Promise((resolve) => setTimeout(resolve, delay));
return {
...context,
metadata: {
...context.metadata,
[name]: true,
},
};
}),
});
const createAbortingProcessor = (name: string, reason = 'test abort'): ContextProcessor => ({
name,
process: vi.fn(async (context) => ({
...context,
abortReason: reason,
isAborted: true,
})),
});
const createErrorProcessor = (name: string, error: Error): ContextProcessor => ({
name,
process: vi.fn(async () => {
throw error;
}),
});
const createInitialContext = (): {
messages: any[];
} => ({
messages: [{ content: 'test', role: 'user' }],
});
describe('constructor', () => {
it('should initialize with pipeline and options', () => {
const processor1 = createMockProcessor('processor1');
const processor2 = createMockProcessor('processor2');
const engine = new ContextEngine({
debug: true,
pipeline: [processor1, processor2],
});
expect(engine.getProcessors()).toHaveLength(2);
expect(engine.getProcessors()[0]).toBe(processor1);
expect(engine.getProcessors()[1]).toBe(processor2);
});
it('should initialize with empty pipeline', () => {
const engine = new ContextEngine({ pipeline: [] });
expect(engine.getProcessors()).toHaveLength(0);
});
});
describe('addProcessor', () => {
it('should add processor to pipeline', () => {
const engine = new ContextEngine({ pipeline: [] });
const processor = createMockProcessor('test');
engine.addProcessor(processor);
expect(engine.getProcessors()).toHaveLength(1);
expect(engine.getProcessors()[0]).toBe(processor);
});
it('should support chaining', () => {
const engine = new ContextEngine({ pipeline: [] });
const processor1 = createMockProcessor('p1');
const processor2 = createMockProcessor('p2');
const result = engine.addProcessor(processor1).addProcessor(processor2);
expect(result).toBe(engine);
expect(engine.getProcessors()).toHaveLength(2);
});
});
describe('removeProcessor', () => {
it('should remove processor by name', () => {
const processor1 = createMockProcessor('p1');
const processor2 = createMockProcessor('p2');
const processor3 = createMockProcessor('p3');
const engine = new ContextEngine({
pipeline: [processor1, processor2, processor3],
});
engine.removeProcessor('p2');
const processors = engine.getProcessors();
expect(processors).toHaveLength(2);
expect(processors[0].name).toBe('p1');
expect(processors[1].name).toBe('p3');
});
it('should support chaining', () => {
const processor1 = createMockProcessor('p1');
const processor2 = createMockProcessor('p2');
const engine = new ContextEngine({
pipeline: [processor1, processor2],
});
const result = engine.removeProcessor('p1').removeProcessor('p2');
expect(result).toBe(engine);
expect(engine.getProcessors()).toHaveLength(0);
});
it('should do nothing if processor not found', () => {
const processor = createMockProcessor('p1');
const engine = new ContextEngine({ pipeline: [processor] });
engine.removeProcessor('nonexistent');
expect(engine.getProcessors()).toHaveLength(1);
});
});
describe('getProcessors', () => {
it('should return copy of processor list', () => {
const processor = createMockProcessor('test');
const engine = new ContextEngine({ pipeline: [processor] });
const processors = engine.getProcessors();
processors.push(createMockProcessor('new'));
expect(engine.getProcessors()).toHaveLength(1);
});
});
describe('clear', () => {
it('should remove all processors', () => {
const engine = new ContextEngine({
pipeline: [createMockProcessor('p1'), createMockProcessor('p2')],
});
engine.clear();
expect(engine.getProcessors()).toHaveLength(0);
});
it('should support chaining', () => {
const engine = new ContextEngine({
pipeline: [createMockProcessor('p1')],
});
const result = engine.clear();
expect(result).toBe(engine);
});
});
describe('process', () => {
it('should execute processors in sequence', async () => {
const processor1 = createMockProcessor('p1');
const processor2 = createMockProcessor('p2');
const processor3 = createMockProcessor('p3');
const engine = new ContextEngine({
pipeline: [processor1, processor2, processor3],
});
const input = createInitialContext();
const result = await engine.process(input);
expect(result.isAborted).toBe(false);
expect(result.messages).toEqual(input.messages);
expect(result.metadata.p1).toBe(true);
expect(result.metadata.p2).toBe(true);
expect(result.metadata.p3).toBe(true);
expect(result.stats.processedCount).toBe(3);
});
it('should handle messages array correctly', async () => {
const processor = createMockProcessor('p1');
const engine = new ContextEngine({ pipeline: [processor] });
const input = createInitialContext();
const result = await engine.process(input);
expect(result.messages).toEqual(input.messages);
});
it('should handle empty messages', async () => {
const processor = createMockProcessor('p1');
const engine = new ContextEngine({ pipeline: [processor] });
const result = await engine.process({ messages: [] });
expect(result.messages).toEqual([]);
});
it('should include metadata in context', async () => {
const processor: ContextProcessor = {
name: 'test',
process: vi.fn(async (context) => {
expect(context.metadata).toBeDefined();
return context;
}),
};
const engine = new ContextEngine({ pipeline: [processor] });
await engine.process(createInitialContext());
});
it('should track execution stats', async () => {
const processor1 = createMockProcessor('p1', 10);
const processor2 = createMockProcessor('p2', 20);
const engine = new ContextEngine({
pipeline: [processor1, processor2],
});
const result = await engine.process(createInitialContext());
expect(result.stats.processedCount).toBe(2);
expect(result.stats.totalDuration).toBeGreaterThanOrEqual(20);
});
it('should stop processing when aborted', async () => {
const processor1 = createMockProcessor('p1');
const processor2 = createAbortingProcessor('p2', 'user requested');
const processor3 = createMockProcessor('p3');
const engine = new ContextEngine({
pipeline: [processor1, processor2, processor3],
});
const result = await engine.process(createInitialContext());
expect(result.isAborted).toBe(true);
expect(result.abortReason).toBe('user requested');
expect(result.stats.processedCount).toBe(2);
expect(processor1.process).toHaveBeenCalled();
expect(processor2.process).toHaveBeenCalled();
expect(processor3.process).not.toHaveBeenCalled();
});
it('should skip remaining processors if context is already aborted', async () => {
const processor1: ContextProcessor = {
name: 'p1',
process: vi.fn(async (context) => ({
...context,
isAborted: true,
})),
};
const processor2 = createMockProcessor('p2');
const engine = new ContextEngine({
pipeline: [processor1, processor2],
});
const result = await engine.process(createInitialContext());
expect(result.isAborted).toBe(true);
expect(result.stats.processedCount).toBe(1);
expect(processor2.process).not.toHaveBeenCalled();
});
it('should throw PipelineError when processor fails', async () => {
const error = new Error('processor failed');
const processor = createErrorProcessor('failing-processor', error);
const engine = new ContextEngine({ pipeline: [processor] });
await expect(engine.process(createInitialContext())).rejects.toThrow(PipelineError);
await expect(engine.process(createInitialContext())).rejects.toThrow(
'Processor [failing-processor] execution failed',
);
});
it('should include processor stats even when it fails', async () => {
const error = new Error('test error');
const processor = createErrorProcessor('failing', error);
const engine = new ContextEngine({ pipeline: [processor] });
try {
await engine.process(createInitialContext());
} catch (e) {
expect(e).toBeInstanceOf(PipelineError);
if (e instanceof PipelineError) {
expect(e.processorName).toBe('failing');
expect(e.originalError).toBe(error);
}
}
});
it('should handle non-Error objects thrown by processor', async () => {
const processor: ContextProcessor = {
name: 'thrower',
process: vi.fn(async () => {
throw 'string error';
}),
};
const engine = new ContextEngine({ pipeline: [processor] });
await expect(engine.process(createInitialContext())).rejects.toThrow(PipelineError);
});
it('should preserve initial state', async () => {
const testContext = createInitialContext();
const processor: ContextProcessor = {
name: 'test',
process: vi.fn(async (context) => {
expect(context.initialState.messages).toEqual(testContext.messages);
return context;
}),
};
const engine = new ContextEngine({ pipeline: [processor] });
await engine.process(testContext);
});
});
describe('getStats', () => {
it('should return processor count and names', () => {
const processor1 = createMockProcessor('p1');
const processor2 = createMockProcessor('p2');
const engine = new ContextEngine({
pipeline: [processor1, processor2],
});
const stats = engine.getStats();
expect(stats.processorCount).toBe(2);
expect(stats.processorNames).toEqual(['p1', 'p2']);
});
it('should return empty stats for empty pipeline', () => {
const engine = new ContextEngine({ pipeline: [] });
const stats = engine.getStats();
expect(stats.processorCount).toBe(0);
expect(stats.processorNames).toEqual([]);
});
});
describe('clone', () => {
it('should create independent copy of engine', () => {
const processor1 = createMockProcessor('p1');
const processor2 = createMockProcessor('p2');
const engine1 = new ContextEngine({
debug: true,
pipeline: [processor1, processor2],
});
const engine2 = engine1.clone();
expect(engine2).not.toBe(engine1);
expect(engine2.getProcessors()).toHaveLength(2);
expect(engine2.getProcessors()[0]).toBe(processor1);
expect(engine2.getProcessors()[1]).toBe(processor2);
// Modify cloned engine
engine2.addProcessor(createMockProcessor('p3'));
// Original should be unchanged
expect(engine1.getProcessors()).toHaveLength(2);
expect(engine2.getProcessors()).toHaveLength(3);
});
});
describe('validate', () => {
it('should return valid for correct pipeline', () => {
const processor1 = createMockProcessor('p1');
const processor2 = createMockProcessor('p2');
const engine = new ContextEngine({
pipeline: [processor1, processor2],
});
const result = engine.validate();
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should detect duplicate processor names', () => {
const processor1 = createMockProcessor('duplicate');
const processor2 = createMockProcessor('duplicate');
const engine = new ContextEngine({
pipeline: [processor1, processor2],
});
const result = engine.validate();
expect(result.valid).toBe(false);
expect(result.errors).toContain('Found duplicate processor names: duplicate');
});
it('should detect empty pipeline', () => {
const engine = new ContextEngine({ pipeline: [] });
const result = engine.validate();
expect(result.valid).toBe(false);
expect(result.errors).toContain('No processors in pipeline');
});
it('should detect missing processor name', () => {
const processor: ContextProcessor = {
name: '',
process: vi.fn(),
};
const engine = new ContextEngine({ pipeline: [processor] });
const result = engine.validate();
expect(result.valid).toBe(false);
expect(result.errors).toContain('Processor missing name');
});
it('should detect missing process method', () => {
const processor = {
name: 'test',
} as any;
const engine = new ContextEngine({ pipeline: [processor] });
const result = engine.validate();
expect(result.valid).toBe(false);
expect(result.errors).toContain('Processor [test] missing process method');
});
it('should detect multiple errors', () => {
const processor1 = createMockProcessor('duplicate');
const processor2 = createMockProcessor('duplicate');
const processor3: ContextProcessor = {
name: '',
process: vi.fn(),
};
const engine = new ContextEngine({
pipeline: [processor1, processor2, processor3],
});
const result = engine.validate();
expect(result.valid).toBe(false);
expect(result.errors.length).toBeGreaterThan(1);
});
});
});