mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* feat(mcp): add trace tool for shortest call path between symbols (#1821) Implement the \ race\ MCP tool and \gitnexus trace\ CLI command that finds the shortest directed call path between two symbols using BFS over CALLS + HAS_METHOD edges. - MCP tool definition in tools.ts with READ_ONLY annotations - Directed BFS in local-backend.ts with parent-map path reconstruction - Symbol resolution via resolveSymbolCandidates (name/UID/file-hint) - Gap reporting with furthest reachable node and depth tracking - CLI wiring: gitnexus trace <from> <to> [--from-uid] [--to-uid] [--depth] - i18n keys in en.ts and zh-CN.ts + help-i18n.ts registration - ARCHITECTURE.md tools table entry - 16 unit tests (11 BFS core + 5 CLI wiring) * test(mcp): account for trace tool in tools.test.ts count The trace tool makes GITNEXUS_TOOLS length 15; update the hardcoded count, add 'trace' to the expected-names list, and refresh the stale "13 tools" comment and it() title. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): sanitize trace maxDepth to reject 0/NaN/negative `Math.min(params.maxDepth ?? 10, 30)` had no lower bound and `??` does not recover 0 or NaN, so `--depth 0|-5|abc` made the BFS loop run zero iterations and return a false `no_path`. Clamp at the real boundary with a `Number.isInteger && > 0` guard (the MCP inputSchema minimum is advisory only), and reject a non-numeric `--depth` in the CLI up front rather than forwarding NaN. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): check trace target before applying test-file filter The `isTestFilePath` filter ran before the target-equality check, but resolveSymbolCandidates does not exclude test-file symbols. A target (or a required hop) that lives in a test file was therefore skipped under the default includeTests=false and produced a false no_path with a misleading dynamic-dispatch suggestion. Match the explicitly-requested target first; non-target test-file nodes are still filtered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf(mcp): bound trace BFS with per-level LIMIT and visited cap The per-level query had no LIMIT and the visited set was uncapped, so a high-fanout hub could materialize an unbounded frontier. Cap per-level rows (interpolated LIMIT — Kuzu does not bind LIMIT) and the total visited set; either cap sets a `truncated` flag so a resulting no_path reports that the search was cut short rather than implying the graph was exhausted. Note: the sibling impact BFS shares the same unbounded pattern; applying the cap there is deferred (out of scope for this PR). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(mcp): clarify trace traverses call + class-member edges trace was advertised as a "shortest call path" but also traverses HAS_METHOD (class→member) containment edges so a class-rooted trace can descend into its methods. Keep that capability (consistent with impact/ context) and make the docs honest: rename EDGE_TYPES→TRAVERSAL_EDGE_TYPES, state the call + class-member traversal in the MCP/CLI/i18n/ARCHITECTURE descriptions, and note each hop's edge type is reported in edges[]. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): set status:'error' on trace failure responses Every trace return path sets a `status` discriminator except the caught-error path, so a consumer switching on `result.status` saw undefined on failure. Add status:'error' to both the backend trace() catch and the CLI traceCommand catch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): return a friendly error for non-string trace from/to A non-string from/to reaching resolveSymbolCandidates surfaced a low-level "x.includes is not a function" via name.includes. Guard the four name/uid params at the top of _traceImpl and return a structured status:'error' with a clear message instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(mcp): single row-decode + drop dead field in trace BFS Decode each BFS row once into named locals instead of repeating `(row.x ?? row[N])` across the two parent.set calls and the furthest-tracking. Drop the `type` field from the parent map value (it was written but never read), and rename the internal `deepestInfo` to `lastReached` for accuracy (the output field `furthest` is unchanged). Pure refactor — no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(cli): dedicated trace includeTests i18n key + guard coverage `trace|--include-tests` reused the impact help key, so rewording the impact option would silently change trace's help text. Add a dedicated help.option.trace.includeTests key in en + zh-CN and repoint it. Add CLI coverage for the (already symmetric) --from-uid/--to-uid flag-value guard and for --include-tests forwarding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp): faithful BFS mock + expand trace coverage Fix makeResolveMock: concatenate neighbors across ALL frontier ids (it returned only the first node's, so a multi-node frontier was unmodelled) and key the UID branch on params.uid (the old query-text match never fired). Add coverage: shortest path through the second frontier node (proves the mock fix), confidence floor fallback, HAS_METHOD traversal with a mixed edge-type chain, no_path furthest:null, and from_file disambiguation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(trace): apply root prettier formatting to trace files The root `quality / format` gate (prettier --check, printWidth 100) runs on the full repo and flagged the trace sources/tests (the local config masks it). Reformat to root style — no behavior change; trace + tools suites and tsc stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(skills): document the trace tool for AI agents Add `trace` to the GitNexus skill docs so agents reach for it instead of hand-chaining context/impact hops. The guide gains a Tools Reference row and a "shortest path between two symbols" subsection (params, result shape, status/furthest/truncated semantics); the debugging skill gains a "how does A reach B?" pattern row and a trace tool example. Mirrored to the .claude and claude-plugin copies (byte-identical) and the cursor copy (compact style). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp): drop unused trace test fixtures (CodeQL js/unused-local-variable) CodeQL flagged two unused locals in the trace BFS tests: the top-level SYMBOL_C and a SYMBOL_D inside the maxDepth test (both defined, never referenced). Remove them. No behavior change — 58 trace/tools tests stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
123 lines
3.6 KiB
TypeScript
123 lines
3.6 KiB
TypeScript
/**
|
|
* Unit Tests: CLI trace command wiring
|
|
*
|
|
* Tests that traceCommand forwards CLI flags to callTool('trace', ...)
|
|
* with correct parameter names. Mocked LocalBackend — no graph/DB.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
const { callTool, init } = vi.hoisted(() => ({
|
|
callTool: vi.fn(),
|
|
init: vi.fn().mockResolvedValue(true),
|
|
}));
|
|
|
|
vi.mock('../../src/mcp/local/local-backend.js', () => ({
|
|
LocalBackend: class {
|
|
init = init;
|
|
callTool = callTool;
|
|
},
|
|
VALID_NODE_LABELS: new Set(['Function', 'Class', 'Interface', 'Method', 'Constructor']),
|
|
}));
|
|
|
|
vi.mock('node:fs', () => ({ writeSync: vi.fn() }));
|
|
|
|
import { traceCommand } from '../../src/cli/tool.js';
|
|
|
|
describe('CLI trace command', () => {
|
|
beforeEach(() => {
|
|
callTool.mockReset();
|
|
callTool.mockResolvedValue({ status: 'ok', hopCount: 1 });
|
|
});
|
|
|
|
it('forwards from/to as positional args', async () => {
|
|
await traceCommand('validateUser', 'executeQuery', {});
|
|
|
|
expect(callTool).toHaveBeenCalledTimes(1);
|
|
expect(callTool).toHaveBeenCalledWith(
|
|
'trace',
|
|
expect.objectContaining({
|
|
from: 'validateUser',
|
|
to: 'executeQuery',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('forwards --from-uid/--to-uid as from_uid/to_uid', async () => {
|
|
await traceCommand('A', 'B', {
|
|
fromUid: 'uid:A',
|
|
toUid: 'uid:B',
|
|
});
|
|
|
|
expect(callTool).toHaveBeenCalledWith(
|
|
'trace',
|
|
expect.objectContaining({
|
|
from_uid: 'uid:A',
|
|
to_uid: 'uid:B',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('forwards --from-file/--to-file as from_file/to_file', async () => {
|
|
await traceCommand('A', 'B', {
|
|
fromFile: 'src/a.ts',
|
|
toFile: 'src/b.ts',
|
|
});
|
|
|
|
expect(callTool).toHaveBeenCalledWith(
|
|
'trace',
|
|
expect.objectContaining({
|
|
from_file: 'src/a.ts',
|
|
to_file: 'src/b.ts',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('forwards --depth as maxDepth', async () => {
|
|
await traceCommand('A', 'B', { depth: '5' });
|
|
|
|
expect(callTool).toHaveBeenCalledWith(
|
|
'trace',
|
|
expect.objectContaining({
|
|
maxDepth: 5,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('exits with usage when from is missing and no from_uid', async () => {
|
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
throw new Error('process.exit');
|
|
});
|
|
await expect(traceCommand(undefined, 'B', {})).rejects.toThrow('process.exit');
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
exitSpy.mockRestore();
|
|
});
|
|
|
|
it('exits with usage when --depth is non-numeric instead of forwarding NaN', async () => {
|
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
throw new Error('process.exit');
|
|
});
|
|
await expect(traceCommand('A', 'B', { depth: 'abc' })).rejects.toThrow('process.exit');
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
expect(callTool).not.toHaveBeenCalled();
|
|
exitSpy.mockRestore();
|
|
});
|
|
|
|
it('exits with usage when --from-uid or --to-uid is a swallowed flag value', async () => {
|
|
for (const opts of [{ fromUid: '--oops' }, { toUid: '--oops' }]) {
|
|
callTool.mockReset();
|
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
throw new Error('process.exit');
|
|
});
|
|
await expect(traceCommand('A', 'B', opts)).rejects.toThrow('process.exit');
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
expect(callTool).not.toHaveBeenCalled();
|
|
exitSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it('forwards --include-tests as includeTests', async () => {
|
|
await traceCommand('A', 'B', { includeTests: true });
|
|
|
|
expect(callTool).toHaveBeenCalledWith('trace', expect.objectContaining({ includeTests: true }));
|
|
});
|
|
});
|