465 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			465 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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();
 |