219 lines
7.0 KiB
TypeScript
219 lines
7.0 KiB
TypeScript
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: {
|
|
'nitu-pm-cloud': {
|
|
filePath: 'D:/repo/.opencode/agent/nitu-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: 'nitu-pm-cloud',
|
|
displayName: 'Cloud PM',
|
|
templateRoleId: 'pm',
|
|
description: 'Global cloud partner',
|
|
skillIds: ['pm-project-plan'],
|
|
filePath: 'D:/other/.opencode/agent/nitu-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: 'nitu-pm-cloud',
|
|
displayName: 'Cloud PM',
|
|
templateRoleId: 'pm',
|
|
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: 'nitu-pm-test',
|
|
displayName: 'PM',
|
|
templateRoleId: 'pm',
|
|
filePath: 'D:/repo/.opencode/agent/nitu-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: 'nitu-pm-test',
|
|
displayName: 'PM',
|
|
templateRoleId: 'pm',
|
|
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');
|
|
});
|
|
});
|