79 lines
3.5 KiB
TypeScript
79 lines
3.5 KiB
TypeScript
// @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();
|
|
});
|
|
}); |