/ clis / deepseek / utils.test.js
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  });