1e17d2c852
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m44s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m57s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m44s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m53s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m21s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m27s
Project CI / Frontend tests (pull_request) Successful in 7m4s
Project CI / AI game creator shell web tests (pull_request) Successful in 6m22s
Project CI / Native shell tests (pull_request) Successful in 10m13s
AI 润色回包与原文相同时给出明确提示,不再落下点了等于没点的「恢复原文」假入口 润色「与原文相同」的判据改为规范化之后要写回宿主的文本,回包被长度上限截回原文同样算没变化 共享聊天输入区显示润色提示,并让提示随用户手改提示词失效 快速编辑未提交草稿改按资源路径归属,收起面板或被旁路重投影后都不再静默丢掉 恢复入口按路径与本会话草稿对号,并区分「本会话未提交」与原生账本条目 账本读取失败时入口仍报出本会话草稿条数,「继续编辑」重开面板回填提示词与 @ 引用 派生资源名预检接入快速编辑与角色动画两条提交路径,早于正规化写盘 名称预检自身抛错时回写面板失败态,不再留下「点了没反应」的提交按钮 浮层关闭判定改在捕获阶段冻结,避免浮层内按钮卸载自己时整块面板被自己人收掉 恢复入口动作按钮收进统一容器,补移动端纵向排布与禁用态样式 空草稿表改工厂函数,跨语言对表用例按模块路径解析 Rust 源码 补回归用例:旁路重投影后的草稿、原样回包提示、提交前名称拦截、草稿表读写与跨卡隔离 同步 decision-log 与画布验收待办的进展说明
251 lines
8.6 KiB
TypeScript
251 lines
8.6 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { act, cleanup, renderHook } from '@testing-library/react';
|
|
import { afterEach, describe, expect, test, vi } from 'vitest';
|
|
|
|
import {
|
|
PROMPT_POLISH_UNCHANGED_NOTICE,
|
|
usePromptPolish,
|
|
} from '../src/features/project-workspace/usePromptPolish';
|
|
|
|
afterEach(cleanup);
|
|
|
|
function renderPolish({
|
|
requestPolish,
|
|
applyPrompt = vi.fn(),
|
|
normalizeResult,
|
|
readPrompt = () => '原始需求',
|
|
}: {
|
|
requestPolish: (
|
|
prompt: string,
|
|
context: string | null,
|
|
) => Promise<string | null>;
|
|
applyPrompt?: (text: string) => void;
|
|
normalizeResult?: (polished: string) => {
|
|
text: string;
|
|
notice?: string | null;
|
|
};
|
|
readPrompt?: () => string;
|
|
}) {
|
|
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('原始需求');
|
|
});
|
|
|
|
test('成功回包与当前 prompt 逐字相同:给「未做修改」提示,不落快照也不回填', async () => {
|
|
const applyPrompt = vi.fn();
|
|
const { result } = renderPolish({
|
|
requestPolish: async () => '原始需求',
|
|
applyPrompt,
|
|
});
|
|
|
|
let returned: string | null = null;
|
|
await act(async () => {
|
|
returned = await result.current.polish();
|
|
});
|
|
|
|
// 验收项 AGC-004:润色结果就是原文时必须说得清,不能让用户以为按钮没反应、
|
|
// 或者以为文案已经被改过。
|
|
expect(returned).toBe('原始需求');
|
|
expect(result.current.notice).toBe(PROMPT_POLISH_UNCHANGED_NOTICE);
|
|
expect(result.current.notice).toBe('AI 润色结果与原文相同,未做修改');
|
|
expect(result.current.error).toBeNull();
|
|
expect(result.current.polishing).toBe(false);
|
|
// 没有可回退的变化 ⇒ 不落原文快照,宿主也就不会渲染「恢复原文」这个假入口。
|
|
expect(result.current.originalText).toBeNull();
|
|
// 不回填:宿主侧的回填还牵着「提示词变了就重铸请求身份」这类副作用,这次什么都没变。
|
|
expect(applyPrompt).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('回包被上限截回原文:算「与原文相同」,提示改说截断原因', async () => {
|
|
const applyPrompt = vi.fn();
|
|
const { result } = renderPolish({
|
|
// 原文正好顶到长度上限、模型又在原文后面追加了内容:切片后与原文逐字相同。
|
|
requestPolish: async () => '原始需求加上很长很长的尾巴',
|
|
applyPrompt,
|
|
normalizeResult: () => ({ text: '原始需求', notice: '已按长度上限截断' }),
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.polish();
|
|
});
|
|
|
|
// 判据是「最终要写回宿主的文本」而不是回包字符串:写回去一字未变,就没有可回退的变化,
|
|
// 不该为它落快照、渲染一枚点了等于没点的「恢复原文」;截断提示优先,说清这次为什么没变。
|
|
expect(result.current.notice).toBe('已按长度上限截断');
|
|
expect(result.current.notice).not.toBe(PROMPT_POLISH_UNCHANGED_NOTICE);
|
|
expect(applyPrompt).not.toHaveBeenCalled();
|
|
expect(result.current.originalText).toBeNull();
|
|
});
|
|
|
|
test('规范化确实改动了内容:照旧回填并给规范化提示(对照)', async () => {
|
|
const applyPrompt = vi.fn();
|
|
const { result } = renderPolish({
|
|
requestPolish: async () => '润色后的需求加上要被截掉的尾巴',
|
|
applyPrompt,
|
|
normalizeResult: () => ({
|
|
text: '润色后的需求',
|
|
notice: '已按长度上限截断',
|
|
}),
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.polish();
|
|
});
|
|
|
|
expect(result.current.notice).toBe('已按长度上限截断');
|
|
expect(applyPrompt).toHaveBeenCalledWith('润色后的需求');
|
|
expect(result.current.originalText).toBe('原始需求');
|
|
});
|
|
|
|
test('clearNotice 收掉上一轮提示,快照与错误不受影响', async () => {
|
|
const { result } = renderPolish({
|
|
requestPolish: async () => '润色后的需求',
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.polish();
|
|
});
|
|
expect(result.current.originalText).toBe('原始需求');
|
|
|
|
await act(async () => {
|
|
result.current.clearNotice();
|
|
});
|
|
|
|
// 用户手改提示词只作废「上一轮的结论」,不该顺手把「恢复原文」也取消掉。
|
|
expect(result.current.notice).toBeNull();
|
|
expect(result.current.originalText).toBe('原始需求');
|
|
});
|
|
|
|
test('先真润色再遇到「回包等于原文」:提示互不覆盖,快照与恢复入口都留着', async () => {
|
|
let current = '原始需求';
|
|
const applied: string[] = [];
|
|
// 第二次回包等于第一次回填后的当前文本。
|
|
const results = ['润色后的需求', '润色后的需求'];
|
|
const { result } = renderHook(() =>
|
|
usePromptPolish({
|
|
readPrompt: () => current,
|
|
applyPrompt: (text) => {
|
|
current = text;
|
|
applied.push(text);
|
|
},
|
|
requestPolish: async () => results.shift() ?? null,
|
|
}),
|
|
);
|
|
|
|
await act(async () => {
|
|
await result.current.polish();
|
|
});
|
|
expect(applied).toEqual(['润色后的需求']);
|
|
expect(result.current.originalText).toBe('原始需求');
|
|
|
|
// 第二次回包就是当前文本:只换提示,宿主状态与原文快照都不动。
|
|
await act(async () => {
|
|
await result.current.polish();
|
|
});
|
|
expect(result.current.notice).toBe('AI 润色结果与原文相同,未做修改');
|
|
expect(result.current.originalText).toBe('原始需求');
|
|
expect(applied).toEqual(['润色后的需求']);
|
|
});
|
|
});
|