mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-22 00:31:17 +00:00
Some checks are pending
Release Candidate / Publish release candidate to npm (push) Blocked by required conditions
Release Candidate / Check if release candidate should run (push) Waiting to run
Release Candidate / ci (push) Blocked by required conditions
Release Candidate / Build & Push RC Docker images (push) Blocked by required conditions
* fix(cli): keep GitNexus ignores inside .gitnexus Avoid mutating analyzed repositories' root .gitignore while keeping generated GitNexus state untracked via .gitnexus/.gitignore. Made-with: Cursor * fix(cli): also use git info exclude for GitNexus storage When an analyzed repo has a real .git directory, add .gitnexus/ to .git/info/exclude so local Git metadata ignores generated storage without touching root .gitignore. Made-with: Cursor * fix(cli): keep skip-git subdir indexes ignored Ensure full analyze always writes the internal GitNexus ignore file so parent Git repositories stay clean for --skip-git subdirectory indexes. Made-with: Cursor
206 lines
7.3 KiB
TypeScript
206 lines
7.3 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import path from 'node:path';
|
|
|
|
const mockAccess = vi.fn();
|
|
const mockGetStoragePaths = vi.fn();
|
|
const mockLoadMeta = vi.fn();
|
|
const mockRegisterRepo = vi.fn();
|
|
const mockEnsureGitNexusIgnored = vi.fn();
|
|
const mockGetGitRoot = vi.fn();
|
|
const mockIsGitRepo = vi.fn();
|
|
|
|
vi.mock('fs/promises', () => ({
|
|
default: {
|
|
access: mockAccess,
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../src/storage/repo-manager.js', () => ({
|
|
getStoragePaths: mockGetStoragePaths,
|
|
loadMeta: mockLoadMeta,
|
|
registerRepo: mockRegisterRepo,
|
|
ensureGitNexusIgnored: mockEnsureGitNexusIgnored,
|
|
}));
|
|
|
|
vi.mock('../../src/storage/git.js', () => ({
|
|
getGitRoot: mockGetGitRoot,
|
|
isGitRepo: mockIsGitRepo,
|
|
// `index-repo.ts` calls `getRemoteUrl` to backfill `remoteUrl` on
|
|
// older `.gitnexus/meta.json` files. The unit tests don't care
|
|
// about the remote URL, so a static `undefined` keeps behaviour
|
|
// identical to the pre-feature path.
|
|
getRemoteUrl: vi.fn().mockReturnValue(undefined),
|
|
}));
|
|
|
|
describe('indexCommand', () => {
|
|
const resolvedRepo = path.resolve('/repo');
|
|
const resolvedOutside = path.resolve('/outside/path');
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.restoreAllMocks();
|
|
process.exitCode = undefined;
|
|
|
|
mockGetStoragePaths.mockImplementation((repoPath: string) => ({
|
|
storagePath: `${repoPath}/.gitnexus`,
|
|
lbugPath: `${repoPath}/.gitnexus/lbug`,
|
|
metaPath: `${repoPath}/.gitnexus/meta.json`,
|
|
}));
|
|
mockLoadMeta.mockResolvedValue({
|
|
repoPath: resolvedRepo,
|
|
lastCommit: 'abc123',
|
|
indexedAt: '2026-03-20T00:00:00.000Z',
|
|
stats: { nodes: 10, edges: 20 },
|
|
});
|
|
mockAccess.mockResolvedValue(undefined);
|
|
mockEnsureGitNexusIgnored.mockResolvedValue(undefined);
|
|
mockGetGitRoot.mockReturnValue(resolvedRepo);
|
|
mockIsGitRepo.mockReturnValue(true);
|
|
});
|
|
|
|
it('fails when target path is not a git repository', async () => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
mockIsGitRepo.mockReturnValue(false);
|
|
|
|
const { indexCommand } = await import('../../src/cli/index-repo.js');
|
|
await indexCommand(['/outside/path']);
|
|
|
|
expect(mockRegisterRepo).not.toHaveBeenCalled();
|
|
expect(process.exitCode).toBe(1);
|
|
expect(logSpy).toHaveBeenCalledWith(` Not a git repository: ${resolvedOutside}`);
|
|
});
|
|
|
|
it('fails when .gitnexus folder does not exist', async () => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
mockAccess.mockRejectedValueOnce(new Error('missing .gitnexus'));
|
|
|
|
const { indexCommand } = await import('../../src/cli/index-repo.js');
|
|
await indexCommand(['/repo']);
|
|
|
|
expect(mockRegisterRepo).not.toHaveBeenCalled();
|
|
expect(process.exitCode).toBe(1);
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
` No .gitnexus/ folder found at: ${resolvedRepo}/.gitnexus`,
|
|
);
|
|
});
|
|
|
|
it('fails when lbug database does not exist', async () => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
mockAccess.mockResolvedValueOnce(undefined).mockRejectedValueOnce(new Error('missing lbug'));
|
|
|
|
const { indexCommand } = await import('../../src/cli/index-repo.js');
|
|
await indexCommand(['/repo']);
|
|
|
|
expect(mockRegisterRepo).not.toHaveBeenCalled();
|
|
expect(process.exitCode).toBe(1);
|
|
expect(logSpy).toHaveBeenCalledWith(
|
|
' .gitnexus/ folder exists but contains no LadybugDB index.',
|
|
);
|
|
});
|
|
|
|
it('fails when meta.json is missing and --force is not set', async () => {
|
|
mockLoadMeta.mockResolvedValue(null);
|
|
|
|
const { indexCommand } = await import('../../src/cli/index-repo.js');
|
|
await indexCommand(['/repo']);
|
|
|
|
expect(mockRegisterRepo).not.toHaveBeenCalled();
|
|
expect(process.exitCode).toBe(1);
|
|
});
|
|
|
|
it('registers with minimal metadata when meta is missing and --force is set', async () => {
|
|
mockLoadMeta.mockResolvedValue(null);
|
|
|
|
const { indexCommand } = await import('../../src/cli/index-repo.js');
|
|
await indexCommand(['/repo'], { force: true });
|
|
|
|
expect(mockRegisterRepo).toHaveBeenCalledTimes(1);
|
|
expect(mockRegisterRepo).toHaveBeenCalledWith(
|
|
resolvedRepo,
|
|
expect.objectContaining({
|
|
repoPath: resolvedRepo,
|
|
lastCommit: '',
|
|
}),
|
|
);
|
|
expect(process.exitCode).toBeUndefined();
|
|
});
|
|
|
|
it('registers successfully with existing metadata', async () => {
|
|
const { indexCommand } = await import('../../src/cli/index-repo.js');
|
|
await indexCommand(['/repo']);
|
|
|
|
expect(mockRegisterRepo).toHaveBeenCalledTimes(1);
|
|
expect(mockRegisterRepo).toHaveBeenCalledWith(
|
|
resolvedRepo,
|
|
expect.objectContaining({ repoPath: resolvedRepo }),
|
|
);
|
|
expect(mockEnsureGitNexusIgnored).toHaveBeenCalledTimes(1);
|
|
expect(mockEnsureGitNexusIgnored).toHaveBeenCalledWith(resolvedRepo);
|
|
expect(process.exitCode).toBeUndefined();
|
|
});
|
|
|
|
it('registers non-git path when --allow-non-git is set', async () => {
|
|
mockIsGitRepo.mockReturnValue(false);
|
|
|
|
const { indexCommand } = await import('../../src/cli/index-repo.js');
|
|
await indexCommand(['/outside/path'], { allowNonGit: true });
|
|
|
|
expect(mockRegisterRepo).toHaveBeenCalledTimes(1);
|
|
expect(process.exitCode).toBeUndefined();
|
|
});
|
|
|
|
it('fails when called with no path and cwd is not inside a git repo', async () => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
mockGetGitRoot.mockReturnValue(null);
|
|
|
|
const { indexCommand } = await import('../../src/cli/index-repo.js');
|
|
await indexCommand(); // no args
|
|
|
|
expect(mockRegisterRepo).not.toHaveBeenCalled();
|
|
expect(process.exitCode).toBe(1);
|
|
expect(logSpy).toHaveBeenCalledWith(' Not inside a git repository, try to run git init\n');
|
|
});
|
|
|
|
it('registers from cwd when no path is provided', async () => {
|
|
// getGitRoot already mocked to return resolvedRepo in beforeEach
|
|
const { indexCommand } = await import('../../src/cli/index-repo.js');
|
|
await indexCommand(); // no args
|
|
|
|
expect(mockRegisterRepo).toHaveBeenCalledWith(
|
|
resolvedRepo,
|
|
expect.objectContaining({ repoPath: resolvedRepo }),
|
|
);
|
|
expect(mockEnsureGitNexusIgnored).toHaveBeenCalledWith(resolvedRepo);
|
|
expect(process.exitCode).toBeUndefined();
|
|
});
|
|
|
|
it('fails when multiple path parts do not resolve to a single existing path', async () => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
const ambiguousPath = path.resolve('/repo /other');
|
|
|
|
mockAccess.mockImplementation(async (targetPath: string) => {
|
|
if (targetPath === ambiguousPath) {
|
|
throw new Error('missing combined path');
|
|
}
|
|
return undefined;
|
|
});
|
|
|
|
const { indexCommand } = await import('../../src/cli/index-repo.js');
|
|
await indexCommand(['/repo', '/other']);
|
|
|
|
expect(mockRegisterRepo).not.toHaveBeenCalled();
|
|
expect(mockEnsureGitNexusIgnored).not.toHaveBeenCalled();
|
|
expect(process.exitCode).toBe(1);
|
|
expect(logSpy).toHaveBeenCalledWith(' The `index` command accepts a single path only.');
|
|
});
|
|
|
|
it('prints node and edge stats after registration', async () => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
|
|
const { indexCommand } = await import('../../src/cli/index-repo.js');
|
|
await indexCommand(['/repo']);
|
|
|
|
expect(logSpy).toHaveBeenCalledWith(' Repository registered: repo');
|
|
expect(logSpy).toHaveBeenCalledWith(' 10 nodes | 20 edges');
|
|
});
|
|
});
|