mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-04 02:31:36 +00:00
* fix(group): extract NestJS GraphQL contracts against real 0-based indexes (#3201) Provider lookup used 1-based startLine while the graph stores tree-sitter rows, so every resolver missed. Also try PascalCased Document names and inline sibling FragmentDoc interpolations from graphql-codegen output. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(group): bind arrow-field providers and fail closed on interpolations Match Method startLine to the public_field_definition wrapper, decode template escape sequences, and reject FragmentDoc names that mix static and dynamic declarators. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(group): only inline interpolated templates under a gql tag Cooked reconstruction is not the runtime value for String.raw or unknown tags, so those interpolations stay fail-closed. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(group): tighten gql-tag trust and PascalCase Document lookup Only the identifier `gql` is a trusted interpolating tag. Underscored operation names now try the full pascal-case Document candidate graphql-codegen emits. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(group): memoize GraphQL interpolation source resolution Avoid exponential re-walks when the same fragment name is declared twice at each layer of a ${FragmentDoc} chain. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(group): fail closed on invalid tagged-template escapes Treat line continuations as empty cooked text and reject \8/\9 plus legacy octals so reconstructed gql source matches runtime. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com>
183 lines
6.4 KiB
TypeScript
183 lines
6.4 KiB
TypeScript
import * as fs from 'node:fs/promises';
|
|
import * as path from 'node:path';
|
|
import { afterAll, expect, it, vi } from 'vitest';
|
|
import { GraphqlExtractor } from '../../../src/core/group/extractors/graphql-extractor.js';
|
|
import { syncGroup } from '../../../src/core/group/sync.js';
|
|
import { closeLbug, executeParameterized } from '../../../src/core/lbug/pool-adapter.js';
|
|
import type { GroupConfig, RepoHandle } from '../../../src/core/group/types.js';
|
|
import { withTestLbugDB } from '../../helpers/test-indexed-db.js';
|
|
|
|
// Graph Method.startLine is 0-based (tree-sitter row). `health()` is source line 5.
|
|
const SEED = [
|
|
`CREATE (:Method {id:'method:health', name:'health', filePath:'src/health.resolver.ts', startLine:4, endLine:4, content:'', description:''})`,
|
|
`CREATE (:Method {id:'method:save', name:'save', filePath:'src/health.resolver.ts', startLine:6, endLine:6, content:'', description:''})`,
|
|
`CREATE (:Const {id:'const:health-document', name:'HealthDocument', filePath:'src/generated.ts', startLine:0, endLine:0, content:'', description:''})`,
|
|
];
|
|
|
|
withTestLbugDB(
|
|
'graphql-resolve-symbol',
|
|
(handle) => {
|
|
let providerRoot = '';
|
|
let consumerRoot = '';
|
|
|
|
afterAll(async () => {
|
|
try {
|
|
await closeLbug(handle.repoId);
|
|
} catch {
|
|
/* best-effort */
|
|
}
|
|
});
|
|
|
|
it('anchors provider and generated Document consumer through real LadybugDB queries', async () => {
|
|
providerRoot = path.join(handle.tmpHandle.dbPath, 'provider-repo');
|
|
consumerRoot = path.join(handle.tmpHandle.dbPath, 'consumer-repo');
|
|
await fs.mkdir(path.join(providerRoot, 'src'), { recursive: true });
|
|
await fs.mkdir(path.join(consumerRoot, 'src'), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(providerRoot, 'src/health.resolver.ts'),
|
|
`import { Query, Resolver } from '@nestjs/graphql';\n@Resolver()\nclass HealthResolver {\n @Query()\n health() { return 'ok'; }\n}`,
|
|
'utf8',
|
|
);
|
|
await fs.writeFile(
|
|
path.join(consumerRoot, 'src/health.graphql'),
|
|
'query Health { health }',
|
|
'utf8',
|
|
);
|
|
await fs.writeFile(
|
|
path.join(consumerRoot, 'src/generated.ts'),
|
|
`export const HealthDocument = {
|
|
kind: 'Document',
|
|
definitions: [{
|
|
kind: 'OperationDefinition',
|
|
operation: 'query',
|
|
name: { kind: 'Name', value: 'Health' },
|
|
selectionSet: {
|
|
kind: 'SelectionSet',
|
|
selections: [{ kind: 'Field', name: { kind: 'Name', value: 'health' } }]
|
|
}
|
|
}]
|
|
};`,
|
|
'utf8',
|
|
);
|
|
const providerRepo: RepoHandle = {
|
|
id: handle.repoId,
|
|
path: 'api',
|
|
repoPath: providerRoot,
|
|
storagePath: handle.tmpHandle.dbPath,
|
|
};
|
|
const consumerRepo: RepoHandle = {
|
|
id: handle.repoId,
|
|
path: 'web',
|
|
repoPath: consumerRoot,
|
|
storagePath: handle.tmpHandle.dbPath,
|
|
};
|
|
|
|
const execute = (query: string, params: Record<string, unknown> = {}) =>
|
|
executeParameterized(handle.repoId, query, params);
|
|
const contracts = [
|
|
...(await new GraphqlExtractor().extract(execute, providerRoot, providerRepo)),
|
|
...(await new GraphqlExtractor().extract(execute, consumerRoot, consumerRepo)),
|
|
];
|
|
|
|
expect(contracts).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
contractId: 'graphql::query::health',
|
|
role: 'provider',
|
|
symbolUid: 'method:health',
|
|
}),
|
|
expect.objectContaining({
|
|
contractId: 'graphql::query::health',
|
|
role: 'consumer',
|
|
symbolUid: 'const:health-document',
|
|
}),
|
|
]),
|
|
);
|
|
|
|
const repoManager = await import('../../../src/storage/repo-manager.js');
|
|
const registrySpy = vi.spyOn(repoManager, 'readRegistryStrict').mockResolvedValue([]);
|
|
const config: GroupConfig = {
|
|
version: 1,
|
|
name: 'graphql-production-wiring',
|
|
description: '',
|
|
repos: { api: 'api', web: 'web' },
|
|
links: [],
|
|
packages: {},
|
|
detect: {
|
|
http: false,
|
|
graphql: true,
|
|
grpc: false,
|
|
thrift: false,
|
|
topics: false,
|
|
includes: false,
|
|
workspace_deps: false,
|
|
},
|
|
matching: {},
|
|
};
|
|
try {
|
|
const synced = await syncGroup(config, {
|
|
resolveRepoHandle: async (_regName, groupPath) =>
|
|
groupPath === 'api' ? providerRepo : consumerRepo,
|
|
skipWrite: true,
|
|
});
|
|
expect(
|
|
synced.contracts.map((contract) => [
|
|
contract.contractId,
|
|
contract.role,
|
|
contract.symbolUid,
|
|
]),
|
|
).toEqual([
|
|
['graphql::query::health', 'provider', 'method:health'],
|
|
['graphql::query::health', 'consumer', 'const:health-document'],
|
|
]);
|
|
expect(synced.crossLinks).toEqual([
|
|
expect.objectContaining({
|
|
contractId: 'graphql::query::health',
|
|
matchType: 'exact',
|
|
from: expect.objectContaining({ repo: 'web', symbolUid: 'const:health-document' }),
|
|
to: expect.objectContaining({ repo: 'api', symbolUid: 'method:health' }),
|
|
}),
|
|
]);
|
|
} finally {
|
|
registrySpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it('binds a decorated arrow-field provider at the wrapper startLine (#3201)', async () => {
|
|
providerRoot = path.join(handle.tmpHandle.dbPath, 'arrow-provider-repo');
|
|
await fs.mkdir(path.join(providerRoot, 'src'), { recursive: true });
|
|
await fs.writeFile(
|
|
path.join(providerRoot, 'src/health.resolver.ts'),
|
|
`import { Query, Mutation, Resolver } from '@nestjs/graphql';
|
|
@Resolver()
|
|
class HealthResolver {
|
|
@Query()
|
|
health() { return 'ok'; }
|
|
|
|
@Mutation()
|
|
save = async () => true;
|
|
}`,
|
|
'utf8',
|
|
);
|
|
const providerRepo: RepoHandle = {
|
|
id: handle.repoId,
|
|
path: 'api',
|
|
repoPath: providerRoot,
|
|
storagePath: handle.tmpHandle.dbPath,
|
|
};
|
|
const execute = (query: string, params: Record<string, unknown> = {}) =>
|
|
executeParameterized(handle.repoId, query, params);
|
|
const contracts = await new GraphqlExtractor().extract(execute, providerRoot, providerRepo);
|
|
expect(contracts).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
contractId: 'graphql::mutation::save',
|
|
role: 'provider',
|
|
symbolUid: 'method:save',
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
},
|
|
{ seed: SEED, poolAdapter: true },
|
|
);
|