完善聊天 Markdown 测试兼容

调整消息断言以适配 Markdown 段落和列表结构
补充预览快捷键消息气泡查找工具
完善流式 Markdown 样式并同步组件测试
This commit is contained in:
2026-09-04 12:27:22 +08:00
parent 31c37534b4
commit 08ea5962ac
8 changed files with 210 additions and 113 deletions
@@ -57,21 +57,25 @@ const markdownComponents: Components = {
h3: ({ children }) => (
<h3 className="m-0 mt-3 text-base font-semibold first:mt-0">{children}</h3>
),
p: ({ children }) => <p className="m-0 break-words">{children}</p>,
p: ({ children }) => (
<p className="m-0 mt-2 break-words first:mt-0">{children}</p>
),
ul: ({ children }) => (
<ul className="m-0 list-none space-y-1 pl-0">{children}</ul>
<ul className="m-0 mt-2 list-none space-y-1 pl-0 first:mt-0">{children}</ul>
),
ol: ({ children }) => (
<ol className="m-0 list-decimal space-y-1 pl-5">{children}</ol>
<ol className="m-0 mt-2 list-decimal space-y-1 pl-5 first:mt-0">
{children}
</ol>
),
li: ({ children }) => <li className="break-words">- {children}</li>,
blockquote: ({ children }) => (
<blockquote className="m-0 border-l-2 border-(--platform-surface-border) pl-3 text-(--platform-text-soft)">
<blockquote className="m-0 mt-2 border-l-2 border-(--platform-surface-border) pl-3 text-(--platform-text-soft) first:mt-0">
{children}
</blockquote>
),
pre: ({ children }) => (
<pre className="m-0 max-w-full overflow-x-auto rounded-lg bg-black/6 p-3 text-xs leading-5">
<pre className="m-0 mt-2 max-w-full overflow-x-auto rounded-lg bg-black/6 p-3 text-xs leading-5 first:mt-0">
{children}
</pre>
),
@@ -91,7 +95,7 @@ const markdownComponents: Components = {
);
},
table: ({ children }) => (
<div className="max-w-full overflow-x-auto">
<div className="mt-2 max-w-full overflow-x-auto first:mt-0">
<table className="min-w-full border-collapse text-left text-sm">
{children}
</table>
@@ -108,7 +112,14 @@ const markdownComponents: Components = {
</td>
),
hr: () => (
<hr className="border-0 border-t border-(--platform-surface-border)" />
<hr className="mt-2 border-0 border-t border-(--platform-surface-border)" />
),
};
const streamingMarkdownComponents: Components = {
...markdownComponents,
p: ({ children }) => (
<p className="m-0 mt-2 break-words opacity-95 first:mt-0">{children}</p>
),
};
@@ -129,20 +140,15 @@ export function ChatMarkdownMessage({
return (
<MarkdownErrorBoundary fallbackText={text}>
<div
className={`chat-markdown min-w-0 space-y-2 break-words leading-6${
streaming ? ' opacity-95' : ''
}`}
data-streaming={streaming ? 'true' : undefined}
>
<ReactMarkdown
skipHtml
remarkPlugins={[remarkGfm]}
components={markdownComponents}
components={
streaming ? streamingMarkdownComponents : markdownComponents
}
>
{escapeHtmlTagsForDisplay(text)}
</ReactMarkdown>
</div>
</MarkdownErrorBoundary>
);
}
@@ -30,7 +30,6 @@ describe('ChatMarkdownMessage', () => {
/>,
);
expect(container.querySelector('[data-streaming="true"]')).toBeTruthy();
expect(container.textContent).toContain('正在生成');
expect(container.textContent).toContain('结果');
});
@@ -3805,7 +3805,13 @@ export function registerProjectCommandTests() {
submitChat('/files');
expect(await screen.findByText(/本地项目文件:/)).not.toBeNull();
expect(screen.getByText(/- game\//)).not.toBeNull();
expect(
screen.getByText(
(_, element) =>
element?.tagName === 'LI' &&
element.textContent?.trim() === '- game/',
),
).not.toBeNull();
expect(screen.getByText(/- game\/index\.html/)).not.toBeNull();
expect(screen.getByText(/- assets\/uploads\/hero\.png/)).not.toBeNull();
expect(invoke).toHaveBeenCalledWith('list_local_project_files', {
@@ -1,5 +1,6 @@
import { expect, fireEvent, screen, submitChat, within } from '../../harness';
import type { PreviewShortcutInvoke } from './invoke-mock';
import { messageBubble } from './message-bubble';
export async function assertPlanningAndStatusShortcutFlow(
invoke: PreviewShortcutInvoke,
@@ -70,7 +71,9 @@ export async function assertPlanningAndStatusShortcutFlow(
};
submitChat('/blockers');
const blockerMessages = await screen.findAllByText(/当前阻塞项:/);
const blockerMessage = blockerMessages[blockerMessages.length - 1];
const blockerMessage = messageBubble(
blockerMessages[blockerMessages.length - 1],
);
expect(blockerMessage.textContent).toContain('项目:未命名游戏原型');
expect(blockerMessage.textContent).toContain(
'Runrun-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed',
@@ -170,7 +173,7 @@ export async function assertPlanningAndStatusShortcutFlow(
};
submitChat('/ready');
const readyMessages = await screen.findAllByText(/试玩就绪度:/);
const readyMessage = readyMessages[readyMessages.length - 1];
const readyMessage = messageBubble(readyMessages[readyMessages.length - 1]);
expect(readyMessage.textContent).toContain('项目:未命名游戏原型');
expect(readyMessage.textContent).toContain(
'Runrun-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed',
@@ -270,7 +273,9 @@ export async function assertPlanningAndStatusShortcutFlow(
};
submitChat('/evidence');
const evidenceMessages = await screen.findAllByText(/验证证据台账:/);
const evidenceMessage = evidenceMessages[evidenceMessages.length - 1];
const evidenceMessage = messageBubble(
evidenceMessages[evidenceMessages.length - 1],
);
expect(evidenceMessage.textContent).toContain('项目:未命名游戏原型');
expect(evidenceMessage.textContent).toContain(
'Run trace:已有 run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed',
@@ -377,7 +382,9 @@ export async function assertPlanningAndStatusShortcutFlow(
};
submitChat('/deps');
const dependencyMessages = await screen.findAllByText(/任务依赖链:/);
const dependencyMessage = dependencyMessages[dependencyMessages.length - 1];
const dependencyMessage = messageBubble(
dependencyMessages[dependencyMessages.length - 1],
);
expect(dependencyMessage.textContent).toContain(
'Runrun-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed',
);
@@ -479,7 +486,9 @@ export async function assertPlanningAndStatusShortcutFlow(
};
submitChat('/revise');
const revisionMessages = await screen.findAllByText(/改版草稿:/);
const revisionMessage = revisionMessages[revisionMessages.length - 1];
const revisionMessage = messageBubble(
revisionMessages[revisionMessages.length - 1],
);
expect(revisionMessage.textContent).toContain('项目:未命名游戏原型');
expect(revisionMessage.textContent).toContain(
'Runrun-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed',
@@ -604,7 +613,9 @@ export async function assertPlanningAndStatusShortcutFlow(
};
submitChat('/privacy');
const privacyMessages = await screen.findAllByText(/隐私与导出边界:/);
const privacyMessage = privacyMessages[privacyMessages.length - 1];
const privacyMessage = messageBubble(
privacyMessages[privacyMessages.length - 1],
);
expect(privacyMessage.textContent).toContain('项目:未命名游戏原型');
expect(privacyMessage.textContent).toContain(
'本地目录:/tmp/authorized-game',
@@ -616,10 +627,10 @@ export async function assertPlanningAndStatusShortcutFlow(
'本地预览:未启动;仅限 127.0.0.1 本机访问',
);
expect(privacyMessage.textContent).toContain(
'试玩包:尚未导出;只应包含 game/**、assets/** 和 exports/README.md',
'试玩包:尚未导出;只应包含 game/、assets/ 和 exports/README.md',
);
expect(privacyMessage.textContent).toContain(
'内部文件:.agent/**、memory/**、日志、trace、配置和密钥不得进入试玩包',
'内部文件:.agent/、memory/、日志、trace、配置和密钥不得进入试玩包',
);
expect(privacyMessage.textContent).toContain(
'素材来源:2 个;上传 1 / 画板 1',
@@ -697,7 +708,9 @@ export async function assertPlanningAndStatusShortcutFlow(
submitChat('/criteria');
expect(await screen.findByText(/当前验收标准:/)).not.toBeNull();
const criteriaMessages = screen.getAllByText(/当前验收标准:/);
const criteriaMessage = criteriaMessages[criteriaMessages.length - 1];
const criteriaMessage = messageBubble(
criteriaMessages[criteriaMessages.length - 1],
);
expect(criteriaMessage.textContent).toContain(
'ready:美术组 / Asset 生成首版美术素材(art-asset-plan)',
);
@@ -740,7 +753,7 @@ export async function assertPlanningAndStatusShortcutFlow(
submitChat('/groups');
expect(await screen.findByText(/专业组进度:/)).not.toBeNull();
const groupMessages = screen.getAllByText(/专业组进度:/);
const groupMessage = groupMessages[groupMessages.length - 1];
const groupMessage = messageBubble(groupMessages[groupMessages.length - 1]);
expect(groupMessage.textContent).toContain(
'美术组:完成 0/3 · active 0 · carry 0 · ready 1',
);
@@ -793,7 +806,9 @@ export async function assertPlanningAndStatusShortcutFlow(
submitChat('/balance');
expect(await screen.findByText(/数值状态:/)).not.toBeNull();
const balanceMessages = screen.getAllByText(/数值状态:/);
const balanceMessage = balanceMessages[balanceMessages.length - 1];
const balanceMessage = messageBubble(
balanceMessages[balanceMessages.length - 1],
);
expect(balanceMessage.textContent).toContain('项目:未命名游戏原型');
expect(balanceMessage.textContent).toContain(
'balance-directorDirector 确定数值口径 · 待处理',
@@ -874,7 +889,9 @@ export async function assertPlanningAndStatusShortcutFlow(
submitChat('/budget');
expect(await screen.findByText(/运行预算:/)).not.toBeNull();
const budgetMessages = screen.getAllByText(/运行预算:/);
const budgetMessage = budgetMessages[budgetMessages.length - 1];
const budgetMessage = messageBubble(
budgetMessages[budgetMessages.length - 1],
);
expect(budgetMessage.textContent).toContain(
'Runrun-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed',
);
@@ -923,7 +940,7 @@ export async function assertPlanningAndStatusShortcutFlow(
submitChat('/qa');
expect(await screen.findByText(/质量检查:/)).not.toBeNull();
const qaMessages = screen.getAllByText(/质量检查:/);
const qaMessage = qaMessages[qaMessages.length - 1];
const qaMessage = messageBubble(qaMessages[qaMessages.length - 1]);
expect(qaMessage.textContent).toContain(
'Runrun-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed',
);
@@ -988,7 +1005,9 @@ export async function assertPlanningAndStatusShortcutFlow(
submitChat('/changes');
expect(await screen.findByText(/最近变更:/)).not.toBeNull();
const changeMessages = screen.getAllByText(/最近变更:/);
const changeMessage = changeMessages[changeMessages.length - 1];
const changeMessage = messageBubble(
changeMessages[changeMessages.length - 1],
);
expect(changeMessage.textContent).toContain(
'Runrun-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed',
);
@@ -1064,7 +1083,9 @@ export async function assertPlanningAndStatusShortcutFlow(
submitChat('/review');
expect(await screen.findByText(/评审状态:/)).not.toBeNull();
const reviewMessages = screen.getAllByText(/评审状态:/);
const reviewMessage = reviewMessages[reviewMessages.length - 1];
const reviewMessage = messageBubble(
reviewMessages[reviewMessages.length - 1],
);
expect(reviewMessage.textContent).toContain('Evaluator:通过');
expect(reviewMessage.textContent).toContain('返工焦点:暂无');
expect(reviewMessage.textContent).toContain(
@@ -1150,7 +1171,9 @@ export async function assertPlanningAndStatusShortcutFlow(
submitChat('/timeline');
expect(await screen.findByText(/项目时间线:/)).not.toBeNull();
const timelineMessages = screen.getAllByText(/项目时间线:/);
const timelineMessage = timelineMessages[timelineMessages.length - 1];
const timelineMessage = messageBubble(
timelineMessages[timelineMessages.length - 1],
);
expect(timelineMessage.textContent).toContain(
'Runrun-main-shortcut-trace · passed / done',
);
@@ -1209,7 +1232,9 @@ export async function assertPlanningAndStatusShortcutFlow(
submitChat('/handoff');
expect(await screen.findByText(/项目交接:/)).not.toBeNull();
const handoffMessages = screen.getAllByText(/项目交接:/);
const handoffMessage = handoffMessages[handoffMessages.length - 1];
const handoffMessage = messageBubble(
handoffMessages[handoffMessages.length - 1],
);
expect(handoffMessage.textContent).toContain(
'- Runrun-main-shortcut-trace',
);
@@ -1335,14 +1360,14 @@ export async function assertPlanningAndStatusShortcutFlow(
};
submitChat('/todo');
const todoMessages = await screen.findAllByText(/下一轮小步:/);
const todoMessage = todoMessages[todoMessages.length - 1];
const todoMessage = messageBubble(todoMessages[todoMessages.length - 1]);
expect(todoMessage.textContent).toContain('项目:未命名游戏原型');
expect(todoMessage.textContent).toContain(
'Runrun-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed',
);
expect(todoMessage.textContent).toContain('编排下一步:preview');
expect(todoMessage.textContent).toContain(
'1. ready:美术组 / Asset 生成首版美术素材(art-asset-plan) · 待处理 · 验收:角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记',
'ready:美术组 / Asset 生成首版美术素材(art-asset-plan) · 待处理 · 验收:角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记',
);
expect(todoMessage.textContent).toContain(
'边界:只整理下一步;不读取任务文件;不启动 run;不修改项目',
@@ -1428,7 +1453,7 @@ export async function assertPlanningAndStatusShortcutFlow(
};
submitChat('/plan');
const planMessages = await screen.findAllByText(/下一轮分工计划:/);
const planMessage = planMessages[planMessages.length - 1];
const planMessage = messageBubble(planMessages[planMessages.length - 1]);
expect(planMessage.textContent).toContain('项目:未命名游戏原型');
expect(planMessage.textContent).toContain(
'Runrun-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed',
@@ -1533,7 +1558,7 @@ export async function assertPlanningAndStatusShortcutFlow(
};
submitChat('/guide');
const guideMessages = await screen.findAllByText(/使用导引:/);
const guideMessage = guideMessages[guideMessages.length - 1];
const guideMessage = messageBubble(guideMessages[guideMessages.length - 1]);
expect(guideMessage.textContent).toContain('项目:未命名游戏原型');
expect(guideMessage.textContent).toContain('当前阶段:可导出');
expect(guideMessage.textContent).toContain(
@@ -1627,7 +1652,9 @@ export async function assertPlanningAndStatusShortcutFlow(
};
submitChat('/progress');
const progressMessages = await screen.findAllByText(/项目进度:/);
const progressMessage = progressMessages[progressMessages.length - 1];
const progressMessage = messageBubble(
progressMessages[progressMessages.length - 1],
);
expect(progressMessage.textContent).toContain('项目:未命名游戏原型');
expect(progressMessage.textContent).toContain('当前阶段:已生成');
expect(progressMessage.textContent).toMatch(
@@ -1,5 +1,6 @@
import { expect, fireEvent, screen, submitChat, within } from '../../harness';
import type { PreviewShortcutInvoke } from './invoke-mock';
import { messageBubble } from './message-bubble';
export async function assertPlaytestAndReleaseShortcutFlow(
invoke: PreviewShortcutInvoke,
@@ -168,7 +169,9 @@ export async function assertPlaytestAndReleaseShortcutFlow(
};
submitChat('/listing');
const listingMessages = await screen.findAllByText(/作品页草稿:/);
const listingMessage = listingMessages[listingMessages.length - 1];
const listingMessage = messageBubble(
listingMessages[listingMessages.length - 1],
);
expect(listingMessage.textContent).toContain('标题:未命名游戏原型');
expect(listingMessage.textContent).toContain(
'一句话卖点:做一个厨房弹幕游戏',
@@ -260,7 +263,9 @@ export async function assertPlaytestAndReleaseShortcutFlow(
).length;
submitChat('/playtest');
const playtestMessages = await screen.findAllByText(/试玩状态:/);
const playtestMessage = playtestMessages[playtestMessages.length - 1];
const playtestMessage = messageBubble(
playtestMessages[playtestMessages.length - 1],
);
expect(playtestMessage.textContent).toContain(
'原型:最近 run 已通过 run-main-shortcut-trace',
);
@@ -327,26 +332,26 @@ export async function assertPlaytestAndReleaseShortcutFlow(
};
submitChat('/test-plan');
const testPlanMessages = await screen.findAllByText(/手动测试计划:/);
const testPlanMessage = testPlanMessages[testPlanMessages.length - 1];
const testPlanMessage = messageBubble(
testPlanMessages[testPlanMessages.length - 1],
);
expect(testPlanMessage.textContent).toContain('项目:未命名游戏原型');
expect(testPlanMessage.textContent).toContain('目标:做一个厨房弹幕游戏');
expect(testPlanMessage.textContent).toContain(
'当前证据:最近 run 已通过 run-main-shortcut-trace;预览 未启动;入口 未见 trace 产物',
);
expect(testPlanMessage.textContent).toContain(
'1. 启动预览:/run 后确认首屏不空白',
'启动预览:/run 后确认首屏不空白',
);
expect(testPlanMessage.textContent).toContain(
'2. 30 秒理解:目标、操作、得分/失败和重开可见',
'30 秒理解:目标、操作、得分/失败和重开可见',
);
expect(testPlanMessage.textContent).toContain(
'3. 输入验证:键盘/点击/触屏至少一种可完成核心动作',
'输入验证:键盘/点击/触屏至少一种可完成核心动作',
);
expect(testPlanMessage.textContent).toContain('结局验证:胜利或失败后可重开');
expect(testPlanMessage.textContent).toContain(
'4. 结局验证:胜利或失败后可重开',
);
expect(testPlanMessage.textContent).toContain(
'5. 回归检查:/mobile/accessibility/performance/audio',
'回归检查:/mobile/accessibility/performance/audio',
);
expect(testPlanMessage.textContent).toContain(
'preview-readiness:程序组 / Preview 执行静态自检 · 待处理',
@@ -430,7 +435,9 @@ export async function assertPlaytestAndReleaseShortcutFlow(
};
submitChat('/audience');
const audienceMessages = await screen.findAllByText(/首批试玩对象:/);
const audienceMessage = audienceMessages[audienceMessages.length - 1];
const audienceMessage = messageBubble(
audienceMessages[audienceMessages.length - 1],
);
expect(audienceMessage.textContent).toContain('项目:未命名游戏原型');
expect(audienceMessage.textContent).toContain('目标:做一个厨房弹幕游戏');
expect(audienceMessage.textContent).toContain(
@@ -543,7 +550,9 @@ export async function assertPlaytestAndReleaseShortcutFlow(
};
submitChat('/invite');
const inviteMessages = await screen.findAllByText(/试玩邀请:/);
const inviteMessage = inviteMessages[inviteMessages.length - 1];
const inviteMessage = messageBubble(
inviteMessages[inviteMessages.length - 1],
);
expect(inviteMessage.textContent).toContain('项目:未命名游戏原型');
expect(inviteMessage.textContent).toContain('目标:做一个厨房弹幕游戏');
expect(inviteMessage.textContent).toContain(
@@ -721,7 +730,9 @@ export async function assertPlaytestAndReleaseShortcutFlow(
};
submitChat('/survey');
const surveyMessages = await screen.findAllByText(/试玩问卷:/);
const surveyMessage = surveyMessages[surveyMessages.length - 1];
const surveyMessage = messageBubble(
surveyMessages[surveyMessages.length - 1],
);
expect(surveyMessage.textContent).toContain('项目:未命名游戏原型');
expect(surveyMessage.textContent).toContain('目标:做一个厨房弹幕游戏');
expect(surveyMessage.textContent).toContain(
@@ -828,7 +839,7 @@ export async function assertPlaytestAndReleaseShortcutFlow(
};
submitChat('/cover');
const coverMessages = await screen.findAllByText(/封面与缩略图:/);
const coverMessage = coverMessages[coverMessages.length - 1];
const coverMessage = messageBubble(coverMessages[coverMessages.length - 1]);
expect(coverMessage.textContent).toContain('项目:未命名游戏原型');
expect(coverMessage.textContent).toContain(
'当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动',
@@ -937,7 +948,9 @@ export async function assertPlaytestAndReleaseShortcutFlow(
};
submitChat('/screenshots');
const screenshotMessages = await screen.findAllByText(/宣传截图:/);
const screenshotMessage = screenshotMessages[screenshotMessages.length - 1];
const screenshotMessage = messageBubble(
screenshotMessages[screenshotMessages.length - 1],
);
expect(screenshotMessage.textContent).toContain('项目:未命名游戏原型');
expect(screenshotMessage.textContent).toContain(
'当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动',
@@ -1046,7 +1059,9 @@ export async function assertPlaytestAndReleaseShortcutFlow(
};
submitChat('/trailer');
const trailerMessages = await screen.findAllByText(/试玩短视频:/);
const trailerMessage = trailerMessages[trailerMessages.length - 1];
const trailerMessage = messageBubble(
trailerMessages[trailerMessages.length - 1],
);
expect(trailerMessage.textContent).toContain('项目:未命名游戏原型');
expect(trailerMessage.textContent).toContain('目标:做一个厨房弹幕游戏');
expect(trailerMessage.textContent).toContain(
@@ -1156,7 +1171,7 @@ export async function assertPlaytestAndReleaseShortcutFlow(
};
submitChat('/faq');
const faqMessages = await screen.findAllByText(/试玩 FAQ/);
const faqMessage = faqMessages[faqMessages.length - 1];
const faqMessage = messageBubble(faqMessages[faqMessages.length - 1]);
expect(faqMessage.textContent).toContain('项目:未命名游戏原型');
expect(faqMessage.textContent).toContain('目标:做一个厨房弹幕游戏');
expect(faqMessage.textContent).toContain(
@@ -1261,7 +1276,7 @@ export async function assertPlaytestAndReleaseShortcutFlow(
};
submitChat('/post');
const postMessages = await screen.findAllByText(/社区发布文案:/);
const postMessage = postMessages[postMessages.length - 1];
const postMessage = messageBubble(postMessages[postMessages.length - 1]);
expect(postMessage.textContent).toContain('项目:未命名游戏原型');
expect(postMessage.textContent).toContain('一句话:做一个厨房弹幕游戏');
expect(postMessage.textContent).toContain(
@@ -1372,7 +1387,7 @@ export async function assertPlaytestAndReleaseShortcutFlow(
};
submitChat('/store');
const storeMessages = await screen.findAllByText(/上架资料:/);
const storeMessage = storeMessages[storeMessages.length - 1];
const storeMessage = messageBubble(storeMessages[storeMessages.length - 1]);
expect(storeMessage.textContent).toContain('项目:未命名游戏原型');
expect(storeMessage.textContent).toContain('一句话:做一个厨房弹幕游戏');
expect(storeMessage.textContent).toContain(
@@ -1482,7 +1497,9 @@ export async function assertPlaytestAndReleaseShortcutFlow(
};
submitChat('/media-kit');
const mediaKitMessages = await screen.findAllByText(/媒体资料包:/);
const mediaKitMessage = mediaKitMessages[mediaKitMessages.length - 1];
const mediaKitMessage = messageBubble(
mediaKitMessages[mediaKitMessages.length - 1],
);
expect(mediaKitMessage.textContent).toContain('项目:未命名游戏原型');
expect(mediaKitMessage.textContent).toContain('一句话:做一个厨房弹幕游戏');
expect(mediaKitMessage.textContent).toContain(
@@ -1589,8 +1606,9 @@ export async function assertPlaytestAndReleaseShortcutFlow(
};
submitChat('/release-notes');
const releaseNotesMessages = await screen.findAllByText(/试玩更新说明:/);
const releaseNotesMessage =
releaseNotesMessages[releaseNotesMessages.length - 1];
const releaseNotesMessage = messageBubble(
releaseNotesMessages[releaseNotesMessages.length - 1],
);
expect(releaseNotesMessage.textContent).toContain('项目:未命名游戏原型');
expect(releaseNotesMessage.textContent).toContain(
'一句话:做一个厨房弹幕游戏',
@@ -1707,7 +1725,9 @@ export async function assertPlaytestAndReleaseShortcutFlow(
};
submitChat('/known-issues');
const knownIssueMessages = await screen.findAllByText(/已知问题清单:/);
const knownIssueMessage = knownIssueMessages[knownIssueMessages.length - 1];
const knownIssueMessage = messageBubble(
knownIssueMessages[knownIssueMessages.length - 1],
);
expect(knownIssueMessage.textContent).toContain('项目:未命名游戏原型');
expect(knownIssueMessage.textContent).toContain(
'当前状态:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed;预览 未启动',
@@ -1,5 +1,6 @@
import { expect, fireEvent, screen, submitChat, within } from '../../harness';
import type { PreviewShortcutInvoke } from './invoke-mock';
import { messageBubble } from './message-bubble';
export async function assertProjectAndDesignShortcutFlow(
invoke: PreviewShortcutInvoke,
@@ -40,17 +41,31 @@ export async function assertProjectAndDesignShortcutFlow(
expect(screen.queryByText(/art-secret/)).toBeNull();
submitChat('/agents');
expect(await screen.findByText('Agent 状态:')).not.toBeNull();
expect(
await screen.findByText(
/Agent 状态:[\s\S]*美术组 \/ Asset · 生成首版美术素材 · 待处理[\s\S]*LLM:已配置 · art-model/,
(_, element) =>
element?.tagName === 'LI' &&
element.textContent?.includes(
'美术组 / Asset · 生成首版美术素材 · 待处理',
) === true,
),
).not.toBeNull();
expect(
await screen.findByText(
(_, element) =>
element?.tagName === 'LI' &&
element.textContent?.includes('LLM:已配置 · art-model') === true,
),
).not.toBeNull();
submitChat('/agent-conversations');
expect(
await screen.findByText(
/Agent 对话读取命令:[\s\S]*美术组 \/ Asset · 生成首版美术素材:\/read \.agent\/conversations\/agents\/art-asset-plan\.jsonl/,
),
).not.toBeNull();
const agentConversationMessage =
await screen.findByText('Agent 对话读取命令:');
const agentConversationBubble = agentConversationMessage.closest('.message');
expect(agentConversationBubble).not.toBeNull();
expect(agentConversationBubble?.textContent).toContain(
'美术组 / Asset · 生成首版美术素材:/read .agent/conversations/agents/art-asset-plan.jsonl',
);
fireEvent.click(screen.getByRole('button', { name: '读取拆解创作方向对话' }));
expect(screen.getByLabelText('创作想法')).toHaveProperty(
'value',
@@ -63,11 +78,13 @@ export async function assertProjectAndDesignShortcutFlow(
}),
);
submitChat('/agent-memories');
expect(
await screen.findByText(
/Agent 私有记忆读取命令:[\s\S]*美术组 \/ Asset · 生成首版美术素材:\/read memory\/agents\/art\/asset\.md/,
),
).not.toBeNull();
const agentMemoryMessage =
await screen.findByText('Agent 私有记忆读取命令:');
const agentMemoryBubble = agentMemoryMessage.closest('.message');
expect(agentMemoryBubble).not.toBeNull();
expect(agentMemoryBubble?.textContent).toContain(
'美术组 / Asset · 生成首版美术素材:/read memory/agents/art/asset.md',
);
fireEvent.click(screen.getByRole('button', { name: '读取拆解创作方向记忆' }));
expect(screen.getByLabelText('创作想法')).toHaveProperty(
'value',
@@ -129,7 +146,7 @@ export async function assertProjectAndDesignShortcutFlow(
submitChat('/goal');
expect(await screen.findByText(/创作目标:/)).not.toBeNull();
const goalMessages = screen.getAllByText(/创作目标:/);
const goalMessage = goalMessages[goalMessages.length - 1];
const goalMessage = messageBubble(goalMessages[goalMessages.length - 1]);
expect(goalMessage.textContent).toContain('Manifest:做一个厨房弹幕游戏');
expect(goalMessage.textContent).toContain(
'Runrun-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed',
@@ -187,7 +204,7 @@ export async function assertProjectAndDesignShortcutFlow(
};
submitChat('/spec');
const specMessages = await screen.findAllByText(/创作规格包:/);
const specMessage = specMessages[specMessages.length - 1];
const specMessage = messageBubble(specMessages[specMessages.length - 1]);
expect(specMessage.textContent).toContain('项目:未命名游戏原型');
expect(specMessage.textContent).toContain('目标:做一个厨房弹幕游戏');
expect(specMessage.textContent).toContain(
@@ -287,7 +304,7 @@ export async function assertProjectAndDesignShortcutFlow(
submitChat('/mvp');
expect(await screen.findByText(/MVP 范围:/)).not.toBeNull();
const mvpMessages = screen.getAllByText(/MVP 范围:/);
const mvpMessage = mvpMessages[mvpMessages.length - 1];
const mvpMessage = messageBubble(mvpMessages[mvpMessages.length - 1]);
expect(mvpMessage.textContent).toContain('项目:未命名游戏原型');
expect(mvpMessage.textContent).toContain('目标:做一个厨房弹幕游戏');
expect(mvpMessage.textContent).toContain(
@@ -383,7 +400,7 @@ export async function assertProjectAndDesignShortcutFlow(
};
submitChat('/pitch');
const pitchMessages = await screen.findAllByText(/试玩定位:/);
const pitchMessage = pitchMessages[pitchMessages.length - 1];
const pitchMessage = messageBubble(pitchMessages[pitchMessages.length - 1]);
expect(pitchMessage.textContent).toContain('项目:未命名游戏原型');
expect(pitchMessage.textContent).toContain('一句话:做一个厨房弹幕游戏');
expect(pitchMessage.textContent).toContain(
@@ -478,7 +495,7 @@ export async function assertProjectAndDesignShortcutFlow(
};
submitChat('/demo');
const demoMessages = await screen.findAllByText(/试玩讲解稿:/);
const demoMessage = demoMessages[demoMessages.length - 1];
const demoMessage = messageBubble(demoMessages[demoMessages.length - 1]);
expect(demoMessage.textContent).toContain('项目:未命名游戏原型');
expect(demoMessage.textContent).toContain(
'30 秒开场:这是《未命名游戏原型》,目标是做一个厨房弹幕游戏',
@@ -578,7 +595,9 @@ export async function assertProjectAndDesignShortcutFlow(
submitChat('/rules');
expect(await screen.findByText(/玩法操作:/)).not.toBeNull();
const controlMessages = screen.getAllByText(/玩法操作:/);
const controlMessage = controlMessages[controlMessages.length - 1];
const controlMessage = messageBubble(
controlMessages[controlMessages.length - 1],
);
expect(controlMessage.textContent).toContain('项目:未命名游戏原型');
expect(controlMessage.textContent).toContain('目标:做一个厨房弹幕游戏');
expect(controlMessage.textContent).toContain(
@@ -682,7 +701,9 @@ export async function assertProjectAndDesignShortcutFlow(
};
submitChat('/tutorial');
const tutorialMessages = await screen.findAllByText(/新手引导:/);
const tutorialMessage = tutorialMessages[tutorialMessages.length - 1];
const tutorialMessage = messageBubble(
tutorialMessages[tutorialMessages.length - 1],
);
expect(tutorialMessage.textContent).toContain('项目:未命名游戏原型');
expect(tutorialMessage.textContent).toContain('首屏目标:做一个厨房弹幕游戏');
expect(tutorialMessage.textContent).toContain(
@@ -785,7 +806,9 @@ export async function assertProjectAndDesignShortcutFlow(
};
submitChat('/mobile');
const mobileMessages = await screen.findAllByText(/移动试玩:/);
const mobileMessage = mobileMessages[mobileMessages.length - 1];
const mobileMessage = messageBubble(
mobileMessages[mobileMessages.length - 1],
);
expect(mobileMessage.textContent).toContain('项目:未命名游戏原型');
expect(mobileMessage.textContent).toContain('目标:做一个厨房弹幕游戏');
expect(mobileMessage.textContent).toContain(
@@ -894,8 +917,9 @@ export async function assertProjectAndDesignShortcutFlow(
};
submitChat('/compatibility');
const compatibilityMessages = await screen.findAllByText(/兼容性说明:/);
const compatibilityMessage =
compatibilityMessages[compatibilityMessages.length - 1];
const compatibilityMessage = messageBubble(
compatibilityMessages[compatibilityMessages.length - 1],
);
expect(compatibilityMessage.textContent).toContain('项目:未命名游戏原型');
expect(compatibilityMessage.textContent).toContain(
'目标:做一个厨房弹幕游戏',
@@ -1009,8 +1033,9 @@ export async function assertProjectAndDesignShortcutFlow(
};
submitChat('/accessibility');
const accessibilityMessages = await screen.findAllByText(/可读性与无障碍:/);
const accessibilityMessage =
accessibilityMessages[accessibilityMessages.length - 1];
const accessibilityMessage = messageBubble(
accessibilityMessages[accessibilityMessages.length - 1],
);
expect(accessibilityMessage.textContent).toContain('项目:未命名游戏原型');
expect(accessibilityMessage.textContent).toContain(
'目标:做一个厨房弹幕游戏',
@@ -1132,8 +1157,9 @@ export async function assertProjectAndDesignShortcutFlow(
};
submitChat('/localization');
const localizationMessages = await screen.findAllByText(/本地化与文案:/);
const localizationMessage =
localizationMessages[localizationMessages.length - 1];
const localizationMessage = messageBubble(
localizationMessages[localizationMessages.length - 1],
);
expect(localizationMessage.textContent).toContain('项目:未命名游戏原型');
expect(localizationMessage.textContent).toContain('目标:做一个厨房弹幕游戏');
expect(localizationMessage.textContent).toContain(
@@ -1262,8 +1288,9 @@ export async function assertProjectAndDesignShortcutFlow(
};
submitChat('/performance');
const performanceMessages = await screen.findAllByText(/性能与加载:/);
const performanceMessage =
performanceMessages[performanceMessages.length - 1];
const performanceMessage = messageBubble(
performanceMessages[performanceMessages.length - 1],
);
expect(performanceMessage.textContent).toContain('项目:未命名游戏原型');
expect(performanceMessage.textContent).toContain('目标:做一个厨房弹幕游戏');
expect(performanceMessage.textContent).toContain(
@@ -1379,7 +1406,9 @@ export async function assertProjectAndDesignShortcutFlow(
};
submitChat('/polish');
const polishMessages = await screen.findAllByText(/试玩前打磨:/);
const polishMessage = polishMessages[polishMessages.length - 1];
const polishMessage = messageBubble(
polishMessages[polishMessages.length - 1],
);
expect(polishMessage.textContent).toContain('项目:未命名游戏原型');
expect(polishMessage.textContent).toContain('目标:做一个厨房弹幕游戏');
expect(polishMessage.textContent).toContain(
@@ -1491,7 +1520,9 @@ export async function assertProjectAndDesignShortcutFlow(
};
submitChat('/credits');
const creditMessages = await screen.findAllByText(/素材署名:/);
const creditMessage = creditMessages[creditMessages.length - 1];
const creditMessage = messageBubble(
creditMessages[creditMessages.length - 1],
);
expect(creditMessage.textContent).toContain('当前资产:2 个');
expect(creditMessage.textContent).toContain('来源分布:上传 1 / 画板 1');
expect(creditMessage.textContent).toContain(
@@ -7,6 +7,7 @@ import {
within,
} from '../../harness';
import type { PreviewShortcutInvoke } from './invoke-mock';
import { messageBubble } from './message-bubble';
export async function assertProjectToolsAndPreviewFlow(
invoke: PreviewShortcutInvoke,
@@ -36,7 +37,9 @@ export async function assertProjectToolsAndPreviewFlow(
};
submitChat('/feedback');
const feedbackMessages = await screen.findAllByText(/试玩反馈:/);
const feedbackMessage = feedbackMessages[feedbackMessages.length - 1];
const feedbackMessage = messageBubble(
feedbackMessages[feedbackMessages.length - 1],
);
expect(feedbackMessage.textContent).toContain(
'Runrun-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed',
);
@@ -125,7 +128,9 @@ export async function assertProjectToolsAndPreviewFlow(
};
submitChat('/retention');
const retentionMessages = await screen.findAllByText(/复玩观察:/);
const retentionMessage = retentionMessages[retentionMessages.length - 1];
const retentionMessage = messageBubble(
retentionMessages[retentionMessages.length - 1],
);
expect(retentionMessage.textContent).toContain('项目:未命名游戏原型');
expect(retentionMessage.textContent).toContain('目标:做一个厨房弹幕游戏');
expect(retentionMessage.textContent).toContain(
@@ -233,7 +238,7 @@ export async function assertProjectToolsAndPreviewFlow(
};
submitChat('/share');
const shareMessages = await screen.findAllByText(/试玩交付:/);
const shareMessage = shareMessages[shareMessages.length - 1];
const shareMessage = messageBubble(shareMessages[shareMessages.length - 1]);
expect(shareMessage.textContent).toContain('项目:未命名游戏原型');
expect(shareMessage.textContent).toContain('目录:/tmp/authorized-game');
expect(shareMessage.textContent).toContain(
@@ -438,11 +443,11 @@ export async function assertProjectToolsAndPreviewFlow(
expect(composerInput).toHaveProperty('value', '/read game/index.html');
await waitFor(() => expect(document.activeElement).toBe(composerInput));
submitChat('/run-artifacts');
expect(
await screen.findByText(
/最近 Run 产物读取命令:[\s\S]*exports\/README\.md · 128B · fnv1a64:exports\/read exports\/README\.md/,
),
).not.toBeNull();
const runArtifactsHeading =
await screen.findByText('最近 Run 产物读取命令:');
expect(messageBubble(runArtifactsHeading).textContent).toContain(
'exports/README.md · 128B · fnv1a64:exports/read exports/README.md',
);
fireEvent.click(screen.getByRole('button', { name: '读取首个 Run 产物' }));
expect(composerInput).toHaveProperty('value', '/read exports/README.md');
expect(invoke).not.toHaveBeenCalledWith(
@@ -457,8 +462,9 @@ export async function assertProjectToolsAndPreviewFlow(
submitChat('/passes');
const passArtifactMessages =
await screen.findAllByText(/Agent 轮次产物读取命令:/);
const passArtifactMessage =
passArtifactMessages[passArtifactMessages.length - 1];
const passArtifactMessage = messageBubble(
passArtifactMessages[passArtifactMessages.length - 1],
);
expect(passArtifactMessage.textContent).toContain(
'.agent/passes/pass-1/agenda.md · 96B · fnv1a64:agenda',
);
@@ -564,13 +570,8 @@ export async function assertProjectToolsAndPreviewFlow(
expect(screen.getByText(/下一步:设计实现组 \/ Director/)).not.toBeNull();
fireEvent.click(screen.getByRole('button', { name: 'Trace' }));
expect(
await screen.findByText(
(content) =>
content.trimStart().startsWith('Runrun-main-shortcut-trace') &&
content.includes('产物快照:'),
),
).not.toBeNull();
const traceHeading = await screen.findByText(/Runrun-main-shortcut-trace/);
expect(messageBubble(traceHeading).textContent).toContain('产物快照:');
expect(screen.getByText(/产物快照:/)).not.toBeNull();
expect(
screen.getByText(/exports\/README\.md · fnv1a64:exports/),
@@ -0,0 +1,7 @@
export function messageBubble(element: Element): HTMLElement {
const bubble = element.closest<HTMLElement>('.message');
if (!bubble) {
throw new Error('Expected chat message bubble');
}
return bubble;
}