恢复被 #193 误删的策划前端全部样式,并加上组件到样式的守门测试 #195

Merged
kdletters merged 6 commits from fix/design_frontend into master 2026-08-25 18:45:18 +08:00
3 changed files with 608 additions and 18 deletions
File diff suppressed because it is too large Load Diff
@@ -3,6 +3,7 @@ import type { ProjectSupervisorComponentProps } from '../../src/features/app-she
import { WorkspaceLauncherShell } from '../../src/features/app-shell/WorkspaceLauncher';
import {
act,
agentRuntimeUserInputRequest,
App,
cleanup,
createGameCreationAppManifest,
@@ -1611,6 +1612,93 @@ export function registerHomeProjectCreationTests() {
).toHaveLength(1);
},
);
it('surfaces the planning clarification card after 做方案 creates the project from home', async () => {
// 上面那条只断言到「run 起来了、source 对」。真实故障恰好落在它之后:plan 根 run
// 停在 waiting-for-user-input 并带回澄清请求,而工作台一直停在前端本地的占位文案,
// 澄清卡永远不出现。策划链路现有用例全部走「打开已有项目 + 直接注入 initialRuntime」,
// 正好绕开首页自动建项目这条路,所以这个缺口一直没人守。
const projectPath = '/tmp/home-planning-clarification';
const manifest = createGameCreationAppManifest(
'local-project-draft',
'home-planning-clarification',
);
const supervisorHarness = createProjectSupervisorRuntimeHarness({
projectPath,
expectedRunProfile: 'standard',
});
let planRootRunId = '';
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'pick_local_project_directory') {
return projectPath;
}
if (command === 'is_local_project_directory_non_empty') {
return false;
}
if (command === 'create_automatic_local_game_project') {
return {
projectPath,
manifestPath: `${projectPath}/.agent/manifest.json`,
manifest,
};
}
if (command === 'start_game_creator_supervisor_runtime_task') {
planRootRunId = String(args?.runId ?? '');
}
const result = await supervisorHarness.invoke(command, args);
if (command !== 'read_game_creator_agent_runtime' || !planRootRunId) {
return result;
}
// 后端此刻的真实形态:pending 是 user.input_request,读命令把澄清请求投影在
// 结果的**顶层**`AgentRuntimeResult.user_input_request`,与 `state` 平级),
// 前端的 agentRuntimeStateFromResult 也优先读顶层。放进 state 会被顶层的 null
// 盖掉,那是 fixture 写错,不是产品缺陷。
const runtimeResult = result as { state: Record<string, unknown> };
return {
...runtimeResult,
state: {
...runtimeResult.state,
status: 'waiting-for-user-input',
phase: 'waiting-for-user-input',
},
userInputRequest: agentRuntimeUserInputRequest({
agentId: 'project-supervisor',
sessionId: supervisorHarness.sessionId,
runId: planRootRunId,
requestId: 'request-plan-round-1',
actionId: 'action-plan-round-1',
}),
};
},
);
window.__TAURI__ = {
core: { invoke },
event: { listen: supervisorHarness.listen },
};
renderLauncherAt('/?launcher', 'home', true);
fireEvent.click(screen.getByRole('button', { name: '做方案' }));
const promptInput = screen.getByLabelText('创作想法');
nativeClipboardMock.text = '2D射击游戏';
fireEvent.paste(promptInput);
await waitFor(() => {
expect(promptInput.textContent).toContain('2D射击游戏');
});
fireEvent.keyDown(promptInput, { key: 'Enter', code: 'Enter' });
await waitFor(() => {
expect(invoke).toHaveBeenCalledWith(
'start_game_creator_supervisor_runtime_task',
expect.objectContaining({ source: PROJECT_SUPERVISOR_PLAN_SOURCE }),
);
});
const strip = await screen.findByLabelText('立项策划运行状态');
expect(
within(strip).getByText('首版角色规范图采用哪种美术方向?'),
).not.toBeNull();
});
it('refreshes Direct Codex art commits while the turn is still running and after a later failure', async () => {
const projectPath =
'C:\\Users\\tester\\Documents\\Genarrative GameAgent\\live-direct-art';
@@ -1,3 +1,6 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import {
agentRuntimeUserInputRequest,
createPlanGddStateView,
@@ -521,4 +524,91 @@ export function registerPlanGddApprovalTests() {
expectSupervisorRuntimePanelAbsent();
expect(screen.queryByText('策划子 Run 退出')).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<string>();
for (const match of componentSources.matchAll(
/className=(?:"([^"]+)"|\{`([^`]+)`\})/g,
)) {
// 条件类长在插值里:`plan-gdd-surface${showCard ? ' …--with-card' : ''}`。
lhk229 marked this conversation as resolved Outdated
Outdated
Review

[P2] 这个类名守门会漏掉条件类并产生子串误报。这里把模板中的 ${...} 整段替换为空,因此 plan-gdd-surface--with-card 永远不会进入 referencedClasses;后面的 styles.includes(. + name) 也不能证明存在该类的精确 selector。当前删除 .plan-gdd-surface--with-card .plan-gdd-stage-progress 的唯一规则时,本测试仍可通过。请显式覆盖条件类,并用 CSS selector AST/精确 selector 匹配,或按状态渲染后做 computed-style/布局断言;补一个删除该唯一规则就会失败的负向回归。

[P2] 这个类名守门会漏掉条件类并产生子串误报。这里把模板中的 `${...}` 整段替换为空,因此 `plan-gdd-surface--with-card` 永远不会进入 `referencedClasses`;后面的 `styles.includes(`.` + name)` 也不能证明存在该类的精确 selector。当前删除 `.plan-gdd-surface--with-card .plan-gdd-stage-progress` 的唯一规则时,本测试仍可通过。请显式覆盖条件类,并用 CSS selector AST/精确 selector 匹配,或按状态渲染后做 computed-style/布局断言;补一个删除该唯一规则就会失败的负向回归。
Outdated
Review

已修复

已修复
// 把 `${…}` 整段丢掉等于把它们排除在守门之外,而它们恰恰是最容易被顺手删干净
// 的一档——`--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,
);
});
}