diff --git a/pyproject.toml b/pyproject.toml index ce1b8b3da..88b39df4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -237,7 +237,7 @@ plugins = [ ] [tool.setuptools.packages.find] -include = ["agent", "agent.*", "tools", "tools.*", "hermes_cli", "gateway", "gateway.*", "tui_gateway", "tui_gateway.*", "cron", "acp_adapter", "plugins", "plugins.*", "providers", "providers.*"] +include = ["agent", "agent.*", "tools", "tools.*", "hermes_cli", "hermes_cli.*", "gateway", "gateway.*", "tui_gateway", "tui_gateway.*", "cron", "acp_adapter", "plugins", "plugins.*", "providers", "providers.*"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/tests/test_packaging_metadata.py b/tests/test_packaging_metadata.py index b53355f1c..89d10f03b 100644 --- a/tests/test_packaging_metadata.py +++ b/tests/test_packaging_metadata.py @@ -1,10 +1,59 @@ from pathlib import Path import tomllib +from setuptools import find_packages + REPO_ROOT = Path(__file__).resolve().parents[1] +def _packages_find_include(): + data = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) + return data["tool"]["setuptools"]["packages"]["find"]["include"] + + +def test_every_on_disk_subpackage_is_covered_by_packages_find(): + """Regression test for #34701 (and the bug class behind #34034 / #28149). + + ``[tool.setuptools.packages.find]`` ``include`` is hand-maintained. Every + top-level package is listed twice — bare (``hermes_cli``) for the package + itself and ``hermes_cli.*`` for its subpackages — EXCEPT when someone + forgets the wildcard. v0.15.x listed ``hermes_cli`` without ``hermes_cli.*``, + so the wheel shipped ``hermes_cli/*.py`` but dropped the ``dashboard_auth`` + and ``proxy`` subpackages. The dashboard then died on every install with + ``ModuleNotFoundError: No module named 'hermes_cli.dashboard_auth'``. + + This drives setuptools' own discovery against the live tree: every package + that exists on disk and would be found by a permissive ``.*`` scan + must also be found by the actual ``include`` list. A subpackage added under + any listed package without the matching wildcard fails here instead of in a + user's container. + """ + include = _packages_find_include() + + # What the real include list actually selects. + selected = set(find_packages(where=str(REPO_ROOT), include=include)) + + # Top-level packages we ship (bare names in the include list, no wildcard). + top_level = sorted({name for name in include if "." not in name}) + + # For each shipped top-level package, every on-disk subpackage must be + # covered by the include list. + expected = set( + find_packages( + where=str(REPO_ROOT), + include=[pattern for name in top_level for pattern in (name, f"{name}.*")], + ) + ) + + missing = sorted(expected - selected) + assert not missing, ( + "These packages exist on disk but are dropped from the wheel because " + "[tool.setuptools.packages.find] include is missing a wildcard. Add the " + f"matching '.*' entry in pyproject.toml: {missing}" + ) + + def test_faster_whisper_is_not_a_base_dependency(): data = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) deps = data["project"]["dependencies"]