diff --git a/apps/ai-game-creator-shell/tests/appSurface.test.ts b/apps/ai-game-creator-shell/tests/appSurface.test.ts index 51f42dffe..726c7cf1e 100644 --- a/apps/ai-game-creator-shell/tests/appSurface.test.ts +++ b/apps/ai-game-creator-shell/tests/appSurface.test.ts @@ -14,7 +14,6 @@ import { registerHomeProjectCreationTests, registerRecentProjectsTests, } from './appSurface/home.suite'; -import { registerPlanGddApprovalTests } from './appSurface/plan-gdd.suite'; import { registerCanvasAssetTests, registerProjectAssetTests, @@ -72,6 +71,5 @@ describe('AI 游戏创作 App 界面边界', () => { registerProjectAssetTests(); registerAgentRuntimeCommandTests(); registerCanvasAssetTests(); - registerPlanGddApprovalTests(); registerDesignAgentSurfaceTests(); }); diff --git a/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts deleted file mode 100644 index 510d73029..000000000 --- a/apps/ai-game-creator-shell/tests/appSurface/plan-gdd.suite.ts +++ /dev/null @@ -1,823 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; - -import { PlanGddStageProgress } from '../../src/features/project-workspace/GddApprovalCard'; -import { - act, - App, - createPlanGddStateView, - createProjectSupervisorRuntimeHarness, - expect, - fireEvent, - it, - React, - render, - screen, - vi, - waitFor, - within, -} from './harness'; - -function mountFormalSupervisor( - harness: ReturnType, - planningStartMode = false, -) { - window.history.pushState({}, '', '/'); - render( - React.createElement(App, { - initialProjectPath: harness.projectPath, - orchestrationMode: 'single-supervisor', - planningStartMode, - projectSupervisorOnly: true, - }), - ); -} - -function planningV2ApprovalResult() { - const state = createPlanGddStateView(); - const displayGdd = state.displayGdd; - const session = state.session; - if (!displayGdd || !session) { - throw new Error('fixture 应当带 GDD 和 session'); - } - return planningV2ResultFor( - session, - displayGdd, - 'awaiting_approval', - 'ready_for_approval', - ); -} - -function planningV2ResultFor( - session: NonNullable, - displayGdd: NonNullable, - sessionStatus: string, - artifactStatus: string, -) { - return { - session: { - schemaVersion: 'planning-session.v2', - engine: 'planning-session-v2', - sessionId: session.sessionId, - projectId: 'local-project-draft', - mode: 'gdd', - status: sessionStatus, - turnIndex: 3, - questionCount: session.clarificationRound, - questionLimit: 8, - revisionCount: 0, - currentArtifactVersion: displayGdd.version, - currentQuestion: null, - capabilities: { tools: [], skills: [] }, - processingSeconds: session.accumulatedAgentMillis / 1000, - createdAtUtc: displayGdd.createdAtUtc, - updatedAtUtc: displayGdd.createdAtUtc, - lastError: null, - }, - result: null, - currentArtifact: { - artifactId: displayGdd.gddId, - kind: 'gdd', - version: displayGdd.version, - status: artifactStatus, - fingerprint: displayGdd.fingerprint, - payload: { - schemaVersion: 'plan-gdd.v2', - projectId: displayGdd.projectId, - gddId: displayGdd.gddId, - version: displayGdd.version, - createdAtUtc: displayGdd.createdAtUtc, - game: displayGdd.game, - decisions: displayGdd.decisions, - prototypeValidationItems: displayGdd.prototypeValidationItems, - fingerprint: displayGdd.fingerprint, - }, - }, - replayed: false, - conversation: [], - }; -} - -/** 已批准:审批卡整张收掉,只剩标题栏和它下面的交付行。 */ -function planningV2ApprovedResult() { - const state = createPlanGddStateView(); - const displayGdd = state.displayGdd; - const session = state.session; - if (!displayGdd || !session) { - throw new Error('fixture 应当带 GDD 和 session'); - } - return planningV2ResultFor(session, displayGdd, 'approved', 'approved'); -} - -/** 策划中、尚未提交 GDD 的状态:没有待审批,也没有可展示的 GDD。 */ -function planningV2WorkingResult(questionCount = 2) { - const questionResult = planningV2QuestionResult(); - return { - ...questionResult, - session: { - ...questionResult.session, - status: 'planning', - questionCount, - currentQuestion: null, - }, - result: null, - }; -} - -function planningV2QuestionResult() { - return { - session: { - schemaVersion: 'planning-session.v2', - engine: 'planning-session-v2', - sessionId: 'plan-session-v2-question', - projectId: 'local-project-draft', - mode: 'gdd', - status: 'awaiting_user', - turnIndex: 1, - questionCount: 1, - questionLimit: 8, - revisionCount: 0, - currentArtifactVersion: null, - currentQuestion: { - id: 'core_loop', - header: '当前要决定:核心循环', - question: '玩家在一局中主要反复做什么?', - options: [ - { label: '持续闪避', description: '保持移动并躲避来袭弹幕。' }, - { label: '规划路线', description: '观察弹幕并选择安全路线。' }, - ], - }, - capabilities: { tools: [], skills: [] }, - processingSeconds: 1.2, - createdAtUtc: '2026-09-03T00:00:00Z', - updatedAtUtc: '2026-09-03T00:00:01Z', - lastError: null, - }, - result: { - schemaVersion: 'planning-turn-result.v2', - kind: 'question', - payload: { - question: { - id: 'core_loop', - header: '当前要决定:核心循环', - question: '玩家在一局中主要反复做什么?', - options: [ - { label: '持续闪避', description: '保持移动并躲避来袭弹幕。' }, - { label: '规划路线', description: '观察弹幕并选择安全路线。' }, - ], - }, - }, - }, - currentArtifact: null, - replayed: false, - }; -} - -async function mountApprovalCard( - harness: ReturnType, - planningStartMode = false, -) { - window.__TAURI__ = { - core: { invoke: harness.invoke }, - event: { listen: harness.listen }, - }; - mountFormalSupervisor(harness, planningStartMode); - return await screen.findByLabelText('GDD 审批卡'); -} - -async function mountPlanningSurface( - harness: ReturnType, - planningStartMode = false, -) { - window.__TAURI__ = { - core: { invoke: harness.invoke }, - event: { listen: harness.listen }, - }; - mountFormalSupervisor(harness, planningStartMode); - return await screen.findByLabelText('立项策划阶段进度'); -} - -type PlanGddStateView = ReturnType; - -/** 已批准:审批卡整张收掉,只剩标题栏和它下面的交付行。 */ -function approvedPlanGddState() { - const base = createPlanGddStateView(); - const gddRef = base.versions[0]!.gddRef; - return createPlanGddStateView({ - state: 'approved', - pendingApproval: null, - approvedGddRef: gddRef, - versions: [ - { - ...base.versions[0]!, - status: 'approved', - decision: { - action: 'approve' as const, - decidedAtUtc: '2026-08-18T01:00:00Z', - }, - }, - ], - session: base.session - ? { ...base.session, phase: 'approved', awaitingAnswerFor: null } - : null, - }); -} - -/** - * 完整总控面板在策划链路下漏给用户的几块:面板本体、currentAction/计划进度、 - * 五段式紧凑进度、子 Agent 列表。策划链路的断言统一从这里查。 - */ -function expectSupervisorRuntimePanelAbsent() { - expect(screen.queryByLabelText('项目总控 Agent 状态')).toBeNull(); - expect(screen.queryByLabelText('当前工作状态')).toBeNull(); - expect(screen.queryByLabelText('项目总控 Agent 进度')).toBeNull(); - expect(screen.queryByLabelText('专业 Agent 实时状态')).toBeNull(); - expect(screen.queryByText(/专业 Agent 协作:/)).toBeNull(); -} - -function openReviseDialog() { - fireEvent.click(screen.getByRole('button', { name: '修改' })); - return screen.getByRole('dialog'); -} - -function typeComment(dialog: HTMLElement, text: string) { - const textarea = within(dialog).getByPlaceholderText('请输入原因'); - fireEvent.change(textarea, { target: { value: text } }); - return textarea as HTMLTextAreaElement; -} - -export function registerPlanGddApprovalTests() { - it('ignores a late hydrate from the previously opened project', async () => { - const message = (text: string) => ({ - role: 'assistant', - kind: 'assistant_text', - messageId: text, - atUtc: '2026-09-08T00:00:00Z', - text, - payload: { text }, - }); - const harness = createProjectSupervisorRuntimeHarness({ - planningV2Result: { - ...planningV2ApprovalResult(), - conversation: [message('当前项目的策划记录')], - }, - }); - window.__TAURI__ = { - core: { invoke: harness.invoke }, - event: { listen: harness.listen }, - }; - window.history.pushState({}, '', '/?dev'); - render(React.createElement(App, { planningStartMode: true })); - const openProject = async (path: string) => { - fireEvent.change(screen.getByLabelText('本地项目目录'), { - target: { value: path }, - }); - fireEvent.click(screen.getByRole('button', { name: '初始化' })); - const pending = screen - .getByText(`创建 ${path}`) - .closest('.pending-command'); - await act(async () => { - fireEvent.click( - within(pending as HTMLElement).getByRole('button', { name: '确认' }), - ); - }); - }; - await openProject('/tmp/planning-a'); - const originalInvoke = harness.invoke.getMockImplementation()!; - let finishOldHydrate!: (result: unknown) => void; - harness.invoke.mockImplementation(async (command, args) => { - if ( - command === 'hydrate_planning_session_v2' && - args?.projectPath === '/tmp/planning-a' - ) { - return new Promise((resolve) => { - finishOldHydrate = resolve; - }); - } - return originalInvoke(command, args); - }); - fireEvent.focus(window); - await waitFor(() => expect(finishOldHydrate).toBeTypeOf('function')); - await openProject('/tmp/planning-b'); - expect(screen.getByText('当前项目的策划记录')).not.toBeNull(); - await act(async () => { - finishOldHydrate({ - ...planningV2ApprovalResult(), - conversation: [message('旧项目的迟到策划')], - }); - }); - expect(screen.queryByText('旧项目的迟到策划')).toBeNull(); - expect(screen.getByText('当前项目的策划记录')).not.toBeNull(); - }); - - it.each(['question', 'empty'] as const)( - 'ignores a late %s hydrate after a newer hydrate has restored the GDD', - async (lateResult) => { - const harness = createProjectSupervisorRuntimeHarness({ - planningV2Result: planningV2ApprovalResult(), - }); - await mountApprovalCard(harness, true); - const originalInvoke = harness.invoke.getMockImplementation()!; - let finishOldHydrate!: (result: unknown) => void; - let deferNextHydrate = true; - harness.invoke.mockImplementation(async (command, args) => { - if (command === 'hydrate_planning_session_v2' && deferNextHydrate) { - deferNextHydrate = false; - return new Promise((resolve) => { - finishOldHydrate = resolve; - }); - } - return originalInvoke(command, args); - }); - - fireEvent.focus(window); - await waitFor(() => expect(finishOldHydrate).toBeTypeOf('function')); - await act(async () => { - fireEvent.focus(window); - }); - expect(screen.getByLabelText('GDD 审批卡')).not.toBeNull(); - - await act(async () => { - finishOldHydrate( - lateResult === 'question' ? planningV2QuestionResult() : null, - ); - }); - expect(screen.getByLabelText('GDD 审批卡')).not.toBeNull(); - expect(screen.queryByText('玩家在一局中主要反复做什么?')).toBeNull(); - }, - ); - - it('re-hydrates authority after a failed GDD decision and keeps the failure visible', async () => { - const harness = createProjectSupervisorRuntimeHarness({ - planningV2Result: planningV2ApprovalResult(), - }); - await mountApprovalCard(harness); - - const hydrateCallsBeforeDecision = harness.invoke.mock.calls.filter( - ([command]) => command === 'hydrate_planning_session_v2', - ).length; - harness.failNextPlanningV2Decision('PLAN_STALE_APPROVAL'); - - fireEvent.click(screen.getByRole('button', { name: '批准 v1' })); - - // 失败分支必须重灌权威状态,否则卡片会停在已失效的 pending 身份上。 - await waitFor(() => { - expect( - harness.invoke.mock.calls.filter( - ([command]) => command === 'hydrate_planning_session_v2', - ).length, - ).toBeGreaterThan(hydrateCallsBeforeDecision); - }); - // 而且重灌不能把决定失败的原因擦掉:hydrate 入口会 setPlanGddError(null), - // 两句顺序写反这条断言就红。 - expect(screen.getByRole('alert').textContent).toContain( - 'PLAN_STALE_APPROVAL', - ); - }); - - it('reuses one decisionId while the decision is unchanged and mints a new one once the comment changes', async () => { - const harness = createProjectSupervisorRuntimeHarness({ - planningV2Result: planningV2ApprovalResult(), - }); - await mountApprovalCard(harness); - - const dialog = openReviseDialog(); - typeComment(dialog, '把核心循环压到三步'); - await waitFor(() => { - expect( - within(dialog).getByRole('button', { name: '提交决定' }), - ).toHaveProperty('disabled', false); - }); - harness.failNextPlanningV2Decision('PLAN_DURABILITY_FAILED'); - fireEvent.click(screen.getByRole('button', { name: '提交决定' })); - await waitFor(() => { - expect(harness.planningV2DecisionCalls).toHaveLength(1); - }); - - // 原样重试:属于方案 §13.2 的 busy/超时/网络重试,必须复用同一 decisionId。 - harness.failNextPlanningV2Decision('PLAN_DURABILITY_FAILED'); - await waitFor(() => { - expect( - within(dialog).getByRole('button', { name: '提交决定' }), - ).toHaveProperty('disabled', false); - }); - fireEvent.click(screen.getByRole('button', { name: '提交决定' })); - await waitFor(() => { - expect(harness.planningV2DecisionCalls).toHaveLength(2); - }); - expect(harness.planningV2DecisionCalls[1].decisionId).toBe( - harness.planningV2DecisionCalls[0].decisionId, - ); - - // 改写修改意见:审批意图变了,必须换新的 decisionId,否则后端会以 - // 「同 decisionId 的审批意图不一致」硬拒,用户改写后的原因永远落不了盘。 - typeComment(dialog, '把核心循环压到两步,并去掉天气系统'); - harness.failNextPlanningV2Decision('PLAN_DURABILITY_FAILED'); - await waitFor(() => { - expect( - within(dialog).getByRole('button', { name: '提交决定' }), - ).toHaveProperty('disabled', false); - }); - fireEvent.click(screen.getByRole('button', { name: '提交决定' })); - await waitFor(() => { - expect(harness.planningV2DecisionCalls).toHaveLength(3); - }); - expect(harness.planningV2DecisionCalls[2].decisionId).not.toBe( - harness.planningV2DecisionCalls[0].decisionId, - ); - expect(harness.planningV2DecisionCalls[2].comment).toBe( - '把核心循环压到两步,并去掉天气系统', - ); - }); - - it('labels the clarification round the user is actually on rather than the 0-indexed answered count', async () => { - // questionCount 是「已展示问题数」:等待第 3 轮回答时它恒为 3,当前轮次 - // 由 currentQuestion 钉住。直接渲染成「轮次 2/8」会暗示还剩一轮。 - const questionResult = planningV2QuestionResult(); - const harness = createProjectSupervisorRuntimeHarness({ - planningV2Result: { - ...questionResult, - session: { - ...questionResult.session, - questionCount: 3, - }, - result: null, - }, - }); - await mountPlanningSurface(harness); - - const progress = await screen.findByLabelText('立项策划阶段进度'); - expect(within(progress).getByText('第 3 轮 / 共 8 轮')).not.toBeNull(); - expect(within(progress).queryByText(/轮次 \d+\/\d+/)).toBeNull(); - }); - - it('falls back to an answered-count label when no clarification answer is outstanding', async () => { - // currentQuestion 为 null(含恢复态)时不去猜当前是第几轮,只报已完成多少轮。 - const harness = createProjectSupervisorRuntimeHarness({ - planningV2Result: planningV2WorkingResult(2), - }); - await mountPlanningSurface(harness); - - const progress = await screen.findByLabelText('立项策划阶段进度'); - expect(within(progress).getByText('已完成 2/8 轮澄清')).not.toBeNull(); - }); - - it('paints the planning state once in the stage strip and keeps the fingerprint inside the GDD dialog', async () => { - // 阶段进度和审批卡曾各自带框叠在一起,「立项策划 / 待审批」在标题栏和卡头各画一遍, - // 卡头还露一截指纹。合成一个面之后:状态与版本只在标题栏,卡头只剩游戏标题和一句 - // 话,指纹整条放进正文弹层做追溯。 - const result = planningV2ApprovalResult(); - const harness = createProjectSupervisorRuntimeHarness({ - planningV2Result: result, - }); - const card = await mountApprovalCard(harness); - - const surface = screen.getByLabelText('立项策划'); - const strip = within(surface).getByLabelText('立项策划阶段进度'); - expect(within(strip).getByText('待审批')).not.toBeNull(); - expect(within(strip).getByText('当前版本:v1')).not.toBeNull(); - expect(surface.contains(card)).toBe(true); - - expect(within(card).queryByText('立项策划')).toBeNull(); - expect(within(card).queryByText('待审批')).toBeNull(); - expect(within(card).queryByText(/^版本 v1$/)).toBeNull(); - expect(within(card).queryByText(/指纹/)).toBeNull(); - expect(within(card).getByRole('heading', { level: 2 }).textContent).toBe( - result.currentArtifact.payload.game.title, - ); - - fireEvent.click( - within(card).getByRole('button', { name: '查看 GDD 正文' }), - ); - const dialog = screen.getByRole('dialog'); - expect(within(dialog).getByLabelText('GDD 版本与指纹').textContent).toBe( - `Fast GDD v1 · ${result.currentArtifact.fingerprint}`, - ); - }); - - it('hands the approved GDD back through the stage strip instead of a card', async () => { - // 批准之后审批卡按设计整张收掉,此前那一刻起用户就再也够不到 GDD:approvedGddRef - // 前端没人读,渲染好的 Markdown 也没有出口。交付行补的就是这个缺口——不额外占一 - // 张卡,只在标题栏下多一行:文件在哪、看正文、用外部程序打开。 - const harness = createProjectSupervisorRuntimeHarness({ - planningV2Result: planningV2ApprovedResult(), - }); - await mountPlanningSurface(harness); - - expect(screen.queryByLabelText('GDD 审批卡')).toBeNull(); - const delivery = await screen.findByLabelText('GDD 交付'); - expect(delivery.textContent).toContain( - `${harness.projectPath}/game/fast_gdd.md`, - ); - expect(screen.queryByRole('button', { name: /^批准/ })).toBeNull(); - expect(screen.queryByRole('button', { name: '修改' })).toBeNull(); - expect(screen.queryByRole('button', { name: '退回重做' })).toBeNull(); - - // 正文弹层与审批时是同一份,批准前后看到的内容一致。 - fireEvent.click( - within(delivery).getByRole('button', { name: '查看 GDD 正文' }), - ); - const dialog = screen.getByRole('dialog'); - expect(within(dialog).getByLabelText('GDD 版本与指纹')).not.toBeNull(); - }); - - it('asks the shell to open the rendered GDD for the current project', async () => { - const harness = createProjectSupervisorRuntimeHarness({ - planningV2Result: planningV2ApprovedResult(), - }); - await mountPlanningSurface(harness); - - const delivery = await screen.findByLabelText('GDD 交付'); - fireEvent.click(within(delivery).getByRole('button', { name: '打开文件' })); - - await waitFor(() => { - expect( - harness.invoke.mock.calls.filter( - ([command]) => command === 'open_local_project_plan_gdd_markdown', - ), - ).toHaveLength(1); - }); - const [, args] = harness.invoke.mock.calls.find( - ([command]) => command === 'open_local_project_plan_gdd_markdown', - )!; - // 路径交给后端自己在项目根下解析,前端只报项目——避免两侧各拼一次相对路径。 - expect(args).toEqual({ projectPath: harness.projectPath }); - }); - - it('offers to make an approved GDD into a game reference on the home entry', async () => { - const onMakeGame = vi.fn(async () => undefined); - render( - React.createElement(PlanGddStageProgress, { - state: approvedPlanGddState(), - active: true, - projectPath: '/tmp/approved-gdd-project', - onMakeGame, - }), - ); - - fireEvent.click(screen.getByRole('button', { name: '做成游戏' })); - - await waitFor(() => { - expect(onMakeGame).toHaveBeenCalledTimes(1); - }); - }); - - it('keeps the supervisor runtime panel off the planning lane while the planner is working', async () => { - // 完整面板是给做游戏链路的:十几个专业 Agent、多步计划、逐 Agent 重试。策划链路 - // 只有一个策划会话,面板画出来的全是内部记账。状态由顶部的阶段进度承担; - // 底部在策划正常进行时什么都不该画。 - const harness = createProjectSupervisorRuntimeHarness({ - planningV2Result: planningV2WorkingResult(), - }); - const progress = await mountPlanningSurface(harness); - - expect(within(progress).getByText('策划中')).not.toBeNull(); - expectSupervisorRuntimePanelAbsent(); - expect(screen.queryByLabelText('立项策划运行状态')).toBeNull(); - expect(screen.queryByText(/计划 \d+\/\d+/)).toBeNull(); - }); - - it('still surfaces the clarification card on the planning lane', async () => { - // 澄清卡是 V2 策划链路唯一需要用户动手的交互面之一。 - const harness = createProjectSupervisorRuntimeHarness({ - planningV2Result: planningV2QuestionResult(), - }); - await mountPlanningSurface(harness, true); - - const strip = await screen.findByLabelText('立项策划运行状态'); - expect(within(strip).getByLabelText('Needs input')).not.toBeNull(); - expect( - within(strip).getByText('玩家在一局中主要反复做什么?'), - ).not.toBeNull(); - expectSupervisorRuntimePanelAbsent(); - expect(screen.queryByText(/agent\.delegate/)).toBeNull(); - }); - - it('surfaces a V2 provider failure on the planning lane', async () => { - // V2 provider_failed 由同一策划会话承接,错误显示在窄条中,用户可用输入框重新提交。 - const harness = createProjectSupervisorRuntimeHarness({ - planningV2Result: { - ...planningV2QuestionResult(), - session: { - ...planningV2QuestionResult().session, - status: 'provider_failed', - currentQuestion: null, - lastError: { - code: 'PROVIDER_FAILED', - summary: 'Planning V2 Provider 调用失败', - }, - }, - result: { - schemaVersion: 'planning-turn-result.v2', - kind: 'error', - payload: { - code: 'PROVIDER_FAILED', - summary: 'Planning V2 Provider 调用失败', - }, - }, - }, - }); - await mountPlanningSurface(harness, true); - - const strip = await screen.findByLabelText('立项策划运行状态'); - expect(within(strip).getByRole('alert').textContent).toContain('执行失败'); - expectSupervisorRuntimePanelAbsent(); - expect(screen.queryByText(/project-planning/)).toBeNull(); - }); - - it('keeps a stylesheet rule for every class the planning components reference', () => { - // #193「Codex/agent chat layout fix」重写聊天样式时,把策划前端的选择器整段 - // 误删:组件(TSX)原样保留、样式全数消失,审批卡/阶段条/交付行裸奔,正文弹层 - // 失去 fixed 定位变成内联平铺。组件和它的样式分居两个文件,重构样式的人看不见 - // 使用方——这条测试就是那根缺失的连线:类名清单直接从组件源码里推导,组件加了 - // 新类而样式没跟上、或样式又被顺手清掉,这里都会红。 - const componentSources = [ - 'src/features/project-workspace/GddApprovalCard.tsx', - 'src/features/project-workspace/PlanningLaneRuntimeStrip.tsx', - ] - .map((path) => - readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell', path), - 'utf8', - ), - ) - .join('\n'); - const referencedClasses = new Set(); - for (const match of componentSources.matchAll( - /className=(?:"([^"]+)"|\{`([^`]+)`\})/g, - )) { - // 条件类长在插值里:`plan-gdd-surface${showCard ? ' …--with-card' : ''}`。 - // 把 `${…}` 整段丢掉等于把它们排除在守门之外,而它们恰恰是最容易被顺手删干净 - // 的一档——`--with-card` 挂着审批卡的行模板,没有它卡片底部会被外壳的 - // `overflow: hidden` 切掉。只取插值里的字符串字面量:三元的条件、变量名都不是 - // 类名,不能混进清单。 - const literal = (match[1] ?? match[2] ?? '').replace( - /\$\{([^}]*)\}/g, - (_whole, expression: string) => - [...expression.matchAll(/'([^']*)'|"([^"]*)"/g)] - .map((piece) => piece[1] ?? piece[2] ?? '') - .join(' '), - ); - for (const name of literal.split(/\s+/)) { - if (name) { - referencedClasses.add(name); - } - } - } - expect(referencedClasses.size).toBeGreaterThan(10); - const styles = readFileSync( - resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), - 'utf8', - ); - for (const name of referencedClasses) { - // 子串匹配会把 `.gdd-approval-card__header` 当成 `.gdd-approval-card` 的证据: - // 前缀类的规则被删光、只剩派生类时这里照样绿。要求类名后面不能再跟类名字符, - // 才是「存在这个类的精确 selector」。 - const exactSelector = new RegExp( - `\\.${name.replace(/[^\w-]/g, '\\$&')}(?![\\w-])`, - ); - expect( - exactSelector.test(styles), - `styles.css 缺少 .${name} 的精确 selector`, - ).toBe(true); - } - // 正文弹层必须是浮层:backdrop 一旦丢掉 fixed 定位,整个 GDD 会内联平铺进 - // 消息流里——这正是误删当时最刺眼的症状。 - expect(styles).toMatch( - /\.gdd-approval-card__dialog-backdrop\s*\{[^}]*position:\s*fixed/s, - ); - // 做方案的单栏工作台不在上面两个组件文件里(类名由 - // view/project-development/index.tsx 拼出),显式钉住:没有这两条规则时, - // 策划项目一打开就是左边一整片空资源画布。它们和策划样式死在 #193 同一刀里。 - expect(styles).toMatch( - /\.game-workbench-layout\.is-conversation-only\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\)/s, - ); - expect(styles).toMatch( - /\.game-workbench-layout\.is-conversation-only \.game-workbench-stage\s*\{[^}]*display:\s*none/s, - ); - // 工作台里的会话列是 `overflow: hidden` 的定高列。策划链路比另外两条多出窄条 - // (澄清卡 / 失败恢复),只有把这一列排成 flex 列、窄条不参与压缩,它才落在可视 - // 区里;否则组件照常渲染却被静默裁到线外,屏幕上什么都没有。命中判据必须包含窄条 - // 自身——只认 `.plan-gdd-surface` 时,`planGddState` 还没 hydrate 出来的那一格里 - // 澄清卡照样被裁。 - expect(styles).toMatch( - /\.game-workbench-chat\s+\.project-supervisor-conversation:has\([^)]*\.planning-lane-runtime-strip[^)]*\)\s*\{[^}]*display:\s*flex/s, - ); - // `--with-card` 有两条规则,上面的存在性检查只要还剩一条就绿。承重的是这一条: - // 策划面是 `display: grid` + `overflow: hidden` 的外壳,审批卡的 `max-height` - // 和内滚要靠这个行模板才有边界。只删它、留下那条分隔线,表现是批准后的交付行 - // 连同路径和两个按钮被切在壳外。 - expect(styles).toMatch( - /\.game-workbench-chat\s+\.plan-gdd-surface--with-card\s*\{[^}]*grid-template-rows:\s*auto minmax\(0, 1fr\)/s, - ); - }); - - it('routes a formal planning entry through Runtime V2 commands', async () => { - const harness = createProjectSupervisorRuntimeHarness({ - planningV2Result: planningV2ApprovalResult(), - }); - window.__TAURI__ = { - core: { invoke: harness.invoke }, - event: { listen: harness.listen }, - }; - window.history.pushState({}, '', '/'); - render( - React.createElement(App, { - initialProjectPath: harness.projectPath, - orchestrationMode: 'single-supervisor', - planningStartMode: true, - projectSupervisorOnly: true, - }), - ); - - await screen.findByLabelText('GDD 审批卡'); - const planningHydrateCall = harness.invoke.mock.calls.find( - ([command]) => command === 'hydrate_planning_session_v2', - ); - expect(planningHydrateCall).toBeDefined(); - fireEvent.click(screen.getByRole('button', { name: '批准 v1' })); - await waitFor(() => { - expect( - harness.invoke.mock.calls.some( - ([command]) => command === 'decide_planning_artifact_v2', - ), - ).toBe(true); - }); - }); - - it('starts a new planning entry through the design agent', async () => { - const harness = createProjectSupervisorRuntimeHarness({ - designAgentContinueView: { - session: { - sessionId: 'design-session-1', - projectId: 'local-project-draft', - currentPhase: 'concept', - approvedPhases: [], - pendingApproval: null, - pendingClarification: { - requestId: 'clarify-1', - question: '玩家在一局中主要反复做什么?', - options: ['持续闪避', '站桩输出'], - createdAt: 1, - }, - turnIndex: 1, - lastError: null, - }, - messages: [ - { - id: 'u1', - role: 'user', - text: '做一个2D弹幕射击游戏', - }, - { - id: 'a1', - role: 'assistant', - text: '先确认核心循环。', - }, - ], - running: false, - canRetry: false, - }, - }); - window.__TAURI__ = { - core: { invoke: harness.invoke }, - event: { listen: harness.listen }, - }; - window.history.pushState({}, '', '/'); - render( - React.createElement(App, { - initialProjectPath: harness.projectPath, - initialSupervisorMessage: '做一个2D弹幕射击游戏', - orchestrationMode: 'single-supervisor', - planningStartMode: true, - projectSupervisorOnly: true, - }), - ); - - await screen.findByText('玩家在一局中主要反复做什么?'); - expect( - harness.invoke.mock.calls.some( - ([command]) => command === 'hydrate_design_agent_session', - ), - ).toBe(true); - expect( - harness.invoke.mock.calls.some( - ([command]) => command === 'start_planning_session_v2', - ), - ).toBe(false); - expect(screen.queryByText(/agent\.delegate/)).toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '持续闪避' })); - await waitFor(() => { - expect(harness.invoke).toHaveBeenCalledWith( - 'continue_design_agent_session', - expect.objectContaining({ - input: expect.objectContaining({ - type: 'clarification', - requestId: 'clarify-1', - optionIndex: 0, - }), - }), - ); - }); - }); -}