import { beforeEach, describe, expect, it, vi } from 'vitest'; import { useAuthStore } from '@/stores/auth'; import { useOpencodeStore } from '@/stores/opencode'; import { usePartnerProfilesStore } from '@/stores/partner-profiles'; import { useProviderStore } from '@/stores/providers'; import { useSettingsStore } from '@/stores/settings'; import { useUserSyncStore } from '@/stores/user-sync'; const hostApiFetchMock = vi.hoisted(() => vi.fn()); const fetchProviderSnapshotMock = vi.hoisted(() => vi.fn()); vi.mock('@/lib/host-api', () => ({ hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args), ensureHostApiToken: vi.fn().mockResolvedValue('test-token'), createHostEventSource: vi.fn(() => ({ close: vi.fn() })), })); vi.mock('@/lib/provider-accounts', () => ({ fetchProviderSnapshot: (...args: unknown[]) => fetchProviderSnapshotMock(...args), isHostApiRouteMissing: () => false, })); function resetStores() { useAuthStore.setState({ initialized: true, loading: false, error: null, authBase: 'https://biz.nianxx.cn/auth/', clientId: 'app', accessToken: 'access-token', refreshToken: 'refresh-token', tokenType: 'Bearer', expiresAt: Date.now() + 60_000, user: { username: 'zhangsan', userId: '1', tenantId: 7, deptId: 9, authorities: ['ROLE_USER'], }, }); useSettingsStore.setState({ theme: 'system', language: 'en', sidebarCollapsed: false, devModeUnlocked: false, launchAtStartup: true, proxyServer: 'http://local-proxy.example.com', }); usePartnerProfilesStore.setState({ subagents: [], projectRuntimeConfigsByProjectId: { prj_1: { 'makelore-pm-cloud': { filePath: 'D:/repo/.opencode/agent/makelore-pm-cloud.md', skillIds: ['pm-project-plan'], updatedAt: '2026-07-04T00:00:00.000Z', }, }, }, profilesById: {}, }); useOpencodeStore.setState({ activeProject: { id: 'prj_1', path: 'D:/repo', name: 'Course Project', createdAt: '2026-07-04T00:00:00.000Z', updatedAt: '2026-07-04T00:00:00.000Z', lastOpenedAt: '2026-07-04T00:00:00.000Z', }, sessions: [{ id: 'sess_1', title: 'Local session' }], sessionsByProjectId: { prj_1: [{ id: 'sess_1', title: 'Local session' }] }, selectedSessionId: 'sess_1', }); useProviderStore.setState({ statuses: [], accounts: [], vendors: [], defaultAccountId: null, loading: false, error: null, }); useUserSyncStore.setState({ loading: false, error: null, lastSyncedAt: null, }); } describe('useUserSyncStore', () => { beforeEach(() => { vi.clearAllMocks(); window.localStorage.clear(); hostApiFetchMock.mockReset(); fetchProviderSnapshotMock.mockResolvedValue({ statuses: [], accounts: [], vendors: [], defaultAccountId: 'openai-account-1', }); resetStores(); }); it('hydrates user-level data while leaving project-owned stores untouched', async () => { hostApiFetchMock.mockResolvedValueOnce({ success: true, bootstrap: { preferences: { theme: 'dark', language: 'zh-CN', sidebarCollapsed: true, launchAtStartup: false, activeProjectId: 'other-project', }, providerAccounts: [{ id: 'openai-account-1', vendorId: 'openai', label: 'OpenAI', authMode: 'api_key', enabled: true, isDefault: true, metadata: {}, updatedAt: '2026-07-04T00:00:00.000Z', }], partners: [{ id: 'makelore-pm-cloud', displayName: 'Cloud PM', templateRoleId: 'custom', description: 'Global cloud partner', skillIds: ['pm-project-plan'], filePath: 'D:/other/.opencode/agent/makelore-pm-cloud.md', projectRuntimeConfigsByProjectId: { other: { filePath: 'D:/other/file.md' } }, createdAt: '2026-07-04T00:00:00.000Z', updatedAt: '2026-07-04T00:00:00.000Z', }], usageAggregates: [], projects: [{ id: 'other-project' }], }, applied: { providerAccounts: 1 }, }); await useUserSyncStore.getState().bootstrap(); expect(hostApiFetchMock).toHaveBeenCalledWith('/api/user-sync/bootstrap', { headers: { 'x-niancode-access-token': 'access-token' }, }); expect(useSettingsStore.getState().theme).toBe('dark'); expect(useSettingsStore.getState().language).toBe('zh'); expect(useSettingsStore.getState().sidebarCollapsed).toBe(true); expect(useSettingsStore.getState().launchAtStartup).toBe(true); expect(useSettingsStore.getState().proxyServer).toBe('http://local-proxy.example.com'); expect(usePartnerProfilesStore.getState().subagents).toEqual([{ id: 'makelore-pm-cloud', displayName: 'Cloud PM', templateRoleId: 'custom', description: 'Global cloud partner', avatarTone: undefined, avatarDataUrl: undefined, prompt: undefined, skillIds: ['pm-project-plan'], createdAt: '2026-07-04T00:00:00.000Z', updatedAt: '2026-07-04T00:00:00.000Z', }]); expect(usePartnerProfilesStore.getState().projectRuntimeConfigsByProjectId).toHaveProperty('prj_1'); expect(useOpencodeStore.getState().activeProject?.id).toBe('prj_1'); expect(useOpencodeStore.getState().sessions).toHaveLength(1); expect(fetchProviderSnapshotMock).toHaveBeenCalledOnce(); }); it('pushes only safe preferences and excludes machine settings', async () => { hostApiFetchMock.mockResolvedValueOnce({ success: true, bootstrap: { preferences: {}, providerAccounts: [], partners: [], usageAggregates: [] }, }); await useUserSyncStore.getState().pushPreferences({ theme: 'dark', language: 'zh-CN', launchAtStartup: true, proxyServer: 'http://proxy.example.com', } as never); expect(hostApiFetchMock).toHaveBeenCalledWith('/api/user-sync/preferences', { method: 'PUT', headers: { 'x-niancode-access-token': 'access-token' }, body: JSON.stringify({ theme: 'dark', language: 'zh-CN' }), }); }); it('pushes partner records without project runtime mappings', async () => { hostApiFetchMock.mockResolvedValueOnce({ success: true, bootstrap: { preferences: {}, providerAccounts: [], partners: [], usageAggregates: [] }, }); await useUserSyncStore.getState().pushPartner({ id: 'makelore-pm-test', displayName: 'PM', templateRoleId: 'custom', filePath: 'D:/repo/.opencode/agent/makelore-pm-test.md', skillIds: ['pm-project-plan'], createdAt: '2026-07-04T00:00:00.000Z', updatedAt: '2026-07-04T00:00:00.000Z', } as never); const body = JSON.parse(String(hostApiFetchMock.mock.calls[0][1].body)); expect(body).toEqual({ id: 'makelore-pm-test', displayName: 'PM', templateRoleId: 'custom', skillIds: ['pm-project-plan'], createdAt: '2026-07-04T00:00:00.000Z', updatedAt: '2026-07-04T00:00:00.000Z', }); expect(JSON.stringify(body)).not.toContain('filePath'); }); });