From 8295801934c46bc30b30999777cef4bf3393d84b Mon Sep 17 00:00:00 2001 From: Mir Sameer Date: Fri, 19 Jun 2026 13:29:26 -0700 Subject: [PATCH] Add test intelligence graph edges Signed-off-by: Mir Sameer --- README.md | 4 +- docs/agent-guide.md | 4 +- docs/research-notes.md | 4 +- package.json | 2 +- src/core/extractor.ts | 145 +++++++++++++++++++++++++++++++++++++++++ src/core/indexer.ts | 4 +- tests/indexer.test.ts | 90 ++++++++++++++++++++++++- 7 files changed, 245 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 3a250ce..bc20133 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ RepoLens MCP is an original TypeScript implementation built around fast local ve ## Why It Stands Out -- **MCP-native**: exposes 38 tools for indexing, version/update status, repeatable benchmarking, persistent config, project inventory/status, fleet summaries, cross-repo graphing, multi-agent setup, optional startup auto-indexing and git-aware auto-sync, BM25 code search, redacted secret scanning, symbol search, reference lookup, semantic search, vector search, context packs, source snippets, graph schema with relationship patterns and label properties, structural graph search, graph community detection, read-only Cypher-like graph queries, route-call links, runtime trace ingestion, channel/event edges, typed inheritance/implementation/use edges, receiver-aware method call edges, conservative data-flow edges, import-resolved file graphs, multi-ecosystem package manifests, lockfile resolved-dependency graphs, Docker/Kubernetes infrastructure nodes, dependency-cycle detection, architecture reports, architecture summaries, git-history hotspots, tracing, git-change impact, dead-code candidates, maintainable ADR memory, graph snapshots, and graph package exchange. +- **MCP-native**: exposes 38 tools for indexing, version/update status, repeatable benchmarking, persistent config, project inventory/status, fleet summaries, cross-repo graphing, multi-agent setup, optional startup auto-indexing and git-aware auto-sync, BM25 code search, redacted secret scanning, symbol search, reference lookup, semantic search, vector search, context packs, source snippets, graph schema with relationship patterns and label properties, structural graph search, graph community detection, read-only Cypher-like graph queries, route-call links, runtime trace ingestion, channel/event edges, test-to-code edges, typed inheritance/implementation/use edges, receiver-aware method call edges, conservative data-flow edges, import-resolved file graphs, multi-ecosystem package manifests, lockfile resolved-dependency graphs, Docker/Kubernetes infrastructure nodes, dependency-cycle detection, architecture reports, architecture summaries, git-history hotspots, tracing, git-change impact, dead-code candidates, maintainable ADR memory, graph snapshots, and graph package exchange. - **Agent-ready setup**: `doctor` inspects the local Codex MCP configuration, `install-codex` can add a managed MCP block with dry-run and force safeguards, `uninstall-codex` removes only managed RepoLens config, and `agent-setup`/`install-agents` generate reviewable guidance plus opt-in hook/reminder files for Codex, Claude, Gemini, Zed, OpenCode, Antigravity, Aider, KiloCode, VS Code, OpenClaw, and Kiro. - **Local-first SQLite memory**: all indexed data stays in `.repolens/memory.db`. - **Project catalog and cross-repo graphing**: `list-projects`, `project-status`, `fleet-summary`, `fleet-graph`, and `delete-project` track indexed repositories, aggregate languages/routes/HTTP calls/dependencies, and produce a catalog-wide graph with shared dependencies, route overlaps, and inferred consumer/provider service links. @@ -26,6 +26,7 @@ RepoLens MCP is an original TypeScript implementation built around fast local ve - **Reference lookup**: finds exact indexed identifier references and labels definition lines for language-server-style navigation without requiring an external LSP process. - **Type relationship graph**: adds `INHERITS`, `IMPLEMENTS`, and `USES_TYPE` edges from class/interface/protocol declarations and typed signatures for safer impact analysis. - **Receiver-aware call graph**: resolves TypeScript/JavaScript calls such as `repo.save()` to the method on the inferred constructed class, avoiding ambiguous same-name method edges. +- **Test intelligence**: extracts common test cases and links them to referenced functions, methods, classes, and routes with `TESTS` edges. - **Local semantic and vector search**: adds dependency-free `SIMILAR_TO` and `SEMANTICALLY_RELATED` edges, concept search, and persisted local vector embeddings over names, paths, signatures, metadata, and symbol bodies. - **Context packs for agents**: one query can return semantic matches, vector matches, graph matches, BM25 code hits, snippets, and nearby edges for focused development context. - **Redacted secret scan**: review high-confidence token shapes, sensitive assignments, and environment references from indexed source/config lines without returning raw secret values. @@ -182,6 +183,7 @@ The extractor is intentionally compact and extensible: - TypeScript and JavaScript: classes, interfaces, types, functions, const functions, imports, resolved local import edges, Express-style routes, and Next.js App Router `app/api/**/route.ts` handlers. - Receiver-aware TypeScript/JavaScript call edges: method symbols are attached to parent classes, and constructor-assigned receivers such as `const repo = new MemoryOrderRepository()` resolve `repo.save()` to the matching class method instead of every method named `save`. - Conservative data-flow edges: maps meaningful call arguments to target parameters when the callee can be resolved without ambiguous duplicate names, and prunes stale `DATA_FLOWS` edges during incremental refreshes. +- Test graph edges: JavaScript/TypeScript `test`/`it`, pytest functions, Go tests, JUnit methods, Rust `#[test]`, and XCTest-style methods become `test` nodes connected to referenced functions, methods, classes, and route paths with `TESTS` edges. - Trace modes: `trace_path` can focus on call paths, value propagation through `DATA_FLOWS`, cross-service HTTP/event paths, or all nearby edges. - HTTP call linking: literal `fetch`, Axios, and Node `http` calls become `http_call` nodes with `CALLS_HTTP_ENDPOINT`; matching route nodes also receive `HTTP_CALLS`. - GraphQL, gRPC, and OpenAPI: `.graphql`, `.gql`, `.proto`, OpenAPI JSON, and OpenAPI YAML files produce protocol nodes; protobuf `rpc` methods become route nodes using `/Service/Method` paths, and OpenAPI `{id}` path params normalize to `:id`. diff --git a/docs/agent-guide.md b/docs/agent-guide.md index 6edb52d..494e91c 100644 --- a/docs/agent-guide.md +++ b/docs/agent-guide.md @@ -35,7 +35,7 @@ On Windows PowerShell, the local installer mirrors the shell installer: 1. Run `index` or `benchmark` for the target repository. 2. Use `architecture`, `schema`, `communities`, or `fleet-summary` to understand the project shape. `schema` includes relationship patterns and label property hints for safer graph queries. -3. Use `search`, `symbols`, `references`, `trace`, `cycles`, and `context-pack` for focused code context. +3. Use `search`, `symbols`, `references`, `trace`, `cycles`, `query-graph`, and `context-pack` for focused code context, including `TESTS` edges when choosing validation coverage. 4. Use `changes` after edits to map uncommitted files back to graph impact, including per-file blast radius, relationship counts, and risk reasons. ## Useful MCP Tools @@ -47,7 +47,7 @@ On Windows PowerShell, the local installer mirrors the shell installer: - `trace_symbol` / `trace_path`: walk call, data-flow, or cross-service relationships. - `context_pack`: combine semantic, vector, graph, search, snippets, and nearby edges. - `get_architecture`: summarize languages, hotspots, packages, entrypoints, risks, and recommendations. -- `get_graph_schema`: inspect labels, edge types, relationship patterns, and label properties before writing graph queries. +- `get_graph_schema`: inspect labels, edge types such as `TESTS`, relationship patterns, and label properties before writing graph queries. - `scan_secrets`: return redacted high-signal secret findings from indexed lines. - `architecture_report`: generate Markdown or HTML reports. - `export_graph_package` / `import_graph_package`: share or bootstrap local graph snapshots. diff --git a/docs/research-notes.md b/docs/research-notes.md index 57206a8..72ec076 100644 --- a/docs/research-notes.md +++ b/docs/research-notes.md @@ -25,7 +25,7 @@ RepoLens MCP is not a fork or a drop-in static C replacement. It is an original - Node 24 plus native SQLite for a dependency-light local graph store. - Stable MCP SDK v1 package rather than the pre-alpha v2 branch. -- Clear CLI commands and MCP tools for Codex setup checks, persistent config, 11-agent setup guidance, graph package bootstrap, optional MCP startup auto-indexing, git-aware MCP auto-sync, indexing, repeatable full/incremental benchmarks, project inventory/status, fleet summaries with inferred service links, cross-repo fleet graphs, graph package exchange, BM25 code search, reference lookup, typed inheritance/implementation/use/data-flow edges, redacted secret scanning, semantic search, local vector search, context packs, source snippets, graph schema with relationship patterns and label properties, graph community detection, structural graph search, read-only Cypher-like graph queries with `DISTINCT`, `count`, `ORDER BY`, and `SKIP`, runtime trace ingestion, import-resolved file graph edges, multi-ecosystem manifest and lockfile extraction, Docker/Kubernetes/Kustomize infrastructure graph extraction, channel/event graph extraction, GraphQL/gRPC/tRPC/OpenAPI protocol extraction, route-call linking, relative and workspace-package import cycle detection, architecture reports, architecture summaries, git-history hotspots, tracing, impact analysis, dead-code candidates, git-change impact, and architecture decisions. +- Clear CLI commands and MCP tools for Codex setup checks, persistent config, 11-agent setup guidance, graph package bootstrap, optional MCP startup auto-indexing, git-aware MCP auto-sync, indexing, repeatable full/incremental benchmarks, project inventory/status, fleet summaries with inferred service links, cross-repo fleet graphs, graph package exchange, BM25 code search, reference lookup, typed inheritance/implementation/use/data-flow edges, test-to-code edges, redacted secret scanning, semantic search, local vector search, context packs, source snippets, graph schema with relationship patterns and label properties, graph community detection, structural graph search, read-only Cypher-like graph queries with `DISTINCT`, `count`, `ORDER BY`, and `SKIP`, runtime trace ingestion, import-resolved file graph edges, multi-ecosystem manifest and lockfile extraction, Docker/Kubernetes/Kustomize infrastructure graph extraction, channel/event graph extraction, GraphQL/gRPC/tRPC/OpenAPI protocol extraction, route-call linking, relative and workspace-package import cycle detection, architecture reports, architecture summaries, git-history hotspots, tracing, impact analysis, dead-code candidates, git-change impact, and architecture decisions. - Incremental indexing skips unchanged files, prunes removed files, and avoids call-edge rebuilds when there is no repository delta. - Watch mode keeps the graph fresh with polling-based incremental refreshes while preserving deterministic CLI behavior for tests and automation; git-aware auto-sync skips unchanged HEAD/status polls during long-running MCP sessions. - Browser dashboard without a bundler so the project is easy to build and inspect. @@ -44,7 +44,7 @@ RepoLens MCP is not a fork or a drop-in static C replacement. It is an original - Built-in ADR memory, not just structural graph search. - Dashboard API and HTML are included in the same binary entrypoint, avoiding a separate frontend build while still exposing graph exploration, fleet service links, schema counts, relationship patterns, label property hints, review signals, dead-code samples, and report links. - Swift extraction and big-repo validation now cover a mixed mobile/web monorepo, not only TypeScript services. -- Structural graph search, BM25 source search with code-aware token expansion, reference lookup, typed inheritance/implementation/use edges, conservative data-flow edges, persistent startup config, redacted secret scanning, context packs for agents, multi-agent setup guidance, graph package bootstrap, optional startup auto-indexing, git-aware auto-sync, repeatable benchmark output, import-resolved local file edges, multi-ecosystem package/dependency nodes, resolved lockfile dependency nodes, project inventory/status, fleet summaries with cross-project service links, cross-repo graphing, runtime trace ingestion, Docker/Kubernetes infrastructure nodes, channel/event edges, first-class HTTP call nodes, GraphQL/gRPC/tRPC/OpenAPI protocol nodes, route-call edges, deterministic graph communities, dependency-free semantic search, persisted local vector search, read-only Cypher-like graph queries, graph schema relationship/property summaries, dependency-cycle detection, dead-code candidates, git-history hotspots, git-diff impact mapping with per-file blast radius, watch indexing, and portable graph/package exports are first-class workflows. +- Structural graph search, BM25 source search with code-aware token expansion, reference lookup, typed inheritance/implementation/use edges, conservative data-flow edges, test-to-code `TESTS` edges, persistent startup config, redacted secret scanning, context packs for agents, multi-agent setup guidance, graph package bootstrap, optional startup auto-indexing, git-aware auto-sync, repeatable benchmark output, import-resolved local file edges, multi-ecosystem package/dependency nodes, resolved lockfile dependency nodes, project inventory/status, fleet summaries with cross-project service links, cross-repo graphing, runtime trace ingestion, Docker/Kubernetes infrastructure nodes, channel/event edges, first-class HTTP call nodes, GraphQL/gRPC/tRPC/OpenAPI protocol nodes, route-call edges, deterministic graph communities, dependency-free semantic search, persisted local vector search, read-only Cypher-like graph queries, graph schema relationship/property summaries, dependency-cycle detection, dead-code candidates, git-history hotspots, git-diff impact mapping with per-file blast radius, watch indexing, and portable graph/package exports are first-class workflows. - Indexing now writes local `SIMILAR_TO` and `SEMANTICALLY_RELATED` edges plus deterministic symbol vectors without external embeddings or network calls. - Architecture reports combine metrics, language mix, schema counts, structural hotspots, git-history churn, boundaries, import-resolved cycle checks, recommendations, dead-code samples, review signals, and a graph preview into one shareable artifact; the live schema API additionally exposes relationship patterns and label property hints for query authors. - A repeatable benchmark run on the large validation repo completed a full index in 16,484 ms and a no-op incremental index in 233 ms while preserving a 5,812-symbol, 38,645-edge graph with 153 Next.js route nodes, 653 resolved import edges, 5,957 type-use edges, 976 data-flow edges, and 387 locked dependencies. diff --git a/package.json b/package.json index 50b88f4..060c951 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "repolens-mcp", "version": "1.0.0", - "description": "Local-first repository intelligence MCP server with multi-agent setup, persistent config, project catalog, fleet summaries, cross-repo graphing, graph package bootstrap, optional startup auto-indexing, git-aware MCP auto-sync, incremental indexing, BM25 code search, reference lookup, typed relationship and data-flow edges, local vector search, redacted secret scanning, context packs, runtime trace ingestion, import-resolved file graphs, multi-ecosystem manifest and lockfile parsing, Docker/Kubernetes graph indexing, channel/event graph edges, git-history hotspots, watch mode, graph search, graph communities, semantic search, route-call links, read-only graph queries, source snippets, dependency-cycle checks, architecture reports, graph packages, ADR memory, graph export, and a dashboard.", + "description": "Local-first repository intelligence MCP server with multi-agent setup, persistent config, project catalog, fleet summaries, cross-repo graphing, graph package bootstrap, optional startup auto-indexing, git-aware MCP auto-sync, incremental indexing, BM25 code search, reference lookup, typed relationship, test, and data-flow edges, local vector search, redacted secret scanning, context packs, runtime trace ingestion, import-resolved file graphs, multi-ecosystem manifest and lockfile parsing, Docker/Kubernetes graph indexing, channel/event graph edges, git-history hotspots, watch mode, graph search, graph communities, semantic search, route-call links, read-only graph queries, source snippets, dependency-cycle checks, architecture reports, graph packages, ADR memory, graph export, and a dashboard.", "type": "module", "mcpName": "io.github.sameer2191/repolens-mcp", "bin": { diff --git a/src/core/extractor.ts b/src/core/extractor.ts index 05f2268..d7b2e4f 100644 --- a/src/core/extractor.ts +++ b/src/core/extractor.ts @@ -205,6 +205,11 @@ export function extractFromFile(filePath: string, language: Language, content: s edges.push({ source: String(method.metadata?.parentQualifiedName ?? fileNode), target: method.qualifiedName, type: "DEFINES", weight: 0.95 }); } + for (const testCase of extractTestCases(filePath, language, content, lines)) { + symbols.push(testCase); + edges.push({ source: fileNode, target: testCase.qualifiedName, type: "DEFINES", weight: 0.9 }); + } + const channels = extractChannelLinks(filePath, language, content, symbols); for (const symbol of channels.symbols) { symbols.push(symbol); @@ -412,6 +417,53 @@ function isReservedMethodName(name: string): boolean { return ["constructor", "if", "for", "while", "switch", "catch", "function", "return"].includes(name.toLowerCase()); } +function extractTestCases(filePath: string, language: Language, content: string, lines: string[]): SymbolNode[] { + if (!isLikelyTestFile(filePath) && !hasInlineTestMarkers(language, content)) { + return []; + } + const symbols: SymbolNode[] = []; + if (["typescript", "javascript"].includes(language)) { + const regex = /\b(?:test|it)(?:\.(?:only|skip|todo|concurrent))?\s*\(\s*(['"`])([^'"`\n]{1,180})\1/g; + for (const match of content.matchAll(regex)) { + const name = normalizeTestName(match[2]); + if (!name) continue; + const line = offsetToLine(content, match.index ?? 0); + symbols.push(makeSymbol(filePath, language, "test", name, line, findBlockEndLine(language, lines, line), lines[line - 1]?.trim().slice(0, 220), false, { framework: "js-test" })); + } + } else if (language === "python") { + const regex = /^\s*(?:async\s+)?def\s+(test_[A-Za-z_]\w*)\s*\(/gm; + for (const match of content.matchAll(regex)) { + const line = offsetToLine(content, match.index ?? 0); + symbols.push(makeSymbol(filePath, language, "test", match[1], line, findBlockEndLine(language, lines, line), lines[line - 1]?.trim().slice(0, 220), false, { framework: "pytest" })); + } + } else if (language === "go") { + const regex = /^func\s+(Test[A-Za-z_]\w*)\s*\(/gm; + for (const match of content.matchAll(regex)) { + const line = offsetToLine(content, match.index ?? 0); + symbols.push(makeSymbol(filePath, language, "test", match[1], line, findBlockEndLine(language, lines, line), lines[line - 1]?.trim().slice(0, 220), false, { framework: "go-test" })); + } + } else if (language === "java") { + const regex = /@Test[\s\r\n]+(?:public|private|protected)?\s*(?:void|[\w<>\[\]]+)\s+([A-Za-z_]\w*)\s*\(/g; + for (const match of content.matchAll(regex)) { + const line = offsetToLine(content, match.index ?? 0); + symbols.push(makeSymbol(filePath, language, "test", match[1], line, findBlockEndLine(language, lines, line), lines[line - 1]?.trim().slice(0, 220), false, { framework: "junit" })); + } + } else if (language === "rust") { + const regex = /#\s*\[\s*test\s*\][\s\r\n]+(?:pub\s+)?fn\s+([A-Za-z_]\w*)\s*\(/g; + for (const match of content.matchAll(regex)) { + const line = offsetToLine(content, match.index ?? 0); + symbols.push(makeSymbol(filePath, language, "test", match[1], line, findBlockEndLine(language, lines, line), lines[line - 1]?.trim().slice(0, 220), false, { framework: "rust-test" })); + } + } else if (language === "swift") { + const regex = /^\s*(?:public|internal|private|fileprivate|\s)*func\s+(test[A-Za-z_]\w*)\s*\(/gm; + for (const match of content.matchAll(regex)) { + const line = offsetToLine(content, match.index ?? 0); + symbols.push(makeSymbol(filePath, language, "test", match[1], line, findBlockEndLine(language, lines, line), lines[line - 1]?.trim().slice(0, 220), false, { framework: "xctest" })); + } + } + return symbols; +} + export function addTypeRelationEdges(symbols: SymbolNode[], fileContents: Map): Edge[] { const typeSymbols = symbols.filter(isTypeSymbol); if (typeSymbols.length === 0) { @@ -579,6 +631,53 @@ export function addHttpEdges(symbols: SymbolNode[], fileContents: Map): Edge[] { + const tests = symbols.filter((symbol) => symbol.kind === "test"); + if (tests.length === 0) { + return []; + } + const targets = symbols.filter(isTestTargetSymbol); + const targetNameCounts = new Map(); + for (const target of targets) { + targetNameCounts.set(target.name, (targetNameCounts.get(target.name) ?? 0) + 1); + } + + const edges: Edge[] = []; + const seen = new Set(); + for (const testCase of tests) { + const body = textForSymbol(testCase, fileContents); + if (!body) { + continue; + } + for (const target of targets) { + if (target.qualifiedName === testCase.qualifiedName) { + continue; + } + const reason = testTargetsSymbol(body, testCase, target, targetNameCounts); + if (!reason) { + continue; + } + const key = `${testCase.qualifiedName}\0${target.qualifiedName}`; + if (seen.has(key)) { + continue; + } + seen.add(key); + edges.push({ + source: testCase.qualifiedName, + target: target.qualifiedName, + type: "TESTS", + weight: testCase.filePath === target.filePath ? 0.82 : 0.72, + metadata: { + testName: testCase.name, + targetKind: target.kind, + reason + } + }); + } + } + return edges; +} + function makeSymbol( filePath: string, language: Language, @@ -2119,6 +2218,52 @@ function shouldScanTypeUses(symbol: SymbolNode): boolean { return ["function", "method", "class", "interface", "type", "struct", "enum", "protocol", "actor", "trait"].includes(symbol.kind); } +function isTestTargetSymbol(symbol: SymbolNode): boolean { + return ["function", "method", "class", "route"].includes(symbol.kind) && !isLikelyTestFile(symbol.filePath); +} + +function testTargetsSymbol(body: string, testCase: SymbolNode, target: SymbolNode, targetNameCounts: Map): string | null { + if (target.kind === "route") { + const routePath = typeof target.metadata?.path === "string" ? target.metadata.path : undefined; + return routePath && body.includes(routePath) ? "route_path_literal" : null; + } + if (testCase.filePath !== target.filePath && (targetNameCounts.get(target.name) ?? 0) > 1) { + return null; + } + if (["function", "method"].includes(target.kind) && callsName(body, target.name, { allowMember: target.kind !== "method" })) { + return "call_reference"; + } + if (target.kind === "class" && new RegExp(`\\bnew\\s+${escapeRegExp(target.name)}\\b`).test(body)) { + return "constructor_reference"; + } + return null; +} + +function isLikelyTestFile(filePath: string): boolean { + const normalized = filePath.replace(/\\/g, "/"); + return ( + /(^|\/)(?:__tests__|tests?|specs?)\//i.test(normalized) || + /\.(?:test|spec)\.[cm]?[jt]sx?$/i.test(normalized) || + /(?:^|\/)test_[^/]+\.py$/i.test(normalized) || + /_test\.go$/i.test(normalized) || + /(?:^|\/)[^/]+Test\.java$/i.test(normalized) || + /(?:^|\/)[^/]+Tests\.swift$/i.test(normalized) + ); +} + +function hasInlineTestMarkers(language: Language, content: string): boolean { + if (["typescript", "javascript"].includes(language)) return /\b(?:test|it)\s*\(/.test(content); + if (language === "java") return /@Test\b/.test(content); + if (language === "rust") return /#\s*\[\s*test\s*\]/.test(content); + if (language === "swift") return /\bfunc\s+test[A-Za-z_]\w*\s*\(/.test(content); + return false; +} + +function normalizeTestName(value: string | undefined): string | null { + const normalized = value?.trim().replace(/\s+/g, " "); + return normalized && normalized.length <= 180 ? normalized : null; +} + function declarationTextForSymbol(symbol: SymbolNode, fileContents: Map): string { const content = fileContents.get(symbol.filePath); if (!content) { diff --git a/src/core/indexer.ts b/src/core/indexer.ts index 00f0148..b530002 100644 --- a/src/core/indexer.ts +++ b/src/core/indexer.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { performance } from "node:perf_hooks"; import { exportGraphPackage, importGraphPackage } from "./artifact.js"; -import { addCallEdges, addDataFlowEdges, addHttpEdges, addTypeRelationEdges, extractFromFile } from "./extractor.js"; +import { addCallEdges, addDataFlowEdges, addHttpEdges, addTestEdges, addTypeRelationEdges, extractFromFile } from "./extractor.js"; import { sha256 } from "./hash.js"; import { buildResolvedImportEdges } from "./import-resolver.js"; import { loadRepoIgnoreMatcher, shouldIgnoreDirectory, shouldIgnoreFile, type RepoIgnoreMatcher } from "./ignore.js"; @@ -154,6 +154,7 @@ export async function indexRepository(options: IndexOptions): Promise { @@ -163,6 +164,7 @@ export async function indexRepository(options: IndexOptions): Promise { + const sourceContent = ` +export function createOrder(id: string) { + return { id }; +} + +export function listOrders() { + return []; +} +`; + const testContent = ` +import { createOrder, listOrders } from "../src/orders"; + +test("creates order records", () => { + const order = createOrder("1"); + expect(listOrders()).toContain(order); +}); +`; + const source = extractFromFile("src/orders.ts", "typescript", sourceContent); + const tests = extractFromFile("tests/orders.test.ts", "typescript", testContent); + const testCase = tests.symbols.find((symbol) => symbol.kind === "test" && symbol.name === "creates order records"); + assert.ok(testCase); + + const edges = addTestEdges( + [...source.symbols, ...tests.symbols], + new Map([ + ["src/orders.ts", sourceContent], + ["tests/orders.test.ts", testContent] + ]) + ); + const createOrder = source.symbols.find((symbol) => symbol.name === "createOrder")?.qualifiedName; + const listOrders = source.symbols.find((symbol) => symbol.name === "listOrders")?.qualifiedName; + + assert.ok(edges.some((edge) => edge.type === "TESTS" && edge.source === testCase.qualifiedName && edge.target === createOrder)); + assert.ok(edges.some((edge) => edge.type === "TESTS" && edge.source === testCase.qualifiedName && edge.target === listOrders)); + assert.ok(edges.every((edge) => edge.metadata?.reason === "call_reference")); +}); + test("extracts typed inheritance, implementation, and usage edges", () => { const content = ` export interface Order { @@ -300,6 +338,56 @@ export function checkout(order: Order) { } }); +test("indexes test case nodes and TESTS edges", async () => { + const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "repolens-test-graph-")); + const repo = path.join(tmp, "repo"); + await fs.mkdir(path.join(repo, "src"), { recursive: true }); + await fs.mkdir(path.join(repo, "tests"), { recursive: true }); + await fs.writeFile( + path.join(repo, "src", "orders.ts"), + ` +export function createOrder(id: string) { + return { id }; +} + +export function listOrders() { + return []; +} +` + ); + await fs.writeFile( + path.join(repo, "tests", "orders.test.ts"), + ` +import { createOrder, listOrders } from "../src/orders"; + +test("creates order records", () => { + const order = createOrder("1"); + expect(listOrders()).toContain(order); +}); +` + ); + + const dbPath = path.join(tmp, "memory.db"); + await indexRepository({ root: repo, dbPath }); + const store = new MemoryStore(dbPath); + try { + const schema = store.graphSchema(); + assert.ok(schema.nodeLabels.some((label) => label.kind === "test")); + assert.ok(schema.edgeTypes.some((edge) => edge.type === "TESTS" && edge.count === 2)); + + const query = store.queryGraph("MATCH (t)-[r:TESTS]->(f:Function) RETURN t.name,f.name,r.type ORDER BY f.name LIMIT 5"); + assert.deepEqual( + query.rows.map((row) => [row["t.name"], row["f.name"], row["r.type"]]), + [ + ["creates order records", "createOrder", "TESTS"], + ["creates order records", "listOrders", "TESTS"] + ] + ); + } finally { + store.close(); + } +}); + test("indexes a TypeScript repo with symbols, routes, search, and architecture", async () => { const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "memory-test-")); const dbPath = path.join(tmp, "memory.db");