diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f18031..f74978a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,66 +24,6 @@ jobs: exit 1 fi - - name: Resolve QuantPlatformKit ref - id: quant-platform-kit-ref - run: | - set -euo pipefail - ref="main" - for candidate in "${GITHUB_HEAD_REF:-}" "${GITHUB_BASE_REF:-}"; do - if [ -n "$candidate" ] && git ls-remote --exit-code --heads https://github.com/QuantStrategyLab/QuantPlatformKit.git "$candidate" >/dev/null 2>&1; then - ref="$candidate" - break - fi - done - echo "ref=${ref}" >> "$GITHUB_OUTPUT" - - - name: Resolve UsEquityStrategies ref - id: us-equity-strategies-ref - run: | - set -euo pipefail - ref="main" - for candidate in "${GITHUB_HEAD_REF:-}" "${GITHUB_BASE_REF:-}"; do - if [ -n "$candidate" ] && git ls-remote --exit-code --heads https://github.com/QuantStrategyLab/UsEquityStrategies.git "$candidate" >/dev/null 2>&1; then - ref="$candidate" - break - fi - done - echo "ref=${ref}" >> "$GITHUB_OUTPUT" - - - name: Resolve UsEquitySnapshotPipelines ref - id: us-equity-snapshot-pipelines-ref - run: | - set -euo pipefail - ref="main" - for candidate in "${GITHUB_HEAD_REF:-}" "${GITHUB_BASE_REF:-}"; do - if [ -n "$candidate" ] && git ls-remote --exit-code --heads https://github.com/QuantStrategyLab/UsEquitySnapshotPipelines.git "$candidate" >/dev/null 2>&1; then - ref="$candidate" - break - fi - done - echo "ref=${ref}" >> "$GITHUB_OUTPUT" - - - name: Checkout QuantPlatformKit - uses: actions/checkout@v6 - with: - repository: QuantStrategyLab/QuantPlatformKit - ref: ${{ steps.quant-platform-kit-ref.outputs.ref }} - path: external/QuantPlatformKit - - - name: Checkout UsEquityStrategies - uses: actions/checkout@v6 - with: - repository: QuantStrategyLab/UsEquityStrategies - ref: ${{ steps.us-equity-strategies-ref.outputs.ref }} - path: external/UsEquityStrategies - - - name: Checkout UsEquitySnapshotPipelines - uses: actions/checkout@v6 - with: - repository: QuantStrategyLab/UsEquitySnapshotPipelines - ref: ${{ steps.us-equity-snapshot-pipelines-ref.outputs.ref }} - path: external/UsEquitySnapshotPipelines - - name: Setup Python uses: actions/setup-python@v6 with: @@ -111,11 +51,6 @@ jobs: - name: Validate production Cloud Run startup run: uv run --no-sync python scripts/validate_cloud_run_startup.py - - name: Install editable shared repositories - run: | - set -euo pipefail - uv pip install --no-deps -e external/QuantPlatformKit -e external/UsEquityStrategies -e external/UsEquitySnapshotPipelines - - name: Run ruff run: | set -euo pipefail @@ -124,9 +59,7 @@ jobs: - name: Check QPK pin consistency run: | set -euo pipefail - uv run --no-sync python external/QuantPlatformKit/scripts/check_qpk_pin_consistency.py \ - --root . \ - --pin-file external/QuantPlatformKit/QPK_PIN + QPK_EXPECTED_PIN=b13e28759a880dcb446dbfbc580dc032333b065e uv run --no-sync python scripts/check_qpk_pin_consistency.py - name: Ensure uv.lock matches pyproject.toml run: uv lock --check @@ -134,4 +67,4 @@ jobs: - name: Run unit tests run: | set -euo pipefail - PYTHONPATH=. PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 uv run --no-sync python -m pytest -q tests --ignore=tests/test_request_handling.py --ignore=tests/test_event_loop.py --ignore=tests/test_monitor_dispatcher.py --ignore=tests/test_notifications.py --ignore=tests/test_connect_timeout_alert.py + PYTHONPATH=. PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 uv run --no-sync python -m pytest -q tests diff --git a/scripts/check_qpk_pin_consistency.py b/scripts/check_qpk_pin_consistency.py index cde69e7..105facd 100644 --- a/scripts/check_qpk_pin_consistency.py +++ b/scripts/check_qpk_pin_consistency.py @@ -10,6 +10,7 @@ """ from __future__ import annotations +import os import re import subprocess import sys @@ -30,6 +31,10 @@ def _extract_pin(raw: str) -> str: def fetch_pin() -> str: + configured = os.getenv("QPK_EXPECTED_PIN") + if configured: + return _extract_pin(configured) + errors: list[str] = [] try: with urllib.request.urlopen(QPK_PIN_URL, timeout=10) as response: diff --git a/tests/test_ci_unit_test_contract.py b/tests/test_ci_unit_test_contract.py index e2c93e2..229172b 100644 --- a/tests/test_ci_unit_test_contract.py +++ b/tests/test_ci_unit_test_contract.py @@ -2,17 +2,10 @@ from pathlib import Path -_LEGACY_FULL_SUITE_COMMAND = ( - "PYTHONPATH=. PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 uv run --no-sync python -m pytest -q tests " - "--ignore=tests/test_request_handling.py " - "--ignore=tests/test_event_loop.py " - "--ignore=tests/test_monitor_dispatcher.py " - "--ignore=tests/test_notifications.py " - "--ignore=tests/test_connect_timeout_alert.py" -) +_FULL_SUITE_COMMAND = "PYTHONPATH=. PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 uv run --no-sync python -m pytest -q tests" -def test_ci_runs_unsuppressed_legacy_full_suite() -> None: +def test_ci_runs_unsuppressed_full_suite() -> None: workflow = Path(".github/workflows/ci.yml").read_text(encoding="utf-8") match = re.search( r"^ - name: Run unit tests\n(?P.*)\Z", @@ -22,6 +15,7 @@ def test_ci_runs_unsuppressed_legacy_full_suite() -> None: assert match is not None step = match.group("block") - assert _LEGACY_FULL_SUITE_COMMAND in step + assert _FULL_SUITE_COMMAND in step + assert "--ignore=" not in step assert "|| true" not in step assert "continue-on-error" not in step diff --git a/tests/test_dependency_pin_guard.py b/tests/test_dependency_pin_guard.py index 9cbe5f5..4681501 100644 --- a/tests/test_dependency_pin_guard.py +++ b/tests/test_dependency_pin_guard.py @@ -55,6 +55,24 @@ def test_dependency_pin_guard_rejects_internal_qsl_git_ref_drift(tmp_path, monke assert "inconsistent QuantStrategyLab dependency pin for UsEquityStrategies" in output +def test_dependency_pin_guard_prefers_explicit_qpk_pin(monkeypatch) -> None: + module = _load_guard_module() + expected_ref = "a" * 40 + monkeypatch.setenv("QPK_EXPECTED_PIN", expected_ref) + monkeypatch.setattr( + module.urllib.request, + "urlopen", + lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("network must not be used")), + ) + monkeypatch.setattr( + module.subprocess, + "check_output", + lambda *_args, **_kwargs: (_ for _ in ()).throw(AssertionError("network must not be used")), + ) + + assert module.fetch_pin() == expected_ref + + def test_dependency_pin_guard_is_blocking_in_ci() -> None: workflow = CI_WORKFLOW.read_text(encoding="utf-8") step_start = workflow.index("name: Check QPK pin consistency") diff --git a/tests/test_request_handling.py b/tests/test_request_handling.py index 8874670..dda42fd 100644 --- a/tests/test_request_handling.py +++ b/tests/test_request_handling.py @@ -1,9 +1,22 @@ import json import types +import pytest + +import application.execution_receipt_adapter as execution_receipt_adapter from application.cycle_result import StrategyCycleResult +@pytest.fixture(autouse=True) +def isolate_cycle_receipt_contract(monkeypatch): + # Receipt semantics have dedicated adapter tests; these tests isolate HTTP routing/reporting. + monkeypatch.setattr( + execution_receipt_adapter, + "attach_cycle_execution_receipt", + lambda *_args, **_kwargs: None, + ) + + def route_methods(strategy_module): return { rule.rule: sorted(rule.methods - {"HEAD", "OPTIONS"}) @@ -649,7 +662,7 @@ def test_handle_probe_connect_timeout_sends_concise_connection_notification(stra monkeypatch.setattr( strategy_module, "connect_ib", - lambda: (_ for _ in ()).throw(TimeoutError(timeout_message)), + lambda **_kwargs: (_ for _ in ()).throw(TimeoutError(timeout_message)), ) monkeypatch.setattr( strategy_module, @@ -693,7 +706,7 @@ def test_handle_probe_failure_sends_notification(strategy_module, monkeypatch): monkeypatch.setattr( strategy_module, "connect_ib", - lambda: (_ for _ in ()).throw(RuntimeError("probe failed")), + lambda **_kwargs: (_ for _ in ()).throw(RuntimeError("probe failed")), ) monkeypatch.setattr( strategy_module, diff --git a/tests/test_uv_dependency_workflow.py b/tests/test_uv_dependency_workflow.py index 3a8391c..8430d75 100644 --- a/tests/test_uv_dependency_workflow.py +++ b/tests/test_uv_dependency_workflow.py @@ -1,3 +1,5 @@ +import re +import tomllib from pathlib import Path @@ -28,7 +30,7 @@ def test_ci_docker_and_runtime_monitoring_use_uv_lock() -> None: assert lockfile.startswith("version = ") assert "uv sync --frozen --extra test" in ci assert "uv run --no-sync ruff check --exclude external ." in ci - assert "uv run --no-sync python external/QuantPlatformKit/scripts/check_qpk_pin_consistency.py" in ci + assert "uv run --no-sync python scripts/check_qpk_pin_consistency.py" in ci assert "uv sync --frozen --no-dev" in env_sync assert "uv run --no-sync python scripts/build_cloud_run_env_sync_plan.py --json" in env_sync setup_uv = "uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78" @@ -59,3 +61,31 @@ def test_ci_docker_and_runtime_monitoring_use_uv_lock() -> None: assert "--no-install-project" not in ci assert "--no-install-project" not in env_sync assert "--no-install-project" not in dockerfile + + +def test_ci_uses_declared_immutable_internal_dependency_revisions() -> None: + pyproject = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8")) + lockfile = Path("uv.lock").read_text(encoding="utf-8") + ci = Path(".github/workflows/ci.yml").read_text(encoding="utf-8") + revisions = {} + for dependency in pyproject["project"]["dependencies"]: + match = re.search(r"QuantStrategyLab/([^/]+)\.git@([0-9a-f]{40})$", dependency) + if match: + revisions[match.group(1)] = match.group(2) + + assert set(revisions) == { + "HkEquityStrategies", + "QuantPlatformKit", + "UsEquityStrategies", + } + for repository, revision in revisions.items(): + assert f"{repository}.git?rev={revision}#{revision}" in lockfile + assert ( + f"QPK_EXPECTED_PIN={revisions['QuantPlatformKit']} " + "uv run --no-sync python scripts/check_qpk_pin_consistency.py" + ) in ci + assert "ref: main" not in ci + assert "Resolve QuantPlatformKit ref" not in ci + assert "repository: QuantStrategyLab/" not in ci + assert "external/" not in ci + assert "uv pip install --no-deps -e external/" not in ci