Files
railiance-fabric/tests/test_discovery.py

271 lines
9.9 KiB
Python

from __future__ import annotations
from pathlib import Path
import pytest
import jsonschema
from railiance_fabric.discovery import (
attribute_stable_key,
discovery_stable_key,
relationship_stable_key,
replacement_scope_id,
source_fingerprint,
)
from railiance_fabric.schema_validation import draft202012_validator
def test_discovery_identity_helpers_are_stable_and_scoped() -> None:
service_key = discovery_stable_key("Repo Scoping", "ServiceDeclaration", "Scope Generator")
duplicate_key = discovery_stable_key("repo-scoping", "service declaration", "scope-generator")
readme_key = discovery_stable_key(
"repo-scoping",
"ServiceDeclaration",
"Scope Generator",
source_anchor={"path": "README.md", "line_start": 12},
)
pyproject_key = discovery_stable_key(
"repo-scoping",
"ServiceDeclaration",
"Scope Generator",
source_anchor={"path": "pyproject.toml", "line_start": 1},
)
assert service_key == duplicate_key
assert service_key == "discovery:repo-scoping:service-declaration:scope-generator"
assert readme_key != pyproject_key
assert readme_key.startswith(service_key)
edge_key = relationship_stable_key(
service_key,
"provides",
discovery_stable_key("repo-scoping", "CapabilityDeclaration", "Scope Generation"),
evidence_scope="fabric/declarations",
)
assert edge_key.startswith("edge:")
assert edge_key == relationship_stable_key(
service_key,
"provides",
discovery_stable_key("repo-scoping", "CapabilityDeclaration", "Scope Generation"),
evidence_scope="fabric/declarations",
)
attribute_key = attribute_stable_key(service_key, "runtime")
assert attribute_key == "attribute:discovery:repo-scoping:service-declaration:scope-generator:runtime"
assert replacement_scope_id(
"Repo Scoping",
"python-package",
"package_manifest",
source_path="pyproject.toml",
) != replacement_scope_id(
"Repo Scoping",
"python-package",
"package_manifest",
source_path="package.json",
)
def test_source_fingerprint_ignores_review_snippet_noise() -> None:
base = {
"source_kind": "file",
"path": "README.md",
"line_start": 10,
"line_end": 18,
"snippet": "first wording",
}
changed_snippet = {**base, "snippet": "edited wording"}
assert source_fingerprint(base) == source_fingerprint(changed_snippet)
assert source_fingerprint(base) != source_fingerprint({**base, "line_start": 11})
def test_discovery_snapshot_schema_accepts_candidate_graph() -> None:
service_key = discovery_stable_key("repo-scoping", "ServiceDeclaration", "Scope Generator")
capability_key = discovery_stable_key("repo-scoping", "CapabilityDeclaration", "Scope Generation")
edge_key = relationship_stable_key(service_key, "provides", capability_key)
scope_id = replacement_scope_id(
"repo-scoping",
"python-package",
"package_manifest",
source_path="pyproject.toml",
)
anchor = {
"source_kind": "package_manifest",
"path": "pyproject.toml",
"json_pointer": "/project/name",
"fingerprint": source_fingerprint(
{
"source_kind": "package_manifest",
"path": "pyproject.toml",
"json_pointer": "/project/name",
}
),
}
provenance = {
"extractor_id": "python-package",
"extractor_version": "0.1.0",
"method": "deterministic",
"origin": "deterministic",
}
payload = {
"apiVersion": "railiance.fabric/v1alpha1",
"kind": "FabricDiscoverySnapshot",
"generated_at": "2026-05-19T00:00:00Z",
"source": {
"repo_slug": "repo-scoping",
"repo_name": "repo-scoping",
"domain": "capabilities",
"commit": "abc123",
"path": "/home/worsch/repo-scoping",
},
"scan": {
"run_id": "scan:repo-scoping:abc123",
"profile": "deterministic",
"deterministic_only": True,
"llm_enabled": False,
"started_at": "2026-05-19T00:00:00Z",
"completed_at": "2026-05-19T00:00:01Z",
},
"replacement_scopes": [
{
"id": scope_id,
"extractor_id": "python-package",
"source_kind": "package_manifest",
"source_path": "pyproject.toml",
"mode": "replacement",
}
],
"candidates": {
"nodes": [
{
"stable_key": service_key,
"kind": "ServiceDeclaration",
"label": "Scope Generator",
"repo": "repo-scoping",
"domain": "capabilities",
"lifecycle": "active",
"aliases": ["repo-scoping", "scope-generator"],
"attributes": {"language": "python"},
"origin": "deterministic",
"review_state": "candidate",
"status": "active",
"confidence": 0.85,
"replacement_scope": scope_id,
"provenance": [provenance],
"source_anchors": [anchor],
},
{
"stable_key": capability_key,
"kind": "CapabilityDeclaration",
"label": "Scope Generation",
"repo": "repo-scoping",
"domain": "capabilities",
"aliases": ["scope-generation"],
"attributes": {"capability_type": "scope-generation"},
"origin": "llm",
"review_state": "needs_review",
"status": "active",
"confidence": 0.62,
"replacement_scope": scope_id,
"provenance": [
{
"extractor_id": "readme-llm",
"method": "llm",
"origin": "llm",
"prompt_version": "repo-summary-v1",
"provider": "mock",
"model": "mock",
"usage": {"total_tokens": 0},
"rationale": "Fixture output for schema coverage.",
}
],
"source_anchors": [anchor],
},
],
"edges": [
{
"stable_key": edge_key,
"edge_type": "provides",
"source_key": service_key,
"target_key": capability_key,
"origin": "deterministic",
"review_state": "candidate",
"status": "active",
"confidence": 0.8,
"replacement_scope": scope_id,
"provenance": [provenance],
"source_anchors": [anchor],
}
],
"attributes": [
{
"stable_key": attribute_stable_key(service_key, "language"),
"entity_key": service_key,
"name": "language",
"value": "python",
"origin": "deterministic",
"review_state": "candidate",
"confidence": 0.95,
"replacement_scope": scope_id,
"provenance": [provenance],
"source_anchors": [anchor],
}
],
},
"tombstones": [
{
"stable_key": discovery_stable_key("repo-scoping", "ServiceDeclaration", "Old Scope Tool"),
"entity_kind": "node",
"replacement_scope": scope_id,
"retired_at": "2026-05-19T00:00:01Z",
"reason": "source_missing",
}
],
"reconciliation": {
"precedence": ["repo_declaration", "deterministic", "catalog", "registry", "llm", "manual"],
"duplicate_policy": "stable-key matches merge automatically; alias-only matches require review",
"retirement_policy": "missing candidates retire only inside their replacement scope",
},
}
_validate_schema("discovery-snapshot.schema.yaml", payload)
def test_discovery_snapshot_schema_rejects_unscoped_tombstone() -> None:
payload = {
"apiVersion": "railiance.fabric/v1alpha1",
"kind": "FabricDiscoverySnapshot",
"source": {"repo_slug": "repo-scoping", "commit": "abc123"},
"scan": {
"run_id": "scan:repo-scoping:abc123",
"profile": "deterministic",
"deterministic_only": True,
"llm_enabled": False,
},
"replacement_scopes": [],
"candidates": {"nodes": [], "edges": [], "attributes": []},
"tombstones": [
{
"stable_key": "discovery:repo-scoping:service:old",
"entity_kind": "node",
"retired_at": "2026-05-19T00:00:01Z",
"reason": "source_missing",
}
],
"reconciliation": {
"precedence": ["repo_declaration", "deterministic", "catalog", "registry", "llm", "manual"],
"duplicate_policy": "stable-key matches merge automatically",
"retirement_policy": "missing candidates retire only inside their replacement scope",
},
}
validator = draft202012_validator(Path("schemas") / "discovery-snapshot.schema.yaml")
with pytest.raises(jsonschema.ValidationError):
validator.validate(payload)
def _validate_schema(schema_name: str, payload: dict[str, object]) -> None:
validator = draft202012_validator(Path("schemas") / schema_name)
validator.validate(payload)