feat(cli): refactor indexCommand for improved error handling and add unit tests

This commit is contained in:
adonis 2026-03-20 19:36:07 -03:00
parent c1996f45d8
commit 680cefd00c
2 changed files with 159 additions and 27 deletions

View file

@ -9,17 +9,14 @@
* shared team index).
*/
import path from 'path';
import fs from 'fs/promises';
import { execFileSync } from 'child_process';
import path from "path";
import fs from "fs/promises";
import {
getStoragePaths,
loadMeta,
registerRepo,
addToGitignore,
getGlobalRegistryPath,
} from '../storage/repo-manager.js';
import { getGitRoot, isGitRepo } from '../storage/git.js';
} from "../storage/repo-manager.js";
import { getGitRoot, isGitRepo } from "../storage/git.js";
export interface IndexOptions {
force?: boolean;
@ -29,9 +26,11 @@ export const indexCommand = async (
inputPathParts?: string[],
options?: IndexOptions,
) => {
console.log('\n GitNexus Index\n');
console.log("\n GitNexus Index\n");
const inputPath = inputPathParts?.length ? inputPathParts.join(' ') : undefined;
const inputPath = inputPathParts?.length
? inputPathParts.join(" ")
: undefined;
let repoPath: string;
if (inputPath) {
@ -39,21 +38,28 @@ export const indexCommand = async (
} else {
const gitRoot = getGitRoot(process.cwd());
if (!gitRoot) {
console.log(' Not inside a git repository, try to run git init\n');
console.log(" Not inside a git repository, try to run git init\n");
process.exitCode = 1;
return;
}
repoPath = gitRoot;
}
const { storagePath, lbugPath, metaPath } = getStoragePaths(repoPath);
if (!isGitRepo(repoPath)) {
console.log(` Not a git repository: ${repoPath}`);
console.log(" Initialize one with `git init` or choose a valid repo path.\n");
process.exitCode = 1;
return;
}
const { storagePath, lbugPath } = getStoragePaths(repoPath);
// ── Verify .gitnexus/ exists ──────────────────────────────────────
try {
await fs.access(storagePath);
} catch {
console.log(` No .gitnexus/ folder found at: ${storagePath}`);
console.log(' Run `gitnexus analyze` to build the index first.\n');
console.log(" Run `gitnexus analyze` to build the index first.\n");
process.exitCode = 1;
return;
}
@ -63,7 +69,7 @@ export const indexCommand = async (
await fs.access(lbugPath);
} catch {
console.log(` .gitnexus/ folder exists but contains no LadybugDB index.`);
console.log(' Run `gitnexus analyze` to build the index.\n');
console.log(" Run `gitnexus analyze` to build the index.\n");
process.exitCode = 1;
return;
}
@ -74,8 +80,8 @@ export const indexCommand = async (
if (!meta) {
if (!options?.force) {
console.log(` .gitnexus/ exists but meta.json is missing.`);
console.log(' Use --force to register anyway (stats will be empty),');
console.log(' or run `gitnexus analyze` to rebuild properly.\n');
console.log(" Use --force to register anyway (stats will be empty),");
console.log(" or run `gitnexus analyze` to rebuild properly.\n");
process.exitCode = 1;
return;
}
@ -83,14 +89,13 @@ export const indexCommand = async (
// --force: build a minimal meta so the repo can be registered
meta = {
repoPath,
lastCommit: '',
lastCommit: "",
indexedAt: new Date().toISOString(),
};
}
// ── Register in global registry ───────────────────────────────────
await registerRepo(repoPath, meta);
await addToGitignore(repoPath);
const projectName = path.basename(repoPath);
const { stats } = meta;
@ -98,19 +103,17 @@ export const indexCommand = async (
console.log(` Repository registered: ${projectName}`);
if (stats) {
const parts: string[] = [];
if (stats.nodes != null) parts.push(`${stats.nodes.toLocaleString()} nodes`);
if (stats.edges != null) parts.push(`${stats.edges.toLocaleString()} edges`);
if (stats.nodes != null) {
parts.push(`${stats.nodes.toLocaleString()} nodes`);
}
if (stats.edges != null) {
parts.push(`${stats.edges.toLocaleString()} edges`);
}
if (stats.communities != null) parts.push(`${stats.communities} clusters`);
if (stats.processes != null) parts.push(`${stats.processes} flows`);
if (parts.length) console.log(` ${parts.join(' | ')}`);
if (parts.length) console.log(` ${parts.join(" | ")}`);
}
console.log(` ${repoPath}`);
try {
await fs.access(getGlobalRegistryPath());
} catch {
console.log('\n Tip: Run `gitnexus setup` to configure MCP for your editor.');
}
console.log('');
console.log("");
};

View file

@ -0,0 +1,129 @@
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 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,
}));
vi.mock('../../src/storage/git.js', () => ({
getGitRoot: mockGetGitRoot,
isGitRepo: mockIsGitRepo,
}));
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);
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(process.exitCode).toBeUndefined();
});
});