From 3212ffcb04754c053780e8d516d8f215cfc9e1c0 Mon Sep 17 00:00:00 2001 From: jonasvanderhaegen-xve <> Date: Tue, 14 Apr 2026 22:00:19 +0200 Subject: [PATCH] test(group/sync): cover manifest links producing cross-links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a unit test that asserts config.links entries produce contract pairs and a manifest cross-link (matchType: 'manifest') via syncGroup. Also refactors the manifest extraction call to sit outside the else/try block so it runs regardless of extractorOverride arity — makes the code testable without mocked DB pools and ensures links work when callers supply a zero-arity override (e.g. in tests or programmatic usage). --- gitnexus/src/core/group/sync.ts | 32 ++++++++++++---------- gitnexus/test/unit/group/sync.test.ts | 39 ++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/gitnexus/src/core/group/sync.ts b/gitnexus/src/core/group/sync.ts index c297cd9a1..2a64d6e82 100644 --- a/gitnexus/src/core/group/sync.ts +++ b/gitnexus/src/core/group/sync.ts @@ -154,20 +154,6 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis } } - // Process manifest links while DB pools are still open. - // ManifestExtractor is fully implemented but was never wired into this - // pipeline — config.links were parsed and validated but silently dropped. - if (config.links.length > 0) { - const manifestEx = new ManifestExtractor(); - const manifestResult = await manifestEx.extractFromManifest(config.links, dbExecutors); - autoContracts.push(...manifestResult.contracts); - manifestCrossLinks = manifestResult.crossLinks; - if (opts?.verbose) { - console.log( - ` manifest: ${manifestCrossLinks.length} cross-links from ${config.links.length} declared links`, - ); - } - } } finally { for (const id of [...new Set(openPoolIds)]) { await closeLbug(id).catch(() => {}); @@ -175,6 +161,24 @@ export async function syncGroup(config: GroupConfig, opts?: SyncOptions): Promis } } + // Process manifest links declared in group.yaml. + // ManifestExtractor is fully implemented but was never wired into this + // pipeline — config.links were parsed and validated but silently dropped. + // Placed after the DB try/finally: resolveSymbol falls back to synthetic + // UIDs when dbExecutors is undefined or a pool is closed, so cross-links + // are always generated regardless of whether real DB executors are available. + if (config.links.length > 0) { + const manifestEx = new ManifestExtractor(); + const manifestResult = await manifestEx.extractFromManifest(config.links, dbExecutors); + autoContracts.push(...manifestResult.contracts); + manifestCrossLinks = manifestResult.crossLinks; + if (opts?.verbose) { + console.log( + ` manifest: ${manifestCrossLinks.length} cross-links from ${config.links.length} declared links`, + ); + } + } + const { matched, unmatched } = runExactMatch(autoContracts); const crossLinks: CrossLink[] = [...matched, ...manifestCrossLinks]; const allContracts: StoredContract[] = autoContracts; diff --git a/gitnexus/test/unit/group/sync.test.ts b/gitnexus/test/unit/group/sync.test.ts index 50c9093b9..a2d9bca7d 100644 --- a/gitnexus/test/unit/group/sync.test.ts +++ b/gitnexus/test/unit/group/sync.test.ts @@ -3,7 +3,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import { syncGroup, stableRepoPoolId } from '../../../src/core/group/sync.js'; -import type { GroupConfig, StoredContract, RepoHandle } from '../../../src/core/group/types.js'; +import type { GroupConfig, StoredContract, RepoHandle, GroupManifestLink } from '../../../src/core/group/types.js'; import type { RegistryEntry } from '../../../src/storage/repo-manager.js'; describe('syncGroup', () => { @@ -202,6 +202,43 @@ describe('syncGroup', () => { } }); + it('manifest links in config.links produce cross-links with matchType manifest', async () => { + const links: GroupManifestLink[] = [ + { + from: 'app/consumer', + to: 'app/provider', + type: 'http', + contract: 'GET::/api/orders', + role: 'consumer', + }, + ]; + + const config: GroupConfig = { + version: 1, + name: 'test', + description: '', + repos: { 'app/consumer': 'consumer-repo', 'app/provider': 'provider-repo' }, + links, + packages: {}, + detect: { http: true, grpc: false, topics: false, shared_libs: false, embedding_fallback: false }, + matching: { bm25_threshold: 0.7, embedding_threshold: 0.65, max_candidates_per_step: 3 }, + }; + + const result = await syncGroup(config, { + extractorOverride: async () => [], + skipWrite: true, + }); + + // ManifestExtractor should inject 2 contracts (provider + consumer) and 1 cross-link + expect(result.contracts).toHaveLength(2); + const manifestLinks = result.crossLinks.filter((cl) => cl.matchType === 'manifest'); + expect(manifestLinks).toHaveLength(1); + expect(manifestLinks[0].contractId).toBe('http::GET::/api/orders'); + expect(manifestLinks[0].from.repo).toBe('app/consumer'); + expect(manifestLinks[0].to.repo).toBe('app/provider'); + expect(manifestLinks[0].confidence).toBe(1.0); + }); + it('writes registry to groupDir when skipWrite is false', async () => { const tmpDir = path.join(os.tmpdir(), `gitnexus-sync-write-${Date.now()}`); fs.mkdirSync(tmpDir, { recursive: true });