Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,29 @@ jobs:
# discovery to this repository: editable dependencies keep their own
# test suites and may expose same-named helper modules.
run: PYTHONPATH=. uv run --no-sync python -m pytest -q tests

exact-pin:
name: Exact pinned dependency contracts
runs-on: ubuntu-latest
timeout-minutes: 15
env:
BINANCE_EXACT_PIN_TESTS: "1"
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.11"

- name: Install frozen dependencies only
run: |
python -m pip install --upgrade pip uv
uv sync --frozen --extra test

- name: Verify installed pins and real catalog risk gates
# No sibling checkouts, editable overrides, credentials, or broker calls.
run: >-
PYTHONPATH=. uv run --no-sync python -m pytest -q
tests/test_exact_pin_install.py tests/test_strategy_runtime.py
35 changes: 35 additions & 0 deletions docs/exact_pin_ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Exact-pin CI contract

The `exact-pin` job installs only `uv sync --frozen --extra test` in a separate
GitHub-hosted runner. It does not check out sibling repositories or apply editable
overrides. The existing `test` job remains the candidate integration check,
including its same-named sibling branch support.

The opt-in installed-dependency test checks both QPK and CryptoStrategies:

- The declared full SHA agrees with the resolved `uv.lock` Git source.
- The real installed `direct_url.json` names that repository and commit, with no
editable/path install.
- The imported module belongs to the distribution's installed file list, so a
sibling source checkout cannot stand in for the pinned package.

`BINANCE_EXACT_PIN_TESTS=1` disables the runtime test module's local sibling-path
convenience. The job reuses the real catalog/runtime tests, including member-risk
approval and rejection for candidate mismatch, incomplete portfolio snapshots,
expired mandates and research-only mandates in live mode. All inputs are
synthetic; no broker account, cloud secret, execution grant or deployment is used.
These tests establish the current pinned member-risk semantics. They do not
claim that a legacy 20% cost-loss stop is enforced under member-risk approval.
Observation-only reporting added in a separate PR is outside this check's scope.

Local reproduction in a clean checkout:

```sh
uv sync --frozen --extra test
BINANCE_EXACT_PIN_TESTS=1 PYTHONPATH=. uv run --no-sync python -m pytest -q \
tests/test_exact_pin_install.py tests/test_strategy_runtime.py
```

The installed-dependency test deliberately skips in ordinary candidate tests,
where editable dependency overrides are permitted. Missing or different Git
provenance fails the exact-pin job instead of being treated as verified.
53 changes: 53 additions & 0 deletions tests/test_exact_pin_install.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Opt-in installed-dependency contract for the isolated exact-pin CI job."""

import importlib
from importlib import metadata
import json
import os
from pathlib import Path
import re
import tomllib
import unittest


ROOT = Path(__file__).resolve().parents[1]
PACKAGES = {
"quant-platform-kit": "quant_platform_kit",
"crypto-strategies": "crypto_strategies",
}


@unittest.skipUnless(os.environ.get("BINANCE_EXACT_PIN_TESTS") == "1", "isolated frozen-install job only")
class ExactPinInstallTests(unittest.TestCase):
def test_imported_dependencies_are_the_noneditable_declared_locked_commits(self):
project = tomllib.loads((ROOT / "pyproject.toml").read_text())
lock = tomllib.loads((ROOT / "uv.lock").read_text())
for name, module_name in PACKAGES.items():
with self.subTest(package=name):
declarations = [
item for item in project["project"]["dependencies"]
if item.startswith(f"{name} @ git+")
]
self.assertEqual(len(declarations), 1)
url, pin = declarations[0].split(" @ git+", 1)[1].rsplit("@", 1)
self.assertRegex(pin, re.compile(r"\A[0-9a-f]{40}\Z"))
locked = [item for item in lock["package"] if item["name"] == name]
self.assertEqual(len(locked), 1)
self.assertEqual(locked[0]["source"], {"git": f"{url}?rev={pin}#{pin}"})

dist = metadata.distribution(name)
# Read the real installed PEP 610 metadata, never a mocked version.
direct_url = json.loads(dist.read_text("direct_url.json") or "{}")
self.assertEqual(direct_url.get("url"), url)
self.assertNotIn("dir_info", direct_url, "editable/path install is not an exact Git install")
vcs = direct_url.get("vcs_info", {})
self.assertEqual(vcs.get("vcs"), "git")
self.assertEqual(vcs.get("requested_revision"), pin)
self.assertEqual(vcs.get("commit_id"), pin)

module = importlib.import_module(module_name)
imported = Path(module.__file__).resolve()
installed = {
Path(dist.locate_file(item)).resolve() for item in (dist.files or ())
}
self.assertIn(imported, installed, "import is shadowed by a sibling/source checkout")
5 changes: 4 additions & 1 deletion tests/test_strategy_runtime.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import sys
import types
import unittest
Expand All @@ -10,7 +11,9 @@
PROJECT_ROOT = Path(__file__).resolve().parents[1]
QPK_SRC = PROJECT_ROOT.parent / "QuantPlatformKit" / "src"
CRYPTO_STRATEGIES_SRC = PROJECT_ROOT.parent / "CryptoStrategies" / "src"
for path in (PROJECT_ROOT, QPK_SRC, CRYPTO_STRATEGIES_SRC):
# Exact-pin CI must use installed wheels, never sibling source checkouts.
paths = (PROJECT_ROOT,) if os.environ.get("BINANCE_EXACT_PIN_TESTS") == "1" else (PROJECT_ROOT, QPK_SRC, CRYPTO_STRATEGIES_SRC)
for path in paths:
if str(path) not in sys.path:
sys.path.insert(0, str(path))

Expand Down
12 changes: 12 additions & 0 deletions tests/test_uv_dependency_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ def test_ci_runtime_and_watchdog_use_uv_lock(self) -> None:
self.assertNotIn("requirements-lock.txt", ci)
self.assertNotIn("requirements.txt", ci)

def test_exact_pin_job_is_separate_from_candidate_overrides(self) -> None:
ci = Path(".github/workflows/ci.yml").read_text(encoding="utf-8")
candidate, exact = ci.split("\n exact-pin:", 1)
self.assertIn("uv pip install --no-deps -e external/QuantPlatformKit", candidate)
self.assertIn("uv sync --frozen --extra test", exact)
self.assertIn('BINANCE_EXACT_PIN_TESTS: "1"', exact)
self.assertIn("tests/test_exact_pin_install.py tests/test_strategy_runtime.py", exact)
self.assertNotIn("uv pip install", exact)
self.assertNotIn("external/", exact)
self.assertNotIn("GITHUB_HEAD_REF", exact)
self.assertNotIn("secrets.", exact)

def test_uv_outside_project_env_remains_callable_after_sync(self) -> None:
"""Regression for run 35540307399: uv used for sync must stay callable after it."""
with tempfile.TemporaryDirectory() as tmp:
Expand Down
Loading