// @vitest-environment node import { afterEach, describe, expect, it, vi } from 'vitest'; import * as cloud from '../../electron/coding-teacher/config-client'; import * as transport from '../../electron/utils/proxy-fetch'; import { prepareTeacherModel } from '../../electron/coding-teacher/model-runner'; import type { TeacherDefinition } from '../../shared/coding-teacher'; const account: cloud.TeacherAccount = { id: '11111111-1111-4111-8111-111111111111', binding: { accountKey: 'teacher-test', epoch: 1 }, }; // JSON round trip reproduces the cloud/durable-topic boundary, including Pydantic nulls. function definition(choice: unknown): TeacherDefinition { return JSON.parse(JSON.stringify({ schema_version: 1, teacher_id: 'coding-teacher', name: 'Teacher', description: '', avatar_id: 'avatar-01', welcome_message: '', suggested_questions: [], system_prompt: 'Explain code.', skills: [], model: { model_id: 'deepseek-flash', reasoning_choice: choice }, limits: { max_input_tokens: 8000, max_output_tokens: 1500 }, })); } function setup(canDisable = true) { vi.spyOn(cloud, 'teacherCloudRequest').mockResolvedValue({ api_key: 'synthetic-key', base_url: 'https://teacher-model.invalid/v1', models: ['deepseek-flash'], model_capabilities_v2: { schema_version: 2, models: { 'deepseek-flash': { input_modalities: ['text', 'image'], output_modalities: ['text'], reasoning: { supported: true, can_disable: canDisable, default_enabled: true, effort_values: ['low', 'high', 'max'], default_effort: 'high', control_format: 'deepseek', }, }, }, }, }); vi.spyOn(cloud, 'assertTeacherAccount').mockReturnValue(undefined); return vi.spyOn(transport, 'proxyAwareFetch').mockResolvedValue(new Response( 'data: {"choices":[{"delta":{"content":"Explanation"},"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n', { headers: { 'content-type': 'text/event-stream' } }, )); } afterEach(() => vi.restoreAllMocks()); describe('teacher published reasoning wire contract', () => { it.each([ [{ mode: 'enabled', effort: null }, { thinking: { type: 'enabled' } }], [{ mode: 'enabled' }, { thinking: { type: 'enabled' } }], [{ mode: 'disabled', effort: null }, { thinking: { type: 'disabled' } }], [{ mode: 'default', effort: null }, {}], [{ mode: 'enabled', effort: 'high' }, { thinking: { type: 'enabled' }, reasoning_effort: 'high' }], ])('prepares saved choice %j and sends its native controls', async (choice, fields) => { const fetch = setup(); const prepared = await prepareTeacherModel(account, definition(choice)); const onText = vi.fn(); await prepared.run([{ role: 'user', content: 'Explain this.' }], new AbortController().signal, onText); expect(onText).toHaveBeenCalledWith('Explanation'); const body = JSON.parse(String(fetch.mock.calls[0][1]?.body)); expect({ ...(body.thinking === undefined ? {} : { thinking: body.thinking }), ...(body.reasoning_effort === undefined ? {} : { reasoning_effort: body.reasoning_effort }), }).toEqual(fields); }); it.each([ [{ mode: 'enabled', effort: 'unsupported' }, true], [{ mode: 'disabled', effort: null }, false], ])('keeps rejecting unsupported choice %j', async (choice, canDisable) => { const fetch = setup(Boolean(canDisable)); await expect(prepareTeacherModel(account, definition(choice))) .rejects.toThrow('老师所用思考选项已不可用,请联系运营调整。'); expect(fetch).not.toHaveBeenCalled(); }); });