@kridaydave/code-mapper 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +1 -0
  3. package/bin/code-mapper.mjs +86 -0
  4. package/dist/graph/GraphAnalyzer.js +32 -65
  5. package/dist/graph/GraphAnalyzer.js.map +1 -1
  6. package/dist/graph/GraphBuilder.js +18 -45
  7. package/dist/graph/GraphBuilder.js.map +1 -1
  8. package/dist/index.js +100 -23
  9. package/dist/index.js.map +1 -1
  10. package/dist/mcp/cache.js +8 -17
  11. package/dist/mcp/cache.js.map +1 -1
  12. package/dist/mcp/resources.js +5 -1
  13. package/dist/mcp/resources.js.map +1 -1
  14. package/dist/mcp/tools.js +190 -35
  15. package/dist/mcp/tools.js.map +1 -1
  16. package/dist/parser/ComplexityAnalyzer.js +19 -2
  17. package/dist/parser/ComplexityAnalyzer.js.map +1 -1
  18. package/dist/parser/FileAnalyzer.js +8 -30
  19. package/dist/parser/FileAnalyzer.js.map +1 -1
  20. package/dist/parser/ProjectParser.js +8 -5
  21. package/dist/parser/ProjectParser.js.map +1 -1
  22. package/dist/parser/ProjectParser.test.js +1 -17
  23. package/dist/parser/ProjectParser.test.js.map +1 -1
  24. package/dist/tui/index.js +239 -0
  25. package/dist/tui/index.js.map +1 -0
  26. package/package.json +82 -35
  27. package/AGENTS.md +0 -174
  28. package/docs/PHASE2_PLAN.md +0 -435
  29. package/fixtures/test-project/calculator.ts +0 -28
  30. package/fixtures/test-project/index.ts +0 -2
  31. package/fixtures/test-project/math.ts +0 -11
  32. package/src/graph/Graph.test.ts +0 -222
  33. package/src/graph/GraphAnalyzer.ts +0 -502
  34. package/src/graph/GraphBuilder.ts +0 -258
  35. package/src/graph/types.ts +0 -42
  36. package/src/index.ts +0 -38
  37. package/src/mcp/cache.ts +0 -89
  38. package/src/mcp/resources.ts +0 -137
  39. package/src/mcp/tools.test.ts +0 -104
  40. package/src/mcp/tools.ts +0 -529
  41. package/src/parser/ComplexityAnalyzer.ts +0 -275
  42. package/src/parser/FileAnalyzer.ts +0 -215
  43. package/src/parser/ProjectParser.test.ts +0 -96
  44. package/src/parser/ProjectParser.ts +0 -172
  45. package/src/parser/types.ts +0 -77
  46. package/src/types/graphology-pagerank.d.ts +0 -20
  47. package/tsconfig.json +0 -17
  48. package/vitest.config.ts +0 -15
