mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-12 23:02:45 +00:00
fix: expose detect-changes in direct CLI
Squashed commits: - test: fix risk_level mock case and prettier formatting in tool-direct-cli.test - test: add edge-case coverage for detectChangesCommand formatter
This commit is contained in:
parent
d976038dc8
commit
4dc8a0c9f9
4 changed files with 181 additions and 0 deletions
|
|
@ -151,6 +151,15 @@ program
|
|||
.option('-r, --repo <name>', 'Target repository')
|
||||
.action(createLazyAction(() => import('./tool.js'), 'cypherCommand'));
|
||||
|
||||
program
|
||||
.command('detect-changes')
|
||||
.alias('detect_changes')
|
||||
.description('Map git diff hunks to indexed symbols and affected execution flows')
|
||||
.option('-s, --scope <scope>', 'What to analyze: unstaged, staged, all, or compare', 'unstaged')
|
||||
.option('-b, --base-ref <ref>', 'Branch/commit for compare scope (e.g. main)')
|
||||
.option('-r, --repo <name>', 'Target repository')
|
||||
.action(createLazyAction(() => import('./tool.js'), 'detectChangesCommand'));
|
||||
|
||||
// ─── Eval Server (persistent daemon for SWE-bench) ─────────────────
|
||||
|
||||
program
|
||||
|
|
|
|||
|
|
@ -164,3 +164,55 @@ export async function cypherCommand(
|
|||
});
|
||||
output(result);
|
||||
}
|
||||
|
||||
function formatDetectChangesResult(result: any): string {
|
||||
if (result?.error) return `Error: ${result.error}`;
|
||||
|
||||
const summary = result?.summary || {};
|
||||
if ((summary.changed_count || 0) === 0) {
|
||||
return 'No changes detected.';
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push(`Changes: ${summary.changed_files || 0} files, ${summary.changed_count || 0} symbols`);
|
||||
lines.push(`Affected processes: ${summary.affected_count || 0}`);
|
||||
lines.push(`Risk level: ${summary.risk_level || 'unknown'}`);
|
||||
lines.push('');
|
||||
|
||||
const changed = result?.changed_symbols || [];
|
||||
if (changed.length > 0) {
|
||||
lines.push('Changed symbols:');
|
||||
for (const symbol of changed.slice(0, 15)) {
|
||||
lines.push(` ${symbol.type} ${symbol.name} → ${symbol.filePath}`);
|
||||
}
|
||||
if (changed.length > 15) {
|
||||
lines.push(` ... and ${changed.length - 15} more`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const affected = result?.affected_processes || [];
|
||||
if (affected.length > 0) {
|
||||
lines.push('Affected execution flows:');
|
||||
for (const processInfo of affected.slice(0, 10)) {
|
||||
const steps = (processInfo.changed_steps || []).map((s: any) => s.symbol).join(', ');
|
||||
lines.push(` • ${processInfo.name} (${processInfo.step_count} steps) — changed: ${steps}`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n').trim();
|
||||
}
|
||||
|
||||
export async function detectChangesCommand(options?: {
|
||||
scope?: string;
|
||||
baseRef?: string;
|
||||
repo?: string;
|
||||
}): Promise<void> {
|
||||
const backend = await getBackend();
|
||||
const result = await backend.callTool('detect_changes', {
|
||||
scope: options?.scope || 'unstaged',
|
||||
base_ref: options?.baseRef,
|
||||
repo: options?.repo,
|
||||
});
|
||||
output(formatDetectChangesResult(result));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,16 @@ describe('CLI help surface', () => {
|
|||
expect(result.stdout).toContain('--repo <name>');
|
||||
});
|
||||
|
||||
it('detect-changes help exposes compare scope and base-ref flags', () => {
|
||||
const result = runHelp('detect-changes');
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain('gitnexus detect-changes|detect_changes [options]');
|
||||
expect(result.stdout).toContain('--scope <scope>');
|
||||
expect(result.stdout).toContain('--base-ref <ref>');
|
||||
expect(result.stdout).toContain('--repo <name>');
|
||||
});
|
||||
|
||||
it('wiki help shows provider, review, and verbose flags', () => {
|
||||
const result = runHelp('wiki');
|
||||
|
||||
|
|
|
|||
110
gitnexus/test/unit/tool-direct-cli.test.ts
Normal file
110
gitnexus/test/unit/tool-direct-cli.test.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const initMock = vi.fn();
|
||||
const callToolMock = vi.fn();
|
||||
const writeSyncMock = vi.fn();
|
||||
|
||||
vi.mock('../../src/mcp/local/local-backend.js', () => ({
|
||||
LocalBackend: class {
|
||||
init = initMock;
|
||||
callTool = callToolMock;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('node:fs', () => ({
|
||||
writeSync: writeSyncMock,
|
||||
}));
|
||||
|
||||
describe('direct CLI tool commands', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
initMock.mockReset();
|
||||
callToolMock.mockReset();
|
||||
writeSyncMock.mockReset();
|
||||
initMock.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it('dispatches detect_changes with CLI-shaped arguments', async () => {
|
||||
callToolMock.mockResolvedValue({
|
||||
summary: {
|
||||
changed_files: 1,
|
||||
changed_count: 2,
|
||||
affected_count: 1,
|
||||
risk_level: 'low',
|
||||
},
|
||||
});
|
||||
const { detectChangesCommand } = await import('../../src/cli/tool.js');
|
||||
|
||||
await detectChangesCommand({
|
||||
scope: 'compare',
|
||||
baseRef: 'main',
|
||||
repo: 'gitnexus',
|
||||
});
|
||||
|
||||
expect(callToolMock).toHaveBeenCalledWith('detect_changes', {
|
||||
scope: 'compare',
|
||||
base_ref: 'main',
|
||||
repo: 'gitnexus',
|
||||
});
|
||||
expect(writeSyncMock).toHaveBeenCalledWith(1, expect.stringContaining('Risk level: low'));
|
||||
});
|
||||
|
||||
it('prints "No changes detected." when changed_count is 0', async () => {
|
||||
callToolMock.mockResolvedValue({
|
||||
summary: { changed_files: 0, changed_count: 0, affected_count: 0, risk_level: 'low' },
|
||||
});
|
||||
const { detectChangesCommand } = await import('../../src/cli/tool.js');
|
||||
|
||||
await detectChangesCommand({});
|
||||
|
||||
expect(writeSyncMock).toHaveBeenCalledWith(1, expect.stringContaining('No changes detected.'));
|
||||
});
|
||||
|
||||
it('prints error message when result contains an error', async () => {
|
||||
callToolMock.mockResolvedValue({ error: 'index is stale' });
|
||||
const { detectChangesCommand } = await import('../../src/cli/tool.js');
|
||||
|
||||
await detectChangesCommand({});
|
||||
|
||||
expect(writeSyncMock).toHaveBeenCalledWith(1, expect.stringContaining('Error: index is stale'));
|
||||
});
|
||||
|
||||
it('truncates changed_symbols list beyond 15 and shows overflow count', async () => {
|
||||
const symbols = Array.from({ length: 17 }, (_, i) => ({
|
||||
type: 'function',
|
||||
name: `fn${i}`,
|
||||
filePath: `src/file${i}.ts`,
|
||||
}));
|
||||
callToolMock.mockResolvedValue({
|
||||
summary: { changed_files: 17, changed_count: 17, affected_count: 0, risk_level: 'low' },
|
||||
changed_symbols: symbols,
|
||||
});
|
||||
const { detectChangesCommand } = await import('../../src/cli/tool.js');
|
||||
|
||||
await detectChangesCommand({});
|
||||
|
||||
const output: string = writeSyncMock.mock.calls[0][1];
|
||||
expect(output).toContain('function fn14 → src/file14.ts');
|
||||
expect(output).not.toContain('fn15');
|
||||
expect(output).toContain('... and 2 more');
|
||||
});
|
||||
|
||||
it('truncates affected_processes list beyond 10', async () => {
|
||||
const processes = Array.from({ length: 12 }, (_, i) => ({
|
||||
name: `proc${i}`,
|
||||
step_count: 3,
|
||||
changed_steps: [{ symbol: `sym${i}` }],
|
||||
}));
|
||||
callToolMock.mockResolvedValue({
|
||||
summary: { changed_files: 1, changed_count: 1, affected_count: 12, risk_level: 'low' },
|
||||
affected_processes: processes,
|
||||
});
|
||||
const { detectChangesCommand } = await import('../../src/cli/tool.js');
|
||||
|
||||
await detectChangesCommand({});
|
||||
|
||||
const output: string = writeSyncMock.mock.calls[0][1];
|
||||
expect(output).toContain('proc9');
|
||||
expect(output).not.toContain('proc10');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue