import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as path from 'path'; import { ContextAnalyzer } from '../ts/context/context-analyzer.js'; import type { IFileMetadata } from '../ts/context/types.js'; const testProjectRoot = process.cwd(); tap.test('ContextAnalyzer should create instance with default weights', async () => { const analyzer = new ContextAnalyzer(testProjectRoot); expect(analyzer).toBeInstanceOf(ContextAnalyzer); }); tap.test('ContextAnalyzer should create instance with custom weights', async () => { const analyzer = new ContextAnalyzer( testProjectRoot, { dependencyWeight: 0.5, relevanceWeight: 0.3, efficiencyWeight: 0.1, recencyWeight: 0.1 } ); expect(analyzer).toBeInstanceOf(ContextAnalyzer); }); tap.test('ContextAnalyzer.analyze should return analysis result with files', async () => { const analyzer = new ContextAnalyzer(testProjectRoot); const metadata: IFileMetadata[] = [ { path: path.join(testProjectRoot, 'ts/context/types.ts'), relativePath: 'ts/context/types.ts', size: 5000, mtime: Date.now(), estimatedTokens: 1250 }, { path: path.join(testProjectRoot, 'ts/context/enhanced-context.ts'), relativePath: 'ts/context/enhanced-context.ts', size: 10000, mtime: Date.now(), estimatedTokens: 2500 } ]; const result = await analyzer.analyze(metadata, 'readme'); expect(result.taskType).toEqual('readme'); expect(result.files.length).toEqual(2); expect(result.totalFiles).toEqual(2); expect(result.analysisDuration).toBeGreaterThan(0); expect(result.dependencyGraph).toBeDefined(); }); tap.test('ContextAnalyzer.analyze should assign importance scores to files', async () => { const analyzer = new ContextAnalyzer(testProjectRoot); const metadata: IFileMetadata[] = [ { path: path.join(testProjectRoot, 'ts/context/types.ts'), relativePath: 'ts/context/types.ts', size: 3000, mtime: Date.now(), estimatedTokens: 750 } ]; const result = await analyzer.analyze(metadata, 'readme'); expect(result.files[0].importanceScore).toBeGreaterThanOrEqual(0); expect(result.files[0].importanceScore).toBeLessThanOrEqual(1); }); tap.test('ContextAnalyzer.analyze should sort files by importance score', async () => { const analyzer = new ContextAnalyzer(testProjectRoot); const metadata: IFileMetadata[] = [ { path: path.join(testProjectRoot, 'ts/context/types.ts'), relativePath: 'ts/context/types.ts', size: 3000, mtime: Date.now(), estimatedTokens: 750 }, { path: path.join(testProjectRoot, 'test/test.basic.node.ts'), relativePath: 'test/test.basic.node.ts', size: 2000, mtime: Date.now(), estimatedTokens: 500 } ]; const result = await analyzer.analyze(metadata, 'readme'); // Files should be sorted by importance (highest first) for (let i = 0; i < result.files.length - 1; i++) { expect(result.files[i].importanceScore).toBeGreaterThanOrEqual( result.files[i + 1].importanceScore ); } }); tap.test('ContextAnalyzer.analyze should assign tiers based on scores', async () => { const analyzer = new ContextAnalyzer(testProjectRoot); const metadata: IFileMetadata[] = [ { path: path.join(testProjectRoot, 'ts/index.ts'), relativePath: 'ts/index.ts', size: 3000, mtime: Date.now(), estimatedTokens: 750 } ]; const result = await analyzer.analyze(metadata, 'readme'); const file = result.files[0]; expect(['essential', 'important', 'optional', 'excluded']).toContain(file.tier); }); tap.test('ContextAnalyzer should prioritize index.ts files for README task', async () => { const analyzer = new ContextAnalyzer(testProjectRoot); const metadata: IFileMetadata[] = [ { path: path.join(testProjectRoot, 'ts/index.ts'), relativePath: 'ts/index.ts', size: 3000, mtime: Date.now(), estimatedTokens: 750 }, { path: path.join(testProjectRoot, 'ts/some-helper.ts'), relativePath: 'ts/some-helper.ts', size: 3000, mtime: Date.now(), estimatedTokens: 750 } ]; const result = await analyzer.analyze(metadata, 'readme'); // index.ts should have higher relevance score const indexFile = result.files.find(f => f.path.includes('index.ts')); const helperFile = result.files.find(f => f.path.includes('some-helper.ts')); if (indexFile && helperFile) { expect(indexFile.relevanceScore).toBeGreaterThan(helperFile.relevanceScore); } }); tap.test('ContextAnalyzer should deprioritize test files for README task', async () => { const analyzer = new ContextAnalyzer(testProjectRoot); const metadata: IFileMetadata[] = [ { path: path.join(testProjectRoot, 'ts/context/types.ts'), relativePath: 'ts/context/types.ts', size: 3000, mtime: Date.now(), estimatedTokens: 750 }, { path: path.join(testProjectRoot, 'test/test.basic.node.ts'), relativePath: 'test/test.basic.node.ts', size: 3000, mtime: Date.now(), estimatedTokens: 750 } ]; const result = await analyzer.analyze(metadata, 'readme'); // Source file should have higher relevance than test file const sourceFile = result.files.find(f => f.path.includes('ts/context/types.ts')); const testFile = result.files.find(f => f.path.includes('test/test.basic.node.ts')); if (sourceFile && testFile) { expect(sourceFile.relevanceScore).toBeGreaterThan(testFile.relevanceScore); } }); tap.test('ContextAnalyzer should prioritize changed files for commit task', async () => { const analyzer = new ContextAnalyzer(testProjectRoot); const changedFile = path.join(testProjectRoot, 'ts/context/types.ts'); const unchangedFile = path.join(testProjectRoot, 'ts/index.ts'); const metadata: IFileMetadata[] = [ { path: changedFile, relativePath: 'ts/context/types.ts', size: 3000, mtime: Date.now(), estimatedTokens: 750 }, { path: unchangedFile, relativePath: 'ts/index.ts', size: 3000, mtime: Date.now(), estimatedTokens: 750 } ]; const result = await analyzer.analyze(metadata, 'commit', [changedFile]); const changed = result.files.find(f => f.path === changedFile); const unchanged = result.files.find(f => f.path === unchangedFile); if (changed && unchanged) { // Changed file should have recency score of 1.0 expect(changed.recencyScore).toEqual(1.0); // Unchanged file should have recency score of 0 expect(unchanged.recencyScore).toEqual(0); } }); tap.test('ContextAnalyzer should calculate efficiency scores', async () => { const analyzer = new ContextAnalyzer(testProjectRoot); const metadata: IFileMetadata[] = [ { path: path.join(testProjectRoot, 'ts/context/types.ts'), relativePath: 'ts/context/types.ts', size: 5000, // Optimal size mtime: Date.now(), estimatedTokens: 1250 }, { path: path.join(testProjectRoot, 'ts/very-large-file.ts'), relativePath: 'ts/very-large-file.ts', size: 50000, // Too large mtime: Date.now(), estimatedTokens: 12500 } ]; const result = await analyzer.analyze(metadata, 'readme'); // Optimal size file should have better efficiency score const optimalFile = result.files.find(f => f.path.includes('types.ts')); const largeFile = result.files.find(f => f.path.includes('very-large-file.ts')); if (optimalFile && largeFile) { expect(optimalFile.efficiencyScore).toBeGreaterThan(largeFile.efficiencyScore); } }); tap.test('ContextAnalyzer should build dependency graph', async () => { const analyzer = new ContextAnalyzer(testProjectRoot); const metadata: IFileMetadata[] = [ { path: path.join(testProjectRoot, 'ts/context/enhanced-context.ts'), relativePath: 'ts/context/enhanced-context.ts', size: 10000, mtime: Date.now(), estimatedTokens: 2500 }, { path: path.join(testProjectRoot, 'ts/context/types.ts'), relativePath: 'ts/context/types.ts', size: 5000, mtime: Date.now(), estimatedTokens: 1250 } ]; const result = await analyzer.analyze(metadata, 'readme'); expect(result.dependencyGraph.size).toBeGreaterThan(0); // Check that each file has dependency info for (const meta of metadata) { const deps = result.dependencyGraph.get(meta.path); expect(deps).toBeDefined(); expect(deps!.path).toEqual(meta.path); expect(deps!.imports).toBeDefined(); expect(deps!.importedBy).toBeDefined(); expect(deps!.centrality).toBeGreaterThanOrEqual(0); } }); tap.test('ContextAnalyzer should calculate centrality scores', async () => { const analyzer = new ContextAnalyzer(testProjectRoot); const metadata: IFileMetadata[] = [ { path: path.join(testProjectRoot, 'ts/context/types.ts'), relativePath: 'ts/context/types.ts', size: 5000, mtime: Date.now(), estimatedTokens: 1250 }, { path: path.join(testProjectRoot, 'ts/context/enhanced-context.ts'), relativePath: 'ts/context/enhanced-context.ts', size: 10000, mtime: Date.now(), estimatedTokens: 2500 } ]; const result = await analyzer.analyze(metadata, 'readme'); // All centrality scores should be between 0 and 1 for (const [, deps] of result.dependencyGraph) { expect(deps.centrality).toBeGreaterThanOrEqual(0); expect(deps.centrality).toBeLessThanOrEqual(1); } }); tap.test('ContextAnalyzer should assign higher centrality to highly imported files', async () => { const analyzer = new ContextAnalyzer(testProjectRoot); // types.ts is likely imported by many files const typesPath = path.join(testProjectRoot, 'ts/context/types.ts'); // A test file is likely imported by fewer files const testPath = path.join(testProjectRoot, 'test/test.basic.node.ts'); const metadata: IFileMetadata[] = [ { path: typesPath, relativePath: 'ts/context/types.ts', size: 5000, mtime: Date.now(), estimatedTokens: 1250 }, { path: testPath, relativePath: 'test/test.basic.node.ts', size: 3000, mtime: Date.now(), estimatedTokens: 750 } ]; const result = await analyzer.analyze(metadata, 'readme'); const typesDeps = result.dependencyGraph.get(typesPath); const testDeps = result.dependencyGraph.get(testPath); if (typesDeps && testDeps) { // types.ts should generally have higher centrality due to being imported more expect(typesDeps.centrality).toBeGreaterThanOrEqual(0); expect(testDeps.centrality).toBeGreaterThanOrEqual(0); } }); tap.test('ContextAnalyzer should provide reason for scoring', async () => { const analyzer = new ContextAnalyzer(testProjectRoot); const metadata: IFileMetadata[] = [ { path: path.join(testProjectRoot, 'ts/index.ts'), relativePath: 'ts/index.ts', size: 3000, mtime: Date.now(), estimatedTokens: 750 } ]; const result = await analyzer.analyze(metadata, 'readme'); expect(result.files[0].reason).toBeDefined(); expect(result.files[0].reason!.length).toBeGreaterThan(0); }); tap.test('ContextAnalyzer should handle empty metadata array', async () => { const analyzer = new ContextAnalyzer(testProjectRoot); const result = await analyzer.analyze([], 'readme'); expect(result.files.length).toEqual(0); expect(result.totalFiles).toEqual(0); expect(result.dependencyGraph.size).toEqual(0); }); tap.test('ContextAnalyzer should respect custom tier configuration', async () => { const analyzer = new ContextAnalyzer( testProjectRoot, {}, { essential: { minScore: 0.9, trimLevel: 'none' }, important: { minScore: 0.7, trimLevel: 'light' }, optional: { minScore: 0.5, trimLevel: 'aggressive' } } ); const metadata: IFileMetadata[] = [ { path: path.join(testProjectRoot, 'ts/context/types.ts'), relativePath: 'ts/context/types.ts', size: 3000, mtime: Date.now(), estimatedTokens: 750 } ]; const result = await analyzer.analyze(metadata, 'readme'); // Should use custom tier thresholds const file = result.files[0]; expect(['essential', 'important', 'optional', 'excluded']).toContain(file.tier); }); tap.test('ContextAnalyzer should calculate combined importance score from all factors', async () => { const analyzer = new ContextAnalyzer(testProjectRoot, { dependencyWeight: 0.25, relevanceWeight: 0.25, efficiencyWeight: 0.25, recencyWeight: 0.25 }); const metadata: IFileMetadata[] = [ { path: path.join(testProjectRoot, 'ts/context/types.ts'), relativePath: 'ts/context/types.ts', size: 5000, mtime: Date.now(), estimatedTokens: 1250 } ]; const result = await analyzer.analyze(metadata, 'readme'); const file = result.files[0]; // Importance score should be weighted sum of all factors // With equal weights (0.25 each), importance should be average of all scores const expectedImportance = (file.relevanceScore * 0.25) + (file.centralityScore * 0.25) + (file.efficiencyScore * 0.25) + (file.recencyScore * 0.25); expect(file.importanceScore).toBeCloseTo(expectedImportance, 2); }); tap.test('ContextAnalyzer should complete analysis within reasonable time', async () => { const analyzer = new ContextAnalyzer(testProjectRoot); const metadata: IFileMetadata[] = Array.from({ length: 10 }, (_, i) => ({ path: path.join(testProjectRoot, `ts/file${i}.ts`), relativePath: `ts/file${i}.ts`, size: 3000, mtime: Date.now(), estimatedTokens: 750 })); const startTime = Date.now(); const result = await analyzer.analyze(metadata, 'readme'); const endTime = Date.now(); const duration = endTime - startTime; expect(result.analysisDuration).toBeGreaterThan(0); expect(duration).toBeLessThan(10000); // Should complete within 10 seconds }); export default tap.start();