5e16837f3d
普通 Launcher、开发工作台和独立 game-chat 统一显示 External Editor 配置。 补齐配置可见性、密码输入和持久化回归测试。 同步配置静态门禁、PRD、技术合同与共享决策。
1281 lines
42 KiB
TypeScript
1281 lines
42 KiB
TypeScript
import {
|
|
act,
|
|
createGameCreationAppManifest,
|
|
createGameCreationAppSeedTasks,
|
|
deriveAgentStatusCards,
|
|
expect,
|
|
fireEvent,
|
|
GAME_CREATION_AGENT_RUN_SCHEMA_VERSION,
|
|
type GameCreationAgentRunTrace,
|
|
it,
|
|
renderAppAt,
|
|
renderLauncherAt,
|
|
screen,
|
|
vi,
|
|
waitFor,
|
|
within,
|
|
} from './harness';
|
|
|
|
export function registerAgentStatusDerivationTests() {
|
|
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('switches all Agent modes and exposes effective LLM config for app-server', 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: '',
|
|
},
|
|
mcpServers: {},
|
|
},
|
|
};
|
|
}
|
|
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 mode = await screen.findByLabelText('Agent 模式');
|
|
expect(mode).toHaveProperty('value', 'codex_app_server');
|
|
expect(screen.getByLabelText('LLM 模型')).toHaveProperty(
|
|
'value',
|
|
'preserved-provider-model',
|
|
);
|
|
|
|
fireEvent.change(mode, { target: { value: 'codex_cli' } });
|
|
expect(mode).toHaveProperty('value', 'codex_cli');
|
|
expect(screen.queryByLabelText('LLM 模型')).toBeNull();
|
|
fireEvent.change(mode, { target: { value: 'provider' } });
|
|
expect(screen.getByLabelText('LLM 模型')).toHaveProperty(
|
|
'value',
|
|
'preserved-provider-model',
|
|
);
|
|
fireEvent.change(mode, { target: { value: 'codex_app_server' } });
|
|
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: 'preserved-provider-key',
|
|
model: 'preserved-provider-model',
|
|
}),
|
|
}),
|
|
});
|
|
});
|
|
});
|
|
|
|
it('preserves an unknown Agent mode returned by a newer runtime', 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: '',
|
|
},
|
|
mcpServers: {},
|
|
},
|
|
};
|
|
}
|
|
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.findByLabelText('Agent 模式')).toHaveProperty(
|
|
'value',
|
|
'future_runtime_mode',
|
|
);
|
|
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
|
|
await waitFor(() => {
|
|
expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', {
|
|
config: expect.objectContaining({
|
|
agentMode: 'future_runtime_mode',
|
|
}),
|
|
});
|
|
});
|
|
});
|
|
|
|
it('edits runtime config directly from the launcher', 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(await screen.findByDisplayValue('gpt-launcher')).not.toBeNull();
|
|
expect(screen.getByLabelText('LLM 超时 ms')).toHaveProperty(
|
|
'value',
|
|
'1000',
|
|
);
|
|
expect(screen.getByLabelText('LLM 重试次数')).toHaveProperty('value', '0');
|
|
expect(screen.getByLabelText('LLM 退避 ms')).toHaveProperty('value', '1');
|
|
expect(screen.getByLabelText('LLM API 类型')).toHaveProperty(
|
|
'value',
|
|
'openai_responses',
|
|
);
|
|
expect(screen.getByLabelText('LLM 上下文窗口 tokens')).toHaveProperty(
|
|
'value',
|
|
'128000',
|
|
);
|
|
expect(screen.getByLabelText('LLM 自动压缩阈值 tokens')).toHaveProperty(
|
|
'value',
|
|
'64000',
|
|
);
|
|
expect(screen.getByLabelText('LLM 工具输出上限 tokens')).toHaveProperty(
|
|
'value',
|
|
'12000',
|
|
);
|
|
expect(screen.getByLabelText('External Editor Base URL')).toHaveProperty(
|
|
'value',
|
|
'http://127.0.0.1:8082',
|
|
);
|
|
expect(screen.getByLabelText('External Editor API Key')).toHaveProperty(
|
|
'type',
|
|
'password',
|
|
);
|
|
expect(
|
|
screen
|
|
.getByLabelText('External Editor API Key')
|
|
.getAttribute('autocomplete'),
|
|
).toBe('off');
|
|
fireEvent.change(screen.getByLabelText('LLM 模型'), {
|
|
target: { value: 'gpt-launcher-updated' },
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
|
|
await waitFor(() => {
|
|
expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', {
|
|
config: expect.objectContaining({
|
|
llm: expect.objectContaining({
|
|
model: 'gpt-launcher-updated',
|
|
apiKind: 'openai_responses',
|
|
requestTimeoutMs: 1000,
|
|
maxRetries: 0,
|
|
retryBackoffMs: 1,
|
|
}),
|
|
}),
|
|
});
|
|
});
|
|
expect(
|
|
await screen.findByText(
|
|
'已保存:/home/test/AppData/game-creator.config.json',
|
|
),
|
|
).not.toBeNull();
|
|
fireEvent.mouseDown(dialog.parentElement as HTMLElement);
|
|
expect(screen.queryByRole('dialog', { name: '运行时配置' })).toBeNull();
|
|
});
|
|
|
|
it('configures MCP servers and reads the bounded Runner catalog', 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: '',
|
|
baseUrl: 'https://api.openai.com/v1',
|
|
model: 'gpt-4.1',
|
|
apiKind: 'openai_responses',
|
|
reasoningEffort: 'high',
|
|
stream: true,
|
|
webSearchEnabled: false,
|
|
requestTimeoutMs: 180000,
|
|
maxRetries: 0,
|
|
retryBackoffMs: 500,
|
|
},
|
|
agentLlm: {},
|
|
editorApi: {
|
|
baseUrl: 'http://127.0.0.1:8082',
|
|
apiKey: '',
|
|
},
|
|
mcpServers: {
|
|
local: {
|
|
enabled: true,
|
|
required: false,
|
|
transport: 'stdio',
|
|
command: 'node',
|
|
args: ['fixture.mjs'],
|
|
cwd: '',
|
|
env: { MCP_FIXTURE_TOKEN: 'loaded-mcp-secret' },
|
|
url: '',
|
|
bearerToken: '',
|
|
httpHeaders: {},
|
|
allowInsecureLocalhost: false,
|
|
startupTimeoutMs: 10000,
|
|
toolTimeoutMs: 60000,
|
|
enabledTools: [],
|
|
disabledTools: [],
|
|
defaultApprovalMode: 'confirm',
|
|
tools: {},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
}
|
|
if (command === 'write_game_creator_app_config') {
|
|
return {
|
|
path: '/home/test/AppData/game-creator.config.json',
|
|
config: args?.config,
|
|
};
|
|
}
|
|
if (command === 'read_game_creator_mcp_catalog') {
|
|
return {
|
|
fingerprint: 'catalog-fingerprint',
|
|
servers: [
|
|
{
|
|
serverId: 'local',
|
|
enabled: true,
|
|
required: false,
|
|
transport: 'streamableHttp',
|
|
connected: true,
|
|
serverName: 'fixture',
|
|
serverVersion: '1.0.0',
|
|
instructionsChars: 37,
|
|
toolCount: 1,
|
|
error: null,
|
|
},
|
|
],
|
|
tools: [
|
|
{
|
|
serverId: 'local',
|
|
name: 'lookup',
|
|
title: 'Lookup',
|
|
description: 'fixture tool',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: { query: { type: 'string' } },
|
|
},
|
|
readOnlyHint: true,
|
|
effectiveApprovalMode: 'auto',
|
|
},
|
|
],
|
|
};
|
|
}
|
|
throw new Error(`unexpected invoke ${command}`);
|
|
},
|
|
);
|
|
window.__TAURI__ = { core: { invoke } };
|
|
renderLauncherAt('/?launcher');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
|
const mcpRegion = await screen.findByRole('region', {
|
|
name: 'MCP servers',
|
|
});
|
|
const serverCard = within(mcpRegion)
|
|
.getByText('local')
|
|
.closest('article') as HTMLElement;
|
|
fireEvent.click(within(serverCard).getByText('配置'));
|
|
fireEvent.change(within(serverCard).getByLabelText('local Transport'), {
|
|
target: { value: 'streamableHttp' },
|
|
});
|
|
fireEvent.change(within(serverCard).getByLabelText('local URL'), {
|
|
target: { value: 'http://127.0.0.1:43123/mcp' },
|
|
});
|
|
const bearerToken = within(serverCard).getByLabelText('local Bearer token');
|
|
expect(bearerToken).toHaveProperty('type', 'password');
|
|
expect(bearerToken.getAttribute('autocomplete')).toBe('off');
|
|
fireEvent.change(bearerToken, {
|
|
target: { value: 'new-mcp-secret' },
|
|
});
|
|
fireEvent.click(
|
|
within(serverCard).getByLabelText('local 允许不安全 localhost HTTP'),
|
|
);
|
|
fireEvent.change(
|
|
within(serverCard).getByLabelText('local HTTP headers JSON'),
|
|
{ target: { value: '{bad json' } },
|
|
);
|
|
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
|
|
expect(
|
|
await screen.findByText('MCP local 的 httpHeaders 不是有效 JSON'),
|
|
).not.toBeNull();
|
|
expect(invoke).not.toHaveBeenCalledWith(
|
|
'write_game_creator_app_config',
|
|
expect.anything(),
|
|
);
|
|
|
|
fireEvent.change(
|
|
within(serverCard).getByLabelText('local HTTP headers JSON'),
|
|
{ target: { value: '{"X-Fixture":"enabled"}' } },
|
|
);
|
|
fireEvent.change(within(serverCard).getByLabelText('local 允许工具 JSON'), {
|
|
target: { value: '["lookup"]' },
|
|
});
|
|
fireEvent.change(within(serverCard).getByLabelText('local 工具覆盖 JSON'), {
|
|
target: {
|
|
value: '{"lookup":{"enabled":true,"approvalMode":"auto"}}',
|
|
},
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
|
|
await waitFor(() => {
|
|
expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', {
|
|
config: expect.objectContaining({
|
|
mcpServers: {
|
|
local: expect.objectContaining({
|
|
transport: 'streamableHttp',
|
|
url: 'http://127.0.0.1:43123/mcp',
|
|
bearerToken: 'new-mcp-secret',
|
|
httpHeaders: { 'X-Fixture': 'enabled' },
|
|
allowInsecureLocalhost: true,
|
|
enabledTools: ['lookup'],
|
|
defaultApprovalMode: 'confirm',
|
|
tools: {
|
|
lookup: { enabled: true, approvalMode: 'auto' },
|
|
},
|
|
}),
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
fireEvent.click(screen.getByRole('button', { name: '测试连接' }));
|
|
|
|
await waitFor(() => {
|
|
expect(invoke).toHaveBeenCalledWith('read_game_creator_mcp_catalog', {
|
|
projectPath: '/tmp/genarrative-ai-game-draft',
|
|
});
|
|
});
|
|
expect(await screen.findByText('已连接 · 1 个工具')).not.toBeNull();
|
|
expect(screen.getByRole('region', { name: 'MCP 连接状态' })).not.toBeNull();
|
|
expect(screen.getByText('local / Lookup')).not.toBeNull();
|
|
expect(screen.queryByText('loaded-mcp-secret')).toBeNull();
|
|
});
|
|
|
|
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('LLM Base URL'), {
|
|
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;
|
|
const invoke = vi.fn((command: string) => {
|
|
if (command === 'read_game_creator_app_config') {
|
|
readCount += 1;
|
|
return new Promise((resolve) => {
|
|
resolveRead = resolve as typeof resolveRead;
|
|
});
|
|
}
|
|
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();
|
|
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,
|
|
);
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '读取' }));
|
|
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
|
|
expect(readCount).toBe(1);
|
|
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,
|
|
);
|
|
});
|
|
}
|
|
|
|
export function registerPublishedRuntimeSettingsTests() {
|
|
it('edits the published runtime config without leaking API keys into chat', async () => {
|
|
let persistedConfig: Record<string, unknown> | undefined;
|
|
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: persistedConfig ?? {
|
|
agentMode: 'provider',
|
|
llm: {
|
|
apiKey: 'unit-loaded-secret-value',
|
|
baseUrl: 'https://llm.example.test/v1',
|
|
model: 'gpt-test',
|
|
apiKind: 'openai_responses',
|
|
reasoningEffort: 'medium',
|
|
stream: false,
|
|
webSearchEnabled: false,
|
|
requestTimeoutMs: 180000,
|
|
maxRetries: 0,
|
|
retryBackoffMs: 500,
|
|
},
|
|
agentLlm: {
|
|
planner: {
|
|
apiKey: 'planner-loaded-secret',
|
|
baseUrl: 'https://api.anthropic.com',
|
|
model: 'claude-3-5-sonnet-latest',
|
|
apiKind: 'anthropic',
|
|
reasoningEffort: 'default',
|
|
stream: false,
|
|
webSearchEnabled: false,
|
|
},
|
|
'art-asset-plan': {
|
|
apiKey: 'art-loaded-secret',
|
|
baseUrl: 'https://api.deepseek.com',
|
|
model: 'deepseek-chat',
|
|
apiKind: 'openai_chat',
|
|
stream: true,
|
|
webSearchEnabled: true,
|
|
},
|
|
},
|
|
editorApi: {
|
|
baseUrl: 'http://127.0.0.1:8082',
|
|
apiKey: 'editor-loaded-secret',
|
|
},
|
|
},
|
|
};
|
|
}
|
|
if (command === 'write_game_creator_app_config') {
|
|
persistedConfig = args?.config as Record<string, unknown>;
|
|
return {
|
|
path: '/home/test/AppData/game-creator.config.json',
|
|
config: persistedConfig,
|
|
};
|
|
}
|
|
throw new Error(`unexpected invoke ${command}`);
|
|
},
|
|
);
|
|
window.__TAURI__ = { core: { invoke } };
|
|
renderAppAt('/?dev');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
|
|
|
expect(await screen.findByDisplayValue('gpt-test')).not.toBeNull();
|
|
const runtimeConfigDialog = screen.getByRole('dialog', {
|
|
name: '运行时配置',
|
|
});
|
|
expect(
|
|
screen.getByText('/home/test/AppData/game-creator.config.json'),
|
|
).not.toBeNull();
|
|
expect(screen.getByLabelText('聊天').textContent).not.toContain(
|
|
'unit-loaded-secret-value',
|
|
);
|
|
expect(screen.getByLabelText('聊天').textContent).not.toContain(
|
|
'planner-loaded-secret',
|
|
);
|
|
expect(screen.getByLabelText('LLM API Key')).toHaveProperty(
|
|
'type',
|
|
'password',
|
|
);
|
|
expect(
|
|
screen.getByLabelText('LLM API Key').getAttribute('autocomplete'),
|
|
).toBe('off');
|
|
expect(screen.getByLabelText('External Editor API Key')).toHaveProperty(
|
|
'type',
|
|
'password',
|
|
);
|
|
expect(
|
|
screen
|
|
.getByLabelText('External Editor API Key')
|
|
.getAttribute('autocomplete'),
|
|
).toBe('off');
|
|
expect(screen.getByLabelText('Planner LLM API Key')).toHaveProperty(
|
|
'type',
|
|
'password',
|
|
);
|
|
expect(
|
|
screen.getByLabelText('Planner LLM API Key').getAttribute('autocomplete'),
|
|
).toBe('off');
|
|
expect(
|
|
screen
|
|
.getByLabelText('生成首版美术素材 (art/Asset) LLM API Key')
|
|
.getAttribute('autocomplete'),
|
|
).toBe('off');
|
|
expect(screen.getByLabelText('Planner LLM Provider')).toHaveProperty(
|
|
'value',
|
|
'anthropic',
|
|
);
|
|
expect(screen.getByLabelText('Planner LLM 模型')).toHaveProperty(
|
|
'value',
|
|
'claude-3-5-sonnet-latest',
|
|
);
|
|
expect(screen.getByLabelText('LLM 推理档')).toHaveProperty(
|
|
'value',
|
|
'medium',
|
|
);
|
|
expect(screen.getByLabelText('Planner LLM 推理档')).toHaveProperty(
|
|
'value',
|
|
'default',
|
|
);
|
|
expect(screen.getByLabelText('Generator LLM 推理档')).toHaveProperty(
|
|
'value',
|
|
'',
|
|
);
|
|
expect(
|
|
screen.getByLabelText('生成首版美术素材 (art/Asset) LLM 推理档'),
|
|
).toHaveProperty('value', '');
|
|
expect(screen.getByLabelText('Planner LLM 流式请求')).toHaveProperty(
|
|
'value',
|
|
'false',
|
|
);
|
|
expect(
|
|
screen.getByLabelText('生成首版美术素材 (art/Asset) LLM Provider'),
|
|
).toHaveProperty('value', 'deepseek');
|
|
expect(
|
|
screen.getByLabelText('生成首版美术素材 (art/Asset) LLM 流式请求'),
|
|
).toHaveProperty('value', 'true');
|
|
expect(screen.getByLabelText('LLM 联网检索')).toHaveProperty(
|
|
'checked',
|
|
false,
|
|
);
|
|
expect(screen.getByLabelText('Planner LLM 联网检索')).toHaveProperty(
|
|
'value',
|
|
'false',
|
|
);
|
|
expect(
|
|
screen.getByLabelText('生成首版美术素材 (art/Asset) LLM 联网检索'),
|
|
).toHaveProperty('value', 'true');
|
|
const supervisorWebSearch = screen.getByLabelText(
|
|
'项目总控 Agent LLM 联网检索',
|
|
);
|
|
expect(supervisorWebSearch).toHaveProperty('value', '');
|
|
fireEvent.change(supervisorWebSearch, { target: { value: 'true' } });
|
|
expect(supervisorWebSearch).toHaveProperty('value', 'true');
|
|
fireEvent.change(supervisorWebSearch, { target: { value: 'false' } });
|
|
expect(supervisorWebSearch).toHaveProperty('value', 'false');
|
|
expect(screen.getByLabelText('LLM 上下文窗口 tokens')).toHaveProperty(
|
|
'value',
|
|
'128000',
|
|
);
|
|
expect(screen.getByLabelText('LLM 自动压缩阈值 tokens')).toHaveProperty(
|
|
'value',
|
|
'64000',
|
|
);
|
|
expect(screen.getByLabelText('LLM 工具输出上限 tokens')).toHaveProperty(
|
|
'value',
|
|
'12000',
|
|
);
|
|
expect(screen.getByLabelText('Planner 上下文窗口 tokens')).toHaveProperty(
|
|
'value',
|
|
'',
|
|
);
|
|
|
|
fireEvent.change(screen.getByLabelText('LLM API Key'), {
|
|
target: { value: 'unit-new-secret-value' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('LLM Base URL'), {
|
|
target: { value: 'https://new-llm.example.test/v1' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('LLM 模型'), {
|
|
target: { value: 'gpt-next' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('LLM API 类型'), {
|
|
target: { value: 'openai_chat' },
|
|
});
|
|
fireEvent.click(screen.getByLabelText('LLM 流式请求'));
|
|
fireEvent.click(screen.getByLabelText('LLM 联网检索'));
|
|
fireEvent.change(screen.getByLabelText('LLM 上下文窗口 tokens'), {
|
|
target: { value: '160000' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('LLM 自动压缩阈值 tokens'), {
|
|
target: { value: '80000' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('LLM 工具输出上限 tokens'), {
|
|
target: { value: '10000' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('LLM 超时 ms'), {
|
|
target: { value: '90000' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('LLM 重试次数'), {
|
|
target: { value: '3' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('LLM 退避 ms'), {
|
|
target: { value: '800' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('Generator LLM API Key'), {
|
|
target: { value: 'generator-new-secret' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('Generator LLM Provider'), {
|
|
target: { value: 'deepseek' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('Generator LLM 推理档'), {
|
|
target: { value: 'high' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('Generator LLM 流式请求'), {
|
|
target: { value: 'true' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('Planner 上下文窗口 tokens'), {
|
|
target: { value: '96000' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('Planner 自动压缩阈值 tokens'), {
|
|
target: { value: '48000' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('Planner 工具输出上限 tokens'), {
|
|
target: { value: '8000' },
|
|
});
|
|
fireEvent.change(
|
|
screen.getByLabelText('生成首版美术素材 (art/Asset) LLM Provider'),
|
|
{
|
|
target: { value: 'ark' },
|
|
},
|
|
);
|
|
fireEvent.change(screen.getByLabelText('External Editor Base URL'), {
|
|
target: { value: 'http://127.0.0.1:8099' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('External Editor API Key'), {
|
|
target: { value: 'editor-new-secret' },
|
|
});
|
|
fireEvent.click(
|
|
within(runtimeConfigDialog).getByRole('button', { name: '保存' }),
|
|
);
|
|
|
|
expect(await screen.findByText(/已保存:/)).not.toBeNull();
|
|
expect(invoke).toHaveBeenCalledWith('read_game_creator_app_config');
|
|
expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', {
|
|
config: {
|
|
agentMode: 'provider',
|
|
llm: {
|
|
apiKey: 'unit-new-secret-value',
|
|
baseUrl: 'https://new-llm.example.test/v1',
|
|
model: 'gpt-next',
|
|
apiKind: 'openai_chat',
|
|
reasoningEffort: 'medium',
|
|
stream: true,
|
|
webSearchEnabled: true,
|
|
contextWindowTokens: 160000,
|
|
autoCompactTokenLimit: 80000,
|
|
toolOutputTokenLimit: 10000,
|
|
requestTimeoutMs: 90000,
|
|
maxRetries: 3,
|
|
retryBackoffMs: 800,
|
|
},
|
|
agentLlm: {
|
|
planner: {
|
|
apiKey: 'planner-loaded-secret',
|
|
baseUrl: 'https://api.anthropic.com',
|
|
model: 'claude-3-5-sonnet-latest',
|
|
apiKind: 'anthropic',
|
|
reasoningEffort: 'default',
|
|
stream: false,
|
|
webSearchEnabled: false,
|
|
contextWindowTokens: 96000,
|
|
autoCompactTokenLimit: 48000,
|
|
toolOutputTokenLimit: 8000,
|
|
},
|
|
'project-supervisor': {
|
|
webSearchEnabled: false,
|
|
},
|
|
generator: {
|
|
apiKey: 'generator-new-secret',
|
|
baseUrl: 'https://api.deepseek.com',
|
|
model: 'deepseek-chat',
|
|
apiKind: 'openai_chat',
|
|
reasoningEffort: 'high',
|
|
stream: true,
|
|
},
|
|
'art-asset-plan': {
|
|
apiKey: 'art-loaded-secret',
|
|
baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
|
|
model: 'doubao-seed-1-6',
|
|
apiKind: 'openai_chat',
|
|
stream: true,
|
|
webSearchEnabled: true,
|
|
},
|
|
},
|
|
editorApi: {
|
|
baseUrl: 'http://127.0.0.1:8099',
|
|
apiKey: 'editor-new-secret',
|
|
},
|
|
mcpServers: {},
|
|
},
|
|
});
|
|
fireEvent.click(
|
|
within(runtimeConfigDialog).getByRole('button', { name: '读取' }),
|
|
);
|
|
expect(await screen.findByText(/已读取:/)).not.toBeNull();
|
|
expect(screen.getByLabelText('LLM 联网检索')).toHaveProperty(
|
|
'checked',
|
|
true,
|
|
);
|
|
expect(screen.getByLabelText('项目总控 Agent LLM 联网检索')).toHaveProperty(
|
|
'value',
|
|
'false',
|
|
);
|
|
expect(screen.getByLabelText('聊天').textContent).not.toContain(
|
|
'unit-new-secret-value',
|
|
);
|
|
expect(screen.getByLabelText('聊天').textContent).not.toContain(
|
|
'generator-new-secret',
|
|
);
|
|
|
|
fireEvent.change(screen.getByLabelText('LLM 超时 ms'), {
|
|
target: { value: '' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('LLM 重试次数'), {
|
|
target: { value: '-2' },
|
|
});
|
|
fireEvent.change(screen.getByLabelText('LLM 退避 ms'), {
|
|
target: { value: '0' },
|
|
});
|
|
fireEvent.click(
|
|
within(runtimeConfigDialog).getByRole('button', { name: '保存' }),
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', {
|
|
config: expect.objectContaining({
|
|
llm: expect.objectContaining({
|
|
requestTimeoutMs: 1000,
|
|
maxRetries: 0,
|
|
retryBackoffMs: 1,
|
|
}),
|
|
}),
|
|
});
|
|
});
|
|
|
|
fireEvent.click(
|
|
within(runtimeConfigDialog).getByRole('button', { name: '恢复默认' }),
|
|
);
|
|
|
|
expect(screen.getByText('已恢复默认配置,保存后生效')).not.toBeNull();
|
|
expect(screen.getByLabelText('Agent 模式')).toHaveProperty(
|
|
'value',
|
|
'codex_app_server',
|
|
);
|
|
fireEvent.change(screen.getByLabelText('Agent 模式'), {
|
|
target: { value: 'provider' },
|
|
});
|
|
expect(screen.getByLabelText('LLM API Key')).toHaveProperty('value', '');
|
|
expect(screen.getByLabelText('LLM Base URL')).toHaveProperty(
|
|
'value',
|
|
'https://api.openai.com/v1',
|
|
);
|
|
expect(screen.getByLabelText('LLM 模型')).toHaveProperty(
|
|
'value',
|
|
'gpt-4.1',
|
|
);
|
|
expect(screen.getByLabelText('LLM 推理档')).toHaveProperty('value', 'high');
|
|
expect(screen.getByLabelText('LLM 联网检索')).toHaveProperty(
|
|
'checked',
|
|
false,
|
|
);
|
|
expect(screen.getByLabelText('项目总控 Agent LLM 联网检索')).toHaveProperty(
|
|
'value',
|
|
'',
|
|
);
|
|
expect(screen.getByLabelText('LLM 上下文窗口 tokens')).toHaveProperty(
|
|
'value',
|
|
'128000',
|
|
);
|
|
expect(screen.getByLabelText('LLM 自动压缩阈值 tokens')).toHaveProperty(
|
|
'value',
|
|
'64000',
|
|
);
|
|
expect(screen.getByLabelText('LLM 工具输出上限 tokens')).toHaveProperty(
|
|
'value',
|
|
'12000',
|
|
);
|
|
expect(screen.getByLabelText('External Editor Base URL')).toHaveProperty(
|
|
'value',
|
|
'http://127.0.0.1:8082',
|
|
);
|
|
fireEvent.click(
|
|
within(runtimeConfigDialog).getByRole('button', { name: '保存' }),
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', {
|
|
config: {
|
|
agentMode: 'provider',
|
|
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: '',
|
|
},
|
|
mcpServers: {},
|
|
},
|
|
});
|
|
});
|
|
}, 15000);
|
|
|
|
it('keeps Anthropic web search choices visible when the save gate rejects them', 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: 'unit-secret',
|
|
baseUrl: 'https://api.openai.com/v1',
|
|
model: 'gpt-4.1',
|
|
apiKind: 'openai_responses',
|
|
reasoningEffort: 'high',
|
|
stream: false,
|
|
webSearchEnabled: false,
|
|
requestTimeoutMs: 180000,
|
|
maxRetries: 0,
|
|
retryBackoffMs: 500,
|
|
},
|
|
agentLlm: {},
|
|
editorApi: {
|
|
baseUrl: 'http://127.0.0.1:8082',
|
|
apiKey: '',
|
|
},
|
|
},
|
|
};
|
|
}
|
|
if (command === 'write_game_creator_app_config') {
|
|
throw new Error('Anthropic 当前不支持 Provider 原生联网检索');
|
|
}
|
|
throw new Error(`unexpected invoke ${command}`);
|
|
},
|
|
);
|
|
window.__TAURI__ = { core: { invoke } };
|
|
renderAppAt('/');
|
|
|
|
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
|
expect(await screen.findByDisplayValue('gpt-4.1')).not.toBeNull();
|
|
fireEvent.change(screen.getByLabelText('LLM Provider'), {
|
|
target: { value: 'anthropic' },
|
|
});
|
|
fireEvent.click(screen.getByLabelText('LLM 联网检索'));
|
|
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
|
|
expect(
|
|
await screen.findByText('Anthropic 当前不支持 Provider 原生联网检索'),
|
|
).not.toBeNull();
|
|
expect(screen.getByLabelText('LLM Provider')).toHaveProperty(
|
|
'value',
|
|
'anthropic',
|
|
);
|
|
expect(screen.getByLabelText('LLM 联网检索')).toHaveProperty(
|
|
'checked',
|
|
true,
|
|
);
|
|
expect(invoke).toHaveBeenCalledWith('write_game_creator_app_config', {
|
|
config: expect.objectContaining({
|
|
llm: expect.objectContaining({
|
|
apiKind: 'anthropic',
|
|
webSearchEnabled: true,
|
|
}),
|
|
}),
|
|
});
|
|
});
|
|
}
|