254 lines
8.8 KiB
TypeScript
254 lines
8.8 KiB
TypeScript
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('does not persist temporary signed avatar URLs locally', () => {
|
|
useUserProfileStore.getState().setAvatarUrl('user-a', 'https://oss.example/avatar.webp?signature=temporary');
|
|
|
|
const persisted = JSON.parse(localStorage.getItem('niancode-user-agent-profiles') ?? '{}') as {
|
|
state?: { profilesByUserId?: Record<string, { avatarUrl?: string | null }> };
|
|
};
|
|
expect(persisted.state?.profilesByUserId?.['user-a']?.avatarUrl).toBeNull();
|
|
});
|
|
|
|
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,
|
|
avatar_url: 'https://cdn.example/avatar.webp',
|
|
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',
|
|
avatarUrl: 'https://cdn.example/avatar.webp',
|
|
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('uploads session data without replacing local profile fields with cloud values', async () => {
|
|
putAgentProfileMock.mockResolvedValue({
|
|
display_name: '云端其他名字',
|
|
age: 20,
|
|
gender: 'female',
|
|
share_age_with_agents: true,
|
|
share_gender_with_agents: true,
|
|
analysis_enabled: true,
|
|
completed: true,
|
|
version: 5,
|
|
updated_at: '2026-07-12T00:00:00Z',
|
|
});
|
|
useUserProfileStore.getState().saveProfile('user-a', {
|
|
displayName: '本地名字', age: 16, gender: 'male',
|
|
});
|
|
useUserProfileStore.setState((state) => ({
|
|
profilesByUserId: {
|
|
...state.profilesByUserId,
|
|
'user-a': { ...state.profilesByUserId['user-a'], version: 4 },
|
|
},
|
|
}));
|
|
const sessionData = {
|
|
project_id: 'prj_1',
|
|
session_id: 'ses_1',
|
|
updated_at: '2026-08-13T10:00:00.000Z',
|
|
messages: [{ role: 'assistant' as const, text: '这是自然语言回答。' }],
|
|
};
|
|
|
|
await useUserProfileStore.getState().pushSessionData('user-a', 'access-token', sessionData);
|
|
|
|
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: 4,
|
|
session_data: sessionData,
|
|
});
|
|
expect(useUserProfileStore.getState().profilesByUserId['user-a']).toMatchObject({
|
|
displayName: '本地名字',
|
|
age: 16,
|
|
gender: 'male',
|
|
version: 5,
|
|
pendingSync: false,
|
|
});
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|