Files
railiance-fabric/tests/test_scanner.py

320 lines
9.4 KiB
Python

from __future__ import annotations
import json
from pathlib import Path
from railiance_fabric.cli import main as cli_main
from railiance_fabric.scanner import ScanOptions, scan_repo
from railiance_fabric.schema_validation import draft202012_validator
def test_scan_repo_emits_schema_valid_deterministic_snapshot(tmp_path: Path) -> None:
repo = _fixture_repo(tmp_path)
snapshot = scan_repo(
ScanOptions(
repo_path=repo,
repo_slug="fixture-repo",
repo_name="Fixture Repo",
domain="testing",
commit="abc123",
)
)
_validate_schema("discovery-snapshot.schema.yaml", snapshot)
assert snapshot["source"]["repo_slug"] == "fixture-repo"
assert snapshot["source"]["commit"] == "abc123"
assert snapshot["scan"]["deterministic_only"] is True
assert snapshot["scan"]["llm_enabled"] is False
candidates = snapshot["candidates"]
nodes_by_label = {(node["kind"], node["label"]): node for node in candidates["nodes"]}
assert nodes_by_label[("Repository", "Fixture Repo")]["review_state"] == "candidate"
assert nodes_by_label[("ServiceDeclaration", "Fixture API")]["review_state"] == "accepted"
assert nodes_by_label[("Library", "fixture-service")]["attributes"]["language"] == "python"
assert nodes_by_label[("ExternalLibrary", "PyYAML")]["attributes"]["ecosystem"] == "python"
assert nodes_by_label[("DeploymentService", "api")]["attributes"]["orchestrator"] == "docker-compose"
assert nodes_by_label[("ContainerBuild", "Dockerfile")]["attributes"]["base_images"] == ["python:3.12-slim"]
assert nodes_by_label[("InterfaceDeclaration", "Fixture API Contract")]["attributes"]["contract_kind"] == "openapi"
assert nodes_by_label[("KubernetesDeployment", "fixture-api")]["attributes"]["manifest_kind"] == "Deployment"
assert nodes_by_label[("ScoreWorkload", "fixture-api")]["attributes"]["container_count"] == 1
assert nodes_by_label[("Lockfile", "package-lock.json")]["attributes"]["path"] == "package-lock.json"
assert nodes_by_label[("ServiceConfig", "application.yaml")]["attributes"]["format"] == "yaml"
assert nodes_by_label[("Server", "127.0.0.1")]["attributes"]["runtime_target_type"] == "compose-host"
assert (
nodes_by_label[("RuntimeService", "fixture-api.testing.svc.cluster.local")]["attributes"]["runtime_target_type"]
== "kubernetes-service-dns"
)
assert (
nodes_by_label[("ApplicationEndpoint", "declared.fixture.test")]["attributes"]["runtime_target_type"]
== "declared-endpoint"
)
assert nodes_by_label[("NetworkPort", "127.0.0.1:8080/tcp")]["attributes"]["target_port"] == 8080
assert nodes_by_label[("NetworkPort", "fixture-api.testing.svc.cluster.local:8080/tcp")]["attributes"]["service_port"] == 8080
assert nodes_by_label[("NetworkPort", "declared.fixture.test:9443/tcp")]["attributes"]["scheme"] == "https"
assert nodes_by_label[("DomainName", "api.fixture.test")]["attributes"]["domain"] == "api.fixture.test"
assert nodes_by_label[("DomainName", "api.k8s.fixture.test")]["attributes"]["domain"] == "api.k8s.fixture.test"
assert nodes_by_label[("DomainName", "declared.fixture.test")]["attributes"]["domain"] == "declared.fixture.test"
edge_types = {edge["edge_type"] for edge in candidates["edges"]}
assert edge_types >= {
"declares_package",
"depends_on_library",
"defines_deployment",
"builds_container",
"documents_interface",
"defines_runtime_object",
"defines_workload",
"uses_config",
"provides",
"exposes",
"opens_port",
"listens_on",
"names_endpoint",
"exposes_port",
"routes_to_port",
"routes_to_service",
"resolves_to",
}
assert {attribute["name"] for attribute in candidates["attributes"]} >= {
"readme_title",
"intent_present",
"scope_present",
}
for collection_name in ("nodes", "edges", "attributes"):
stable_keys = [item["stable_key"] for item in candidates[collection_name]]
assert len(stable_keys) == len(set(stable_keys))
assert all(item["source_anchors"][0]["fingerprint"] for item in candidates[collection_name])
scope_ids = [scope["id"] for scope in snapshot["replacement_scopes"]]
assert len(scope_ids) == len(set(scope_ids))
assert {scope["source_kind"] for scope in snapshot["replacement_scopes"]} >= {
"declaration",
"package_manifest",
"lockfile",
"deployment_manifest",
"api_contract",
"service_config",
"file",
}
def test_scan_cli_can_write_snapshot_and_print_summary(tmp_path: Path, capsys) -> None:
repo = _fixture_repo(tmp_path)
output = tmp_path / "snapshot.json"
assert cli_main(
[
"scan",
str(repo),
"--repo-slug",
"fixture-repo",
"--repo-name",
"Fixture Repo",
"--commit",
"abc123",
"--dry-run",
"--output",
str(output),
]
) == 0
summary = capsys.readouterr().out
assert "dry-run scan fixture-repo (abc123):" in summary
assert "replacement scope(s)" in summary
payload = json.loads(output.read_text(encoding="utf-8"))
_validate_schema("discovery-snapshot.schema.yaml", payload)
def _fixture_repo(tmp_path: Path) -> Path:
repo = tmp_path / "fixture-repo"
repo.mkdir()
_write(repo / "README.md", "# Fixture Repo\n\nRuns the fixture API.\n")
_write(repo / "INTENT.md", "# Intent\n\nShow deterministic scanner evidence.\n")
_write(repo / "SCOPE.md", "# Scope\n\nLocal test fixture.\n")
_write(
repo / "pyproject.toml",
"""
[project]
name = "fixture-service"
version = "0.1.0"
description = "Fixture service"
dependencies = [
"PyYAML>=6.0",
"jsonschema>=4.18",
]
""".lstrip(),
)
_write(
repo / "package.json",
json.dumps(
{
"name": "@fixture/web",
"version": "0.1.0",
"private": True,
"scripts": {"build": "vite build"},
"dependencies": {"cytoscape": "^3.30.0"},
"devDependencies": {"vite": "^5.0.0"},
},
indent=2,
),
)
_write(repo / "package-lock.json", '{"lockfileVersion": 3}\n')
_write(repo / "Dockerfile", "FROM python:3.12-slim\nCOPY . /app\n")
_write(
repo / "compose.yaml",
"""
services:
api:
build: .
ports:
- "127.0.0.1:8080:8080"
labels:
- "traefik.http.routers.fixture.rule=Host(`api.fixture.test`)"
- "VIRTUAL_HOST=api-alt.fixture.test"
""".lstrip(),
)
_write(
repo / "openapi.yaml",
"""
openapi: 3.1.0
info:
title: Fixture API Contract
version: 0.1.0
paths: {}
""".lstrip(),
)
_write(
repo / "score.yaml",
"""
metadata:
name: fixture-api
containers:
api:
image: fixture/api
""".lstrip(),
)
_write(
repo / "application.yaml",
"server:\n port: 8080\n",
)
_write(
repo / "deploy" / "deployment.yaml",
"""
apiVersion: apps/v1
kind: Deployment
metadata:
name: fixture-api
spec: {}
""".lstrip(),
)
_write(
repo / "fabric" / "services" / "fixture-api.yaml",
"""
apiVersion: railiance.fabric/v1alpha1
kind: ServiceDeclaration
metadata:
id: fixture.api
name: Fixture API
owner: test
repo: fixture-repo
domain: testing
spec:
lifecycle: active
provides_capabilities:
- fixture.api-capability
exposes_interfaces:
- fixture.api-http
""".lstrip(),
)
_write(
repo / "fabric" / "capabilities" / "fixture-api-capability.yaml",
"""
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: fixture.api-capability
name: Fixture API Capability
owner: test
repo: fixture-repo
domain: testing
spec:
capability_type: fixture-api
lifecycle: active
service_id: fixture.api
interface_ids:
- fixture.api-http
""".lstrip(),
)
_write(
repo / "fabric" / "interfaces" / "fixture-api-http.yaml",
"""
apiVersion: railiance.fabric/v1alpha1
kind: InterfaceDeclaration
metadata:
id: fixture.api-http
name: Fixture API HTTP
owner: test
repo: fixture-repo
domain: testing
spec:
interface_type: http-api
lifecycle: active
service_id: fixture.api
endpoint:
url: https://declared.fixture.test:9443/api
""".lstrip(),
)
_write(
repo / "deploy" / "service.yaml",
"""
apiVersion: v1
kind: Service
metadata:
name: fixture-api
namespace: testing
spec:
ports:
- port: 8080
targetPort: 8080
protocol: TCP
""".lstrip(),
)
_write(
repo / "deploy" / "ingress.yaml",
"""
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: fixture-api
namespace: testing
spec:
tls:
- hosts:
- secure.fixture.test
rules:
- host: api.k8s.fixture.test
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: fixture-api
port:
number: 8080
""".lstrip(),
)
return repo
def _write(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
def _validate_schema(schema_name: str, payload: dict[str, object]) -> None:
validator = draft202012_validator(Path("schemas") / schema_name)
validator.validate(payload)