/** * KHAELOR * File: tests/repograph/repograph.test.ts * Description: RepoGraph tests — TS/Python extraction, symbol queries, refs directions, skeletons (v2 §3). * * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai */ import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { extractFile, RepoGraphService } from "../../src/repograph/index.js"; import { LocalWorkspace } from "../../src/workspace/index.js"; const TS_SAMPLE = `/** * Handles authentication. */ export async function handleAuth(token: string): Promise { return verifyToken(token); } export class AuthController { /** Login entrypoint. */ async login(user: string): Promise { await handleAuth(user); } } export interface Session { id: string } export type AuthResult = boolean; const secret = "x"; import { verifyToken } from "./token.js"; `; describe("extractFile (TS)", () => { it("extracts functions, classes, methods, types, and imports", () => { const result = extractFile("src/auth.ts", TS_SAMPLE); const names = result.symbols.map((symbol) => `${symbol.kind}:${symbol.name}`); expect(names).toContain("function:handleAuth"); expect(names).toContain("class:AuthController"); expect(names).toContain("method:login"); expect(names).toContain("type:Session"); expect(names).toContain("type:AuthResult"); expect(names).toContain("variable:secret"); const auth = result.symbols.find((symbol) => symbol.name === "handleAuth"); expect(auth?.docComment).toContain("Handles authentication"); expect(auth?.exported).toBe(true); expect(result.imports[0]?.spec).toBe("./token.js"); expect(result.imports[0]?.names).toContain("verifyToken"); }); }); describe("extractFile (Python)", () => { it("extracts defs, classes, and imports", () => { const result = extractFile("app.py", "import os\nfrom flask import Flask\n\nclass App:\n def run(self):\n pass\n\ndef main():\n pass\n"); const names = result.symbols.map((symbol) => `${symbol.kind}:${symbol.name}`); expect(names).toContain("class:App"); expect(names).toContain("method:run"); expect(names).toContain("function:main"); expect(result.imports.map((imp) => imp.spec)).toEqual(["os", "flask"]); }); }); describe("RepoGraphService", () => { function makeRepo(): string { const dir = mkdtempSync(join(tmpdir(), "khaelor-repograph-")); mkdirSync(join(dir, "src"), { recursive: true }); writeFileSync(join(dir, "src", "auth.ts"), TS_SAMPLE); writeFileSync( join(dir, "src", "caller.ts"), `import { handleAuth } from "./auth.js";\nexport function guard(): void {\n void handleAuth("t");\n}\n`, ); return dir; } it("answers symbol queries with wildcards, kind, and scope filters", async () => { const service = new RepoGraphService({ workspace: new LocalWorkspace(makeRepo()) }); const exact = await service.querySymbols("handleAuth"); expect(exact).toHaveLength(1); expect(exact[0]?.file).toBe("src/auth.ts"); const wildcard = await service.querySymbols("*Controller", "class"); expect(wildcard.map((hit) => hit.symbol)).toEqual(["AuthController"]); const shorthand = await service.querySymbols("class:*Controller"); expect(shorthand).toHaveLength(1); const scoped = await service.querySymbols("handleAuth", undefined, "docs/**"); expect(scoped).toHaveLength(0); }); it("finds importers and callers of a symbol", async () => { const service = new RepoGraphService({ workspace: new LocalWorkspace(makeRepo()) }); const importers = await service.queryRefs("handleAuth", "importers"); expect(importers.some((hit) => hit.file === "src/caller.ts")).toBe(true); const callers = await service.queryRefs("handleAuth", "callers"); expect(callers.some((hit) => hit.file === "src/caller.ts" && hit.context.includes("handleAuth"))).toBe(true); }); it("produces a token-cheap skeleton with line anchors", async () => { const service = new RepoGraphService({ workspace: new LocalWorkspace(makeRepo()) }); const skeleton = await service.skeleton("src/auth.ts"); expect(skeleton).not.toBeNull(); expect(skeleton).toContain("export async function handleAuth"); expect(skeleton).toContain("re-read the file if you need bodies"); expect((skeleton as string).length).toBeLessThan(TS_SAMPLE.length * 2); }); });