6981648796
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m56s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m48s
Project CI / Native shell tests (pull_request) Failing after 45s
Project CI / Repository checks (pull_request) Failing after 13s
Project CI / Frontend tests (pull_request) Failing after 1m52s
Project CI / AI game creator shell web tests (pull_request) Failing after 1m42s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Failing after 6m57s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Failing after 8m1s
- 解决 refactor/split-direct-project 与 origin/master 在 App.tsx、立项策划聊天视图、Direct composer/引用输入区、styles.css、Rust direct user item 与 appSurface 用例上的冲突,按「Supervisor 永久退役」口径保留 DirectProject 独立聊天容器与 Design Agent 两条产品路径 - 采纳 master 的策划 Agent V1/V2 退役:删除 GDD 审批卡、策划输入卡、planningLane、planningSessionV2、planningSessionContract、规划展示适配与 Rust planning_*_v2 命令、模块、契约及对应用例,不保留兼容别名或双跑路径 - 把 master「折叠思考显示单行预览」的目的落到当前结构:新增共享表现 chat/components/AgentReasoning/AgentReasoning.tsx(折叠态单行纯文本预览 + 箭头、展开态安全 Markdown),DirectProject 回合与策划回合共用,删掉两处写死的 pre 折叠实现 - 把 master「策划入口可选模型 / 推理档」的目的接到当前策划输入盒:复用 ConversationModelSelect 与 ComposerReasoningEffortSelect,配置写回仍走客户端配置通道 - App.tsx 删除只服务退役 Supervisor / 策划 V2 的 state、ref、effect、回调与死参数,并删除两条读路径都退役后的 workspaceProjectKind;openWorkspace 的工程类型入参保留为未使用契约 - Rust 侧保留本分支 canonical→wire 投影、无审计 Direct 回合与 direct user item 严格校验,并入 master 的 prepare_new_web_project_at 前置复核 - 更新 ADR 与 shared-memory 决策记录:策划当前只有 Design Agent、两条路径的共享表现清单,以及本次合并的口径、代价与验证证据 - 验证:AGC 与仓库 typecheck、check:encoding、check:doc-index、git diff --check、改动文件 eslint 0 error;AGC vitest 168 个文件中除 5 个 jsdom localStorage 环境失败文件与本分支既有 resourceTagStatsRefresh 失败外全绿,appSurface 198 passed / 13 skipped;Rust 定向用例 direct_codex_user_item、skill_pack、sessions 全过(整套分片在本容器受 /sbin -> usr/bin 触发沙箱预检失败,与本合并无关)
342 lines
12 KiB
TypeScript
342 lines
12 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { afterEach, describe, expect, test, vi } from 'vitest';
|
|
|
|
import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
|
|
import {
|
|
cleanup,
|
|
createGameCreationAppManifest,
|
|
fireEvent,
|
|
ProjectDevelopmentView,
|
|
React,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
within,
|
|
} from './appSurface/harness';
|
|
|
|
const PROJECT_PATH = '/tmp/workbench-asset-rename';
|
|
|
|
/** 资源卡懒加载预览要靠 IntersectionObserver 才认成可见,这里只做最小替身。 */
|
|
function installResourceCardIntersectionObserver() {
|
|
class ResourceCardIntersectionObserver {
|
|
readonly root = null;
|
|
readonly rootMargin = '160px';
|
|
readonly thresholds = [0];
|
|
readonly observed = new Set<Element>();
|
|
|
|
constructor(readonly callback: IntersectionObserverCallback) {}
|
|
|
|
observe(element: Element) {
|
|
this.observed.add(element);
|
|
}
|
|
|
|
unobserve(element: Element) {
|
|
this.observed.delete(element);
|
|
}
|
|
|
|
disconnect() {
|
|
this.observed.clear();
|
|
}
|
|
|
|
takeRecords() {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
Object.defineProperty(window, 'IntersectionObserver', {
|
|
configurable: true,
|
|
value: ResourceCardIntersectionObserver,
|
|
});
|
|
}
|
|
|
|
function installInvoke(
|
|
implementation: (command: string, args?: unknown) => Promise<unknown>,
|
|
) {
|
|
const invoke = vi.fn(implementation);
|
|
(
|
|
window as unknown as {
|
|
__TAURI__?: { core?: { invoke?: typeof invoke } };
|
|
}
|
|
).__TAURI__ = { core: { invoke } };
|
|
return invoke;
|
|
}
|
|
|
|
function createManifestWithHero(localPath: string): GameCreationAppManifest {
|
|
const manifest = createGameCreationAppManifest(
|
|
'workbench-asset-rename',
|
|
'素材重命名测试',
|
|
);
|
|
manifest.assets = [
|
|
{
|
|
id: 'asset-hero',
|
|
kind: 'character',
|
|
mediaType: 'image/png',
|
|
localPath,
|
|
source: { kind: 'generated' },
|
|
},
|
|
];
|
|
return manifest;
|
|
}
|
|
|
|
async function openHeroCard(name = 'hero.png') {
|
|
fireEvent.click(screen.getByRole('button', { name: '按类型' }));
|
|
// 左侧栏目大纲导航已按用户要求删除:切栏目走「资源总览」的栏目缩略卡片。
|
|
if (document.querySelector('[data-resource-book-view="child"]')) {
|
|
fireEvent.click(await screen.findByRole('button', { name: '收起资源' }));
|
|
await waitFor(() =>
|
|
expect(
|
|
document.querySelector('[data-resource-book-view="main"]'),
|
|
).not.toBeNull(),
|
|
);
|
|
}
|
|
fireEvent.click(
|
|
await screen.findByRole('button', { name: '打开角色与对象' }),
|
|
);
|
|
// 资源画本先切到子画布再渲染卡片,等它落到 character 栏目再找卡。
|
|
await waitFor(() => {
|
|
const manager = document.querySelector('[data-resource-book-view="child"]');
|
|
expect(manager).not.toBeNull();
|
|
expect(
|
|
manager?.querySelector(
|
|
'.game-resource-book-scene-titlebar.is-active[data-resource-book-category="character"]',
|
|
),
|
|
).not.toBeNull();
|
|
});
|
|
// C3 后点卡是「选中 + 浮出工具条」,重命名入口在工具条上。
|
|
fireEvent.click(
|
|
await screen.findByRole('button', {
|
|
name: new RegExp(`^选中资源:角色与对象 ${name}$`),
|
|
}),
|
|
);
|
|
return screen.findByRole('toolbar', { name: '图片工具栏' });
|
|
}
|
|
|
|
function renderWorkbench(
|
|
manifest: GameCreationAppManifest,
|
|
onManifestChange?: (
|
|
projectPath: string,
|
|
next: GameCreationAppManifest,
|
|
metadata: unknown,
|
|
) => void,
|
|
) {
|
|
const { rerender } = render(
|
|
React.createElement(ProjectDevelopmentView, {
|
|
projectName: manifest.name,
|
|
projectPath: PROJECT_PATH,
|
|
manifest,
|
|
attachments: [],
|
|
recentRunStatus: null,
|
|
recentRunStopReason: null,
|
|
chat: React.createElement('div', null, '项目对话'),
|
|
onHomeOpen: vi.fn(),
|
|
onProjectsOpen: vi.fn(),
|
|
onManifestChange,
|
|
}),
|
|
);
|
|
return {
|
|
rerenderWith(next: GameCreationAppManifest) {
|
|
rerender(
|
|
React.createElement(ProjectDevelopmentView, {
|
|
projectName: next.name,
|
|
projectPath: PROJECT_PATH,
|
|
manifest: next,
|
|
attachments: [],
|
|
recentRunStatus: null,
|
|
recentRunStopReason: null,
|
|
chat: React.createElement('div', null, '项目对话'),
|
|
onHomeOpen: vi.fn(),
|
|
onProjectsOpen: vi.fn(),
|
|
onManifestChange,
|
|
}),
|
|
);
|
|
},
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
cleanup();
|
|
delete (window as unknown as { __TAURI__?: unknown }).__TAURI__;
|
|
});
|
|
|
|
describe('素材重命名前端链路', () => {
|
|
test('renames through the strict native command and refreshes the resource card name', async () => {
|
|
installResourceCardIntersectionObserver();
|
|
const renamedManifest = createManifestWithHero('assets/hero-v2.png');
|
|
const invoke = installInvoke(async (command) => {
|
|
if (command === 'get_local_game_project_revision') {
|
|
return { revision: 11, hasCommittedEdit: true };
|
|
}
|
|
if (command === 'rename_local_project_asset') {
|
|
return {
|
|
asset: renamedManifest.assets[0],
|
|
previousLocalPath: 'assets/hero.png',
|
|
committedProjectRevision: 12,
|
|
};
|
|
}
|
|
if (command === 'get_local_game_manifest') {
|
|
return renamedManifest;
|
|
}
|
|
throw new Error(`unexpected command: ${command}`);
|
|
});
|
|
const onManifestChange = vi.fn();
|
|
|
|
const rendered = renderWorkbench(
|
|
createManifestWithHero('assets/hero.png'),
|
|
onManifestChange,
|
|
);
|
|
const toolbar = await openHeroCard();
|
|
fireEvent.click(within(toolbar).getByRole('button', { name: '重命名' }));
|
|
const field = (await screen.findByLabelText(
|
|
'新文件名',
|
|
)) as HTMLInputElement;
|
|
expect(field.value).toBe('hero.png');
|
|
fireEvent.change(field, { target: { value: 'hero-v2.png' } });
|
|
fireEvent.click(screen.getByRole('button', { name: '确认重命名素材' }));
|
|
|
|
await waitFor(() => {
|
|
expect(onManifestChange).toHaveBeenCalledTimes(1);
|
|
});
|
|
// 与删除 / 分类保存同一套 CAS 口径:带上项目身份 + **这一次读到的** revision。
|
|
expect(invoke).toHaveBeenCalledWith('rename_local_project_asset', {
|
|
input: {
|
|
projectPath: PROJECT_PATH,
|
|
expectedProjectId: 'workbench-asset-rename',
|
|
expectedProjectRevision: 11,
|
|
assetId: 'asset-hero',
|
|
newFileName: 'hero-v2.png',
|
|
},
|
|
});
|
|
expect(invoke).toHaveBeenCalledWith('get_local_game_project_revision', {
|
|
projectPath: PROJECT_PATH,
|
|
});
|
|
// revision 必须来自那一次读取:先读再改名,顺序反了就等于把过期值贴上去。
|
|
expect(
|
|
invoke.mock.calls.findIndex(
|
|
([command]) => command === 'get_local_game_project_revision',
|
|
),
|
|
).toBeLessThan(
|
|
invoke.mock.calls.findIndex(
|
|
([command]) => command === 'rename_local_project_asset',
|
|
),
|
|
);
|
|
expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', {
|
|
projectPath: PROJECT_PATH,
|
|
commandId: 'asset.list',
|
|
});
|
|
expect(onManifestChange.mock.calls[0]?.[2]).toMatchObject({
|
|
revision: 12,
|
|
source: 'asset-command',
|
|
commitId: 'asset-rename:asset-hero',
|
|
});
|
|
|
|
rendered.rerenderWith(renamedManifest);
|
|
await waitFor(() => {
|
|
const renamedSelect = screen.getByRole('button', {
|
|
name: '选中资源:角色与对象 hero-v2.png',
|
|
});
|
|
const renamedCard = renamedSelect.closest('.game-resource-card');
|
|
// 卡面名称与重命名后的 manifest `localPath` 同步:名称就是正式文件名的投影,
|
|
// 不存在第二份需要一起改的显示名;完整名挂在命中指针的选中按钮 `title` 上。
|
|
const nameNode = renamedCard?.querySelector('.game-resource-card-name');
|
|
expect(nameNode?.textContent).toBe('hero-v2.png');
|
|
expect(renamedSelect.getAttribute('title')).toBe('hero-v2.png');
|
|
});
|
|
});
|
|
|
|
test('reads the project revision before renaming so a stale local value cannot be replayed', async () => {
|
|
installResourceCardIntersectionObserver();
|
|
// 本地 manifest 的 revision 落后(5)而磁盘上已经是 42:提交的必须是读到的 42,
|
|
// 否则后端按「陈旧改写」拒收,用户会看到一次凭空失败的重命名。
|
|
const staleManifest = createManifestWithHero('assets/hero.png');
|
|
const invoke = installInvoke(async (command) => {
|
|
if (command === 'get_local_game_project_revision') {
|
|
return { revision: 42, hasCommittedEdit: true };
|
|
}
|
|
if (command === 'rename_local_project_asset') {
|
|
return {
|
|
asset: createManifestWithHero('assets/hero-v2.png').assets[0],
|
|
previousLocalPath: 'assets/hero.png',
|
|
committedProjectRevision: 43,
|
|
};
|
|
}
|
|
if (command === 'get_local_game_manifest') {
|
|
return createManifestWithHero('assets/hero-v2.png');
|
|
}
|
|
throw new Error(`unexpected command: ${command}`);
|
|
});
|
|
|
|
renderWorkbench(staleManifest);
|
|
const toolbar = await openHeroCard();
|
|
fireEvent.click(within(toolbar).getByRole('button', { name: '重命名' }));
|
|
const field = (await screen.findByLabelText(
|
|
'新文件名',
|
|
)) as HTMLInputElement;
|
|
fireEvent.change(field, { target: { value: 'hero-v2.png' } });
|
|
fireEvent.click(screen.getByRole('button', { name: '确认重命名素材' }));
|
|
|
|
await waitFor(() => {
|
|
expect(invoke).toHaveBeenCalledWith(
|
|
'rename_local_project_asset',
|
|
expect.anything(),
|
|
);
|
|
});
|
|
expect(
|
|
invoke.mock.calls.find(
|
|
([command]) => command === 'rename_local_project_asset',
|
|
)?.[1],
|
|
).toMatchObject({ input: { expectedProjectRevision: 42 } });
|
|
});
|
|
|
|
test('surfaces the project CAS rejection as readable copy instead of the raw code', async () => {
|
|
installResourceCardIntersectionObserver();
|
|
installInvoke(async (command) => {
|
|
if (command === 'get_local_game_project_revision') {
|
|
return { revision: 7, hasCommittedEdit: true };
|
|
}
|
|
if (command === 'rename_local_project_asset') {
|
|
throw 'project-revision-conflict';
|
|
}
|
|
throw new Error(`unexpected command: ${command}`);
|
|
});
|
|
|
|
renderWorkbench(createManifestWithHero('assets/hero.png'));
|
|
const toolbar = await openHeroCard();
|
|
fireEvent.click(within(toolbar).getByRole('button', { name: '重命名' }));
|
|
const field = (await screen.findByLabelText(
|
|
'新文件名',
|
|
)) as HTMLInputElement;
|
|
fireEvent.change(field, { target: { value: 'hero-v2.png' } });
|
|
fireEvent.click(screen.getByRole('button', { name: '确认重命名素材' }));
|
|
|
|
const alert = await screen.findByRole('alert');
|
|
expect(alert.textContent).toBe('项目已被其它操作改动,请刷新后重试');
|
|
expect(alert.textContent).not.toContain('project-revision-conflict');
|
|
});
|
|
|
|
test('keeps the panel open and surfaces the native rejection', async () => {
|
|
installResourceCardIntersectionObserver();
|
|
installInvoke(async (command) => {
|
|
if (command === 'get_local_game_project_revision') {
|
|
return { revision: 7, hasCommittedEdit: true };
|
|
}
|
|
if (command === 'rename_local_project_asset') {
|
|
throw '新文件名非法:扩展名必须与原文件一致';
|
|
}
|
|
throw new Error(`unexpected command: ${command}`);
|
|
});
|
|
|
|
renderWorkbench(createManifestWithHero('assets/hero.png'));
|
|
const toolbar = await openHeroCard();
|
|
fireEvent.click(within(toolbar).getByRole('button', { name: '重命名' }));
|
|
const field = (await screen.findByLabelText(
|
|
'新文件名',
|
|
)) as HTMLInputElement;
|
|
fireEvent.change(field, { target: { value: 'hero.jpg' } });
|
|
fireEvent.click(screen.getByRole('button', { name: '确认重命名素材' }));
|
|
|
|
const alert = await screen.findByRole('alert');
|
|
expect(alert.textContent).toContain('扩展名必须与原文件一致');
|
|
expect(screen.getByLabelText('新文件名')).not.toBeNull();
|
|
});
|
|
});
|