test_builds.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import json
  2. from pathlib import Path
  3. from ontorefactor_governance.builds import (
  4. list_ontology_versions,
  5. publish_ontology_build,
  6. run_ontology_build,
  7. )
  8. from ontorefactor_governance.db import Database
  9. from ontorefactor_governance.llm import LongCatProvider, PROMPT_VERSION
  10. from ontorefactor_governance.service import graph, overview, seed_challenge_cup_demo
  11. SAMPLES = Path(__file__).parents[1] / "src" / "ontorefactor_governance" / "static" / "samples"
  12. def demo_sources():
  13. definitions = (
  14. ("01-dcp-user.sql", "ddl"),
  15. ("02-user-profile-openapi.yaml", "openapi"),
  16. ("03-dcp-asset-inventory.json", "inventory"),
  17. ("04-governance-evidence.json", "inventory"),
  18. )
  19. return [
  20. {"source_name": name, "source_type": source_type, "content": (SAMPLES / name).read_text(encoding="utf-8")}
  21. for name, source_type in definitions
  22. ]
  23. def test_empty_demo_build_review_publish_and_decide(monkeypatch):
  24. monkeypatch.delenv("AGENTPAAS_BASE_URL", raising=False)
  25. db = Database("sqlite:///:memory:")
  26. seeded = seed_challenge_cup_demo(db, "build-test")
  27. project_id = seeded["project"]["id"]
  28. assert seeded["overview"]["totals"]["elements"] == 0
  29. assert seeded["overview"]["totals"]["assertions"] == 0
  30. assert seeded["overview"]["demo"]["decision_code"] == "NOT_BUILT"
  31. build = run_ontology_build(
  32. db, "build-test", project_id, demo_sources(),
  33. semantic_mode="deterministic",
  34. instruction="从上传证据构建候选本体",
  35. environment="production",
  36. decision_target="dcp_user.mobile",
  37. )
  38. assert build["status"] == "review_required"
  39. assert build["agents"] == [
  40. "asset-construction-agent", "semantic-fusion-agent", "ontology-validation-agent",
  41. ]
  42. assert build["diff"]["new_elements"] > 0
  43. assert build["diff"]["new_assertions"] > 0
  44. assert build["diff"]["candidate_rules"] == 5
  45. assert build["candidate_validation"]["valid"] is True
  46. assert all(item["status"] == "candidate" for item in db.fetchall(
  47. "SELECT status FROM ontology_elements WHERE project_id=?", (project_id,)
  48. ))
  49. assert all(item["review_status"] == "pending" for item in db.fetchall(
  50. "SELECT review_status FROM governance_assertions WHERE project_id=?", (project_id,)
  51. ))
  52. assert not db.fetchall("SELECT id FROM governance_rules WHERE project_id=?", (project_id,))
  53. published = publish_ontology_build(
  54. db, "build-test", project_id, build["build_id"],
  55. reviewer="test-reviewer",
  56. accept_semantic_assertions=False,
  57. decision_target="dcp_user.mobile",
  58. )
  59. assert published["version"]["label"] == "v1"
  60. assert published["decision"]["status"] == "BLOCKED"
  61. assert published["decision"]["summary"] == {"passed": 1, "failed": 4, "unknown": 0, "total": 5}
  62. assert published["published"]["accepted_assertions"] > 0
  63. assert published["published"]["pending_assertions"] > 0
  64. assert len(list_ontology_versions(db, "build-test", project_id)) == 1
  65. assert overview(db, "build-test", project_id)["demo"]["decision_code"] == "BLOCKED"
  66. db.close()
  67. def test_second_build_reports_matches_instead_of_reseeding():
  68. db = Database("sqlite:///:memory:")
  69. project_id = seed_challenge_cup_demo(db, "repeat-build")["project"]["id"]
  70. first = run_ontology_build(
  71. db, "repeat-build", project_id, demo_sources(), semantic_mode="deterministic",
  72. )
  73. publish_ontology_build(db, "repeat-build", project_id, first["build_id"], reviewer="reviewer")
  74. second = run_ontology_build(
  75. db, "repeat-build", project_id, demo_sources(), semantic_mode="deterministic",
  76. )
  77. assert second["diff"]["new_elements"] == 0
  78. assert second["diff"]["matched_elements"] == second["summary"]["elements"]
  79. assert second["diff"]["matched_assertions"] > 0
  80. db.close()
  81. def test_different_input_constructs_a_different_ontology():
  82. db = Database("sqlite:///:memory:")
  83. project_id = seed_challenge_cup_demo(db, "custom-build")["project"]["id"]
  84. result = run_ontology_build(db, "custom-build", project_id, [{
  85. "source_name": "factory.sql",
  86. "source_type": "ddl",
  87. "content": "CREATE TABLE mes.machine_alarm (alarm_id BIGINT PRIMARY KEY, machine_id BIGINT NOT NULL, occurred_at TIMESTAMP);",
  88. }], semantic_mode="deterministic")
  89. uris = {item["uri"] for item in result["diff"]["preview_elements"]}
  90. assert any("machine_alarm" in uri for uri in uris)
  91. assert not any("dcp_user" in uri for uri in uris)
  92. assert result["diff"]["candidate_rules"] == 0
  93. db.close()
  94. def test_build_uses_stable_m3_and_assembles_only_required_m2_types():
  95. db = Database("sqlite:///:memory:")
  96. project_id = seed_challenge_cup_demo(db, "catalog-build")["project"]["id"]
  97. result = run_ontology_build(db, "catalog-build", project_id, [{
  98. "source_name": "orders.sql",
  99. "source_type": "ddl",
  100. "content": "CREATE TABLE sales.orders (order_id BIGINT PRIMARY KEY, amount DECIMAL(12,2));",
  101. }], semantic_mode="deterministic")
  102. elements = db.fetchall(
  103. "SELECT id,layer,profile,name,source FROM ontology_elements WHERE project_id=?",
  104. (project_id,),
  105. )
  106. assert {item["name"] for item in elements if item["layer"] == "M3"} == {
  107. "MetaElement", "MetaEntityType", "MetaRelationType", "MetaAttributeType",
  108. "MetaConstraintType", "MetaAssertionType", "MetaProfile", "MetaView",
  109. }
  110. m2_names = {item["name"] for item in elements if item["layer"] == "M2"}
  111. assert m2_names == {"Table", "Column", "BusinessObject"}
  112. assert "Owner" not in m2_names
  113. assert all(item["source"] == "ontology/m3/meta-core.ttl" for item in elements if item["layer"] == "M3")
  114. assert all(item["source"].startswith("ontology/m2/") for item in elements if item["layer"] == "M2")
  115. adjacent_types = db.fetchall(
  116. "SELECT s.layer subject_layer,o.layer object_layer,a.generated_by "
  117. "FROM governance_assertions a JOIN ontology_elements s ON s.id=a.subject_id "
  118. "JOIN ontology_elements o ON o.id=a.object_id "
  119. "WHERE a.project_id=? AND a.predicate='INSTANCE_OF' AND s.layer IN ('M1','M2')",
  120. (project_id,),
  121. )
  122. assert adjacent_types
  123. assert all(
  124. (item["subject_layer"], item["object_layer"]) in {("M1", "M2"), ("M2", "M3")}
  125. for item in adjacent_types
  126. )
  127. assert result["summary"]["elements"] == len(elements)
  128. db.close()
  129. def test_graph_returns_node_audit_and_evidence_details():
  130. db = Database("sqlite:///:memory:")
  131. project_id = seed_challenge_cup_demo(db, "graph-detail")["project"]["id"]
  132. run_ontology_build(db, "graph-detail", project_id, [{
  133. "source_name": "customer.sql",
  134. "source_type": "ddl",
  135. "content": "CREATE TABLE crm.customer (customer_id BIGINT PRIMARY KEY, mobile VARCHAR(32));",
  136. }], semantic_mode="deterministic")
  137. data = graph(db, "graph-detail", project_id)
  138. physical = next(item for item in data["nodes"] if item["kind"] == "PhysicalTable")
  139. meta = next(item for item in data["nodes"] if item["name"] == "MetaEntityType")
  140. assert physical["source_files"]
  141. assert physical["evidence_ids"]
  142. assert physical["confidence"] == 1.0
  143. assert physical["review_status"] == "pending"
  144. assert data["evidence"]
  145. assert meta["source"] == "ontology/m3/meta-core.ttl"
  146. assert data["semantics"]["node_status"]["candidate"] == "dashed"
  147. assert data["semantics"]["edge_status"]["accepted"] == "solid"
  148. db.close()
  149. def test_semantic_agent_receives_current_upload_evidence(monkeypatch):
  150. captured = {}
  151. def fake_chat(provider, messages):
  152. captured["prompt"] = messages[-1]["content"]
  153. provider.last_metrics = {
  154. "provider": "longcat", "model": "LongCat-2.0", "prompt_version": PROMPT_VERSION,
  155. "input_tokens": 40, "output_tokens": 10, "latency_ms": 5,
  156. "request_hash": "request", "response_hash": "response",
  157. }
  158. return json.dumps({
  159. "business_objects": [], "owners": [], "classifications": [],
  160. "quality_rules": [], "relationships": [], "issues": [],
  161. })
  162. monkeypatch.setenv("LONGCAT_API_KEY", "test-key")
  163. monkeypatch.setattr(LongCatProvider, "chat", fake_chat)
  164. db = Database("sqlite:///:memory:")
  165. project_id = seed_challenge_cup_demo(db, "llm-build")["project"]["id"]
  166. result = run_ontology_build(
  167. db, "llm-build", project_id, demo_sources(), semantic_mode="llm",
  168. decision_target="dcp_user.mobile",
  169. )
  170. assert result["semantic_mode"] == "llm"
  171. assert "手机号明文与密文双跑比对" in captured["prompt"]
  172. assert "consumer_count" in captured["prompt"]
  173. assert "生产消费者必须归零" in captured["prompt"]
  174. db.close()