GitNexus/gitnexus/test/unit/group/resolve-bridge-neighbors.test.ts
Gergő Magyar 7e993ab897
fix(group): fail ambiguous sync names and honor analyze --name (#3094)
* fix(group): fail sync when a member name is ambiguous

Silent first-match bound the wrong clone when --allow-duplicate-name
registered two paths under one alias. Refs #3028.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(analyze): apply --name on the already-up-to-date path

A rename should not require --force when the index is already current.
Register before the same-commit branch restamp. Refs #3028.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(group): hint member path when impact --repo is an alias

$localRepo stays the yaml key; joining on the registry alias is a
non-join. List matching keys so operators can retry. Refs #3028.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(group): keep injected sync and alias hints consistent

Workspace-deps path maps reuse the resolved handle so duplicate names
cannot throw after an injected resolver. Alias hints match case-insensitively.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 21:48:46 +00:00

186 lines
6.1 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fsp from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
import { cleanupTempDir } from '../../helpers/test-db.js';
import { resolveBridgeNeighbors } from '../../../src/core/group/cross-impact.js';
import {
writeBridge,
openBridgeDbReadOnly,
closeBridgeDb,
} from '../../../src/core/group/bridge-db.js';
import type { CrossLink } from '../../../src/core/group/types.js';
import { makeContract } from './fixtures.js';
/**
* U1 — direct coverage for the shared bridge-neighbor join extracted from
* `runGroupImpact`. Mirrors the close-then-reopen Windows guard used by the
* other writeBridge tests (`itLbugReopen`).
*/
const itLbugReopen = process.platform === 'win32' ? it.skip : it;
/** Build a bridge with one consumer→provider ContractLink (both UIDs populated). */
async function writeLinkedBridge(groupDir: string): Promise<void> {
const consumer = makeContract({
repo: 'app/frontend',
role: 'consumer',
symbolUid: 'consumer-uid',
symbolRef: { filePath: 'src/api.ts', name: 'fetchUsers' },
symbolName: 'fetchUsers',
contractId: 'http::GET::/api/users',
confidence: 0.5,
});
const provider = makeContract({
repo: 'app/backend',
role: 'provider',
symbolUid: 'provider-uid',
symbolRef: { filePath: 'src/routes.ts', name: 'getUsers' },
symbolName: 'getUsers',
contractId: 'http::GET::/api/users',
confidence: 0.9,
});
const link: CrossLink = {
from: { repo: 'app/frontend', symbolUid: 'consumer-uid', symbolRef: consumer.symbolRef },
to: { repo: 'app/backend', symbolUid: 'provider-uid', symbolRef: provider.symbolRef },
type: 'http',
contractId: 'http::GET::/api/users',
matchType: 'exact',
confidence: 0.9,
};
await writeBridge(groupDir, {
contracts: [consumer, provider],
crossLinks: [link],
repoSnapshots: {},
missingRepos: [],
});
}
describe('resolveBridgeNeighbors', () => {
let tmpDir: string;
beforeEach(async () => {
tmpDir = await fsp.mkdtemp(path.join(os.tmpdir(), 'bridge-neighbors-'));
});
afterEach(async () => {
await cleanupTempDir(tmpDir);
});
it('returns [] for an empty uid set without touching the DB', async () => {
// A null handle would throw if the DB were queried; the empty-set guard
// must short-circuit before any query.
const handleSentinel = null as unknown as Parameters<typeof resolveBridgeNeighbors>[0];
const rows = await resolveBridgeNeighbors(handleSentinel, {
localRepo: 'app/backend',
uids: [],
direction: 'upstream',
});
expect(rows).toEqual([]);
});
itLbugReopen('downstream: consumer uid resolves to its provider neighbor', async () => {
await writeLinkedBridge(tmpDir);
const handle = await openBridgeDbReadOnly(tmpDir);
expect(handle).not.toBeNull();
const rows = await resolveBridgeNeighbors(handle!, {
localRepo: 'app/frontend',
uids: ['consumer-uid'],
direction: 'downstream',
});
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({
neighborRepo: 'app/backend',
neighborUid: 'provider-uid',
matchType: 'exact',
confidence: 0.9,
contractId: 'http::GET::/api/users',
contractType: 'http',
});
await closeBridgeDb(handle!);
});
itLbugReopen('upstream: provider uid resolves to its consumer neighbor', async () => {
await writeLinkedBridge(tmpDir);
const handle = await openBridgeDbReadOnly(tmpDir);
const rows = await resolveBridgeNeighbors(handle!, {
localRepo: 'app/backend',
uids: ['provider-uid'],
direction: 'upstream',
});
expect(rows).toHaveLength(1);
expect(rows[0]).toMatchObject({
neighborRepo: 'app/frontend',
neighborUid: 'consumer-uid',
contractId: 'http::GET::/api/users',
});
await closeBridgeDb(handle!);
});
itLbugReopen('unknown uid yields no neighbors', async () => {
await writeLinkedBridge(tmpDir);
const handle = await openBridgeDbReadOnly(tmpDir);
const rows = await resolveBridgeNeighbors(handle!, {
localRepo: 'app/frontend',
uids: ['no-such-uid'],
direction: 'downstream',
});
expect(rows).toEqual([]);
await closeBridgeDb(handle!);
});
itLbugReopen(
'registry alias as localRepo does not join contracts stamped with the member path',
async () => {
const consumer = makeContract({
repo: 'demo/api',
role: 'consumer',
symbolUid: 'consumer-uid',
symbolRef: { filePath: 'src/api.ts', name: 'fetchUsers' },
symbolName: 'fetchUsers',
contractId: 'http::GET::/api/users',
confidence: 0.5,
});
const provider = makeContract({
repo: 'demo/api',
role: 'provider',
symbolUid: 'provider-uid',
symbolRef: { filePath: 'src/routes.ts', name: 'getUsers' },
symbolName: 'getUsers',
contractId: 'http::GET::/api/users',
confidence: 0.9,
});
const link: CrossLink = {
from: { repo: 'web', symbolUid: 'web-uid', symbolRef: consumer.symbolRef },
to: { repo: 'demo/api', symbolUid: 'provider-uid', symbolRef: provider.symbolRef },
type: 'http',
contractId: 'http::GET::/api/users',
matchType: 'manifest',
confidence: 0.9,
};
await writeBridge(tmpDir, {
contracts: [{ ...consumer, repo: 'web' }, provider],
crossLinks: [link],
repoSnapshots: {},
missingRepos: [],
});
const handle = await openBridgeDbReadOnly(tmpDir);
const aliasMiss = await resolveBridgeNeighbors(handle!, {
localRepo: 'demo-api',
uids: ['provider-uid'],
direction: 'upstream',
});
expect(aliasMiss).toEqual([]);
const pathHit = await resolveBridgeNeighbors(handle!, {
localRepo: 'demo/api',
uids: ['provider-uid'],
direction: 'upstream',
});
expect(pathHit).toHaveLength(1);
expect(pathHit[0]).toMatchObject({
neighborRepo: 'web',
matchType: 'manifest',
});
await closeBridgeDb(handle!);
},
);
});