Skip to content

Commit 90f1ebe

Browse files
committed
test: 26 tests for dependency analyzer -- TS parsing, resolution, include_paths
Full test coverage for the three fixes in this PR: TestParserInitialization (5): TS/TSX parsers loaded, not same as JS parser TestLanguageDetection (6): file extension -> language mapping TestTypeScriptImportExtraction (5): import type, export from, TSX, counts TestImportResolution (2): .js extension resolves to .ts file on disk TestIncludePaths (3): filters by directory, excludes unrelated files TestGraphMetrics (3): node counts, edge validity, metric fields TestPythonImports (2): regression -- Python imports still work Uses tmp_path fixtures with realistic Effect-TS-style code structure. All 26 pass in 2.09s.
1 parent d832021 commit 90f1ebe

1 file changed

Lines changed: 285 additions & 0 deletions

File tree

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
"""
2+
Tests for DependencyAnalyzer -- TypeScript parsing, import resolution, include_paths
3+
"""
4+
import pytest
5+
import tempfile
6+
import os
7+
from pathlib import Path
8+
9+
10+
@pytest.fixture
11+
def analyzer():
12+
"""Create a fresh DependencyAnalyzer instance"""
13+
from services.dependency_analyzer import DependencyAnalyzer
14+
return DependencyAnalyzer()
15+
16+
17+
@pytest.fixture
18+
def ts_repo(tmp_path):
19+
"""Create a minimal TypeScript repo structure for testing"""
20+
# packages/effect/src/Option.ts
21+
pkg_effect = tmp_path / "packages" / "effect" / "src"
22+
pkg_effect.mkdir(parents=True)
23+
(pkg_effect / "Option.ts").write_text('''
24+
import type { Effect } from "./Effect.js"
25+
import { pipe, flow } from "./Function.js"
26+
import * as Predicate from "./Predicate.js"
27+
export { type TypeLambda } from "./HKT.js"
28+
''')
29+
(pkg_effect / "Effect.ts").write_text('''
30+
import { pipe } from "./Function.js"
31+
import type { Context } from "./Context.js"
32+
''')
33+
(pkg_effect / "Function.ts").write_text('''
34+
export const pipe = (...args: any[]) => args
35+
export const flow = (...args: any[]) => args
36+
''')
37+
(pkg_effect / "Predicate.ts").write_text('''
38+
export const isString = (u: unknown): u is string => typeof u === "string"
39+
''')
40+
(pkg_effect / "HKT.ts").write_text('''
41+
export interface TypeLambda { readonly _A: unknown }
42+
''')
43+
(pkg_effect / "Context.ts").write_text('''
44+
export interface Context<A> { readonly _tag: "Context" }
45+
''')
46+
47+
# packages/schema/src/Schema.ts
48+
pkg_schema = tmp_path / "packages" / "schema" / "src"
49+
pkg_schema.mkdir(parents=True)
50+
(pkg_schema / "Schema.ts").write_text('''
51+
import * as Option from "../../effect/src/Option.js"
52+
import { pipe } from "../../effect/src/Function.js"
53+
''')
54+
55+
# Python file that should be excluded for TS repos
56+
backend = tmp_path / "backend"
57+
backend.mkdir()
58+
(backend / "main.py").write_text('''
59+
from fastapi import FastAPI
60+
import os
61+
''')
62+
63+
return tmp_path
64+
65+
66+
@pytest.fixture
67+
def tsx_repo(tmp_path):
68+
"""Create a minimal TSX repo for testing"""
69+
src = tmp_path / "src" / "components"
70+
src.mkdir(parents=True)
71+
(src / "Button.tsx").write_text('''
72+
import React from "react"
73+
import { cn } from "../utils.js"
74+
import type { ButtonProps } from "./types.js"
75+
export function Button({ children }: ButtonProps) { return <button>{children}</button> }
76+
''')
77+
(tmp_path / "src" / "utils.ts").write_text('''
78+
export function cn(...args: string[]) { return args.join(" ") }
79+
''')
80+
(src / "types.ts").write_text('''
81+
export interface ButtonProps { children: React.ReactNode }
82+
''')
83+
return tmp_path
84+
85+
86+
class TestParserInitialization:
87+
"""Verify correct tree-sitter parsers are loaded"""
88+
89+
def test_has_typescript_parser(self, analyzer):
90+
assert 'typescript' in analyzer.parsers
91+
92+
def test_has_tsx_parser(self, analyzer):
93+
assert 'tsx' in analyzer.parsers
94+
95+
def test_has_python_parser(self, analyzer):
96+
assert 'python' in analyzer.parsers
97+
98+
def test_has_javascript_parser(self, analyzer):
99+
assert 'javascript' in analyzer.parsers
100+
101+
def test_ts_parser_is_not_js(self, analyzer):
102+
"""The TS parser must NOT be the JS parser (the original bug)"""
103+
ts_parser = analyzer.parsers['typescript']
104+
js_parser = analyzer.parsers['javascript']
105+
# They should be different Language objects
106+
assert ts_parser is not js_parser
107+
108+
109+
class TestLanguageDetection:
110+
"""Verify file extension to language mapping"""
111+
112+
def test_ts_detected(self, analyzer):
113+
assert analyzer._detect_language("src/index.ts") == "typescript"
114+
115+
def test_tsx_detected(self, analyzer):
116+
assert analyzer._detect_language("src/App.tsx") == "tsx"
117+
118+
def test_js_detected(self, analyzer):
119+
assert analyzer._detect_language("lib/utils.js") == "javascript"
120+
121+
def test_jsx_detected(self, analyzer):
122+
assert analyzer._detect_language("src/App.jsx") == "javascript"
123+
124+
def test_py_detected(self, analyzer):
125+
assert analyzer._detect_language("backend/main.py") == "python"
126+
127+
def test_unknown_extension(self, analyzer):
128+
assert analyzer._detect_language("README.md") == "unknown"
129+
130+
131+
class TestTypeScriptImportExtraction:
132+
"""Verify TS import/export patterns are correctly extracted"""
133+
134+
def test_basic_ts_imports(self, analyzer, ts_repo):
135+
"""Standard TS import with .js extension (nodenext convention)"""
136+
result = analyzer.analyze_file_dependencies(
137+
str(ts_repo / "packages/effect/src/Option.ts")
138+
)
139+
imports = set(result['imports'])
140+
assert "./Effect.js" in imports
141+
assert "./Function.js" in imports
142+
assert "./Predicate.js" in imports
143+
144+
def test_type_imports(self, analyzer, ts_repo):
145+
"""import type should be detected"""
146+
result = analyzer.analyze_file_dependencies(
147+
str(ts_repo / "packages/effect/src/Option.ts")
148+
)
149+
imports = set(result['imports'])
150+
assert "./Effect.js" in imports # import type { Effect } from "./Effect.js"
151+
152+
def test_re_exports(self, analyzer, ts_repo):
153+
"""export { x } from should be detected"""
154+
result = analyzer.analyze_file_dependencies(
155+
str(ts_repo / "packages/effect/src/Option.ts")
156+
)
157+
imports = set(result['imports'])
158+
assert "./HKT.js" in imports # export { type TypeLambda } from "./HKT.js"
159+
160+
def test_tsx_imports(self, analyzer, tsx_repo):
161+
"""TSX files should be parsed without errors"""
162+
result = analyzer.analyze_file_dependencies(
163+
str(tsx_repo / "src/components/Button.tsx")
164+
)
165+
imports = set(result['imports'])
166+
assert "react" in imports
167+
assert "../utils.js" in imports
168+
169+
def test_import_count(self, analyzer, ts_repo):
170+
result = analyzer.analyze_file_dependencies(
171+
str(ts_repo / "packages/effect/src/Option.ts")
172+
)
173+
assert result['import_count'] == 4 # Effect, Function, Predicate, HKT
174+
175+
176+
class TestImportResolution:
177+
"""Verify .js -> .ts resolution and relative path handling"""
178+
179+
def test_js_extension_resolves_to_ts(self, analyzer, ts_repo):
180+
"""import from './Function.js' should resolve to Function.ts"""
181+
graph = analyzer.build_dependency_graph(str(ts_repo))
182+
deps = graph['dependencies']
183+
184+
option_deps = deps.get('packages/effect/src/Option.ts', [])
185+
# Function.js should resolve to Function.ts
186+
resolved_targets = set()
187+
for edge in graph['edges']:
188+
if edge['source'] == 'packages/effect/src/Option.ts':
189+
resolved_targets.add(edge['target'])
190+
191+
assert 'packages/effect/src/Function.ts' in resolved_targets
192+
193+
def test_relative_imports_resolve(self, analyzer, ts_repo):
194+
"""Relative paths should resolve to actual files"""
195+
graph = analyzer.build_dependency_graph(str(ts_repo))
196+
edges_from_option = [
197+
e['target'] for e in graph['edges']
198+
if e['source'] == 'packages/effect/src/Option.ts'
199+
]
200+
# Should resolve at least some internal deps
201+
assert len(edges_from_option) > 0
202+
203+
204+
class TestIncludePaths:
205+
"""Verify include_paths filtering works correctly"""
206+
207+
def test_without_include_paths_scans_everything(self, analyzer, ts_repo):
208+
"""No include_paths should scan all files"""
209+
graph = analyzer.build_dependency_graph(str(ts_repo))
210+
file_paths = set(graph['dependencies'].keys())
211+
# Should include both packages AND backend
212+
assert any('backend' in f for f in file_paths)
213+
assert any('packages/effect' in f for f in file_paths)
214+
215+
def test_include_paths_filters_to_subset(self, analyzer, ts_repo):
216+
"""include_paths should restrict to specified directories"""
217+
graph = analyzer.build_dependency_graph(
218+
str(ts_repo),
219+
include_paths=['packages/effect']
220+
)
221+
file_paths = set(graph['dependencies'].keys())
222+
# Should only have effect package files
223+
assert all('packages/effect' in f for f in file_paths)
224+
# Should NOT have backend files
225+
assert not any('backend' in f for f in file_paths)
226+
# Should NOT have schema files
227+
assert not any('packages/schema' in f for f in file_paths)
228+
229+
def test_include_paths_multiple_dirs(self, analyzer, ts_repo):
230+
"""Multiple include_paths should include all specified dirs"""
231+
graph = analyzer.build_dependency_graph(
232+
str(ts_repo),
233+
include_paths=['packages/effect', 'packages/schema']
234+
)
235+
file_paths = set(graph['dependencies'].keys())
236+
assert any('packages/effect' in f for f in file_paths)
237+
assert any('packages/schema' in f for f in file_paths)
238+
assert not any('backend' in f for f in file_paths)
239+
240+
241+
class TestGraphMetrics:
242+
"""Verify graph statistics are correct"""
243+
244+
def test_node_count_matches_files(self, analyzer, ts_repo):
245+
graph = analyzer.build_dependency_graph(
246+
str(ts_repo),
247+
include_paths=['packages/effect']
248+
)
249+
nodes = graph['nodes']
250+
deps = graph['dependencies']
251+
assert len(nodes) == len(deps)
252+
253+
def test_edges_are_valid(self, analyzer, ts_repo):
254+
"""Every edge source and target should be a known file"""
255+
graph = analyzer.build_dependency_graph(str(ts_repo))
256+
known_files = set(graph['dependencies'].keys())
257+
for edge in graph['edges']:
258+
assert edge['source'] in known_files, f"Unknown source: {edge['source']}"
259+
assert edge['target'] in known_files, f"Unknown target: {edge['target']}"
260+
261+
def test_metrics_have_required_fields(self, analyzer, ts_repo):
262+
graph = analyzer.build_dependency_graph(str(ts_repo))
263+
metrics = graph['metrics']
264+
assert 'most_critical_files' in metrics
265+
assert 'most_complex_files' in metrics
266+
assert 'avg_dependencies' in metrics
267+
assert 'total_edges' in metrics
268+
269+
270+
class TestPythonImports:
271+
"""Verify Python import extraction still works (regression test)"""
272+
273+
def test_python_from_import(self, analyzer, ts_repo):
274+
result = analyzer.analyze_file_dependencies(
275+
str(ts_repo / "backend" / "main.py")
276+
)
277+
imports = set(result['imports'])
278+
assert 'fastapi' in imports
279+
assert 'os' in imports
280+
281+
def test_python_language_detected(self, analyzer, ts_repo):
282+
result = analyzer.analyze_file_dependencies(
283+
str(ts_repo / "backend" / "main.py")
284+
)
285+
assert result['language'] == 'python'

0 commit comments

Comments
 (0)