Leon4gr45/builder
0
1import { describe, it, expect, beforeEach, afterEach } from 'vitest';2import { TaskManager } from '../task-manager';3import { SSEEventBus } from '../sse-event-bus';4import { ServerConfigManager } from '../server-config-manager';5 6describe('Server-side generation integration', () => {7 let tm: TaskManager;8 let bus: SSEEventBus;9 10 beforeEach(() => {11 tm = new TaskManager({ maxConcurrentPerScope: 3, keyTTLMs: 30 * 60 * 1000 });12 bus = new SSEEventBus({ maxBufferSize: 500 });13 });14 15 afterEach(() => {16 tm.dispose();17 });18 19 it('full lifecycle: create task → emit events → complete → cleanup', () => {20 const taskId = tm.createTask('proj-1', 'sess-1', 'sk-key');21 expect(tm.getTask(taskId)?.status).toBe('running');22 expect(tm.getApiKey(taskId)).toBe('sk-key');23 24 const received: any[] = [];25 bus.addListener('sess-1', (e) => received.push(e));26 27 bus.emit(taskId, 'proj-1', 'assistant_delta', { text: 'Hello' }, 'sess-1');28 bus.emit(taskId, 'proj-1', 'tool_status', { status: 'running' }, 'sess-1');29 bus.emit(taskId, 'proj-1', 'task_complete', { result: 'success' }, 'sess-1');30 31 expect(received).toHaveLength(3);32 expect(bus.getBuffer(taskId)).toHaveLength(2); // delta not buffered33 34 tm.completeTask(taskId, 'completed');35 expect(tm.getTask(taskId)?.status).toBe('completed');36 expect(tm.getApiKey(taskId)).toBeUndefined();37 });38 39 it('SSE reconnect replays non-delta events', () => {40 const taskId = tm.createTask('proj-1', 'sess-1', 'sk-key');41 bus.emit(taskId, 'proj-1', 'assistant_delta', { text: 'a' }, 'sess-1');42 bus.emit(taskId, 'proj-1', 'tool_status', { status: 'started' }, 'sess-1');43 bus.emit(taskId, 'proj-1', 'conversation_message', { msg: 'hi' }, 'sess-1');44 45 const replayed = bus.replayFrom(taskId, 0);46 expect(replayed).toHaveLength(2);47 expect(replayed![0].event).toBe('tool_status');48 expect(replayed![1].event).toBe('conversation_message');49 });50 51 it('task limit enforcement across create and complete', () => {52 const t1 = tm.createTask('p1', 's1', 'k1', 'ws-1');53 tm.createTask('p2', 's1', 'k2', 'ws-1');54 tm.createTask('p3', 's1', 'k3', 'ws-1');55 56 expect(() => tm.createTask('p4', 's1', 'k4', 'ws-1')).toThrow();57 58 tm.completeTask(t1, 'completed');59 const t4 = tm.createTask('p4', 's1', 'k4', 'ws-1');60 expect(t4).toBeDefined();61 });62 63 it('fresh tab reconnect (lastEventId=0) replays full buffer via getBuffer', () => {64 const taskId = tm.createTask('proj-1', 'sess-1', 'sk-key');65 bus.emit(taskId, 'proj-1', 'conversation_message', {66 message: { role: 'user', content: 'hello', ui_metadata: { projectContext: 'files...' } },67 }, 'sess-1');68 bus.emit(taskId, 'proj-1', 'assistant_delta', { text: 'Hi' }, 'sess-1');69 bus.emit(taskId, 'proj-1', 'tool_status', { status: 'running', name: 'bash' }, 'sess-1');70 bus.emit(taskId, 'proj-1', 'conversation_message', {71 message: { role: 'assistant', content: 'Done' },72 }, 'sess-1');73 74 // Fresh tab: getBuffer returns all buffered (non-delta) events75 const buffer = bus.getBuffer(taskId);76 expect(buffer).toHaveLength(3); // user msg, tool_status, assistant msg (delta excluded)77 78 // replayFrom with 0 also returns full buffer79 const replayed = bus.replayFrom(taskId, 0);80 expect(replayed).toHaveLength(3);81 expect(replayed![0].event).toBe('conversation_message');82 expect((replayed![0].data as any).message.ui_metadata.projectContext).toBe('files...');83 });84 85 it('task metadata is available in getTasksForSession for shelf display', () => {86 const taskId = tm.createTask('proj-1', 'sess-1', 'sk-key');87 const task = tm.getTask(taskId)!;88 task.prompt = 'add a navbar';89 task.model = 'claude-3.5-sonnet';90 task.projectName = 'Portfolio';91 92 const tasks = tm.getTasksForSession('sess-1');93 expect(tasks).toHaveLength(1);94 expect(tasks[0].prompt).toBe('add a navbar');95 expect(tasks[0].model).toBe('claude-3.5-sonnet');96 expect(tasks[0].projectName).toBe('Portfolio');97 });98 99 it('ServerConfigManager tracks cost across multiple updates', () => {100 const config = new ServerConfigManager({101 provider: 'openai',102 model: 'gpt-4o',103 apiKey: 'sk-test',104 modelPricing: { 'gpt-4o': { prompt: 2.5, completion: 10 } },105 });106 107 config.updateSessionCost({ promptTokens: 1000, completionTokens: 500 }, 0.075);108 config.updateSessionCost({ promptTokens: 2000, completionTokens: 1000 }, 0.15);109 110 const session = config.getSessionCost();111 expect(session.totalCost).toBeCloseTo(0.225);112 expect(session.requestCount).toBe(2);113 expect(session.totalPromptTokens).toBe(3000);114 expect(session.totalCompletionTokens).toBe(1500);115 });116});117 