2630b0f690
- AdminAgcModel 新增 access 字段(缺省 basic,失败开放),GET 回读 PUT 写入 - api-server 保存时校验权限档只允许 basic/full - 后台 AGC 模型页新增「权限档」列,可按模型选择基础/高性能模型 - fake API 与页面用例覆盖权限档选择与保存
192 lines
5.7 KiB
TypeScript
192 lines
5.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
import {
|
|
cleanup,
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
} from '@testing-library/react';
|
|
import { afterEach, expect, test, vi } from 'vitest';
|
|
|
|
import { getAgcModelCatalog, saveAgcModelCatalog } from '../api/adminApiClient';
|
|
import { AdminAgcModelsPage } from './AdminAgcModelsPage';
|
|
|
|
vi.mock('../api/adminApiClient', () => ({
|
|
getAgcModelCatalog: vi.fn(),
|
|
saveAgcModelCatalog: vi.fn(),
|
|
isAdminApiError: vi.fn(() => false),
|
|
formatAdminApiError: vi.fn(() => '保存失败'),
|
|
}));
|
|
vi.mock('../components/useAdminWriteConfirm', () => ({
|
|
useAdminWriteConfirm: () => ({
|
|
confirmWrite: async () => true,
|
|
confirmDialog: null,
|
|
}),
|
|
}));
|
|
afterEach(() => {
|
|
cleanup();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
test('edits alias and upstream model without changing the stable identifier or revision', async () => {
|
|
const catalog = {
|
|
revision: 3,
|
|
defaultModelId: 'quality',
|
|
models: [
|
|
{
|
|
id: 'quality',
|
|
alias: '高质量',
|
|
modelId: 'gpt-6-astra',
|
|
enabled: true,
|
|
agentMode: 'codex' as const,
|
|
},
|
|
],
|
|
};
|
|
vi.mocked(getAgcModelCatalog).mockResolvedValue(catalog);
|
|
vi.mocked(saveAgcModelCatalog).mockImplementation(async (_, input) => ({
|
|
...input,
|
|
revision: 4,
|
|
}));
|
|
render(<AdminAgcModelsPage token="test" onUnauthorized={vi.fn()} />);
|
|
await screen.findByDisplayValue('gpt-6-astra');
|
|
fireEvent.change(screen.getByLabelText('模型 1 别名'), {
|
|
target: { value: '精细创作' },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
await waitFor(() =>
|
|
expect(saveAgcModelCatalog).toHaveBeenCalledWith('test', {
|
|
...catalog,
|
|
models: [{ ...catalog.models[0], alias: '精细创作' }],
|
|
}),
|
|
);
|
|
await waitFor(() => {
|
|
expect(screen.getAllByText('已保存').length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
});
|
|
|
|
test('new models default to Codex and save the selected Agent mode', async () => {
|
|
const catalog = {
|
|
revision: 3,
|
|
defaultModelId: 'quality',
|
|
models: [
|
|
{
|
|
id: 'quality',
|
|
alias: '高质量',
|
|
modelId: 'gpt-6-astra',
|
|
enabled: true,
|
|
agentMode: 'codex' as const,
|
|
},
|
|
],
|
|
};
|
|
vi.mocked(getAgcModelCatalog).mockResolvedValue(catalog);
|
|
vi.mocked(saveAgcModelCatalog).mockImplementation(async (_, input) => ({
|
|
...input,
|
|
revision: 4,
|
|
}));
|
|
render(<AdminAgcModelsPage token="test" onUnauthorized={vi.fn()} />);
|
|
await screen.findByDisplayValue('gpt-6-astra');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '添加模型' }));
|
|
const agentMode = screen.getByLabelText('模型 2 Agent 模式');
|
|
expect((agentMode as HTMLSelectElement).value).toBe('codex');
|
|
fireEvent.change(agentMode, { target: { value: 'cc' } });
|
|
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
|
|
await waitFor(() => {
|
|
const calls = vi.mocked(saveAgcModelCatalog).mock.calls;
|
|
const payload = calls[calls.length - 1]?.[1];
|
|
expect(payload?.models).toHaveLength(2);
|
|
expect(payload?.models[1]).toMatchObject({
|
|
alias: '',
|
|
modelId: '',
|
|
enabled: true,
|
|
agentMode: 'cc',
|
|
access: 'basic',
|
|
});
|
|
});
|
|
});
|
|
|
|
test('权限档可按模型单独选择并随目录保存', async () => {
|
|
const catalog = {
|
|
revision: 3,
|
|
defaultModelId: 'quality',
|
|
models: [
|
|
{
|
|
id: 'quality',
|
|
alias: '高质量',
|
|
modelId: 'gpt-6-astra',
|
|
enabled: true,
|
|
agentMode: 'codex' as const,
|
|
access: 'basic' as const,
|
|
},
|
|
],
|
|
};
|
|
vi.mocked(getAgcModelCatalog).mockResolvedValue(catalog);
|
|
vi.mocked(saveAgcModelCatalog).mockImplementation(async (_, input) => ({
|
|
...input,
|
|
revision: 4,
|
|
}));
|
|
render(<AdminAgcModelsPage token="test" onUnauthorized={vi.fn()} />);
|
|
await screen.findByDisplayValue('gpt-6-astra');
|
|
|
|
const access = screen.getByLabelText('模型 1 权限档') as HTMLSelectElement;
|
|
expect(access.value).toBe('basic');
|
|
fireEvent.change(access, { target: { value: 'full' } });
|
|
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
|
|
await waitFor(() => {
|
|
const calls = vi.mocked(saveAgcModelCatalog).mock.calls;
|
|
const payload = calls[calls.length - 1]?.[1];
|
|
expect(payload?.models[0]).toMatchObject({ access: 'full' });
|
|
});
|
|
});
|
|
|
|
test('协议与 Agent 模式是独立字段,可按模型单独选择', async () => {
|
|
const catalog = {
|
|
revision: 3,
|
|
defaultModelId: 'quality',
|
|
models: [
|
|
{
|
|
id: 'quality',
|
|
alias: '高质量',
|
|
modelId: 'claude-opus-5-5',
|
|
enabled: true,
|
|
agentMode: 'cc' as const,
|
|
protocol: 'anthropic' as const,
|
|
},
|
|
],
|
|
};
|
|
vi.mocked(getAgcModelCatalog).mockResolvedValue(catalog);
|
|
vi.mocked(saveAgcModelCatalog).mockImplementation(async (_, input) => ({
|
|
...input,
|
|
revision: 4,
|
|
}));
|
|
render(<AdminAgcModelsPage token="test" onUnauthorized={vi.fn()} />);
|
|
await screen.findByDisplayValue('claude-opus-5-5');
|
|
|
|
const agentMode = screen.getByLabelText(
|
|
'模型 1 Agent 模式',
|
|
) as HTMLSelectElement;
|
|
const protocol = screen.getByLabelText('模型 1 协议') as HTMLSelectElement;
|
|
expect(agentMode.value).toBe('cc');
|
|
expect(protocol.value).toBe('anthropic');
|
|
// 执行器下拉里不再混入协议取值。
|
|
expect(Array.from(agentMode.options).map((option) => option.value)).toEqual([
|
|
'codex',
|
|
'cc',
|
|
]);
|
|
|
|
fireEvent.change(agentMode, { target: { value: 'codex' } });
|
|
fireEvent.change(protocol, { target: { value: 'openai_chat' } });
|
|
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
|
|
await waitFor(() => {
|
|
const calls = vi.mocked(saveAgcModelCatalog).mock.calls;
|
|
const payload = calls[calls.length - 1]?.[1];
|
|
expect(payload?.models[0]).toMatchObject({
|
|
agentMode: 'codex',
|
|
protocol: 'openai_chat',
|
|
});
|
|
});
|
|
});
|