Files
kurihada 5e32375f0e feat: 重构为 Monorepo 架构并实现 HTTP Server
架构变更:
- 采用 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 - 配置管理
2025-12-12 10:42:20 +08:00

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:');
});
});