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(); 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); }); });