generated from coulomb/repo-seed
Add Fabric graph read model ingest
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user