import json from pathlib import Path from railiance_fabric.accountability_roots import ( AccountabilityEvidenceStore, build_identity_projection, collect_accountability_root_evidence, load_accountability_root_manifest, ) from railiance_fabric.cli import main as cli_main from railiance_fabric.schema_validation import draft202012_validator def test_collect_accountability_root_evidence_from_manifest(tmp_path: Path) -> None: manifest = _fixture_manifest(tmp_path) evidence_run = collect_accountability_root_evidence(manifest, max_items_per_root=20) validator = draft202012_validator(Path("schemas/accountability-root-evidence.schema.yaml")) assert list(validator.iter_errors(evidence_run)) == [] assert evidence_run["kind"] == "AccountabilityRootEvidenceRun" assert evidence_run["manifest"]["id"] == "fixture.accountability-roots" evidence = [ item for root in evidence_run["roots"] for item in root["evidence"] ] evidence_types = {item["evidence_type"] for item in evidence} assert {"registered_repository", "repository_checkout", "deployment_automation", "secret_root"} <= evidence_types assert all(item["durable"] is True for item in evidence) assert all(item["live_telemetry"] is False for item in evidence) registered_repo = next(item for item in evidence if item["evidence_type"] == "registered_repository") assert registered_repo["attributes"]["state_hub_repo_id"] == "fixture-state-hub-id" secret_root = next(item for item in evidence if item["evidence_type"] == "secret_root") assert "secret-value" not in json.dumps(secret_root) def test_identity_projection_is_stable_and_reviewable(tmp_path: Path) -> None: manifest_path = _fixture_manifest(tmp_path) manifest = load_accountability_root_manifest(manifest_path) first = build_identity_projection(collect_accountability_root_evidence(manifest_path), manifest) second = build_identity_projection(collect_accountability_root_evidence(manifest_path), manifest) validator = draft202012_validator(Path("schemas/accountability-identity-projection.schema.yaml")) assert list(validator.iter_errors(first)) == [] first_keys = {candidate["stable_key"] for candidate in first["identity_candidates"]} second_keys = {candidate["stable_key"] for candidate in second["identity_candidates"]} assert first_keys == second_keys assert { "Actor", "Fabric", "Repository", "Deployable", "SecretRoot", } <= {candidate["identity_type"] for candidate in first["identity_candidates"]} assert first["candidate_graph"]["nodes"] assert first["candidate_graph"]["edges"] def test_evidence_store_persists_runs_items_and_identities(tmp_path: Path) -> None: manifest_path = _fixture_manifest(tmp_path) manifest = load_accountability_root_manifest(manifest_path) evidence_run = collect_accountability_root_evidence(manifest_path) projection = build_identity_projection(evidence_run, manifest) store = AccountabilityEvidenceStore(tmp_path / "evidence.sqlite3") stored = store.add_evidence_run(evidence_run, projection) latest = store.latest_run() assert latest is not None assert latest["id"] == stored["run_id"] assert stored["evidence_count"] == len(store.list_evidence(stored["run_id"])) assert stored["identity_candidate_count"] == len(store.list_identity_candidates(stored["run_id"])) def test_discover_roots_cli_prints_evidence_json(tmp_path: Path, capsys) -> None: manifest = _fixture_manifest(tmp_path) assert cli_main(["discover-roots", "--manifest", str(manifest), "--max-items-per-root", "20"]) == 0 payload = json.loads(capsys.readouterr().out) assert payload["kind"] == "AccountabilityRootEvidenceRun" assert payload["roots"] def test_discover_roots_cli_can_print_identities_and_store(tmp_path: Path, capsys) -> None: manifest = _fixture_manifest(tmp_path) store_path = tmp_path / "evidence.sqlite3" assert ( cli_main( [ "discover-roots", "--manifest", str(manifest), "--identity-projection", "--store-db", str(store_path), ] ) == 0 ) payload = json.loads(capsys.readouterr().out) assert payload["kind"] == "AccountabilityIdentityProjection" assert AccountabilityEvidenceStore(store_path).latest_run() is not None def _fixture_manifest(tmp_path: Path) -> Path: workspace = tmp_path / "workspace" repo = workspace / "fixture-repo" repo.mkdir(parents=True) (repo / ".git").mkdir() (repo / "fabric").mkdir() (repo / "Dockerfile").write_text("FROM python:3.12-slim\n", encoding="utf-8") (repo / "compose.yaml").write_text("services:\n api:\n image: fixture/api\n", encoding="utf-8") secret_metadata = repo / "secret-root.txt" secret_metadata.write_text("secret-value-never-promote\n", encoding="utf-8") registry_manifest = tmp_path / "local-repos.yaml" registry_manifest.write_text( """ apiVersion: railiance.fabric/v1alpha1 kind: RegistryOnboardingManifest repositories: - slug: fixture-repo name: Fixture Repo domain: testing path: {repo} default_branch: main state_hub_repo_id: fixture-state-hub-id remote_url: gitea-remote:coulomb/fixture-repo.git """.format(repo=repo), encoding="utf-8", ) manifest = tmp_path / "accountability-roots.yaml" manifest.write_text( """ apiVersion: railiance.fabric/v1alpha2 kind: AccountabilityRootManifest metadata: id: fixture.accountability-roots name: Fixture Accountability Roots netkingdom: id: fixture.netkingdom name: Fixture Netkingdom king_actor_id: actor.fixture.king actors: - id: actor.fixture.king role: king name: Fixture King - id: actor.fixture.lord role: lord name: Fixture Lord fabrics: - id: fabric.fixture.primary kind: Fabric name: Fixture Primary Fabric netkingdom_id: fixture.netkingdom lord_actor_id: actor.fixture.lord parent_fabric_id: null status: active boundary: boundary_type: fabric criterion: financial_and_operational_accountability discovery_roots: - id: root.fixture.registry type: registry_manifest status: active fabric_id: fabric.fixture.primary owner_actor_id: actor.fixture.king source: manifest_path: {registry_manifest} safe_discovery: local_files evidence_scope: - repo_inventory - repository_identity - id: root.fixture.checkout type: repository_checkout status: active fabric_id: fabric.fixture.primary owner_actor_id: actor.fixture.lord source: repo_slug: fixture-repo path: {repo} safe_discovery: local_files evidence_scope: - repository_identity - local_checkout - id: root.fixture.deployment type: deployment_automation status: active fabric_id: fabric.fixture.primary owner_actor_id: actor.fixture.lord source: path: {repo} patterns: - Dockerfile - compose.yaml safe_discovery: local_files evidence_scope: - deployment_topology - id: root.fixture.secret type: secret_root status: planned fabric_id: fabric.fixture.primary owner_actor_id: actor.fixture.king source: path: {secret_metadata} safe_discovery: metadata_only evidence_scope: - secret_metadata refresh: cadence: manual triggers: - operator_request """.format( registry_manifest=registry_manifest, repo=repo, secret_metadata=secret_metadata, ), encoding="utf-8", ) return manifest