import assert from 'node:assert/strict' import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { describe, it } from 'node:test' import { DEFAULT_CONFIG } from '../config.ts' import { openCodeIntelligenceDb } from '../db/connection.ts' import { getRelationshipStats, listCodeRelationshipsForPath } from '../db/repositories/relationshipsRepo.ts' import { runFullRepoIndex, runIncrementalIndex } from '../indexing/indexScheduler.ts' import type { CodeIntelligenceLogger } from '../logger.ts' import type { RepoIdentity } from '../repo/identifyRepo.ts' const silentLogger = { debug() {}, info() {}, warn() {}, error() {} } as unknown as CodeIntelligenceLogger describe('code relationship extraction', () => { it('stores call, render, hook, and construct relationships and replaces them on edit', async () => { const root = await mkdtemp(join(tmpdir(), 'pi-code-intelligence-code-rels-')) const storage = await mkdtemp(join(tmpdir(), 'pi-code-intelligence-code-rels-db-')) const db = await openCodeIntelligenceDb(storage) try { await mkdir(join(root, 'src'), { recursive: true }) await writeFile(join(root, 'src', 'app.tsx'), [ 'export function helper() { return 1 }', 'export function useThing() { return helper() }', 'export class Service {}', 'export function Child() { return }', 'export function App() {', ' const service = new Service()', ' useThing()', ' helper()', ' return ', '}', ].join('\n')) const identity: RepoIdentity = { repoKey: 'code-rels-repo', gitRoot: root, identitySource: 'path' } const config = { ...DEFAULT_CONFIG, include: ['src/**'] } await runFullRepoIndex({ identity, db, config, logger: silentLogger }, 'test') const relationships = listCodeRelationshipsForPath(db, identity.repoKey, 'src/app.tsx') assert(relationships.some((rel) => rel.kind === 'calls' && rel.sourceName === 'App' && rel.targetName === 'helper')) assert(relationships.some((rel) => rel.kind === 'uses_hook' && rel.sourceName === 'App' && rel.targetName === 'useThing')) assert(relationships.some((rel) => rel.kind === 'renders' && rel.sourceName === 'App' && rel.targetName === 'Child')) assert(relationships.some((rel) => rel.kind === 'constructs' && rel.sourceName === 'App' && rel.targetName === 'Service')) assert((getRelationshipStats(db, identity.repoKey).byKind.renders ?? 0) >= 1) await writeFile(join(root, 'src', 'app.tsx'), 'export function App() { return 1 }\n') await runIncrementalIndex({ identity, db, config, logger: silentLogger }, { changedPaths: ['src/app.tsx'], deletedPaths: [], reason: 'test' }) assert.equal(listCodeRelationshipsForPath(db, identity.repoKey, 'src/app.tsx').length, 0) } finally { db.close() await rm(root, { recursive: true, force: true }) await rm(storage, { recursive: true, force: true }) } }) it('refreshes dependent file code relationships during incremental indexing', async () => { const root = await mkdtemp(join(tmpdir(), 'pi-code-intelligence-code-rels-incremental-deps-')) const storage = await mkdtemp(join(tmpdir(), 'pi-code-intelligence-code-rels-incremental-deps-db-')) const db = await openCodeIntelligenceDb(storage) try { await mkdir(join(root, 'src'), { recursive: true }) await writeFile(join(root, 'src', 'app.ts'), "import { helper } from './helper'\nexport function app() { return helper() }\n") await writeFile(join(root, 'src', 'helper.ts'), 'export function helper() { return 1 }\n') const identity: RepoIdentity = { repoKey: 'code-rels-incremental-deps-repo', gitRoot: root, identitySource: 'path' } const config = { ...DEFAULT_CONFIG, include: ['src/**'] } await runFullRepoIndex({ identity, db, config, logger: silentLogger }, 'test') const initialCall = listCodeRelationshipsForPath(db, identity.repoKey, 'src/app.ts').find((rel) => rel.kind === 'calls' && rel.targetName === 'helper') assert(initialCall) assert.equal(initialCall.targetPath, 'src/helper.ts') assert(initialCall.targetEntityId) await writeFile(join(root, 'src', 'helper.ts'), 'export function renamedHelper() { return 1 }\n') await runIncrementalIndex({ identity, db, config, logger: silentLogger }, { changedPaths: ['src/helper.ts'], deletedPaths: [], reason: 'test' }) const refreshedCall = listCodeRelationshipsForPath(db, identity.repoKey, 'src/app.ts').find((rel) => rel.kind === 'calls' && rel.targetName === 'helper') assert(refreshedCall) assert.equal(refreshedCall.targetPath, null) assert.equal(refreshedCall.targetEntityId, null) assert.equal(refreshedCall.confidence, 0.6) await writeFile(join(root, 'src', 'helper.ts'), 'export function helper() { return 2 }\n') await runIncrementalIndex({ identity, db, config, logger: silentLogger }, { changedPaths: ['src/helper.ts'], deletedPaths: [], reason: 'test' }) const restoredCall = listCodeRelationshipsForPath(db, identity.repoKey, 'src/app.ts').find((rel) => rel.kind === 'calls' && rel.targetName === 'helper') assert(restoredCall) assert.equal(restoredCall.targetPath, 'src/helper.ts') assert(restoredCall.targetEntityId) } finally { db.close() await rm(root, { recursive: true, force: true }) await rm(storage, { recursive: true, force: true }) } }) it('resolves imported cross-file call targets after full indexing', async () => { const root = await mkdtemp(join(tmpdir(), 'pi-code-intelligence-code-rels-cross-file-')) const storage = await mkdtemp(join(tmpdir(), 'pi-code-intelligence-code-rels-cross-file-db-')) const db = await openCodeIntelligenceDb(storage) try { await mkdir(join(root, 'src'), { recursive: true }) await writeFile(join(root, 'src', 'app.ts'), "import { helper } from './helper'\nexport function app() { return helper() }\n") await writeFile(join(root, 'src', 'helper.ts'), 'export function helper() { return 1 }\n') const identity: RepoIdentity = { repoKey: 'code-rels-cross-file-repo', gitRoot: root, identitySource: 'path' } const config = { ...DEFAULT_CONFIG, include: ['src/**'] } await runFullRepoIndex({ identity, db, config, logger: silentLogger }, 'test') const relationships = listCodeRelationshipsForPath(db, identity.repoKey, 'src/app.ts') const call = relationships.find((rel) => rel.kind === 'calls' && rel.sourceName === 'app' && rel.targetName === 'helper') assert(call) assert.equal(call.targetPath, 'src/helper.ts') assert(call.targetEntityId) } finally { db.close() await rm(root, { recursive: true, force: true }) await rm(storage, { recursive: true, force: true }) } }) })