mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-08 22:22:52 +00:00
296 lines
12 KiB
TypeScript
296 lines
12 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
createRegistrationTable,
|
|
CALLABLE_ONLY_LABELS,
|
|
INERT_LABELS,
|
|
DISPATCH_LABELS,
|
|
} from '../../../src/core/ingestion/model/registration-table.js';
|
|
import { createTypeRegistry } from '../../../src/core/ingestion/model/type-registry.js';
|
|
import { createMethodRegistry } from '../../../src/core/ingestion/model/method-registry.js';
|
|
import { createFieldRegistry } from '../../../src/core/ingestion/model/field-registry.js';
|
|
import { ALL_NODE_LABELS } from '../../../src/core/ingestion/model/index.js';
|
|
import {
|
|
CLASS_TYPES_TUPLE,
|
|
FREE_CALLABLE_TUPLE,
|
|
} from '../../../src/core/ingestion/model/symbol-table.js';
|
|
import { EMBEDDABLE_LABELS } from '../../../src/core/embeddings/types.js';
|
|
import type { SymbolDefinition } from 'gitnexus-shared';
|
|
import { makeDef as makeBaseDef } from './helpers.js';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const makeDeps = () => ({
|
|
types: createTypeRegistry(),
|
|
methods: createMethodRegistry(),
|
|
fields: createFieldRegistry(),
|
|
});
|
|
|
|
const makeDef = (overrides: Partial<SymbolDefinition> = {}): SymbolDefinition =>
|
|
makeBaseDef({ nodeId: 'node:test', type: 'Class', ...overrides });
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Basic factory + table shape
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('createRegistrationTable', () => {
|
|
it('returns a Map with one entry per DISPATCH_LABELS value', () => {
|
|
const table = createRegistrationTable(makeDeps());
|
|
expect(table.size).toBe(DISPATCH_LABELS.size);
|
|
for (const label of DISPATCH_LABELS) {
|
|
expect(table.has(label)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('every DISPATCH_LABELS entry maps to a hook function', () => {
|
|
const table = createRegistrationTable(makeDeps());
|
|
for (const [, hook] of table) {
|
|
expect(typeof hook).toBe('function');
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Kind taxonomy exhaustiveness
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('NodeLabel taxonomy coverage', () => {
|
|
// ALL_NODE_LABELS is imported from model/index.ts (re-exported from
|
|
// semantic-model.ts) so that the production list and the test list
|
|
// cannot drift. If the shared NodeLabel union gains a new member, add
|
|
// it to the single list in semantic-model.ts AND to one of the
|
|
// registration-table allowlists in the same commit.
|
|
|
|
it('every NodeLabel appears in exactly one of DISPATCH / CALLABLE_ONLY / INERT', () => {
|
|
for (const label of ALL_NODE_LABELS) {
|
|
const inDispatch = DISPATCH_LABELS.has(label);
|
|
const inCallableOnly = CALLABLE_ONLY_LABELS.has(label);
|
|
const inInert = INERT_LABELS.has(label);
|
|
const count = Number(inDispatch) + Number(inCallableOnly) + Number(inInert);
|
|
expect(count, `label ${label} must be in exactly one category`).toBe(1);
|
|
}
|
|
});
|
|
|
|
it('CALLABLE_ONLY_LABELS includes Function, Macro, Delegate', () => {
|
|
expect(CALLABLE_ONLY_LABELS.has('Function')).toBe(true);
|
|
expect(CALLABLE_ONLY_LABELS.has('Macro')).toBe(true);
|
|
expect(CALLABLE_ONLY_LABELS.has('Delegate')).toBe(true);
|
|
});
|
|
|
|
it('DISPATCH_LABELS includes all 10 routed kinds', () => {
|
|
const expected = [
|
|
'Class',
|
|
'Struct',
|
|
'Interface',
|
|
'Enum',
|
|
'Record',
|
|
'Trait',
|
|
'Method',
|
|
'Constructor',
|
|
'Property',
|
|
'Impl',
|
|
] as const;
|
|
for (const label of expected) {
|
|
expect(DISPATCH_LABELS.has(label)).toBe(true);
|
|
}
|
|
expect(DISPATCH_LABELS.size).toBe(expected.length);
|
|
});
|
|
|
|
it('INERT_LABELS includes metadata-only node kinds', () => {
|
|
expect(INERT_LABELS.has('File')).toBe(true);
|
|
expect(INERT_LABELS.has('Folder')).toBe(true);
|
|
expect(INERT_LABELS.has('Namespace')).toBe(true);
|
|
expect(INERT_LABELS.has('Variable')).toBe(true);
|
|
expect(INERT_LABELS.has('Import')).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// BasicBlock — taint/PDG substrate node (issue #2080). It is a control-flow
|
|
// node, never a symbol-resolution or embedding target (KTD4). These guards
|
|
// fail if a future change accidentally promotes it into a dispatch/callable
|
|
// tuple or the embeddable set.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('BasicBlock taint/PDG substrate label (issue #2080)', () => {
|
|
it('is classified inert — not a dispatch or callable resolution target', () => {
|
|
expect(INERT_LABELS.has('BasicBlock')).toBe(true);
|
|
expect(DISPATCH_LABELS.has('BasicBlock')).toBe(false);
|
|
expect(CALLABLE_ONLY_LABELS.has('BasicBlock')).toBe(false);
|
|
});
|
|
|
|
it('is excluded from the class-like and free-callable tuples', () => {
|
|
expect((CLASS_TYPES_TUPLE as readonly string[]).includes('BasicBlock')).toBe(false);
|
|
expect((FREE_CALLABLE_TUPLE as readonly string[]).includes('BasicBlock')).toBe(false);
|
|
});
|
|
|
|
it('is not embeddable', () => {
|
|
expect((EMBEDDABLE_LABELS as readonly string[]).includes('BasicBlock')).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Behavior group coverage — every label in a behavior group routes to the
|
|
// group's registry write, regardless of how hooks are implemented (shared
|
|
// closure, per-label closure, etc.). These tests survive an internal
|
|
// refactor to per-label closures for tracing/metrics — unlike
|
|
// reference-equality assertions on the hook functions themselves.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('class-like behavior group — all 6 labels route to types.registerClass', () => {
|
|
const CLASS_LIKE_LABELS = ['Class', 'Struct', 'Interface', 'Enum', 'Record', 'Trait'] as const;
|
|
|
|
for (const label of CLASS_LIKE_LABELS) {
|
|
it(`${label} writes to types.registerClass`, () => {
|
|
const deps = makeDeps();
|
|
const table = createRegistrationTable(deps);
|
|
const def = makeDef({
|
|
nodeId: `${label.toLowerCase()}:User`,
|
|
type: label,
|
|
qualifiedName: `app.User`,
|
|
});
|
|
table.get(label)!('User', def);
|
|
expect(deps.types.lookupClassByName('User')).toHaveLength(1);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('method-like behavior group — Method and Constructor route to methods.register', () => {
|
|
for (const label of ['Method', 'Constructor'] as const) {
|
|
it(`${label} writes to methods.register when ownerId is set`, () => {
|
|
const deps = makeDeps();
|
|
const table = createRegistrationTable(deps);
|
|
const def = makeDef({
|
|
nodeId: `${label.toLowerCase()}:save`,
|
|
type: label,
|
|
ownerId: 'class:User',
|
|
});
|
|
table.get(label)!('save', def);
|
|
expect(deps.methods.lookupMethodByOwner('class:User', 'save')?.nodeId).toBe(
|
|
`${label.toLowerCase()}:save`,
|
|
);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('behavior group isolation', () => {
|
|
it('class-like hooks never touch methods or fields', () => {
|
|
const deps = makeDeps();
|
|
const table = createRegistrationTable(deps);
|
|
const def = makeDef({
|
|
nodeId: 'class:User',
|
|
type: 'Class',
|
|
ownerId: 'unrelated',
|
|
});
|
|
table.get('Class')!('User', def);
|
|
// No method or field registered — class hook is isolated to types.
|
|
expect(deps.methods.lookupMethodByOwner('unrelated', 'User')).toBeUndefined();
|
|
expect(deps.fields.lookupFieldByOwner('unrelated', 'User')).toBeUndefined();
|
|
});
|
|
|
|
it('Impl hooks write to types.registerImpl, never to types.registerClass', () => {
|
|
const deps = makeDeps();
|
|
const table = createRegistrationTable(deps);
|
|
const def = makeDef({ nodeId: 'impl:User', type: 'Impl' });
|
|
table.get('Impl')!('User', def);
|
|
expect(deps.types.lookupImplByName('User')).toHaveLength(1);
|
|
expect(deps.types.lookupClassByName('User')).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// End-to-end hook behavior with real registries
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('hook behavior (real registries, no mocks)', () => {
|
|
it('classLikeHook writes to types.registerClass', () => {
|
|
const deps = makeDeps();
|
|
const table = createRegistrationTable(deps);
|
|
const def = makeDef({ nodeId: 'class:User', type: 'Class', qualifiedName: 'app.User' });
|
|
table.get('Class')!('User', def);
|
|
expect(deps.types.lookupClassByName('User')).toHaveLength(1);
|
|
expect(deps.types.lookupClassByQualifiedName('app.User')).toHaveLength(1);
|
|
});
|
|
|
|
it('classLikeHook falls back to the simple name when qualifiedName is absent', () => {
|
|
const deps = makeDeps();
|
|
const table = createRegistrationTable(deps);
|
|
const def = makeDef({ nodeId: 'class:User', type: 'Class' });
|
|
table.get('Class')!('User', def);
|
|
expect(deps.types.lookupClassByQualifiedName('User')).toHaveLength(1);
|
|
});
|
|
|
|
it('methodHook writes to methods.register when ownerId is set', () => {
|
|
const deps = makeDeps();
|
|
const table = createRegistrationTable(deps);
|
|
const def = makeDef({
|
|
nodeId: 'mtd:save',
|
|
type: 'Method',
|
|
ownerId: 'class:User',
|
|
});
|
|
table.get('Method')!('save', def);
|
|
expect(deps.methods.lookupMethodByOwner('class:User', 'save')?.nodeId).toBe('mtd:save');
|
|
});
|
|
|
|
it('methodHook silently skips registration when ownerId is missing', () => {
|
|
const deps = makeDeps();
|
|
const table = createRegistrationTable(deps);
|
|
const def = makeDef({ nodeId: 'mtd:free', type: 'Method' });
|
|
table.get('Method')!('free', def);
|
|
expect(deps.methods.lookupMethodByOwner('', 'free')).toBeUndefined();
|
|
});
|
|
|
|
it('propertyHook writes to fields.register when ownerId is set', () => {
|
|
const deps = makeDeps();
|
|
const table = createRegistrationTable(deps);
|
|
const def = makeDef({
|
|
nodeId: 'prop:name',
|
|
type: 'Property',
|
|
ownerId: 'class:User',
|
|
declaredType: 'string',
|
|
});
|
|
table.get('Property')!('name', def);
|
|
expect(deps.fields.lookupFieldByOwner('class:User', 'name')?.nodeId).toBe('prop:name');
|
|
});
|
|
|
|
it('propertyHook silently skips registration when ownerId is missing', () => {
|
|
const deps = makeDeps();
|
|
const table = createRegistrationTable(deps);
|
|
const def = makeDef({ nodeId: 'prop:orphan', type: 'Property' });
|
|
table.get('Property')!('orphan', def);
|
|
expect(deps.fields.lookupFieldByOwner('', 'orphan')).toBeUndefined();
|
|
});
|
|
|
|
it('implHook writes to types.registerImpl, NOT types.registerClass', () => {
|
|
const deps = makeDeps();
|
|
const table = createRegistrationTable(deps);
|
|
const def = makeDef({ nodeId: 'impl:User', type: 'Impl' });
|
|
table.get('Impl')!('User', def);
|
|
expect(deps.types.lookupImplByName('User')).toHaveLength(1);
|
|
// Critical: Impl must not pollute classByName — heritage resolution
|
|
// would otherwise treat an Impl as a parent type candidate.
|
|
expect(deps.types.lookupClassByName('User')).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Factory-per-instance isolation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('factory-per-instance isolation', () => {
|
|
it('two independent tables write to their own registries only', () => {
|
|
const depsA = makeDeps();
|
|
const depsB = makeDeps();
|
|
const tableA = createRegistrationTable(depsA);
|
|
const tableB = createRegistrationTable(depsB);
|
|
|
|
tableA.get('Class')!('UserA', makeDef({ nodeId: 'class:UserA', type: 'Class' }));
|
|
tableB.get('Class')!('UserB', makeDef({ nodeId: 'class:UserB', type: 'Class' }));
|
|
|
|
expect(depsA.types.lookupClassByName('UserA')).toHaveLength(1);
|
|
expect(depsA.types.lookupClassByName('UserB')).toHaveLength(0);
|
|
expect(depsB.types.lookupClassByName('UserB')).toHaveLength(1);
|
|
expect(depsB.types.lookupClassByName('UserA')).toHaveLength(0);
|
|
});
|
|
});
|