diff --git a/gitnexus/src/cli/index.ts b/gitnexus/src/cli/index.ts index 745ebe9f5..b599f7e9e 100644 --- a/gitnexus/src/cli/index.ts +++ b/gitnexus/src/cli/index.ts @@ -219,7 +219,7 @@ program .action(createLbugLazyAction(() => import('./tool.js'), 'contextCommand')); program - .command('impact ') + .command('impact [target]') .description('Blast radius analysis: what breaks if you change a symbol') .option('-d, --direction ', 'upstream (dependants) or downstream (dependencies)', 'upstream') .option('-r, --repo ', 'Target repository') diff --git a/gitnexus/src/cli/tool.ts b/gitnexus/src/cli/tool.ts index a4bc80bea..0d9ad22f6 100644 --- a/gitnexus/src/cli/tool.ts +++ b/gitnexus/src/cli/tool.ts @@ -94,6 +94,11 @@ export async function contextCommand( content?: boolean; }, ): Promise { + // Reject a `--`-prefixed uid swallowed from a following flag (see impactCommand). + if (options?.uid?.startsWith('--')) { + cliErrorKey('tool.usage.context'); + process.exit(1); + } if (!name?.trim() && !options?.uid) { cliErrorKey('tool.usage.context'); process.exit(1); @@ -111,7 +116,7 @@ export async function contextCommand( } export async function impactCommand( - target: string, + target?: string, options?: { direction?: string; repo?: string; @@ -125,7 +130,16 @@ export async function impactCommand( summaryOnly?: boolean; }, ): Promise { - if (!target?.trim()) { + // A `--`-prefixed uid means Commander swallowed a following flag as the uid + // value (e.g. `impact --uid --file x` → uid === '--file'). Reject it rather + // than forwarding a garbage uid that would silently resolve to not-found. + if (options?.uid?.startsWith('--')) { + cliErrorKey('tool.usage.impact'); + process.exit(1); + } + // Target is an optional positional: a uid alone is enough to resolve (parity + // with `context [name]`). Only error when neither a target nor a uid is given. + if (!target?.trim() && !options?.uid) { cliErrorKey('tool.usage.impact'); process.exit(1); } @@ -137,7 +151,7 @@ export async function impactCommand( const parsedLimit = Number.isFinite(rawLimit) ? rawLimit : undefined; const parsedOffset = Number.isFinite(rawOffset) ? rawOffset : undefined; const result = await backend.callTool('impact', { - target, + target: target || undefined, target_uid: options?.uid, file_path: options?.file, kind: options?.kind, diff --git a/gitnexus/test/unit/cli-impact-disambiguation.test.ts b/gitnexus/test/unit/cli-impact-disambiguation.test.ts index 507c3b8dd..6362f4fbc 100644 --- a/gitnexus/test/unit/cli-impact-disambiguation.test.ts +++ b/gitnexus/test/unit/cli-impact-disambiguation.test.ts @@ -72,4 +72,41 @@ describe('CLI impact disambiguation flags (#1907)', () => { expect(params.file_path).toBeUndefined(); expect(params.kind).toBeUndefined(); }); + + // U1 (#1914 review F1): impact's positional target is now optional, so a uid + // alone resolves — parity with `context [name]`. + it('resolves uid-only with no positional target (parity with context)', async () => { + await impactCommand(undefined, { + direction: 'upstream', + uid: 'Function:src/auth.ts:login', + }); + + expect(callTool).toHaveBeenCalledTimes(1); + const params = callTool.mock.calls[0][1] as Record; + expect(params.target_uid).toBe('Function:src/auth.ts:login'); + expect(params.target).toBeUndefined(); + }); + + it('errors when neither a target nor a uid is provided', async () => { + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { + throw new Error('process.exit'); + }) as never); + + await expect(impactCommand(undefined, {})).rejects.toThrow('process.exit'); + expect(exitSpy).toHaveBeenCalledWith(1); + expect(callTool).not.toHaveBeenCalled(); + + exitSpy.mockRestore(); + }); + + it('rejects a --prefixed uid value (a flag swallowed by Commander) without forwarding it', async () => { + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => { + throw new Error('process.exit'); + }) as never); + + await expect(impactCommand(undefined, { uid: '--file' })).rejects.toThrow('process.exit'); + expect(callTool).not.toHaveBeenCalled(); + + exitSpy.mockRestore(); + }); });