fix: restore @group repo routing and prune obsolete migration-message tests

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.
This commit is contained in:
sahal 2026-04-28 14:09:43 -07:00
parent fa115b8203
commit e3784d519c
3 changed files with 96 additions and 35 deletions

View file

@ -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<string, unknown>) : {};
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<string, unknown>,
): Promise<unknown> {
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<string, unknown> = {
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<string, unknown> = {
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<string, unknown> = {
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<string, unknown>): Promise<unknown> {
await this.refreshRepos();
return this.getGroupService().groupQuery(params);

View file

@ -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');

View file

@ -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: {