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[("Repository", "Fixture Repo")]["canon_category"] == "source-repository" assert nodes_by_label[("Repository", "Fixture Repo")]["evidence_state"] == "declared" assert nodes_by_label[("ServiceDeclaration", "Fixture API")]["review_state"] == "accepted" assert nodes_by_label[("ServiceDeclaration", "Fixture API")]["canon_category"] == "service" 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" dev_overlay = nodes_by_label[("Server", "127.0.0.1")]["attributes"]["deployment_overlay"] assert dev_overlay["deployment_environment"] == "dev" assert dev_overlay["deployment_scenario"] == "bernd-laptop" assert dev_overlay["access_zone"] == "private-dev" assert dev_overlay["policy_authority"] == "local-loopback-binding" assert dev_overlay["exposure_class"] == "local-only" assert dev_overlay["routing_authority"] == "docker-compose" assert dev_overlay["route_evidence"]["port"] == 8080 assert ( nodes_by_label[("RuntimeService", "fixture-api.testing.svc.cluster.local")]["attributes"]["runtime_target_type"] == "kubernetes-service-dns" ) assert ( nodes_by_label[("RuntimeService", "fixture-api.testing.svc.cluster.local")]["canon_category"] == "runtime-resource" ) assert ( nodes_by_label[("ApplicationEndpoint", "declared.fixture.test")]["attributes"]["runtime_target_type"] == "declared-endpoint" ) assert nodes_by_label[("ApplicationEndpoint", "declared.fixture.test")]["canon_category"] == "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", } edges_by_type = {edge["edge_type"]: edge for edge in candidates["edges"]} assert edges_by_type["exposes"]["canonical_type"] == "exposes" assert edges_by_type["provides"]["canonical_type"] == "implements" assert edges_by_type["provides"]["mapping_fit"] == "partial" assert edges_by_type["opens_port"]["canonical_type"] == "exposes" assert edges_by_type["resolves_to"]["canonical_type"] == "flows_to" assert all(edge["display_only"] is False for edge in candidates["edges"]) assert all(edge["evidence_state"] in {"declared", "observed", "inferred", "proposed", "gap"} for edge in candidates["edges"]) 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)