@@ -1,258 +0,0 @@
1
- import { createRequire } from "node:module";
2
- const require = createRequire(import.meta.url);
3
- // eslint-disable-next-line @typescript-eslint/no-require-imports
4
- const Graph: typeof import("graphology").default = require("graphology");
5
- import type { AbstractGraph, Attributes } from "graphology-types";
6
- import { ParseResult } from "../parser/types.js";
7
- import { GraphNode, GraphEdge } from "./types.js";
8
-
9
- type CodeGraph = AbstractGraph<Attributes, Attributes, Attributes>;
10
-
11
- export class GraphBuilder {
12
- build(parseResult: ParseResult): { graph: CodeGraph; nodes: GraphNode[]; edges: GraphEdge[] } {
13
- const graph = new Graph({ multi: true });
14
- const nodes: GraphNode[] = [];
15
- const edges: GraphEdge[] = [];
16
-
17
- const classLookup = new Map<string, { filePath: string; name: string; lineNumber: number }>();
18
- for (const fileInfo of parseResult.files) {
19
- for (const cls of fileInfo.classes) {
20
- classLookup.set(fileInfo.filePath + "::" + cls.name, { filePath: fileInfo.filePath, name: cls.name, lineNumber: cls.lineNumber });
21
- }
22
- }
23
-
24
- const fileNodeIds = new Map<string, string>();
25
-
26
- // Phase 1: Create file nodes
27
- for (const fileInfo of parseResult.files) {
28
- const fileId = `file:${fileInfo.filePath}`;
29
- fileNodeIds.set(fileInfo.relativePath, fileId);
30
- fileNodeIds.set(fileInfo.filePath, fileId);
31
-
32
- graph.addNode(fileId, {
33
- kind: "file",
34
- label: fileInfo.relativePath,
35
- filePath: fileInfo.filePath,
36
- });
37
-
38
- nodes.push({
39
- id: fileId,
40
- kind: "file",
41
- label: fileInfo.relativePath,
42
- filePath: fileInfo.filePath,
43
- });
44
- }
45
-
46
- // Phase 2: Create function and class nodes, add containment edges
47
- for (const fileInfo of parseResult.files) {
48
- const fileId = `file:${fileInfo.filePath}`;
49
-
50
- for (const fn of fileInfo.functions) {
51
- const fnId = `fn:${fileInfo.filePath}:${fn.name}:${fn.lineNumber}`;
52
- graph.addNode(fnId, {
53
- kind: "function",
54
- label: fn.name,
55
- filePath: fileInfo.filePath,
56
- lineNumber: fn.lineNumber,
57
- });
58
-
59
- graph.addEdge(fileId, fnId, {
60
- kind: "contains",
61
- label: `contains ${fn.name}`,
62
- });
63
-
64
- edges.push({
65
- source: fileId,
66
- target: fnId,
67
- kind: "contains",
68
- label: `contains ${fn.name}`,
69
- });
70
-
71
- nodes.push({
72
- id: fnId,
73
- kind: "function",
74
- label: fn.name,
75
- filePath: fileInfo.filePath,
76
- lineNumber: fn.lineNumber,
77
- });
78
- }
79
-
80
- for (const cls of fileInfo.classes) {
81
- const clsId = `class:${fileInfo.filePath}:${cls.name}:${cls.lineNumber}`;
82
- graph.addNode(clsId, {
83
- kind: "class",
84
- label: cls.name,
85
- filePath: fileInfo.filePath,
86
- lineNumber: cls.lineNumber,
87
- });
88
-
89
- graph.addEdge(fileId, clsId, {
90
- kind: "contains",
91
- label: `contains ${cls.name}`,
92
- });
93
-
94
- edges.push({
95
- source: fileId,
96
- target: clsId,
97
- kind: "contains",
98
- label: `contains ${cls.name}`,
99
- });
100
-
101
- nodes.push({
102
- id: clsId,
103
- kind: "class",
104
- label: cls.name,
105
- filePath: fileInfo.filePath,
106
- lineNumber: cls.lineNumber,
107
- });
108
- }
109
- }
110
-
111
- // Phase 3: Build import edges between files
112
- for (const fileInfo of parseResult.files) {
113
- const sourceFileId = `file:${fileInfo.filePath}`;
114
-
115
- for (const imp of fileInfo.imports) {
116
- const targetPath = this.resolveImportPath(fileInfo.filePath, imp.moduleSpecifier, parseResult);
117
- if (targetPath) {
118
- const targetFileId = `file:${targetPath}`;
119
- if (graph.hasNode(targetFileId)) {
120
- const edgeLabel = imp.namedImports.length > 0
121
- ? `imports {${imp.namedImports.join(", ")}}`
122
- : imp.defaultImport
123
- ? `imports ${imp.defaultImport}`
124
- : "imports module";
125
-
126
- if (!graph.hasEdge(sourceFileId, targetFileId)) {
127
- graph.addEdge(sourceFileId, targetFileId, {
128
- kind: "imports",
129
- label: edgeLabel,
130
- });
131
-
132
- edges.push({
133
- source: sourceFileId,
134
- target: targetFileId,
135
- kind: "imports",
136
- label: edgeLabel,
137
- });
138
- }
139
- }
140
- }
141
- }
142
- }
143
-
144
- // Phase 4: Build extends/implements edges for classes
145
- for (const fileInfo of parseResult.files) {
146
- for (const cls of fileInfo.classes) {
147
- const clsId = `class:${fileInfo.filePath}:${cls.name}:${cls.lineNumber}`;
148
-
149
- if (cls.extends) {
150
- // Try to find the parent class in the codebase
151
- const parentName = cls.extends.split("<")[0].trim(); // Handle generics
152
- const parentClass = this.findClassByName(parentName, parseResult, classLookup);
153
- if (parentClass) {
154
- const parentId = `class:${parentClass.filePath}:${parentClass.name}:${parentClass.lineNumber}`;
155
- if (graph.hasNode(parentId)) {
156
- graph.addEdge(clsId, parentId, {
157
- kind: "extends",
158
- label: `extends ${parentClass.name}`,
159
- });
160
- edges.push({
161
- source: clsId,
162
- target: parentId,
163
- kind: "extends",
164
- label: `extends ${parentClass.name}`,
165
- });
166
- }
167
- }
168
- }
169
-
170
- for (const impl of cls.implements) {
171
- const implName = impl.split("<")[0].trim();
172
- const implClass = this.findClassByName(implName, parseResult, classLookup);
173
- if (implClass) {
174
- const implId = `class:${implClass.filePath}:${implClass.name}:${implClass.lineNumber}`;
175
- if (graph.hasNode(implId)) {
176
- graph.addEdge(clsId, implId, {
177
- kind: "implements",
178
- label: `implements ${implClass.name}`,
179
- });
180
- edges.push({
181
- source: clsId,
182
- target: implId,
183
- kind: "implements",
184
- label: `implements ${implClass.name}`,
185
- });
186
- }
187
- }
188
- }
189
- }
190
- }
191
-
192
- return { graph, nodes, edges };
193
- }
194
-
195
- private resolveImportPath(fromFile: string, moduleSpecifier: string, parseResult: ParseResult): string | null {
196
- // Skip external packages (no leading . or /)
197
- if (!moduleSpecifier.startsWith(".") && !moduleSpecifier.startsWith("/")) {
198
- return null;
199
- }
200
-
201
- // Strip .js/.jsx/.ts/.tsx extension from the specifier
202
- let specifier = moduleSpecifier;
203
- for (const ext of [".js", ".jsx", ".ts", ".tsx"]) {
204
- if (specifier.endsWith(ext)) {
205
- specifier = specifier.slice(0, -ext.length);
206
- break;
207
- }
208
- }
209
-
210
- // Resolve relative path using forward-slash logic (ts-morph always uses /)
211
- const fromDir = fromFile.replace(/\\/g, "/").replace(/\/[^/]+$/, "");
212
- const parts = [...(fromDir === "" ? [] : fromDir.split("/")), ...specifier.split("/")];
213
- const resolved: string[] = [];
214
- for (const part of parts) {
215
- if (part === "..") {
216
- if (resolved.length > 0) {
217
- resolved.pop();
218
- }
219
- // else: excessive ".." — ignore, prevents escaping root
220
- } else if (part !== "." && part !== "") {
221
- resolved.push(part);
222
- }
223
- }
224
- // Reconstruct with the drive prefix if present
225
- let resolvedPath = resolved.join("/");
226
- // If original fromFile had a Windows drive letter, preserve it
227
- const fromFileNormalized = fromFile.replace(/\\/g, "/");
228
- const driveMatch = fromFileNormalized.match(/^([A-Za-z]:\/)/);
229
- if (driveMatch && !resolvedPath.startsWith(driveMatch[1])) {
230
- resolvedPath = driveMatch[1] + resolvedPath;
231
- }
232
-
233
- // Try exact match first
234
- if (parseResult.files.some(f => f.filePath === resolvedPath)) {
235
- return resolvedPath;
236
- }
237
-
238
- // Try adding extensions
239
- const extensions = [".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js", "/index.jsx"];
240
- for (const ext of extensions) {
241
- const candidate = resolvedPath + ext;
242
- if (parseResult.files.some(f => f.filePath === candidate)) {
243
- return candidate;
244
- }
245
- }
246
-
247
- return null;
248
- }
249
-
250
- private findClassByName(name: string, _parseResult: ParseResult, classLookup: Map<string, { filePath: string; name: string; lineNumber: number }>): { filePath: string; name: string; lineNumber: number } | null {
251
- for (const [_key, value] of classLookup) {
252
- if (value.name === name) {
253
- return value;
254
- }
255
- }
256
- return null;
257
- }
258
- }
@@ -1,42 +0,0 @@
1
- export interface GraphNode {
2
- id: string;
3
- kind: "file" | "function" | "class";
4
- label: string;
5
- filePath: string;
6
- lineNumber?: number;
7
- metadata?: Record<string, unknown>;
8
- }
9
-
10
- export interface GraphEdge {
11
- source: string;
12
- target: string;
13
- kind: "imports" | "calls" | "extends" | "implements" | "contains";
14
- label: string;
15
- }
16
-
17
- export interface RankedFile {
18
- filePath: string;
19
- relativePath: string;
20
- score: number;
21
- metric: string;
22
- functionCount: number;
23
- classCount: number;
24
- importCount: number;
25
- exportCount: number;
26
- }
27
-
28
- export interface FunctionMatch {
29
- name: string;
30
- filePath: string;
31
- relativePath: string;
32
- lineNumber: number;
33
- kind: "function" | "class";
34
- parameters: string[];
35
- returnType: string;
36
- isExported: boolean;
37
- }
38
-
39
- export interface CallChainResult {
40
- found: boolean;
41
- paths: string[][];
42
- }
package/src/index.ts DELETED
@@ -1,38 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { McpServer } from "@modelcontextprotocol/server";
4
- import { StdioServerTransport } from "@modelcontextprotocol/server";
5
- import { registerTools } from "./mcp/tools.js";
6
- import { registerResources } from "./mcp/resources.js";
7
-
8
- async function main() {
9
- const server = new McpServer(
10
- {
11
- name: "code-mapper",
12
- version: "1.0.0",
13
- },
14
- {
15
- instructions:
16
- "CodeMapper analyzes TypeScript/JavaScript codebases using AST parsing. " +
17
- "Always start by calling scan_codebase with the target directory. " +
18
- "Then use find_function to locate symbols, analyze_dependencies to see the graph, " +
19
- "rank_impact to find central files, or trace_call_chain to follow dependency paths. " +
20
- "The codebase://summary and codebase://graph/{format} resources provide cached views.",
21
- }
22
- );
23
-
24
- // Register all tools and resources
25
- registerTools(server);
26
- registerResources(server);
27
-
28
- // Connect via stdio
29
- const transport = new StdioServerTransport();
30
- await server.connect(transport);
31
-
32
- console.error("CodeMapper MCP server running on stdio");
33
- }
34
-
35
- main().catch((error) => {
36
- console.error("Fatal error:", error);
37
- process.exit(1);
38
- });
package/src/mcp/cache.ts DELETED
@@ -1,89 +0,0 @@
1
- import { resolve } from "node:path";
2
- import { GraphAnalyzer } from "../graph/GraphAnalyzer.js";
3
-
4
- interface CacheEntry {
5
- analyzer: GraphAnalyzer;
6
- timestamp: number;
7
- }
8
-
9
- const MAX_CACHE_SIZE = 10;
10
- const CACHE_TTL_MS = 30 * 60 * 1000;
11
-
12
- export const analyzerCache = new Map<string, CacheEntry>();
13
- const pendingAnalyzers = new Map<string, Promise<GraphAnalyzer>>();
14
-
15
- let _lastScannedDirectory: string | null = null;
16
-
17
- export function setLastScannedDirectory(dir: string): void {
18
- _lastScannedDirectory = dir;
19
- }
20
-
21
- export function getLastScannedDirectory(): string | null {
22
- return _lastScannedDirectory;
23
- }
24
-
25
- export function normalizeCacheKey(directory: string): string {
26
- return resolve(directory);
27
- }
28
-
29
- function evictIfNeeded(): void {
30
- if (analyzerCache.size >= MAX_CACHE_SIZE) {
31
- let oldestKey: string | null = null;
32
- let oldestTime = Infinity;
33
- for (const [key, entry] of analyzerCache) {
34
- if (entry.timestamp < oldestTime) {
35
- oldestTime = entry.timestamp;
36
- oldestKey = key;
37
- }
38
- }
39
- if (oldestKey) {
40
- analyzerCache.delete(oldestKey);
41
- }
42
- }
43
- }
44
-
45
- function isEntryExpired(entry: CacheEntry): boolean {
46
- return Date.now() - entry.timestamp > CACHE_TTL_MS;
47
- }
48
-
49
- export function getAnalyzerFromCache(directory: string): GraphAnalyzer | undefined {
50
- const normalizedDir = normalizeCacheKey(directory);
51
- const entry = analyzerCache.get(normalizedDir);
52
- if (entry && !isEntryExpired(entry)) {
53
- return entry.analyzer;
54
- }
55
- if (entry) {
56
- analyzerCache.delete(normalizedDir);
57
- }
58
- return undefined;
59
- }
60
-
61
- export function setAnalyzerInCache(directory: string, analyzer: GraphAnalyzer): void {
62
- evictIfNeeded();
63
- const normalizedDir = normalizeCacheKey(directory);
64
- analyzerCache.set(normalizedDir, {
65
- analyzer,
66
- timestamp: Date.now(),
67
- });
68
- }
69
-
70
- export function getPendingAnalyzer(directory: string): Promise<GraphAnalyzer> | undefined {
71
- const normalizedDir = normalizeCacheKey(directory);
72
- return pendingAnalyzers.get(normalizedDir);
73
- }
74
-
75
- export function setPendingAnalyzer(directory: string, promise: Promise<GraphAnalyzer>): void {
76
- const normalizedDir = normalizeCacheKey(directory);
77
- pendingAnalyzers.set(normalizedDir, promise);
78
- promise.finally(() => {
79
- pendingAnalyzers.delete(normalizedDir);
80
- });
81
- }
82
-
83
- export function clearAnalyzerCache(directory?: string): void {
84
- if (directory) {
85
- analyzerCache.delete(normalizeCacheKey(directory));
86
- } else {
87
- analyzerCache.clear();
88
- }
89
- }
@@ -1,137 +0,0 @@
1
- import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server";
2
- import { GraphNode, GraphEdge, RankedFile } from "../graph/types.js";
3
- import { analyzerCache, getLastScannedDirectory, getAnalyzerFromCache } from "./cache.js";
4
-
5
- const RANK_CACHE_LIMIT = 100;
6
- const rankCache = new Map<string, RankedFile[]>();
7
-
8
- export function registerResources(server: McpServer): void {
9
- // Resource 1: codebase://summary
10
- server.registerResource(
11
- "codebase-summary",
12
- "codebase://summary",
13
- {
14
- title: "Codebase Summary",
15
- description: "Returns a summary of the most recently scanned codebase. Scan a codebase first using the scan_codebase tool.",
16
- mimeType: "application/json",
17
- },
18
- async () => {
19
- if (analyzerCache.size === 0) {
20
- return {
21
- contents: [{
22
- uri: "codebase://summary",
23
- text: JSON.stringify({ message: "No codebase has been scanned yet. Use the scan_codebase tool first." }, null, 2),
24
- }],
25
- };
26
- }
27
-
28
- const summaries: Record<string, unknown>[] = [];
29
- for (const [directory, entry] of analyzerCache) {
30
- const analyzer = entry.analyzer;
31
- const nodes = analyzer.getNodes();
32
- const edges = analyzer.getEdges();
33
- let ranked: RankedFile[];
34
- if (rankCache.has(directory)) {
35
- ranked = rankCache.get(directory)!;
36
- } else {
37
- ranked = analyzer.rankImpact("inDegree");
38
- rankCache.set(directory, ranked);
39
- }
40
- const cycles = analyzer.detectCycles();
41
-
42
- const fileNodes = nodes.filter((n: GraphNode) => n.kind === "file");
43
- const functionNodes = nodes.filter((n: GraphNode) => n.kind === "function");
44
- const classNodes = nodes.filter((n: GraphNode) => n.kind === "class");
45
-
46
- summaries.push({
47
- directory,
48
- totalFiles: fileNodes.length,
49
- totalFunctions: functionNodes.length,
50
- totalClasses: classNodes.length,
51
- totalEdges: edges.length,
52
- mostCentralFile: ranked.length > 0 ? ranked[0] : null,
53
- cycleCount: cycles.length,
54
- topDependencies: ranked.slice(0, 5).map((r: RankedFile) => ({
55
- path: r.relativePath,
56
- inDegree: r.score,
57
- })),
58
- });
59
- }
60
-
61
- return {
62
- contents: [{
63
- uri: "codebase://summary",
64
- text: JSON.stringify({ scannedCodebases: summaries }, null, 2),
65
- }],
66
- };
67
- }
68
- );
69
-
70
- // Resource 2: codebase://graph/{format}
71
- server.registerResource(
72
- "codebase-graph",
73
- new ResourceTemplate("codebase://graph/{format}", {
74
- list: async () => ({
75
- resources: [
76
- { uri: "codebase://graph/json", name: "Graph (JSON)" },
77
- { uri: "codebase://graph/mermaid", name: "Graph (Mermaid)" },
78
- ],
79
- }),
80
- }),
81
- {
82
- title: "Codebase Dependency Graph",
83
- description: "Returns the dependency graph of the scanned codebase in JSON or Mermaid format.",
84
- mimeType: "application/json",
85
- },
86
- async (uri, { format }) => {
87
- if (analyzerCache.size === 0) {
88
- return {
89
- contents: [{
90
- uri: uri.href,
91
- text: JSON.stringify({ message: "No codebase has been scanned yet. Use the scan_codebase tool first." }, null, 2),
92
- }],
93
- };
94
- }
95
-
96
- const targetKey = getLastScannedDirectory();
97
- if (!targetKey) {
98
- return {
99
- contents: [{
100
- uri: uri.href,
101
- text: JSON.stringify({ message: "No codebase has been scanned yet. Use the scan_codebase tool first." }, null, 2),
102
- }],
103
- };
104
- }
105
- const entry = analyzerCache.get(targetKey);
106
- if (!entry) {
107
- return {
108
- contents: [{
109
- uri: uri.href,
110
- text: JSON.stringify({ message: "Analyzer not found for the most recently scanned codebase." }, null, 2),
111
- }],
112
- };
113
- }
114
- const analyzer = entry.analyzer;
115
- const nodes = analyzer.getNodes();
116
- const edges = analyzer.getEdges();
117
-
118
- let content: string;
119
- if (format === "mermaid") {
120
- content = analyzer.toMermaid();
121
- } else {
122
- content = JSON.stringify({
123
- directory: targetKey,
124
- nodes: nodes.map((n: GraphNode) => ({ id: n.id, kind: n.kind, label: n.label, filePath: n.filePath })),
125
- edges: edges.map((e: GraphEdge) => ({ source: e.source, target: e.target, kind: e.kind, label: e.label })),
126
- }, null, 2);
127
- }
128
-
129
- return {
130
- contents: [{
131
- uri: uri.href,
132
- text: content,
133
- }],
134
- };
135
- }
136
- );
137
- }
@@ -1,104 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
2
- import path from "node:path";
3
- import {
4
- validateDirectory,
5
- getAnalyzer,
6
- safeHandler,
7
- clearAnalyzerCache,
8
- } from "../mcp/tools.js";
9
-
10
- describe("tools", () => {
11
- describe("validateDirectory", () => {
12
- it("should accept valid directory", () => {
13
- const testDir = path.resolve("./fixtures/test-project");
14
- const result = validateDirectory(testDir);
15
- expect(result).toBe(testDir);
16
- });
17
-
18
- it("should throw for non-existent directory", () => {
19
- expect(() => validateDirectory("/non/existent/path")).toThrow();
20
- });
21
-
22
- it("should throw for file instead of directory", () => {
23
- const testFile = path.resolve("./fixtures/test-project/math.ts");
24
- expect(() => validateDirectory(testFile)).toThrow();
25
- });
26
-
27
- it("should block Windows system paths", () => {
28
- expect(() => validateDirectory("C:\\Windows\\System32")).toThrow();
29
- expect(() => validateDirectory("C:\\Program Files")).toThrow();
30
- });
31
-
32
- it("should block Unix system paths", () => {
33
- expect(() => validateDirectory("/etc/passwd")).toThrow();
34
- expect(() => validateDirectory("/usr/bin")).toThrow();
35
- });
36
- });
37
-
38
- describe("safeHandler", () => {
39
- it("should return result on success", async () => {
40
- const handler = vi.fn().mockResolvedValue({
41
- content: [{ type: "text", text: "success" }],
42
- });
43
- const result = await safeHandler(handler);
44
-
45
- expect(handler).toHaveBeenCalled();
46
- expect(result.content[0].text).toBe("success");
47
- });
48
-
49
- it("should catch errors and return error content", async () => {
50
- const handler = vi.fn().mockRejectedValue(new Error("test error"));
51
- const result = await safeHandler(handler);
52
-
53
- expect(result.isError).toBe(true);
54
- expect(result.content[0].text).toContain("test error");
55
- });
56
-
57
- it("should handle non-Error rejections", async () => {
58
- const handler = vi.fn().mockRejectedValue("string error");
59
- const result = await safeHandler(handler);
60
-
61
- expect(result.isError).toBe(true);
62
- expect(result.content[0].text).toContain("string error");
63
- });
64
- });
65
-
66
- describe("getAnalyzer", () => {
67
- beforeEach(() => {
68
- clearAnalyzerCache();
69
- });
70
-
71
- it("should create analyzer for valid directory", async () => {
72
- const testDir = path.resolve("./fixtures/test-project");
73
- const analyzer = await getAnalyzer(testDir);
74
-
75
- expect(analyzer).toBeDefined();
76
- expect(analyzer.getParseResult().totalFiles).toBe(3);
77
- }, 30000);
78
-
79
- it("should cache analyzer for same directory", async () => {
80
- const testDir = path.resolve("./fixtures/test-project");
81
- const analyzer1 = await getAnalyzer(testDir);
82
- const analyzer2 = await getAnalyzer(testDir);
83
-
84
- expect(analyzer1).toBe(analyzer2);
85
- }, 30000);
86
-
87
- it("should throw for invalid directory", async () => {
88
- const testDir = path.resolve("./fixtures/test-project");
89
- await getAnalyzer(testDir);
90
- expect(true).toBe(true);
91
- }, 30000);
92
- });
93
-
94
- describe("clearAnalyzerCache", () => {
95
- it("should clear cache when called without argument", async () => {
96
- const testDir = path.resolve("./fixtures/test-project");
97
- await getAnalyzer(testDir);
98
-
99
- clearAnalyzerCache();
100
- const analyzer = await getAnalyzer(testDir);
101
- expect(analyzer.getParseResult().totalFiles).toBe(3);
102
- }, 30000);
103
- });
104
- });