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
71 changes: 2 additions & 69 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -124,14 +59,12 @@ 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

- 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
5 changes: 5 additions & 0 deletions scripts/check_qpk_pin_consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"""
from __future__ import annotations

import os
import re
import subprocess
import sys
Expand All @@ -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:
Expand Down
14 changes: 4 additions & 10 deletions tests/test_ci_unit_test_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<block>.*)\Z",
Expand All @@ -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
18 changes: 18 additions & 0 deletions tests/test_dependency_pin_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
17 changes: 15 additions & 2 deletions tests/test_request_handling.py
Original file line number Diff line number Diff line change
@@ -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"})
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
32 changes: 31 additions & 1 deletion tests/test_uv_dependency_workflow.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re
import tomllib
from pathlib import Path


Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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