mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-08-23 23:53:26 +00:00
327 lines
10 KiB
TypeScript
327 lines
10 KiB
TypeScript
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
|
|
|
import type { ServerProject } from '@/service/spaceApi';
|
|
import { getSessionPreviewSlice, usePageTabStore } from '@/store/pageTabStore';
|
|
import {
|
|
SPACE_SCHEMA_VERSION,
|
|
type Space,
|
|
type SpaceSourceType,
|
|
useSpaceStore,
|
|
} from '@/store/spaceStore';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const authStoreMock = vi.hoisted(() => ({
|
|
state: {
|
|
user_id: 2,
|
|
email: 'new@example.com',
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/store/authStore', () => ({
|
|
getAuthStore: () => authStoreMock.state,
|
|
}));
|
|
|
|
vi.mock('@/api/http', () => ({
|
|
proxyFetchGet: vi.fn().mockResolvedValue({ projects: [] }),
|
|
}));
|
|
|
|
vi.mock('@/service/spaceApi', () => ({
|
|
proxyCreateSpace: vi.fn(),
|
|
proxyEnsureLegacySpace: vi.fn(),
|
|
proxyFetchSpaceProjects: vi.fn(),
|
|
proxyFetchSpaces: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@/service/workspaceApi', () => ({
|
|
reconcileWorkspaceBindings: vi.fn().mockResolvedValue(undefined),
|
|
}));
|
|
|
|
const makeSpace = (
|
|
id: string,
|
|
name: string,
|
|
sourceType: SpaceSourceType,
|
|
userId = '2',
|
|
metadata?: Space['metadata']
|
|
): Space => ({
|
|
id,
|
|
name,
|
|
userId,
|
|
sourceType,
|
|
rootPath: null,
|
|
rootFingerprint: null,
|
|
status: 'active',
|
|
schemaVersion: SPACE_SCHEMA_VERSION,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
metadata,
|
|
});
|
|
|
|
const makeServerProject = (
|
|
id: string,
|
|
spaceId: string,
|
|
status: ServerProject['status'] = 'active'
|
|
): ServerProject => ({
|
|
id,
|
|
user_id: '2',
|
|
space_id: spaceId,
|
|
name: `Project ${id}`,
|
|
status,
|
|
created_at: '2026-01-01T00:00:00.000Z',
|
|
updated_at: '2026-01-01T00:00:00.000Z',
|
|
});
|
|
|
|
describe('spaceStore user scoping', () => {
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
const spaceApi = await import('@/service/spaceApi');
|
|
vi.mocked(spaceApi.proxyFetchSpaces).mockResolvedValue([]);
|
|
vi.mocked(spaceApi.proxyFetchSpaceProjects).mockResolvedValue([]);
|
|
vi.mocked(spaceApi.proxyCreateSpace).mockResolvedValue(
|
|
makeSpace('space_created', 'Untitled Space', 'blank')
|
|
);
|
|
vi.mocked(spaceApi.proxyEnsureLegacySpace).mockResolvedValue(
|
|
makeSpace('legacy_2', 'Legacy Space', 'legacy', '2', { legacy: true })
|
|
);
|
|
authStoreMock.state = {
|
|
email: 'new@example.com',
|
|
user_id: 2,
|
|
};
|
|
globalThis.electronAPI = {
|
|
...globalThis.electronAPI,
|
|
terminalDispose: vi.fn().mockResolvedValue({ success: true }),
|
|
};
|
|
usePageTabStore.setState({
|
|
sessionPreviewProjectId: null,
|
|
sessionPreviewByProject: {},
|
|
});
|
|
useSpaceStore.setState({
|
|
activeSpaceId: 'space_old_blank',
|
|
spaces: {
|
|
space_old_blank: {
|
|
id: 'space_old_blank',
|
|
name: 'Untitled Space',
|
|
userId: '1',
|
|
sourceType: 'blank',
|
|
status: 'active',
|
|
schemaVersion: SPACE_SCHEMA_VERSION,
|
|
createdAt: 1,
|
|
updatedAt: 3,
|
|
},
|
|
legacy_1: {
|
|
id: 'legacy_1',
|
|
name: 'Legacy Space',
|
|
userId: '1',
|
|
sourceType: 'legacy',
|
|
status: 'active',
|
|
schemaVersion: SPACE_SCHEMA_VERSION,
|
|
createdAt: 1,
|
|
updatedAt: 2,
|
|
metadata: { legacy: true },
|
|
},
|
|
space_new_blank: {
|
|
id: 'space_new_blank',
|
|
name: 'Untitled Space',
|
|
userId: '2',
|
|
sourceType: 'blank',
|
|
status: 'active',
|
|
schemaVersion: SPACE_SCHEMA_VERSION,
|
|
createdAt: 1,
|
|
updatedAt: 4,
|
|
},
|
|
},
|
|
lastVisitedProjectBySpace: {
|
|
space_old_blank: 'project_old',
|
|
space_new_blank: 'project_new',
|
|
},
|
|
projectsBySpaceId: {
|
|
space_old_blank: {
|
|
project_old: {
|
|
id: 'project_old',
|
|
userId: '1',
|
|
spaceId: 'space_old_blank',
|
|
name: 'Old project',
|
|
status: 'active',
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
},
|
|
},
|
|
space_new_blank: {
|
|
project_new: {
|
|
id: 'project_new',
|
|
userId: '2',
|
|
spaceId: 'space_new_blank',
|
|
name: 'New project',
|
|
status: 'active',
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
},
|
|
},
|
|
},
|
|
projectIdIndex: {
|
|
project_old: 'space_old_blank',
|
|
project_new: 'space_new_blank',
|
|
},
|
|
projectsSyncedAt: {
|
|
space_old_blank: 100,
|
|
legacy_1: 100,
|
|
space_new_blank: 200,
|
|
},
|
|
});
|
|
});
|
|
|
|
it('disposes project preview shells when their Space is deleted', () => {
|
|
const pageTabs = usePageTabStore.getState();
|
|
pageTabs.setSessionPreviewProject('project_old');
|
|
pageTabs.toggleSessionPreview();
|
|
pageTabs.choosePreviewTabType(
|
|
getSessionPreviewSlice(usePageTabStore.getState()).activeTabId!,
|
|
'terminal'
|
|
);
|
|
const terminal = getSessionPreviewSlice(usePageTabStore.getState()).tabs[0];
|
|
const shellId = terminal.type === 'terminal' ? terminal.shellId : undefined;
|
|
|
|
useSpaceStore.getState().deleteSpace('space_old_blank');
|
|
|
|
expect(globalThis.electronAPI.terminalDispose).toHaveBeenCalledWith(
|
|
shellId
|
|
);
|
|
expect(
|
|
usePageTabStore.getState().sessionPreviewByProject.project_old
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it('removes spaces and project metadata from the previous signed-in user', () => {
|
|
useSpaceStore.getState().resetForUser(2);
|
|
|
|
const state = useSpaceStore.getState();
|
|
expect(Object.keys(state.spaces)).toEqual(['space_new_blank']);
|
|
expect(state.activeSpaceId).toBe('space_new_blank');
|
|
expect(Object.keys(state.projectsBySpaceId)).toEqual(['space_new_blank']);
|
|
expect(state.projectIdIndex).toEqual({ project_new: 'space_new_blank' });
|
|
expect(state.lastVisitedProjectBySpace).toEqual({
|
|
space_new_blank: 'project_new',
|
|
});
|
|
expect(state.projectsSyncedAt).toEqual({ space_new_blank: 200 });
|
|
});
|
|
|
|
it('hydrates new accounts with one blank space and hides empty legacy rows', async () => {
|
|
const spaceApi = await import('@/service/spaceApi');
|
|
vi.mocked(spaceApi.proxyFetchSpaces).mockResolvedValue([
|
|
makeSpace('legacy_2', 'Legacy Space', 'legacy', '2', { legacy: true }),
|
|
]);
|
|
vi.mocked(spaceApi.proxyCreateSpace).mockResolvedValue(
|
|
makeSpace('space_new_blank', 'Untitled Space', 'blank')
|
|
);
|
|
|
|
await useSpaceStore.getState().hydrateFromServer(2);
|
|
|
|
const state = useSpaceStore.getState();
|
|
expect(spaceApi.proxyFetchSpaceProjects).toHaveBeenCalledWith('legacy_2');
|
|
expect(spaceApi.proxyCreateSpace).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
name: 'Untitled Space',
|
|
source_type: 'blank',
|
|
})
|
|
);
|
|
expect(Object.keys(state.spaces)).toEqual(['space_new_blank']);
|
|
expect(state.activeSpaceId).toBe('space_new_blank');
|
|
});
|
|
|
|
it('keeps the previously selected active space when hydrating existing spaces', async () => {
|
|
const spaceApi = await import('@/service/spaceApi');
|
|
vi.mocked(spaceApi.proxyFetchSpaces).mockResolvedValue([
|
|
makeSpace('space_recent', 'Recent Space', 'blank', '2'),
|
|
makeSpace('space_selected', 'Selected Space', 'blank', '2'),
|
|
]);
|
|
useSpaceStore.setState({
|
|
activeSpaceId: 'space_selected',
|
|
projectsSyncedAt: {
|
|
space_selected: Date.now(),
|
|
},
|
|
});
|
|
|
|
await useSpaceStore.getState().hydrateFromServer(2);
|
|
|
|
const state = useSpaceStore.getState();
|
|
expect(spaceApi.proxyCreateSpace).not.toHaveBeenCalled();
|
|
expect(Object.keys(state.spaces).sort()).toEqual([
|
|
'space_recent',
|
|
'space_selected',
|
|
]);
|
|
expect(state.activeSpaceId).toBe('space_selected');
|
|
});
|
|
|
|
it('creates a blank space for legacy migrations and selects it on first hydrate', async () => {
|
|
const spaceApi = await import('@/service/spaceApi');
|
|
vi.mocked(spaceApi.proxyFetchSpaces).mockResolvedValue([
|
|
makeSpace('legacy_2', 'Legacy Space', 'legacy', '2', { legacy: true }),
|
|
]);
|
|
vi.mocked(spaceApi.proxyFetchSpaceProjects).mockImplementation(
|
|
async (spaceId) =>
|
|
spaceId === 'legacy_2'
|
|
? [makeServerProject('project_legacy', 'legacy_2')]
|
|
: []
|
|
);
|
|
vi.mocked(spaceApi.proxyCreateSpace).mockResolvedValue(
|
|
makeSpace('space_migration_blank', 'Untitled Space', 'blank')
|
|
);
|
|
useSpaceStore.setState({
|
|
activeSpaceId: 'legacy_2',
|
|
});
|
|
|
|
await useSpaceStore.getState().hydrateFromServer(2);
|
|
|
|
const state = useSpaceStore.getState();
|
|
expect(spaceApi.proxyCreateSpace).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
name: 'Untitled Space',
|
|
source_type: 'blank',
|
|
})
|
|
);
|
|
expect(Object.keys(state.spaces).sort()).toEqual([
|
|
'legacy_2',
|
|
'space_migration_blank',
|
|
]);
|
|
expect(state.activeSpaceId).toBe('space_migration_blank');
|
|
});
|
|
|
|
it('coalesces concurrent project syncs for the same Space', async () => {
|
|
const spaceApi = await import('@/service/spaceApi');
|
|
const { proxyFetchGet } = await import('@/api/http');
|
|
let resolveProjects: ((projects: ServerProject[]) => void) | undefined;
|
|
vi.mocked(spaceApi.proxyFetchSpaceProjects).mockReturnValue(
|
|
new Promise<ServerProject[]>((resolve) => {
|
|
resolveProjects = resolve;
|
|
})
|
|
);
|
|
|
|
const firstSync = useSpaceStore
|
|
.getState()
|
|
.syncProjectsFromServer('space_new_blank');
|
|
const secondSync = useSpaceStore
|
|
.getState()
|
|
.syncProjectsFromServer('space_new_blank');
|
|
|
|
await vi.waitFor(() => {
|
|
expect(spaceApi.proxyFetchSpaceProjects).toHaveBeenCalledTimes(1);
|
|
expect(proxyFetchGet).toHaveBeenCalledTimes(1);
|
|
});
|
|
resolveProjects?.([]);
|
|
await Promise.all([firstSync, secondSync]);
|
|
|
|
expect(spaceApi.proxyFetchSpaceProjects).toHaveBeenCalledTimes(1);
|
|
expect(proxyFetchGet).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|