Files
Genarrative/apps/ai-game-creator-shell/tests/customLlmSettings.test.tsx
kdletters 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
AGC 新增本地自定义 LLM 配置与展示模型勾选
- 本地配置新增 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 模型选择规范、实施计划与共享概览
2026-09-16 16:39:15 +08:00

158 lines
5.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @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);
});