generated from coulomb/repo-seed
Implement SAND-WP-0003: validation meta-framework extraction
Port e2e-framework schema, runner, and reporter into wise-validator with sand-boxer CLI integration, validate run CLI, unit tests, registry capability, and operator docs.
This commit is contained in:
5
tests/fixtures/minimal_repo/docker-compose.yml
vendored
Normal file
5
tests/fixtures/minimal_repo/docker-compose.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
services:
|
||||
web:
|
||||
image: docker.io/library/nginx:alpine
|
||||
ports:
|
||||
- "127.0.0.1:8080:80"
|
||||
9
tests/fixtures/minimal_repo/e2e/e2e.yml
vendored
Normal file
9
tests/fixtures/minimal_repo/e2e/e2e.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
name: fixture-repo
|
||||
compose_file: docker-compose.yml
|
||||
health_checks:
|
||||
- name: web
|
||||
url: http://localhost:8080
|
||||
timeout: 30
|
||||
test_command: "echo ok"
|
||||
timeout: 60
|
||||
cleanup: always
|
||||
37
tests/test_reporter.py
Normal file
37
tests/test_reporter.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""State Hub reporter tests."""
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from wisevalidator.models import RunResult
|
||||
from wisevalidator.reporter import report
|
||||
|
||||
|
||||
def test_report_posts_e2e_result() -> None:
|
||||
result = RunResult(
|
||||
sandbox_id="abc12345",
|
||||
repo="fixture-repo",
|
||||
passed=True,
|
||||
exit_code=0,
|
||||
duration_s=12.3,
|
||||
output="ok",
|
||||
)
|
||||
captured: dict = {}
|
||||
|
||||
def fake_urlopen(req, timeout=10):
|
||||
captured["url"] = req.full_url
|
||||
captured["body"] = json.loads(req.data.decode())
|
||||
resp = MagicMock()
|
||||
resp.status = 201
|
||||
resp.__enter__ = MagicMock(return_value=resp)
|
||||
resp.__exit__ = MagicMock(return_value=False)
|
||||
return resp
|
||||
|
||||
with patch("wisevalidator.reporter.urllib.request.urlopen", fake_urlopen):
|
||||
ok = report(result, workstream_id="ws-uuid")
|
||||
|
||||
assert ok is True
|
||||
assert captured["url"].endswith("/progress/")
|
||||
assert captured["body"]["event_type"] == "e2e_result"
|
||||
assert "PASSED" in captured["body"]["summary"]
|
||||
assert captured["body"]["workstream_id"] == "ws-uuid"
|
||||
104
tests/test_runner.py
Normal file
104
tests/test_runner.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""Validation runner tests with mocked sand-boxer and SSH."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from wisevalidator.runner import run_validation
|
||||
from wisevalidator.sandbox_client import SandboxHandle
|
||||
|
||||
FIXTURE_REPO = Path(__file__).parent / "fixtures" / "minimal_repo"
|
||||
|
||||
|
||||
def test_run_validation_success() -> None:
|
||||
handle = SandboxHandle(
|
||||
sandbox_id="run12345",
|
||||
host="coulombcore",
|
||||
ssh="root@coulombcore",
|
||||
remote_dir="/tmp/sandboxer/run12345",
|
||||
compose_project="sbx-e2e-run12345",
|
||||
raw={},
|
||||
)
|
||||
|
||||
class FakeSSH:
|
||||
remote_dir = handle.remote_dir
|
||||
|
||||
def wait_for_url(self, url: str, timeout: int = 120, interval: int = 5) -> bool:
|
||||
return True
|
||||
|
||||
def run(self, cmd: str, *, timeout: int = 60) -> tuple[int, str]:
|
||||
return 0, "test output\n"
|
||||
|
||||
with (
|
||||
patch("wisevalidator.runner.create_sandbox", return_value=handle),
|
||||
patch("wisevalidator.runner.destroy_sandbox") as destroy,
|
||||
patch("wisevalidator.runner.SSHSession.from_reachability", return_value=FakeSSH()),
|
||||
):
|
||||
result = run_validation(FIXTURE_REPO, host="coulombcore")
|
||||
|
||||
assert result.passed is True
|
||||
assert result.exit_code == 0
|
||||
assert result.sandbox_id == "run12345"
|
||||
destroy.assert_called_once_with("run12345")
|
||||
|
||||
|
||||
def test_run_validation_keep_skips_destroy() -> None:
|
||||
handle = SandboxHandle(
|
||||
sandbox_id="keep1234",
|
||||
host="coulombcore",
|
||||
ssh="coulombcore",
|
||||
remote_dir="/tmp/sandboxer/keep1234",
|
||||
compose_project="sbx-e2e-keep1234",
|
||||
raw={},
|
||||
)
|
||||
|
||||
class FakeSSH:
|
||||
remote_dir = handle.remote_dir
|
||||
|
||||
def wait_for_url(self, url: str, timeout: int = 120, interval: int = 5) -> bool:
|
||||
return True
|
||||
|
||||
def run(self, cmd: str, *, timeout: int = 60) -> tuple[int, str]:
|
||||
return 0, ""
|
||||
|
||||
with (
|
||||
patch("wisevalidator.runner.create_sandbox", return_value=handle),
|
||||
patch("wisevalidator.runner.destroy_sandbox") as destroy,
|
||||
patch("wisevalidator.runner.SSHSession.from_reachability", return_value=FakeSSH()),
|
||||
):
|
||||
result = run_validation(FIXTURE_REPO, keep=True)
|
||||
|
||||
assert result.passed is True
|
||||
destroy.assert_not_called()
|
||||
|
||||
|
||||
def test_run_validation_health_failure() -> None:
|
||||
handle = SandboxHandle(
|
||||
sandbox_id="fail1234",
|
||||
host="coulombcore",
|
||||
ssh="coulombcore",
|
||||
remote_dir="/tmp/sandboxer/fail1234",
|
||||
compose_project=None,
|
||||
raw={},
|
||||
)
|
||||
|
||||
class FakeSSH:
|
||||
remote_dir = handle.remote_dir
|
||||
|
||||
def wait_for_url(self, url: str, timeout: int = 120, interval: int = 5) -> bool:
|
||||
return False
|
||||
|
||||
def run(self, cmd: str, *, timeout: int = 60) -> tuple[int, str]:
|
||||
return 0, ""
|
||||
|
||||
with (
|
||||
patch("wisevalidator.runner.create_sandbox", return_value=handle),
|
||||
patch("wisevalidator.runner.destroy_sandbox") as destroy,
|
||||
patch("wisevalidator.runner.SSHSession.from_reachability", return_value=FakeSSH()),
|
||||
):
|
||||
result = run_validation(FIXTURE_REPO)
|
||||
|
||||
assert result.passed is False
|
||||
assert "Health check failed" in result.error
|
||||
destroy.assert_called_once_with("fail1234")
|
||||
59
tests/test_sandbox_client.py
Normal file
59
tests/test_sandbox_client.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""sand-boxer CLI client tests."""
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from wisevalidator.sandbox_client import create_sandbox, destroy_sandbox
|
||||
|
||||
|
||||
def test_create_sandbox_parses_ready_status(tmp_path) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
payload = {
|
||||
"sandbox_id": "cli12345",
|
||||
"state": "ready",
|
||||
"host": "coulombcore",
|
||||
"reachability": {
|
||||
"ssh": "root@coulombcore",
|
||||
"host": "coulombcore",
|
||||
"remote_dir": "/tmp/sandboxer/cli12345",
|
||||
"compose_project": "sbx-e2e-cli12345",
|
||||
},
|
||||
}
|
||||
|
||||
proc = MagicMock(returncode=0, stdout=json.dumps(payload), stderr="")
|
||||
|
||||
with (
|
||||
patch("wisevalidator.sandbox_client.shutil.which", return_value="/bin/sandboxer"),
|
||||
patch("wisevalidator.sandbox_client.subprocess.run", return_value=proc),
|
||||
):
|
||||
handle = create_sandbox(repo)
|
||||
|
||||
assert handle.sandbox_id == "cli12345"
|
||||
assert handle.remote_dir == "/tmp/sandboxer/cli12345"
|
||||
|
||||
|
||||
def test_create_sandbox_not_ready_raises(tmp_path) -> None:
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
payload = {"sandbox_id": "bad12345", "state": "failed", "error": "compose up failed"}
|
||||
proc = MagicMock(returncode=0, stdout=json.dumps(payload), stderr="")
|
||||
|
||||
with (
|
||||
patch("wisevalidator.sandbox_client.shutil.which", return_value="/bin/sandboxer"),
|
||||
patch("wisevalidator.sandbox_client.subprocess.run", return_value=proc),
|
||||
pytest.raises(RuntimeError, match="sandbox not ready"),
|
||||
):
|
||||
create_sandbox(repo)
|
||||
|
||||
|
||||
def test_destroy_sandbox() -> None:
|
||||
payload = {"sandbox_id": "cli12345", "state": "destroyed"}
|
||||
proc = MagicMock(returncode=0, stdout=json.dumps(payload), stderr="")
|
||||
|
||||
with patch("wisevalidator.sandbox_client.subprocess.run", return_value=proc):
|
||||
out = destroy_sandbox("cli12345")
|
||||
|
||||
assert out["state"] == "destroyed"
|
||||
25
tests/test_schema.py
Normal file
25
tests/test_schema.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""E2EConfig schema tests."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from wisevalidator.schema import E2EConfig, HealthCheck
|
||||
|
||||
FIXTURE_REPO = Path(__file__).parent / "fixtures" / "minimal_repo"
|
||||
|
||||
|
||||
def test_load_minimal_fixture() -> None:
|
||||
config = E2EConfig.load(FIXTURE_REPO)
|
||||
assert config.name == "fixture-repo"
|
||||
assert config.compose_file == "docker-compose.yml"
|
||||
assert config.test_command == "echo ok"
|
||||
assert len(config.health_checks) == 1
|
||||
assert config.health_checks[0] == HealthCheck(
|
||||
name="web", url="http://localhost:8080", timeout=30
|
||||
)
|
||||
|
||||
|
||||
def test_missing_e2e_yml(tmp_path: Path) -> None:
|
||||
with pytest.raises(FileNotFoundError, match="No e2e.yml"):
|
||||
E2EConfig.load(tmp_path)
|
||||
Reference in New Issue
Block a user