knowledge-graph-preview / cli /input-resolver.test.js
mr4's picture
Upload 136 files
fd8cdf5 verified
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
import { resolveInput } from './input-resolver.js';
/**
* Helper to create a valid dashboard JSON object
*/
function makeDashboard(projectName = 'test-project') {
return {
version: '1.0.0',
project: { name: projectName, description: 'Test', languages: [], frameworks: [] },
nodes: [{ id: '1', type: 'file', name: 'index.ts' }],
edges: [{ source: '1', target: '1', type: 'imports' }],
};
}
/**
* Helper to create a meta.json object
*/
function makeMeta() {
return {
lastAnalyzedAt: '2024-01-01T00:00:00.000Z',
gitCommitHash: 'abc123',
version: '1.0.0',
analyzedFiles: 10,
};
}
describe('resolveInput - recursive directory scanning', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'input-resolver-test-'));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe('handleFlatDirectory - recursive child directory scanning (Task 40.1)', () => {
it('loads JSON files from nested child directories', () => {
// Create structure:
// tmpDir/
// top-level.json
// child-a/
// graph-a.json
// child-b/
// graph-b.json
fs.writeFileSync(path.join(tmpDir, 'top-level.json'), JSON.stringify(makeDashboard('top')));
fs.mkdirSync(path.join(tmpDir, 'child-a'));
fs.writeFileSync(path.join(tmpDir, 'child-a', 'graph-a.json'), JSON.stringify(makeDashboard('project-a')));
fs.mkdirSync(path.join(tmpDir, 'child-b'));
fs.writeFileSync(path.join(tmpDir, 'child-b', 'graph-b.json'), JSON.stringify(makeDashboard('project-b')));
const result = resolveInput(tmpDir);
expect(result.success).toBe(true);
if (!result.success)
return;
// Should have entries for root, child-a, and child-b
expect(result.manifest.length).toBe(3);
const dirNames = result.manifest.map(e => e.dirName);
const baseName = path.basename(tmpDir);
expect(dirNames).toContain(baseName);
expect(dirNames).toContain(`${baseName}/child-a`);
expect(dirNames).toContain(`${baseName}/child-b`);
});
it('discovers deeply nested directories (3+ levels)', () => {
// Create structure:
// tmpDir/
// level1/
// level2/
// level3/
// deep.json
const deepPath = path.join(tmpDir, 'level1', 'level2', 'level3');
fs.mkdirSync(deepPath, { recursive: true });
fs.writeFileSync(path.join(deepPath, 'deep.json'), JSON.stringify(makeDashboard('deep-project')));
const result = resolveInput(tmpDir);
expect(result.success).toBe(true);
if (!result.success)
return;
const baseName = path.basename(tmpDir);
const dirNames = result.manifest.map(e => e.dirName);
expect(dirNames).toContain(`${baseName}/level1/level2/level3`);
// Verify file mapping has correct URL path
const expectedKey = `${baseName}/level1/level2/level3/deep.json`;
expect(result.fileMapping[expectedKey]).toBe(path.join(deepPath, 'deep.json'));
});
it('skips common non-source directories', () => {
// Create structure with skippable dirs
const skipDirs = ['node_modules', '.git', 'dist', 'build', '.next', '.cache', '__pycache__', '.turbo', 'target', 'obj', '.understand-anything'];
for (const dir of skipDirs) {
const dirPath = path.join(tmpDir, dir);
fs.mkdirSync(dirPath);
fs.writeFileSync(path.join(dirPath, 'graph.json'), JSON.stringify(makeDashboard(`skip-${dir}`)));
}
// Add one valid directory
fs.mkdirSync(path.join(tmpDir, 'valid-dir'));
fs.writeFileSync(path.join(tmpDir, 'valid-dir', 'graph.json'), JSON.stringify(makeDashboard('valid')));
const result = resolveInput(tmpDir);
expect(result.success).toBe(true);
if (!result.success)
return;
// Should only find the valid-dir, not any of the skipped dirs
const baseName = path.basename(tmpDir);
const dirNames = result.manifest.map(e => e.dirName);
for (const dir of skipDirs) {
expect(dirNames).not.toContain(`${baseName}/${dir}`);
}
expect(dirNames).toContain(`${baseName}/valid-dir`);
});
it('preserves correct URL paths in file mapping for nested files', () => {
// Create structure:
// tmpDir/
// root.json
// sub/
// nested.json
fs.writeFileSync(path.join(tmpDir, 'root.json'), JSON.stringify(makeDashboard('root-proj')));
fs.mkdirSync(path.join(tmpDir, 'sub'));
fs.writeFileSync(path.join(tmpDir, 'sub', 'nested.json'), JSON.stringify(makeDashboard('nested-proj')));
const result = resolveInput(tmpDir);
expect(result.success).toBe(true);
if (!result.success)
return;
const baseName = path.basename(tmpDir);
// Top-level file mapping
expect(result.fileMapping[`${baseName}/root.json`]).toBe(path.join(tmpDir, 'root.json'));
// Nested file mapping
expect(result.fileMapping[`${baseName}/sub/nested.json`]).toBe(path.join(tmpDir, 'sub', 'nested.json'));
});
it('creates separate manifest entries per child directory', () => {
// Create structure with multiple children
fs.mkdirSync(path.join(tmpDir, 'alpha'));
fs.writeFileSync(path.join(tmpDir, 'alpha', 'a1.json'), JSON.stringify(makeDashboard('alpha-1')));
fs.writeFileSync(path.join(tmpDir, 'alpha', 'a2.json'), JSON.stringify(makeDashboard('alpha-2')));
fs.mkdirSync(path.join(tmpDir, 'beta'));
fs.writeFileSync(path.join(tmpDir, 'beta', 'b1.json'), JSON.stringify(makeDashboard('beta-1')));
const result = resolveInput(tmpDir);
expect(result.success).toBe(true);
if (!result.success)
return;
const baseName = path.basename(tmpDir);
const alphaEntry = result.manifest.find(e => e.dirName === `${baseName}/alpha`);
const betaEntry = result.manifest.find(e => e.dirName === `${baseName}/beta`);
expect(alphaEntry).toBeDefined();
expect(alphaEntry.graphFiles).toHaveLength(2);
expect(betaEntry).toBeDefined();
expect(betaEntry.graphFiles).toHaveLength(1);
});
});
describe('resolveInput - recursive meta.json detection (Task 40.2)', () => {
it('finds meta.json in deeply nested subdirectories', () => {
// Create structure:
// tmpDir/
// deep/
// nested/
// meta.json
// graph.json
const nestedPath = path.join(tmpDir, 'deep', 'nested');
fs.mkdirSync(nestedPath, { recursive: true });
fs.writeFileSync(path.join(nestedPath, 'meta.json'), JSON.stringify(makeMeta()));
fs.writeFileSync(path.join(nestedPath, 'graph.json'), JSON.stringify(makeDashboard('nested-project')));
const result = resolveInput(tmpDir);
expect(result.success).toBe(true);
if (!result.success)
return;
// Should treat deep/nested as a project directory
expect(result.manifest.length).toBe(1);
// The dirName should be the relative path
const dirName = result.manifest[0].dirName.replace(/\\/g, '/');
expect(dirName).toContain('nested');
});
it('finds multiple meta.json directories at different nesting levels', () => {
// Create structure:
// tmpDir/
// project-a/
// meta.json
// graph-a.json
// deep/
// project-b/
// meta.json
// graph-b.json
fs.mkdirSync(path.join(tmpDir, 'project-a'));
fs.writeFileSync(path.join(tmpDir, 'project-a', 'meta.json'), JSON.stringify(makeMeta()));
fs.writeFileSync(path.join(tmpDir, 'project-a', 'graph-a.json'), JSON.stringify(makeDashboard('proj-a')));
const deepPath = path.join(tmpDir, 'deep', 'project-b');
fs.mkdirSync(deepPath, { recursive: true });
fs.writeFileSync(path.join(deepPath, 'meta.json'), JSON.stringify(makeMeta()));
fs.writeFileSync(path.join(deepPath, 'graph-b.json'), JSON.stringify(makeDashboard('proj-b')));
const result = resolveInput(tmpDir);
expect(result.success).toBe(true);
if (!result.success)
return;
expect(result.manifest.length).toBe(2);
});
it('directories with meta.json are treated as project directories', () => {
// Create a directory with meta.json - should use meta.json data, not synthesize
fs.mkdirSync(path.join(tmpDir, 'my-project'));
const meta = makeMeta();
fs.writeFileSync(path.join(tmpDir, 'my-project', 'meta.json'), JSON.stringify(meta));
fs.writeFileSync(path.join(tmpDir, 'my-project', 'graph.json'), JSON.stringify(makeDashboard('my-proj')));
const result = resolveInput(tmpDir);
expect(result.success).toBe(true);
if (!result.success)
return;
expect(result.manifest[0].meta.gitCommitHash).toBe('abc123');
expect(result.manifest[0].meta.version).toBe('1.0.0');
expect(result.manifest[0].meta.analyzedFiles).toBe(10);
});
it('falls back to recursive flat scanning when no meta.json found anywhere', () => {
// Create structure with no meta.json at all
fs.mkdirSync(path.join(tmpDir, 'flat-a'));
fs.writeFileSync(path.join(tmpDir, 'flat-a', 'graph.json'), JSON.stringify(makeDashboard('flat-proj')));
fs.mkdirSync(path.join(tmpDir, 'flat-b'));
fs.writeFileSync(path.join(tmpDir, 'flat-b', 'data.json'), JSON.stringify(makeDashboard('flat-proj-2')));
const result = resolveInput(tmpDir);
expect(result.success).toBe(true);
if (!result.success)
return;
// Should use synthesized meta (version 0.0.0)
for (const entry of result.manifest) {
expect(entry.meta.version).toBe('0.0.0');
expect(entry.meta.gitCommitHash).toBe('');
}
});
it('skips non-source directories when searching for meta.json', () => {
// Put meta.json inside node_modules - should be skipped
fs.mkdirSync(path.join(tmpDir, 'node_modules', 'some-pkg'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, 'node_modules', 'some-pkg', 'meta.json'), JSON.stringify(makeMeta()));
fs.writeFileSync(path.join(tmpDir, 'node_modules', 'some-pkg', 'graph.json'), JSON.stringify(makeDashboard('pkg')));
// Add a valid flat directory
fs.mkdirSync(path.join(tmpDir, 'valid'));
fs.writeFileSync(path.join(tmpDir, 'valid', 'graph.json'), JSON.stringify(makeDashboard('valid-proj')));
const result = resolveInput(tmpDir);
expect(result.success).toBe(true);
if (!result.success)
return;
// Should NOT find the node_modules project, should fall through to flat scanning
const dirNames = result.manifest.map(e => e.dirName);
for (const name of dirNames) {
expect(name).not.toContain('node_modules');
}
});
});
describe('preserves existing behavior', () => {
it('directory with meta.json at root is still treated as project directory', () => {
// tmpDir itself has meta.json
fs.writeFileSync(path.join(tmpDir, 'meta.json'), JSON.stringify(makeMeta()));
fs.writeFileSync(path.join(tmpDir, 'graph.json'), JSON.stringify(makeDashboard('root-proj')));
const result = resolveInput(tmpDir);
expect(result.success).toBe(true);
if (!result.success)
return;
expect(result.manifest.length).toBe(1);
expect(result.manifest[0].meta.gitCommitHash).toBe('abc123');
});
it('single file input still works', () => {
const filePath = path.join(tmpDir, 'single.json');
fs.writeFileSync(filePath, JSON.stringify(makeDashboard('single-proj')));
const result = resolveInput(filePath);
expect(result.success).toBe(true);
if (!result.success)
return;
expect(result.manifest.length).toBe(1);
expect(result.manifest[0].graphFiles[0].projectName).toBe('single-proj');
});
});
});