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 => 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 12 routed kinds', () => { const expected = [ 'Class', 'Protocol', 'Category', '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 8 labels route to types.registerClass', () => { const CLASS_LIKE_LABELS = [ 'Class', 'Protocol', 'Category', '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); }); });