This is an automated email from the ASF dual-hosted git repository.

FreeOnePlus pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git


The following commit(s) were added to refs/heads/master by this push:
     new 1d64994  build: separate runtime and development dependencies (#153)
1d64994 is described below

commit 1d6499499480048c54bc850d2c8634303b407851
Author: Yijia Su <[email protected]>
AuthorDate: Thu Jul 30 15:57:59 2026 +0800

    build: separate runtime and development dependencies (#153)
---
 .github/workflows/ci.yml                        |   2 +
 CHANGELOG.md                                    |   3 +
 README.md                                       |  19 +-
 generate_requirements.py                        | 272 ++++++++++++------------
 pyproject.toml                                  |  23 +-
 requirements-dev.txt                            |  27 +--
 requirements.txt                                |  29 +--
 test/deployment/check_runtime_dependencies.py   |  69 ++++++
 test/deployment/test_dependency_layers.py       | 160 ++++++++++++++
 test/deployment/test_github_actions_contract.py |   1 +
 uv.lock                                         |  39 ++--
 11 files changed, 426 insertions(+), 218 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index adb7090..d25229f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -107,6 +107,8 @@ jobs:
           "$smoke_root/venv/bin/doris-mcp-client" --help
           "$smoke_root/venv/bin/python" -c \
             "import doris_mcp_client, doris_mcp_server"
+          "$smoke_root/venv/bin/python" \
+            "$GITHUB_WORKSPACE/test/deployment/check_runtime_dependencies.py"
 
   conformance:
     name: MCP 2026-07-28 conformance
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 31ec2a3..1e9fa59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -78,6 +78,9 @@ under **Unreleased** until a new version is selected and 
published.
   token files.
 - Added coverage gates for the protocol, authentication, and core manager
   domains at 80%, alongside a 55% whole-repository floor.
+- Separated production dependencies from test, lint, type-check, and build
+  tooling across package metadata, generated requirements, Docker, and clean
+  wheel verification.
 
 ### Fixed
 
diff --git a/README.md b/README.md
index f1cab45..5fbbbd8 100644
--- a/README.md
+++ b/README.md
@@ -262,10 +262,24 @@ cd doris-mcp-server
 
 ### 2. Install Dependencies
 
+The `dev` dependency group contains the test and quality tools used by CI:
+
+```bash
+uv sync --frozen --group dev
+```
+
+For pip-based workflows, `requirements.txt` contains production dependencies
+only. `requirements-dev.txt` includes that runtime manifest and adds the same
+lean test and quality toolchain:
+
 ```bash
-pip install -r requirements.txt
+pip install -r requirements-dev.txt
 ```
 
+Production images and runtime environments must install only the package or
+`requirements.txt`; pytest, linters, type checkers, and build backends are not
+runtime dependencies.
+
 ### 3. Configure Environment Variables
 
 Copy the `.env.example` file to `.env` and modify the settings according to 
your environment:
@@ -1451,7 +1465,8 @@ doris-mcp-server/
 ├── README.md                   # This documentation
 ├── CHANGELOG.md                 # Tagged release history and unreleased 
changes
 ├── .env.example                # Environment variables template
-├── requirements.txt            # Python dependencies
+├── requirements.txt            # Runtime-only Python dependencies
+├── requirements-dev.txt        # Runtime plus test and quality dependencies
 ├── pyproject.toml              # Project configuration and entry points
 ├── uv.lock                     # UV package manager lock file
 ├── generate_requirements.py    # Requirements generation script
diff --git a/generate_requirements.py b/generate_requirements.py
index 755c82b..2b90186 100644
--- a/generate_requirements.py
+++ b/generate_requirements.py
@@ -16,156 +16,148 @@
 # specific language governing permissions and limitations
 # under the License.
 """
-Generate requirements.txt from pyproject.toml
-Ensures consistency in dependency management
+Generate separated runtime and development requirement manifests.
+
+``pyproject.toml`` remains the source of truth:
+
+* ``project.dependencies`` feeds the production ``requirements.txt``.
+* ``dependency-groups.dev`` feeds ``requirements-dev.txt``.
 """
 
+from __future__ import annotations
+
 import re
-import sys
+import tomllib
 from pathlib import Path
+from typing import Any
 
-import toml
+PROJECT_PATH = Path("pyproject.toml")
+RUNTIME_REQUIREMENTS_PATH = Path("requirements.txt")
+DEV_REQUIREMENTS_PATH = Path("requirements-dev.txt")
 
 
-def generate_requirements():
-    """Generate requirements.txt from pyproject.toml"""
+def load_pyproject(project_path: Path = PROJECT_PATH) -> dict[str, Any]:
+    """Load project metadata from a PEP 518 TOML document."""
 
-    # Read pyproject.toml
     try:
-        with open('pyproject.toml', encoding='utf-8') as f:
-            pyproject = toml.load(f)
-    except FileNotFoundError:
-        print("❌ pyproject.toml not found")
-        sys.exit(1)
-
-    # Get main dependencies
-    dependencies = pyproject.get('project', {}).get('dependencies', [])
-
-    # Get development dependencies
-    dev_dependencies = pyproject.get('project', 
{}).get('optional-dependencies', {}).get('dev', [])
-
-    # Generate requirements.txt
-    requirements_content = []
-
-    # Add header comment
-    requirements_content.append("# Main dependencies - auto-generated from 
pyproject.toml")
-    requirements_content.append("# Do not edit this file manually, use 'python 
generate_requirements.py' to regenerate")
-    requirements_content.append("")
-
-    # Add main dependencies
-    requirements_content.append("# === Core Dependencies ===")
-    for dep in dependencies:
-        requirements_content.append(dep)
-
-    requirements_content.append("")
-    requirements_content.append("# === Development Dependencies ===")
-    for dep in dev_dependencies:
-        requirements_content.append(dep)
-
-    # Write requirements.txt
-    with open('requirements.txt', 'w', encoding='utf-8') as f:
-        f.write('\n'.join(requirements_content))
-
-    print("✅ Generated requirements.txt")
-    print(f"   Main dependencies: {len(dependencies)} items")
-    print(f"   Dev dependencies: {len(dev_dependencies)} items")
-
-def generate_requirements_dev():
-    """Generate requirements-dev.txt (only development dependencies)"""
-
-    pyproject_path = Path("pyproject.toml")
-    if not pyproject_path.exists():
-        print("Error: pyproject.toml not found")
-        return
-
-    with open(pyproject_path, encoding='utf-8') as f:
-        data = toml.load(f)
-
-    # Get development dependencies
-    dev_dependencies = data.get('project', {}).get('optional-dependencies', 
{}).get('dev', [])
-
-    # Generate requirements-dev.txt
-    content = []
-    content.append("# Development dependencies - auto-generated from 
pyproject.toml")
-    content.append("# Installation command: pip install -r 
requirements-dev.txt")
-    content.append("")
-
-    for dep in dev_dependencies:
-        content.append(dep)
-
-    # Write file
-    dev_requirements_path = Path("requirements-dev.txt")
-    with open(dev_requirements_path, 'w', encoding='utf-8') as f:
-        f.write('\n'.join(content))
-
-    print(f"✅ Generated requirements-dev.txt ({len(dev_dependencies)} 
development dependencies)")
-
-def verify_consistency():
-    """Verify dependency consistency"""
-
-    def extract_packages_from_requirements():
-        """Extract package names from requirements.txt"""
-        packages = set()
-        try:
-            with open('requirements.txt') as f:
-                for line in f:
-                    line = line.strip()
-                    if line and not line.startswith('#'):
-                        # Extract package name (remove version)
-                        pkg = re.split(r'[>=<\[]', line)[0].strip()
-                        if pkg:
-                            packages.add(pkg.lower())
-        except FileNotFoundError:
-            print("requirements.txt not found")
-        return packages
-
-    def extract_packages_from_pyproject():
-        """Extract package names from pyproject.toml"""
-        packages = set()
-        try:
-            with open('pyproject.toml') as f:
-                data = toml.load(f)
-
-            # Get main dependencies
-            dependencies = data.get('project', {}).get('dependencies', [])
-            for dep in dependencies:
-                pkg = re.split(r'[>=<\[]', dep)[0].strip()
-                if pkg:
-                    packages.add(pkg.lower())
-
-            # Get development dependencies
-            dev_deps = data.get('project', {}).get('optional-dependencies', 
{}).get('dev', [])
-            for dep in dev_deps:
-                pkg = re.split(r'[>=<\[]', dep)[0].strip()
-                if pkg:
-                    packages.add(pkg.lower())
-
-        except FileNotFoundError:
-            print("pyproject.toml not found")
-        return packages
-
-    req_packages = extract_packages_from_requirements()
-    toml_packages = extract_packages_from_pyproject()
-
-    only_in_req = req_packages - toml_packages
-    only_in_toml = toml_packages - req_packages
-
-    if len(only_in_req) == 0 and len(only_in_toml) == 0:
-        print("✅ Dependency consistency verification passed!")
+        with project_path.open("rb") as project_file:
+            return tomllib.load(project_file)
+    except FileNotFoundError as exc:
+        raise FileNotFoundError(f"{project_path} not found") from exc
+
+
+def runtime_dependencies(pyproject: dict[str, Any]) -> list[str]:
+    """Return dependencies installed for every package consumer."""
+
+    return list(pyproject.get("project", {}).get("dependencies", []))
+
+
+def development_dependencies(pyproject: dict[str, Any]) -> list[str]:
+    """Return the lean development group used by CI and local contributors."""
+
+    return list(pyproject.get("dependency-groups", {}).get("dev", []))
+
+
+def generate_requirements(
+    project_path: Path = PROJECT_PATH,
+    output_path: Path = RUNTIME_REQUIREMENTS_PATH,
+) -> int:
+    """Generate the production-only requirements manifest."""
+
+    dependencies = runtime_dependencies(load_pyproject(project_path))
+    content = [
+        "# Runtime dependencies - auto-generated from pyproject.toml",
+        "# Do not edit manually; run `python generate_requirements.py`.",
+        "",
+        *dependencies,
+        "",
+    ]
+    output_path.write_text("\n".join(content), encoding="utf-8")
+    return len(dependencies)
+
+
+def generate_requirements_dev(
+    project_path: Path = PROJECT_PATH,
+    output_path: Path = DEV_REQUIREMENTS_PATH,
+) -> int:
+    """Generate the development manifest on top of runtime requirements."""
+
+    dependencies = development_dependencies(load_pyproject(project_path))
+    content = [
+        "# Development dependencies - auto-generated from pyproject.toml",
+        "# Install with `pip install -r requirements-dev.txt`.",
+        "",
+        "-r requirements.txt",
+        "",
+        *dependencies,
+        "",
+    ]
+    output_path.write_text("\n".join(content), encoding="utf-8")
+    return len(dependencies)
+
+
+def _manifest_dependencies(path: Path) -> set[str]:
+    return {
+        line
+        for raw_line in path.read_text(encoding="utf-8").splitlines()
+        if (line := raw_line.strip())
+        and not line.startswith("#")
+        and not line.startswith("-r ")
+    }
+
+
+def _dependency_names(dependencies: set[str]) -> set[str]:
+    return {
+        re.split(r"[<>=!~;\[]", dependency, maxsplit=1)[0].strip().lower()
+        for dependency in dependencies
+    }
+
+
+def verify_consistency(
+    project_path: Path = PROJECT_PATH,
+    runtime_path: Path = RUNTIME_REQUIREMENTS_PATH,
+    dev_path: Path = DEV_REQUIREMENTS_PATH,
+) -> bool:
+    """Verify both generated manifests against their independent source 
layers."""
+
+    pyproject = load_pyproject(project_path)
+    expected_runtime = set(runtime_dependencies(pyproject))
+    expected_dev = set(development_dependencies(pyproject))
+    actual_runtime = _manifest_dependencies(runtime_path)
+    actual_dev = _manifest_dependencies(dev_path)
+
+    problems = {
+        "runtime only in manifest": actual_runtime - expected_runtime,
+        "runtime only in pyproject": expected_runtime - actual_runtime,
+        "development only in manifest": actual_dev - expected_dev,
+        "development only in pyproject": expected_dev - actual_dev,
+        "runtime/development overlap": _dependency_names(expected_runtime)
+        & _dependency_names(expected_dev),
+    }
+    active_problems = {name: values for name, values in problems.items() if 
values}
+    if not active_problems:
+        print("✅ Dependency layer consistency verification passed!")
         return True
-    else:
-        print("⚠️  Found dependency inconsistencies:")
-        if only_in_req:
-            print(f"   Only in requirements.txt: {sorted(only_in_req)}")
-        if only_in_toml:
-            print(f"   Only in pyproject.toml: {sorted(only_in_toml)}")
-        return False
 
-if __name__ == "__main__":
-    print("🔄 Generating requirements.txt from pyproject.toml...")
-    generate_requirements()
-    generate_requirements_dev()
+    print("❌ Found dependency layer inconsistencies:")
+    for name, values in active_problems.items():
+        print(f"   {name}: {sorted(values)}")
+    return False
+
+
+def main() -> int:
+    print("🔄 Generating separated requirement manifests...")
+    runtime_count = generate_requirements()
+    dev_count = generate_requirements_dev()
+    print(f"   Runtime dependencies: {runtime_count} items")
+    print(f"   Development dependencies: {dev_count} items")
     print()
-    print("🔍 Verifying dependency consistency...")
-    verify_consistency()
+    print("🔍 Verifying dependency layer consistency...")
+    if not verify_consistency():
+        return 1
     print("✨ Completed!")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/pyproject.toml b/pyproject.toml
index 11d2ea8..355c976 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -65,7 +65,6 @@ dependencies = [
     "pydantic>=2.5.0",
     "pydantic-settings>=2.1.0",
     "jsonschema>=4.23.0,<5.0.0",
-    "toml>=0.10.0",
     "PyYAML>=6.0.0",
     "python-dotenv>=1.0.0",
     # Security and authentication
@@ -86,32 +85,29 @@ dependencies = [
     "uvicorn[standard]>=0.25.0",
     "fastapi>=0.108.0",
     "starlette>=0.27.0",
-    # Development utilities
+    # CLI and runtime utilities
     "click>=8.1.0",
     "typer>=0.9.0",
     "requests>=2.31.0",
     "tqdm>=4.66.0",
-    "pytest>=8.4.0",
-    "pytest-asyncio>=1.0.0",
-    "pytest-cov>=6.1.1",
 ]
 
 [project.optional-dependencies]
 dev = [
     # Testing
-    "pytest>=7.4.0",
-    "pytest-asyncio>=0.23.0",
-    "pytest-cov>=4.1.0",
+    "pytest>=8.4.0",
+    "pytest-asyncio>=1.0.0",
+    "pytest-cov>=6.1.1",
     "pytest-mock>=3.12.0",
     "pytest-xdist>=3.5.0",
     
     # Code quality
-    "ruff>=0.1.0",
+    "ruff>=0.11.13",
     "black>=23.12.0",
     "isort>=5.13.0",
     "flake8>=7.0.0",
-    "mypy>=1.8.0",
-    "bandit>=1.7.0",
+    "mypy>=1.16.0",
+    "bandit>=1.8.3",
     "safety>=2.3.0",
     
     # Documentation
@@ -305,6 +301,11 @@ dev = [
     "bandit>=1.8.3",
     "mypy>=1.16.0",
     "pandas-stubs>=2.2.0",
+    "pytest>=8.4.0",
+    "pytest-asyncio>=1.0.0",
+    "pytest-cov>=6.1.1",
+    "pytest-mock>=3.12.0",
+    "pytest-xdist>=3.5.0",
     "ruff>=0.11.13",
     "types-jsonschema>=4.23.0",
 ]
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 18990be..8982d85 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,20 +1,15 @@
 # Development dependencies - auto-generated from pyproject.toml
-# Installation command: pip install -r requirements-dev.txt
+# Install with `pip install -r requirements-dev.txt`.
 
-pytest>=7.4.0
-pytest-asyncio>=0.23.0
-pytest-cov>=4.1.0
+-r requirements.txt
+
+bandit>=1.8.3
+mypy>=1.16.0
+pandas-stubs>=2.2.0
+pytest>=8.4.0
+pytest-asyncio>=1.0.0
+pytest-cov>=6.1.1
 pytest-mock>=3.12.0
 pytest-xdist>=3.5.0
-ruff>=0.1.0
-black>=23.12.0
-isort>=5.13.0
-flake8>=7.0.0
-mypy>=1.8.0
-bandit>=1.7.0
-safety>=2.3.0
-sphinx>=7.2.0
-sphinx-rtd-theme>=2.0.0
-myst-parser>=2.0.0
-pre-commit>=3.6.0
-tox>=4.11.0
\ No newline at end of file
+ruff>=0.11.13
+types-jsonschema>=4.23.0
diff --git a/requirements.txt b/requirements.txt
index 670907c..1bd8783 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,6 @@
-# Main dependencies - auto-generated from pyproject.toml
-# Do not edit this file manually, use 'python generate_requirements.py' to 
regenerate
+# Runtime dependencies - auto-generated from pyproject.toml
+# Do not edit manually; run `python generate_requirements.py`.
 
-# === Core Dependencies ===
 mcp>=2.0.0,<2.1.0
 aiomysql>=0.2.0
 PyMySQL>=1.1.0
@@ -19,7 +18,7 @@ python-dateutil>=2.8.0
 orjson>=3.9.0
 pydantic>=2.5.0
 pydantic-settings>=2.1.0
-toml>=0.10.0
+jsonschema>=4.23.0,<5.0.0
 PyYAML>=6.0.0
 python-dotenv>=1.0.0
 cryptography>=41.0.0
@@ -41,25 +40,3 @@ click>=8.1.0
 typer>=0.9.0
 requests>=2.31.0
 tqdm>=4.66.0
-pytest>=8.4.0
-pytest-asyncio>=1.0.0
-pytest-cov>=6.1.1
-
-# === Development Dependencies ===
-pytest>=7.4.0
-pytest-asyncio>=0.23.0
-pytest-cov>=4.1.0
-pytest-mock>=3.12.0
-pytest-xdist>=3.5.0
-ruff>=0.1.0
-black>=23.12.0
-isort>=5.13.0
-flake8>=7.0.0
-mypy>=1.8.0
-bandit>=1.7.0
-safety>=2.3.0
-sphinx>=7.2.0
-sphinx-rtd-theme>=2.0.0
-myst-parser>=2.0.0
-pre-commit>=3.6.0
-tox>=4.11.0
\ No newline at end of file
diff --git a/test/deployment/check_runtime_dependencies.py 
b/test/deployment/check_runtime_dependencies.py
new file mode 100644
index 0000000..fa0fb3d
--- /dev/null
+++ b/test/deployment/check_runtime_dependencies.py
@@ -0,0 +1,69 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Fail when a clean runtime environment contains development-only packages."""
+
+from __future__ import annotations
+
+from importlib.metadata import PackageNotFoundError, version
+
+DEVELOPMENT_ONLY_DISTRIBUTIONS = (
+    "bandit",
+    "black",
+    "build",
+    "flake8",
+    "hatchling",
+    "isort",
+    "mypy",
+    "pre-commit",
+    "pytest",
+    "pytest-asyncio",
+    "pytest-cov",
+    "ruff",
+    "safety",
+    "sphinx",
+    "tox",
+)
+
+
+def installed_development_packages() -> dict[str, str]:
+    """Return forbidden development distributions installed in this 
interpreter."""
+
+    installed: dict[str, str] = {}
+    for distribution_name in DEVELOPMENT_ONLY_DISTRIBUTIONS:
+        try:
+            installed[distribution_name] = version(distribution_name)
+        except PackageNotFoundError:
+            continue
+    return installed
+
+
+def main() -> int:
+    installed = installed_development_packages()
+    if installed:
+        details = ", ".join(
+            f"{distribution_name}=={distribution_version}"
+            for distribution_name, distribution_version in 
sorted(installed.items())
+        )
+        print(f"Runtime environment contains development-only packages: 
{details}")
+        return 1
+
+    print("Runtime dependency boundary: PASS")
+    return 0
+
+
+if __name__ == "__main__":
+    raise SystemExit(main())
diff --git a/test/deployment/test_dependency_layers.py 
b/test/deployment/test_dependency_layers.py
new file mode 100644
index 0000000..999e2da
--- /dev/null
+++ b/test/deployment/test_dependency_layers.py
@@ -0,0 +1,160 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Contracts for production and development dependency separation."""
+
+from __future__ import annotations
+
+import re
+import tomllib
+from pathlib import Path
+
+import generate_requirements
+from test.deployment import check_runtime_dependencies
+
+REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
+PYPROJECT_PATH = REPOSITORY_ROOT / "pyproject.toml"
+RUNTIME_REQUIREMENTS_PATH = REPOSITORY_ROOT / "requirements.txt"
+DEV_REQUIREMENTS_PATH = REPOSITORY_ROOT / "requirements-dev.txt"
+DEVELOPMENT_ONLY = {
+    *check_runtime_dependencies.DEVELOPMENT_ONLY_DISTRIBUTIONS,
+    "pandas-stubs",
+    "pytest-mock",
+    "pytest-xdist",
+    "toml",
+    "types-jsonschema",
+}
+REQUIRED_CI_TOOLS = {
+    "bandit",
+    "mypy",
+    "pytest",
+    "pytest-asyncio",
+    "pytest-cov",
+    "ruff",
+}
+
+
+def _dependency_name(requirement: str) -> str:
+    return re.split(r"[<>=!~;\[]", requirement, maxsplit=1)[0].strip().lower()
+
+
+def _manifest_requirements(path: Path) -> list[str]:
+    return [
+        line
+        for raw_line in path.read_text(encoding="utf-8").splitlines()
+        if (line := raw_line.strip())
+        and not line.startswith("#")
+        and not line.startswith("-r ")
+    ]
+
+
+def _pyproject() -> dict:
+    return tomllib.loads(PYPROJECT_PATH.read_text(encoding="utf-8"))
+
+
+def test_runtime_metadata_excludes_development_and_build_tools() -> None:
+    pyproject = _pyproject()
+    runtime = {
+        _dependency_name(requirement)
+        for requirement in pyproject["project"]["dependencies"]
+    }
+
+    assert runtime.isdisjoint(DEVELOPMENT_ONLY)
+    assert pyproject["build-system"]["requires"] == ["hatchling"]
+
+
+def test_ci_development_group_owns_test_and_quality_dependencies() -> None:
+    pyproject = _pyproject()
+    development = {
+        _dependency_name(requirement)
+        for requirement in pyproject["dependency-groups"]["dev"]
+    }
+
+    assert REQUIRED_CI_TOOLS <= development
+    assert development.isdisjoint(
+        {
+            _dependency_name(requirement)
+            for requirement in pyproject["project"]["dependencies"]
+        }
+    )
+
+
+def test_generated_manifests_match_their_independent_dependency_layers() -> 
None:
+    pyproject = _pyproject()
+
+    assert (
+        _manifest_requirements(RUNTIME_REQUIREMENTS_PATH)
+        == pyproject["project"]["dependencies"]
+    )
+    assert (
+        _manifest_requirements(DEV_REQUIREMENTS_PATH)
+        == pyproject["dependency-groups"]["dev"]
+    )
+    assert "-r requirements.txt" in 
DEV_REQUIREMENTS_PATH.read_text(encoding="utf-8")
+    assert generate_requirements.verify_consistency(
+        PYPROJECT_PATH,
+        RUNTIME_REQUIREMENTS_PATH,
+        DEV_REQUIREMENTS_PATH,
+    )
+
+
+def test_requirement_generator_is_reproducible(tmp_path: Path) -> None:
+    runtime_path = tmp_path / "requirements.txt"
+    dev_path = tmp_path / "requirements-dev.txt"
+
+    generate_requirements.generate_requirements(PYPROJECT_PATH, runtime_path)
+    generate_requirements.generate_requirements_dev(PYPROJECT_PATH, dev_path)
+
+    assert runtime_path.read_bytes() == RUNTIME_REQUIREMENTS_PATH.read_bytes()
+    assert dev_path.read_bytes() == DEV_REQUIREMENTS_PATH.read_bytes()
+
+
+def test_consistency_rejects_cross_layer_package_with_different_specifiers(
+    tmp_path: Path,
+    capsys,
+) -> None:
+    project_path = tmp_path / "pyproject.toml"
+    runtime_path = tmp_path / "requirements.txt"
+    dev_path = tmp_path / "requirements-dev.txt"
+    project_path.write_text(
+        '[project]\ndependencies = ["shared>=1"]\n'
+        '[dependency-groups]\ndev = ["shared>=2"]\n',
+        encoding="utf-8",
+    )
+    runtime_path.write_text("shared>=1\n", encoding="utf-8")
+    dev_path.write_text("-r requirements.txt\nshared>=2\n", encoding="utf-8")
+
+    assert not generate_requirements.verify_consistency(
+        project_path,
+        runtime_path,
+        dev_path,
+    )
+    assert "runtime/development overlap: ['shared']" in capsys.readouterr().out
+
+
+def test_runtime_checker_reports_only_installed_development_packages(
+    monkeypatch,
+) -> None:
+    def fake_version(distribution_name: str) -> str:
+        if distribution_name == "pytest":
+            return "9.0.0"
+        raise check_runtime_dependencies.PackageNotFoundError
+
+    monkeypatch.setattr(check_runtime_dependencies, "version", fake_version)
+
+    assert check_runtime_dependencies.installed_development_packages() == {
+        "pytest": "9.0.0"
+    }
diff --git a/test/deployment/test_github_actions_contract.py 
b/test/deployment/test_github_actions_contract.py
index 36082ea..b508f9a 100644
--- a/test/deployment/test_github_actions_contract.py
+++ b/test/deployment/test_github_actions_contract.py
@@ -96,6 +96,7 @@ def 
test_quality_test_and_package_gates_cover_the_release_contract():
     assert 'doris-mcp-server" --version' in package
     assert 'doris-mcp-client" --help' in package
     assert "import doris_mcp_client, doris_mcp_server" in package
+    assert "test/deployment/check_runtime_dependencies.py" in package
 
 
 def test_conformance_gate_is_official_pinned_and_has_no_failure_baseline():
diff --git a/uv.lock b/uv.lock
index 3ee19f8..8486a54 100644
--- a/uv.lock
+++ b/uv.lock
@@ -628,9 +628,6 @@ dependencies = [
     { name = "pydantic-settings" },
     { name = "pyjwt" },
     { name = "pymysql" },
-    { name = "pytest" },
-    { name = "pytest-asyncio" },
-    { name = "pytest-cov" },
     { name = "python-dateutil" },
     { name = "python-dotenv" },
     { name = "python-jose", extra = ["cryptography"] },
@@ -642,7 +639,6 @@ dependencies = [
     { name = "starlette", version = "0.46.2", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version < '3.14'" },
     { name = "starlette", version = "1.3.1", source = { registry = 
"https://pypi.org/simple"; }, marker = "python_full_version >= '3.14'" },
     { name = "structlog" },
-    { name = "toml" },
     { name = "tqdm" },
     { name = "typer" },
     { name = "uvicorn", extra = ["standard"] },
@@ -693,6 +689,11 @@ dev = [
     { name = "bandit" },
     { name = "mypy" },
     { name = "pandas-stubs" },
+    { name = "pytest" },
+    { name = "pytest-asyncio" },
+    { name = "pytest-cov" },
+    { name = "pytest-mock" },
+    { name = "pytest-xdist" },
     { name = "ruff" },
     { name = "types-jsonschema" },
 ]
@@ -706,7 +707,7 @@ requires-dist = [
     { name = "aiomysql", specifier = ">=0.2.0" },
     { name = "aioredis", specifier = ">=2.0.0" },
     { name = "asyncio-mqtt", specifier = ">=0.16.0" },
-    { name = "bandit", marker = "extra == 'dev'", specifier = ">=1.7.0" },
+    { name = "bandit", marker = "extra == 'dev'", specifier = ">=1.8.3" },
     { name = "bcrypt", specifier = ">=4.1.0" },
     { name = "black", marker = "extra == 'dev'", specifier = ">=23.12.0" },
     { name = "cchardet", marker = "extra == 'performance'", specifier = 
">=2.1.0" },
@@ -721,7 +722,7 @@ requires-dist = [
     { name = "jaeger-client", marker = "extra == 'monitoring'", specifier = 
">=4.8.0" },
     { name = "jsonschema", specifier = ">=4.23.0,<5.0.0" },
     { name = "mcp", specifier = ">=2.0.0,<2.1.0" },
-    { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" },
+    { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.16.0" },
     { name = "myst-parser", marker = "extra == 'dev'", specifier = ">=2.0.0" },
     { name = "myst-parser", marker = "extra == 'docs'", specifier = ">=2.0.0" 
},
     { name = "numpy", specifier = ">=1.24.0" },
@@ -739,12 +740,9 @@ requires-dist = [
     { name = "pydantic-settings", specifier = ">=2.1.0" },
     { name = "pyjwt", specifier = ">=2.8.0" },
     { name = "pymysql", specifier = ">=1.1.0" },
-    { name = "pytest", specifier = ">=8.4.0" },
-    { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" },
-    { name = "pytest-asyncio", specifier = ">=1.0.0" },
-    { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = 
">=0.23.0" },
-    { name = "pytest-cov", specifier = ">=6.1.1" },
-    { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" },
+    { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.0" },
+    { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = 
">=1.0.0" },
+    { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.1.1" },
     { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.12.0" 
},
     { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.5.0" 
},
     { name = "python-dateutil", specifier = ">=2.8.0" },
@@ -754,7 +752,7 @@ requires-dist = [
     { name = "pyyaml", specifier = ">=6.0.0" },
     { name = "requests", specifier = ">=2.31.0" },
     { name = "rich", specifier = ">=13.7.0" },
-    { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" },
+    { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.11.13" },
     { name = "safety", marker = "extra == 'dev'", specifier = ">=2.3.0" },
     { name = "sphinx", marker = "extra == 'dev'", specifier = ">=7.2.0" },
     { name = "sphinx", marker = "extra == 'docs'", specifier = ">=7.2.0" },
@@ -764,7 +762,6 @@ requires-dist = [
     { name = "sqlparse", specifier = ">=0.4.4" },
     { name = "starlette", specifier = ">=0.27.0" },
     { name = "structlog", specifier = ">=23.2.0" },
-    { name = "toml", specifier = ">=0.10.0" },
     { name = "tox", marker = "extra == 'dev'", specifier = ">=4.11.0" },
     { name = "tqdm", specifier = ">=4.66.0" },
     { name = "typer", specifier = ">=0.9.0" },
@@ -779,6 +776,11 @@ dev = [
     { name = "bandit", specifier = ">=1.8.3" },
     { name = "mypy", specifier = ">=1.16.0" },
     { name = "pandas-stubs", specifier = ">=2.2.0" },
+    { name = "pytest", specifier = ">=8.4.0" },
+    { name = "pytest-asyncio", specifier = ">=1.0.0" },
+    { name = "pytest-cov", specifier = ">=6.1.1" },
+    { name = "pytest-mock", specifier = ">=3.12.0" },
+    { name = "pytest-xdist", specifier = ">=3.5.0" },
     { name = "ruff", specifier = ">=0.11.13" },
     { name = "types-jsonschema", specifier = ">=4.23.0" },
 ]
@@ -2863,15 +2865,6 @@ version = "0.22.0"
 source = { registry = "https://pypi.org/simple"; }
 sdist = { url = 
"https://files.pythonhosted.org/packages/b2/c2/db648cc10dd7d15560f2eafd92a27cd280811924696e0b4a87175fb28c94/thrift-0.22.0.tar.gz";,
 hash = 
"sha256:42e8276afbd5f54fe1d364858b6877bc5e5a4a5ed69f6a005b94ca4918fe1466", size 
= 62303, upload-time = "2025-05-23T20:49:33.309Z" }
 
-[[package]]
-name = "toml"
-version = "0.10.2"
-source = { registry = "https://pypi.org/simple"; }
-sdist = { url = 
"https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz";,
 hash = 
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size 
= 22253, upload-time = "2020-11-01T01:40:22.204Z" }
-wheels = [
-    { url = 
"https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl";,
 hash = 
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size 
= 16588, upload-time = "2020-11-01T01:40:20.672Z" },
-]
-
 [[package]]
 name = "tomlkit"
 version = "0.13.3"


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to