GitNexus/gitnexus/test/unit/index-repo-command.test.ts
Copilot 962f22482b
Some checks are pending
CI / quality (push) Waiting to run
CI / tests (push) Waiting to run
CI / e2e (push) Waiting to run
CI / scope-parity (push) Waiting to run
CI / Save PR Metadata (push) Blocked by required conditions
CI / CI Gate (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 / Publish release candidate to npm (push) Blocked by required conditions
Release Candidate / Build & Push RC Docker images (push) Blocked by required conditions
feat(cli): Fingerprint indexed repos by remote URL to detect sibling-clone graph drift (#982)
* Initial plan

* feat: detect sibling-clone graph drift via remote URL fingerprint

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e5decb67-7fec-40e7-b2a1-b5e94a0d393f

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test: address review feedback — fake commit, same-commit case, regex docs

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e5decb67-7fec-40e7-b2a1-b5e94a0d393f

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix(mcp): address review feedback — CI green, perf, dead branch, one-shot test

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/cc2259f7-94e4-4243-aaa9-e03b7c632d32

* Merge branch 'main' into copilot/fix-single-path-indexing-issue

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/5840b3dd-e879-4854-a067-d1622bec2634

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* Merge branch 'main' into copilot/fix-single-path-indexing-issue

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/9025262f-4dd4-4774-8f32-e14434100004

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* style: prettier format run-analyze.ts after merge with main

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/a7be18dd-102f-4a7b-ac56-53fbd414fe3b

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* test: realpath both sides of cwdGitRoot assertion for Windows 8.3 short-name compat

Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b2a1c6a3-e454-4b87-b0e4-69d7c0d9a51b

Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>

* fix(test): use path-agnostic assertion for cwdGitRoot on Windows (#1015)

git rev-parse --show-toplevel returns long path names on Windows
while os.tmpdir() returns 8.3 short names. fs.realpathSync does not
expand short names, so exact path comparison always fails on Windows
CI runners. Replace with behavioral assertions instead.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: magyargergo <11230420+magyargergo@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <copilot-swe-agent[bot]@users.noreply.github.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: evolution <wjc163@sina.cn>
2026-04-21 21:58:54 +01:00

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 mockAddToGitignore = 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,
addToGitignore: mockAddToGitignore,
}));
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);
mockAddToGitignore.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(mockAddToGitignore).toHaveBeenCalledTimes(1);
expect(mockAddToGitignore).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(mockAddToGitignore).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(mockAddToGitignore).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');
});
});