Add Fabric graph read model ingest

This commit is contained in:
2026-05-23 21:17:58 +02:00
parent 52d456a7a1
commit ec5742543c
10 changed files with 1439 additions and 5 deletions

View File

@@ -942,3 +942,224 @@ class TestExecutionQueueEndpoints:
r = await client.get(f"/execution/launch-requests?workstream_id={ws['id']}")
assert len(r.json()) == 1
def _fabric_graph_export(generated_at="2026-05-23T12:00:00Z", extra_node=False):
nodes = [
{
"id": "the-custodian.state-hub",
"kind": "ServiceDeclaration",
"name": "State Hub",
"repo": "state-hub",
"domain": "custodian",
"lifecycle": "active",
"canon_category": "service",
"canon_anchor": "state-hub",
"mapping_fit": "direct",
"evidence_state": "declared",
"attributes": {"state_hub_repo_id": "state-hub"},
},
{
"id": "the-custodian.state-hub.http-api",
"kind": "InterfaceDeclaration",
"name": "State Hub HTTP API",
"repo": "state-hub",
"domain": "custodian",
"lifecycle": "active",
"canon_category": "interface",
"mapping_fit": "direct",
"evidence_state": "observed",
"attributes": {},
},
{
"id": "the-custodian.state-hub.coordination",
"kind": "CapabilityDeclaration",
"name": "Coordination",
"repo": "state-hub",
"domain": "custodian",
"lifecycle": "active",
"canon_category": "capability",
"mapping_fit": "direct",
"evidence_state": "declared",
"attributes": {},
},
]
edges = [
{
"from": "the-custodian.state-hub",
"to": "the-custodian.state-hub.http-api",
"type": "exposes",
"canonical_type": "exposes",
"canon_anchor": "state-hub-http",
"mapping_fit": "direct",
"display_only": False,
"evidence_state": "observed",
"attributes": {},
},
{
"from": "the-custodian.state-hub",
"to": "the-custodian.state-hub.coordination",
"type": "provides",
"canonical_type": "implements",
"mapping_fit": "direct",
"display_only": False,
"evidence_state": "declared",
"attributes": {},
},
{
"from": "the-custodian.state-hub.coordination",
"to": "the-custodian.state-hub.http-api",
"type": "depends_on",
"canonical_type": "depends_on",
"mapping_fit": "direct",
"display_only": False,
"evidence_state": "declared",
"attributes": {},
},
]
if extra_node:
nodes.append(
{
"id": "railiance.fabric.registry",
"kind": "ServiceDeclaration",
"name": "Railiance Fabric Registry",
"repo": "railiance-fabric",
"domain": "custodian",
"lifecycle": "active",
"canon_category": "service",
"mapping_fit": "direct",
"evidence_state": "observed",
"attributes": {},
}
)
edges.append(
{
"from": "railiance.fabric.registry",
"to": "the-custodian.state-hub",
"type": "exposes",
"canonical_type": "exposes",
"mapping_fit": "direct",
"display_only": False,
"evidence_state": "observed",
"attributes": {},
}
)
return {
"apiVersion": "railiance.fabric/v1alpha1",
"kind": "FabricGraphExport",
"generated_at": generated_at,
"source": {
"repo": "registry",
"commit": "abc123",
"path": ".railiance-fabric/registry.sqlite3",
},
"nodes": nodes,
"edges": edges,
}
class TestFabricGraphReadModel:
async def test_validation_failure_records_failed_import_without_read_model_rows(self, client):
payload = _fabric_graph_export()
payload.pop("kind")
r = await client.post("/fabric/graph-exports", json=payload)
assert r.status_code == 422
body = r.json()["detail"]
assert body["validation_status"] == "invalid"
assert body["import_id"]
r = await client.get("/fabric/graph-exports?validation_status=invalid")
assert r.status_code == 200
imports = r.json()
assert len(imports) == 1
assert imports[0]["node_count"] == 0
assert imports[0]["edge_count"] == 0
assert imports[0]["error_details"]["error"].startswith("invalid FabricGraphExport")
r = await client.get("/fabric/graph/nodes")
assert r.status_code == 404
r = await client.get("/progress/?event_type=fabric_graph_import")
assert r.status_code == 200
assert "rejected" in r.json()[0]["summary"]
async def test_idempotent_reingest_uses_canonical_graph_content_hash(self, client):
first = _fabric_graph_export(generated_at="2026-05-23T12:00:00Z")
second = _fabric_graph_export(generated_at="2026-05-23T12:05:00Z")
r = await client.post("/fabric/graph-exports", json=first)
assert r.status_code == 200, r.text
first_body = r.json()
assert first_body["created"] is True
assert first_body["idempotent"] is False
r = await client.post("/fabric/graph-exports", json=second)
assert r.status_code == 200, r.text
second_body = r.json()
assert second_body["created"] is False
assert second_body["idempotent"] is True
assert second_body["import_run"]["id"] == first_body["import_run"]["id"]
r = await client.get("/fabric/graph-exports")
assert len(r.json()) == 1
r = await client.get("/fabric/graph/nodes")
assert len(r.json()) == 3
async def test_latest_import_selection_tracks_new_graph_content(self, client):
r = await client.post("/fabric/graph-exports", json=_fabric_graph_export())
assert r.status_code == 200, r.text
first_id = r.json()["import_run"]["id"]
r = await client.post("/fabric/graph-exports", json=_fabric_graph_export(extra_node=True))
assert r.status_code == 200, r.text
second_id = r.json()["import_run"]["id"]
assert second_id != first_id
r = await client.get("/fabric/graph-exports/latest")
latest = r.json()
assert latest["id"] == second_id
assert latest["node_count"] == 4
assert latest["edge_count"] == 4
r = await client.get("/fabric/graph-exports")
latest_flags = {row["id"]: row["is_latest"] for row in r.json()}
assert latest_flags[first_id] is False
assert latest_flags[second_id] is True
async def test_read_only_queries_filter_graph_without_mutating_state_hub_entities(self, client):
await _create_domain(client)
topic = await _create_topic(client)
ws = await _create_workstream(client, topic["id"], status="ready")
task = await _create_task(client, ws["id"])
r = await client.post("/fabric/graph-exports", json=_fabric_graph_export(extra_node=True))
assert r.status_code == 200, r.text
before_progress = await client.get("/progress/")
before_progress_count = len(before_progress.json())
r = await client.get("/fabric/graph/summary")
assert r.status_code == 200
summary = r.json()
assert summary["node_count"] == 4
assert summary["edge_count"] == 4
assert summary["nodes_by_repo"]["state-hub"] == 3
assert summary["edges_by_canonical_type"]["exposes"] == 2
r = await client.get("/fabric/graph/nodes?repo=state-hub&canonical_category=service")
assert r.status_code == 200
assert [node["graph_id"] for node in r.json()] == ["the-custodian.state-hub"]
for relationship in ("exposes", "depends_on", "implements"):
r = await client.get(f"/fabric/graph/edges?canonical_relationship={relationship}")
assert r.status_code == 200
assert len(r.json()) >= 1
after_progress = await client.get("/progress/")
assert len(after_progress.json()) == before_progress_count
r = await client.get(f"/workstreams/{ws['id']}")
assert r.json()["status"] == "ready"
r = await client.get(f"/tasks/{task['id']}")
assert r.json()["status"] == "todo"