/** * Chat seam tests — verify request/response type contracts * * Tests that the TypeScript types match the Rust serde-serialized format. * These are pure type contract tests — no Tauri dependency needed. */ import { describe, it, expect } from 'vitest'; // --------------------------------------------------------------------------- // Rust side: StreamChatRequest (camelCase via serde rename_all) // --------------------------------------------------------------------------- interface StreamChatRequest { agentId: string; sessionId: string; message: string; thinkingEnabled?: boolean; reasoningEffort?: string; planMode?: boolean; subagentEnabled?: boolean; model?: string; } interface ChatRequest { agentId: string; message: string; thinkingEnabled?: boolean; reasoningEffort?: string; planMode?: boolean; subagentEnabled?: boolean; model?: string; } interface ChatResponse { content: string; inputTokens: number; outputTokens: number; } // --------------------------------------------------------------------------- // Rust side: StreamChatEvent (tagged union, tag = "type") // --------------------------------------------------------------------------- type StreamChatEvent = | { type: 'delta'; delta: string } | { type: 'thinkingDelta'; delta: string } | { type: 'toolStart'; name: string; input: unknown } | { type: 'toolEnd'; name: string; output: unknown } | { type: 'subtaskStatus'; taskId: string; description: string; status: string; detail?: string } | { type: 'iterationStart'; iteration: number; maxIterations: number } | { type: 'handStart'; name: string; params: unknown } | { type: 'handEnd'; name: string; result: unknown } | { type: 'complete'; inputTokens: number; outputTokens: number } | { type: 'error'; message: string }; describe('Chat Seam: request format contract', () => { it('StreamChatRequest has required camelCase fields', () => { const req: StreamChatRequest = { agentId: 'test-agent', sessionId: 'session-123', message: 'Hello', }; expect(req.agentId).toBe('test-agent'); expect(req.sessionId).toBe('session-123'); expect(req.message).toBe('Hello'); }); it('StreamChatRequest optional fields are camelCase', () => { const req: StreamChatRequest = { agentId: 'a', sessionId: 's', message: 'm', thinkingEnabled: true, reasoningEffort: 'high', planMode: false, subagentEnabled: true, model: 'gpt-4o', }; expect(req.thinkingEnabled).toBe(true); expect(req.reasoningEffort).toBe('high'); expect(req.planMode).toBe(false); expect(req.subagentEnabled).toBe(true); expect(req.model).toBe('gpt-4o'); }); it('ChatRequest format for non-streaming', () => { const req: ChatRequest = { agentId: 'test-agent', message: 'Hello', model: 'gpt-4o', }; expect(req.agentId).toBe('test-agent'); expect(req.message).toBe('Hello'); }); it('ChatResponse has expected fields', () => { const resp: ChatResponse = { content: 'Hello back!', inputTokens: 10, outputTokens: 5, }; expect(resp.content).toBe('Hello back!'); expect(resp.inputTokens).toBe(10); expect(resp.outputTokens).toBe(5); }); }); describe('Chat Seam: StreamChatEvent format contract', () => { it('delta event matches Rust StreamChatEvent::Delta', () => { const event: StreamChatEvent = { type: 'delta', delta: 'Hello' }; expect(event.type).toBe('delta'); if (event.type === 'delta') { expect(typeof event.delta).toBe('string'); } }); it('complete event has token counts', () => { const event: StreamChatEvent = { type: 'complete', inputTokens: 10, outputTokens: 5 }; if (event.type === 'complete') { expect(event.inputTokens).toBeGreaterThanOrEqual(0); expect(event.outputTokens).toBeGreaterThanOrEqual(0); } }); it('handStart/handEnd events have correct structure', () => { const start: StreamChatEvent = { type: 'handStart', name: 'hand_quiz', params: { topic: 'math' } }; const end: StreamChatEvent = { type: 'handEnd', name: 'hand_quiz', result: { questions: [] } }; if (start.type === 'handStart') { expect(start.name).toMatch(/^hand_/); expect(start.params).toBeDefined(); } if (end.type === 'handEnd') { expect(end.name).toMatch(/^hand_/); expect(end.result).toBeDefined(); } }); it('error event has message field', () => { const event: StreamChatEvent = { type: 'error', message: '已取消' }; if (event.type === 'error') { expect(event.message).toBeTruthy(); } }); });