- Added architectural plan and iteration plan for the in-app project editor. - Introduced new components for the editor, including CodeEditorOverlay, FileTreePanel, and EditorTabsPanel. - Established state management using Zustand for editor state persistence. - Implemented IPC channels for file operations and editor functionality. - Enhanced TeamDetailView with a button to open the editor overlay. - Conducted reuse analysis for existing components to optimize codebase integration.
292 lines
9.4 KiB
TypeScript
292 lines
9.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { resolveProjectIdByPath } from '@renderer/utils/projectLookup';
|
|
|
|
import type { Project, RepositoryGroup } from '@renderer/types/data';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Minimal fixtures
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type ProjectLike = Pick<Project, 'id' | 'path'>;
|
|
type RepoGroupLike = Pick<RepositoryGroup, 'worktrees'>;
|
|
|
|
const CRYPTO_PROJECT: ProjectLike = {
|
|
id: '-Users-belief-dev-projects-crypto-research',
|
|
path: '/Users/belief/dev/projects/crypto_research',
|
|
};
|
|
|
|
const CLAUDE_PROJECT: ProjectLike = {
|
|
id: '-Users-belief-dev-projects-claude-claude-team',
|
|
path: '/Users/belief/dev/projects/claude/claude_team',
|
|
};
|
|
|
|
function makeRepoGroup(worktrees: { id: string; path: string }[]): RepoGroupLike {
|
|
return {
|
|
worktrees: worktrees.map((w) => ({
|
|
...w,
|
|
name: w.id,
|
|
gitBranch: 'main',
|
|
isMainWorktree: true,
|
|
source: 'unknown' as const,
|
|
sessions: [],
|
|
createdAt: 0,
|
|
})),
|
|
};
|
|
}
|
|
|
|
const CRYPTO_REPO_GROUP = makeRepoGroup([
|
|
{
|
|
id: '-Users-belief-dev-projects-crypto-research',
|
|
path: '/Users/belief/dev/projects/crypto_research',
|
|
},
|
|
]);
|
|
|
|
const CLAUDE_REPO_GROUP = makeRepoGroup([
|
|
{
|
|
id: '-Users-belief-dev-projects-claude-claude-team',
|
|
path: '/Users/belief/dev/projects/claude/claude_team',
|
|
},
|
|
]);
|
|
|
|
const MULTI_WORKTREE_GROUP = makeRepoGroup([
|
|
{
|
|
id: '-Users-belief-dev-projects-app',
|
|
path: '/Users/belief/dev/projects/app',
|
|
},
|
|
{
|
|
id: '-Users-belief-dev-projects-app-wt-feature',
|
|
path: '/Users/belief/dev/projects/app-wt-feature',
|
|
},
|
|
]);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('resolveProjectIdByPath', () => {
|
|
// -----------------------------------------------------------------------
|
|
// Null / undefined / empty input
|
|
// -----------------------------------------------------------------------
|
|
describe('null/undefined/empty projectPath', () => {
|
|
it('returns null for undefined projectPath', () => {
|
|
expect(resolveProjectIdByPath(undefined, [CRYPTO_PROJECT], [])).toBeNull();
|
|
});
|
|
|
|
it('returns null for null projectPath', () => {
|
|
expect(resolveProjectIdByPath(null, [CRYPTO_PROJECT], [])).toBeNull();
|
|
});
|
|
|
|
it('returns null for empty string projectPath', () => {
|
|
expect(resolveProjectIdByPath('', [CRYPTO_PROJECT], [])).toBeNull();
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Lookup from projects (flat view mode)
|
|
// -----------------------------------------------------------------------
|
|
describe('lookup from projects (flat mode)', () => {
|
|
it('finds project by exact path match', () => {
|
|
expect(
|
|
resolveProjectIdByPath(
|
|
'/Users/belief/dev/projects/crypto_research',
|
|
[CRYPTO_PROJECT, CLAUDE_PROJECT],
|
|
[]
|
|
)
|
|
).toBe('-Users-belief-dev-projects-crypto-research');
|
|
});
|
|
|
|
it('returns null when path not in projects', () => {
|
|
expect(
|
|
resolveProjectIdByPath('/Users/belief/dev/projects/unknown', [CRYPTO_PROJECT], [])
|
|
).toBeNull();
|
|
});
|
|
|
|
it('returns null when projects list is empty', () => {
|
|
expect(
|
|
resolveProjectIdByPath('/Users/belief/dev/projects/crypto_research', [], [])
|
|
).toBeNull();
|
|
});
|
|
|
|
it('does not do substring matching', () => {
|
|
expect(
|
|
resolveProjectIdByPath(
|
|
'/Users/belief/dev/projects/crypto_research/subdir',
|
|
[CRYPTO_PROJECT],
|
|
[]
|
|
)
|
|
).toBeNull();
|
|
});
|
|
|
|
it('does not do prefix matching', () => {
|
|
expect(
|
|
resolveProjectIdByPath('/Users/belief/dev/projects/crypto', [CRYPTO_PROJECT], [])
|
|
).toBeNull();
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Lookup from repositoryGroups (grouped view mode)
|
|
// -----------------------------------------------------------------------
|
|
describe('lookup from repositoryGroups (grouped mode)', () => {
|
|
it('finds project in worktrees when projects is empty', () => {
|
|
expect(
|
|
resolveProjectIdByPath(
|
|
'/Users/belief/dev/projects/crypto_research',
|
|
[],
|
|
[CRYPTO_REPO_GROUP]
|
|
)
|
|
).toBe('-Users-belief-dev-projects-crypto-research');
|
|
});
|
|
|
|
it('finds project across multiple repo groups', () => {
|
|
expect(
|
|
resolveProjectIdByPath(
|
|
'/Users/belief/dev/projects/claude/claude_team',
|
|
[],
|
|
[CRYPTO_REPO_GROUP, CLAUDE_REPO_GROUP]
|
|
)
|
|
).toBe('-Users-belief-dev-projects-claude-claude-team');
|
|
});
|
|
|
|
it('finds correct worktree in multi-worktree group', () => {
|
|
expect(
|
|
resolveProjectIdByPath(
|
|
'/Users/belief/dev/projects/app-wt-feature',
|
|
[],
|
|
[MULTI_WORKTREE_GROUP]
|
|
)
|
|
).toBe('-Users-belief-dev-projects-app-wt-feature');
|
|
});
|
|
|
|
it('returns null when path not in any worktree', () => {
|
|
expect(
|
|
resolveProjectIdByPath('/Users/belief/dev/projects/unknown', [], [CRYPTO_REPO_GROUP])
|
|
).toBeNull();
|
|
});
|
|
|
|
it('returns null when repositoryGroups is empty', () => {
|
|
expect(
|
|
resolveProjectIdByPath('/Users/belief/dev/projects/crypto_research', [], [])
|
|
).toBeNull();
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Priority: projects takes precedence over repositoryGroups
|
|
// -----------------------------------------------------------------------
|
|
describe('priority order', () => {
|
|
it('prefers projects match over repositoryGroups match', () => {
|
|
const projectWithDifferentId: ProjectLike = {
|
|
id: 'flat-mode-id',
|
|
path: '/Users/belief/dev/projects/crypto_research',
|
|
};
|
|
|
|
const repoGroupWithDifferentId = makeRepoGroup([
|
|
{
|
|
id: 'grouped-mode-id',
|
|
path: '/Users/belief/dev/projects/crypto_research',
|
|
},
|
|
]);
|
|
|
|
expect(
|
|
resolveProjectIdByPath(
|
|
'/Users/belief/dev/projects/crypto_research',
|
|
[projectWithDifferentId],
|
|
[repoGroupWithDifferentId]
|
|
)
|
|
).toBe('flat-mode-id');
|
|
});
|
|
|
|
it('falls back to repositoryGroups when projects has no match', () => {
|
|
expect(
|
|
resolveProjectIdByPath(
|
|
'/Users/belief/dev/projects/crypto_research',
|
|
[CLAUDE_PROJECT], // different project, no match
|
|
[CRYPTO_REPO_GROUP]
|
|
)
|
|
).toBe('-Users-belief-dev-projects-crypto-research');
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Both sources populated (e.g. user switched view modes)
|
|
// -----------------------------------------------------------------------
|
|
describe('both sources populated', () => {
|
|
it('resolves from projects even when same data in groups', () => {
|
|
expect(
|
|
resolveProjectIdByPath(
|
|
'/Users/belief/dev/projects/crypto_research',
|
|
[CRYPTO_PROJECT],
|
|
[CRYPTO_REPO_GROUP]
|
|
)
|
|
).toBe('-Users-belief-dev-projects-crypto-research');
|
|
});
|
|
|
|
it('resolves path only in groups when projects has different entries', () => {
|
|
expect(
|
|
resolveProjectIdByPath(
|
|
'/Users/belief/dev/projects/claude/claude_team',
|
|
[CRYPTO_PROJECT],
|
|
[CLAUDE_REPO_GROUP]
|
|
)
|
|
).toBe('-Users-belief-dev-projects-claude-claude-team');
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Edge cases: path format variations
|
|
// -----------------------------------------------------------------------
|
|
describe('path format edge cases', () => {
|
|
it('does not normalize trailing slashes — exact match required', () => {
|
|
expect(
|
|
resolveProjectIdByPath(
|
|
'/Users/belief/dev/projects/crypto_research/',
|
|
[CRYPTO_PROJECT],
|
|
[CRYPTO_REPO_GROUP]
|
|
)
|
|
).toBeNull();
|
|
});
|
|
|
|
it('is case-sensitive', () => {
|
|
expect(
|
|
resolveProjectIdByPath(
|
|
'/Users/belief/dev/projects/Crypto_Research',
|
|
[CRYPTO_PROJECT],
|
|
[CRYPTO_REPO_GROUP]
|
|
)
|
|
).toBeNull();
|
|
});
|
|
|
|
it('handles Windows-style paths if stored that way', () => {
|
|
const winProject: ProjectLike = {
|
|
id: 'C--Users-name-project',
|
|
path: 'C:\\Users\\name\\project',
|
|
};
|
|
expect(resolveProjectIdByPath('C:\\Users\\name\\project', [winProject], [])).toBe(
|
|
'C--Users-name-project'
|
|
);
|
|
});
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Regression: the original bug scenario
|
|
// -----------------------------------------------------------------------
|
|
describe('regression: grouped view mode with no flat projects', () => {
|
|
it('resolves team projectPath when only repositoryGroups is populated', () => {
|
|
// This is the exact scenario that caused "Project not found":
|
|
// viewMode=grouped → fetchRepositoryGroups() is called, fetchProjects() is NOT
|
|
// → projects=[] but repositoryGroups has the data
|
|
const emptyProjects: ProjectLike[] = [];
|
|
const populatedGroups: RepoGroupLike[] = [CRYPTO_REPO_GROUP, CLAUDE_REPO_GROUP];
|
|
|
|
expect(
|
|
resolveProjectIdByPath(
|
|
'/Users/belief/dev/projects/crypto_research',
|
|
emptyProjects,
|
|
populatedGroups
|
|
)
|
|
).toBe('-Users-belief-dev-projects-crypto-research');
|
|
});
|
|
});
|
|
});
|