Files
makelore/tests/unit/coding-teacher-model-catalog.test.ts

41 lines
2.8 KiB
TypeScript

// @vitest-environment node
import { beforeEach, expect, it, vi } from 'vitest';
import { teacherModelCatalog, validateTeacherModelId } from '../../electron/coding-teacher/model-catalog';
import { TeacherError } from '../../electron/coding-teacher/config-client';
const mocks = vi.hoisted(() => ({ json: vi.fn(), request: vi.fn(), assert: vi.fn() }));
vi.mock('../../electron/coding-teacher/cloud-runner', () => ({ teacherCloudTransport: () => ({ json: mocks.json }) }));
vi.mock('../../electron/coding-teacher/config-client', async importOriginal => ({
...await importOriginal<typeof import('../../electron/coding-teacher/config-client')>(),
teacherCloudRequest: mocks.request, assertTeacherAccount: mocks.assert,
}));
const account = { id: 'student', binding: { accountKey: 'student', epoch: 1 } };
beforeEach(() => { vi.resetAllMocks(); });
it.each([false, undefined])('does not advertise selection on an older cloud or gateway (%s)', async flag => {
if (flag === undefined) mocks.json.mockRejectedValue(new TeacherError(404, 'not_found', 'Not Found'));
else mocks.json.mockResolvedValue({ model_selection: flag, models: [] });
expect(await teacherModelCatalog(account, 'yuxi')).toMatchObject({ supported: false, models: [] });
expect(mocks.request).not.toHaveBeenCalled();
});
it('does not turn authentication or transient failures into unsupported capability', async () => {
const error = new TeacherError(401, 'expired', '请登录');
mocks.json.mockRejectedValue(error);
await expect(teacherModelCatalog(account, 'yuxi')).rejects.toBe(error);
});
it.each([null, '', ' model ', 123, 'x'.repeat(201)])('rejects malformed explicit model %s', value => {
expect(() => validateTeacherModelId(value)).toThrow(TeacherError);
});
it('preserves absent model and exact provider ids', () => {
expect(validateTeacherModelId(undefined)).toBeUndefined();
expect(validateTeacherModelId('provider/model')).toBe('provider/model');
});
it('returns only shared text models and never exposes gateway credentials', async () => {
mocks.json.mockResolvedValue({ model_selection: true, models: [{ id: 'text-model' }, { id: 'image-model' }, { id: 'unlisted' }] });
mocks.request.mockResolvedValue({ api_key: 'private', base_url: 'https://private.invalid',
models: ['text-model', 'image-model'], model_capabilities_v2: { schema_version: 2, models: {
'text-model': { input_modalities: ['text'], output_modalities: ['text'], reasoning: { supported: false, can_disable: false, default_enabled: false, effort_values: [], control_format: 'none' } },
'image-model': { input_modalities: ['text'], output_modalities: ['image'], reasoning: { supported: false, can_disable: false, default_enabled: false, effort_values: [], control_format: 'none' } },
} } });
expect(await teacherModelCatalog(account, 'yuxi')).toEqual({ supported: true, models: [{ id: 'text-model' }] });
});