mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-11 08:37:35 +00:00
232 lines
8 KiB
TypeScript
232 lines
8 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const { runFullAnalysisMock, generateAIContextFilesMock, generateSkillFilesMock, cliErrorMock } =
|
|
vi.hoisted(() => {
|
|
const runFullAnalysisMock = vi.fn();
|
|
const generateAIContextFilesMock = vi.fn<
|
|
typeof import('../../src/cli/ai-context.js').generateAIContextFiles
|
|
>(async () => ({ files: [] as string[] }));
|
|
const generateSkillFilesMock = vi.fn(async () => ({
|
|
skills: [{ name: 'c', label: 'Community', symbolCount: 1, fileCount: 1 }],
|
|
outputPath: '/repo/.claude/skills',
|
|
}));
|
|
const cliErrorMock = vi.fn();
|
|
return {
|
|
runFullAnalysisMock,
|
|
generateAIContextFilesMock,
|
|
generateSkillFilesMock,
|
|
cliErrorMock,
|
|
};
|
|
});
|
|
|
|
vi.mock('../../src/core/run-analyze.js', () => ({
|
|
runFullAnalysis: runFullAnalysisMock,
|
|
}));
|
|
|
|
vi.mock('../../src/cli/ai-context.js', () => ({
|
|
generateAIContextFiles: generateAIContextFilesMock,
|
|
}));
|
|
|
|
vi.mock('../../src/cli/skill-gen.js', () => ({
|
|
generateSkillFiles: generateSkillFilesMock,
|
|
}));
|
|
|
|
vi.mock('../../src/cli/cli-message.js', () => ({
|
|
cliError: cliErrorMock,
|
|
}));
|
|
|
|
vi.mock('../../src/core/lbug/lbug-adapter.js', () => ({
|
|
closeLbug: vi.fn(async () => undefined),
|
|
closeLbugBeforeExit: vi.fn(async () => undefined),
|
|
isLbugReady: vi.fn(() => false),
|
|
}));
|
|
|
|
vi.mock('../../src/storage/repo-manager.js', () => ({
|
|
getStoragePaths: vi.fn(() => ({ storagePath: '.gitnexus', lbugPath: '.gitnexus/lbug' })),
|
|
getGlobalRegistryPath: vi.fn(() => 'registry.json'),
|
|
RegistryNameCollisionError: class RegistryNameCollisionError extends Error {},
|
|
AnalysisNotFinalizedError: class AnalysisNotFinalizedError extends Error {},
|
|
assertAnalysisFinalized: vi.fn(async () => undefined),
|
|
}));
|
|
|
|
vi.mock('../../src/storage/git.js', () => ({
|
|
getGitRoot: vi.fn(() => '/repo'),
|
|
hasGitDir: vi.fn(() => true),
|
|
// #243: default-branch auto-detection. Return null so the resolver falls back
|
|
// to "main" deterministically in this mocked environment.
|
|
getDefaultBranch: vi.fn(() => null),
|
|
}));
|
|
|
|
vi.mock('../../src/core/ingestion/utils/max-file-size.js', () => ({
|
|
getMaxFileSizeBannerMessage: vi.fn(() => null),
|
|
}));
|
|
|
|
describe('analyzeCommand commander → runFullAnalysis noStats bridge (#1477)', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
runFullAnalysisMock.mockReset();
|
|
runFullAnalysisMock.mockResolvedValue({
|
|
repoName: 'repo',
|
|
repoPath: '/repo',
|
|
stats: {},
|
|
alreadyUpToDate: true,
|
|
});
|
|
generateAIContextFilesMock.mockReset();
|
|
generateAIContextFilesMock.mockResolvedValue({ files: [] });
|
|
generateSkillFilesMock.mockReset();
|
|
generateSkillFilesMock.mockResolvedValue({
|
|
skills: [{ name: 'c', label: 'Community', symbolCount: 1, fileCount: 1 }],
|
|
outputPath: '/repo/.claude/skills',
|
|
});
|
|
cliErrorMock.mockReset();
|
|
process.exitCode = undefined;
|
|
process.env.NODE_OPTIONS = `${process.env.NODE_OPTIONS ?? ''} --max-old-space-size=8192`.trim();
|
|
});
|
|
|
|
it('maps commander-shaped stats:false to noStats:true (equivalent to --no-stats)', async () => {
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, { stats: false });
|
|
|
|
expect(runFullAnalysisMock).toHaveBeenCalledTimes(1);
|
|
const opts = runFullAnalysisMock.mock.calls[0][1];
|
|
expect(opts.noStats).toBe(true);
|
|
});
|
|
|
|
it('threads the capture-before-import runner receipt into runFullAnalysis', async () => {
|
|
const { analyzeCommandWithRunnerIdentity } = await import('../../src/cli/analyze.js');
|
|
const receipt = { schemaVersion: 4 } as never;
|
|
|
|
await analyzeCommandWithRunnerIdentity(receipt, undefined, {});
|
|
|
|
expect(runFullAnalysisMock.mock.calls[0]?.[3]).toBe(receipt);
|
|
});
|
|
|
|
it('maps omitted stats to noStats:false (default-on preserved)', async () => {
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, {});
|
|
|
|
const opts = runFullAnalysisMock.mock.calls[0][1];
|
|
expect(opts.noStats).toBe(false);
|
|
});
|
|
|
|
it('maps explicit stats:true to noStats:false', async () => {
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, { stats: true });
|
|
|
|
const opts = runFullAnalysisMock.mock.calls[0][1];
|
|
expect(opts.noStats).toBe(false);
|
|
});
|
|
|
|
it('still maps stats:false to noStats:true when skipAgentsMd is set', async () => {
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, { stats: false, skipAgentsMd: true });
|
|
|
|
const opts = runFullAnalysisMock.mock.calls[0][1];
|
|
expect(opts.noStats).toBe(true);
|
|
expect(opts.skipAgentsMd).toBe(true);
|
|
});
|
|
|
|
it('passes --repair-fts through to runFullAnalysis', async () => {
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, { repairFts: true });
|
|
|
|
const opts = runFullAnalysisMock.mock.calls[0][1];
|
|
expect(opts.repairFts).toBe(true);
|
|
});
|
|
|
|
it('maps --no-parse-cache to a cold parser run', async () => {
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, { parseCache: false });
|
|
|
|
const opts = runFullAnalysisMock.mock.calls[0][1];
|
|
expect(opts.useParseCache).toBe(false);
|
|
// Not folded into --force: runFullAnalysis names the bypass as its own
|
|
// rebuild reason (#3137), so no run announces a --force nobody passed.
|
|
expect(opts.force).toBeFalsy();
|
|
});
|
|
|
|
it('passes --skills through without folding it into --force', async () => {
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, { skills: true });
|
|
|
|
const opts = runFullAnalysisMock.mock.calls[0][1];
|
|
expect(opts.skills).toBe(true);
|
|
expect(opts.force).toBeFalsy();
|
|
});
|
|
|
|
it('reuses parser output by default', async () => {
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, {});
|
|
|
|
const opts = runFullAnalysisMock.mock.calls[0][1];
|
|
expect(opts.useParseCache).toBe(true);
|
|
});
|
|
|
|
it('rejects combining --repair-fts with --force', async () => {
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, { repairFts: true, force: true });
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
expect(cliErrorMock).toHaveBeenCalledWith(
|
|
expect.stringMatching(/cannot combine `--repair-fts` with a full rebuild/i),
|
|
);
|
|
expect(runFullAnalysisMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('rejects combining --repair-fts with --no-parse-cache', async () => {
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, { repairFts: true, parseCache: false });
|
|
|
|
expect(process.exitCode).toBe(1);
|
|
expect(runFullAnalysisMock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('passes stats:false as noStats to generateAIContextFiles on the --skills regeneration path (#1477)', async () => {
|
|
runFullAnalysisMock.mockResolvedValueOnce({
|
|
repoName: 'repo',
|
|
repoPath: '/repo',
|
|
stats: {
|
|
files: 1,
|
|
nodes: 10,
|
|
edges: 20,
|
|
communities: 0,
|
|
processes: 5,
|
|
},
|
|
alreadyUpToDate: false,
|
|
pipelineResult: { communityResult: undefined },
|
|
});
|
|
|
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
|
|
try {
|
|
const { analyzeCommand } = await import('../../src/cli/analyze.js');
|
|
|
|
await analyzeCommand(undefined, { skills: true, stats: false });
|
|
|
|
expect(generateSkillFilesMock).toHaveBeenCalledTimes(1);
|
|
expect(generateAIContextFilesMock).toHaveBeenCalledTimes(1);
|
|
const aiCtxOpts = generateAIContextFilesMock.mock.calls[0]![5];
|
|
expect(aiCtxOpts).toEqual({
|
|
skipAgentsMd: undefined,
|
|
skipSkills: undefined,
|
|
// #243: resolved default branch threaded into the --skills regen path.
|
|
defaultBranch: 'main',
|
|
noStats: true,
|
|
// #2086 M6: the --pdg gate is threaded too; false here (no --pdg flag).
|
|
hasPdg: false,
|
|
hasSpringActuator: false,
|
|
});
|
|
} finally {
|
|
exitSpy.mockRestore();
|
|
}
|
|
});
|
|
});
|