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();
|