mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
Some checks failed
Scorecard / Scorecard analysis (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Gitleaks / gitleaks (push) Has been cancelled
Publish / Classify release event (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-cli) (push) Has been cancelled
Trivy Image Scan / Trivy (gitnexus-web) (push) Has been cancelled
Publish / RC guard (marker + release-PR skip) (push) Has been cancelled
Publish / ci (push) Has been cancelled
Publish / Publish to npm (push) Has been cancelled
Publish / Build & Push RC Docker images (push) Has been cancelled
* fix(mcp): stabilize api_impact response shape for same-URL multi-verb routes After #2302 made Route identity method-aware, a same URL exposes one Route node per HTTP verb, so a bare-URL api_impact lookup could silently flip from a direct route object to the wrapped { routes, total } envelope. Surface each route's `method` (via the shared fetch) so multi-verb results are distinguishable, and add an optional `method` selector that narrows a multi-verb URL/file to one verb and forces the singular shape. A verb that matches no route returns a clear error. Document the match-count contract in the tool schema. Refs #2308 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(mcp): cover same-URL multi-verb api_impact contract Regression coverage for #2308: bare-URL and bare-file lookups of a same-URL GET+POST pair return the wrapped form with distinct per-route methods; the method selector collapses to the singular shape (case-insensitively); an unmatched verb returns a verb-not-found error; and verbless routes surface a null method. Refs #2308 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(review): apply autofix feedback - tools.ts: correct api_impact contract docs — `method` narrows to one verb but the singular shape only holds when exactly one route remains after filtering (substring route/file matches can still wrap); cover file lookups; enumerate verbs. - local-backend.ts: surface `method` in route_map and shape_check output (the shared fetch already returns it; agents discover verbs there before api_impact). - local-backend.ts: compute routeCountByHandler from the unfiltered match so a method-scoped api_impact still flags a multi-verb handler's partial middleware. - tests: add file+method and verbless-exclusion cases; assert unconditionally via toMatchObject; lowercase the verb-not-found input to exercise error uppercasing. Refs #2308 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(mcp): treat wildcard '*' routes as matching any api_impact method selector (#2308) Method-agnostic routes (Django function views) persist with Route method '*', not null. The api_impact method selector used exact verb equality, so '*' routes were excluded and api_impact({route, method:'POST'}) falsely reported 'No routes found' for a route that handles every verb. Treat '*' as matching any requested verb, and correct the comment + tool-description strings that wrongly grouped Django wildcards with null/verbless routes. * fix(mcp): harden api_impact method input against non-string and empty values (#2308) The MCP envelope is not schema-validated, so a non-string `method` reached `.toUpperCase()` and threw a TypeError. Widen the param to `unknown` and guard it with a typeof check that returns a structured error (mirroring the resolveAliasString pattern from #2175), and collapse empty/whitespace verbs to no selector. * fix(mcp): distinguish url-not-found from verb-not-found in api_impact error (#2308) The verb-not-found error appended 'with method "X"' even when the URL/file itself did not exist, implying the URL exists with other verbs. Gate the verb clause on matched.length > 0 so a non-existent URL/file gets the plain message. * fix(mcp): clarify api_impact middlewareNote wording for verbless siblings (#2308) The partial-middleware note claimed 'other methods in this handler' even when the co-located sibling is a verbless (null) route rather than another HTTP verb. Refer to 'other route exports' instead, which covers both cases. * docs(mcp): document and test the method field on route_map and shape_check (#2308) The shared fetchRoutesWithConsumers change surfaced a method key on route_map and shape_check responses too, but their tool descriptions never mentioned it and no test covered it. Document the field on both descriptions and add unit tests asserting it (shape_check rows carry responseKeys + a consumer so they survive shape_check's keys-and-consumers filter). * test(mcp): cover middlewareDetection 'partial' survival under a method filter (#2308) The diff's core behavioral line counts verbs-per-handler from the unfiltered match set so a method-scoped query still flags a multi-verb handler's partial middleware, but no test exercised it (every verbRow hardcoded middleware:null). Add a middleware param to verbRow and a test that fails if the count is taken from the post-filter set instead. Verified via mutation: matched->routes fails it. * test(mcp): add live-LadybugDB integration coverage for route method round-trip (#2308) The new n.method query column was only unit-mocked. Add a self-contained integration suite that seeds GET+POST /api/orders and a method-agnostic '*' Django route, then asserts api_impact surfaces method, narrows by verb, and matches the '*' route end-to-end (the U1 fix), plus route_map surfacing. Own seed + no FTS so it neither perturbs api-impact-e2e nor silently skips. * refactor(mcp): type the api_impact response shape instead of Promise<any> (#2308) Replace apiImpact's Promise<any> with an explicit ApiImpactResult union (single route | wrapped { routes, total } | { error }) and a typed ApiImpactRoute. The results.map is annotated so the response builder is checked against the declared shape. Behavior unchanged; sibling MCP methods keep their Promise<any> convention. * fix(mcp): express the route-or-file requirement in the api_impact schema (#2308) The inputSchema left route/file as bare optionals, so the 'at least one of route/file' rule the handler enforces was invisible to clients. Add an optional anyOf to ToolDefinition (forwarded verbatim by the ListTools handler) and an anyOf:[{required:[route]},{required:[file]}] on api_impact. Matches runtime (both allowed, route wins); 'at least one' not 'exactly one'. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
112 lines
4.7 KiB
TypeScript
112 lines
4.7 KiB
TypeScript
/**
|
|
* E2E Integration Tests: api_impact / route_map method round-trip (#2308)
|
|
*
|
|
* Proves the `n.method` column round-trips through a real LadybugDB — the
|
|
* fetchRoutesWithConsumers query, the polymorphic api_impact shape, and the
|
|
* method selector (including the method-agnostic '*' case) — beyond the unit
|
|
* mocks in calltool-dispatch.test.ts.
|
|
*
|
|
* Self-contained seed (own routes, no FTS) so it neither perturbs the shared
|
|
* api-impact-e2e fixture nor silently skips on an FTS-less box: api_impact and
|
|
* route_map use graph queries, not full-text search.
|
|
*/
|
|
import { describe, it, expect, beforeAll, vi } from 'vitest';
|
|
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
|
|
import { listRegisteredRepos } from '../../src/storage/repo-manager.js';
|
|
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
|
|
|
|
vi.mock('../../src/storage/repo-manager.js', () => ({
|
|
listRegisteredRepos: vi.fn().mockResolvedValue([]),
|
|
cleanupOldKuzuFiles: vi.fn().mockResolvedValue({ found: false, needsReindex: false }),
|
|
findSiblingClones: vi.fn().mockResolvedValue([]),
|
|
}));
|
|
|
|
// After #2302 a Route node is keyed by (method, url): GET/POST of the same URL
|
|
// are distinct nodes, while a method-agnostic route (Django function view)
|
|
// keys by URL alone and persists with the literal method '*'.
|
|
const METHOD_SEED_DATA = [
|
|
`CREATE (f:File {id: 'file:app/api/orders/route.ts', name: 'route.ts', filePath: 'app/api/orders/route.ts', content: 'export async function GET() {} export async function POST() {}'})`,
|
|
`CREATE (f:File {id: 'file:orders/views.py', name: 'views.py', filePath: 'orders/views.py', content: 'def order_view(request): ...'})`,
|
|
`CREATE (r:Route {id: 'Route:GET /api/orders', name: '/api/orders', filePath: 'app/api/orders/route.ts', method: 'GET', responseKeys: ['data'], errorKeys: [], middleware: []})`,
|
|
`CREATE (r:Route {id: 'Route:POST /api/orders', name: '/api/orders', filePath: 'app/api/orders/route.ts', method: 'POST', responseKeys: ['id'], errorKeys: [], middleware: []})`,
|
|
`CREATE (r:Route {id: 'Route:/django/view', name: '/django/view', filePath: 'orders/views.py', method: '*', responseKeys: [], errorKeys: [], middleware: []})`,
|
|
];
|
|
|
|
withTestLbugDB(
|
|
'api-impact-method-e2e',
|
|
(handle) => {
|
|
let backend: LocalBackend;
|
|
|
|
beforeAll(async () => {
|
|
const ext = handle as typeof handle & { _backend?: LocalBackend };
|
|
if (!ext._backend) {
|
|
throw new Error(
|
|
'LocalBackend not initialized — afterSetup did not attach _backend to handle',
|
|
);
|
|
}
|
|
backend = ext._backend;
|
|
});
|
|
|
|
describe('api_impact method round-trip (live LadybugDB)', () => {
|
|
it('returns the wrapped form for a same-URL multi-verb route, each with its method', async () => {
|
|
const result = await backend.callTool('api_impact', { route: '/api/orders' });
|
|
expect(result).not.toHaveProperty('error');
|
|
expect(result.total).toBe(2);
|
|
expect(result.routes.map((r: { method: string | null }) => r.method).sort()).toEqual([
|
|
'GET',
|
|
'POST',
|
|
]);
|
|
});
|
|
|
|
it('narrows a multi-verb URL to one route when method is given', async () => {
|
|
const result = await backend.callTool('api_impact', {
|
|
route: '/api/orders',
|
|
method: 'POST',
|
|
});
|
|
expect(result.method).toBe('POST');
|
|
expect(result.route).toBe('/api/orders');
|
|
expect(result.routes).toBeUndefined();
|
|
});
|
|
|
|
it('matches a method-agnostic (*) route against a specific method selector', async () => {
|
|
const result = await backend.callTool('api_impact', {
|
|
route: '/django/view',
|
|
method: 'POST',
|
|
});
|
|
expect(result).not.toHaveProperty('error');
|
|
expect(result.method).toBe('*');
|
|
expect(result.route).toBe('/django/view');
|
|
});
|
|
});
|
|
|
|
describe('route_map method round-trip (live LadybugDB)', () => {
|
|
it('surfaces each route method', async () => {
|
|
const result = await backend.callTool('route_map', { route: '/api/orders' });
|
|
expect(result.routes.map((r: { method: string | null }) => r.method).sort()).toEqual([
|
|
'GET',
|
|
'POST',
|
|
]);
|
|
});
|
|
});
|
|
},
|
|
{
|
|
seed: METHOD_SEED_DATA,
|
|
poolAdapter: true,
|
|
afterSetup: async (handle) => {
|
|
vi.mocked(listRegisteredRepos).mockResolvedValue([
|
|
{
|
|
name: 'test-method-repo',
|
|
path: '/test/method-repo',
|
|
storagePath: handle.tmpHandle.dbPath,
|
|
indexedAt: new Date().toISOString(),
|
|
lastCommit: 'abc789',
|
|
stats: { files: 2, nodes: 5, communities: 0, processes: 0 },
|
|
},
|
|
]);
|
|
|
|
const backend = new LocalBackend();
|
|
await backend.init();
|
|
(handle as typeof handle & { _backend?: LocalBackend })._backend = backend;
|
|
},
|
|
},
|
|
);
|