GitNexus/gitnexus/test/integration/group/include-extractor-sync.test.ts
azizur100389 ff0124e067
feat(cpp): parse CUDA source extensions (#2213)
* feat(cpp): parse CUDA source extensions

* test(cpp): characterize CUDA parser limitations

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-06-16 07:32:53 +01:00

230 lines
8.3 KiB
TypeScript

/**
* Integration test: IncludeExtractor output → group matching → bridge DB.
*
* Covers PR #1156 review finding #7: verifies that the full runtime path
* (IncludeExtractor → StoredContract → runExactMatch → CrossLinks → writeBridge)
* stays wired up. A regression in either normalizeContractId or the include
* branch of ManifestExtractor.resolveSymbol would produce 0 cross-links and
* fail this test.
*/
import { describe, it, expect } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { parseGroupConfig } from '../../../src/core/group/config-parser.js';
import { syncGroup } from '../../../src/core/group/sync.js';
import type { StoredContract } from '../../../src/core/group/types.js';
import { IncludeExtractor } from '../../../src/core/group/extractors/include-extractor.js';
import { normalizeContractId } from '../../../src/core/group/matching.js';
const GROUP_YAML = [
'version: 1',
'name: include-test-group',
'description: "IncludeExtractor integration test"',
'',
'repos:',
' app/provider: include-provider',
' app/consumer: include-consumer',
'',
'links: []',
'packages: {}',
'',
'detect:',
' http: false',
' grpc: false',
' topics: false',
' shared_libs: false',
' includes: true',
' embedding_fallback: false',
'',
'matching:',
' bm25_threshold: 0.7',
' embedding_threshold: 0.65',
' max_candidates_per_step: 3',
].join('\n');
describe('IncludeExtractor → syncGroup integration (finding #7)', () => {
it('produces a CrossLink when provider and consumer emit the same include contract-id', async () => {
const config = parseGroupConfig(GROUP_YAML);
// Mock the IncludeExtractor output directly — a header provider in one
// repo and a quoted #include consumer in the other, both normalized to
// the same include::map/base/view.h contract-id.
const mockContracts: StoredContract[] = [
{
contractId: 'include::map/base/view.h',
type: 'include',
role: 'provider',
symbolUid: 'File:map/base/view.h',
symbolRef: { filePath: 'map/base/view.h', name: 'view.h' },
symbolName: 'view.h',
confidence: 0.95,
meta: { source: 'filesystem' },
repo: 'app/provider',
},
{
contractId: 'include::map/base/view.h',
type: 'include',
role: 'consumer',
symbolUid: 'File:src/controller.cpp',
symbolRef: { filePath: 'src/controller.cpp', name: 'map/base/view.h' },
symbolName: 'map/base/view.h',
confidence: 0.85,
meta: { source: 'tree_sitter', includePath: 'map/base/view.h' },
repo: 'app/consumer',
},
];
const result = await syncGroup(config, {
extractorOverride: async () => mockContracts,
skipWrite: true,
});
const includeLinks = result.crossLinks.filter((l) => l.type === 'include');
expect(includeLinks.length).toBeGreaterThanOrEqual(1);
const link = includeLinks[0];
expect(link.contractId).toBe('include::map/base/view.h');
expect(link.matchType).toBe('exact');
expect(link.from.repo).toBe('app/consumer');
expect(link.to.repo).toBe('app/provider');
});
it('normalizes mixed-case / backslash include paths to the same contract-id end-to-end', async () => {
const config = parseGroupConfig(GROUP_YAML);
// Provider writes the canonical form; consumer's include has mixed case
// and a backslash. After normalizeContractId they must still match.
const providerId = 'include::map/base/view.h';
const rawConsumerId = 'include::Map\\Base\\View.h';
// Sanity — normalizeContractId must collapse them.
expect(normalizeContractId(rawConsumerId)).toBe(providerId);
const mockContracts: StoredContract[] = [
{
contractId: providerId,
type: 'include',
role: 'provider',
symbolUid: 'File:map/base/view.h',
symbolRef: { filePath: 'map/base/view.h', name: 'view.h' },
symbolName: 'view.h',
confidence: 0.95,
meta: { source: 'filesystem' },
repo: 'app/provider',
},
{
contractId: rawConsumerId,
type: 'include',
role: 'consumer',
symbolUid: 'File:src/controller.cpp',
symbolRef: { filePath: 'src/controller.cpp', name: 'Map/Base/View.h' },
symbolName: 'Map/Base/View.h',
confidence: 0.85,
meta: { source: 'tree_sitter', includePath: 'Map\\Base\\View.h' },
repo: 'app/consumer',
},
];
const result = await syncGroup(config, {
extractorOverride: async () => mockContracts,
skipWrite: true,
});
const includeLinks = result.crossLinks.filter((l) => l.type === 'include');
expect(includeLinks.length).toBeGreaterThanOrEqual(1);
});
it('round-trip: extractor output from two real temp repos produces matching contract-ids', async () => {
// Drives the extractor directly (no `syncGroup`) against two on-disk
// fixture repos, then hands the StoredContract-shaped output to
// syncGroup via extractorOverride. This exercises the real extraction
// code + the matching pipeline together.
const providerDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-include-int-provider-'));
const consumerDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-include-int-consumer-'));
try {
fs.mkdirSync(path.join(providerDir, 'shared/api'), { recursive: true });
fs.writeFileSync(
path.join(providerDir, 'shared/api/client.h'),
'#pragma once\nstruct Client {};',
);
fs.mkdirSync(path.join(consumerDir, 'src'), { recursive: true });
fs.writeFileSync(
path.join(consumerDir, 'src/main.cpp'),
'#include "shared/api/client.h"\nint main(){return 0;}',
);
const extractor = new IncludeExtractor();
const providerOutput = await extractor.extract(null, providerDir, {
id: 'provider',
path: 'app/provider',
repoPath: providerDir,
storagePath: path.join(providerDir, '.gitnexus'),
});
const consumerOutput = await extractor.extract(null, consumerDir, {
id: 'consumer',
path: 'app/consumer',
repoPath: consumerDir,
storagePath: path.join(consumerDir, '.gitnexus'),
});
const stored: StoredContract[] = [
...providerOutput
.filter((c) => c.role === 'provider')
.map((c) => ({ ...c, repo: 'app/provider' })),
...consumerOutput
.filter((c) => c.role === 'consumer')
.map((c) => ({ ...c, repo: 'app/consumer' })),
];
const config = parseGroupConfig(GROUP_YAML);
const result = await syncGroup(config, {
extractorOverride: async () => stored,
skipWrite: true,
});
const includeLinks = result.crossLinks.filter((l) => l.type === 'include');
expect(includeLinks.length).toBeGreaterThanOrEqual(1);
expect(includeLinks[0].contractId).toBe('include::shared/api/client.h');
expect(includeLinks[0].matchType).toBe('exact');
} finally {
fs.rmSync(providerDir, { recursive: true, force: true });
fs.rmSync(consumerDir, { recursive: true, force: true });
}
});
it('suppresses extensionless local includes that resolve to .cuh headers', async () => {
const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-include-cuh-'));
try {
fs.mkdirSync(path.join(repoDir, 'include'), { recursive: true });
fs.mkdirSync(path.join(repoDir, 'src'), { recursive: true });
fs.writeFileSync(
path.join(repoDir, 'include/kernel.cuh'),
'#pragma once\n__global__ void kernel();',
);
fs.writeFileSync(
path.join(repoDir, 'src/main.cu'),
'#include "include/kernel"\nvoid host() {}',
);
const extractor = new IncludeExtractor();
const contracts = await extractor.extract(null, repoDir, {
id: 'cuda-repo',
path: 'app/cuda-repo',
repoPath: repoDir,
storagePath: path.join(repoDir, '.gitnexus'),
});
expect(
contracts.some(
(c) => c.role === 'provider' && c.contractId === 'include::include/kernel.cuh',
),
).toBe(true);
expect(
contracts.some((c) => c.role === 'consumer' && c.contractId === 'include::include/kernel'),
).toBe(false);
} finally {
fs.rmSync(repoDir, { recursive: true, force: true });
}
});
});