测试:修正 AGC 测试文件的类型错误
- 按 vitest 0.34 的参数元组口径改写 mock 泛型 - 修正 noUncheckedIndexedAccess 下的下标访问 - 补齐与当前类型脱节的测试桩字段 - 按 scripts/*.mjs 的运行时契约补局部类型声明
This commit is contained in:
@@ -11,8 +11,11 @@ import {
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ProjectResourceCanvasPosition } from '../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import type { ProjectResourceCanvasCategory } from '../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import {
|
||||
isProjectResourceCanvasCategory,
|
||||
type ProjectResourceCanvasCategory,
|
||||
type ProjectResourceCanvasPosition,
|
||||
} from '../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import {
|
||||
RESOURCE_CANVAS_CARD_WIDTH,
|
||||
RESOURCE_CANVAS_DEPENDENCY_COLUMN_GAP,
|
||||
@@ -130,7 +133,11 @@ function OverlayHarness({
|
||||
overlayRef?: React.Ref<ResourceDependencyOverlayHandle>;
|
||||
}) {
|
||||
const sections = Array.from(
|
||||
new Set(positions.map((position) => position.section)),
|
||||
new Set(
|
||||
positions
|
||||
.map((position) => position.section)
|
||||
.filter(isProjectResourceCanvasCategory),
|
||||
),
|
||||
);
|
||||
return React.createElement(
|
||||
'div',
|
||||
@@ -809,7 +816,7 @@ describe('ResourceDependencyOverlay', () => {
|
||||
it('keeps drag previews in plane-local coordinates', async () => {
|
||||
const getBoundingClientRect = vi
|
||||
.spyOn(HTMLElement.prototype, 'getBoundingClientRect')
|
||||
.mockImplementation(function () {
|
||||
.mockImplementation(function (this: HTMLElement) {
|
||||
const isPlane = this.hasAttribute('data-resource-section-plane');
|
||||
return {
|
||||
x: isPlane ? 160 : 20,
|
||||
@@ -1046,7 +1053,9 @@ describe('ResourceDependencyOverlay', () => {
|
||||
.map((value) =>
|
||||
value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4,
|
||||
);
|
||||
return channels[0] * 0.2126 + channels[1] * 0.7152 + channels[2] * 0.0722;
|
||||
return (
|
||||
channels[0]! * 0.2126 + channels[1]! * 0.7152 + channels[2]! * 0.0722
|
||||
);
|
||||
};
|
||||
const lineLuminance = luminance('c45f20');
|
||||
const canvasLuminance = luminance('fffdfa');
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
import React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ResourceEditSurface } from '../src/view/project-development/ResourceEditSurface';
|
||||
import {
|
||||
type ResourceEditSubmitInput,
|
||||
ResourceEditSurface,
|
||||
} from '../src/view/project-development/ResourceEditSurface';
|
||||
import { fireEvent, render, screen, waitFor } from './appSurface/harness';
|
||||
|
||||
describe('统一资源编辑面板', () => {
|
||||
it('失败后锁定原请求字段并使用相同输入重试', async () => {
|
||||
const submit = vi
|
||||
.fn<(input: { prompt: string; assetName: string }) => Promise<void>>()
|
||||
.fn<[input: ResourceEditSubmitInput], Promise<void>>()
|
||||
.mockRejectedValueOnce(new Error('网络暂不可用'))
|
||||
.mockResolvedValueOnce(undefined);
|
||||
render(
|
||||
|
||||
@@ -68,18 +68,20 @@ function renderAuthSurface(
|
||||
) {
|
||||
const bridge = installTauriBridge(invoke, options);
|
||||
render(
|
||||
React.createElement(AuthenticatedClient, null, ({ user, logout }) =>
|
||||
React.createElement(
|
||||
'main',
|
||||
{ 'aria-label': '已登录' },
|
||||
React.createElement('span', null, user.displayName),
|
||||
// React 19 的 createElement 的 children 形参只接受 ReactNode,渲染函数子节点改编进 props。
|
||||
React.createElement(AuthenticatedClient, {
|
||||
children: ({ user, logout }) =>
|
||||
React.createElement(
|
||||
'button',
|
||||
{ type: 'button', onClick: logout },
|
||||
'退出',
|
||||
'main',
|
||||
{ 'aria-label': '已登录' },
|
||||
React.createElement('span', null, user.displayName),
|
||||
React.createElement(
|
||||
'button',
|
||||
{ type: 'button', onClick: logout },
|
||||
'退出',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
}),
|
||||
);
|
||||
return bridge;
|
||||
}
|
||||
|
||||
@@ -40,6 +40,19 @@ type InvokeOverrides = Record<
|
||||
(args: Record<string, unknown> | undefined) => unknown
|
||||
>;
|
||||
|
||||
/**
|
||||
* 安装测试用 Tauri 替身。
|
||||
*
|
||||
* `vi.fn` 的返回类型是固定签名,装不进 `window.__TAURI__` 声明的泛型签名(invoke/listen 都是
|
||||
* 泛型函数),这里在装入点整体显式转换,行为与直接赋值一致。
|
||||
*/
|
||||
function installTauriRuntime(runtime: {
|
||||
core: { invoke: unknown };
|
||||
event?: { listen: unknown };
|
||||
}) {
|
||||
window.__TAURI__ = runtime as unknown as Window['__TAURI__'];
|
||||
}
|
||||
|
||||
function gameCreatorConfigView(reasoningEffort: string) {
|
||||
return {
|
||||
path: '/tmp/chat-composer-config.json',
|
||||
@@ -108,10 +121,10 @@ async function openDirectCodexSurface(
|
||||
return chatHarness.invoke(command, args);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
installTauriRuntime({
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
};
|
||||
});
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
pickProjectFromLauncher(DYNAMIC_GAME_PROJECT_PATH);
|
||||
const surface = await screen.findByLabelText('陶泥儿项目对话');
|
||||
@@ -325,7 +338,7 @@ export function registerChatComposerControlTests() {
|
||||
const manifestAssets = [
|
||||
{
|
||||
id: 'asset:hero',
|
||||
kind: 'character',
|
||||
kind: 'character' as const,
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/hero.png',
|
||||
source: { kind: 'uploaded' as const },
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Mock } from 'vitest';
|
||||
|
||||
import {
|
||||
act,
|
||||
App,
|
||||
@@ -12,16 +14,46 @@ import {
|
||||
waitFor,
|
||||
} from './harness';
|
||||
|
||||
function renderDesignAgent(
|
||||
harness: ReturnType<typeof createProjectChatRuntimeHarness>,
|
||||
) {
|
||||
type ChatRuntimeHarness = ReturnType<typeof createProjectChatRuntimeHarness>;
|
||||
|
||||
/**
|
||||
* 安装测试用 Tauri 替身。
|
||||
*
|
||||
* harness 的 invoke/listen 是 `vi.fn` 的固定签名,装不进 `window.__TAURI__` 声明的泛型签名
|
||||
* (invoke/listen 都是泛型函数),这里在装入点整体显式转换,行为与直接赋值一致。
|
||||
*/
|
||||
function installTauriRuntime(harness: ChatRuntimeHarness) {
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
} as unknown as Window['__TAURI__'];
|
||||
}
|
||||
|
||||
/**
|
||||
* harness.invoke 的返回类型被 harness 内全部命令分支收敛成字面量联合,用例覆盖命令时要返回
|
||||
* 联合外的自定义值(例如导入文件路径),这里按固定签名 mock 取用,只放宽返回值类型。
|
||||
*/
|
||||
type LooseInvokeMock = Mock<
|
||||
[command: string, args?: Record<string, unknown>],
|
||||
Promise<unknown>
|
||||
>;
|
||||
|
||||
function looseInvoke(harness: ChatRuntimeHarness): LooseInvokeMock {
|
||||
return harness.invoke as unknown as LooseInvokeMock;
|
||||
}
|
||||
|
||||
// App 的 props 类型未导出,React 19 的 createElement 重载推断不出参数类型,这里显式转成组件类型。
|
||||
const AppComponent = App as React.ComponentType<
|
||||
NonNullable<React.ComponentProps<typeof App>>
|
||||
>;
|
||||
|
||||
function renderDesignAgent(
|
||||
harness: ReturnType<typeof createProjectChatRuntimeHarness>,
|
||||
) {
|
||||
installTauriRuntime(harness);
|
||||
window.history.pushState({}, '', '/');
|
||||
return render(
|
||||
React.createElement(App, {
|
||||
React.createElement(AppComponent, {
|
||||
initialProjectPath: harness.projectPath,
|
||||
planningStartMode: true,
|
||||
}),
|
||||
@@ -127,13 +159,14 @@ export function registerDesignAgentSurfaceTests() {
|
||||
designAgentView: designConversationView(),
|
||||
designAgentContinueView: designConversationView(),
|
||||
});
|
||||
const originalInvoke = harness.invoke.getMockImplementation()!;
|
||||
const invoke = looseInvoke(harness);
|
||||
const originalInvoke = invoke.getMockImplementation()!;
|
||||
let failImport = true;
|
||||
let finishImport!: () => void;
|
||||
const importGate = new Promise<void>((resolve) => {
|
||||
finishImport = resolve;
|
||||
});
|
||||
harness.invoke.mockImplementation(async (command, args) => {
|
||||
invoke.mockImplementation(async (command, args) => {
|
||||
if (command === 'import_design_workspace_file') {
|
||||
await importGate;
|
||||
if (args?.fileName === '补充.md' && failImport)
|
||||
@@ -224,8 +257,9 @@ export function registerDesignAgentSurfaceTests() {
|
||||
const harness = createProjectChatRuntimeHarness({
|
||||
designAgentView: designApprovalView(),
|
||||
});
|
||||
const originalInvoke = harness.invoke.getMockImplementation()!;
|
||||
harness.invoke.mockImplementation(async (command, args) =>
|
||||
const invoke = looseInvoke(harness);
|
||||
const originalInvoke = invoke.getMockImplementation()!;
|
||||
invoke.mockImplementation(async (command, args) =>
|
||||
command === 'import_design_workspace_file'
|
||||
? 'references/需求.md'
|
||||
: originalInvoke(command, args),
|
||||
@@ -367,13 +401,10 @@ export function registerDesignAgentSurfaceTests() {
|
||||
],
|
||||
},
|
||||
});
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
installTauriRuntime(harness);
|
||||
window.history.pushState({}, '', '/');
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(AppComponent, {
|
||||
initialProjectPath: harness.projectPath,
|
||||
planningStartMode: true,
|
||||
}),
|
||||
@@ -411,9 +442,10 @@ export function registerDesignAgentSurfaceTests() {
|
||||
const harness = createProjectChatRuntimeHarness({
|
||||
designAgentView: designConversationView(),
|
||||
});
|
||||
const originalInvoke = harness.invoke.getMockImplementation()!;
|
||||
const invoke = looseInvoke(harness);
|
||||
const originalInvoke = invoke.getMockImplementation()!;
|
||||
let finish!: (view: unknown) => void;
|
||||
harness.invoke.mockImplementation((command, args) =>
|
||||
invoke.mockImplementation((command, args) =>
|
||||
command === 'continue_design_agent_session'
|
||||
? new Promise((resolve) => {
|
||||
finish = resolve;
|
||||
@@ -467,12 +499,13 @@ export function registerDesignAgentSurfaceTests() {
|
||||
const harness = createProjectChatRuntimeHarness({
|
||||
designAgentView: { ...designConversationView(), messages: [history] },
|
||||
});
|
||||
const originalInvoke = harness.invoke.getMockImplementation()!;
|
||||
const invoke = looseInvoke(harness);
|
||||
const originalInvoke = invoke.getMockImplementation()!;
|
||||
const requests: {
|
||||
clientTurnId: string;
|
||||
finish: (view: unknown) => void;
|
||||
}[] = [];
|
||||
harness.invoke.mockImplementation((command, args) =>
|
||||
invoke.mockImplementation((command, args) =>
|
||||
command === 'continue_design_agent_session'
|
||||
? new Promise((resolve) => {
|
||||
requests.push({
|
||||
@@ -489,7 +522,7 @@ export function registerDesignAgentSurfaceTests() {
|
||||
await setComposerText(input, '继续');
|
||||
fireEvent.submit(input.closest('form')!);
|
||||
await waitFor(() => expect(requests).toHaveLength(1));
|
||||
const request = requests[0];
|
||||
const request = requests[0]!;
|
||||
const messageId = `${request.clientTurnId}:response:0`;
|
||||
const reply = '整块返回也逐步显示';
|
||||
const terminal = {
|
||||
@@ -543,7 +576,7 @@ export function registerDesignAgentSurfaceTests() {
|
||||
screen.getByRole('button', { name: '思考中' }).hasAttribute('disabled'),
|
||||
).toBe(true);
|
||||
expect(screen.queryByText('迟到旧回复')).toBeNull();
|
||||
await act(async () => requests[1].finish(terminal));
|
||||
await act(async () => requests[1]!.finish(terminal));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -732,13 +765,10 @@ export function registerDesignAgentSurfaceTests() {
|
||||
const harness = createProjectChatRuntimeHarness({
|
||||
designAgentView: designApprovalView(),
|
||||
});
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
installTauriRuntime(harness);
|
||||
window.history.pushState({}, '', '/');
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(AppComponent, {
|
||||
initialProjectPath: harness.projectPath,
|
||||
planningStartMode: true,
|
||||
}),
|
||||
@@ -778,13 +808,10 @@ export function registerDesignAgentSurfaceTests() {
|
||||
},
|
||||
},
|
||||
});
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
installTauriRuntime(harness);
|
||||
window.history.pushState({}, '', '/');
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(AppComponent, {
|
||||
initialProjectPath: harness.projectPath,
|
||||
planningStartMode: true,
|
||||
}),
|
||||
@@ -816,13 +843,10 @@ export function registerDesignAgentSurfaceTests() {
|
||||
designAgentView: designConversationView(),
|
||||
designAgentContinueView: designConversationView(),
|
||||
});
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
installTauriRuntime(harness);
|
||||
window.history.pushState({}, '', '/');
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(AppComponent, {
|
||||
initialProjectPath: harness.projectPath,
|
||||
planningStartMode: true,
|
||||
}),
|
||||
@@ -886,13 +910,10 @@ export function registerDesignAgentSurfaceTests() {
|
||||
const harness = createProjectChatRuntimeHarness({
|
||||
designAgentView: designHistoryView(),
|
||||
});
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: harness.invoke },
|
||||
event: { listen: harness.listen },
|
||||
};
|
||||
installTauriRuntime(harness);
|
||||
window.history.pushState({}, '', '/');
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(AppComponent, {
|
||||
initialProjectPath: harness.projectPath,
|
||||
planningStartMode: true,
|
||||
}),
|
||||
@@ -911,9 +932,9 @@ export function registerDesignAgentSurfaceTests() {
|
||||
(element) => element.getAttribute('data-agent-content') === 'process',
|
||||
),
|
||||
).toBe(true);
|
||||
fireEvent.click(summaries[0]);
|
||||
expect(details[0].open).toBe(true);
|
||||
expect(details[1].open).toBe(false);
|
||||
await waitFor(() => expect(summaries[0].textContent).toBe('思考过程'));
|
||||
fireEvent.click(summaries[0]!);
|
||||
expect(details[0]!.open).toBe(true);
|
||||
expect(details[1]!.open).toBe(false);
|
||||
await waitFor(() => expect(summaries[0]!.textContent).toBe('思考过程'));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -195,18 +195,20 @@ function pickProjectFromLauncher(
|
||||
projectPath: string,
|
||||
action: '打开项目' | '新建项目' = '打开项目',
|
||||
) {
|
||||
const invoke = window.__TAURI__?.core?.invoke;
|
||||
if (!invoke) {
|
||||
const tauri = window.__TAURI__;
|
||||
const invoke = tauri?.core?.invoke;
|
||||
if (!tauri || !invoke) {
|
||||
throw new Error('Tauri invoke is not configured');
|
||||
}
|
||||
window.__TAURI__ = {
|
||||
...window.__TAURI__,
|
||||
...tauri,
|
||||
core: {
|
||||
...window.__TAURI__.core,
|
||||
invoke: async (command, args) =>
|
||||
...tauri.core,
|
||||
// 泛型 invoke 无法由非泛型包装函数精确表达:命中目录选择命令时固定返回路径,其余转发。
|
||||
invoke: (async (command: string, args?: Record<string, unknown>) =>
|
||||
command === 'pick_local_project_directory'
|
||||
? projectPath
|
||||
: invoke(command, args),
|
||||
: invoke(command, args)) as unknown as typeof invoke,
|
||||
},
|
||||
};
|
||||
fireEvent.click(screen.getByRole('button', { name: action }));
|
||||
@@ -787,13 +789,13 @@ function createProjectChatRuntimeHarness({
|
||||
manifestInvalidatedHandlers.delete(
|
||||
handler as unknown as ManifestInvalidatedHandler,
|
||||
);
|
||||
if (progressHandler === handler) {
|
||||
if ((progressHandler as unknown) === handler) {
|
||||
progressHandler = null;
|
||||
}
|
||||
if (designAgentUpdateHandler === handler) {
|
||||
if ((designAgentUpdateHandler as unknown) === handler) {
|
||||
designAgentUpdateHandler = null;
|
||||
}
|
||||
if (directThreadNotifyHandler === handler) {
|
||||
if ((directThreadNotifyHandler as unknown) === handler) {
|
||||
directThreadNotifyHandler = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,6 +28,40 @@ import {
|
||||
within,
|
||||
} from './harness';
|
||||
|
||||
type TauriInvoke = NonNullable<
|
||||
NonNullable<NonNullable<Window['__TAURI__']>['core']>['invoke']
|
||||
>;
|
||||
|
||||
type TauriListen = NonNullable<
|
||||
NonNullable<NonNullable<Window['__TAURI__']>['event']>['listen']
|
||||
>;
|
||||
|
||||
// Tauri 的 invoke / listen 都是泛型签名,而测试里的 vi.fn mock 只有具体返回类型与
|
||||
// 事件负载,非泛型函数无法直接满足泛型签名。下面两个 helper 只做类型收敛,运行时
|
||||
// 原样返回同一个 mock,调用方仍持有原引用(mock.calls 等断言不受影响)。
|
||||
function asTauriInvoke(
|
||||
invoke: (command: string, args?: Record<string, unknown>) => Promise<unknown>,
|
||||
): TauriInvoke {
|
||||
// 泛型 invoke 与具体的非泛型 mock 结构上不可兼容,只能用 as unknown as 收敛。
|
||||
return invoke as unknown as TauriInvoke;
|
||||
}
|
||||
|
||||
function asTauriListen(
|
||||
listen: (
|
||||
event: string,
|
||||
handler: (event: { payload: unknown }) => void,
|
||||
) => Promise<unknown>,
|
||||
): TauriListen {
|
||||
// 泛型 listen 与携带具体负载的 mock 结构上不可兼容,只能用 as unknown as 收敛。
|
||||
return listen as unknown as TauriListen;
|
||||
}
|
||||
|
||||
// App 的 props 参数带默认值,`React.ComponentProps<typeof App>` 会带上 undefined,
|
||||
// createElement 因此推断不出 props;这里收窄成非空组件类型,运行时仍是同一个组件。
|
||||
const TestApp = App as React.ComponentType<
|
||||
NonNullable<React.ComponentProps<typeof App>>
|
||||
>;
|
||||
|
||||
/**
|
||||
* 外壳级浮层提示的挂点。
|
||||
*
|
||||
@@ -126,7 +160,7 @@ export function registerClientHomeTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
// 挂载即加载目录,触发按钮直接落在默认模型上,不出现「选择模型」空态。
|
||||
@@ -174,7 +208,7 @@ export function registerClientHomeTests() {
|
||||
if (command === 'read_game_creator_app_config') return { config: {} };
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherAt('/?launcher');
|
||||
await waitFor(() =>
|
||||
expect(invoke).toHaveBeenCalledWith('get_game_template_library_access'),
|
||||
@@ -256,7 +290,7 @@ export function registerClientHomeTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
// 首页推荐位只展示封面、标题、运行时与已下载徽标,本机灵感图库已随模板库上线删除。
|
||||
@@ -308,7 +342,7 @@ export function registerClientHomeTests() {
|
||||
assets: [
|
||||
{
|
||||
id: 'live-hero',
|
||||
kind: 'icon-spritesheet',
|
||||
kind: 'icon-spritesheet' as const,
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/live-hero.png',
|
||||
source: { kind: 'generated' as const, taskId: 'art-asset-plan' },
|
||||
@@ -398,7 +432,7 @@ export function registerClientHomeTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
render(
|
||||
React.createElement(WorkspaceLauncherShell, {
|
||||
currentUser: testAuthUser,
|
||||
@@ -541,8 +575,8 @@ export function registerClientHomeTests() {
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke },
|
||||
event: { listen: runtimeHarness.listen },
|
||||
core: { invoke: asTauriInvoke(invoke) },
|
||||
event: { listen: asTauriListen(runtimeHarness.listen) },
|
||||
};
|
||||
render(
|
||||
React.createElement(WorkspaceLauncherShell, {
|
||||
@@ -714,8 +748,8 @@ export function registerClientHomeTests() {
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke },
|
||||
event: { listen: runtimeHarness.listen },
|
||||
core: { invoke: asTauriInvoke(invoke) },
|
||||
event: { listen: asTauriListen(runtimeHarness.listen) },
|
||||
};
|
||||
render(
|
||||
React.createElement(WorkspaceLauncherShell, {
|
||||
@@ -820,7 +854,7 @@ export function registerClientHomeTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
expect(screen.getByLabelText('GameAgent 客户端')).not.toBeNull();
|
||||
@@ -1017,7 +1051,7 @@ export function registerClientHomeTests() {
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
|
||||
renderLauncherAt('/?launcher');
|
||||
fireEvent.click(screen.getByRole('button', { name: '充值' }));
|
||||
@@ -1117,7 +1151,7 @@ export function registerClientHomeTests() {
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '我的' }));
|
||||
@@ -1166,7 +1200,9 @@ export function registerClientHomeTests() {
|
||||
|
||||
export function registerHomeProjectCreationTests() {
|
||||
it('scopes the 策划补全 option to the game entry and clears it on creation-type switch', () => {
|
||||
window.__TAURI__ = { core: { invoke: vi.fn(async () => null) } };
|
||||
window.__TAURI__ = {
|
||||
core: { invoke: asTauriInvoke(vi.fn(async () => null)) },
|
||||
};
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
const creationTypes = screen.getByRole('group', { name: '创作类型' });
|
||||
@@ -1224,8 +1260,8 @@ export function registerHomeProjectCreationTests() {
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
core: { invoke: asTauriInvoke(invoke) },
|
||||
event: { listen: asTauriListen(chatHarness.listen) },
|
||||
};
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
@@ -1279,7 +1315,7 @@ export function registerHomeProjectCreationTests() {
|
||||
'genarrative-ai-game-creator.recent-workspaces.v1',
|
||||
JSON.stringify(['/tmp/authorized-game']),
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
expect(await screen.findByText('authorized-game')).not.toBeNull();
|
||||
@@ -1329,7 +1365,7 @@ export function registerHomeProjectCreationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
pickProjectFromLauncher('/tmp/missing-game');
|
||||
@@ -1371,7 +1407,7 @@ export function registerHomeProjectCreationTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
@@ -1424,7 +1460,7 @@ export function registerHomeProjectCreationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开项目' }));
|
||||
@@ -1457,7 +1493,7 @@ export function registerHomeProjectCreationTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开项目' }));
|
||||
@@ -1477,7 +1513,7 @@ export function registerHomeProjectCreationTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
const openButton = screen.getByRole('button', { name: '打开项目' });
|
||||
@@ -1537,8 +1573,8 @@ export function registerHomeProjectCreationTests() {
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
core: { invoke: asTauriInvoke(invoke) },
|
||||
event: { listen: asTauriListen(chatHarness.listen) },
|
||||
};
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
@@ -1684,8 +1720,8 @@ export function registerHomeProjectCreationTests() {
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
core: { invoke: asTauriInvoke(invoke) },
|
||||
event: { listen: asTauriListen(chatHarness.listen) },
|
||||
};
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
@@ -1796,7 +1832,7 @@ export function registerHomeProjectCreationTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
const promptInput = screen.getByLabelText('创作想法');
|
||||
@@ -1868,7 +1904,7 @@ export function registerHomeProjectCreationTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
@@ -1963,8 +1999,8 @@ export function registerHomeProjectCreationTests() {
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
core: { invoke: asTauriInvoke(invoke) },
|
||||
event: { listen: asTauriListen(chatHarness.listen) },
|
||||
};
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
@@ -2030,8 +2066,8 @@ export function registerHomeProjectCreationTests() {
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
core: { invoke: asTauriInvoke(invoke) },
|
||||
event: { listen: asTauriListen(chatHarness.listen) },
|
||||
};
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
@@ -2127,8 +2163,8 @@ export function registerHomeProjectCreationTests() {
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
core: { invoke: asTauriInvoke(invoke) },
|
||||
event: { listen: asTauriListen(chatHarness.listen) },
|
||||
};
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
@@ -2265,8 +2301,8 @@ export function registerHomeProjectCreationTests() {
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke },
|
||||
event: { listen: chatHarness.listen },
|
||||
core: { invoke: asTauriInvoke(invoke) },
|
||||
event: { listen: asTauriListen(chatHarness.listen) },
|
||||
};
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
@@ -2331,8 +2367,8 @@ export function registerHomeProjectCreationTests() {
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke },
|
||||
event: { listen: runtimeHarness.listen },
|
||||
core: { invoke: asTauriInvoke(invoke) },
|
||||
event: { listen: asTauriListen(runtimeHarness.listen) },
|
||||
};
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
@@ -2360,7 +2396,7 @@ export function registerHomeProjectCreationTests() {
|
||||
const committedAssets = [
|
||||
{
|
||||
id: 'direct-art-spec',
|
||||
kind: 'icon-spec',
|
||||
kind: 'icon-spec' as const,
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/art-spec.png',
|
||||
source: {
|
||||
@@ -2370,7 +2406,7 @@ export function registerHomeProjectCreationTests() {
|
||||
},
|
||||
{
|
||||
id: 'direct-game-background',
|
||||
kind: 'scene',
|
||||
kind: 'scene' as const,
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/direct-game-background.png',
|
||||
source: {
|
||||
@@ -2380,7 +2416,7 @@ export function registerHomeProjectCreationTests() {
|
||||
},
|
||||
{
|
||||
id: 'direct-art-spritesheet',
|
||||
kind: 'icon-spritesheet',
|
||||
kind: 'icon-spritesheet' as const,
|
||||
mediaType: 'image/png',
|
||||
localPath: 'assets/art-spritesheet.png',
|
||||
source: {
|
||||
@@ -2506,10 +2542,10 @@ export function registerHomeProjectCreationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(TestApp, {
|
||||
initialProjectPath: projectPath,
|
||||
initialProjectManifest: manifest,
|
||||
initialPlanningPrompt: '继续修改已有项目',
|
||||
@@ -2590,10 +2626,10 @@ export function registerHomeProjectCreationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(TestApp, {
|
||||
initialProjectPath: projectPath,
|
||||
initialProjectManifest: manifest,
|
||||
initialPlanningPrompt: '生成一个游戏',
|
||||
@@ -2624,7 +2660,7 @@ export function registerHomeProjectCreationTests() {
|
||||
|
||||
// 重进项目:被拒消息不得从历史里被重放,拒绝文案也不该被当成历史条目。
|
||||
render(
|
||||
React.createElement(App, {
|
||||
React.createElement(TestApp, {
|
||||
initialProjectPath: projectPath,
|
||||
initialProjectManifest: manifest,
|
||||
}),
|
||||
@@ -2701,7 +2737,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
window.localStorage.setItem(
|
||||
'genarrative-ai-game-creator.recent-workspaces.v1',
|
||||
JSON.stringify(paths),
|
||||
@@ -2772,7 +2808,7 @@ export function registerRecentProjectsTests() {
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
expect(screen.getByLabelText('最近项目')).not.toBeNull();
|
||||
@@ -2831,7 +2867,11 @@ export function registerRecentProjectsTests() {
|
||||
'星轨夜航',
|
||||
);
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
// 回调体内要读 invoke.mock.calls,显式标注返回类型以打断类型推断的自引用环。
|
||||
async (
|
||||
command: string,
|
||||
args?: Record<string, unknown>,
|
||||
): Promise<unknown> => {
|
||||
if (command === 'get_design_agent_runtime_mode') return null;
|
||||
if (command === 'inspect_local_project_directory') {
|
||||
const renamed =
|
||||
@@ -2858,7 +2898,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
window.localStorage.setItem(
|
||||
'genarrative-ai-game-creator.recent-workspaces.v1',
|
||||
JSON.stringify([projectPath]),
|
||||
@@ -2906,7 +2946,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
window.localStorage.setItem(
|
||||
'genarrative-ai-game-creator.recent-workspaces.v1',
|
||||
JSON.stringify([projectPath]),
|
||||
@@ -2955,7 +2995,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
window.localStorage.setItem(
|
||||
'genarrative-ai-game-creator.recent-workspaces.v1',
|
||||
JSON.stringify(['/tmp/recent-one']),
|
||||
@@ -2994,7 +3034,7 @@ export function registerRecentProjectsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
pickProjectFromLauncher('/tmp/open-failed-game');
|
||||
@@ -3044,7 +3084,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
window.localStorage.setItem(
|
||||
'genarrative-ai-game-creator.recent-workspaces.v1',
|
||||
JSON.stringify([
|
||||
@@ -3191,7 +3231,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
window.localStorage.setItem(
|
||||
'genarrative-ai-game-creator.recent-workspaces.v1',
|
||||
JSON.stringify(['/tmp/refreshable-game']),
|
||||
@@ -3256,7 +3296,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
expect(screen.getByLabelText('最近项目')).not.toBeNull();
|
||||
@@ -3304,7 +3344,7 @@ export function registerRecentProjectsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开项目' }));
|
||||
@@ -3334,7 +3374,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建项目' }));
|
||||
@@ -3375,7 +3415,7 @@ export function registerRecentProjectsTests() {
|
||||
},
|
||||
);
|
||||
const confirm = vi.spyOn(window, 'confirm').mockReturnValue(false);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建项目' }));
|
||||
@@ -3421,7 +3461,7 @@ export function registerRecentProjectsTests() {
|
||||
},
|
||||
);
|
||||
const confirm = vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建项目' }));
|
||||
@@ -3474,7 +3514,7 @@ export function registerRecentProjectsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建项目' }));
|
||||
@@ -3501,7 +3541,7 @@ export function registerRecentProjectsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
window.__TAURI__ = { core: { invoke: asTauriInvoke(invoke) } };
|
||||
renderLauncherProjectsAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '新建项目' }));
|
||||
@@ -3569,8 +3609,8 @@ export function registerRecentProjectsTests() {
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = {
|
||||
core: { invoke },
|
||||
event: { listen: supervisorHarness.listen },
|
||||
core: { invoke: asTauriInvoke(invoke) },
|
||||
event: { listen: asTauriListen(supervisorHarness.listen) },
|
||||
};
|
||||
renderLauncherAt('/?launcher', 'home', true);
|
||||
|
||||
|
||||
@@ -20,6 +20,19 @@ import {
|
||||
within,
|
||||
} from './harness';
|
||||
|
||||
/**
|
||||
* 安装测试用 Tauri 替身。
|
||||
*
|
||||
* `vi.fn` 的返回类型是固定签名,装不进 `window.__TAURI__` 声明的泛型签名(invoke/listen 都是
|
||||
* 泛型函数),这里在装入点整体显式转换,行为与直接赋值一致。
|
||||
*/
|
||||
function installTauriRuntime(runtime: {
|
||||
core: { invoke: unknown };
|
||||
event?: { listen: unknown };
|
||||
}) {
|
||||
window.__TAURI__ = runtime as unknown as Window['__TAURI__'];
|
||||
}
|
||||
|
||||
export function registerProjectConversationTests() {
|
||||
it('splits structured rejections by variant and never by copy', () => {
|
||||
// 入队失败是**结构化**载荷:分流只看 `instanceof` 与 `error.type`,文案不参与任何判断,
|
||||
@@ -192,7 +205,7 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
invoke.mockClear();
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
@@ -266,7 +279,7 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
invoke.mockClear();
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
@@ -352,7 +365,7 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
invoke.mockClear();
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
@@ -428,7 +441,7 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
const input = await screen.findByLabelText('Agent 对话内容');
|
||||
@@ -520,7 +533,7 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
const input = await screen.findByLabelText('Agent 对话内容');
|
||||
@@ -616,7 +629,7 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
|
||||
@@ -685,7 +698,7 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
const input = await screen.findByLabelText('Agent 对话内容');
|
||||
@@ -787,7 +800,7 @@ export function registerProjectConversationTests() {
|
||||
}
|
||||
throw new Error(`unexpected listen ${eventName}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke }, event: { listen } };
|
||||
installTauriRuntime({ core: { invoke }, event: { listen } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
const input = await screen.findByLabelText('Agent 对话内容');
|
||||
@@ -876,7 +889,7 @@ export function registerProjectConversationTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ }));
|
||||
expect(await screen.findByText('旧 Agent 历史消息')).not.toBeNull();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,16 @@ import {
|
||||
within,
|
||||
} from './harness';
|
||||
|
||||
/**
|
||||
* 安装测试用 Tauri invoke 替身。
|
||||
*
|
||||
* `vi.fn` 的返回类型是固定签名,装不进 `window.__TAURI__` 声明的泛型签名(invoke 是泛型
|
||||
* 函数),这里在装入点整体显式转换,行为与直接赋值一致。
|
||||
*/
|
||||
function installTauriRuntime(runtime: { core: { invoke: unknown } }) {
|
||||
window.__TAURI__ = runtime as unknown as Window['__TAURI__'];
|
||||
}
|
||||
|
||||
export function registerAgentStatusDerivationTests() {
|
||||
it('keeps fixed overlays below the in-page window title bar', () => {
|
||||
const styles = fs.readFileSync(
|
||||
@@ -111,6 +121,9 @@ export function registerAgentStatusDerivationTests() {
|
||||
pass: 1,
|
||||
agent: 'Evaluator',
|
||||
phase: 'evaluation',
|
||||
taskId: null,
|
||||
group: null,
|
||||
role: null,
|
||||
status: 'passed',
|
||||
inputPaths: ['.agent/passes/pass-1/draft.json', '.agent/findings.md'],
|
||||
outputPaths: ['.agent/findings.md'],
|
||||
@@ -119,11 +132,14 @@ export function registerAgentStatusDerivationTests() {
|
||||
{
|
||||
toolId: 'agent.evaluate',
|
||||
status: 'ok',
|
||||
inputPaths: [],
|
||||
outputPaths: [],
|
||||
summary: 'Evaluator 质量评审',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
artifacts: [],
|
||||
taskGraph: {
|
||||
goal: '做一个厨房弹幕游戏',
|
||||
readyTaskIds: ['audio-director'],
|
||||
@@ -220,7 +236,7 @@ export function registerRuntimeSettingsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
||||
@@ -269,7 +285,7 @@ export function registerRuntimeSettingsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
||||
@@ -338,7 +354,7 @@ export function registerRuntimeSettingsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
||||
@@ -412,7 +428,7 @@ export function registerRuntimeSettingsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
||||
@@ -484,7 +500,7 @@ export function registerRuntimeSettingsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
||||
@@ -536,7 +552,7 @@ export function registerRuntimeSettingsTests() {
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
||||
@@ -617,7 +633,7 @@ export function registerRuntimeSettingsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
document.documentElement.style.overflow = 'auto';
|
||||
document.body.style.overflow = 'scroll';
|
||||
renderLauncherAt('/?launcher');
|
||||
@@ -659,7 +675,7 @@ export function registerRuntimeSettingsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '配置' }));
|
||||
@@ -715,7 +731,7 @@ export function registerRuntimeSettingsTests() {
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
installTauriRuntime({ core: { invoke } });
|
||||
renderLauncherAt('/?launcher');
|
||||
|
||||
// 首页模型选择器挂载后会在目录请求之后读取一次配置,等它完成再打开对话框。
|
||||
|
||||
@@ -58,8 +58,8 @@ export function registerToolCallGroupTests() {
|
||||
'running',
|
||||
'completed',
|
||||
]);
|
||||
expect(within(rows[0]).getByText('失败')).not.toBeNull();
|
||||
expect(within(rows[2]).queryByText('失败')).toBeNull();
|
||||
expect(within(rows[0]!).getByText('失败')).not.toBeNull();
|
||||
expect(within(rows[2]!).queryByText('失败')).toBeNull();
|
||||
});
|
||||
|
||||
it('summarizes the group as its own operation count', () => {
|
||||
@@ -458,9 +458,9 @@ export function registerToolCallGroupTests() {
|
||||
view.rerender(
|
||||
React.createElement(ToolCallGroup, {
|
||||
calls: [
|
||||
{ ...calls[0], status: 'completed' as const, updatedAt: lastEnd },
|
||||
{ ...calls[0]!, status: 'completed' as const, updatedAt: lastEnd },
|
||||
{
|
||||
...calls[1],
|
||||
...calls[1]!,
|
||||
status: 'completed' as const,
|
||||
updatedAt: lastEnd - 400,
|
||||
},
|
||||
|
||||
@@ -44,9 +44,10 @@ function reportCalls() {
|
||||
function renderLogin(invokeAuth: AuthInvoke) {
|
||||
window.__TAURI__ = { core: { invoke: invokeAuth } } as never;
|
||||
render(
|
||||
createElement(AuthenticatedClient, null, () =>
|
||||
createElement('main', { 'aria-label': '已登录' }),
|
||||
),
|
||||
// React 19 的 createElement 的 children 形参只接受 ReactNode,渲染函数子节点改编进 props。
|
||||
createElement(AuthenticatedClient, {
|
||||
children: () => createElement('main', { 'aria-label': '已登录' }),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ describe('聊天输入盒的附件上限', () => {
|
||||
});
|
||||
expect(imported[0]).toHaveLength(5);
|
||||
expect(imported[1]).toHaveLength(3);
|
||||
expect(imported[0].length + imported[1].length).toBe(
|
||||
expect(imported[0]!.length + imported[1]!.length).toBe(
|
||||
MAX_CHAT_COMPOSER_ATTACHMENTS,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -59,6 +59,7 @@ function mount(
|
||||
() =>
|
||||
useHomeProjectCreation({
|
||||
setStatus: vi.fn(),
|
||||
showTransientNotice: vi.fn(),
|
||||
setLauncherView: vi.fn(),
|
||||
rememberRecentWorkspace: vi.fn(),
|
||||
}),
|
||||
@@ -88,7 +89,7 @@ it.each(['recent', 'picker', 'create'] as const)(
|
||||
);
|
||||
});
|
||||
expect(records()).toHaveLength(1);
|
||||
expect(records()[0][1]).toMatchObject({ context, openSource: source });
|
||||
expect(records()[0]![1]).toMatchObject({ context, openSource: source });
|
||||
},
|
||||
);
|
||||
|
||||
@@ -105,7 +106,7 @@ it('身份抓取挂起不阻塞打开,成功时间在抓取完成前冻结', a
|
||||
captured.resolve(context);
|
||||
});
|
||||
expect(records()).toHaveLength(1);
|
||||
expect(Date.parse(String(records()[0][1]?.eventTime))).toBeLessThanOrEqual(
|
||||
expect(Date.parse(String(records()[0]![1]?.eventTime))).toBeLessThanOrEqual(
|
||||
latestSuccessTime,
|
||||
);
|
||||
});
|
||||
@@ -179,7 +180,7 @@ it('同项目再次显式打开是新操作,重渲染不会产生额外打开'
|
||||
await result.current.openProject('C:/project', 'open');
|
||||
});
|
||||
expect(records()).toHaveLength(2);
|
||||
expect(records()[0][1]?.operationId).not.toBe(records()[1][1]?.operationId);
|
||||
expect(records()[0]![1]?.operationId).not.toBe(records()[1]![1]?.operationId);
|
||||
});
|
||||
|
||||
it('身份抓取和记录写入失败静默,不改变成功打开', async () => {
|
||||
|
||||
@@ -89,7 +89,7 @@ test('预设映射到渠道地址,自定义走归一化', async () => {
|
||||
});
|
||||
|
||||
test('开发态切换服务器会落盘并在内存中生效', async () => {
|
||||
vi.stubEnv('DEV', true);
|
||||
vi.stubEnv('DEV', 'true');
|
||||
const mod = await loadClientHttp();
|
||||
|
||||
expect(mod.isClientServerSelectionEnabled()).toBe(true);
|
||||
@@ -108,7 +108,7 @@ test('开发态切换服务器会落盘并在内存中生效', async () => {
|
||||
});
|
||||
|
||||
test('开发态从 Rust 偏好恢复服务器选择', async () => {
|
||||
vi.stubEnv('DEV', true);
|
||||
vi.stubEnv('DEV', 'true');
|
||||
const mod = await loadClientHttp();
|
||||
invoke.mockResolvedValue({
|
||||
clientServerSelection: {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/** @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { createElement } from 'react';
|
||||
import { createElement, type ReactNode } from 'react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ClientRuntimeErrorBoundary } from '../src/app/AuthenticatedClient';
|
||||
|
||||
function BrokenAuthenticatedContent() {
|
||||
// 该组件永远抛错,声明 ReactNode 返回类型才能作为 children 传给错误边界。
|
||||
function BrokenAuthenticatedContent(): ReactNode {
|
||||
throw new Error('首页渲染失败');
|
||||
}
|
||||
|
||||
@@ -20,11 +21,11 @@ describe('AGC authenticated client error boundary', () => {
|
||||
const onLogout = vi.fn();
|
||||
|
||||
render(
|
||||
createElement(
|
||||
ClientRuntimeErrorBoundary,
|
||||
{ onLogout },
|
||||
createElement(BrokenAuthenticatedContent),
|
||||
),
|
||||
createElement(ClientRuntimeErrorBoundary, {
|
||||
onLogout,
|
||||
// `children` 是必填 prop(createElement 第三参不再满足必填 props 约束)。
|
||||
children: createElement(BrokenAuthenticatedContent),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
|
||||
@@ -38,13 +38,15 @@ test('打开 Cocos 目录走导入入口,不创建 Web 脚手架或发送初
|
||||
}
|
||||
throw new Error(`意外调用:${command}`);
|
||||
});
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
// TauriInvoke 是泛型函数签名,vi.fn 的具体返回类型无法直接赋给它,这里只在装配处桥接。
|
||||
window.__TAURI__ = { core: { invoke } } as unknown as typeof window.__TAURI__;
|
||||
const setLauncherView = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useHomeProjectCreation({
|
||||
setStatus: vi.fn(),
|
||||
// 现行 UseHomeProjectCreationOptions 必填 showTransientNotice,旧的 setAgentChatProjectPath 已移除。
|
||||
showTransientNotice: vi.fn(),
|
||||
setLauncherView,
|
||||
setAgentChatProjectPath: vi.fn(),
|
||||
rememberRecentWorkspace: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -20,7 +20,6 @@ function setup(invoke: ReturnType<typeof vi.fn>) {
|
||||
setStatus: vi.fn(),
|
||||
showTransientNotice: vi.fn(),
|
||||
setLauncherView: vi.fn(),
|
||||
setAgentChatProjectPath: vi.fn(),
|
||||
rememberRecentWorkspace: vi.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -5,7 +5,9 @@ import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
||||
import type { DesignView } from '../src/app/types';
|
||||
import { useDesignReplyAnimation } from '../src/view/project-development/planning/useDesignReplyAnimation';
|
||||
|
||||
beforeEach(() => vi.useFakeTimers());
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.useRealTimers();
|
||||
@@ -39,7 +41,7 @@ it('keeps each reply progress across tool snapshots and repeated completion', ()
|
||||
result.current.receiveText(first.id, first.text);
|
||||
vi.advanceTimersByTime(100);
|
||||
});
|
||||
expect(result.current.replies[0].visible).toBe('第一');
|
||||
expect(result.current.replies[0]!.visible).toBe('第一');
|
||||
act(() =>
|
||||
result.current.receiveView(
|
||||
view(
|
||||
@@ -49,7 +51,7 @@ it('keeps each reply progress across tool snapshots and repeated completion', ()
|
||||
),
|
||||
);
|
||||
expect(result.current.replies).toHaveLength(1);
|
||||
expect(result.current.replies[0].visible).toBe('第一');
|
||||
expect(result.current.replies[0]!.visible).toBe('第一');
|
||||
act(() => result.current.receiveText(second.id, second.text));
|
||||
const terminal = view([history, first, second]);
|
||||
act(() => {
|
||||
@@ -74,9 +76,9 @@ it('resets only an unpersisted retry attempt and discards failed partial output'
|
||||
result.current.receiveText('t:response:0', '尝试失败');
|
||||
vi.advanceTimersByTime(100);
|
||||
});
|
||||
expect(result.current.replies[0].visible).toBe('尝试');
|
||||
expect(result.current.replies[0]!.visible).toBe('尝试');
|
||||
act(() => result.current.receiveText('t:response:0', ''));
|
||||
expect(result.current.replies[0].visible).toBe('');
|
||||
expect(result.current.replies[0]!.visible).toBe('');
|
||||
const saved = { id: 't:response:0', role: 'assistant', text: '成功' };
|
||||
act(() => {
|
||||
result.current.receiveText(saved.id, saved.text);
|
||||
|
||||
@@ -4,14 +4,77 @@ import {
|
||||
createAgcAdminWebEndpoint,
|
||||
createAgcDevEndpoint,
|
||||
readConfiguredAgcAdminWebPort,
|
||||
resolveAgcAdminWebEndpoint,
|
||||
resolveAgcDevEndpoint,
|
||||
resolveAgcAdminWebEndpoint as resolveAgcAdminWebEndpointImpl,
|
||||
resolveAgcDevEndpoint as resolveAgcDevEndpointImpl,
|
||||
withAgcDevEndpointEnv,
|
||||
} from '../scripts/dev-port.mjs';
|
||||
|
||||
// scripts/dev-port.mjs 没有 JSDoc,TS 按 JS 默认值把可选参数推成了过窄类型
|
||||
// (findPort 的 portRange 推成 null、reservedPorts 推成 never[]),与测试注入的 mock 结构不兼容。
|
||||
// 下面按运行时真实契约重新声明局部签名,只做类型层校正,调用方式与行为完全不变。
|
||||
type EndpointPortRange = { start: number; end: number; label: string };
|
||||
|
||||
type AgcDevEndpointLike = {
|
||||
host: string;
|
||||
port: number;
|
||||
url: string;
|
||||
markerUrl: string;
|
||||
portRange: EndpointPortRange | null;
|
||||
};
|
||||
|
||||
type AgcAdminWebEndpointLike = {
|
||||
host: string;
|
||||
port: number;
|
||||
origin: string;
|
||||
basePath: string;
|
||||
url: string;
|
||||
portRange: EndpointPortRange | null;
|
||||
};
|
||||
|
||||
type FindPortOptions = {
|
||||
host?: string;
|
||||
preferredPort: number;
|
||||
reservedPorts?: Set<number>;
|
||||
maxAttempts?: number | null;
|
||||
portRange?: EndpointPortRange | null;
|
||||
strict?: boolean;
|
||||
};
|
||||
|
||||
type ReservePortRangeResult =
|
||||
| { range?: EndpointPortRange | null }
|
||||
| null
|
||||
| undefined;
|
||||
|
||||
type ResolveAgcDevEndpointOptions = {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
platform?: NodeJS.Platform;
|
||||
strictConfigured?: boolean;
|
||||
reservePortRange?: (options?: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}) => Promise<ReservePortRangeResult>;
|
||||
findPort?: (options: FindPortOptions) => number | Promise<number>;
|
||||
};
|
||||
|
||||
type ResolveAgcAdminWebEndpointOptions = ResolveAgcDevEndpointOptions & {
|
||||
reservedPorts?: number[];
|
||||
};
|
||||
|
||||
// 导入实现本身是同一份,仅按运行时契约修正推导过窄的签名(js 无 JSDoc 导致结构上不兼容)。
|
||||
const resolveAgcDevEndpoint = resolveAgcDevEndpointImpl as unknown as (
|
||||
options?: ResolveAgcDevEndpointOptions,
|
||||
) => Promise<AgcDevEndpointLike>;
|
||||
|
||||
// 同上:reservedPorts 在 JS 推断里被收窄为 never[],与实际可传入的保留端口数组不兼容。
|
||||
const resolveAgcAdminWebEndpoint =
|
||||
resolveAgcAdminWebEndpointImpl as unknown as (
|
||||
options?: ResolveAgcAdminWebEndpointOptions,
|
||||
) => Promise<AgcAdminWebEndpointLike>;
|
||||
|
||||
describe('AI 游戏创作 dev 端口', () => {
|
||||
test('Linux 使用用户端口段的 start + 5 槽位', async () => {
|
||||
const findPort = vi.fn(async ({ preferredPort }) => preferredPort);
|
||||
const findPort = vi.fn(
|
||||
async ({ preferredPort }: FindPortOptions) => preferredPort,
|
||||
);
|
||||
const endpoint = await resolveAgcDevEndpoint({
|
||||
env: { USER: 'alice', LOGNAME: 'alice' },
|
||||
platform: 'linux',
|
||||
@@ -44,7 +107,7 @@ describe('AI 游戏创作 dev 端口', () => {
|
||||
username: 'alice',
|
||||
range: { start: 10000, end: 10099, label: '10000-10099' },
|
||||
}),
|
||||
findPort: async ({ preferredPort, portRange }) => {
|
||||
findPort: async ({ preferredPort, portRange }: FindPortOptions) => {
|
||||
expect(preferredPort).toBe(10005);
|
||||
expect(portRange?.label).toBe('10000-10099');
|
||||
return 10006;
|
||||
@@ -55,7 +118,9 @@ describe('AI 游戏创作 dev 端口', () => {
|
||||
});
|
||||
|
||||
test('父启动器选定端口后子启动器严格使用同一端口', async () => {
|
||||
const findPort = vi.fn(async ({ preferredPort }) => preferredPort);
|
||||
const findPort = vi.fn(
|
||||
async ({ preferredPort }: FindPortOptions) => preferredPort,
|
||||
);
|
||||
await resolveAgcDevEndpoint({
|
||||
env: {
|
||||
USER: 'alice',
|
||||
@@ -134,7 +199,9 @@ describe('AI 游戏创作 dev 端口', () => {
|
||||
|
||||
describe('AGC 开发态后台 Web 端口', () => {
|
||||
test('Linux 使用用户端口段的 start + 3 槽位', async () => {
|
||||
const findPort = vi.fn(async ({ preferredPort }) => preferredPort);
|
||||
const findPort = vi.fn(
|
||||
async ({ preferredPort }: FindPortOptions) => preferredPort,
|
||||
);
|
||||
const endpoint = await resolveAgcAdminWebEndpoint({
|
||||
env: { USER: 'alice', LOGNAME: 'alice' },
|
||||
platform: 'linux',
|
||||
@@ -161,7 +228,9 @@ describe('AGC 开发态后台 Web 端口', () => {
|
||||
});
|
||||
|
||||
test('AGC Vite 端口作为保留端口传入,避免被后台 Web 抢先占用', async () => {
|
||||
const findPort = vi.fn(async ({ preferredPort }) => preferredPort);
|
||||
const findPort = vi.fn(
|
||||
async ({ preferredPort }: FindPortOptions) => preferredPort,
|
||||
);
|
||||
await resolveAgcAdminWebEndpoint({
|
||||
env: {},
|
||||
platform: 'win32',
|
||||
@@ -170,11 +239,13 @@ describe('AGC 开发态后台 Web 端口', () => {
|
||||
});
|
||||
|
||||
const [call] = findPort.mock.calls;
|
||||
expect([...call[0].reservedPorts]).toEqual([10005]);
|
||||
expect([...call![0].reservedPorts!]).toEqual([10005]);
|
||||
});
|
||||
|
||||
test('非 Linux 保留 3102 兼容优先端口并允许统一漂移', async () => {
|
||||
const findPort = vi.fn(async ({ preferredPort }) => preferredPort + 1);
|
||||
const findPort = vi.fn(
|
||||
async ({ preferredPort }: FindPortOptions) => preferredPort + 1,
|
||||
);
|
||||
const endpoint = await resolveAgcAdminWebEndpoint({
|
||||
env: {},
|
||||
platform: 'win32',
|
||||
@@ -189,7 +260,9 @@ describe('AGC 开发态后台 Web 端口', () => {
|
||||
});
|
||||
|
||||
test('显式 ADMIN_WEB_PORT 与脚本统一命名并支持严格占用', async () => {
|
||||
const findPort = vi.fn(async ({ preferredPort }) => preferredPort);
|
||||
const findPort = vi.fn(
|
||||
async ({ preferredPort }: FindPortOptions) => preferredPort,
|
||||
);
|
||||
await resolveAgcAdminWebEndpoint({
|
||||
env: { ADMIN_WEB_PORT: '3109' },
|
||||
platform: 'win32',
|
||||
|
||||
@@ -26,11 +26,15 @@ afterEach(() => {
|
||||
eventFake = null;
|
||||
});
|
||||
|
||||
const ACTIVE_TURN = {
|
||||
const ACTIVE_TURN: GameCreatorDirectActiveTurn = {
|
||||
projectPath: 'C:/projects/demo',
|
||||
agentId: 'planning-agent-v2',
|
||||
runId: 'run-1',
|
||||
} as unknown as GameCreatorDirectActiveTurn;
|
||||
projectName: '示例项目',
|
||||
turnId: 'turn-1',
|
||||
startedAt: 1,
|
||||
status: 'running',
|
||||
updatedAt: 2,
|
||||
sequence: 1,
|
||||
};
|
||||
|
||||
describe('useDirectActiveTurns', () => {
|
||||
it('keeps the snapshot identity when an event-triggered read returns the same content', async () => {
|
||||
@@ -56,7 +60,10 @@ describe('useDirectActiveTurns', () => {
|
||||
});
|
||||
|
||||
it('subscribes before the first snapshot and does not read while quiet', async () => {
|
||||
const invoke = vi.fn(async () => [ACTIVE_TURN]);
|
||||
const invoke = vi.fn<
|
||||
[command: string],
|
||||
Promise<GameCreatorDirectActiveTurn[]>
|
||||
>(async () => [ACTIVE_TURN]);
|
||||
eventFake = createTauriEventFake({ invoke });
|
||||
eventFake.install();
|
||||
const setIntervalSpy = vi.spyOn(window, 'setInterval');
|
||||
@@ -235,7 +242,7 @@ describe('useDirectActiveTurns', () => {
|
||||
completeOld = resolve;
|
||||
}),
|
||||
)
|
||||
.mockResolvedValue([{ ...ACTIVE_TURN, runId: 'new-run' }]);
|
||||
.mockResolvedValue([{ ...ACTIVE_TURN, turnId: 'new-run' }]);
|
||||
const { result, rerender } = renderHook(
|
||||
({ enabled }) =>
|
||||
useDirectActiveTurns({ invoke: invoke as never, enabled }),
|
||||
@@ -244,7 +251,7 @@ describe('useDirectActiveTurns', () => {
|
||||
rerender({ enabled: false });
|
||||
rerender({ enabled: true });
|
||||
await waitFor(() =>
|
||||
expect(result.current.activeTurns[0]?.runId).toBe('new-run'),
|
||||
expect(result.current.activeTurns[0]?.turnId).toBe('new-run'),
|
||||
);
|
||||
const current = result.current.activeTurns;
|
||||
await act(async () => {
|
||||
@@ -289,7 +296,7 @@ describe('ActiveProjectRunsPanel', () => {
|
||||
true,
|
||||
false,
|
||||
]);
|
||||
fireEvent.click(items[0]);
|
||||
fireEvent.click(items[0]!);
|
||||
expect(onOpenProject).toHaveBeenCalledWith('C:/projects/first');
|
||||
});
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ describe('DirectProject 历史分页连拉', () => {
|
||||
expect(result.error).toBeNull();
|
||||
const merged = selectDirectChatEntries(
|
||||
mergeDirectHistoryItems(
|
||||
{ turnRunning: false, history: existing, live: [] },
|
||||
{ ...emptyDirectThreadChatState(), history: existing },
|
||||
result.items,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ test('运行中状态条的读秒按 100ms 刷新:不足一分钟的耗时以
|
||||
vi.setSystemTime(STARTED_AT);
|
||||
const view = render(
|
||||
<DirectProjectConversation
|
||||
conversationKey="direct-process-status"
|
||||
turns={[]}
|
||||
historyHasMore={false}
|
||||
historyLoading={false}
|
||||
@@ -59,6 +60,7 @@ test('没有运行中的回合时不订阅时钟', () => {
|
||||
try {
|
||||
render(
|
||||
<DirectProjectConversation
|
||||
conversationKey="direct-process-status"
|
||||
turns={[]}
|
||||
historyHasMore={false}
|
||||
historyLoading={false}
|
||||
|
||||
@@ -22,7 +22,8 @@ const turn = (
|
||||
users: [{ kind: 'user', key: 'u', text: '帮我做一个跳跃动作', at: SENT_AT }],
|
||||
process: [
|
||||
{ kind: 'reasoning', key: 'r', text: '先看目录' },
|
||||
{ kind: 'assistant', key: 'a', text: '正在写' },
|
||||
// assistant 块按真实类型需要 `at`(只有用户气泡会读它,这里给回合开始时间)。
|
||||
{ kind: 'assistant', key: 'a', text: '正在写', at: SENT_AT },
|
||||
],
|
||||
finals: [{ kind: 'assistant', key: 'a-final', text: '改好了', at: ENDED_AT }],
|
||||
state,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type {
|
||||
DirectChatEntry,
|
||||
DirectThreadChatState,
|
||||
} from '../src/features/project-workspace/directThreadChat';
|
||||
import {
|
||||
type DirectChatEntry,
|
||||
type DirectThreadChatState,
|
||||
emptyDirectThreadChatState,
|
||||
finishDirectThreadTurn,
|
||||
mergeDirectHistoryItems,
|
||||
@@ -12,8 +10,8 @@ import {
|
||||
resolveDirectThreadBootstrap,
|
||||
selectDirectChatEntries,
|
||||
} from '../src/view/project-development/chat/conversation/directThreadChat';
|
||||
import type { ThreadItem } from '../src/view/project-development/chat/conversation/directThreadItemProjection';
|
||||
import type { ThreadEvent } from '../src/view/project-development/chat/generated/ThreadEvent';
|
||||
import type { ThreadItem } from '../src/view/project-development/chat/generated/ThreadItem';
|
||||
|
||||
function event(
|
||||
partial: Pick<ThreadEvent, 'type'> & Partial<ThreadEvent>,
|
||||
@@ -26,7 +24,15 @@ function withUserItemId(
|
||||
lifecycle: ThreadEvent,
|
||||
userItemId: string,
|
||||
): ThreadEvent {
|
||||
return { ...lifecycle, userItemId };
|
||||
// `userItemId` 只属于 `turn.started` / `turn.completed` 两种生命周期事件;
|
||||
// 其余事件没有这个字段,原样返回。
|
||||
if (
|
||||
lifecycle.type === 'turn.started' ||
|
||||
lifecycle.type === 'turn.completed'
|
||||
) {
|
||||
return { ...lifecycle, userItemId };
|
||||
}
|
||||
return lifecycle;
|
||||
}
|
||||
|
||||
/** app-server `item/started`:工具真正开始执行,itemId 就是归一后的唯一身份。 */
|
||||
@@ -44,12 +50,17 @@ function toolStarted(
|
||||
}
|
||||
|
||||
/** 原始 response item 的输出条目:Rust 已经把它归一成同一个 itemId。 */
|
||||
function toolOutput(): ThreadItem {
|
||||
function toolOutput(
|
||||
overrides: Partial<
|
||||
Extract<ThreadItem, { itemType: 'function_call_output' }>
|
||||
> = {},
|
||||
): ThreadItem {
|
||||
return {
|
||||
itemType: 'function_call_output',
|
||||
itemId: 'call_00_Gpd0s0Ytm9YgIbwbEXva1473',
|
||||
output: 'assets\ngame',
|
||||
at: 2000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -295,7 +306,7 @@ describe('DirectProject 聊天 reducer', () => {
|
||||
id: `direct-codex:${clientTurnId}:user`,
|
||||
content: [{ type: 'input_text', text }],
|
||||
},
|
||||
} as Partial<ThreadEvent>);
|
||||
});
|
||||
}
|
||||
|
||||
it('按事件顺序折成队列,取消与放行都只按身份移除', () => {
|
||||
@@ -738,12 +749,12 @@ describe('DirectProject 聊天 reducer', () => {
|
||||
});
|
||||
|
||||
it('内部工具的阶段状态:开始事件就是运行中,正式完成事件才结束', () => {
|
||||
const fileChange = {
|
||||
const fileChange: Extract<ThreadItem, { itemType: 'fileChange' }> = {
|
||||
itemType: 'fileChange',
|
||||
itemId: 'file-1',
|
||||
changes: [{ path: 'game/src/hero.ts', kind: 'update' }],
|
||||
at: 0,
|
||||
} as const;
|
||||
};
|
||||
const started = reduceDirectThreadEvents(emptyDirectThreadChatState(), [
|
||||
event({ type: 'item.started', at: 1_000_100, item: fileChange }),
|
||||
]);
|
||||
@@ -811,7 +822,7 @@ describe('DirectProject 聊天 reducer', () => {
|
||||
event({ type: 'turn.started', at: 1_000_000 }),
|
||||
// 宿主收口的毫秒时间:原生回合时间是秒级精度,"上一轮结束之后又来一条开始事件"
|
||||
// 就是新回合,不能因为时间不比终点晚就把它当重放吞掉。
|
||||
event({ type: 'turn.completed', status: 'cancelled', at: 1_000_900 }),
|
||||
event({ type: 'turn.completed', status: 'aborted', at: 1_000_900 }),
|
||||
]);
|
||||
const next = reduceDirectThreadEvents(done, [
|
||||
event({ type: 'turn.started', at: 1_000_000 }),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user