|
16 | 16 |
|
17 | 17 | """Tests for the TypeScript analysis facade (backend subprocess mocked).""" |
18 | 18 |
|
| 19 | +import json |
19 | 20 | from unittest.mock import MagicMock, patch |
20 | 21 |
|
21 | 22 | import networkx as nx |
@@ -64,7 +65,7 @@ def test_phantom_external_nodes(ts_analysis): |
64 | 65 | ext = ts_analysis.get_external_symbols() |
65 | 66 | assert "node:crypto.createHash" in ext |
66 | 67 | assert ext["node:crypto.createHash"].module == "node:crypto" |
67 | | - assert ext["node:crypto.createHash"].is_external is True |
| 68 | + assert ext["node:crypto.createHash"].name == "createHash" |
68 | 69 | assert "node:path.extname" in ext |
69 | 70 |
|
70 | 71 | graph = ts_analysis.get_call_graph() |
@@ -169,6 +170,46 @@ def test_exports_and_variables(ts_analysis): |
169 | 170 | assert set(variables) == set(ts_analysis.get_symbol_table()) |
170 | 171 |
|
171 | 172 |
|
| 173 | +def test_exports_and_variables_are_parsed(typescript_application, typescript_analysis_json, monkeypatch): |
| 174 | + """Behavioral: a real export + module-level const must be parsed and surfaced (the slim |
| 175 | + fixture carries none, so inject them into the analyzed JSON).""" |
| 176 | + data = json.loads(typescript_analysis_json) |
| 177 | + models = data["symbol_table"]["src/models.ts"] |
| 178 | + models["exports"].append( |
| 179 | + {"name": "User", "module": None, "alias": None, "is_type_only": False, "export_kind": "named"} |
| 180 | + ) |
| 181 | + models["variables"].append( |
| 182 | + { |
| 183 | + "name": "DEFAULT_ROLE", |
| 184 | + "type": "Role", |
| 185 | + "initializer": "Role.Member", |
| 186 | + "scope": "module", |
| 187 | + "declaration_kind": "const", |
| 188 | + "is_exported": True, |
| 189 | + } |
| 190 | + ) |
| 191 | + |
| 192 | + monkeypatch.setenv("CODEANALYZER_TS_BIN", "codeanalyzer-typescript") |
| 193 | + with patch("cldk.analysis.typescript.codeanalyzer.codeanalyzer.subprocess.run") as run_mock: |
| 194 | + run_mock.return_value = MagicMock(stdout=json.dumps(data), returncode=0) |
| 195 | + analysis = CLDK(language="typescript").analysis( |
| 196 | + project_path=typescript_application, |
| 197 | + analysis_backend_path=None, |
| 198 | + eager=True, |
| 199 | + analysis_level=AnalysisLevel.call_graph, |
| 200 | + ) |
| 201 | + |
| 202 | + exported = analysis.get_exports()["src/models.ts"] |
| 203 | + assert [e.name for e in exported] == ["User"] |
| 204 | + assert exported[0].export_kind == "named" |
| 205 | + |
| 206 | + variables = analysis.get_variables()["src/models.ts"] |
| 207 | + assert [v.name for v in variables] == ["DEFAULT_ROLE"] |
| 208 | + assert variables[0].declaration_kind == "const" |
| 209 | + assert variables[0].initializer == "Role.Member" |
| 210 | + assert variables[0].is_exported is True |
| 211 | + |
| 212 | + |
172 | 213 | def test_class_decorators(ts_analysis): |
173 | 214 | decos = ts_analysis.get_class_decorators("src/controllers.UserController") |
174 | 215 | assert [d.name for d in decos] == ["Controller"] |
@@ -209,3 +250,79 @@ def test_source_code_mode_rejected(typescript_application): |
209 | 250 | def test_python_only_kwargs_rejected(typescript_application): |
210 | 251 | with pytest.raises(CldkInitializationException): |
211 | 252 | CLDK(language="typescript").analysis(project_path=typescript_application, cache_dir="/tmp/x") |
| 253 | + |
| 254 | + |
| 255 | +# -----[ facade -> backend wiring / subprocess invocation ]----- |
| 256 | +def test_subprocess_args_stdout_pipe(typescript_application, typescript_analysis_json, monkeypatch): |
| 257 | + """The facade must forward target_files + analysis_level to the backend, which builds the |
| 258 | + right subprocess command. With no analysis_json_path, output is read from the stdout pipe |
| 259 | + (no ``-o``).""" |
| 260 | + monkeypatch.setenv("CODEANALYZER_TS_BIN", "codeanalyzer-typescript") |
| 261 | + with patch("cldk.analysis.typescript.codeanalyzer.codeanalyzer.subprocess.run") as run_mock: |
| 262 | + run_mock.return_value = MagicMock(stdout=typescript_analysis_json, returncode=0) |
| 263 | + CLDK(language="typescript").analysis( |
| 264 | + project_path=typescript_application, |
| 265 | + analysis_backend_path=None, |
| 266 | + eager=True, |
| 267 | + analysis_level=AnalysisLevel.call_graph, |
| 268 | + target_files=["src/models.ts", "src/services.ts"], |
| 269 | + ) |
| 270 | + |
| 271 | + args = run_mock.call_args.args[0] |
| 272 | + assert args[0] == "codeanalyzer-typescript" # resolved from $CODEANALYZER_TS_BIN |
| 273 | + assert args[args.index("-i") + 1] == str(typescript_application) |
| 274 | + assert args[args.index("-a") + 1] == "2" # call_graph -> level 2 |
| 275 | + # each target file forwarded with its own -t |
| 276 | + assert args.count("-t") == 2 |
| 277 | + assert "src/models.ts" in args and "src/services.ts" in args |
| 278 | + # stdout-pipe mode: no output directory |
| 279 | + assert "-o" not in args |
| 280 | + |
| 281 | + |
| 282 | +def test_subprocess_args_output_dir(typescript_application, typescript_analysis_json, tmp_path, monkeypatch): |
| 283 | + """With analysis_json_path set, the backend passes ``-o <dir>`` and reads analysis.json back |
| 284 | + from disk. Exercises the output-dir branch and level-1 (symbol_table) mapping.""" |
| 285 | + monkeypatch.setenv("CODEANALYZER_TS_BIN", "codeanalyzer-typescript") |
| 286 | + out_dir = tmp_path / "out" |
| 287 | + out_dir.mkdir() |
| 288 | + |
| 289 | + def fake_run(cmd, *a, **kw): |
| 290 | + # the analyzer writes analysis.json into the -o directory |
| 291 | + (out_dir / "analysis.json").write_text(typescript_analysis_json, encoding="utf-8") |
| 292 | + return MagicMock(stdout="", returncode=0) |
| 293 | + |
| 294 | + with patch("cldk.analysis.typescript.codeanalyzer.codeanalyzer.subprocess.run", side_effect=fake_run) as run_mock: |
| 295 | + analysis = CLDK(language="typescript").analysis( |
| 296 | + project_path=typescript_application, |
| 297 | + analysis_backend_path=None, |
| 298 | + eager=True, |
| 299 | + analysis_level=AnalysisLevel.symbol_table, |
| 300 | + analysis_json_path=out_dir, |
| 301 | + ) |
| 302 | + |
| 303 | + args = run_mock.call_args.args[0] |
| 304 | + assert args[args.index("-a") + 1] == "1" # symbol_table -> level 1 |
| 305 | + assert args[args.index("-o") + 1] == str(out_dir) |
| 306 | + # the disk read-back path produced a usable application |
| 307 | + assert len(analysis.get_symbol_table()) == 6 |
| 308 | + |
| 309 | + |
| 310 | +def test_cached_analysis_json_skips_subprocess(typescript_application, typescript_analysis_json, tmp_path, monkeypatch): |
| 311 | + """When a cached analysis.json already exists and eager is False, the backend must reuse it |
| 312 | + and not invoke the analyzer subprocess.""" |
| 313 | + monkeypatch.setenv("CODEANALYZER_TS_BIN", "codeanalyzer-typescript") |
| 314 | + out_dir = tmp_path / "out" |
| 315 | + out_dir.mkdir() |
| 316 | + (out_dir / "analysis.json").write_text(typescript_analysis_json, encoding="utf-8") |
| 317 | + |
| 318 | + with patch("cldk.analysis.typescript.codeanalyzer.codeanalyzer.subprocess.run") as run_mock: |
| 319 | + analysis = CLDK(language="typescript").analysis( |
| 320 | + project_path=typescript_application, |
| 321 | + analysis_backend_path=None, |
| 322 | + eager=False, |
| 323 | + analysis_level=AnalysisLevel.call_graph, |
| 324 | + analysis_json_path=out_dir, |
| 325 | + ) |
| 326 | + |
| 327 | + run_mock.assert_not_called() |
| 328 | + assert len(analysis.get_symbol_table()) == 6 |
0 commit comments