agent-ecosystem/test/renderer/utils/projectLookup.test.ts
iliya 0c2f70b2b2 feat: implement in-app project editor with CodeMirror integration
- 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.
2026-02-27 22:36:06 +02:00

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');
});
});
});