Files
Genarrative/apps/ai-game-creator-shell/tests/usePromptPolish.test.tsx
suzmii d04fe83f10 修复 PR #316 review(前端 TS/TSX):@ 引用与提示词润色主链路
- App.tsx:`conversation.write` 策略确认后的重跑改为整体复用首轮入参(`directCodexPolicyRetryInput`),不再手写字段,修掉漏传 `references` 导致 @ 引用被静默丢弃的缺陷
- resourceReferences.ts:运行画面引用的判别指纹补上 `resourceIds`(排序集合口径)/`versionId`/`elementRole`/`width`/`height`,避免不同选点撞 key 被 `dedupeChatReferences` 去重丢掉
- ResourceReferenceInput.tsx:程序化重建编辑器内容改为把 chip 内联嵌在文本里的同名 token 位置(不再另起一段堆在末尾),并把 `lastEmittedDraftRef` 记为「编辑器实际读回来的草稿」而不是 props,消除每轮多一段 `@显示名` 的渲染循环
- ResourceReferenceInput.tsx:候选浮层(候选菜单 / 素材选择器)打开时 Enter 不再提交表单,把按键让给 `LexicalTypeaheadMenuPlugin`
- ResourceReferenceInput.tsx:`assets` / `currentVersionAssetReferences` 的 memo 依赖改用内容签名;预览请求在换素材 / 卸载时主动作废上一个 scope;空态文案的嵌套三元抽成 `pickerEmptyMessage`
- usePromptPolish.ts:`polish()` 补 `catch`,注入式 `requestPolish` 拒绝时给出可见失败提示而不是未处理拒绝;`reset()` 递增请求代次作废在飞请求
- ChatPromptPolishReminder.tsx:章节级 Esc 分支补 `busy` 判据,与关闭按钮 / 遮罩同一口径(在飞不许关面板)
- LocalGamePreviewFrame.tsx:资源 id 改用专用净化(允许 `:` 与可打印非 ASCII),修掉 `local-asset:<id>`、`persisted-角色草图.png` 被整条过滤导致引用丢素材关联
- sessionPreview.ts:`get_local_game_preview_status` 读失败不再当作「确认没有在跑」去发停止命令,避免停掉真在跑的活体预览
- useHomeProjectCreation.ts:进项目流程加代次令牌,慢请求后到不再覆盖新项目
- WorkspaceLauncher.tsx:拒收恢复的阶段回写按 `projectId`/`heldRevision`/`snapshotRevision` 关联,不再把恢复结果盖到后续新拒收的提示上
- 用例:`directCodexPolicyRetryInput` 透传、引用指纹区分度、重建不循环、浮层打开时 Enter 不提交、润色拒绝 / reset 作废、预览读失败不停预览
2026-09-12 20:48:13 +08:00

130 lines
3.9 KiB
TypeScript

// @vitest-environment jsdom
import { act, cleanup, renderHook } from '@testing-library/react';
import { afterEach, describe, expect, test, vi } from 'vitest';
import { usePromptPolish } from '../src/features/project-workspace/usePromptPolish';
afterEach(cleanup);
function renderPolish({
requestPolish,
applyPrompt = vi.fn(),
normalizeResult,
}: {
requestPolish: (
prompt: string,
context: string | null,
) => Promise<string | null>;
applyPrompt?: (text: string) => void;
normalizeResult?: (polished: string) => {
text: string;
notice?: string | null;
};
}) {
return renderHook(() =>
usePromptPolish({
readPrompt: () => '原始需求',
applyPrompt,
requestPolish,
normalizeResult,
}),
);
}
describe('usePromptPolish', () => {
test('注入的 requestPolish 拒绝时收敛成失败提示,不产生未处理拒绝', async () => {
const applyPrompt = vi.fn();
const { result } = renderPolish({
requestPolish: vi.fn(async () => {
throw new Error('platform llm unavailable');
}),
applyPrompt,
});
let returned: string | null = 'sentinel';
await act(async () => {
returned = await result.current.polish();
});
// 默认 `requestChatPromptPolish` 自己吞掉失败;注入实现可以拒绝,此时必须
// 变成用户可见的失败提示,而不是把 promise 变成未处理拒绝、界面上什么都没有。
expect(returned).toBeNull();
expect(result.current.error).toBe('AI 润色失败,可重试');
expect(result.current.polishing).toBe(false);
expect(applyPrompt).not.toHaveBeenCalled();
});
test('notice 文案覆盖本次失败,并且回填前的抛错同样被接住', async () => {
const applyPrompt = vi.fn(() => {
throw new Error('composer write failed');
});
const { result } = renderPolish({
requestPolish: async () => '润色结果',
applyPrompt,
});
await act(async () => {
await result.current.polish({ failureMessage: '润色失败,可直接发送' });
});
expect(result.current.error).toBe('润色失败,可直接发送');
expect(result.current.polishing).toBe(false);
});
test('reset 之后的在飞润色结果不再回填宿主草稿', async () => {
let resolvePolish: (value: string) => void = () => {};
const applyPrompt = vi.fn();
const { result } = renderPolish({
requestPolish: vi.fn(
() =>
new Promise<string>((resolve) => {
resolvePolish = resolve;
}),
),
applyPrompt,
});
let pending: Promise<string | null> | null = null;
await act(async () => {
pending = result.current.polish();
});
// 宿主清空草稿 / 换资源:在飞请求必须被作废,迟到的结果不许写进新草稿。
await act(async () => {
result.current.reset();
});
await act(async () => {
resolvePolish('迟到的润色结果');
await pending;
});
expect(applyPrompt).not.toHaveBeenCalled();
expect(result.current.originalText).toBeNull();
expect(result.current.notice).toBeNull();
expect(result.current.error).toBeNull();
});
test('没有 reset 时在飞结果照旧回填(对照)', async () => {
let resolvePolish: (value: string) => void = () => {};
const applyPrompt = vi.fn();
const { result } = renderPolish({
requestPolish: () =>
new Promise<string>((resolve) => {
resolvePolish = resolve;
}),
applyPrompt,
});
let pending: Promise<string | null> | null = null;
await act(async () => {
pending = result.current.polish();
});
await act(async () => {
resolvePolish('润色后的需求');
await pending;
});
expect(applyPrompt).toHaveBeenCalledWith('润色后的需求');
expect(result.current.originalText).toBe('原始需求');
});
});