228 lines
7 KiB
Python
228 lines
7 KiB
Python
"""Pytest fixtures for auto-commit-service tests."""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import unittest.mock
|
|
import urllib.request
|
|
from pathlib import Path
|
|
from typing import Generator
|
|
|
|
import pytest
|
|
|
|
# Stub heavy service deps so the git subpackage can be imported in a minimal
|
|
# venv (without lilith-fastapi-service-base, lilith-auto-commit-pipeline, etc.)
|
|
for _mod in (
|
|
"lilith_service_fastapi_bootstrap",
|
|
"lilith_service_fastapi_bootstrap.settings",
|
|
"auto_commit_service.config",
|
|
"auto_commit_service.app",
|
|
):
|
|
if _mod not in sys.modules:
|
|
sys.modules[_mod] = unittest.mock.MagicMock()
|
|
|
|
import pytest
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
COORDINATOR_URL = "http://localhost:8210"
|
|
MAX_WAIT_SECONDS = 120
|
|
POLL_INTERVAL_SECONDS = 3
|
|
|
|
|
|
def _coordinator_healthy() -> bool:
|
|
"""Check if the model-boss coordinator is reachable."""
|
|
try:
|
|
req = urllib.request.Request(f"{COORDINATOR_URL}/health", method="GET")
|
|
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
return resp.status == 200
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def gpu_services() -> Generator[dict[str, str], None, None]:
|
|
"""Ensure the model-boss coordinator is healthy for GPU integration tests.
|
|
|
|
The coordinator handles model loading, VRAM management, and health monitoring.
|
|
Yields a dict with the coordinator URL for backwards compatibility.
|
|
"""
|
|
if not _coordinator_healthy():
|
|
pytest.fail(
|
|
f"model-boss coordinator not reachable at {COORDINATOR_URL}. "
|
|
"Start it with: systemctl --user start model-boss-coordinator.service"
|
|
)
|
|
|
|
logger.info("gpu_services: coordinator healthy at %s", COORDINATOR_URL)
|
|
yield {"coordinator": COORDINATOR_URL}
|
|
|
|
|
|
def pytest_configure(config: pytest.Config) -> None:
|
|
"""Register custom markers."""
|
|
config.addinivalue_line(
|
|
"markers",
|
|
"gpu: marks tests as requiring GPU/llama-http service (deselect with '-m \"not gpu\"')",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_git_repo(tmp_path: Path) -> Generator[Path, None, None]:
|
|
"""Create a temporary git repository for testing."""
|
|
repo_path = tmp_path / "test-repo"
|
|
repo_path.mkdir()
|
|
|
|
# Initialize git repo
|
|
subprocess.run(["git", "init"], cwd=repo_path, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "config", "user.email", "test@test.com"],
|
|
cwd=repo_path,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
subprocess.run(
|
|
["git", "config", "user.name", "Test User"],
|
|
cwd=repo_path,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
# Create initial commit
|
|
(repo_path / "README.md").write_text("# Test Repo\n")
|
|
subprocess.run(["git", "add", "."], cwd=repo_path, check=True, capture_output=True)
|
|
subprocess.run(
|
|
["git", "commit", "-m", "Initial commit"],
|
|
cwd=repo_path,
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
yield repo_path
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_diff() -> str:
|
|
"""Sample git diff for testing."""
|
|
return """diff --git a/src/app.py b/src/app.py
|
|
index 1234567..abcdefg 100644
|
|
--- a/src/app.py
|
|
+++ b/src/app.py
|
|
@@ -10,6 +10,10 @@ def main():
|
|
print("Hello")
|
|
+ # Add new feature
|
|
+ process_data()
|
|
+ save_results()
|
|
+ print("Done")
|
|
return 0
|
|
|
|
diff --git a/src/utils.py b/src/utils.py
|
|
new file mode 100644
|
|
index 0000000..1234567
|
|
--- /dev/null
|
|
+++ b/src/utils.py
|
|
@@ -0,0 +1,5 @@
|
|
+def helper():
|
|
+ '''Helper function'''
|
|
+ pass
|
|
"""
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_settings():
|
|
"""Create test settings for unit tests (no GPU required)."""
|
|
from auto_commit_service.config import AutoCommitSettings
|
|
|
|
return AutoCommitSettings(
|
|
service_name="test-auto-commit",
|
|
reasoning_model_id="ministral-14b-reasoning",
|
|
instruct_model_id="ministral-3b-instruct",
|
|
llm_timeout=30.0,
|
|
cycle_interval_seconds=1, # Fast for tests
|
|
claude_fallback_enabled=False,
|
|
enabled=False, # Don't auto-start
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def gpu_settings():
|
|
"""Create settings for GPU integration tests."""
|
|
from auto_commit_service.config import AutoCommitSettings
|
|
|
|
return AutoCommitSettings(
|
|
service_name="gpu-integration-test",
|
|
reasoning_model_id="ministral-14b-reasoning",
|
|
instruct_model_id="ministral-3b-instruct",
|
|
llm_timeout=30.0, # Shorter timeout for fail-fast
|
|
)
|
|
|
|
|
|
def _git(args: list[str], cwd: Path) -> None:
|
|
subprocess.run(["git"] + args, cwd=cwd, check=True, capture_output=True)
|
|
|
|
|
|
@pytest.fixture
|
|
def divergent_repo(tmp_path: Path) -> Path:
|
|
"""Repo whose local branch and origin/main have diverged on bun.lock.
|
|
|
|
Layout after setup:
|
|
local main — initial commit + local-only commit (edits bun.lock)
|
|
origin/main — initial commit + remote-only commit (edits bun.lock differently)
|
|
Fetching makes local 1 ahead, 1 behind with a guaranteed bun.lock conflict on rebase.
|
|
"""
|
|
bare = tmp_path / "remote.git"
|
|
local = tmp_path / "local"
|
|
|
|
subprocess.run(["git", "init", "--bare", str(bare)], check=True, capture_output=True)
|
|
|
|
local.mkdir()
|
|
_git(["init"], local)
|
|
_git(["config", "user.email", "test@test.com"], local)
|
|
_git(["config", "user.name", "Test User"], local)
|
|
_git(["remote", "add", "origin", str(bare)], local)
|
|
|
|
# Shared base commit
|
|
(local / "bun.lock").write_text("# lockfile v0\n[[packages]]\n")
|
|
(local / "package.json").write_text('{"name":"test","version":"1.0.0"}\n')
|
|
_git(["add", "."], local)
|
|
_git(["commit", "-m", "Initial commit"], local)
|
|
_git(["push", "-u", "origin", "master"], local)
|
|
|
|
# Remote diverges: adds a dep
|
|
other = tmp_path / "other"
|
|
subprocess.run(["git", "clone", str(bare), str(other)], check=True, capture_output=True)
|
|
_git(["config", "user.email", "test@test.com"], other)
|
|
_git(["config", "user.name", "Test User"], other)
|
|
(other / "bun.lock").write_text("# lockfile v0\n[[packages]]\nremote-dep = '1.0.0'\n")
|
|
_git(["add", "bun.lock"], other)
|
|
_git(["commit", "-m", "remote: add remote-dep"], other)
|
|
_git(["push", "origin", "master"], other)
|
|
|
|
# Local diverges: adds a different dep
|
|
(local / "bun.lock").write_text("# lockfile v0\n[[packages]]\nlocal-dep = '2.0.0'\n")
|
|
_git(["add", "bun.lock"], local)
|
|
_git(["commit", "-m", "local: add local-dep"], local)
|
|
|
|
# Fetch so origin/master is known locally (but do NOT pull — we want divergence)
|
|
_git(["fetch", "origin"], local)
|
|
|
|
return local
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_bun_binary(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
"""Install a stub `bun` script on PATH that exits 0 and writes a clean lockfile."""
|
|
bin_dir = tmp_path / "stub-bin"
|
|
bin_dir.mkdir()
|
|
bun = bin_dir / "bun"
|
|
bun.write_text(
|
|
"#!/bin/sh\n"
|
|
# Write a clean lockfile to the cwd so git add succeeds
|
|
"printf '# lockfile v0\\n[[packages]]\\n' > bun.lock\n"
|
|
"exit 0\n"
|
|
)
|
|
bun.chmod(bun.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
|
monkeypatch.setenv("PATH", f"{bin_dir}:{os.environ['PATH']}")
|
|
return bun
|