diff --git a/openspec/changes/uv-workspace-packaging/tasks.md b/openspec/changes/uv-workspace-packaging/tasks.md index ca7d9e9..e6eb2c1 100644 --- a/openspec/changes/uv-workspace-packaging/tasks.md +++ b/openspec/changes/uv-workspace-packaging/tasks.md @@ -11,7 +11,7 @@ - [x] 2.1 Move the existing `cloud` package under `packages/cloud-platform` while preserving every `cloud.*` import path. - [x] 2.2 Remove `cloud*` from the root distribution's setuptools discovery and update path-sensitive composition tests or tooling. -- [ ] 2.3 Add packaging tests that identify the owning distribution for Runtime and cloud modules and reject reverse cloud dependencies. +- [x] 2.3 Add packaging tests that identify the owning distribution for Runtime and cloud modules and reject reverse cloud dependencies. ## 3. Locking And Developer Commands diff --git a/tests/test_workspace_packaging.py b/tests/test_workspace_packaging.py new file mode 100644 index 0000000..2ced277 --- /dev/null +++ b/tests/test_workspace_packaging.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import ast +import re +import tomllib +from importlib.metadata import packages_distributions +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +RUNTIME_PACKAGE_DIRS = ( + "agents", + "api", + "core", + "device", + "driver", + "perception", + "runtime", + "semantic", + "skills_learning", + "storage", + "tools", + "workflow", + "world", +) + + +def test_workspace_distributions_own_expected_import_packages() -> None: + distributions = packages_distributions() + + assert distributions.get("cloud") == ["device-cloud-platform"] + assert distributions.get("core") == ["device-agent-runtime"] + assert distributions.get("runtime") == ["device-agent-runtime"] + + +def test_runtime_project_does_not_declare_cloud_distribution() -> None: + project = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text("utf-8")) + dependencies = { + _requirement_name(requirement) + for requirement in project["project"].get("dependencies", []) + } + package_patterns = project["tool"]["setuptools"]["packages"]["find"]["include"] + + assert "device-cloud-platform" not in dependencies + assert "cloud*" not in package_patterns + + +def test_runtime_source_does_not_import_cloud() -> None: + offenders: list[str] = [] + for package_dir in RUNTIME_PACKAGE_DIRS: + for path in (PROJECT_ROOT / package_dir).rglob("*.py"): + tree = ast.parse(path.read_text("utf-8"), filename=str(path)) + for node in ast.walk(tree): + if isinstance(node, ast.Import): + names = [alias.name for alias in node.names] + elif isinstance(node, ast.ImportFrom) and node.module: + names = [node.module] + else: + continue + if any(name == "cloud" or name.startswith("cloud.") for name in names): + offenders.append(str(path.relative_to(PROJECT_ROOT))) + + assert not offenders, f"runtime distribution imports cloud: {sorted(set(offenders))}" + + +def _requirement_name(requirement: str) -> str: + name = re.split(r"[\s<>=!~\[]", requirement, maxsplit=1)[0] + return name.lower().replace("_", "-")