GitNexus/gitnexus/test/unit/group/data-route-table.test.ts
azizur100389 7f0ab16ffe
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / Classify release event (push) Waiting to run
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
feat(routes): support JS data route tables (#2972)
2026-08-18 04:39:45 +01:00

259 lines
9.2 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { HttpRouteExtractor } from '../../../src/core/group/extractors/http-route-extractor.js';
import type { RepoHandle } from '../../../src/core/group/types.js';
import { DATA_ROUTE_TABLE_SOURCE } from '../../../src/core/ingestion/route-extractors/data-route-table.js';
describe('data route table group contracts', () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-route-table-'));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
const repo = (): RepoHandle => ({
id: 'route-table-test',
path: 'test/route-table',
repoPath: tmpDir,
storagePath: path.join(tmpDir, '.gitnexus'),
});
it('resolves imported handlers and leaves source-only members unattributed', async () => {
fs.writeFileSync(
path.join(tmpDir, 'routes.js'),
`import { listUsers as handleUsers } from './handlers.js';
const routes = [
{ path: '/users', method: 'GET', handler: handleUsers },
{ path: '/auth/me', method: 'GET', handler: auth.getCurrentUser },
];
for (const route of routes) {
if (route.path === request.path && route.method === request.method) route.handler();
}
`,
);
fs.writeFileSync(
path.join(tmpDir, 'handlers.js'),
`export function listUsers() { return []; }
export const auth = { getCurrentUser() { return {}; } };
`,
);
const queriedNames: string[] = [];
const db = async (_query: string, params?: Record<string, unknown>) => {
const name = String(params?.name ?? '');
queriedNames.push(name);
if (name !== 'listUsers' && name !== 'getCurrentUser') return [];
return [
{
uid: `symbol:${name}`,
name,
filePath: 'handlers.js',
startLine: 1,
endLine: 2,
labels: ['Function'],
},
];
};
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
const providers = contracts.filter(
(contract) =>
contract.role === 'provider' && contract.meta.framework === DATA_ROUTE_TABLE_SOURCE,
);
expect(
providers
.map((provider) => ({
contractId: provider.contractId,
symbolUid: provider.symbolUid,
symbolName: provider.symbolName,
extractionStrategy: provider.meta.extractionStrategy,
}))
.sort((left, right) => left.contractId.localeCompare(right.contractId)),
).toEqual([
{
contractId: 'http::GET::/auth/me',
symbolUid: '',
symbolName: 'handler',
extractionStrategy: 'source_scan',
},
{
contractId: 'http::GET::/users',
symbolUid: 'symbol:listUsers',
symbolName: 'listUsers',
extractionStrategy: 'source_scan_resolved',
},
]);
expect(queriedNames).not.toContain('getCurrentUser');
});
it('keeps ambiguous handlers unattributed instead of guessing', async () => {
fs.writeFileSync(
path.join(tmpDir, 'routes.js'),
`const routes = [{ path: '/users', method: 'GET', handler: listUsers }];
for (const route of routes) {
if (route.path === request.path && route.method === request.method) route.handler();
}`,
);
const db = async (_query: string, params?: Record<string, unknown>) =>
params?.name === 'listUsers'
? [
{ uid: 'a', name: 'listUsers', filePath: 'a.js' },
{ uid: 'b', name: 'listUsers', filePath: 'b.js' },
]
: [];
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
const provider = contracts.find((contract) => contract.contractId === 'http::GET::/users');
expect(provider).toMatchObject({ symbolUid: '', symbolName: 'listUsers' });
});
it('keeps duplicate same-file handlers unattributed', async () => {
fs.writeFileSync(
path.join(tmpDir, 'routes.js'),
`const routes = [{ path: '/users', method: 'GET', handler: listUsers }];
for (const route of routes) {
if (route.path === request.path && route.method === request.method) route.handler();
}`,
);
const db = async (query: string) =>
query.includes('UNION ALL')
? [
{ uid: 'a', name: 'listUsers', filePath: 'routes.js', labels: ['Function'] },
{ uid: 'b', name: 'listUsers', filePath: 'routes.js', labels: ['Function'] },
]
: [];
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
const provider = contracts.find((contract) => contract.contractId === 'http::GET::/users');
expect(provider).toMatchObject({ symbolUid: '', symbolName: 'listUsers' });
});
it('does not bind an unimported handler to a unique same-name symbol in another file', async () => {
fs.writeFileSync(
path.join(tmpDir, 'routes.js'),
`const routes = [{ path: '/users', method: 'GET', handler: listUsers }];
for (const route of routes) {
if (route.path === request.path && route.method === request.method) route.handler();
}`,
);
const db = async (query: string, params?: Record<string, unknown>) => {
if (query.includes('UNION ALL')) return [];
return params?.name === 'listUsers'
? [{ uid: 'decoy', name: 'listUsers', filePath: 'unrelated.js' }]
: [];
};
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
const provider = contracts.find((contract) => contract.contractId === 'http::GET::/users');
expect(provider).toMatchObject({ symbolUid: '', symbolName: 'listUsers' });
});
it('does not fall back from an unresolved import to a same-name local symbol', async () => {
fs.writeFileSync(
path.join(tmpDir, 'routes.js'),
`import { listUsers } from 'external-router-package';
const routes = [{ path: '/users', method: 'GET', handler: listUsers }];
for (const route of routes) {
if (route.path === request.path && route.method === request.method) route.handler();
}`,
);
const db = async (_query: string, params?: Record<string, unknown>) =>
params?.name === 'listUsers'
? [{ uid: 'local-decoy', name: 'listUsers', filePath: 'local.js' }]
: [];
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
const provider = contracts.find((contract) => contract.contractId === 'http::GET::/users');
expect(provider).toMatchObject({ symbolUid: '', symbolName: 'listUsers' });
});
it('does not attribute an unresolved member handler to its registrar function', async () => {
fs.writeFileSync(
path.join(tmpDir, 'routes.js'),
`function setupRoutes(path, method) {
const routes = [{ path: '/me', method: 'GET', handler: auth.getCurrentUser }];
for (const route of routes) {
if (route.path === request.path && route.method === request.method) route.handler();
}
}
`,
);
const db = async (query: string) =>
query.includes('UNION ALL')
? [
{
uid: 'function:setupRoutes',
name: 'setupRoutes',
filePath: 'routes.js',
startLine: 0,
endLine: 5,
labels: ['Function'],
},
]
: [];
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
const provider = contracts.find((contract) => contract.contractId === 'http::GET::/me');
expect(provider).toMatchObject({ symbolUid: '', symbolName: 'handler' });
});
it('suppresses duplicate identities when any data-table handler is unresolved', async () => {
fs.writeFileSync(
path.join(tmpDir, 'routes.js'),
`function second() {}
const routes = [
{ path: '/users', method: 'GET', handler: missing },
{ path: '/users', method: 'GET', handler: second },
];
for (const route of routes) {
if (route.path === request.path && route.method === request.method) route.handler();
}`,
);
const db = async (query: string) =>
query.includes('UNION ALL')
? [{ uid: 'second', name: 'second', filePath: 'routes.js', labels: ['Function'] }]
: [];
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
expect(contracts.some((contract) => contract.contractId === 'http::GET::/users')).toBe(false);
});
it('suppresses duplicate identities that resolve to different handlers', async () => {
fs.writeFileSync(
path.join(tmpDir, 'routes.js'),
`function first() {}
function second() {}
const routes = [
{ path: '/users', method: 'GET', handler: first },
{ path: '/users', method: 'GET', handler: second },
];
for (const route of routes) {
if (route.path === request.path && route.method === request.method) route.handler();
}`,
);
const db = async (query: string) =>
query.includes('UNION ALL')
? [
{ uid: 'first', name: 'first', filePath: 'routes.js', labels: ['Function'] },
{ uid: 'second', name: 'second', filePath: 'routes.js', labels: ['Function'] },
]
: [];
const contracts = await new HttpRouteExtractor().extract(db, tmpDir, repo());
expect(contracts.some((contract) => contract.contractId === 'http::GET::/users')).toBe(false);
});
});