Files
makelore/tests/unit/user-profile-store.test.ts
inman 22add3f01f
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled
完善客户端模块与工作区能力
2026-08-13 19:51:02 +08:00

254 lines
8.8 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('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);
});
});