c36a5170f8
Project CI / AI game creator shell Rust shard 1/4 (push) Successful in 5m28s
Project CI / AI game creator shell Rust shard 3/4 (push) Successful in 5m29s
Project CI / AI game creator shell Rust shard 4/4 (push) Successful in 6m6s
Project CI / AI game creator shell Rust shard 2/4 (push) Successful in 6m6s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m46s
Project CI / AI game creator shell Rust crates (push) Successful in 2m51s
Project CI / Frontend tests (push) Successful in 4m44s
Project CI / Native shell tests (push) Successful in 6m31s
Project CI / Repository checks (push) Successful in 4m17s
Project CI / Backend tests (push) Successful in 7m48s
Project CI / AI game creator shell web tests (push) Successful in 3m43s
- 本地配置新增 llm.customEnabled 与 llm.visibleModels,显式开启后保留自定义连接和勾选列表,官方路由下仍清空凭据 - 配置迁移始终写出 customEnabled、visibleModels、apiKey、baseUrl、model、apiKind、reasoningEffort,便于手写自定义连接 - 新增 discover_game_creator_llm_models 命令,直连自定义端点 GET /models,限制超时与响应大小并脱敏错误 - codex_app_server 自定义模式改用配置里的地址与 Key 走本地凭据代理,不再经过平台 /api/llm,也不回退官方路由 - 模型目录按官方与自定义来源隔离缓存,自定义模式只展示勾选模型,选项失效时回退第一项 - 设置页在自定义模式展示 API 地址、API Key、协议与推理档,并提供模型读取、搜索、勾选与已勾选预览 - 补齐固定项排版与读取按钮样式,同步 AGC 模型选择规范、实施计划与共享概览
158 lines
5.4 KiB
TypeScript
158 lines
5.4 KiB
TypeScript
// @vitest-environment jsdom
|
||
import {
|
||
act,
|
||
cleanup,
|
||
fireEvent,
|
||
render,
|
||
screen,
|
||
waitFor,
|
||
within,
|
||
} from '@testing-library/react';
|
||
import { useState } from 'react';
|
||
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
||
|
||
import defaults from '../game-creator.config.json';
|
||
import type {
|
||
GameCreatorAppConfig,
|
||
GameCreatorLlmConfig,
|
||
} from '../src/app/types';
|
||
import { CustomLlmSettings } from '../src/features/runtime-config/CustomLlmSettings';
|
||
import { RuntimeConfigDialog } from '../src/features/runtime-config/RuntimeConfigDialog';
|
||
|
||
const invoke = vi.fn();
|
||
const llm = {
|
||
...defaults.llm,
|
||
customEnabled: true,
|
||
baseUrl: 'https://models.example/v1',
|
||
apiKey: 'test-custom-key',
|
||
visibleModels: ['vendor/model.v1'],
|
||
} as GameCreatorLlmConfig;
|
||
function Harness() {
|
||
const [draft, setDraft] = useState(llm);
|
||
return <CustomLlmSettings llm={draft} disabled={false} onChange={setDraft} />;
|
||
}
|
||
|
||
beforeEach(() => {
|
||
invoke.mockReset();
|
||
window.__TAURI__ = { core: { invoke } };
|
||
});
|
||
afterEach(() => {
|
||
cleanup();
|
||
delete window.__TAURI__;
|
||
});
|
||
|
||
test('reads endpoint models, filters candidates, and previews only checked models', async () => {
|
||
invoke.mockResolvedValue([
|
||
'vendor/model.v1',
|
||
'vendor/fast:latest',
|
||
'hidden-model',
|
||
]);
|
||
render(<Harness />);
|
||
fireEvent.click(screen.getByRole('button', { name: '读取模型列表' }));
|
||
await screen.findByText('已读取 3 个模型');
|
||
expect(invoke).toHaveBeenCalledWith('discover_game_creator_llm_models', {
|
||
llm,
|
||
});
|
||
fireEvent.click(screen.getByRole('checkbox', { name: 'vendor/fast:latest' }));
|
||
const preview = screen.getByRole('region', { name: '已勾选模型预览' });
|
||
expect(
|
||
within(preview)
|
||
.getAllByRole('listitem')
|
||
.map((item) => item.textContent),
|
||
).toEqual(['vendor/model.v1 默认', 'vendor/fast:latest']);
|
||
expect(within(preview).queryByText('hidden-model')).toBeNull();
|
||
fireEvent.change(screen.getByRole('textbox', { name: '搜索模型' }), {
|
||
target: { value: 'FAST' },
|
||
});
|
||
expect(screen.getAllByRole('checkbox')).toHaveLength(1);
|
||
fireEvent.click(screen.getByRole('checkbox', { name: 'vendor/fast:latest' }));
|
||
expect(within(preview).getAllByRole('listitem')).toHaveLength(1);
|
||
});
|
||
|
||
test('failed discovery preserves checked models and allows retry', async () => {
|
||
invoke
|
||
.mockRejectedValueOnce('模型列表读取失败(HTTP 401)')
|
||
.mockResolvedValueOnce(['vendor/model.v1']);
|
||
render(<Harness />);
|
||
fireEvent.click(screen.getByRole('button', { name: '读取模型列表' }));
|
||
await screen.findByRole('alert');
|
||
expect(
|
||
within(screen.getByRole('region', { name: '已勾选模型预览' })).getByText(
|
||
/vendor\/model.v1/,
|
||
),
|
||
).not.toBeNull();
|
||
fireEvent.click(screen.getByRole('button', { name: '读取模型列表' }));
|
||
await screen.findByText('已读取 1 个模型');
|
||
expect(screen.queryByRole('alert')).toBeNull();
|
||
});
|
||
|
||
test('changing endpoint discards an old in-flight result and clears old selections', async () => {
|
||
let finish!: (models: string[]) => void;
|
||
invoke.mockImplementation(
|
||
() =>
|
||
new Promise((resolve) => {
|
||
finish = resolve;
|
||
}),
|
||
);
|
||
render(<Harness />);
|
||
fireEvent.click(screen.getByRole('button', { name: '读取模型列表' }));
|
||
fireEvent.change(screen.getByLabelText('自定义 LLM API 地址'), {
|
||
target: { value: 'https://new.example/v1' },
|
||
});
|
||
await act(async () => finish(['old-model']));
|
||
expect(screen.queryByRole('checkbox', { name: 'old-model' })).toBeNull();
|
||
expect(screen.getByText('尚未勾选模型')).not.toBeNull();
|
||
expect(screen.getByRole('button', { name: '读取模型列表' })).toHaveProperty(
|
||
'disabled',
|
||
false,
|
||
);
|
||
});
|
||
|
||
test('settings save retains custom credentials and checked models, and reopening restores them', async () => {
|
||
let config = {
|
||
...defaults,
|
||
llm,
|
||
agentLlm: {},
|
||
editorApi: { baseUrl: 'https://platform.example', apiKey: '' },
|
||
} as GameCreatorAppConfig;
|
||
invoke.mockImplementation(async (command, args) => {
|
||
if (command === 'write_game_creator_app_config') config = args.config;
|
||
if (
|
||
command === 'read_game_creator_app_config' ||
|
||
command === 'write_game_creator_app_config'
|
||
)
|
||
return { path: '/private/game-creator.config.json', config };
|
||
if (command === 'discover_game_creator_llm_models')
|
||
return ['vendor/model.v1', 'vendor/fast:latest'];
|
||
return [];
|
||
});
|
||
const first = render(<RuntimeConfigDialog onClose={() => {}} />);
|
||
await screen.findByLabelText('自定义 LLM API 地址');
|
||
expect(screen.getByLabelText('自定义 LLM API Key')).toHaveProperty(
|
||
'type',
|
||
'password',
|
||
);
|
||
expect(screen.getByText('OpenAI Responses')).not.toBeNull();
|
||
expect(screen.getByText('最高')).not.toBeNull();
|
||
fireEvent.click(screen.getByRole('button', { name: '读取模型列表' }));
|
||
await screen.findByText('已读取 2 个模型');
|
||
fireEvent.click(screen.getByRole('checkbox', { name: 'vendor/fast:latest' }));
|
||
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
||
await waitFor(() =>
|
||
expect(config.llm.visibleModels).toEqual([
|
||
'vendor/model.v1',
|
||
'vendor/fast:latest',
|
||
]),
|
||
);
|
||
expect(config.llm.apiKey).toBe('test-custom-key');
|
||
expect(config.llm.customEnabled).toBe(true);
|
||
first.unmount();
|
||
render(<RuntimeConfigDialog onClose={() => {}} />);
|
||
await screen.findByLabelText('自定义 LLM API 地址');
|
||
expect(
|
||
within(screen.getByRole('region', { name: '已勾选模型预览' })).getAllByRole(
|
||
'listitem',
|
||
),
|
||
).toHaveLength(2);
|
||
});
|