import path from 'node:path'; import { describe, expect, it, vi } from 'vitest'; import { createProjectStorageFromStore, createMemoryProjectStorage, createProjectStore, getProjectIdForPath, } from '@electron/opencode/project-store'; function expectedNormalizedPath(input: string): string { const normalized = path.normalize(path.resolve(input.trim())); return process.platform === 'win32' ? normalized.toLowerCase() : normalized; } describe('opencode project store', () => { it('normalizes equivalent paths to the same project id', () => { const base = path.join('D:', 'repo', 'packages', 'ui'); const equivalent = path.join('D:', 'repo', 'packages', 'ui', '..', 'ui', '.'); const id = getProjectIdForPath(base); expect(id).toMatch(/^prj_[0-9a-f]{16}$/); expect(getProjectIdForPath(equivalent)).toBe(id); }); it('opens the exact normalized folder without expanding to the git root', async () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-05-12T01:02:03.000Z')); try { const store = createProjectStore(createMemoryProjectStorage()); const input = 'D:/repo/packages/ui'; const project = await store.openFolder(input); expect(project.path).toBe(expectedNormalizedPath(input)); expect(project.path).not.toBe(expectedNormalizedPath('D:/repo')); expect(project.name).toBe('ui'); await expect(store.getActiveProject()).resolves.toEqual(project); } finally { vi.useRealTimers(); } }); it('keeps subfolders in the same repo separate and lists newest first', async () => { vi.useFakeTimers(); try { const store = createProjectStore(createMemoryProjectStorage()); vi.setSystemTime(new Date('2026-05-12T00:00:00.000Z')); const api = await store.openFolder('D:/repo/packages/api'); vi.setSystemTime(new Date('2026-05-12T00:01:00.000Z')); const ui = await store.openFolder('D:/repo/packages/ui'); vi.setSystemTime(new Date('2026-05-12T00:02:00.000Z')); const reopenedApi = await store.openFolder('D:/repo/packages/api'); expect(reopenedApi.id).toBe(api.id); expect(reopenedApi.createdAt).toBe(api.createdAt); expect(reopenedApi.updatedAt).toBe('2026-05-12T00:02:00.000Z'); expect(reopenedApi.lastOpenedAt).toBe('2026-05-12T00:02:00.000Z'); const projects = await store.listProjects(); expect(ui.id).not.toBe(api.id); expect(projects.map((project) => project.id)).toEqual([api.id, ui.id]); expect(projects.map((project) => project.path)).toEqual([ expectedNormalizedPath('D:/repo/packages/api'), expectedNormalizedPath('D:/repo/packages/ui'), ]); } finally { vi.useRealTimers(); } }); it('removes a project from the list without touching other projects', async () => { const store = createProjectStore(createMemoryProjectStorage()); const api = await store.openFolder('D:/repo/packages/api'); const ui = await store.openFolder('D:/repo/packages/ui'); await expect(store.getActiveProject()).resolves.toEqual(ui); await store.removeProject(ui.id); await expect(store.listProjects()).resolves.toEqual([api]); await expect(store.getActiveProject()).resolves.toBeNull(); }); it('persists project data through a key-value backing store', async () => { const backing = new Map(); const storage = createProjectStorageFromStore({ get: (key) => backing.get(key), set: (key, value) => backing.set(key, value), }); const firstStore = createProjectStore(storage); const project = await firstStore.openFolder('D:/repo/packages/ui'); const secondStore = createProjectStore(storage); await expect(secondStore.getActiveProject()).resolves.toEqual(project); await expect(secondStore.listProjects()).resolves.toEqual([project]); }); });