Files
makelore/tests/unit/user-profile-store.test.ts
2026-07-29 17:22:35 +08:00

194 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AgentProfileApiError } from '@/lib/agent-profile';
import { buildAgentUserContext, isUserProfileComplete, useUserProfileStore } from '@/stores/user-profile';
const getAgentProfileMock = vi.hoisted(() => vi.fn());
const putAgentProfileMock = vi.hoisted(() => vi.fn());
vi.mock('@/lib/agent-profile', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/agent-profile')>();
return {
...actual,
getAgentProfile: (...args: unknown[]) => getAgentProfileMock(...args),
putAgentProfile: (...args: unknown[]) => putAgentProfileMock(...args),
};
});
describe('global Agent user profile', () => {
beforeEach(() => {
getAgentProfileMock.mockReset();
putAgentProfileMock.mockReset();
useUserProfileStore.setState({ profilesByUserId: {}, syncStatesByUserId: {} });
});
it('stores profiles separately by account and shares every provided field', () => {
useUserProfileStore.getState().saveProfile('user-a', {
displayName: '小明', age: 16, gender: 'male',
});
useUserProfileStore.getState().saveProfile('user-b', {
displayName: '小红', age: null, gender: 'undisclosed',
});
const state = useUserProfileStore.getState();
expect(state.profilesByUserId['user-a']?.displayName).toBe('小明');
expect(state.profilesByUserId['user-b']?.displayName).toBe('小红');
expect(isUserProfileComplete(state.profilesByUserId['user-a'])).toBe(true);
const context = buildAgentUserContext(state.profilesByUserId['user-a']);
expect(context).toContain('用户名字是「小明」');
expect(context).toContain('用户年龄16 岁');
expect(context).toContain('用户性别:男');
});
it('uses the server profile as the source of truth when it already has a version', async () => {
getAgentProfileMock.mockResolvedValue({
display_name: '云端名字',
age: null,
gender: null,
share_age_with_agents: false,
share_gender_with_agents: false,
analysis_enabled: true,
completed: true,
version: 3,
updated_at: '2026-07-12T00:00:00Z',
});
useUserProfileStore.getState().saveProfile('user-a', {
displayName: '本地名字', age: 16, gender: 'male',
});
await useUserProfileStore.getState().syncProfile('user-a', 'access-token');
expect(useUserProfileStore.getState().profilesByUserId['user-a']).toMatchObject({
displayName: '云端名字',
age: null,
gender: 'undisclosed',
version: 3,
pendingSync: false,
});
expect(putAgentProfileMock).not.toHaveBeenCalled();
});
it('migrates a valid local profile only when the server is still at version zero', async () => {
getAgentProfileMock
.mockResolvedValueOnce({
display_name: null,
age: null,
gender: null,
share_age_with_agents: false,
share_gender_with_agents: false,
analysis_enabled: true,
completed: false,
version: 0,
updated_at: null,
})
.mockResolvedValueOnce({
display_name: '小明',
age: 16,
gender: 'male',
share_age_with_agents: true,
share_gender_with_agents: true,
analysis_enabled: true,
completed: true,
version: 1,
updated_at: '2026-07-12T00:00:00Z',
});
putAgentProfileMock.mockResolvedValue({
display_name: '小明',
age: 16,
gender: 'male',
share_age_with_agents: true,
share_gender_with_agents: true,
analysis_enabled: true,
completed: true,
version: 1,
updated_at: '2026-07-12T00:00:00Z',
});
useUserProfileStore.getState().saveProfile('user-a', {
displayName: '小明', age: 16, gender: 'male',
});
await useUserProfileStore.getState().syncProfile('user-a', 'access-token');
expect(putAgentProfileMock).toHaveBeenCalledWith('access-token', {
display_name: '小明',
age: 16,
gender: 'male',
share_age_with_agents: true,
share_gender_with_agents: true,
analysis_enabled: true,
version: 0,
});
expect(getAgentProfileMock).toHaveBeenCalledTimes(2);
expect(useUserProfileStore.getState().profilesByUserId['user-a']?.version).toBe(1);
});
it('saves edits with the current server version', async () => {
putAgentProfileMock.mockResolvedValue({
display_name: '更新后',
age: null,
gender: null,
share_age_with_agents: false,
share_gender_with_agents: false,
analysis_enabled: true,
completed: true,
version: 4,
updated_at: '2026-07-12T00:00:00Z',
});
useUserProfileStore.getState().saveProfile('user-a', {
displayName: '更新后', age: null, gender: 'undisclosed',
});
useUserProfileStore.setState((state) => ({
profilesByUserId: {
...state.profilesByUserId,
'user-a': { ...state.profilesByUserId['user-a'], version: 3 },
},
}));
await useUserProfileStore.getState().saveProfileToServer('user-a', 'access-token');
expect(putAgentProfileMock).toHaveBeenCalledWith('access-token', {
display_name: '更新后',
age: null,
gender: null,
share_age_with_agents: false,
share_gender_with_agents: false,
analysis_enabled: true,
version: 3,
});
expect(useUserProfileStore.getState().profilesByUserId['user-a']?.version).toBe(4);
});
it('reloads the server profile after a version conflict instead of retrying the old edit', async () => {
putAgentProfileMock.mockRejectedValue(new AgentProfileApiError(409, 'Profile version conflict'));
getAgentProfileMock.mockResolvedValue({
display_name: '其他设备的名字',
age: 18,
gender: 'female',
share_age_with_agents: false,
share_gender_with_agents: false,
analysis_enabled: true,
completed: true,
version: 5,
updated_at: '2026-07-12T00:00:00Z',
});
useUserProfileStore.getState().saveProfile('user-a', {
displayName: '本地编辑', age: null, gender: 'undisclosed',
});
useUserProfileStore.setState((state) => ({
profilesByUserId: {
...state.profilesByUserId,
'user-a': { ...state.profilesByUserId['user-a'], version: 4 },
},
}));
await expect(useUserProfileStore.getState().saveProfileToServer('user-a', 'access-token'))
.rejects.toMatchObject({ status: 409 });
expect(getAgentProfileMock).toHaveBeenCalledWith('access-token');
expect(useUserProfileStore.getState().profilesByUserId['user-a']).toMatchObject({
displayName: '其他设备的名字',
version: 5,
pendingSync: false,
});
expect(putAgentProfileMock).toHaveBeenCalledTimes(1);
});
});