Chat 访谈 - 新增 persona 系统(default / warm_listener / curious_guide)与 background_voice 语气层 - 回复长度由 compute_reply_plan 统一决策(brief / standard / expanded),融合信息密度启发式 - 输入净稿(input_normalize):编排层可选 rules/llm 归一用户口语后再喂模型与记忆检索 - 记忆证据注入:按用户话检索 memory evidence 并注入 prompt Memoir 回忆录 - 口述归一(oral_normalize):segment 原文保留,story 管线取派生净稿作叙事输入 - segment 入队批次门闸:累计字数 + 最长等待秒数,减少零碎提交 - fidelity_check / prompts / narrative_agent 微调 - Alembic 0005:清理跨章节 story 外键 Infra - Dockerfile 加入 ffmpeg - pyproject.toml 新增依赖并同步 uv.lock - .env.example / .env.production 补全新配置项 Tests - 新增 test_background_voice、test_chat_input_normalize、test_experience_regressions - 扩展 test_interview_prompts、test_interview_reply_length、test_story_route_oral_invariant Made-with: Cursor
137 lines
3.4 KiB
TypeScript
137 lines
3.4 KiB
TypeScript
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import { renderHook, waitFor } from '@testing-library/react-native';
|
|
import React, { type PropsWithChildren } from 'react';
|
|
|
|
import {
|
|
useConversations,
|
|
useCreateConversation,
|
|
useDeleteConversation,
|
|
} from '@/features/conversation/hooks';
|
|
import { conversationKeys } from '@/features/conversation/query-keys';
|
|
|
|
// ─── Mocks ───
|
|
|
|
const mockList = jest.fn();
|
|
const mockCreate = jest.fn();
|
|
const mockDelete = jest.fn();
|
|
|
|
jest.mock('@/features/conversation/api', () => ({
|
|
conversationApi: {
|
|
list: () => mockList(),
|
|
create: () => mockCreate(),
|
|
delete: (id: string) => mockDelete(id),
|
|
detail: jest.fn(),
|
|
end: jest.fn(),
|
|
messages: jest.fn(),
|
|
organize: jest.fn(),
|
|
},
|
|
}));
|
|
|
|
// ─── Helpers ───
|
|
|
|
let queryClient: QueryClient;
|
|
|
|
function createWrapper() {
|
|
queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false, gcTime: Infinity },
|
|
mutations: { retry: false },
|
|
},
|
|
});
|
|
|
|
return function Wrapper({ children }: PropsWithChildren) {
|
|
return (
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
);
|
|
};
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await queryClient?.cancelQueries();
|
|
queryClient?.clear();
|
|
jest.clearAllMocks();
|
|
});
|
|
|
|
const fakeConversations = [
|
|
{
|
|
id: 'c1',
|
|
title: '第一次对话',
|
|
avatarUrl: null,
|
|
latestMessagePreview: '你好',
|
|
latestMessageTime: 1000,
|
|
startedAt: 1000,
|
|
unreadCount: 0,
|
|
isDefaultAssistant: false,
|
|
hasUserMessage: true,
|
|
},
|
|
];
|
|
|
|
// ─── Tests ───
|
|
|
|
describe('useConversations', () => {
|
|
test('fetches conversation list', async () => {
|
|
mockList.mockResolvedValue(fakeConversations);
|
|
|
|
const { result } = renderHook(() => useConversations(), {
|
|
wrapper: createWrapper(),
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isSuccess).toBe(true);
|
|
});
|
|
|
|
expect(result.current.data).toEqual(fakeConversations);
|
|
});
|
|
});
|
|
|
|
describe('useCreateConversation', () => {
|
|
test('adds new conversation to list cache on success', async () => {
|
|
const newConversation = {
|
|
id: 'c2',
|
|
user_id: 'u1',
|
|
started_at: '2026-01-01T00:00:00Z',
|
|
status: 'active',
|
|
};
|
|
mockCreate.mockResolvedValue(newConversation);
|
|
|
|
const wrapper = createWrapper();
|
|
queryClient.setQueryData(conversationKeys.lists(), fakeConversations);
|
|
|
|
const { result } = renderHook(() => useCreateConversation(), { wrapper });
|
|
|
|
await result.current.mutateAsync();
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isSuccess).toBe(true);
|
|
});
|
|
|
|
const list = queryClient.getQueryData(conversationKeys.lists()) as Array<{
|
|
id: string;
|
|
}>;
|
|
expect(list[0].id).toBe('c2');
|
|
expect(list).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe('useDeleteConversation', () => {
|
|
test('removes conversation from list cache on success', async () => {
|
|
mockDelete.mockResolvedValue({ message: 'ok' });
|
|
|
|
const wrapper = createWrapper();
|
|
queryClient.setQueryData(conversationKeys.lists(), fakeConversations);
|
|
|
|
const { result } = renderHook(() => useDeleteConversation(), { wrapper });
|
|
|
|
await result.current.mutateAsync('c1');
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.isSuccess).toBe(true);
|
|
});
|
|
|
|
const list = queryClient.getQueryData(conversationKeys.lists()) as Array<{
|
|
id: string;
|
|
}>;
|
|
expect(list).toHaveLength(0);
|
|
});
|
|
});
|