5e32375f0e
架构变更: - 采用 pnpm workspaces 实现 Monorepo 结构 - 将现有代码迁移到 packages/core - 新增 packages/server HTTP 服务层 Server 功能: - REST API: 会话管理、工具管理、配置管理 - WebSocket: 实时双向通信支持 - SSE: 服务端事件推送 - Hono + Bun 作为运行时 API 端点: - GET/POST /api/sessions - 会话 CRUD - GET/POST /api/sessions/:id/messages - 消息管理 - GET /api/sessions/:id/events - SSE 事件流 - WS /api/ws/:sessionId - WebSocket 连接 - GET/POST /api/tools - 工具管理 - GET/PUT /api/config - 配置管理
330 lines
8.9 KiB
TypeScript
330 lines
8.9 KiB
TypeScript
/**
|
|
* RepoMap 测试
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
import * as path from 'path';
|
|
import * as fs from 'fs/promises';
|
|
import * as os from 'os';
|
|
import { createRepoMap } from '../../src/repomap/repomap.js';
|
|
import { TagExtractor } from '../../src/repomap/tags/extractor.js';
|
|
import { Graph, pagerank, distributeRanksToDefinitions } from '../../src/repomap/ranking/index.js';
|
|
import { DiskCache } from '../../src/repomap/cache/disk-cache.js';
|
|
|
|
describe('Graph', () => {
|
|
it('should add edges and track nodes', () => {
|
|
const graph = new Graph();
|
|
|
|
graph.addEdge({ from: 'a.ts', to: 'b.ts', weight: 1, ident: 'foo' });
|
|
graph.addEdge({ from: 'a.ts', to: 'c.ts', weight: 2, ident: 'bar' });
|
|
graph.addEdge({ from: 'b.ts', to: 'c.ts', weight: 1, ident: 'baz' });
|
|
|
|
const nodes = graph.getNodes();
|
|
expect(nodes).toContain('a.ts');
|
|
expect(nodes).toContain('b.ts');
|
|
expect(nodes).toContain('c.ts');
|
|
expect(nodes.length).toBe(3);
|
|
});
|
|
|
|
it('should track in and out edges correctly', () => {
|
|
const graph = new Graph();
|
|
|
|
graph.addEdge({ from: 'a.ts', to: 'b.ts', weight: 1, ident: 'foo' });
|
|
graph.addEdge({ from: 'a.ts', to: 'c.ts', weight: 2, ident: 'bar' });
|
|
|
|
const outEdges = graph.getOutEdges('a.ts');
|
|
expect(outEdges.length).toBe(2);
|
|
|
|
const inEdges = graph.getInEdges('b.ts');
|
|
expect(inEdges.length).toBe(1);
|
|
expect(inEdges[0].from).toBe('a.ts');
|
|
});
|
|
|
|
it('should calculate out degree correctly', () => {
|
|
const graph = new Graph();
|
|
|
|
graph.addEdge({ from: 'a.ts', to: 'b.ts', weight: 1, ident: 'foo' });
|
|
graph.addEdge({ from: 'a.ts', to: 'c.ts', weight: 2, ident: 'bar' });
|
|
|
|
expect(graph.getOutDegree('a.ts')).toBe(3); // sum of weights
|
|
expect(graph.getOutDegree('b.ts')).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('PageRank', () => {
|
|
it('should compute ranks for a simple graph', () => {
|
|
const graph = new Graph();
|
|
|
|
// 创建一个简单的图: a -> b -> c
|
|
graph.addEdge({ from: 'a.ts', to: 'b.ts', weight: 1, ident: 'foo' });
|
|
graph.addEdge({ from: 'b.ts', to: 'c.ts', weight: 1, ident: 'bar' });
|
|
|
|
const ranks = pagerank(graph);
|
|
|
|
expect(ranks.size).toBe(3);
|
|
expect(ranks.get('a.ts')).toBeGreaterThan(0);
|
|
expect(ranks.get('b.ts')).toBeGreaterThan(0);
|
|
expect(ranks.get('c.ts')).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should respect personalization vector', () => {
|
|
const graph = new Graph();
|
|
|
|
graph.addEdge({ from: 'a.ts', to: 'b.ts', weight: 1, ident: 'foo' });
|
|
graph.addEdge({ from: 'b.ts', to: 'c.ts', weight: 1, ident: 'bar' });
|
|
graph.addEdge({ from: 'c.ts', to: 'a.ts', weight: 1, ident: 'baz' });
|
|
|
|
// 高度偏好 a.ts
|
|
const personalization = new Map<string, number>();
|
|
personalization.set('a.ts', 100);
|
|
personalization.set('b.ts', 1);
|
|
personalization.set('c.ts', 1);
|
|
|
|
const ranks = pagerank(graph, { personalization });
|
|
|
|
// a.ts 应该有较高的 rank
|
|
expect(ranks.get('a.ts')!).toBeGreaterThan(ranks.get('b.ts')!);
|
|
});
|
|
|
|
it('should distribute ranks to definitions', () => {
|
|
const graph = new Graph();
|
|
|
|
graph.addEdge({ from: 'a.ts', to: 'b.ts', weight: 1, ident: 'foo' });
|
|
graph.addEdge({ from: 'a.ts', to: 'b.ts', weight: 2, ident: 'bar' });
|
|
|
|
const nodeRanks = pagerank(graph);
|
|
const defRanks = distributeRanksToDefinitions(graph, nodeRanks);
|
|
|
|
expect(defRanks.has('b.ts:foo')).toBe(true);
|
|
expect(defRanks.has('b.ts:bar')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('DiskCache', () => {
|
|
let cacheDir: string;
|
|
let cache: DiskCache<{ value: number }>;
|
|
|
|
beforeAll(async () => {
|
|
cacheDir = path.join(os.tmpdir(), `repomap-test-${Date.now()}`);
|
|
cache = new DiskCache(cacheDir);
|
|
});
|
|
|
|
it('should set and get values', async () => {
|
|
await cache.set('key1', { value: 42 });
|
|
const result = await cache.get('key1');
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.value).toBe(42);
|
|
});
|
|
|
|
it('should return null for missing keys', async () => {
|
|
const result = await cache.get('nonexistent');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should delete values', async () => {
|
|
await cache.set('key2', { value: 100 });
|
|
await cache.delete('key2');
|
|
|
|
const result = await cache.get('key2');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('should check existence with has', async () => {
|
|
await cache.set('key3', { value: 200 });
|
|
|
|
expect(await cache.has('key3')).toBe(true);
|
|
expect(await cache.has('nonexistent2')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('TagExtractor', () => {
|
|
let tempDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tempDir = path.join(os.tmpdir(), `repomap-extractor-test-${Date.now()}`);
|
|
await fs.mkdir(tempDir, { recursive: true });
|
|
});
|
|
|
|
it('should extract tags from TypeScript code using regex fallback', async () => {
|
|
const testFile = path.join(tempDir, 'test.ts');
|
|
const code = `
|
|
export function greet(name: string): string {
|
|
return \`Hello, \${name}!\`;
|
|
}
|
|
|
|
export class Greeter {
|
|
private name: string;
|
|
|
|
constructor(name: string) {
|
|
this.name = name;
|
|
}
|
|
|
|
greet(): string {
|
|
return \`Hello, \${this.name}!\`;
|
|
}
|
|
}
|
|
|
|
export interface IGreeter {
|
|
greet(): string;
|
|
}
|
|
`;
|
|
|
|
await fs.writeFile(testFile, code);
|
|
|
|
const extractor = new TagExtractor();
|
|
const tags = await extractor.getTags(testFile, 'test.ts');
|
|
|
|
// 检查是否提取到了一些 tags
|
|
expect(tags.length).toBeGreaterThan(0);
|
|
|
|
// 检查是否有定义
|
|
const defs = tags.filter((t) => t.kind === 'def');
|
|
expect(defs.length).toBeGreaterThan(0);
|
|
|
|
// 检查是否包含函数和类
|
|
const defNames = defs.map((t) => t.name);
|
|
expect(defNames).toContain('greet');
|
|
expect(defNames).toContain('Greeter');
|
|
});
|
|
|
|
it('should extract tags from Python code using regex fallback', async () => {
|
|
const testFile = path.join(tempDir, 'test.py');
|
|
const code = `
|
|
def greet(name: str) -> str:
|
|
return f"Hello, {name}!"
|
|
|
|
class Greeter:
|
|
def __init__(self, name: str):
|
|
self.name = name
|
|
|
|
def greet(self) -> str:
|
|
return f"Hello, {self.name}!"
|
|
|
|
async def async_greet(name: str) -> str:
|
|
return f"Hello async, {name}!"
|
|
`;
|
|
|
|
await fs.writeFile(testFile, code);
|
|
|
|
const extractor = new TagExtractor();
|
|
const tags = await extractor.getTags(testFile, 'test.py');
|
|
|
|
// 检查是否提取到了一些 tags
|
|
expect(tags.length).toBeGreaterThan(0);
|
|
|
|
// 检查是否有定义
|
|
const defs = tags.filter((t) => t.kind === 'def');
|
|
expect(defs.length).toBeGreaterThan(0);
|
|
|
|
// 检查是否包含函数和类
|
|
const defNames = defs.map((t) => t.name);
|
|
expect(defNames).toContain('greet');
|
|
expect(defNames).toContain('Greeter');
|
|
expect(defNames).toContain('async_greet');
|
|
});
|
|
});
|
|
|
|
describe('RepoMap', () => {
|
|
let tempDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tempDir = path.join(os.tmpdir(), `repomap-test-${Date.now()}`);
|
|
await fs.mkdir(tempDir, { recursive: true });
|
|
|
|
// 创建测试文件
|
|
await fs.writeFile(
|
|
path.join(tempDir, 'utils.ts'),
|
|
`
|
|
export function add(a: number, b: number): number {
|
|
return a + b;
|
|
}
|
|
|
|
export function multiply(a: number, b: number): number {
|
|
return a * b;
|
|
}
|
|
`
|
|
);
|
|
|
|
await fs.writeFile(
|
|
path.join(tempDir, 'calculator.ts'),
|
|
`
|
|
import { add, multiply } from './utils';
|
|
|
|
export class Calculator {
|
|
add(a: number, b: number): number {
|
|
return add(a, b);
|
|
}
|
|
|
|
multiply(a: number, b: number): number {
|
|
return multiply(a, b);
|
|
}
|
|
}
|
|
`
|
|
);
|
|
|
|
await fs.writeFile(
|
|
path.join(tempDir, 'main.ts'),
|
|
`
|
|
import { Calculator } from './calculator';
|
|
|
|
const calc = new Calculator();
|
|
console.log(calc.add(1, 2));
|
|
console.log(calc.multiply(3, 4));
|
|
`
|
|
);
|
|
});
|
|
|
|
it('should create a repo map', async () => {
|
|
const repoMap = createRepoMap(tempDir, {
|
|
mapTokens: 2048,
|
|
});
|
|
|
|
const files = [
|
|
path.join(tempDir, 'utils.ts'),
|
|
path.join(tempDir, 'calculator.ts'),
|
|
path.join(tempDir, 'main.ts'),
|
|
];
|
|
|
|
const map = await repoMap.getRepoMap([], files, new Set(), new Set());
|
|
|
|
// 应该生成了一些内容
|
|
expect(map.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should boost mentioned identifiers', async () => {
|
|
const repoMap = createRepoMap(tempDir, {
|
|
mapTokens: 2048,
|
|
});
|
|
|
|
const files = [
|
|
path.join(tempDir, 'utils.ts'),
|
|
path.join(tempDir, 'calculator.ts'),
|
|
path.join(tempDir, 'main.ts'),
|
|
];
|
|
|
|
const mentionedIdents = new Set(['Calculator']);
|
|
const map = await repoMap.getRepoMap([], files, new Set(), mentionedIdents);
|
|
|
|
// 应该包含 Calculator 相关内容
|
|
expect(map.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should exclude chat files from output', async () => {
|
|
const repoMap = createRepoMap(tempDir, {
|
|
mapTokens: 2048,
|
|
});
|
|
|
|
const chatFiles = [path.join(tempDir, 'utils.ts')];
|
|
const otherFiles = [
|
|
path.join(tempDir, 'calculator.ts'),
|
|
path.join(tempDir, 'main.ts'),
|
|
];
|
|
|
|
const map = await repoMap.getRepoMap(chatFiles, otherFiles, new Set(), new Set());
|
|
|
|
// utils.ts 不应该出现在输出中
|
|
expect(map).not.toContain('utils.ts:');
|
|
});
|
|
});
|