mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +00:00
fix(trace): add file disambiguator alias (#2705)
This commit is contained in:
parent
9a351fb319
commit
1f500fed02
9 changed files with 117 additions and 3 deletions
|
|
@ -60,7 +60,7 @@ export const en = {
|
|||
'tool.usage.impact':
|
||||
'Usage: gitnexus impact <symbol_name> [--uid <uid>] [--file <path>] [--kind <kind>] [--direction upstream|downstream]',
|
||||
'tool.usage.trace':
|
||||
'Usage: gitnexus trace <from> <to> [--from-uid <uid>] [--to-uid <uid>] [--depth <n>]',
|
||||
'Usage: gitnexus trace <from> <to> [-f|--file <path>] [--from-file <path>] [--to-file <path>] [--from-uid <uid>] [--to-uid <uid>] [--depth <n>]',
|
||||
'tool.usage.cypher': 'Usage: gitnexus cypher <cypher_query>',
|
||||
'tool.warn.unknownKind':
|
||||
"--kind '{{kind}}' is not a known symbol kind (e.g. Function, Class, Method); it will not narrow the result.",
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export const zhCN = {
|
|||
'tool.usage.impact':
|
||||
'用法:gitnexus impact <符号名> [--uid <uid>] [--file <路径>] [--kind <类型>] [--direction upstream|downstream]',
|
||||
'tool.usage.trace':
|
||||
'用法:gitnexus trace <起点> <终点> [--from-uid <uid>] [--to-uid <uid>] [--depth <n>]',
|
||||
'用法:gitnexus trace <起点> <终点> [-f|--file <路径>] [--from-file <路径>] [--to-file <路径>] [--from-uid <uid>] [--to-uid <uid>] [--depth <n>]',
|
||||
'tool.usage.cypher': '用法:gitnexus cypher <Cypher 查询>',
|
||||
'tool.warn.unknownKind':
|
||||
"--kind '{{kind}}' 不是已知的符号类型(如 Function、Class、Method),不会用于缩小结果范围。",
|
||||
|
|
|
|||
|
|
@ -414,6 +414,7 @@ program
|
|||
.command('trace <from> <to>')
|
||||
.description('Find the shortest directed path between two symbols (call + class-member edges)')
|
||||
.option('--from-uid <uid>', 'Source symbol UID (zero-ambiguity)')
|
||||
.option('-f, --file <path>', 'Source file path hint (alias for --from-file)')
|
||||
.option('--from-file <path>', 'Source file path hint')
|
||||
.option('--to-uid <uid>', 'Target symbol UID (zero-ambiguity)')
|
||||
.option('--to-file <path>', 'Target file path hint')
|
||||
|
|
|
|||
|
|
@ -385,6 +385,7 @@ export async function traceCommand(
|
|||
to?: string,
|
||||
options?: {
|
||||
fromUid?: string;
|
||||
file?: string;
|
||||
fromFile?: string;
|
||||
toUid?: string;
|
||||
toFile?: string;
|
||||
|
|
@ -398,6 +399,14 @@ export async function traceCommand(
|
|||
cliErrorKey('tool.usage.trace');
|
||||
process.exit(1);
|
||||
}
|
||||
if (
|
||||
options?.file !== undefined &&
|
||||
options?.fromFile !== undefined &&
|
||||
options.file !== options.fromFile
|
||||
) {
|
||||
cliErrorKey('tool.usage.trace');
|
||||
process.exit(1);
|
||||
}
|
||||
if ((!from?.trim() && !options?.fromUid) || (!to?.trim() && !options?.toUid)) {
|
||||
cliErrorKey('tool.usage.trace');
|
||||
process.exit(1);
|
||||
|
|
@ -414,10 +423,11 @@ export async function traceCommand(
|
|||
|
||||
try {
|
||||
const backend = await getBackend();
|
||||
const fromFile = options?.fromFile ?? options?.file;
|
||||
const result = await backend.callTool('trace', {
|
||||
from: from || undefined,
|
||||
from_uid: options?.fromUid,
|
||||
from_file: options?.fromFile,
|
||||
from_file: fromFile,
|
||||
to: to || undefined,
|
||||
to_uid: options?.toUid,
|
||||
to_file: options?.toFile,
|
||||
|
|
|
|||
|
|
@ -161,6 +161,7 @@ interface StringAliasDefinition {
|
|||
const TOOL_STRING_ALIASES: Readonly<Record<string, readonly StringAliasDefinition[]>> = {
|
||||
impact: [{ canonical: 'target', aliases: ['name', 'symbol'] }],
|
||||
context: [{ canonical: 'file_path', aliases: ['file'] }],
|
||||
trace: [{ canonical: 'from_file', aliases: ['file'] }],
|
||||
};
|
||||
|
||||
function normalizeToolParams(
|
||||
|
|
|
|||
|
|
@ -846,6 +846,10 @@ DESTINATION TRACE (cross-repo): for an "@groupName" trace, OMIT to/to_uid/to_fil
|
|||
properties: {
|
||||
from: { type: 'string', description: 'Source symbol name' },
|
||||
from_uid: { type: 'string', description: 'Source symbol UID (zero-ambiguity)' },
|
||||
file: {
|
||||
type: 'string',
|
||||
description: 'Source file path hint for disambiguation (alias for from_file)',
|
||||
},
|
||||
from_file: { type: 'string', description: 'Source file path hint for disambiguation' },
|
||||
to: {
|
||||
type: 'string',
|
||||
|
|
|
|||
|
|
@ -134,6 +134,13 @@ describe('GITNEXUS_TOOLS', () => {
|
|||
expect(contextTool.inputSchema.properties.file).toMatchObject({ type: 'string' });
|
||||
});
|
||||
|
||||
it('trace tool advertises file as a compatibility alias for from_file', () => {
|
||||
const traceTool = GITNEXUS_TOOLS.find((t) => t.name === 'trace')!;
|
||||
expect(traceTool.inputSchema.properties.from_file).toBeDefined();
|
||||
expect(traceTool.inputSchema.properties.file).toMatchObject({ type: 'string' });
|
||||
expect(traceTool.inputSchema.properties.file.description).toContain('from_file');
|
||||
});
|
||||
|
||||
it('api_impact tool avoids top-level schema combinators for Bedrock compatibility (#2487)', () => {
|
||||
const apiImpactTool = GITNEXUS_TOOLS.find((t) => t.name === 'api_impact')!;
|
||||
expect(apiImpactTool.inputSchema).not.toHaveProperty('anyOf');
|
||||
|
|
|
|||
|
|
@ -201,6 +201,33 @@ describe('trace: dispatch', () => {
|
|||
expect(result.role).toBe('to');
|
||||
expect(result.candidates).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('accepts file as an MCP alias for from_file', async () => {
|
||||
(executeParameterized as any).mockImplementation(
|
||||
makeResolveMock([SYMBOL_A], [SYMBOL_B], { [SYMBOL_A.id]: [] }),
|
||||
);
|
||||
|
||||
await backend.callTool('trace', { from: 'A', to: 'B', file: 'src/a.ts' });
|
||||
|
||||
expect((executeParameterized as any).mock.calls[0][2]).toMatchObject({
|
||||
symName: 'A',
|
||||
filePath: 'src/a.ts',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects conflicting file and from_file MCP aliases', async () => {
|
||||
const result = await backend.callTool('trace', {
|
||||
from: 'A',
|
||||
to: 'B',
|
||||
file: 'src/a.ts',
|
||||
from_file: 'src/other.ts',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
error: 'Conflicting MCP parameters for trace.from_file: from_file, file must agree.',
|
||||
});
|
||||
expect(executeParameterized).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Group 2: BFS Core ──────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ vi.mock('../../src/mcp/local/local-backend.js', () => ({
|
|||
|
||||
vi.mock('node:fs', () => ({ writeSync: vi.fn() }));
|
||||
|
||||
vi.mock('../../src/core/lbug/native-check.js', () => ({
|
||||
checkLbugNative: () => ({ ok: true }),
|
||||
}));
|
||||
|
||||
import { traceCommand } from '../../src/cli/tool.js';
|
||||
|
||||
describe('CLI trace command', () => {
|
||||
|
|
@ -72,6 +76,45 @@ describe('CLI trace command', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('forwards -f/--file as the source file hint', async () => {
|
||||
await traceCommand('A', 'B', {
|
||||
file: 'src/a.ts',
|
||||
});
|
||||
|
||||
expect(callTool).toHaveBeenCalledWith(
|
||||
'trace',
|
||||
expect.objectContaining({
|
||||
from_file: 'src/a.ts',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts matching --file and --from-file values', async () => {
|
||||
await traceCommand('A', 'B', {
|
||||
file: 'src/a.ts',
|
||||
fromFile: 'src/a.ts',
|
||||
});
|
||||
|
||||
expect(callTool).toHaveBeenCalledWith(
|
||||
'trace',
|
||||
expect.objectContaining({
|
||||
from_file: 'src/a.ts',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('exits with usage when --file and --from-file disagree', async () => {
|
||||
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('process.exit');
|
||||
});
|
||||
await expect(
|
||||
traceCommand('A', 'B', { file: 'src/a.ts', fromFile: 'src/other.ts' }),
|
||||
).rejects.toThrow('process.exit');
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
expect(callTool).not.toHaveBeenCalled();
|
||||
exitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('forwards --depth as maxDepth', async () => {
|
||||
await traceCommand('A', 'B', { depth: '5' });
|
||||
|
||||
|
|
@ -120,4 +163,25 @@ describe('CLI trace command', () => {
|
|||
|
||||
expect(callTool).toHaveBeenCalledWith('trace', expect.objectContaining({ includeTests: true }));
|
||||
});
|
||||
|
||||
it('parses -f through the real CLI dispatcher', async () => {
|
||||
const originalArgv = process.argv;
|
||||
process.argv = ['node', 'gitnexus', 'trace', 'A', 'B', '-f', 'src/a.ts'];
|
||||
try {
|
||||
vi.resetModules();
|
||||
await import('../../src/cli/index.js');
|
||||
await vi.waitFor(() => {
|
||||
expect(callTool).toHaveBeenCalledWith(
|
||||
'trace',
|
||||
expect.objectContaining({
|
||||
from: 'A',
|
||||
to: 'B',
|
||||
from_file: 'src/a.ts',
|
||||
}),
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
process.argv = originalArgv;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue