utils.test.js
1 import fs from 'node:fs'; 2 import os from 'node:os'; 3 import path from 'node:path'; 4 import { afterEach, describe, expect, it, vi } from 'vitest'; 5 import { selectModel, sendWithFile, parseThinkingResponse } from './utils.js'; 6 7 describe('deepseek parseThinkingResponse', () => { 8 it('returns plain response when no thinking header is present', () => { 9 const rawText = 'This is a regular response without thinking.'; 10 const result = parseThinkingResponse(rawText); 11 12 expect(result).toEqual({ 13 response: rawText, 14 thinking: null, 15 thinking_time: null, 16 }); 17 }); 18 19 it('parses English thinking header — all content after header is thinking', () => { 20 const rawText = 'Thought for 3.5 seconds\n\nLet me analyze this problem...\nFirst, I need to consider X.\nThen, Y.\n\nThe answer is 42.'; 21 const result = parseThinkingResponse(rawText); 22 23 // Text-level parser no longer splits on \n\n; everything after header is thinking. 24 // DOM-level extraction in waitForResponse() handles the actual separation. 25 expect(result).toEqual({ 26 response: '', 27 thinking: 'Let me analyze this problem...\nFirst, I need to consider X.\nThen, Y.\n\nThe answer is 42.', 28 thinking_time: '3.5', 29 }); 30 }); 31 32 it('parses Chinese thinking header — all content after header is thinking', () => { 33 const rawText = '已思考(用时 2.3 秒)\n\n让我分析这个问题...\n首先需要考虑X。\n然后是Y。\n\n答案是42。'; 34 const result = parseThinkingResponse(rawText); 35 36 expect(result).toEqual({ 37 response: '', 38 thinking: '让我分析这个问题...\n首先需要考虑X。\n然后是Y。\n\n答案是42。', 39 thinking_time: '2.3', 40 }); 41 }); 42 43 it('multi-paragraph thinking without final answer is not corrupted', () => { 44 const rawText = 'Thought for 1.2 seconds\n\nFirst paragraph.\n\nSecond paragraph.'; 45 const result = parseThinkingResponse(rawText); 46 47 // Both paragraphs must stay in thinking; response is empty. 48 expect(result).toEqual({ 49 response: '', 50 thinking: 'First paragraph.\n\nSecond paragraph.', 51 thinking_time: '1.2', 52 }); 53 }); 54 55 it('multi-paragraph final answer is not split by text parser', () => { 56 const rawText = 'Thought for 3 seconds\n\nreasoning\n\nAnswer para 1.\n\nAnswer para 2.'; 57 const result = parseThinkingResponse(rawText); 58 59 // Text parser treats everything as thinking; DOM handles separation. 60 expect(result).toEqual({ 61 response: '', 62 thinking: 'reasoning\n\nAnswer para 1.\n\nAnswer para 2.', 63 thinking_time: '3', 64 }); 65 }); 66 67 it('handles thinking without final response', () => { 68 const rawText = 'Thought for 1.2 seconds\n\nThinking process here...'; 69 const result = parseThinkingResponse(rawText); 70 71 expect(result).toEqual({ 72 response: '', 73 thinking: 'Thinking process here...', 74 thinking_time: '1.2', 75 }); 76 }); 77 78 it('returns null for empty input', () => { 79 const result = parseThinkingResponse(''); 80 expect(result).toBeNull(); 81 }); 82 83 it('returns null for null input', () => { 84 const result = parseThinkingResponse(null); 85 expect(result).toBeNull(); 86 }); 87 }); 88 89 90 describe('deepseek sendWithFile', () => { 91 const tempDirs = []; 92 93 afterEach(() => { 94 vi.restoreAllMocks(); 95 while (tempDirs.length) { 96 fs.rmSync(tempDirs.pop(), { recursive: true, force: true }); 97 } 98 }); 99 100 it('prefers page.setFileInput over base64-in-evaluate when supported', async () => { 101 const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-deepseek-')); 102 tempDirs.push(dir); 103 const filePath = path.join(dir, 'report.txt'); 104 fs.writeFileSync(filePath, 'hello'); 105 106 const page = { 107 setFileInput: vi.fn().mockResolvedValue(undefined), 108 wait: vi.fn().mockResolvedValue(undefined), 109 evaluate: vi.fn() 110 .mockResolvedValueOnce(undefined) 111 .mockResolvedValueOnce(true) 112 .mockResolvedValueOnce({ ok: true }), 113 }; 114 115 const result = await sendWithFile(page, filePath, 'summarize this'); 116 117 expect(result).toEqual({ ok: true }); 118 expect(page.setFileInput).toHaveBeenCalledWith([filePath], 'input[type="file"]'); 119 }); 120 }); 121 122 describe('deepseek selectModel', () => { 123 afterEach(() => { 124 vi.restoreAllMocks(); 125 delete global.document; 126 }); 127 128 it('fails expert selection when only one radio is present', async () => { 129 const instantRadio = { 130 getAttribute: vi.fn(() => 'true'), 131 click: vi.fn(), 132 }; 133 global.document = { 134 querySelectorAll: vi.fn(() => [instantRadio]), 135 }; 136 const page = { 137 evaluate: vi.fn(async (script) => eval(script)), 138 }; 139 140 const result = await selectModel(page, 'expert'); 141 142 expect(result).toEqual({ ok: false }); 143 expect(instantRadio.click).not.toHaveBeenCalled(); 144 }); 145 });