From e3784d519cfca4a406a9f1b580b5af57749d27f0 Mon Sep 17 00:00:00 2001 From: sahal Date: Tue, 28 Apr 2026 14:09:43 -0700 Subject: [PATCH] fix: restore @group repo routing and prune obsolete migration-message tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restored callToolAtGroupRepo method + import + dispatch block in LocalBackend.callTool — the @-routing path is required by both production code (CLI/MCP @groupName syntax) and the group-repo-routing tests that came in via the rebase from origin/main. Removed two test cases from gitnexus/test/unit/resources.test.ts that exercised readGroupContractsResource / readGroupStatusResource — those backend methods do not exist in this PR's design (group resources are served via the @-routing path, not separate MCP resource readers). Removed three test cases from gitnexus/test/unit/mcp/group-repo-routing.test.ts that asserted group_contracts / group_status / unknown group_* tools throw a 'Removed tools' migration error. This PR explicitly reintroduces those tools as features, so the migration-removal assertions are obsolete. --- gitnexus/src/mcp/local/local-backend.ts | 96 +++++++++++++++++++ .../test/unit/mcp/group-repo-routing.test.ts | 19 ---- gitnexus/test/unit/resources.test.ts | 16 ---- 3 files changed, 96 insertions(+), 35 deletions(-) diff --git a/gitnexus/src/mcp/local/local-backend.ts b/gitnexus/src/mcp/local/local-backend.ts index 2ba17a09f..ef2bacaf3 100644 --- a/gitnexus/src/mcp/local/local-backend.ts +++ b/gitnexus/src/mcp/local/local-backend.ts @@ -28,6 +28,7 @@ import { type RegistryEntry, } from '../../storage/repo-manager.js'; import { GroupService, type GroupToolPort } from '../../core/group/service.js'; +import { resolveAtGroupMemberRepoPath } from '../../core/group/resolve-at-member.js'; import { collectBestChunks } from '../../core/embeddings/types.js'; import { rankExactEmbeddingRows, @@ -639,6 +640,15 @@ export class LocalBackend { return this.handleGroupTool(method, params || {}); } + const p = params && typeof params === 'object' ? (params as Record) : {}; + if ( + (method === 'impact' || method === 'query' || method === 'context') && + typeof p.repo === 'string' && + p.repo.startsWith('@') + ) { + return this.callToolAtGroupRepo(method, p); + } + // Resolve repo from optional param (re-reads registry on miss) const repo = await this.resolveRepo((params as { repo?: string } | undefined)?.repo); @@ -3047,6 +3057,92 @@ export class LocalBackend { } } + private async callToolAtGroupRepo( + method: string, + params: Record, + ): Promise { + await this.refreshRepos(); + + if ( + params.service !== undefined && + params.service !== null && + String(params.service).trim() === '' + ) { + return { error: 'service must not be an empty string' }; + } + + const raw = String(params.repo).slice(1); + const slash = raw.indexOf('/'); + const groupName = (slash === -1 ? raw : raw.slice(0, slash)).trim(); + const memberRest = slash === -1 ? undefined : raw.slice(slash + 1).trim() || undefined; + + const resolved = await resolveAtGroupMemberRepoPath(groupName, memberRest); + if (resolved.ok === false) return { error: resolved.error }; + + const svc = this.getGroupService(); + if (method === 'impact') { + const impactArgs: Record = { + name: groupName, + repo: resolved.repoPath, + target: params.target, + direction: params.direction, + }; + if (params.maxDepth !== undefined) impactArgs.maxDepth = params.maxDepth; + if (params.crossDepth !== undefined) impactArgs.crossDepth = params.crossDepth; + if (params.relationTypes !== undefined) impactArgs.relationTypes = params.relationTypes; + if (params.includeTests !== undefined) impactArgs.includeTests = params.includeTests; + if (params.minConfidence !== undefined) impactArgs.minConfidence = params.minConfidence; + if (params.service !== undefined && params.service !== null) + impactArgs.service = params.service; + if (typeof params.subgroup === 'string') impactArgs.subgroup = params.subgroup; + if (params.timeoutMs !== undefined) impactArgs.timeoutMs = params.timeoutMs; + if (params.timeout !== undefined) impactArgs.timeout = params.timeout; + return svc.groupImpact(impactArgs); + } + if (method === 'query') { + const queryArgs: Record = { + name: groupName, + query: params.query, + }; + if (typeof params.task_context === 'string') queryArgs.task_context = params.task_context; + if (typeof params.goal === 'string') queryArgs.goal = params.goal; + if (typeof params.limit === 'number') queryArgs.limit = params.limit; + if (typeof params.max_symbols === 'number') queryArgs.max_symbols = params.max_symbols; + if (params.include_content !== undefined) queryArgs.include_content = params.include_content; + if (params.service !== undefined && params.service !== null) + queryArgs.service = params.service; + if (memberRest !== undefined) { + queryArgs.subgroup = memberRest; + queryArgs.subgroupExact = true; + } + return svc.groupQuery(queryArgs); + } + if (method === 'context') { + const targetSym = + typeof params.target === 'string' && params.target.trim() !== '' + ? params.target.trim() + : typeof params.name === 'string' && params.name.trim() !== '' + ? params.name.trim() + : undefined; + const contextArgs: Record = { + name: groupName, + target: targetSym, + }; + if (typeof params.uid === 'string') contextArgs.uid = params.uid; + if (typeof params.file_path === 'string') contextArgs.file_path = params.file_path; + if (params.include_content !== undefined) + contextArgs.include_content = params.include_content; + if (params.service !== undefined && params.service !== null) + contextArgs.service = params.service; + if (memberRest !== undefined) { + contextArgs.subgroup = memberRest; + contextArgs.subgroupExact = true; + } + return svc.groupContext(contextArgs); + } + throw new Error(`Internal: unsupported group-repo tool ${method}`); + } + private async groupQuery(params: Record): Promise { await this.refreshRepos(); return this.getGroupService().groupQuery(params); diff --git a/gitnexus/test/unit/mcp/group-repo-routing.test.ts b/gitnexus/test/unit/mcp/group-repo-routing.test.ts index ddf7d03a5..7091f309b 100644 --- a/gitnexus/test/unit/mcp/group-repo-routing.test.ts +++ b/gitnexus/test/unit/mcp/group-repo-routing.test.ts @@ -162,25 +162,6 @@ repos: expect(groupSpyQuery).not.toHaveBeenCalled(); }); - it('unknown group_* tools mention removal', async () => { - const backend = new LocalBackend(); - await expect(backend.callTool('group_query', { name: 'g1', query: 'x' })).rejects.toThrow( - /Removed tools/, - ); - }); - - it('removed group_contracts mentions migration', async () => { - const backend = new LocalBackend(); - await expect(backend.callTool('group_contracts', { name: 'g1' })).rejects.toThrow( - /Removed tools/, - ); - }); - - it('removed group_status mentions migration', async () => { - const backend = new LocalBackend(); - await expect(backend.callTool('group_status', { name: 'g1' })).rejects.toThrow(/Removed tools/); - }); - describe('Issue #794 manual smoke checklist (automated)', () => { beforeEach(() => { const groupDir = path.join(tmpDir, 'groups', 'myproduct'); diff --git a/gitnexus/test/unit/resources.test.ts b/gitnexus/test/unit/resources.test.ts index ccd8461f1..a28ea1b4a 100644 --- a/gitnexus/test/unit/resources.test.ts +++ b/gitnexus/test/unit/resources.test.ts @@ -213,22 +213,6 @@ describe('readResource', () => { expect(result).toContain('No repositories indexed'); }); - it('routes group contracts resource through backend', async () => { - const backend = createMockBackend(); - const uri = 'gitnexus://group/g1/contracts?type=http&unmatchedOnly=true'; - await readResource(uri, backend); - expect(backend.readGroupContractsResource).toHaveBeenCalledWith('g1', { - type: 'http', - unmatchedOnly: true, - }); - }); - - it('routes group status resource through backend', async () => { - const backend = createMockBackend(); - await readResource('gitnexus://group/acme/status', backend); - expect(backend.readGroupStatusResource).toHaveBeenCalledWith('acme'); - }); - it('routes gitnexus://repo/{name}/context correctly', async () => { const backend = createMockBackend({ context: {