mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
test(ingestion): add regression coverage for issue #1358 singleton sub-cases
Closes the remaining sub-cases of issue #1358 surfaced by PR #1718's adversarial review (Finding 4, NOTED): the class-instance singleton (`export const fooService = new FooService();`) and the factory-pattern singleton (`export const fooService = makeFooService();`). Pre-plan investigation (per docs/plans/2026-05-21-002 § "Pre-Plan Investigation Task (T1)") confirmed Outcome A for both patterns — they already resolve end-to-end through scope-resolution's `@type-binding.constructor` capture (languages/typescript/query.ts:489-511) + `propagateImportedReturnTypes` chain-follow (scope-resolution/passes/imported-return-types.ts:114) + receiver-bound Case 4 simple typeBinding lookup (receiver-bound-calls.ts:625). The mechanism was wired correctly before this session; the regression-net wasn't. This test pins the behavior: - Pattern 1: `caller → FooService.getUser` CALLS edge with confidence 0.85 and reason 'import-resolved' - Pattern 2: same edge shape via factory chain-follow (the `@type-binding.alias` capture for `const u = find()` style) Both assertions use exact `.toEqual([{...}])` shape pinning so a future regression that targets a phantom Method node, emits at lower confidence, or drops the cross-file import-resolved reason fails loudly. Verification: 5/5 pass, 127/127 in targeted regression sweep including object-literal-owner-resolution.test.ts, ast-helpers-object-literal- binding.test.ts, has-method.test.ts, and cross-file-binding.test.ts. No production code change. The class methods get a class-qualified node id (`Method:src/service.ts:FooService.getUser#1`) distinguishing them from same-name methods on other classes — distinct from the bare-name node id shape PR #1718's object-literal case uses.
This commit is contained in:
parent
9cf5f37897
commit
0df91b772e
1 changed files with 181 additions and 0 deletions
|
|
@ -0,0 +1,181 @@
|
|||
/**
|
||||
* Regression coverage for issue #1358's class-instance and factory-pattern
|
||||
* singleton sub-cases. PR #1718 closed the object-literal-shorthand case
|
||||
* (`export const fooService = { getUser() {} }`). This file covers the
|
||||
* other two singleton shapes:
|
||||
*
|
||||
* // Pattern 1 — class-instance singleton
|
||||
* export class FooService { getUser(id) {...} }
|
||||
* export const fooService = new FooService();
|
||||
*
|
||||
* // Pattern 2 — factory-pattern singleton
|
||||
* export class FooService { getUser(id) {...} }
|
||||
* export function makeFooService() { return new FooService(); }
|
||||
* export const fooService = makeFooService();
|
||||
*
|
||||
* Both already resolve end-to-end via scope-resolution's
|
||||
* `@type-binding.constructor` capture (TS query) +
|
||||
* `propagateImportedReturnTypes` chain-follow (cross-file mirror) +
|
||||
* receiver-bound Case 4 (simple typeBinding lookup). This test pins that
|
||||
* behavior so a future refactor of any of those three mechanisms cannot
|
||||
* silently regress either pattern.
|
||||
*
|
||||
* Origin: PR #1718 review Finding 4 (NOTED, deferred). T1 pre-plan
|
||||
* investigation per docs/plans/2026-05-21-002-feat-pr1718-followups-class-
|
||||
* instance-and-label-normalization-plan.md confirmed Outcome A — both
|
||||
* patterns already work; this is the regression-net.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
getRelationships,
|
||||
getNodesByLabel,
|
||||
runPipelineFromRepo,
|
||||
type PipelineResult,
|
||||
} from './resolvers/helpers.js';
|
||||
import { generateId } from '../../src/lib/utils.js';
|
||||
|
||||
function writeFixture(files: Record<string, string>): string {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gnx-singleton-'));
|
||||
for (const [rel, content] of Object.entries(files)) {
|
||||
const full = path.join(root, rel);
|
||||
fs.mkdirSync(path.dirname(full), { recursive: true });
|
||||
fs.writeFileSync(full, content);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
function removeFixture(root: string): void {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const CONSUMER_TS = `import { fooService } from './service';
|
||||
|
||||
export function caller(id: string) {
|
||||
return fooService.getUser(id);
|
||||
}
|
||||
`;
|
||||
|
||||
// Class methods carry a class-qualified node id (e.g. `FooService.getUser`)
|
||||
// to distinguish them from same-name methods on other classes. Object-literal
|
||||
// methods (per PR #1718) use the bare name because they have no class owner.
|
||||
const EXPECTED_METHOD_NODE_ID = generateId('Method', 'src/service.ts:FooService.getUser#1');
|
||||
|
||||
// ── Pattern 1: class-instance singleton ─────────────────────────────────────
|
||||
|
||||
describe('class-instance singleton resolution (issue #1358 sub-case 2)', () => {
|
||||
let repoRoot: string;
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
repoRoot = writeFixture({
|
||||
'src/service.ts': `export class FooService {
|
||||
getUser(id: string) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
export const fooService = new FooService();
|
||||
`,
|
||||
'src/consumer.ts': CONSUMER_TS,
|
||||
});
|
||||
result = await runPipelineFromRepo(repoRoot, () => undefined, {
|
||||
skipGraphPhases: true,
|
||||
skipWorkers: true,
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
afterAll(() => removeFixture(repoRoot));
|
||||
|
||||
it('emits Class:FooService, Method:getUser, Function:caller, Const:fooService exactly once', () => {
|
||||
expect(getNodesByLabel(result, 'Class').filter((n) => n === 'FooService').length).toBe(1);
|
||||
expect(getNodesByLabel(result, 'Method').filter((n) => n === 'getUser').length).toBe(1);
|
||||
expect(getNodesByLabel(result, 'Function').filter((n) => n === 'caller').length).toBe(1);
|
||||
expect(getNodesByLabel(result, 'Const').filter((n) => n === 'fooService').length).toBe(1);
|
||||
});
|
||||
|
||||
it('emits HAS_METHOD edge from FooService class to getUser', () => {
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD');
|
||||
const fromClass = hasMethod.filter((e) => e.source === 'FooService').map((e) => e.target);
|
||||
expect(fromClass).toEqual(['getUser']);
|
||||
});
|
||||
|
||||
it('emits CALLS edge caller → FooService.getUser with confidence 0.85 and reason import-resolved', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const projected = calls
|
||||
.filter((e) => e.source === 'caller' && e.target === 'getUser')
|
||||
.map((e) => ({
|
||||
targetId: e.rel.targetId,
|
||||
confidence: e.rel.confidence,
|
||||
reason: e.rel.reason,
|
||||
}));
|
||||
|
||||
expect(projected).toEqual([
|
||||
{
|
||||
targetId: EXPECTED_METHOD_NODE_ID,
|
||||
confidence: 0.85,
|
||||
reason: 'import-resolved',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Pattern 2: factory-pattern singleton ────────────────────────────────────
|
||||
|
||||
describe('factory-pattern singleton resolution (issue #1358 sub-case 3)', () => {
|
||||
let repoRoot: string;
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
repoRoot = writeFixture({
|
||||
'src/service.ts': `export class FooService {
|
||||
getUser(id: string) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
export function makeFooService(): FooService {
|
||||
return new FooService();
|
||||
}
|
||||
|
||||
export const fooService = makeFooService();
|
||||
`,
|
||||
'src/consumer.ts': CONSUMER_TS,
|
||||
});
|
||||
result = await runPipelineFromRepo(repoRoot, () => undefined, {
|
||||
skipGraphPhases: true,
|
||||
skipWorkers: true,
|
||||
});
|
||||
}, 60000);
|
||||
|
||||
afterAll(() => removeFixture(repoRoot));
|
||||
|
||||
it('emits Function:makeFooService alongside the class and consumer nodes', () => {
|
||||
expect(getNodesByLabel(result, 'Function').filter((n) => n === 'makeFooService').length).toBe(
|
||||
1,
|
||||
);
|
||||
expect(getNodesByLabel(result, 'Function').filter((n) => n === 'caller').length).toBe(1);
|
||||
expect(getNodesByLabel(result, 'Const').filter((n) => n === 'fooService').length).toBe(1);
|
||||
});
|
||||
|
||||
it('resolves caller.fooService.getUser to FooService.getUser via factory chain-follow', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
const projected = calls
|
||||
.filter((e) => e.source === 'caller' && e.target === 'getUser')
|
||||
.map((e) => ({
|
||||
targetId: e.rel.targetId,
|
||||
confidence: e.rel.confidence,
|
||||
reason: e.rel.reason,
|
||||
}));
|
||||
|
||||
expect(projected).toEqual([
|
||||
{
|
||||
targetId: EXPECTED_METHOD_NODE_ID,
|
||||
confidence: 0.85,
|
||||
reason: 'import-resolved',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue