import { beforeEach, describe, expect, it, vi } from 'vitest'; import { AgentProfileApiError, deleteAgentAvatar, getAgentProfile, getAgentProfileApiErrorMessage, putAgentProfile, uploadAgentAvatar, } from '@/lib/agent-profile'; const hostApiFetchMock = vi.hoisted(() => vi.fn()); vi.mock('@/lib/host-api', () => ({ hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args), })); const profile = { display_name: '小泥', age: 18, gender: 'female' as const, share_age_with_agents: true, share_gender_with_agents: true, analysis_enabled: true, completed: true, version: 2, updated_at: '2026-07-12T00:00:00Z', }; describe('Agent Profile API client', () => { beforeEach(() => { hostApiFetchMock.mockReset(); }); it('loads the current profile through the authenticated Host API proxy', async () => { hostApiFetchMock.mockResolvedValue({ success: true, profile }); await expect(getAgentProfile('access-token')).resolves.toEqual(profile); expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/user/agent-profile', { headers: { 'X-NianCode-Access-Token': 'access-token' }, }); }); it('sends a complete versioned PUT payload', async () => { hostApiFetchMock.mockResolvedValue({ success: true, profile }); const update = { display_name: '小泥', age: 18, gender: 'female' as const, share_age_with_agents: true, share_gender_with_agents: true, analysis_enabled: true, version: 1, }; await expect(putAgentProfile('access-token', update)).resolves.toEqual(profile); expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/user/agent-profile', { method: 'PUT', body: JSON.stringify(update), headers: { 'X-NianCode-Access-Token': 'access-token' }, }); }); it('sends a completed local session snapshot alongside the profile', async () => { hostApiFetchMock.mockResolvedValue({ success: true, profile }); const sessionData = { project_id: 'prj_1', session_id: 'ses_1', updated_at: '2026-08-13T10:00:00.000Z', messages: [{ id: 'msg_1', role: 'user' as const, created_at: '2026-08-13T09:59:00.000Z', text: '请帮我解释这个问题。', }], }; await expect(putAgentProfile('access-token', { display_name: '小泥', age: 18, gender: 'female', share_age_with_agents: true, share_gender_with_agents: true, analysis_enabled: true, version: 2, session_data: sessionData, })).resolves.toEqual(profile); expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/user/agent-profile', { method: 'PUT', body: JSON.stringify({ display_name: '小泥', age: 18, gender: 'female', share_age_with_agents: true, share_gender_with_agents: true, analysis_enabled: true, version: 2, session_data: sessionData, }), headers: { 'X-NianCode-Access-Token': 'access-token' }, }); }); it('uploads a prepared avatar through the authenticated Host API proxy', async () => { hostApiFetchMock.mockResolvedValue({ success: true, avatar_url: 'https://cdn.example/avatar.webp' }); await expect(uploadAgentAvatar('access-token', { fileName: 'avatar.webp', mimeType: 'image/webp', dataBase64: 'QUJD', })).resolves.toBe('https://cdn.example/avatar.webp'); expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/user/avatar', { method: 'POST', body: JSON.stringify({ fileName: 'avatar.webp', mimeType: 'image/webp', dataBase64: 'QUJD', }), headers: { 'X-NianCode-Access-Token': 'access-token' }, }); }); it('deletes the current avatar through the authenticated Host API proxy', async () => { hostApiFetchMock.mockResolvedValue({ success: true, avatar_url: null }); await expect(deleteAgentAvatar('access-token')).resolves.toBeNull(); expect(hostApiFetchMock).toHaveBeenCalledWith('/api/works/user/avatar', { method: 'DELETE', headers: { 'X-NianCode-Access-Token': 'access-token' }, }); }); it('retains upstream conflict detail for the store to handle', async () => { hostApiFetchMock.mockResolvedValue({ success: false, status: 409, error: 'Profile version conflict', detail: { code: 'agent_profile_version_conflict', current_version: 4 }, }); await expect(getAgentProfile('access-token')).rejects.toMatchObject>({ name: 'AgentProfileApiError', status: 409, detail: { code: 'agent_profile_version_conflict', current_version: 4 }, code: 'agent_profile_version_conflict', }); }); it('retains avatar validation error codes from the Host API proxy', async () => { hostApiFetchMock.mockResolvedValue({ success: false, status: 415, code: 'avatar_unsupported_type', error: 'Unsupported avatar image type', }); await expect(deleteAgentAvatar('access-token')).rejects.toMatchObject>({ status: 415, code: 'avatar_unsupported_type', }); }); it('keeps a structured service error visible instead of replacing it with generic 503 text', async () => { hostApiFetchMock.mockResolvedValue({ success: false, status: 503, error: 'Agent Profile database migration is pending', detail: { code: 'agent_profile_storage_unavailable', message: 'Agent Profile database migration is pending', }, code: 'agent_profile_storage_unavailable', }); const error = await getAgentProfile('access-token').catch((value) => value as AgentProfileApiError); expect(error).toMatchObject({ status: 503, code: 'agent_profile_storage_unavailable', message: 'Agent Profile database migration is pending', }); expect(getAgentProfileApiErrorMessage(error)).toBe('Agent Profile database migration is pending'); }); });