| """No-GPU correctness tests for the static-analysis engine. |
| |
| These run with pydantic (+ tree-sitter if present) only — no model, no network. |
| They lock down the *facts* every story is grounded in: import edges, fan-in/out, |
| roles, entry points, and the safe-to-edit verdicts. If these pass, the narration |
| layer has a trustworthy backbone. |
| |
| Run: python -m pytest tests/ -q (or) python tests/test_analyzer.py |
| """ |
| from __future__ import annotations |
|
|
| import os |
| import sys |
|
|
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
|
|
| import config |
| from analyzer import analyze_project |
| from analyzer import deps as deps_mod |
| from ingest import from_folder, redact_secrets |
|
|
| SAMPLE = os.path.join(os.path.dirname(os.path.abspath(__file__)), |
| "..", "scripts", "sample_project") |
|
|
|
|
| def _model(): |
| return analyze_project(from_folder(SAMPLE, name="doc-qa")) |
|
|
|
|
| def test_all_files_found(): |
| m = _model() |
| paths = {f.path for f in m.files} |
| expected = {"app.py", "config.py", "embed.py", "ingest.py", |
| "retrieve.py", "generate.py"} |
| assert expected <= paths, f"missing files: {expected - paths}" |
|
|
|
|
| def test_edges_resolved(): |
| m = _model() |
| edges = {f.path: set(f.depends_on) for f in m.files} |
| assert edges["embed.py"] == {"config.py"} |
| assert edges["ingest.py"] == {"config.py", "embed.py"} |
| assert edges["retrieve.py"] == {"config.py", "embed.py", "ingest.py"} |
| assert edges["generate.py"] == {"config.py", "retrieve.py"} |
| assert edges["app.py"] == {"generate.py", "ingest.py"} |
| assert edges["config.py"] == set() |
|
|
|
|
| def test_fan_in(): |
| m = _model() |
| fan_in = {f.path: f.fan_in for f in m.files} |
| assert fan_in["config.py"] == 4 |
| assert fan_in["embed.py"] == 2 |
| assert fan_in["ingest.py"] == 2 |
| assert fan_in["retrieve.py"] == 1 |
| assert fan_in["generate.py"] == 1 |
| assert fan_in["app.py"] == 0 |
|
|
|
|
| def test_entry_point(): |
| m = _model() |
| assert m.entry_points == ["app.py"] |
| assert m.by_path("app.py").is_entry is True |
|
|
|
|
| def test_roles(): |
| m = _model() |
| role = {f.path: f.role for f in m.files} |
| assert role["app.py"] == "entry" |
| assert role["config.py"] == "config" |
| assert role["embed.py"] == "data" |
| assert role["ingest.py"] == "data" |
|
|
|
|
| def test_safe_to_edit(): |
| m = _model() |
| safety = {f.path: f.safety for f in m.files} |
| assert safety["app.py"] == config.DANGER |
| assert safety["config.py"] == config.DANGER |
| assert safety["embed.py"] == config.CAREFUL |
| assert safety["retrieve.py"] == config.CAREFUL |
| |
| assert all(f.safety_reason for f in m.files) |
|
|
|
|
| def test_symbols_extracted(): |
| m = _model() |
| embed = m.by_path("embed.py") |
| names = {s.name for s in embed.symbols} |
| assert {"embed_text", "embed_many"} <= names |
|
|
|
|
| def test_dependencies_explained(): |
| m = _model() |
| by_name = {d.name.lower(): d for d in m.deps} |
| assert "openai" in by_name |
| assert "chromadb" in by_name |
| assert by_name["chromadb"].plain |
| assert "memory" in by_name["chromadb"].plain.lower() |
|
|
|
|
| def test_secret_redaction(): |
| clean, hits = redact_secrets('API_KEY = "sk-abc123def456ghi789jkl012"') |
| assert "sk-abc123" not in clean |
| assert hits |
|
|
|
|
| def test_deps_known_map_direct(): |
| out = deps_mod.parse_requirements("gradio==4.44\nopenai\n# comment\n") |
| names = {d.name for d in out} |
| assert names == {"gradio", "openai"} |
|
|
|
|
| if __name__ == "__main__": |
| fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)] |
| passed = 0 |
| for fn in fns: |
| try: |
| fn() |
| except AssertionError as exc: |
| print(f"FAIL {fn.__name__}: {exc}") |
| except Exception as exc: |
| print(f"ERROR {fn.__name__}: {type(exc).__name__}: {exc}") |
| else: |
| print(f"ok {fn.__name__}") |
| passed += 1 |
| print(f"\n{passed}/{len(fns)} passed") |
| sys.exit(0 if passed == len(fns) else 1) |
|
|