mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-04 02:31:36 +00:00
* fix(group): add configurable cross-link path exclusions to reduce false positives Add matching.exclude_links_paths and matching.exclude_links_param_only_paths to group.yaml config. These filter out noisy HTTP contracts (health checks, param-only catch-all routes) from cross-link matching while preserving them in the contract registry for documentation purposes. Defaults are empty/false for backward compatibility — no behavior change unless the operator explicitly configures exclusions. * fix(group): address review findings — filter unmatched, normalize trailing slash, add tests - Excluded contracts no longer inflate SyncResult.unmatched (isNoisy guard) - pathPart in buildNoisyContractFilter strips trailing slashes before comparison - 8 new unit tests for buildNoisyContractFilter covering all code paths - Config-parser test asserts defaults for new matching fields * fix(group): normalize configured exclusion paths and add root-path test - Strip trailing slashes from configured exclude_links_paths at Set-build time so root path '/' (which normalizes to '') matches correctly - Add test: exclude_links_paths: ['/'] suppresses http::GET::/ contracts - Add new matching fields as commented examples in fixture group.yaml (DoD §2.4) * docs(group): document exclude_links_paths and exclude_links_param_only_paths config fields Add JSDoc to MatchingConfig interface, update the microservices guide YAML example and field notes, and scaffold the new fields (commented out) in the group create template.
629 lines
21 KiB
TypeScript
629 lines
21 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
runExactMatch,
|
|
normalizeContractId,
|
|
buildProviderIndex,
|
|
runWildcardMatch,
|
|
} from '../../../src/core/group/matching.js';
|
|
import type { StoredContract, MatchingConfig } from '../../../src/core/group/types.js';
|
|
|
|
describe('normalizeContractId', () => {
|
|
it('lowercases HTTP method', () => {
|
|
expect(normalizeContractId('http::get::/api/users')).toBe('http::GET::/api/users');
|
|
});
|
|
|
|
it('strips trailing slash from HTTP path', () => {
|
|
expect(normalizeContractId('http::GET::/api/users/')).toBe('http::GET::/api/users');
|
|
});
|
|
|
|
it('lowercases gRPC package', () => {
|
|
expect(normalizeContractId('grpc::Hr.UserService/GetUser')).toBe(
|
|
'grpc::hr.userservice/GetUser',
|
|
);
|
|
});
|
|
|
|
it('preserves case for malformed gRPC id with leading slash (no full-string lowercasing)', () => {
|
|
expect(normalizeContractId('grpc::/MyPkg/DoThing')).toBe('grpc::/MyPkg/DoThing');
|
|
});
|
|
|
|
it('handles malformed grpc with leading slash and no package', () => {
|
|
// grpc::/Method — leading slash, no package
|
|
expect(normalizeContractId('grpc::/Method')).toBe('grpc::/Method');
|
|
});
|
|
|
|
it('handles grpc with no slash at all', () => {
|
|
// grpc::ServiceName — no slash, ambiguous; MVP: lowercase entire token
|
|
expect(normalizeContractId('grpc::ServiceName')).toBe('grpc::servicename');
|
|
});
|
|
|
|
it('trims and lowercases topic', () => {
|
|
expect(normalizeContractId('topic:: Employee.Hired ')).toBe('topic::employee.hired');
|
|
});
|
|
|
|
it('lowercases lib package coordinates', () => {
|
|
expect(normalizeContractId('lib::@Hr/Common::UserDTO')).toBe('lib::@hr/common::userdto');
|
|
});
|
|
});
|
|
|
|
describe('runExactMatch', () => {
|
|
const makeContract = (
|
|
id: string,
|
|
role: 'provider' | 'consumer',
|
|
repo: string,
|
|
): StoredContract => ({
|
|
contractId: id,
|
|
type: 'http',
|
|
role,
|
|
symbolUid: `uid-${repo}-${id}`,
|
|
symbolRef: { filePath: `src/${repo}.ts`, name: `fn-${id}` },
|
|
symbolName: `fn-${id}`,
|
|
confidence: 0.8,
|
|
meta: {},
|
|
repo,
|
|
});
|
|
|
|
it('matches provider and consumer with same contract ID', () => {
|
|
const contracts: StoredContract[] = [
|
|
makeContract('http::GET::/api/users', 'provider', 'backend'),
|
|
makeContract('http::GET::/api/users', 'consumer', 'frontend'),
|
|
];
|
|
|
|
const { matched, unmatched } = runExactMatch(contracts);
|
|
|
|
expect(matched).toHaveLength(1);
|
|
expect(matched[0].contractId).toBe('http::GET::/api/users');
|
|
expect(matched[0].matchType).toBe('exact');
|
|
expect(matched[0].confidence).toBe(1.0);
|
|
expect(matched[0].from.repo).toBe('frontend');
|
|
expect(matched[0].to.repo).toBe('backend');
|
|
expect(unmatched).toHaveLength(0);
|
|
});
|
|
|
|
it('handles multiple consumers for one provider', () => {
|
|
const contracts: StoredContract[] = [
|
|
makeContract('http::GET::/api/users', 'provider', 'backend'),
|
|
makeContract('http::GET::/api/users', 'consumer', 'frontend'),
|
|
makeContract('http::GET::/api/users', 'consumer', 'bff'),
|
|
];
|
|
|
|
const { matched } = runExactMatch(contracts);
|
|
expect(matched).toHaveLength(2);
|
|
});
|
|
|
|
it('reports unmatched contracts', () => {
|
|
const contracts: StoredContract[] = [
|
|
makeContract('http::GET::/api/users', 'provider', 'backend'),
|
|
makeContract('http::GET::/api/orphan', 'consumer', 'frontend'),
|
|
];
|
|
|
|
const { matched, unmatched } = runExactMatch(contracts);
|
|
expect(matched).toHaveLength(0);
|
|
expect(unmatched).toHaveLength(2);
|
|
});
|
|
|
|
it('normalizes contract IDs before matching', () => {
|
|
const contracts: StoredContract[] = [
|
|
makeContract('http::GET::/api/users/', 'provider', 'backend'),
|
|
makeContract('http::get::/api/users', 'consumer', 'frontend'),
|
|
];
|
|
|
|
const { matched } = runExactMatch(contracts);
|
|
expect(matched).toHaveLength(1);
|
|
});
|
|
|
|
it('does not match contracts within the same repo', () => {
|
|
const contracts: StoredContract[] = [
|
|
makeContract('http::GET::/api/users', 'provider', 'backend'),
|
|
makeContract('http::GET::/api/users', 'consumer', 'backend'),
|
|
];
|
|
|
|
const { matched } = runExactMatch(contracts);
|
|
expect(matched).toHaveLength(0);
|
|
});
|
|
|
|
it('matches same-repo contracts with different service boundaries', () => {
|
|
const contracts: StoredContract[] = [
|
|
{
|
|
...makeContract('http::GET::/api/users', 'provider', 'monorepo'),
|
|
service: 'services/auth',
|
|
},
|
|
{
|
|
...makeContract('http::GET::/api/users', 'consumer', 'monorepo'),
|
|
service: 'services/gateway',
|
|
},
|
|
];
|
|
|
|
const { matched } = runExactMatch(contracts);
|
|
expect(matched).toHaveLength(1);
|
|
expect(matched[0].from.repo).toBe('monorepo');
|
|
expect(matched[0].to.repo).toBe('monorepo');
|
|
expect(matched[0].from.service).toBe('services/gateway');
|
|
expect(matched[0].to.service).toBe('services/auth');
|
|
});
|
|
|
|
it('does not match same-repo contracts with same service', () => {
|
|
const contracts: StoredContract[] = [
|
|
{
|
|
...makeContract('http::GET::/api/users', 'provider', 'monorepo'),
|
|
service: 'services/auth',
|
|
},
|
|
{
|
|
...makeContract('http::GET::/api/users', 'consumer', 'monorepo'),
|
|
service: 'services/auth',
|
|
},
|
|
];
|
|
|
|
const { matched } = runExactMatch(contracts);
|
|
expect(matched).toHaveLength(0);
|
|
});
|
|
|
|
it('does not match same-repo when only one has service', () => {
|
|
const contracts: StoredContract[] = [
|
|
{
|
|
...makeContract('http::GET::/api/users', 'provider', 'monorepo'),
|
|
service: 'services/auth',
|
|
},
|
|
makeContract('http::GET::/api/users', 'consumer', 'monorepo'),
|
|
];
|
|
|
|
const { matched } = runExactMatch(contracts);
|
|
expect(matched).toHaveLength(0);
|
|
});
|
|
|
|
it('cross-repo matching works regardless of service field', () => {
|
|
const contracts: StoredContract[] = [
|
|
{ ...makeContract('http::GET::/api/users', 'provider', 'backend'), service: 'services/auth' },
|
|
{ ...makeContract('http::GET::/api/users', 'consumer', 'frontend'), service: 'services/web' },
|
|
];
|
|
|
|
const { matched } = runExactMatch(contracts);
|
|
expect(matched).toHaveLength(1);
|
|
expect(matched[0].from.service).toBe('services/web');
|
|
expect(matched[0].to.service).toBe('services/auth');
|
|
});
|
|
|
|
it('matches consumer http::*::path to a concrete provider method on that path', () => {
|
|
const contracts: StoredContract[] = [
|
|
makeContract('http::POST::/api/users', 'provider', 'backend'),
|
|
makeContract('http::*::/api/users', 'consumer', 'frontend'),
|
|
];
|
|
|
|
const { matched, unmatched } = runExactMatch(contracts);
|
|
expect(matched).toHaveLength(1);
|
|
expect(matched[0].contractId).toBe('http::*::/api/users');
|
|
expect(matched[0].to.repo).toBe('backend');
|
|
expect(unmatched).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers for Task 6 tests
|
|
// ---------------------------------------------------------------------------
|
|
function makeGrpcContract(
|
|
id: string,
|
|
role: 'provider' | 'consumer',
|
|
repo: string,
|
|
overrides: Partial<StoredContract> = {},
|
|
): StoredContract {
|
|
return {
|
|
contractId: id,
|
|
type: 'grpc',
|
|
role,
|
|
symbolUid: `uid-${repo}-${id}`,
|
|
symbolRef: { filePath: `src/${repo}.ts`, name: `fn-${id}` },
|
|
symbolName: `fn-${id}`,
|
|
confidence: 0.9,
|
|
meta: {},
|
|
repo,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// buildProviderIndex
|
|
// ---------------------------------------------------------------------------
|
|
describe('buildProviderIndex', () => {
|
|
it('test_buildProviderIndex_creates_normalized_keys', () => {
|
|
const contracts: StoredContract[] = [
|
|
makeGrpcContract('grpc::Com.Example.UserService/GetUser', 'provider', 'backend'),
|
|
makeGrpcContract('grpc::Com.Example.UserService/GetUser', 'consumer', 'frontend'),
|
|
];
|
|
|
|
const index = buildProviderIndex(contracts);
|
|
|
|
// Only providers should be in the index
|
|
expect(index.size).toBe(1);
|
|
// Key should be normalized (lowercased package)
|
|
expect(index.has('grpc::com.example.userservice/GetUser')).toBe(true);
|
|
expect(index.get('grpc::com.example.userservice/GetUser')).toHaveLength(1);
|
|
expect(index.get('grpc::com.example.userservice/GetUser')![0].role).toBe('provider');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// runExactMatch — gRPC wildcard skip
|
|
// ---------------------------------------------------------------------------
|
|
describe('runExactMatch — gRPC wildcard handling', () => {
|
|
it('test_runExactMatch_skips_grpc_wildcard_contracts', () => {
|
|
const contracts: StoredContract[] = [
|
|
makeGrpcContract('grpc::com.example.UserService/*', 'consumer', 'frontend'),
|
|
makeGrpcContract('grpc::com.example.UserService/*', 'provider', 'backend'),
|
|
];
|
|
|
|
const { matched, unmatched } = runExactMatch(contracts);
|
|
|
|
// gRPC wildcards should NOT be matched in exact pass
|
|
expect(matched).toHaveLength(0);
|
|
// Both should appear in unmatched
|
|
expect(unmatched).toHaveLength(2);
|
|
});
|
|
|
|
it('test_runExactMatch_does_not_skip_http_wildcards', () => {
|
|
const contracts: StoredContract[] = [
|
|
{
|
|
contractId: 'http::GET::/api/users',
|
|
type: 'http',
|
|
role: 'provider',
|
|
symbolUid: 'uid-backend-http',
|
|
symbolRef: { filePath: 'src/backend.ts', name: 'fn-http' },
|
|
symbolName: 'fn-http',
|
|
confidence: 0.9,
|
|
meta: {},
|
|
repo: 'backend',
|
|
},
|
|
{
|
|
contractId: 'http::*::/api/users',
|
|
type: 'http',
|
|
role: 'consumer',
|
|
symbolUid: 'uid-frontend-http',
|
|
symbolRef: { filePath: 'src/frontend.ts', name: 'fn-http' },
|
|
symbolName: 'fn-http',
|
|
confidence: 0.9,
|
|
meta: {},
|
|
repo: 'frontend',
|
|
},
|
|
];
|
|
|
|
const { matched } = runExactMatch(contracts);
|
|
// HTTP wildcard should still match via findMatchingKeys
|
|
expect(matched).toHaveLength(1);
|
|
expect(matched[0].contractId).toBe('http::*::/api/users');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// runWildcardMatch
|
|
// ---------------------------------------------------------------------------
|
|
describe('runWildcardMatch', () => {
|
|
it('test_runWildcardMatch_fq_service_match', () => {
|
|
const consumer = makeGrpcContract('grpc::com.example.UserService/*', 'consumer', 'frontend');
|
|
const provider = makeGrpcContract(
|
|
'grpc::com.example.UserService/GetUser',
|
|
'provider',
|
|
'backend',
|
|
);
|
|
|
|
const providerIndex = buildProviderIndex([provider]);
|
|
const { matched } = runWildcardMatch([consumer], providerIndex);
|
|
|
|
expect(matched).toHaveLength(1);
|
|
expect(matched[0].from.repo).toBe('frontend');
|
|
expect(matched[0].to.repo).toBe('backend');
|
|
});
|
|
|
|
it('test_runWildcardMatch_bare_name_match', () => {
|
|
const consumer = makeGrpcContract('grpc::UserService/*', 'consumer', 'frontend');
|
|
const provider = makeGrpcContract(
|
|
'grpc::com.example.UserService/GetUser',
|
|
'provider',
|
|
'backend',
|
|
);
|
|
|
|
const providerIndex = buildProviderIndex([provider]);
|
|
const { matched } = runWildcardMatch([consumer], providerIndex);
|
|
|
|
expect(matched).toHaveLength(1);
|
|
expect(matched[0].from.repo).toBe('frontend');
|
|
expect(matched[0].to.repo).toBe('backend');
|
|
});
|
|
|
|
it('test_runWildcardMatch_no_match_different_service', () => {
|
|
const consumer = makeGrpcContract('grpc::UserService/*', 'consumer', 'frontend');
|
|
const provider = makeGrpcContract(
|
|
'grpc::com.example.OtherService/GetUser',
|
|
'provider',
|
|
'backend',
|
|
);
|
|
|
|
const providerIndex = buildProviderIndex([provider]);
|
|
const { matched, remaining } = runWildcardMatch([consumer], providerIndex);
|
|
|
|
expect(matched).toHaveLength(0);
|
|
expect(remaining).toContainEqual(consumer);
|
|
});
|
|
|
|
it('test_runWildcardMatch_skips_wildcard_providers', () => {
|
|
const consumer = makeGrpcContract('grpc::com.example.UserService/*', 'consumer', 'frontend');
|
|
const provider = makeGrpcContract('grpc::com.example.UserService/*', 'provider', 'backend');
|
|
|
|
const providerIndex = buildProviderIndex([provider]);
|
|
const { matched } = runWildcardMatch([consumer], providerIndex);
|
|
|
|
// Wildcard provider key ends with /*, so it should be skipped
|
|
expect(matched).toHaveLength(0);
|
|
});
|
|
|
|
it('test_runWildcardMatch_confidence_min', () => {
|
|
const consumer = makeGrpcContract('grpc::com.example.UserService/*', 'consumer', 'frontend', {
|
|
confidence: 0.7,
|
|
});
|
|
const provider = makeGrpcContract(
|
|
'grpc::com.example.UserService/GetUser',
|
|
'provider',
|
|
'backend',
|
|
{
|
|
confidence: 0.5,
|
|
},
|
|
);
|
|
|
|
const providerIndex = buildProviderIndex([provider]);
|
|
const { matched } = runWildcardMatch([consumer], providerIndex);
|
|
|
|
expect(matched).toHaveLength(1);
|
|
expect(matched[0].confidence).toBe(0.5);
|
|
});
|
|
|
|
it('test_runWildcardMatch_matchType_wildcard', () => {
|
|
const consumer = makeGrpcContract('grpc::com.example.UserService/*', 'consumer', 'frontend');
|
|
const provider = makeGrpcContract(
|
|
'grpc::com.example.UserService/GetUser',
|
|
'provider',
|
|
'backend',
|
|
);
|
|
|
|
const providerIndex = buildProviderIndex([provider]);
|
|
const { matched } = runWildcardMatch([consumer], providerIndex);
|
|
|
|
expect(matched).toHaveLength(1);
|
|
expect(matched[0].matchType).toBe('wildcard');
|
|
});
|
|
|
|
it('test_runWildcardMatch_contractId_is_consumers', () => {
|
|
const consumer = makeGrpcContract('grpc::com.example.UserService/*', 'consumer', 'frontend');
|
|
const provider = makeGrpcContract(
|
|
'grpc::com.example.UserService/GetUser',
|
|
'provider',
|
|
'backend',
|
|
);
|
|
|
|
const providerIndex = buildProviderIndex([provider]);
|
|
const { matched } = runWildcardMatch([consumer], providerIndex);
|
|
|
|
expect(matched).toHaveLength(1);
|
|
expect(matched[0].contractId).toBe('grpc::com.example.UserService/*');
|
|
});
|
|
});
|
|
|
|
describe('buildNoisyContractFilter (via runExactMatch)', () => {
|
|
const makeContract = (
|
|
id: string,
|
|
role: 'provider' | 'consumer',
|
|
repo: string,
|
|
): StoredContract => ({
|
|
contractId: id,
|
|
type: 'http',
|
|
role,
|
|
symbolUid: `uid-${repo}-${id}`,
|
|
symbolRef: { filePath: `src/${repo}.ts`, name: `fn-${id}` },
|
|
symbolName: `fn-${id}`,
|
|
confidence: 0.8,
|
|
meta: {},
|
|
repo,
|
|
});
|
|
|
|
it('exclude_links_paths prevents cross-links for configured paths', () => {
|
|
const matchingConfig: MatchingConfig = {
|
|
bm25_threshold: 0.7,
|
|
embedding_threshold: 0.65,
|
|
max_candidates_per_step: 3,
|
|
exclude_links_paths: ['/ping'],
|
|
exclude_links_param_only_paths: false,
|
|
};
|
|
|
|
const contracts: StoredContract[] = [
|
|
makeContract('http::GET::/ping', 'provider', 'backend'),
|
|
makeContract('http::GET::/ping', 'consumer', 'frontend'),
|
|
makeContract('http::GET::/api/users', 'provider', 'backend'),
|
|
makeContract('http::GET::/api/users', 'consumer', 'frontend'),
|
|
];
|
|
|
|
const providerIndex = buildProviderIndex(contracts, matchingConfig);
|
|
const { matched, unmatched } = runExactMatch(contracts, providerIndex, matchingConfig);
|
|
|
|
expect(matched).toHaveLength(1);
|
|
expect(matched[0].contractId).toBe('http::GET::/api/users');
|
|
});
|
|
|
|
it('excluded providers do not appear in matched', () => {
|
|
const matchingConfig: MatchingConfig = {
|
|
bm25_threshold: 0.7,
|
|
embedding_threshold: 0.65,
|
|
max_candidates_per_step: 3,
|
|
exclude_links_paths: ['/health'],
|
|
exclude_links_param_only_paths: false,
|
|
};
|
|
|
|
const contracts: StoredContract[] = [
|
|
makeContract('http::GET::/health', 'provider', 'backend'),
|
|
makeContract('http::GET::/health', 'consumer', 'frontend'),
|
|
];
|
|
|
|
const providerIndex = buildProviderIndex(contracts, matchingConfig);
|
|
const { matched } = runExactMatch(contracts, providerIndex, matchingConfig);
|
|
|
|
expect(matched).toHaveLength(0);
|
|
});
|
|
|
|
it('excluded contracts do not appear in unmatched', () => {
|
|
const matchingConfig: MatchingConfig = {
|
|
bm25_threshold: 0.7,
|
|
embedding_threshold: 0.65,
|
|
max_candidates_per_step: 3,
|
|
exclude_links_paths: ['/ping'],
|
|
exclude_links_param_only_paths: false,
|
|
};
|
|
|
|
const contracts: StoredContract[] = [
|
|
makeContract('http::GET::/ping', 'provider', 'backend'),
|
|
makeContract('http::GET::/ping', 'consumer', 'frontend'),
|
|
];
|
|
|
|
const providerIndex = buildProviderIndex(contracts, matchingConfig);
|
|
const { matched, unmatched } = runExactMatch(contracts, providerIndex, matchingConfig);
|
|
|
|
expect(matched).toHaveLength(0);
|
|
expect(unmatched).toHaveLength(0);
|
|
});
|
|
|
|
it('exclude_links_param_only_paths filters /{param} and /{param}/{param}', () => {
|
|
const matchingConfig: MatchingConfig = {
|
|
bm25_threshold: 0.7,
|
|
embedding_threshold: 0.65,
|
|
max_candidates_per_step: 3,
|
|
exclude_links_paths: [],
|
|
exclude_links_param_only_paths: true,
|
|
};
|
|
|
|
const contracts: StoredContract[] = [
|
|
makeContract('http::GET::/{param}', 'provider', 'backend'),
|
|
makeContract('http::GET::/{param}', 'consumer', 'frontend'),
|
|
makeContract('http::GET::/{param}/{param}', 'provider', 'backend'),
|
|
makeContract('http::GET::/{param}/{param}', 'consumer', 'frontend'),
|
|
];
|
|
|
|
const providerIndex = buildProviderIndex(contracts, matchingConfig);
|
|
const { matched, unmatched } = runExactMatch(contracts, providerIndex, matchingConfig);
|
|
|
|
expect(matched).toHaveLength(0);
|
|
expect(unmatched).toHaveLength(0);
|
|
});
|
|
|
|
it('mixed routes like /users/{param} are NOT excluded by param_only', () => {
|
|
const matchingConfig: MatchingConfig = {
|
|
bm25_threshold: 0.7,
|
|
embedding_threshold: 0.65,
|
|
max_candidates_per_step: 3,
|
|
exclude_links_paths: [],
|
|
exclude_links_param_only_paths: true,
|
|
};
|
|
|
|
const contracts: StoredContract[] = [
|
|
makeContract('http::GET::/users/{param}', 'provider', 'backend'),
|
|
makeContract('http::GET::/users/{param}', 'consumer', 'frontend'),
|
|
];
|
|
|
|
const providerIndex = buildProviderIndex(contracts, matchingConfig);
|
|
const { matched } = runExactMatch(contracts, providerIndex, matchingConfig);
|
|
|
|
expect(matched).toHaveLength(1);
|
|
expect(matched[0].contractId).toBe('http::GET::/users/{param}');
|
|
});
|
|
|
|
it('default config (no exclusions) produces no filtering', () => {
|
|
const contracts: StoredContract[] = [
|
|
makeContract('http::GET::/ping', 'provider', 'backend'),
|
|
makeContract('http::GET::/ping', 'consumer', 'frontend'),
|
|
makeContract('http::GET::/{param}', 'provider', 'backend'),
|
|
makeContract('http::GET::/{param}', 'consumer', 'frontend'),
|
|
];
|
|
|
|
const { matched } = runExactMatch(contracts);
|
|
|
|
expect(matched).toHaveLength(2);
|
|
});
|
|
|
|
it('trailing slash on contractId still matches configured exclusion', () => {
|
|
const matchingConfig: MatchingConfig = {
|
|
bm25_threshold: 0.7,
|
|
embedding_threshold: 0.65,
|
|
max_candidates_per_step: 3,
|
|
exclude_links_paths: ['/ping'],
|
|
exclude_links_param_only_paths: false,
|
|
};
|
|
|
|
const contracts: StoredContract[] = [
|
|
makeContract('http::GET::/ping/', 'provider', 'backend'),
|
|
makeContract('http::GET::/ping/', 'consumer', 'frontend'),
|
|
];
|
|
|
|
const providerIndex = buildProviderIndex(contracts, matchingConfig);
|
|
const { matched, unmatched } = runExactMatch(contracts, providerIndex, matchingConfig);
|
|
|
|
expect(matched).toHaveLength(0);
|
|
expect(unmatched).toHaveLength(0);
|
|
});
|
|
|
|
it('root path exclusion ["/"] suppresses http::GET::/ contracts', () => {
|
|
const matchingConfig: MatchingConfig = {
|
|
bm25_threshold: 0.7,
|
|
embedding_threshold: 0.65,
|
|
max_candidates_per_step: 3,
|
|
exclude_links_paths: ['/'],
|
|
exclude_links_param_only_paths: false,
|
|
};
|
|
|
|
const contracts: StoredContract[] = [
|
|
makeContract('http::GET::/', 'provider', 'backend'),
|
|
makeContract('http::GET::/', 'consumer', 'frontend'),
|
|
makeContract('http::GET::/api/users', 'provider', 'backend'),
|
|
makeContract('http::GET::/api/users', 'consumer', 'frontend'),
|
|
];
|
|
|
|
const providerIndex = buildProviderIndex(contracts, matchingConfig);
|
|
const { matched, unmatched } = runExactMatch(contracts, providerIndex, matchingConfig);
|
|
|
|
expect(matched).toHaveLength(1);
|
|
expect(matched[0].contractId).toBe('http::GET::/api/users');
|
|
expect(unmatched).toHaveLength(0);
|
|
});
|
|
|
|
it('non-HTTP contracts are never filtered', () => {
|
|
const matchingConfig: MatchingConfig = {
|
|
bm25_threshold: 0.7,
|
|
embedding_threshold: 0.65,
|
|
max_candidates_per_step: 3,
|
|
exclude_links_paths: ['/ping'],
|
|
exclude_links_param_only_paths: true,
|
|
};
|
|
|
|
const contracts: StoredContract[] = [
|
|
{
|
|
contractId: 'topic::events.ping',
|
|
type: 'topic',
|
|
role: 'provider',
|
|
symbolUid: 'uid-backend-topic',
|
|
symbolRef: { filePath: 'src/backend.ts', name: 'fn-topic' },
|
|
symbolName: 'fn-topic',
|
|
confidence: 0.8,
|
|
meta: {},
|
|
repo: 'backend',
|
|
},
|
|
{
|
|
contractId: 'topic::events.ping',
|
|
type: 'topic',
|
|
role: 'consumer',
|
|
symbolUid: 'uid-frontend-topic',
|
|
symbolRef: { filePath: 'src/frontend.ts', name: 'fn-topic' },
|
|
symbolName: 'fn-topic',
|
|
confidence: 0.8,
|
|
meta: {},
|
|
repo: 'frontend',
|
|
},
|
|
];
|
|
|
|
const providerIndex = buildProviderIndex(contracts, matchingConfig);
|
|
const { matched } = runExactMatch(contracts, providerIndex, matchingConfig);
|
|
|
|
expect(matched).toHaveLength(1);
|
|
});
|
|
});
|