diff --git a/src/backend/tests/test_vision_boundary.py b/src/backend/tests/test_vision_boundary.py index b230fa0..7635c8c 100644 --- a/src/backend/tests/test_vision_boundary.py +++ b/src/backend/tests/test_vision_boundary.py @@ -8,18 +8,54 @@ import ast from pathlib import Path BACKEND = Path(__file__).resolve().parents[1] -FORBIDDEN = {"visionsuite", "pm2d", "dxf_compare", "Acquire", "torch"} + +# Full dotted prefixes, not bare roots: "src" alone would forbid every import +# the backend legitimately makes of itself (src.backend.*), so the entries +# for the vision tree are multi-segment ("src.vision", "src.vision_worker") +# while VisionSuite's own top-level packages stay single-segment. +# +# M2: "Acquire" used to sit here as a stand-in for vs-camera, but it is a +# class/SDK name (Balluff's "mvIMPACT Acquire" SDK), never an import root - +# it could never match. vs-camera imports under the same `visionsuite` +# namespace as vs-core (see +# vendor/visionsuite/packages/vs-camera/pyproject.toml: "il codice si importa +# come visionsuite.camera...."), which "visionsuite" below already forbids, +# so there is nothing separate to add for it. +FORBIDDEN = { + "visionsuite", "pm2d", "dxf_compare", "torch", + "src.vision", "src.vision_worker", +} -def _imported_roots(source: Path) -> set[str]: +def _imported_modules(source: Path) -> set[str]: + """Full dotted module paths this file imports - not just their first + segment. `from src.vision.runner import ...` must be checked against + "src.vision.runner" (inside the forbidden "src.vision"), not against + "src" alone, which is never forbidden since the backend imports itself + constantly. + """ tree = ast.parse(source.read_text(encoding="utf-8"), filename=str(source)) - roots: set[str] = set() + modules: set[str] = set() for node in ast.walk(tree): if isinstance(node, ast.Import): - roots.update(alias.name.split(".")[0] for alias in node.names) + modules.update(alias.name for alias in node.names) elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0: - roots.add(node.module.split(".")[0]) - return roots + modules.add(node.module) + return modules + + +def _forbidden_hits(modules: set[str]) -> set[str]: + """Which FORBIDDEN entries a set of imported module paths triggers. + + A module counts as forbidden if it equals a forbidden entry exactly, or + lives inside it ("src.vision.runner" is inside "src.vision"). + """ + hits: set[str] = set() + for module in modules: + for entry in FORBIDDEN: + if module == entry or module.startswith(entry + "."): + hits.add(entry) + return hits def test_the_server_never_imports_visionsuite(): @@ -27,7 +63,7 @@ def test_the_server_never_imports_visionsuite(): for source in BACKEND.rglob("*.py"): if "tests" in source.parts or "migrations" in source.parts: continue - forbidden = _imported_roots(source) & FORBIDDEN + forbidden = _forbidden_hits(_imported_modules(source)) if forbidden: offenders.append(f"{source.relative_to(BACKEND)}: {sorted(forbidden)}") @@ -42,3 +78,29 @@ def test_the_submodule_is_pinned(): gitmodules = Path(__file__).resolve().parents[3] / ".gitmodules" assert gitmodules.exists(), "vendor/visionsuite non รจ un sottomodulo" assert "vendor/visionsuite" in gitmodules.read_text(encoding="utf-8") + + +def test_a_full_module_path_import_is_caught_not_just_its_root(tmp_path): + """M2: `from src.vision.runner import ...` inside src/backend/** must be + caught. Checking only the first dotted segment ("src") would miss it - + "src" itself is never forbidden, the backend imports itself all the time. + This is the blind spot the reviewer found: same repository, no install + needed, the most tempting shortcut of all. + """ + offender = tmp_path / "sneaky.py" + offender.write_text("from src.vision.runner import run_graph\n") + assert _forbidden_hits(_imported_modules(offender)) == {"src.vision"} + + +def test_a_vision_worker_import_is_also_caught(tmp_path): + offender = tmp_path / "sneaky.py" + offender.write_text("import src.vision_worker.main\n") + assert _forbidden_hits(_imported_modules(offender)) == {"src.vision_worker"} + + +def test_an_unrelated_src_backend_import_is_not_caught(tmp_path): + """The fix must not turn every self-import of the backend into an + offender - only the vision subtrees are forbidden.""" + innocent = tmp_path / "innocent.py" + innocent.write_text("from src.backend.config import settings\n") + assert _forbidden_hits(_imported_modules(innocent)) == set()