ebb288a6a4
Project CI / AI game creator shell Rust smoke (push) Successful in 1m28s
Project CI / AI game creator shell Rust crates (push) Successful in 59s
Project CI / Backend tests (push) Successful in 4m50s
Project CI / Native shell tests (push) Successful in 5m43s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 7m23s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 8m55s
Project CI / Frontend tests (push) Successful in 2m8s
Project CI / AI game creator shell web tests (push) Failing after 1m52s
Project CI / Repository checks (push) Successful in 2m29s
dev 渠道显示为陶泥儿开发版并保持既有 identifier 与升级链。 将产品名注入 Tauri、原生窗口、前端标题栏和关于页面,统一快捷方式与注册表展示来源。 补充渠道配置门禁、发布脚本测试和渠道身份文档。
835 lines
28 KiB
TypeScript
835 lines
28 KiB
TypeScript
import fs from 'node:fs';
|
|
|
|
import { APP_NAME, APP_VERSION } from '../../src/app/appMetadata';
|
|
import { repoPath } from '../repoPath';
|
|
import {
|
|
act,
|
|
createGameCreationAppManifest,
|
|
createGameCreationAppSeedTasks,
|
|
deriveAgentStatusCards,
|
|
expect,
|
|
fireEvent,
|
|
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
|
|
type GameCreationAgentRunTrace,
|
|
it,
|
|
renderLauncherAt,
|
|
screen,
|
|
vi,
|
|
waitFor,
|
|
within,
|
|
} from './harness';
|
|
|
|
export function registerAgentStatusDerivationTests() {
|
|
it('keeps fixed overlays below the in-page window title bar', () => {
|
|
const styles = fs.readFileSync(
|
|
repoPath('apps/ai-game-creator-shell/src/styles.css'),
|
|
'utf8',
|
|
);
|
|
expect(styles).toContain('--window-chrome-height: 50px;');
|
|
expect(styles).toContain('.fixed.inset-0 {');
|
|
expect(styles).toContain('top: var(--window-chrome-height);');
|
|
expect(styles).toContain('.settings-overlay {');
|
|
expect(styles).toContain('.launcher-dialog-backdrop {');
|
|
expect(styles).toContain('.game-approval-backdrop {');
|
|
expect(styles).toContain('> .launcher-shell .launcher-main {');
|
|
expect(styles).toContain('padding-bottom: 0;');
|
|
expect(styles).toContain('> .platform-theme {');
|
|
});
|
|
|
|
it('derives agent card status from the latest run trace step', () => {
|
|
const manifest = createGameCreationAppManifest(
|
|
'local-project-draft',
|
|
'未命名游戏原型',
|
|
);
|
|
const taskGraphTasks = createGameCreationAppSeedTasks().map((task) =>
|
|
task.id === 'audio-director'
|
|
? { ...task, status: 'waiting-for-confirmation' as const }
|
|
: task,
|
|
);
|
|
const trace: GameCreationAgentRunTrace = {
|
|
schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
|
|
runId: 'run-agent-status-cards',
|
|
commandId: 'game.generate_draft',
|
|
status: 'running',
|
|
lifecycleStatus: 'pending',
|
|
passes: 1,
|
|
maxPasses: 3,
|
|
toolCallCount: 3,
|
|
maxToolCalls: 128,
|
|
stopReason: 'running',
|
|
goal: '做一个厨房弹幕游戏',
|
|
coordination: 'Planner -> Orchestrator',
|
|
steps: [
|
|
{
|
|
pass: 1,
|
|
agent: 'Planner',
|
|
phase: 'plan',
|
|
taskId: 'design-director',
|
|
group: 'design',
|
|
role: 'Director',
|
|
status: 'running',
|
|
inputPaths: ['memory/session.md'],
|
|
outputPaths: ['.agent/spec.md'],
|
|
summary: '正在拆解创作方向',
|
|
toolCalls: [
|
|
{
|
|
toolId: 'llm.planner',
|
|
status: 'ok',
|
|
inputPaths: ['memory/session.md'],
|
|
outputPaths: ['.agent/spec.md'],
|
|
summary: 'Planner 已读取短期记忆',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
pass: 1,
|
|
agent: 'Asset',
|
|
phase: 'role',
|
|
taskId: null,
|
|
group: 'art',
|
|
role: 'Asset',
|
|
status: 'failed',
|
|
inputPaths: [],
|
|
outputPaths: [],
|
|
summary: '美术资产生成失败',
|
|
toolCalls: [],
|
|
},
|
|
{
|
|
pass: 1,
|
|
agent: 'Generator',
|
|
phase: 'write',
|
|
taskId: 'code-prototype',
|
|
group: 'code',
|
|
role: 'Code',
|
|
status: 'passed',
|
|
inputPaths: [],
|
|
outputPaths: [],
|
|
summary: '代码已通过生成',
|
|
toolCalls: [],
|
|
},
|
|
{
|
|
pass: 1,
|
|
agent: 'Evaluator',
|
|
phase: 'evaluation',
|
|
status: 'passed',
|
|
inputPaths: ['.agent/passes/pass-1/draft.json', '.agent/findings.md'],
|
|
outputPaths: ['.agent/findings.md'],
|
|
summary: '质量评审通过',
|
|
toolCalls: [
|
|
{
|
|
toolId: 'agent.evaluate',
|
|
status: 'ok',
|
|
summary: 'Evaluator 质量评审',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
taskGraph: {
|
|
goal: '做一个厨房弹幕游戏',
|
|
readyTaskIds: ['audio-director'],
|
|
activeTaskIds: ['design-director'],
|
|
carriedTaskIds: ['balance-director'],
|
|
repairFocus: [],
|
|
repairRoutes: [],
|
|
tasks: taskGraphTasks,
|
|
},
|
|
passPlans: [],
|
|
nextStep: 'continue',
|
|
error: null,
|
|
updatedAt: 1,
|
|
};
|
|
|
|
const runtimeTask = {
|
|
schemaVersion: 'game-creator-agent-runtime-task.v1',
|
|
agentId: 'art-asset-plan',
|
|
taskId: 'art-asset-plan',
|
|
sessionId: 'agent-session-art-asset-plan',
|
|
runId: 'runtime-art-asset-plan-1',
|
|
source: 'agent-background-task',
|
|
task: '补齐主角规范图资产列表',
|
|
status: 'pending',
|
|
phase: 'queued',
|
|
currentAction: '等待当前任务完成',
|
|
error: null,
|
|
updatedAt: 1234,
|
|
};
|
|
const runtimeTaskQueue = {
|
|
total: 1,
|
|
pending: 1,
|
|
running: 0,
|
|
completed: 0,
|
|
failed: 0,
|
|
latestRunId: 'runtime-art-asset-plan-1',
|
|
updatedAt: 1234,
|
|
};
|
|
const cards = deriveAgentStatusCards(manifest, trace, {
|
|
'art-asset-plan': {
|
|
schemaVersion: 'game-creator-agent-runtime.v1',
|
|
agentId: 'art-asset-plan',
|
|
taskId: 'art-asset-plan',
|
|
sessionId: 'agent-session-art-asset-plan',
|
|
runId: 'runtime-art-asset-plan-1',
|
|
source: 'agent-background-task',
|
|
status: 'running',
|
|
phase: 'planning',
|
|
currentTask: '补齐主角规范图资产列表',
|
|
currentAction: '拆解素材规格',
|
|
loopIteration: 2,
|
|
maxLoopIterations: 3,
|
|
toolActionBudget: 3,
|
|
plan: ['读取项目上下文'],
|
|
planSteps: [
|
|
{
|
|
index: 0,
|
|
title: '读取项目上下文',
|
|
status: 'active',
|
|
detail: '拆解素材规格',
|
|
updatedAt: 1235,
|
|
},
|
|
],
|
|
activePlanStepIndex: 0,
|
|
observations: ['已创建本轮 Agent Runtime run。'],
|
|
allowedTools: ['conversation.read'],
|
|
lastResponse: null,
|
|
error: null,
|
|
updatedAt: 1234,
|
|
taskQueue: runtimeTaskQueue,
|
|
recentTasks: [runtimeTask],
|
|
},
|
|
});
|
|
|
|
expect(cards.find((card) => card.id === 'design-director')).toMatchObject({
|
|
taskId: 'design-director',
|
|
status: 'running',
|
|
summary: '正在拆解创作方向',
|
|
pass: 1,
|
|
phase: 'plan',
|
|
lifecycleStatus: 'pending',
|
|
hasRecentEvidence: true,
|
|
taskGraphState: 'active',
|
|
inputPaths: ['memory/session.md'],
|
|
outputPaths: ['.agent/spec.md'],
|
|
toolCalls: [
|
|
expect.objectContaining({
|
|
toolId: 'llm.planner',
|
|
status: 'ok',
|
|
summary: 'Planner 已读取短期记忆',
|
|
}),
|
|
],
|
|
});
|
|
expect(cards.find((card) => card.id === 'art-asset-plan')).toMatchObject({
|
|
taskId: 'art-asset-plan',
|
|
status: 'failed',
|
|
summary: '美术资产生成失败',
|
|
hasRecentEvidence: true,
|
|
runtimeStatus: 'running',
|
|
runtimePhase: 'planning',
|
|
runtimeAction: '拆解素材规格',
|
|
runtimeTask: '补齐主角规范图资产列表',
|
|
runtimeRunId: 'runtime-art-asset-plan-1',
|
|
runtimeLoopIteration: 2,
|
|
runtimeMaxLoopIterations: 3,
|
|
runtimeToolActionBudget: 3,
|
|
runtimeActivePlanStep: '#1 active · 读取项目上下文 · 拆解素材规格',
|
|
runtimeTaskQueue,
|
|
runtimeRecentTasks: [runtimeTask],
|
|
});
|
|
expect(cards.find((card) => card.id === 'code-prototype')).toMatchObject({
|
|
taskId: 'code-prototype',
|
|
status: 'completed',
|
|
summary: '代码已通过生成',
|
|
});
|
|
expect(cards.find((card) => card.id === 'audio-director')).toMatchObject({
|
|
taskId: 'audio-director',
|
|
status: 'waiting-for-confirmation',
|
|
taskGraphState: 'ready',
|
|
hasRecentEvidence: false,
|
|
});
|
|
expect(cards.find((card) => card.id === 'balance-director')).toMatchObject({
|
|
taskId: 'balance-director',
|
|
taskGraphState: 'carried',
|
|
});
|
|
});
|
|
}
|
|
|
|
export function registerRuntimeSettingsTests() {
|
|
it('distinguishes loading the client extension list from an empty list', async () => {
|
|
let resolveExtensions!: (items: unknown[]) => void;
|
|
const extensionsRead = new Promise<unknown[]>((resolve) => {
|
|
resolveExtensions = resolve;
|
|
});
|
|
const invoke = vi.fn((command: string) => {
|
|
if (command === 'read_game_creator_app_config') {
|
|
return Promise.resolve({
|
|
path: '/home/test/AppData/game-creator.config.json',
|
|
config: {
|
|
agentMode: 'codex_app_server',
|
|
llm: {
|
|
apiKey: '',
|
|
baseUrl: 'https://llm.example.test/v1',
|
|
model: 'gpt-settings-extensions',
|
|
apiKind: 'openai_responses',
|
|
reasoningEffort: 'high',
|
|
stream: false,
|
|
webSearchEnabled: false,
|
|
requestTimeoutMs: 180000,
|
|
maxRetries: 0,
|
|
retryBackoffMs: 500,
|
|
},
|
|
editorApi: {
|
|
baseUrl: 'http://127.0.0.1:8082',
|
|
apiKey: '',
|
|
},
|
|
},
|
|
});
|
|
}
|
|
if (command === 'list_client_extensions') {
|
|
return extensionsRead;
|
|
}
|
|
throw new Error(`unexpected invoke ${command}`);
|
|
});
|
|
window.__TAURI__ = { core: { invoke } };
|
|
renderLauncherAt('/?launcher');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
|
await screen.findByRole('dialog', { name: '运行时配置' });
|
|
fireEvent.click(screen.getByRole('button', { name: /^扩展/ }));
|
|
|
|
expect(await screen.findByText('正在加载扩展')).not.toBeNull();
|
|
expect(screen.queryByText('还没有导入扩展')).toBeNull();
|
|
|
|
await act(async () => {
|
|
resolveExtensions([]);
|
|
});
|
|
|
|
expect(await screen.findByText('还没有导入扩展')).not.toBeNull();
|
|
expect(screen.queryByText('正在加载扩展')).toBeNull();
|
|
});
|
|
|
|
it('shows the client version on the About settings page', async () => {
|
|
const invoke = vi.fn(async (command: string) => {
|
|
if (command === 'read_game_creator_app_config') {
|
|
return {
|
|
path: '/home/test/AppData/game-creator.config.json',
|
|
config: {
|
|
agentMode: 'codex_app_server',
|
|
llm: {
|
|
apiKey: '',
|
|
baseUrl: 'https://llm.example.test/v1',
|
|
model: 'gpt-about',
|
|
apiKind: 'openai_responses',
|
|
reasoningEffort: 'high',
|
|
stream: true,
|
|
webSearchEnabled: false,
|
|
contextWindowTokens: 128000,
|
|
autoCompactTokenLimit: 64000,
|
|
toolOutputTokenLimit: 12000,
|
|
requestTimeoutMs: 180000,
|
|
maxRetries: 2,
|
|
retryBackoffMs: 500,
|
|
},
|
|
agentLlm: {},
|
|
editorApi: {
|
|
baseUrl: 'http://127.0.0.1:8082',
|
|
apiKey: '',
|
|
},
|
|
},
|
|
};
|
|
}
|
|
throw new Error(`unexpected invoke ${command}`);
|
|
});
|
|
window.__TAURI__ = { core: { invoke } };
|
|
renderLauncherAt('/?launcher');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
|
expect(
|
|
await screen.findByRole('dialog', { name: '运行时配置' }),
|
|
).not.toBeNull();
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: /关于/ }));
|
|
|
|
expect(screen.getByText(APP_NAME)).not.toBeNull();
|
|
expect(
|
|
screen.getByTestId('runtime-settings-app-logo').getAttribute('src'),
|
|
).toContain('taonier-product-ip.png');
|
|
expect(screen.getByTestId('runtime-settings-app-version').textContent).toBe(
|
|
`v${APP_VERSION}`,
|
|
);
|
|
expect(screen.getByText('桌面客户端')).not.toBeNull();
|
|
});
|
|
|
|
it('keeps the project creation directory in the workspace settings section', async () => {
|
|
const creationDirectory = 'F:\\Projects\\陶泥儿游戏';
|
|
const storageKey =
|
|
'genarrative-ai-game-creator.project-creation-directory.v1';
|
|
const invoke = vi.fn(async (command: string) => {
|
|
if (command === 'read_game_creator_app_config') {
|
|
return {
|
|
path: '/home/test/AppData/game-creator.config.json',
|
|
config: {
|
|
agentMode: 'codex_app_server',
|
|
llm: {
|
|
apiKey: '',
|
|
baseUrl: 'https://llm.example.test/v1',
|
|
model: 'gpt-workspace',
|
|
apiKind: 'openai_responses',
|
|
reasoningEffort: 'high',
|
|
stream: true,
|
|
webSearchEnabled: false,
|
|
contextWindowTokens: 128000,
|
|
autoCompactTokenLimit: 64000,
|
|
toolOutputTokenLimit: 12000,
|
|
requestTimeoutMs: 180000,
|
|
maxRetries: 2,
|
|
retryBackoffMs: 500,
|
|
},
|
|
agentLlm: {},
|
|
editorApi: { baseUrl: 'http://127.0.0.1:8082', apiKey: '' },
|
|
},
|
|
};
|
|
}
|
|
if (command === 'pick_local_project_directory') {
|
|
return creationDirectory;
|
|
}
|
|
throw new Error(`unexpected invoke ${command}`);
|
|
});
|
|
window.__TAURI__ = { core: { invoke } };
|
|
renderLauncherAt('/?launcher');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
|
const dialog = await screen.findByRole('dialog', { name: '运行时配置' });
|
|
fireEvent.click(within(dialog).getByRole('button', { name: /工作区/ }));
|
|
|
|
// 不选目录时是默认位置,且本地不写任何偏好。
|
|
expect(within(dialog).getByText('默认位置')).not.toBeNull();
|
|
expect(window.localStorage.getItem(storageKey)).toBeNull();
|
|
|
|
fireEvent.click(within(dialog).getByRole('button', { name: '选择目录' }));
|
|
expect(await within(dialog).findByText(creationDirectory)).not.toBeNull();
|
|
expect(invoke).toHaveBeenCalledWith('pick_local_project_directory', {
|
|
title: '选择项目创建目录',
|
|
});
|
|
expect(window.localStorage.getItem(storageKey)).toBe(
|
|
JSON.stringify(creationDirectory),
|
|
);
|
|
expect(within(dialog).getByText('已更新项目创建目录')).not.toBeNull();
|
|
|
|
fireEvent.click(
|
|
within(dialog).getByRole('button', { name: '恢复默认位置' }),
|
|
);
|
|
expect(within(dialog).getByText('默认位置')).not.toBeNull();
|
|
expect(window.localStorage.getItem(storageKey)).toBeNull();
|
|
expect(within(dialog).getByText('已恢复默认位置')).not.toBeNull();
|
|
});
|
|
|
|
it('locks the Agent mode and official LLM route while dropping legacy credentials', async () => {
|
|
const invoke = vi.fn(
|
|
async (command: string, args?: Record<string, unknown>) => {
|
|
if (command === 'read_game_creator_app_config') {
|
|
return {
|
|
path: '/home/test/AppData/game-creator.config.json',
|
|
config: {
|
|
agentMode: 'codex_app_server',
|
|
llm: {
|
|
apiKey: 'preserved-provider-key',
|
|
baseUrl: 'https://llm.example.test/v1',
|
|
model: 'preserved-provider-model',
|
|
apiKind: 'openai_responses',
|
|
reasoningEffort: 'high',
|
|
stream: false,
|
|
webSearchEnabled: false,
|
|
contextWindowTokens: 128000,
|
|
autoCompactTokenLimit: 64000,
|
|
toolOutputTokenLimit: 12000,
|
|
requestTimeoutMs: 180000,
|
|
maxRetries: 2,
|
|
retryBackoffMs: 500,
|
|
},
|
|
agentLlm: {},
|
|
editorApi: {
|
|
baseUrl: 'http://127.0.0.1:8082',
|
|
apiKey: '',
|
|
},
|
|
},
|
|
};
|
|
}
|
|
if (command === 'write_game_creator_app_config') {
|
|
return {
|
|
path: '/home/test/AppData/game-creator.config.json',
|
|
config: args?.config,
|
|
};
|
|
}
|
|
throw new Error(`unexpected invoke ${command}`);
|
|
},
|
|
);
|
|
window.__TAURI__ = { core: { invoke } };
|
|
renderLauncherAt('/?launcher');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
|
expect(await screen.findByText('陶泥儿智能创作(固定)')).not.toBeNull();
|
|
expect(screen.queryByLabelText('Agent 模式')).toBeNull();
|
|
expect(screen.getByText('官方账号服务(固定)')).not.toBeNull();
|
|
expect(
|
|
screen.queryByText(/router\.genarrative\.world|gpt-5\.6-sol/),
|
|
).toBeNull();
|
|
expect(screen.queryByLabelText('LLM Provider')).toBeNull();
|
|
expect(screen.queryByLabelText('LLM API Key')).toBeNull();
|
|
expect(screen.queryByLabelText('LLM Base URL')).toBeNull();
|
|
expect(screen.queryByLabelText('LLM 模型')).toBeNull();
|
|
expect(screen.queryByLabelText('LLM API 类型')).toBeNull();
|
|
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
|
|
await waitFor(() => {
|
|
expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', {
|
|
config: expect.objectContaining({
|
|
agentMode: 'codex_app_server',
|
|
llm: expect.objectContaining({
|
|
apiKey: '',
|
|
baseUrl: '',
|
|
model: '',
|
|
apiKind: 'openai_responses',
|
|
}),
|
|
agentLlm: {},
|
|
}),
|
|
});
|
|
});
|
|
});
|
|
|
|
it('migrates an unknown Agent mode to the fixed app-server mode on save', async () => {
|
|
const invoke = vi.fn(
|
|
async (command: string, args?: Record<string, unknown>) => {
|
|
if (command === 'read_game_creator_app_config') {
|
|
return {
|
|
path: '/home/test/AppData/game-creator.config.json',
|
|
config: {
|
|
agentMode: 'future_runtime_mode',
|
|
llm: {
|
|
apiKey: '',
|
|
baseUrl: 'https://api.openai.com/v1',
|
|
model: 'gpt-4.1',
|
|
apiKind: 'openai_responses',
|
|
reasoningEffort: 'high',
|
|
stream: false,
|
|
webSearchEnabled: false,
|
|
contextWindowTokens: 128000,
|
|
autoCompactTokenLimit: 64000,
|
|
toolOutputTokenLimit: 12000,
|
|
requestTimeoutMs: 180000,
|
|
maxRetries: 2,
|
|
retryBackoffMs: 500,
|
|
},
|
|
agentLlm: {},
|
|
editorApi: {
|
|
baseUrl: 'http://127.0.0.1:8082',
|
|
apiKey: '',
|
|
},
|
|
},
|
|
};
|
|
}
|
|
if (command === 'write_game_creator_app_config') {
|
|
return {
|
|
path: '/home/test/AppData/game-creator.config.json',
|
|
config: args?.config,
|
|
};
|
|
}
|
|
throw new Error(`unexpected invoke ${command}`);
|
|
},
|
|
);
|
|
window.__TAURI__ = { core: { invoke } };
|
|
renderLauncherAt('/?launcher');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
|
expect(await screen.findByText('陶泥儿智能创作(固定)')).not.toBeNull();
|
|
expect(screen.queryByLabelText('Agent 模式')).toBeNull();
|
|
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
|
|
await waitFor(() => {
|
|
expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', {
|
|
config: expect.objectContaining({
|
|
agentMode: 'codex_app_server',
|
|
}),
|
|
});
|
|
});
|
|
});
|
|
|
|
it('edits runtime tuning from the launcher without exposing provider fields', async () => {
|
|
const invoke = vi.fn(
|
|
async (command: string, args?: Record<string, unknown>) => {
|
|
if (command === 'read_game_creator_app_config') {
|
|
return {
|
|
path: '/home/test/AppData/game-creator.config.json',
|
|
config: {
|
|
agentMode: 'provider',
|
|
llm: {
|
|
apiKey: 'launcher-loaded-secret',
|
|
baseUrl: 'https://llm.example.test/v1',
|
|
model: 'gpt-launcher',
|
|
apiKind: 'legacy',
|
|
stream: false,
|
|
webSearchEnabled: false,
|
|
requestTimeoutMs: 10,
|
|
maxRetries: -2,
|
|
retryBackoffMs: 0,
|
|
},
|
|
editorApi: {
|
|
baseUrl: 'http://127.0.0.1:8082',
|
|
apiKey: 'editor-loaded-secret',
|
|
},
|
|
},
|
|
};
|
|
}
|
|
if (command === 'write_game_creator_app_config') {
|
|
return {
|
|
path: '/home/test/AppData/game-creator.config.json',
|
|
config: args?.config,
|
|
};
|
|
}
|
|
throw new Error(`unexpected invoke ${command}`);
|
|
},
|
|
);
|
|
window.__TAURI__ = { core: { invoke } };
|
|
renderLauncherAt('/?launcher');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
|
|
|
const dialog = await screen.findByRole('dialog', { name: '运行时配置' });
|
|
expect(screen.getByText('官方账号服务(固定)')).not.toBeNull();
|
|
expect(
|
|
screen.queryByText(/router\.genarrative\.world|gpt-5\.6-sol/),
|
|
).toBeNull();
|
|
expect(screen.queryByLabelText('LLM Provider')).toBeNull();
|
|
expect(screen.queryByLabelText('LLM API Key')).toBeNull();
|
|
expect(screen.queryByLabelText('LLM Base URL')).toBeNull();
|
|
expect(screen.queryByLabelText('LLM 模型')).toBeNull();
|
|
expect(screen.queryByLabelText('LLM API 类型')).toBeNull();
|
|
fireEvent.click(screen.getByRole('button', { name: /高级参数/ }));
|
|
expect(screen.getByLabelText('请求超时 ms')).toHaveProperty(
|
|
'value',
|
|
'1000',
|
|
);
|
|
expect(screen.getByLabelText('重试次数')).toHaveProperty('value', '0');
|
|
expect(screen.getByLabelText('重试退避 ms')).toHaveProperty('value', '1');
|
|
expect(screen.getByLabelText('上下文窗口 tokens')).toHaveProperty(
|
|
'value',
|
|
'128000',
|
|
);
|
|
expect(screen.getByLabelText('自动压缩阈值 tokens')).toHaveProperty(
|
|
'value',
|
|
'64000',
|
|
);
|
|
expect(screen.getByLabelText('工具输出上限 tokens')).toHaveProperty(
|
|
'value',
|
|
'12000',
|
|
);
|
|
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
|
|
await waitFor(() => {
|
|
expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', {
|
|
config: expect.objectContaining({
|
|
llm: expect.objectContaining({
|
|
apiKey: '',
|
|
baseUrl: '',
|
|
model: '',
|
|
apiKind: 'openai_responses',
|
|
requestTimeoutMs: 1000,
|
|
maxRetries: 0,
|
|
retryBackoffMs: 1,
|
|
}),
|
|
}),
|
|
});
|
|
});
|
|
expect(
|
|
(
|
|
await screen.findByText(
|
|
'已保存:/home/test/AppData/game-creator.config.json',
|
|
)
|
|
).getAttribute('data-tone'),
|
|
).toBe('success');
|
|
fireEvent.mouseDown(dialog.parentElement as HTMLElement);
|
|
expect(screen.queryByRole('dialog', { name: '运行时配置' })).toBeNull();
|
|
});
|
|
|
|
it('locks page scrolling while runtime settings are open and restores it after close', async () => {
|
|
const invoke = vi.fn(async (command: string) => {
|
|
if (command === 'read_game_creator_app_config') {
|
|
return {
|
|
path: '/home/test/AppData/game-creator.config.json',
|
|
config: {
|
|
agentMode: 'codex_app_server',
|
|
llm: {
|
|
apiKey: '',
|
|
baseUrl: 'https://llm.example.test/v1',
|
|
model: 'gpt-scroll-lock',
|
|
apiKind: 'openai_responses',
|
|
stream: true,
|
|
webSearchEnabled: false,
|
|
requestTimeoutMs: 180000,
|
|
maxRetries: 2,
|
|
retryBackoffMs: 500,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
throw new Error(`unexpected invoke ${command}`);
|
|
});
|
|
window.__TAURI__ = { core: { invoke } };
|
|
document.documentElement.style.overflow = 'auto';
|
|
document.body.style.overflow = 'scroll';
|
|
renderLauncherAt('/?launcher');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
|
await screen.findByRole('dialog', { name: '运行时配置' });
|
|
expect(document.documentElement.style.overflow).toBe('hidden');
|
|
expect(document.body.style.overflow).toBe('hidden');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '关闭 Agent 设置' }));
|
|
expect(document.documentElement.style.overflow).toBe('auto');
|
|
expect(document.body.style.overflow).toBe('scroll');
|
|
});
|
|
|
|
it('keeps runtime config open when Escape is pressed in an input', async () => {
|
|
const invoke = vi.fn(async (command: string) => {
|
|
if (command === 'read_game_creator_app_config') {
|
|
return {
|
|
path: '/home/test/AppData/game-creator.config.json',
|
|
config: {
|
|
agentMode: 'provider',
|
|
llm: {
|
|
apiKey: '',
|
|
baseUrl: 'https://llm.example.test/v1',
|
|
model: 'gpt-test',
|
|
apiKind: 'openai_responses',
|
|
stream: false,
|
|
webSearchEnabled: false,
|
|
requestTimeoutMs: 180000,
|
|
maxRetries: 0,
|
|
retryBackoffMs: 500,
|
|
},
|
|
editorApi: {
|
|
baseUrl: 'http://127.0.0.1:8082',
|
|
apiKey: '',
|
|
},
|
|
},
|
|
};
|
|
}
|
|
throw new Error(`unexpected invoke ${command}`);
|
|
});
|
|
window.__TAURI__ = { core: { invoke } };
|
|
renderLauncherAt('/?launcher');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
|
|
|
expect(
|
|
await screen.findByRole('dialog', { name: '运行时配置' }),
|
|
).not.toBeNull();
|
|
fireEvent.keyDown(screen.getByLabelText('联网检索'), {
|
|
key: 'Escape',
|
|
});
|
|
expect(screen.getByRole('dialog', { name: '运行时配置' })).not.toBeNull();
|
|
|
|
fireEvent.keyDown(window, { key: 'Escape' });
|
|
|
|
expect(screen.queryByRole('dialog', { name: '运行时配置' })).toBeNull();
|
|
});
|
|
|
|
it('disables runtime config actions while reading config', async () => {
|
|
let resolveRead:
|
|
| ((value: {
|
|
path: string;
|
|
config: {
|
|
agentMode: 'provider';
|
|
llm: {
|
|
apiKey: string;
|
|
baseUrl: string;
|
|
model: string;
|
|
apiKind: string;
|
|
stream: boolean;
|
|
webSearchEnabled: boolean;
|
|
requestTimeoutMs: number;
|
|
maxRetries: number;
|
|
retryBackoffMs: number;
|
|
};
|
|
editorApi: { baseUrl: string; apiKey: string };
|
|
};
|
|
}) => void)
|
|
| undefined;
|
|
let readCount = 0;
|
|
let modelReadResolved = false;
|
|
const invoke = vi.fn((command: string) => {
|
|
if (command === 'read_game_creator_app_config') {
|
|
readCount += 1;
|
|
// 首页模型选择器会在挂载时读取一次配置;让首次读取立即完成,
|
|
// 以免一直处于 pending 干扰运行时配置对话框的读取计数。
|
|
if (!modelReadResolved) {
|
|
modelReadResolved = true;
|
|
return Promise.resolve({ config: { selectedModelId: 'quality' } });
|
|
}
|
|
return new Promise((resolve) => {
|
|
resolveRead = resolve as typeof resolveRead;
|
|
});
|
|
}
|
|
throw new Error(`unexpected invoke ${command}`);
|
|
});
|
|
window.__TAURI__ = { core: { invoke } };
|
|
renderLauncherAt('/?launcher');
|
|
|
|
// 首页模型选择器挂载后会在目录请求之后读取一次配置,等它完成再打开对话框。
|
|
await waitFor(() => expect(readCount).toBe(1));
|
|
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
|
|
|
expect(
|
|
await screen.findByRole('dialog', { name: '运行时配置' }),
|
|
).not.toBeNull();
|
|
expect(await screen.findByText('正在读取')).not.toBeNull();
|
|
expect(screen.getByRole('button', { name: '读取' })).toHaveProperty(
|
|
'disabled',
|
|
true,
|
|
);
|
|
expect(screen.getByRole('button', { name: '恢复默认' })).toHaveProperty(
|
|
'disabled',
|
|
true,
|
|
);
|
|
expect(screen.getByRole('button', { name: '保存' })).toHaveProperty(
|
|
'disabled',
|
|
true,
|
|
);
|
|
|
|
// 首页模型选择器挂载时会读取一次配置(上面已让首次读取立即完成),
|
|
// 这里只校验:对话框处于「正在读取」时点击读取/保存不会新增读取请求。
|
|
const readsWhileReading = readCount;
|
|
fireEvent.click(screen.getByRole('button', { name: '读取' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
|
|
expect(readCount).toBe(readsWhileReading);
|
|
await act(async () => {
|
|
resolveRead?.({
|
|
path: '/home/test/AppData/game-creator.config.json',
|
|
config: {
|
|
agentMode: 'provider',
|
|
llm: {
|
|
apiKey: '',
|
|
baseUrl: 'https://api.openai.com/v1',
|
|
model: 'gpt-4.1',
|
|
apiKind: 'openai_responses',
|
|
stream: false,
|
|
webSearchEnabled: false,
|
|
requestTimeoutMs: 180000,
|
|
maxRetries: 0,
|
|
retryBackoffMs: 500,
|
|
},
|
|
editorApi: {
|
|
baseUrl: 'http://127.0.0.1:8082',
|
|
apiKey: '',
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
expect(await screen.findByText(/已读取:/)).not.toBeNull();
|
|
expect(screen.getByRole('button', { name: '读取' })).toHaveProperty(
|
|
'disabled',
|
|
false,
|
|
);
|
|
});
|
|
}
|