Files
2026-07-13 13:12:33 +08:00

152 lines
5.1 KiB
Python

"""Invariants for pyproject.toml after the channel install contract cleanup.
Real-used channel SDKs stay in base dependencies. Base-install channels
must not be re-exposed as empty optional extras, because package metadata
is also a user-visible install contract.
"""
from __future__ import annotations
import tomllib
from pathlib import Path
import pytest
PYPROJECT = Path(__file__).resolve().parents[2] / "pyproject.toml"
UV_LOCK = Path(__file__).resolve().parents[2] / "uv.lock"
@pytest.fixture(scope="module")
def project_table() -> dict:
data = tomllib.loads(PYPROJECT.read_text(encoding="utf-8"))
return data["project"]
@pytest.fixture(scope="module")
def lock_package() -> dict:
data = tomllib.loads(UV_LOCK.read_text(encoding="utf-8"))
return next(package for package in data["package"] if package["name"] == "opensquilla")
def _dep_names(specs: list[str]) -> set[str]:
"""Extract canonical (lowercased) package names from a list of PEP 508 specs."""
names: set[str] = set()
for spec in specs:
head = spec.strip()
for sep in ("[", " ", ";", "=", ">", "<", "~", "!"):
head = head.split(sep, 1)[0]
if head:
names.add(head.lower())
return names
def test_channel_sdks_in_base(project_table: dict) -> None:
"""Channel adapters that ``import`` a vendor SDK must keep that SDK in base."""
base = _dep_names(project_table["dependencies"])
required = {
"lark-oapi",
"python-telegram-bot",
"dingtalk-stream",
"qq-botpy",
"cryptography",
}
missing = required - base
assert not missing, f"channel SDKs missing from base deps: {sorted(missing)}"
def test_no_dead_extras(project_table: dict) -> None:
"""msteams extra is intentionally absent; matrix extra installs matrix-nio."""
extras = project_table.get("optional-dependencies", {})
assert "msteams" not in extras, (
"msteams extra stays absent: the adapter is text-only and not advertised"
)
assert "matrix" in extras, "matrix extra must exist and pull matrix-nio"
matrix_specs = extras["matrix"]
assert any("matrix-nio" in spec for spec in matrix_specs), (
"matrix extra must declare matrix-nio (without [e2e]); use matrix-e2e for E2EE"
)
def test_base_channel_extras_are_not_exposed_as_noop_aliases(
project_table: dict,
) -> None:
"""Base-install channels must not be exposed as no-op extras."""
extras = project_table.get("optional-dependencies", {})
for name in ("feishu", "telegram", "dingtalk", "wecom", "qq"):
assert name not in extras, f"{name} is installed from base; do not expose a no-op extra"
def test_lockfile_does_not_advertise_removed_base_channel_extras(
lock_package: dict,
) -> None:
"""uv.lock metadata must match the package install contract."""
provides_extras = set(lock_package.get("provides-extras", []))
for name in ("feishu", "telegram", "dingtalk", "wecom", "qq"):
assert name not in provides_extras
def test_no_duplicate_ml_extra(project_table: dict) -> None:
"""``recommended`` and ``model-router`` historically overlapped — only one survives."""
extras = project_table.get("optional-dependencies", {})
has_recommended = "recommended" in extras
has_model_router = "model-router" in extras
assert has_recommended, "recommended extra must exist (router users opt in here)"
assert not has_model_router, (
"model-router extra duplicates recommended — collapse into one"
)
def test_alpha_classifier_present(project_table: dict) -> None:
"""0.1.0 stays pre-stable — the classifier must reflect that."""
classifiers = project_table.get("classifiers", [])
assert "Development Status :: 3 - Alpha" in classifiers, (
"Alpha classifier signals to PyPI/uv that this is pre-stable"
)
def test_readme_points_at_user_facing_file(project_table: dict) -> None:
"""``readme`` must be the user-facing README, not the legacy portable view."""
assert project_table["readme"] == "README.md", (
"readme should point at the canonical README.md after the 0.1.0 refactor"
)
def test_html_coder_reference_files_are_packaged() -> None:
"""html-coder's SKILL.md links to local references that must survive wheels."""
data = tomllib.loads(PYPROJECT.read_text(encoding="utf-8"))
force_include = data["tool"]["hatch"]["build"]["targets"]["wheel"][
"force-include"
]
references_dir = (
PYPROJECT.parent
/ "src"
/ "opensquilla"
/ "skills"
/ "bundled"
/ "html-coder"
/ "references"
)
expected_sources = {
path.relative_to(PYPROJECT.parent).as_posix()
for path in references_dir.glob("*.md")
}
missing_sources = expected_sources - set(force_include)
assert not missing_sources, (
"html-coder reference files must be force-included in wheels: "
f"{sorted(missing_sources)}"
)
for source in expected_sources:
assert (
force_include[source] == source.removeprefix("src/")
), f"{source} should be packaged at its import-time skill path"