Files
Genarrative/apps/ai-game-creator-shell/tests/designReplyAnimation.test.tsx
T
lhk229 688cf7a971
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m28s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m59s
Project CI / Backend tests (pull_request) Successful in 5m1s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m7s
Project CI / Native shell tests (pull_request) Successful in 6m17s
Project CI / Frontend tests (pull_request) Successful in 2m9s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m29s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m42s
Project CI / Repository checks (pull_request) Successful in 2m18s
修复策划回复重复播放并保留伪流式
按消息 ID 合并实时正文与正式消息,保留逐步显示进度
分离请求收尾与动画结束,隔离旧回合事件及异步返回
独立展示工具状态,保留 Provider 重试的正文重置语义
补充重复终态、整块回复、多消息及回合隔离回归测试
同步更新策划展示规范与排障记录
2026-09-23 10:38:01 +00:00

100 lines
3.4 KiB
TypeScript

// @vitest-environment jsdom
import { act, cleanup, renderHook } from '@testing-library/react';
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import type { DesignView } from '../src/app/types';
import { useDesignReplyAnimation } from '../src/view/project-development/planning/useDesignReplyAnimation';
beforeEach(() => vi.useFakeTimers());
afterEach(() => {
cleanup();
vi.useRealTimers();
});
function view(messages: DesignView['messages'], running = false): DesignView {
return {
session: {
sessionId: 's',
projectId: 'p',
currentPhase: 'concept',
approvedPhases: [],
pendingApproval: null,
pendingClarification: null,
turnIndex: 1,
lastError: null,
},
messages,
running,
canRetry: false,
};
}
it('keeps each reply progress across tool snapshots and repeated completion', () => {
const { result } = renderHook(useDesignReplyAnimation);
const history = { id: 'history', role: 'assistant', text: '历史' };
const first = { id: 't:response:0', role: 'assistant', text: '第一条回复' };
const second = { id: 't:response:1', role: 'assistant', text: '第二条回复' };
act(() => {
result.current.reset([history.id]);
result.current.receiveText(first.id, first.text);
vi.advanceTimersByTime(100);
});
expect(result.current.replies[0].visible).toBe('第一');
act(() =>
result.current.receiveView(
view(
[history, first, { id: 'tool', role: 'tool', text: '工具已完成' }],
true,
),
),
);
expect(result.current.replies).toHaveLength(1);
expect(result.current.replies[0].visible).toBe('第一');
act(() => result.current.receiveText(second.id, second.text));
const terminal = view([history, first, second]);
act(() => {
result.current.receiveView(terminal);
result.current.receiveView(terminal);
});
expect(result.current.replies.map((reply) => reply.visible)).toEqual([
'第一',
'',
]);
act(() => vi.advanceTimersByTime(500));
act(() => result.current.receiveView(terminal));
expect(result.current.replies.map((reply) => reply.visible)).toEqual([
first.text,
second.text,
]);
});
it('resets only an unpersisted retry attempt and discards failed partial output', () => {
const { result } = renderHook(useDesignReplyAnimation);
act(() => {
result.current.receiveText('t:response:0', '尝试失败');
vi.advanceTimersByTime(100);
});
expect(result.current.replies[0].visible).toBe('尝试');
act(() => result.current.receiveText('t:response:0', ''));
expect(result.current.replies[0].visible).toBe('');
const saved = { id: 't:response:0', role: 'assistant', text: '成功' };
act(() => {
result.current.receiveText(saved.id, saved.text);
result.current.receiveView(view([saved], true));
vi.advanceTimersByTime(100);
result.current.receiveText(saved.id, '');
result.current.receiveText('t:response:1', '未被接受的文本');
result.current.receiveView(view([saved]));
});
expect(result.current.replies).toEqual([
{ messageId: saved.id, target: '成功', visible: '成功', persisted: true },
]);
act(() => {
result.current.receiveText('t:response:2', '连接中断');
result.current.discardUnpersisted();
});
expect(result.current.replies).toHaveLength(1);
act(() => result.current.reset([saved.id]));
expect(result.current.replies).toEqual([]);
});