From 2a2e4c1ceaad89c6434a4ca5e90927be44b98915 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Mon, 21 Sep 2026 01:23:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8C=89=20review=20=E4=BF=AE=EF=BC=9A?= =?UTF-8?q?=E5=8A=A8=E7=94=BB=E5=9C=A8=E9=80=94=E6=8B=A6=E6=88=AA=E3=80=81?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E5=AE=88=E5=8D=AB=E6=89=A9=E5=88=B0=E4=B8=89?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=E3=80=81=E7=AA=84=E5=B1=8F=E5=B7=AE=E5=BC=82?= =?UTF-8?q?=E6=98=BE=E5=BC=8F=E6=94=BE=E8=A1=8C=E3=80=81=E7=94=A8=E4=BE=8B?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E4=BE=9D=E8=B5=96=20cwd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在途拦截补齐生成动画:提交记录的「属于哪张资源」从 draftKey 拆成 resourcePath, `resourceCanvasResourceEditSubmissionBlocksResource` 被快速编辑与生成动画共用; 此前动画这条键对不上、拦不住,重开面板再提交会复用同一 operationId 卡在原生锁上, 或(改了提示词)直接多出一笔付费生成 - 样式缺口守卫扩到三块面板(动画面板 / 快速编辑面板 / 生图面板),判据改为按 「选择器分支 + 声明」逐条比对;两个面板的实测缺口一并照抄补上(快速编辑面板的 `overflow: auto → visible` 与窄屏底栏两条按宿主差异显式登记,不静默漂移) - 窄屏只搬动画面板那一支:网页端 ≤760px 会把快速编辑浮层也变成整宽底栏,AGC 的 快速编辑是「居中贴资源卡」的绝对定位,跟随它会压住栏目工具栏与任务侧栏 - 用例路径不再依赖 cwd:新增 tests/repoPath.ts(按 import.meta.url 反推仓库根), 19 个用例文件的 `resolve(process.cwd(), …)` 统一改用它;顺手修掉 `HOST_STYLE_SHEETS.filter(existsSync)` 把仓库相对路径交给 cwd 解析的假红 - 清掉两处死代码:不可达的 `quickEditPanel.status === 'generating'` 判据与 未被使用的 prune 导出 --- .../resource-canvas/resourceCanvasChrome.css | 10 +- .../resourceCanvasResourceEditTaskModel.ts | 32 +-- .../src/view/project-development/index.tsx | 39 ++- .../tests/AgentMessageContent.test.tsx | 10 +- .../tests/ResourceDependencyOverlay.test.ts | 4 +- .../appSurface/project-development.suite.ts | 49 ++-- .../appSurface/runtime-settings.suite.ts | 4 +- .../tests/chatDialogFrameLayout.test.ts | 11 +- ...rojectAssetPickerDialogShellStyle.test.tsx | 13 +- apps/ai-game-creator-shell/tests/repoPath.ts | 23 ++ ...asAssetGenerationTasksSidebarStyle.test.ts | 5 +- .../tests/resourceCanvasCardName.test.tsx | 4 +- ...anvasCharacterAnimationPanelStyle.test.tsx | 250 ++++++++++++------ ...nvasGenerationFloatingPanelChrome.test.tsx | 5 +- ...nvasGenerationTasksSidebarDismiss.test.tsx | 6 +- .../resourceCanvasResourceEditTasks.test.ts | 19 +- .../resourceCardCurrentVersionStyle.test.ts | 5 +- ...esourceCardPreviewAlphaBackground.test.tsx | 11 +- .../resourceClassificationPanel.test.tsx | 12 +- .../tests/resourceTypePanel.test.tsx | 8 +- .../tests/sessionPreview.test.ts | 9 +- .../tests/workbenchThemeContrast.test.ts | 5 +- 22 files changed, 332 insertions(+), 202 deletions(-) create mode 100644 apps/ai-game-creator-shell/tests/repoPath.ts diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasChrome.css b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasChrome.css index cfdb5bb14..68071970e 100644 --- a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasChrome.css +++ b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasChrome.css @@ -1781,8 +1781,14 @@ button.image-canvas-editor__reference-chip:disabled { * 两边再各自漂移。 */ @media (max-width: 760px) { - /* src/index.css:9020 */ - .image-canvas-editor__quick-edit-panel, + /* + * src/index.css:9020 —— **只搬动画面板那一支**。 + * + * 网页端这一条把快速编辑浮层与动画面板一起变成 `position: fixed` 的整宽底栏;AGC 的快速编辑 + * 浮层是「居中贴资源卡」的绝对定位(`transform: translateX(-50%)` + 内联 left/top,见本文件 + * 上面那一族),窄屏宽度已经由 `calc(100vw - 1.5rem)` 自己收好——跟网页端改成底栏会压住栏目 + * 画布左下角工具栏与左侧「生成任务」侧栏。这条宿主差异在样式守卫里显式放行,不静默漂移。 + */ .image-canvas-editor__character-animation-panel { position: fixed; left: 0.75rem !important; diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasResourceEditTaskModel.ts b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasResourceEditTaskModel.ts index 654f70123..cf29486ef 100644 --- a/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasResourceEditTaskModel.ts +++ b/apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasResourceEditTaskModel.ts @@ -33,12 +33,15 @@ export type ResourceCanvasResourceEditSubmission = { /** 入口文案:快速编辑 / 生成动画 / 图片抠图…,由 `pendingResourceEditKindLabel` 出。 */ actionLabel: string; /** - * 快速编辑草稿键(资源路径);其它入口为 `null`。 + * 这笔提交属于哪张资源(资源路径,见 `ProjectResource.path`);无源入口(视频 / 音效 / + * 背景音乐)为 `null`。 * - * 提交期间这一笔草稿不再算「未完成编辑」——用户已经交出去了,恢复入口里不该再多一条 - * 点开就重复提交的条目(见 `resourceCanvasQuickEditModel`)。 + * 两个用途:① 快速编辑提交期间,这张资源的未提交草稿不再算「未完成编辑」——用户已经交出去了, + * 恢复入口里不该再多一条点开就重复提交的条目(见 `resourceCanvasQuickEditModel`);② 同一张 + * 资源还有在途提交时,快速编辑与生成动画都不允许再铸一次身份(见 + * `resourceCanvasResourceEditSubmissionBlocksResource`)。 */ - draftKey: string | null; + resourcePath: string | null; createdAtMillis: number; status: 'running' | 'completed' | 'failed'; error: string | null; @@ -163,14 +166,20 @@ export function resourceCanvasResourceEditSubmissionIsLive( return submission.status === 'running'; } -/** 收口判据:这个草稿键上还有一笔没结束的提交。 */ -export function resourceCanvasResourceEditSubmissionBlocksDraft( +/** + * 「这张资源上还有一笔没结束的提交」。 + * + * 快速编辑(顺带把该资源的草稿从「未完成编辑」里让位)与生成动画共用同一条判据:提交即关面板 + * 之后,「重开面板再提交」是唯一的重试路径,不拦就会出现两条并发提交——提示词没变时第二次会卡在 + * 原生 per-operation 锁上、最后以阶段不可恢复报错;改了提示词则直接多出一笔付费生成。 + */ +export function resourceCanvasResourceEditSubmissionBlocksResource( submissions: readonly ResourceCanvasResourceEditSubmission[], - draftKey: string, + resourcePath: string, ): boolean { return submissions.some( (submission) => - submission.draftKey === draftKey && + submission.resourcePath === resourcePath && resourceCanvasResourceEditSubmissionIsLive(submission), ); } @@ -232,13 +241,6 @@ export function settleResourceCanvasResourceEditSubmission( ); } -/** 已结束的提交记录没有继续留着的价值:只保留在途的那几笔。 */ -export function pruneResourceCanvasResourceEditSubmissions( - submissions: readonly ResourceCanvasResourceEditSubmission[], -): ResourceCanvasResourceEditSubmission[] { - return submissions.filter(resourceCanvasResourceEditSubmissionIsLive); -} - function resourceCanvasResourceEditTaskFromPendingEdit( edit: PendingLocalProjectResourceEdit, ): ResourceCanvasResourceEditTask { diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index 35f1957e6..75c48bd51 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -223,7 +223,7 @@ import { pendingResourceEditKindLabel, RESOURCE_CANVAS_RESOURCE_EDIT_PHASE_LABELS, type ResourceCanvasResourceEditSubmission, - resourceCanvasResourceEditSubmissionBlocksDraft, + resourceCanvasResourceEditSubmissionBlocksResource, resourceCanvasResourceEditTaskIsTerminal, resourceCanvasResourceEditTasks, settleResourceCanvasResourceEditSubmission, @@ -4912,7 +4912,7 @@ export default function ProjectDevelopmentView({ assetName: string; actionLabel: string; prompt: string; - draftKey: string | null; + resourcePath: string | null; }) => { setResourceEditSubmissions((current) => beginResourceCanvasResourceEditSubmission(current, { @@ -7582,7 +7582,7 @@ export default function ProjectDevelopmentView({ canvasResources, ).filter( (entry) => - !resourceCanvasResourceEditSubmissionBlocksDraft( + !resourceCanvasResourceEditSubmissionBlocksResource( resourceEditSubmissions, entry.key, ), @@ -7756,7 +7756,7 @@ export default function ProjectDevelopmentView({ 在跑的这笔结束时(成功或失败)这道门自动放行——失败回落成未完成草稿,用户接着改。 */ if ( - resourceCanvasResourceEditSubmissionBlocksDraft( + resourceCanvasResourceEditSubmissionBlocksResource( resourceEditSubmissionsRef.current, resource.path, ) @@ -7852,7 +7852,7 @@ export default function ProjectDevelopmentView({ assetName: derivedName, actionLabel: '快速编辑', prompt, - draftKey: resource.path, + resourcePath: resource.path, }); try { const { sourceResourceId, sourceAssetId, expectedProjectRevision } = @@ -8086,6 +8086,24 @@ export default function ProjectDevelopmentView({ const actionProject = { projectPath, projectId: manifest.projectId }; const flowId = crypto.randomUUID(); const sourceLayerId = layer.id; + // 与快速编辑共用同一条在途判据:这张素材上还有没结束的提交就不再铸一次身份。 + if ( + resourceCanvasResourceEditSubmissionBlocksResource( + resourceEditSubmissionsRef.current, + resource.path, + ) + ) { + setCharacterAnimationPanel((current) => + current + ? { + ...current, + status: 'failed', + errorMessage: '这张素材正在生成动画,请等它出结果后再提交', + } + : current, + ); + return; + } const requestStoreKey = resourceEditRequestKey( 'character-animation', resource.path, @@ -8116,7 +8134,7 @@ export default function ProjectDevelopmentView({ assetName: derivedName, actionLabel: '生成动画', prompt, - draftKey: null, + resourcePath: resource.path, }); try { const { sourceResourceId, sourceAssetId, expectedProjectRevision } = @@ -8257,7 +8275,8 @@ export default function ProjectDevelopmentView({ assetName: input.assetName, actionLabel: option.generationLabel, prompt: input.prompt, - draftKey: null, + // 无源生成:没有对应的资源卡,因此不参与「同一张资源不许并发提交」那条判据。 + resourcePath: null, }); try { const status = await invoke<{ revision: number }>( @@ -10018,9 +10037,6 @@ export default function ProjectDevelopmentView({ multiline rows={3} placeholder="你希望素材如何修改?" - disabled={ - quickEditPanel.status === 'generating' - } // 润色由下面的 ResourcePromptPolishSlot 承担 // (带资源编辑场景上下文与长度上限), // 不在这里再渲染第二个润色入口。 @@ -10033,9 +10049,6 @@ export default function ProjectDevelopmentView({ subject="图片素材的快速编辑提示词" editKind="image-reference" prompt={quickEditPanel.prompt} - disabled={ - quickEditPanel.status === 'generating' - } applyPrompt={applyResourceQuickEditPrompt} /> } diff --git a/apps/ai-game-creator-shell/tests/AgentMessageContent.test.tsx b/apps/ai-game-creator-shell/tests/AgentMessageContent.test.tsx index 2b32c26e5..604775b68 100644 --- a/apps/ai-game-creator-shell/tests/AgentMessageContent.test.tsx +++ b/apps/ai-game-creator-shell/tests/AgentMessageContent.test.tsx @@ -1,13 +1,12 @@ // @vitest-environment jsdom - import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; import { render } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; import { AgentMessageContent } from '../../../packages/shared/src/components/AgentMessageContent'; import { ChatMarkdownMessage } from '../src/components/ChatMarkdownMessage'; +import { repoPath } from './repoPath'; import { declaration, parseStyleSheet, @@ -15,14 +14,11 @@ import { } from './styleCascade'; const sharedCss = readFileSync( - resolve( - process.cwd(), - 'packages/shared/src/components/AgentMessageContent.css', - ), + repoPath('packages/shared/src/components/AgentMessageContent.css'), 'utf8', ); const appCss = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); const base = '.agent-message-content[data-agent-content]'; diff --git a/apps/ai-game-creator-shell/tests/ResourceDependencyOverlay.test.ts b/apps/ai-game-creator-shell/tests/ResourceDependencyOverlay.test.ts index 58a3edffe..bc73e068c 100644 --- a/apps/ai-game-creator-shell/tests/ResourceDependencyOverlay.test.ts +++ b/apps/ai-game-creator-shell/tests/ResourceDependencyOverlay.test.ts @@ -1,6 +1,5 @@ /** @vitest-environment jsdom */ import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; import { act, @@ -28,6 +27,7 @@ import { ResourceDependencyOverlay, type ResourceDependencyOverlayHandle, } from '../src/view/project-development/ResourceDependencyOverlay'; +import { repoPath } from './repoPath'; function position( resourceId: string, @@ -1030,7 +1030,7 @@ describe('ResourceDependencyOverlay', () => { it('uses a persistent orange with at least 3:1 canvas contrast', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); expect(styles).toMatch( diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index c6a1d2c9e..4e48cd06d 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -25,6 +25,7 @@ import { normalizeProjectResourceGraph } from '../../src/view/project-developmen import { ResourceDependencyOverlay } from '../../src/view/project-development/ResourceDependencyOverlay'; import type { ProjectAgentResultSummary } from '../../src/view/project-development/resourceProjectionModel'; import { projectResourcesFromReadModels } from '../../src/view/project-development/resourceProjectionModel'; +import { repoPath } from '../repoPath'; import { generationPromptText, typeGenerationPrompt, @@ -60,7 +61,6 @@ import { render, renderAppAt, renderLauncherProjectsAt, - resolve, screen, setComposerText, submitChat, @@ -1473,12 +1473,11 @@ export function registerProjectWorkbenchFoundationTests() { it('keeps the resource filter panel above the titlebar band and anchored to the dock', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); const tsxSource = readFileSync( - resolve( - process.cwd(), + repoPath( 'apps/ai-game-creator-shell/src/view/project-development/index.tsx', ), 'utf8', @@ -1556,7 +1555,7 @@ export function registerProjectWorkbenchFoundationTests() { it('keeps the resource overview grid off fit-content so its columns stay responsive', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); // 先剥掉 CSS 注释再取声明体:这条修复的注释里就写着 `width: 100%` 等字样, @@ -1686,7 +1685,7 @@ export function registerProjectWorkbenchFoundationTests() { it('keeps the shared selection overlay colours resolvable from the scene root', () => { const sharedStyles = readFileSync( - resolve(process.cwd(), 'packages/image-canvas-react/src/styles.css'), + repoPath('packages/image-canvas-react/src/styles.css'), 'utf8', ); // 消费端 `border/background: var(...)` 没有 fallback:token 缺失或被写透明,框选就没有颜色。 @@ -1914,12 +1913,11 @@ export function registerProjectWorkbenchFoundationTests() { it('keeps the overview-return button out of the floating notice layer hit region', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); const tsxSource = readFileSync( - resolve( - process.cwd(), + repoPath( 'apps/ai-game-creator-shell/src/view/project-development/index.tsx', ), 'utf8', @@ -3497,7 +3495,7 @@ export function registerProjectWorkbenchFoundationTests() { it('资源卡 chrome 只保留选中按钮与当前版本边框', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); /* @@ -3591,7 +3589,7 @@ export function registerProjectWorkbenchFoundationTests() { it('资源卡的 @ 引用入口只留在选中工具条里', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); // 卡片右上角那个圆钮连同它撑出来的 44×44 热区一起退役:卡片本体只剩 @@ -3599,8 +3597,7 @@ export function registerProjectWorkbenchFoundationTests() { expect(styles).not.toContain('.game-resource-card-reference'); const source = readFileSync( - resolve( - process.cwd(), + repoPath( 'apps/ai-game-creator-shell/src/view/project-development/index.tsx', ), 'utf8', @@ -5855,7 +5852,7 @@ export function registerProjectWorkbenchFoundationTests() { it('keeps the landscape workbench edge-to-edge with internal chat scrolling', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); @@ -6011,12 +6008,11 @@ export function registerProjectWorkbenchFoundationTests() { it('keeps the wallet entry available when the workbench opens the UI editor', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); const projectDevelopmentSource = readFileSync( - resolve( - process.cwd(), + repoPath( 'apps/ai-game-creator-shell/src/view/project-development/index.tsx', ), 'utf8', @@ -6325,7 +6321,7 @@ export function registerProjectWorkbenchFoundationTests() { it('keeps resource sort tab keyboard focus inside the clipped segmented control', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); @@ -6336,7 +6332,7 @@ export function registerProjectWorkbenchFoundationTests() { it('keeps the chat composer an inset block inside the conversation dialog', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); // 对话框就是消息列表那只铺满会话区的盒子;输入区是它内部的一块,不再是贴在它下边、 @@ -6385,7 +6381,7 @@ export function registerProjectWorkbenchFoundationTests() { it('keeps workbench chat bubbles aligned without shrinking process cards', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); const messageListRule = @@ -7097,7 +7093,7 @@ export function registerProjectSupervisorSurfaceTests() { it('anchors the model dropdown to its own trigger instead of the composer box', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); @@ -7157,7 +7153,7 @@ export function registerProjectSupervisorSurfaceTests() { it('bounds the model dropdown height so a long catalog cannot cover the composer', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); const menu = styleRuleBody(styles, '\\.conversation-model-menu'); @@ -11657,7 +11653,7 @@ export function registerProjectAgentStatusTests() { ).not.toBeNull(); // 宿主几何与栏目卡共用同一条规则,视觉上不是另写一套。 const previewStyles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); const previewHostRule = styleRuleBody( @@ -11765,7 +11761,7 @@ export function registerProjectAgentStatusTests() { // 视觉不是另写一套:那一页没有任何专属选择器,卡与宿主都只用共享规则 // (声明级断言——它验的是"没有平行样式",验不到布局本身)。 const allPageStyles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); expect(allPageStyles).not.toMatch(/\.game-resource-all-/); @@ -13394,12 +13390,11 @@ export function registerProjectAgentStatusTests() { it('keeps the bottom toolbar clear of the zoom dock and above the book scene', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); const chromeStyles = readFileSync( - resolve( - process.cwd(), + repoPath( 'apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasChrome.css', ), 'utf8', diff --git a/apps/ai-game-creator-shell/tests/appSurface/runtime-settings.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/runtime-settings.suite.ts index 59084103f..00fa9fe5e 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/runtime-settings.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/runtime-settings.suite.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; -import path from 'node:path'; import { APP_VERSION } from '../../src/app/appMetadata'; +import { repoPath } from '../repoPath'; import { act, createGameCreationAppManifest, @@ -23,7 +23,7 @@ import { export function registerAgentStatusDerivationTests() { it('keeps fixed overlays below the in-page window title bar', () => { const styles = fs.readFileSync( - path.join(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); expect(styles).toContain('--window-chrome-height: 50px;'); diff --git a/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts b/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts index 7ead17c07..81963f43c 100644 --- a/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts +++ b/apps/ai-game-creator-shell/tests/chatDialogFrameLayout.test.ts @@ -1,22 +1,17 @@ // @vitest-environment jsdom - import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; +import { repoPath } from './repoPath'; import { declaration, parseStyleSheet, resolveDeclarations, } from './styleCascade'; -const STYLES_PATH = resolve( - process.cwd(), - 'apps/ai-game-creator-shell/src/styles.css', -); -const VIEW_PATH = resolve( - process.cwd(), +const STYLES_PATH = repoPath('apps/ai-game-creator-shell/src/styles.css'); +const VIEW_PATH = repoPath( 'apps/ai-game-creator-shell/src/features/project-workspace/ProjectSupervisorView.tsx', ); diff --git a/apps/ai-game-creator-shell/tests/projectAssetPickerDialogShellStyle.test.tsx b/apps/ai-game-creator-shell/tests/projectAssetPickerDialogShellStyle.test.tsx index 6c37ac024..821c2b3c1 100644 --- a/apps/ai-game-creator-shell/tests/projectAssetPickerDialogShellStyle.test.tsx +++ b/apps/ai-game-creator-shell/tests/projectAssetPickerDialogShellStyle.test.tsx @@ -7,6 +7,7 @@ import { cleanup, render, screen } from '@testing-library/react'; import { afterEach, describe, expect, it } from 'vitest'; import { ImageCanvasProjectAssetPickerDialog } from '../../../src/components/image-editor/ImageCanvasProjectAssetPickerDialog'; +import { REPO_ROOT } from './repoPath'; /** * 「选择替换素材」弹窗在 AGC 的面板底色契约。 @@ -25,7 +26,6 @@ import { ImageCanvasProjectAssetPickerDialog } from '../../../src/components/ima * 这是**弱验证**(声明级,不是真机观感)。真机判据见文件末尾注释。 */ -const REPO_ROOT = process.cwd(); const AGC_ROOT = resolve(REPO_ROOT, 'apps/ai-game-creator-shell'); const PLATFORM_MODAL_SHELL = '.platform-modal-shell'; @@ -336,9 +336,14 @@ describe('「选择替换素材」弹窗在 AGC 的面板底色', () => { }); it('没有平行拷贝:外壳三条规则只在共享表里定义一次', () => { - const hostRules = HOST_STYLE_SHEETS.filter(existsSync).flatMap((sheet) => - readRules(absoluteSheetPath(sheet)), - ); + /* + 这里必须**先转绝对路径再判断存在**:`filter(existsSync)` 拿到的是仓库相对路径,`existsSync` + 按 `process.cwd()` 解析——从 `apps/ai-game-creator-shell` 跑时四张表全被判成不存在, + 断言退化成「谁都没定义」的假绿/假红(本次就是从这条假红里揪出来的)。 + */ + const hostRules = HOST_STYLE_SHEETS.filter((sheet) => + existsSync(absoluteSheetPath(sheet)), + ).flatMap((sheet) => readRules(absoluteSheetPath(sheet))); for (const className of [ PLATFORM_MODAL_SHELL, PLATFORM_MODAL_BACKDROP, diff --git a/apps/ai-game-creator-shell/tests/repoPath.ts b/apps/ai-game-creator-shell/tests/repoPath.ts new file mode 100644 index 000000000..98b017eaf --- /dev/null +++ b/apps/ai-game-creator-shell/tests/repoPath.ts @@ -0,0 +1,23 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** + * 仓库根与"按仓库相对路径取文件"的唯一入口。 + * + * 读样式表 / 脚本 / 文档的用例原先一律写 `resolve(process.cwd(), 'apps/ai-game-creator-shell/…')`: + * 从仓库根跑(CI 的 `npm run test`、根 `vitest.config.ts`)没问题,但只要从 + * `apps/ai-game-creator-shell` 目录跑一次,路径就会翻成 + * `…/apps/ai-game-creator-shell/apps/ai-game-creator-shell/…` 并整片 ENOENT——看起来像"这些用例本来 + * 就红",实际是路径解析跟着 cwd 漂。这里按 `import.meta.url` 反推仓库根,两种跑法结论一致。 + */ +export const REPO_ROOT = resolve( + dirname(fileURLToPath(import.meta.url)), + '..', + '..', + '..', +); + +/** 传仓库相对路径(如 `apps/ai-game-creator-shell/src/styles.css`)。 */ +export function repoPath(...segments: readonly string[]): string { + return resolve(REPO_ROOT, ...segments); +} diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationTasksSidebarStyle.test.ts b/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationTasksSidebarStyle.test.ts index 87c7f7f4d..d2b3f8b71 100644 --- a/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationTasksSidebarStyle.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceCanvasAssetGenerationTasksSidebarStyle.test.ts @@ -1,9 +1,9 @@ // @vitest-environment jsdom import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; import { describe, expect, test } from 'vitest'; +import { repoPath } from './repoPath'; import { declaration, parseStyleSheet, @@ -17,8 +17,7 @@ import { * jsdom 不加载这个 CSS 文件,所以这里按仓库既有做法(`styleCascade`)直接解析**真实生效的声明**: * 「状态 tone 映射」「等宽数字」「圆角 / 悬停」「过渡」「reduced-motion 关动效」都用声明钉住。 */ -const SIDEBAR_CSS_PATH = resolve( - process.cwd(), +const SIDEBAR_CSS_PATH = repoPath( 'apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasAssetGenerationTasksSidebar.css', ); const rules = parseStyleSheet(readFileSync(SIDEBAR_CSS_PATH, 'utf8')); diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasCardName.test.tsx b/apps/ai-game-creator-shell/tests/resourceCanvasCardName.test.tsx index a11503f21..dad22664f 100644 --- a/apps/ai-game-creator-shell/tests/resourceCanvasCardName.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceCanvasCardName.test.tsx @@ -1,6 +1,5 @@ /** @vitest-environment jsdom */ import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; @@ -17,6 +16,7 @@ import { waitFor, within, } from './appSurface/harness'; +import { repoPath } from './repoPath'; vi.mock('@tauri-apps/api/core', async () => ({ ...(await vi.importActual( @@ -237,7 +237,7 @@ function renderWorkbench(manifest: GameCreationAppManifest) { function stylesSource() { return readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); } diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasCharacterAnimationPanelStyle.test.tsx b/apps/ai-game-creator-shell/tests/resourceCanvasCharacterAnimationPanelStyle.test.tsx index 461cda797..b44611ccd 100644 --- a/apps/ai-game-creator-shell/tests/resourceCanvasCharacterAnimationPanelStyle.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceCanvasCharacterAnimationPanelStyle.test.tsx @@ -12,25 +12,51 @@ import { ImageCanvasCharacterAnimationPanelView } from '../../../src/components/ import type { CanvasLayer, CharacterAnimationPanelState, + QuickEditPanelState, } from '../../../src/components/image-editor/ImageCanvasEditorTypes'; +import { ImageCanvasQuickEditPanelView } from '../../../src/components/image-editor/ImageCanvasQuickEditPanelView'; +import { ResourceCanvasAssetGenerationPanelView } from '../src/features/resource-canvas/ResourceCanvasAssetGenerationPanelView'; +import type { ResourceCanvasAssetToolAction } from '../src/features/resource-canvas/resourceCanvasBottomToolbarModel'; +import { createResourceQuickEditPanelDraft } from '../src/features/resource-canvas/resourceCanvasQuickEditModel'; /** - * 「生成动画」面板在 AGC 的样式覆盖契约。 + * 共享画布面板在 AGC 的样式覆盖契约(动画面板 / 快速编辑面板 / 生图面板)。 * - * 现象(客户端验收现场):资源卡上点「生成动画」,面板只剩通用 composer 外观——素材缩略图、 - * 动作预设、`生成NN泥点` 按钮与关闭键全部落回默认流布局,面板里出现一大块空白、预设竖排。 - * 根因是这套 `image-canvas-editor__character-animation-*` / `__generation-close` / - * `__reference-*` 规则只写在网页端整站表 `src/index.css` 里,而 AGC 是独立宿主。 + * 现象(客户端验收现场,两次):①「生成动画」面板只剩通用 composer 外观——素材缩略图、动作预设、 + * `生成NN泥点` 按钮与关闭键全部落回默认流布局,关闭键还跑到面板正中;② 快速编辑 composer 的同族 + * 样式也缺过。根因是这套 `image-canvas-editor__*` 规则只写在网页端整站表 `src/index.css` 里, + * 而 AGC 是独立宿主,宿主的 `resourceCanvasChrome.css` 只能逐条照抄。 * * jsdom 不计算外部 CSS,所以这里不做"渲染出来看观感",而是把两半分别钉死: - * 1) DOM 契约:面板真的渲染出这些类名(含 `reference-chip--${tone}` 这种运行时拼出来的); - * 2) 样式表契约:从 AGC 入口 `src/main.tsx` 按真实 import 关系解析出"AGC 到底加载了哪些 - * 样式表",断言上面每个类都至少命中一条规则,并抽查几条撑开布局的关键声明。 + * 1) DOM 契约:真渲染出这三块面板(含 `reference-chip--${tone}` 这种运行时拼出来的类); + * 2) 样式表契约:从 AGC 入口 `src/main.tsx` 按真实 import 关系解析出"AGC 到底加载了哪些样式表", + * 逐条比对**网页端命中这份 DOM 的规则(选择器分支 + 声明)AGC 是否都有一条**。 * - * 这是**弱验证**(声明级,不是真机观感)。真机判据:资源卡 → 生成动画 → 缩略图、五个动作 - * 预设一排、按钮落在面板右下角、右上角有关闭键;窄屏(≤720px)面板落到底部整宽、按钮整宽。 + * 比对必须按「选择器分支 + 声明」而不是类名或整串选择器:同名规则在网页端会出现两次 + * (`__generation-close` 先给基础外观、再给浮层内的绝对定位),只搬第一条照样"类名有规则"却跑偏; + * 而 `A, B { … }` 里只命中 A 的那一支,也不该逼宿主连 B 一起搬。 + * + * 这是**弱验证**(声明级,不是真机观感)。真机判据:资源卡 → 生成动画 → 缩略图、动作预设一排、 + * 按钮落在面板右下角、右上角有关闭键;窄屏动画面板落到底部整宽、按钮整宽。 */ +/** + * 显式放行的宿主差异(每条都写明原因,避免"守卫红了就加白名单"式的静默漂移): + * + * 1. 快速编辑浮层**窄屏**:网页端在 `@media (max-width: 760px)` 把它与动画面板一起变成 + * `position: fixed` 的整宽底栏(下一条是配套的 bottom 计算)。AGC 的快速编辑浮层是 + * 「居中贴资源卡」的绝对定位(`transform: translateX(-50%)` + 内联 left/top),窄屏宽度由 + * `calc(100vw - 1.5rem)` 自己收;改成底栏会压住栏目画布左下角工具栏与左侧「生成任务」侧栏。 + * 2. 快速编辑浮层**桌面档**:网页端 `overflow: auto`,AGC 必须 `overflow: visible`——AGC 没有 + * portal,共享组件的下拉弹层就地渲染,面板一旦滚动就会把弹层裁掉(见 `resourceCanvasChrome.css` + * 快速编辑面板那段注释)。 + */ +const ACCEPTED_AGENT_HOST_DIVERGENCES = new Set([ + '.image-canvas-editor__quick-edit-panel { position: fixed; left: 0.75rem !important; right: 0.75rem; top: auto !important; bottom: 0.75rem; width: auto; max-height: min(72vh, 34rem); transform: none; }', + '.image-canvas-editor__quick-edit-panel.image-canvas-editor__generation-composer { width: auto; bottom: calc(9rem + env(safe-area-inset-bottom, 0px)); }', + '.image-canvas-editor__quick-edit-panel { position: absolute; z-index: 14; display: grid; width: min(24rem, calc(100vw - 1.5rem)); max-height: min(32rem, calc(100% - 1.5rem)); gap: 0.58rem; overflow: auto; border: 1px solid rgba(148, 163, 184, 0.36); border-radius: 1.1rem; background: rgba(255, 255, 255, 0.96); padding: 0.72rem; color: #1f2937; box-shadow: 0 22px 48px rgba(15, 23, 42, 0.22); transform: translateX(-50%); }', +]); + const TEST_DIR = dirname(fileURLToPath(import.meta.url)); const AGC_ROOT = resolve(TEST_DIR, '..'); const REPO_ROOT = resolve(AGC_ROOT, '..', '..'); @@ -215,46 +241,149 @@ function animationPanel() { return container; } +function quickEditPanel() { + const sourceLayer: CanvasLayer = { + id: 'layer-q', + resourceId: 'resource-q', + title: 'source-art.png', + src: 'data:image/png;base64,c291cmNl', + x: 0, + y: 0, + width: 128, + height: 128, + originalWidth: 128, + originalHeight: 128, + zIndex: 1, + sourceType: 'uploaded', + }; + function Harness() { + const [panel, setPanel] = useState(() => + createResourceQuickEditPanelDraft(sourceLayer), + ); + return panel ? ( + + ) : null; + } + const { container } = render(); + return container; +} + +function assetGenerationPanel() { + const action: ResourceCanvasAssetToolAction = { + route: 'asset', + audioKind: null, + id: 'generate-image', + label: '生成图片', + assetKind: 'image', + assetName: '素材 生成图片', + promptPlaceholder: '描述要生成什么', + adjustableDimensions: true, + aspectRatio: '1:1', + imageSize: '1K', + requiresIconSpecReference: false, + writesIconSpecReference: false, + }; + const referenceAsset: GameCreationAppAssetManifestEntry = { + id: 'asset-a', + kind: 'image', + mediaType: 'image/png', + localPath: 'assets/素材-a.png', + source: { kind: 'canvas' }, + }; + const { container } = render( + undefined} + />, + ); + return container; +} + afterEach(() => { cleanup(); }); -describe('「生成动画」面板的 AGC 样式覆盖', () => { - it('网页端给了规则的类,AGC 也必须有一条(样式缺口守卫)', () => { - const container = animationPanel(); - const classes = new Set(); - for (const element of container.querySelectorAll('[class]')) { - for (const name of element.classList) { - if (name.startsWith('image-canvas-editor__')) { - classes.add(name); - } - } - } - expect(classes.size).toBeGreaterThan(8); - - /** 判据只认「网页端有、AGC 没有」这一类:两边都没有的类是按设计不带样式(如纯语义 span)。 */ - const agcSelectors = collectAgcLoadedStyleSheets() +/** + * 网页端命中这份 DOM、但 AGC 一条都没有的(选择器分支 + 声明)。 + * + * 比对键是**分支 + 声明**而不是类名或整串选择器:同名规则在网页端会出现两次,只搬第一条也 + * "类名有规则"却跑偏;`A, B { … }` 里只命中 A 的那一支,也不该逼宿主连 B 一起搬。 + */ +function missingWebRulesFor(container: HTMLElement): { + missing: string[]; + matchedCount: number; +} { + const elements = [container, ...Array.from(container.querySelectorAll('*'))]; + const matched = readRules(resolve(REPO_ROOT, 'src/index.css')).flatMap( + (rule) => + rule.selector + .split(',') + .map((selector) => selector.trim().replace(/\s+/gu, ' ')) + .filter( + (selector) => + selector.includes('image-canvas-editor__') && + ruleMatchesPanel(selector, elements), + ) + .map((selector) => `${selector} { ${rule.body} }`), + ); + const agcRuleKeys = new Set( + collectAgcLoadedStyleSheets() .flatMap(readRules) - .map((rule) => rule.selector) - .join('\n'); - const webSelectors = readRules(resolve(REPO_ROOT, 'src/index.css')) - .map((rule) => rule.selector) - .join('\n'); - const webStyled = [...classes].filter((name) => - webSelectors.includes(`.${name}`), - ); - expect( - webStyled.length, - '网页端样式这一次一条都没匹配上:断言退化成空集,守卫失效', - ).toBeGreaterThan(8); - const missing = webStyled.filter( - (name) => !agcSelectors.includes(`.${name}`), - ); - expect( - missing, - `这些类网页端有规则、AGC 一条都没有:${missing.join(', ')}`, - ).toEqual([]); - }); + .flatMap((rule) => + rule.selector + .split(',') + .map( + (selector) => + `${selector.trim().replace(/\s+/gu, ' ')} { ${rule.body} }`, + ), + ), + ); + return { + missing: [ + ...new Set( + matched.filter( + (key) => + !agcRuleKeys.has(key) && !ACCEPTED_AGENT_HOST_DIVERGENCES.has(key), + ), + ), + ], + matchedCount: matched.length, + }; +} + +describe('共享画布面板的 AGC 样式覆盖', () => { + for (const probe of [ + { name: '「生成动画」面板', render: animationPanel }, + { name: '快速编辑面板', render: quickEditPanel }, + { name: '生图(资源生成)面板', render: assetGenerationPanel }, + ]) { + it(`${probe.name}:网页端命中它的每条规则,AGC 都有同名同声明的一条`, () => { + const { missing, matchedCount } = missingWebRulesFor(probe.render()); + expect( + matchedCount, + `${probe.name}一条网页端规则都没匹配上:判据退化成空集,守卫失效`, + ).toBeGreaterThan(0); + expect( + missing, + `${probe.name}缺这些规则(网页端有、AGC 没有):\n${missing.join('\n')}`, + ).toEqual([]); + }); + } it('撑开面板布局的关键声明逐条在位', () => { const rules = collectAgcLoadedStyleSheets().flatMap(readRules); @@ -311,17 +440,12 @@ describe('「生成动画」面板的 AGC 样式覆盖', () => { ).toBe('3.95rem'); }); - it('网页端命中这块面板的每条规则,AGC 都照抄了一条同名规则', () => { + it('判据本身可信:动画面板至少要匹配上十几条网页端规则', () => { const container = animationPanel(); const elements = [ container, ...Array.from(container.querySelectorAll('*')), ]; - - /* - 只守画布组件这一族(`image-canvas-editor__*`):共享 chrome 类(`.platform-button` 等) - 与全局 reset 本来就不归宿主 chrome 表照抄——它们活在共享样式表里,宿主引入与否是另一件事。 - */ const matched = readRules(resolve(REPO_ROOT, 'src/index.css')).filter( (rule) => rule.selector.includes('image-canvas-editor__') && @@ -329,28 +453,6 @@ describe('「生成动画」面板的 AGC 样式覆盖', () => { .split(',') .some((selector) => ruleMatchesPanel(selector, elements)), ); - expect( - matched.length, - '一条规则都没匹配上:判据退化成空集,守卫失效', - ).toBeGreaterThan(10); - - /* - 比对键是**选择器 + 声明**而不是选择器:同名规则在网页端出现两次时(例如 - `.image-canvas-editor__generation-close` 先给基础外观、再给浮层内的绝对定位), - 只按选择器比对会让「只搬了第一条」判成已覆盖——这正是关闭键掉进网格流、跑到面板正中的原因。 - */ - const ruleKey = (rule: CssRule) => `${rule.selector} { ${rule.body} }`; - const agcRuleKeys = new Set( - collectAgcLoadedStyleSheets().flatMap(readRules).map(ruleKey), - ); - const missing = [ - ...new Set( - matched.filter((rule) => !agcRuleKeys.has(ruleKey(rule))).map(ruleKey), - ), - ]; - expect( - missing, - `这些规则网页端会命中动画面板、AGC 一条都没有:\n${missing.join('\n')}`, - ).toEqual([]); + expect(matched.length).toBeGreaterThan(10); }); }); diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasGenerationFloatingPanelChrome.test.tsx b/apps/ai-game-creator-shell/tests/resourceCanvasGenerationFloatingPanelChrome.test.tsx index 5b1dd62e4..6d3a686d3 100644 --- a/apps/ai-game-creator-shell/tests/resourceCanvasGenerationFloatingPanelChrome.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceCanvasGenerationFloatingPanelChrome.test.tsx @@ -1,6 +1,5 @@ // @vitest-environment jsdom import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; import { cleanup, render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -9,6 +8,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest'; import { ResourceCanvasAssetGenerationPanelView } from '../src/features/resource-canvas/ResourceCanvasAssetGenerationPanelView'; import type { ResourceCanvasAssetToolAction } from '../src/features/resource-canvas/resourceCanvasBottomToolbarModel'; import { ResourceCanvasGenerationPanelView } from '../src/features/resource-canvas/ResourceCanvasGenerationPanelView'; +import { repoPath } from './repoPath'; afterEach(cleanup); @@ -112,8 +112,7 @@ describe('生成浮层的面板外观与高度合同', () => { describe('生成浮层样式结构', () => { const panelCss = () => readFileSync( - resolve( - process.cwd(), + repoPath( 'apps/ai-game-creator-shell/src/features/resource-canvas/resourceCanvasGenerationPanel.css', ), 'utf8', diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasGenerationTasksSidebarDismiss.test.tsx b/apps/ai-game-creator-shell/tests/resourceCanvasGenerationTasksSidebarDismiss.test.tsx index 5e94acb39..e4eccc910 100644 --- a/apps/ai-game-creator-shell/tests/resourceCanvasGenerationTasksSidebarDismiss.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceCanvasGenerationTasksSidebarDismiss.test.tsx @@ -1,7 +1,6 @@ /** @vitest-environment jsdom */ import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; @@ -14,6 +13,7 @@ import { screen, waitFor, } from './appSurface/harness'; +import { repoPath } from './repoPath'; /** * 「生成任务」侧栏与工具条入口的三条口径: @@ -144,7 +144,7 @@ describe('「生成任务」侧栏的开合与入口位置', () => { it('「依赖 / 类型」与前一按钮之间的间距跟行内其他按钮一致', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); // 这一行是 gap: 7px 的 flex 容器;分段控件曾靠自带 padding: 3px 与前一按钮只隔 10px, @@ -157,7 +157,7 @@ describe('「生成任务」侧栏的开合与入口位置', () => { it('布局状态提示不参与动作行排版,不会按文案宽度顶开按钮', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); // 它是随保存过程变长的文案(空 →「保存中」→「布局已保存」→ 失败原因)。作为 flex 子项 diff --git a/apps/ai-game-creator-shell/tests/resourceCanvasResourceEditTasks.test.ts b/apps/ai-game-creator-shell/tests/resourceCanvasResourceEditTasks.test.ts index 523757579..fbd252a11 100644 --- a/apps/ai-game-creator-shell/tests/resourceCanvasResourceEditTasks.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceCanvasResourceEditTasks.test.ts @@ -6,7 +6,7 @@ import { pendingResourceEditKindLabel, resourceCanvasResourceEditPhaseLabel, type ResourceCanvasResourceEditSubmission, - resourceCanvasResourceEditSubmissionBlocksDraft, + resourceCanvasResourceEditSubmissionBlocksResource, resourceCanvasResourceEditTaskElapsedMillis, resourceCanvasResourceEditTaskIsTerminal, resourceCanvasResourceEditTasks, @@ -38,7 +38,7 @@ function liveSubmission( prompt: '把嘴改小一点', assetName: 'source-art-编辑版', actionLabel: '快速编辑', - draftKey: 'assets/source-art.png', + resourcePath: 'assets/source-art.png', createdAtMillis: 1_700_000_000_000, status: 'running', error: null, @@ -120,7 +120,7 @@ describe('资源派生任务:本地提交记录', () => { assetName: 'source-art-编辑版', actionLabel: '快速编辑', prompt: '把嘴改小一点', - draftKey: 'assets/source-art.png', + resourcePath: 'assets/source-art.png', createdAtMillis: 1_700_000_000_000, }); expect(begun).toHaveLength(1); @@ -131,7 +131,7 @@ describe('资源派生任务:本地提交记录', () => { assetName: 'source-art-编辑版', actionLabel: '快速编辑', prompt: '把嘴改小一点', - draftKey: 'assets/source-art.png', + resourcePath: 'assets/source-art.png', createdAtMillis: 1_700_000_001_000, }); expect(rebegun).toHaveLength(1); @@ -158,18 +158,21 @@ describe('资源派生任务:本地提交记录', () => { assetName: 'source-art-编辑版', actionLabel: '快速编辑', prompt: '把嘴改小一点', - draftKey: 'assets/source-art.png', + resourcePath: 'assets/source-art.png', createdAtMillis: 1_700_000_000_000, }); expect( - resourceCanvasResourceEditSubmissionBlocksDraft( + resourceCanvasResourceEditSubmissionBlocksResource( live, 'assets/source-art.png', ), ).toBe(true); // 别的素材不受影响,其它入口(没有草稿键)也不受这条判据牵制。 expect( - resourceCanvasResourceEditSubmissionBlocksDraft(live, 'assets/other.png'), + resourceCanvasResourceEditSubmissionBlocksResource( + live, + 'assets/other.png', + ), ).toBe(false); const failed = settleResourceCanvasResourceEditSubmission(live, 'op-1', { @@ -178,7 +181,7 @@ describe('资源派生任务:本地提交记录', () => { finishedAtMillis: 1_700_000_090_000, }); expect( - resourceCanvasResourceEditSubmissionBlocksDraft( + resourceCanvasResourceEditSubmissionBlocksResource( failed, 'assets/source-art.png', ), diff --git a/apps/ai-game-creator-shell/tests/resourceCardCurrentVersionStyle.test.ts b/apps/ai-game-creator-shell/tests/resourceCardCurrentVersionStyle.test.ts index 892b390ce..1bc8f037d 100644 --- a/apps/ai-game-creator-shell/tests/resourceCardCurrentVersionStyle.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceCardCurrentVersionStyle.test.ts @@ -1,8 +1,9 @@ import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; +import { repoPath } from './repoPath'; + /** * 「当前使用」资源卡的发光档案:声明级断言。 * @@ -12,7 +13,7 @@ import { describe, expect, it } from 'vitest'; */ function stylesSource() { return readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); } diff --git a/apps/ai-game-creator-shell/tests/resourceCardPreviewAlphaBackground.test.tsx b/apps/ai-game-creator-shell/tests/resourceCardPreviewAlphaBackground.test.tsx index b611522f5..6a2f1122d 100644 --- a/apps/ai-game-creator-shell/tests/resourceCardPreviewAlphaBackground.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceCardPreviewAlphaBackground.test.tsx @@ -32,11 +32,11 @@ import { React, readFileSync, render, - resolve, screen, vi, waitFor, } from './appSurface/harness'; +import { repoPath } from './repoPath'; import { declaration, parseStyleSheet, @@ -294,10 +294,7 @@ function cardFor(label: string) { */ function visualDeclarationsForCard(card: HTMLElement) { const rules = parseStyleSheet( - readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), - 'utf8', - ), + readFileSync(repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8'), ); const kind = card.getAttribute('data-preview-kind'); const hasAlpha = card.getAttribute('data-preview-has-alpha'); @@ -381,7 +378,7 @@ describe('资源卡棋盘格底按真实 alpha 决定', () => { it('透明图自身不被填底色:没有任何规则给 .game-resource-card-visual > img 声明背景', () => { const rules = parseStyleSheet( readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ), ); @@ -409,7 +406,7 @@ describe('资源卡棋盘格底按真实 alpha 决定', () => { // 这条用例必须同步改成「有判据才铺」。 const rules = parseStyleSheet( readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ), ); diff --git a/apps/ai-game-creator-shell/tests/resourceClassificationPanel.test.tsx b/apps/ai-game-creator-shell/tests/resourceClassificationPanel.test.tsx index eb8aa048c..6a1786ec8 100644 --- a/apps/ai-game-creator-shell/tests/resourceClassificationPanel.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceClassificationPanel.test.tsx @@ -1,6 +1,5 @@ // @vitest-environment jsdom import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; import { cleanup, @@ -15,6 +14,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest'; import type { GameCreationAppAssetManifestEntry } from '../../../packages/shared/src/contracts/gameCreationApp'; import { ResourceClassificationPanel } from '../src/view/project-development/ResourceClassificationPanel'; +import { repoPath } from './repoPath'; import { declaration, parseStyleSheet, @@ -431,8 +431,7 @@ describe('ResourceClassificationPanel 编辑素材标签', () => { test('资源卡工具条入口 label 与面板标题一致,不再是「分类与标签」', () => { const viewSource = readFileSync( - resolve( - process.cwd(), + repoPath( 'apps/ai-game-creator-shell/src/view/project-development/index.tsx', ), 'utf8', @@ -452,8 +451,7 @@ describe('ResourceClassificationPanel 编辑素材标签', () => { test('标签 pill 的删除按钮热区不小于 32px', () => { const panelStyles = readFileSync( - resolve( - process.cwd(), + repoPath( 'apps/ai-game-creator-shell/src/features/project-workspace/resourceClassificationTagPanel.css', ), 'utf8', @@ -606,7 +604,7 @@ describe('ResourceClassificationPanel 不再承载删除素材', () => { */ test('底部只有一个「添加」并靠右下角', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); const rule = styles.match( @@ -641,7 +639,7 @@ describe('ResourceClassificationPanel 不再承载删除素材', () => { */ describe('ResourceClassificationPanel 标签区可滚动', () => { const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), + repoPath('apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); const rules = parseStyleSheet(styles); diff --git a/apps/ai-game-creator-shell/tests/resourceTypePanel.test.tsx b/apps/ai-game-creator-shell/tests/resourceTypePanel.test.tsx index 434cbedde..bcaf34569 100644 --- a/apps/ai-game-creator-shell/tests/resourceTypePanel.test.tsx +++ b/apps/ai-game-creator-shell/tests/resourceTypePanel.test.tsx @@ -1,6 +1,5 @@ // @vitest-environment jsdom import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; import { cleanup, @@ -14,6 +13,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest'; import type { GameCreationAppAssetManifestEntry } from '../../../packages/shared/src/contracts/gameCreationApp'; import { ResourceTypePanel } from '../src/view/project-development/ResourceTypePanel'; +import { repoPath } from './repoPath'; import { declaration, parseStyleSheet, @@ -106,8 +106,7 @@ function optionsList() { function panelStyleSheet() { return parseStyleSheet( readFileSync( - resolve( - process.cwd(), + repoPath( 'apps/ai-game-creator-shell/src/features/project-workspace/resourceTypePanel.css', ), 'utf8', @@ -279,8 +278,7 @@ describe('ResourceTypePanel 设置素材类型', () => { expect(declaration(idle, 'background')).toBe('rgb(255 255 255 / 62%)'); const source = readFileSync( - resolve( - process.cwd(), + repoPath( 'apps/ai-game-creator-shell/src/features/project-workspace/resourceTypePanel.css', ), 'utf8', diff --git a/apps/ai-game-creator-shell/tests/sessionPreview.test.ts b/apps/ai-game-creator-shell/tests/sessionPreview.test.ts index 0f7fc58a0..872fb48d1 100644 --- a/apps/ai-game-creator-shell/tests/sessionPreview.test.ts +++ b/apps/ai-game-creator-shell/tests/sessionPreview.test.ts @@ -1,10 +1,10 @@ import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; import { describe, expect, it, vi } from 'vitest'; import type { GameCreationAppPreviewState } from '../../../packages/shared/src/contracts/gameCreationApp'; import { resolveSessionPreviewOnProjectOpen } from '../src/features/app-shell/sessionPreview'; +import { repoPath } from './repoPath'; const projectPath = '/tmp/session-preview-project'; @@ -172,14 +172,11 @@ describe('进入项目时的预览活体核验', () => { describe('本地预览记录的生命周期(Rust 侧结构性守卫)', () => { const mainSource = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src-tauri/src/main.rs'), + repoPath('apps/ai-game-creator-shell/src-tauri/src/main.rs'), 'utf8', ); const previewSource = readFileSync( - resolve( - process.cwd(), - 'apps/ai-game-creator-shell/src-tauri/src/preview.rs', - ), + repoPath('apps/ai-game-creator-shell/src-tauri/src/preview.rs'), 'utf8', ); diff --git a/apps/ai-game-creator-shell/tests/workbenchThemeContrast.test.ts b/apps/ai-game-creator-shell/tests/workbenchThemeContrast.test.ts index 8968364ec..c54b50f16 100644 --- a/apps/ai-game-creator-shell/tests/workbenchThemeContrast.test.ts +++ b/apps/ai-game-creator-shell/tests/workbenchThemeContrast.test.ts @@ -1,11 +1,12 @@ import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; +import { repoPath } from './repoPath'; + type Rgba = readonly [number, number, number, number]; -const themePath = resolve(process.cwd(), 'packages/shared/src/theme.css'); +const themePath = repoPath('packages/shared/src/theme.css'); function getCssBlock(source: string, selector: string) { const selectorIndex = source.indexOf(selector);