test_api.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. from fastapi.testclient import TestClient
  2. from app.contracts import SCHEMA_CATALOG
  3. from app.fake_data import (
  4. ARTIFACT_DIGESTS,
  5. GRAPH,
  6. RUN_ID,
  7. canonical_digest,
  8. canonical_wire_digest,
  9. )
  10. from app.main import app
  11. client = TestClient(app)
  12. def test_health_declares_fake_mode() -> None:
  13. response = client.get("/api/health")
  14. assert response.status_code == 200
  15. assert response.json()["data_mode"] == "fake"
  16. def test_graph_is_fine_grained_and_connected() -> None:
  17. response = client.get(f"/api/runs/{RUN_ID}/graph")
  18. assert response.status_code == 200
  19. body = response.json()
  20. assert body["data_mode"] == "fake"
  21. assert len(body["nodes"]) >= 55
  22. assert len(body["edges"]) >= 50
  23. assert {item["phase"] for item in body["nodes"]} == {
  24. "phase-1",
  25. "phase-2",
  26. "phase-3",
  27. "delivery",
  28. }
  29. assert {item["node_type"] for item in body["nodes"]} >= {
  30. "contract",
  31. "operation",
  32. "attempt",
  33. "artifact",
  34. "validation",
  35. "decision",
  36. "publication",
  37. "transaction",
  38. "readback",
  39. }
  40. def test_every_fake_record_uses_exact_contract_fields() -> None:
  41. used_models: set[str] = set()
  42. for node in GRAPH.nodes:
  43. model = node.record.model_name
  44. used_models.add(model)
  45. assert set(node.record.payload) == set(SCHEMA_CATALOG[model]), node.id
  46. required = {
  47. "ScriptBuildRecord",
  48. "ScriptBuildInputSnapshotV1",
  49. "MissionBinding",
  50. "MissionOwnerToken",
  51. "ScriptTaskContractV1",
  52. "OperationView",
  53. "AttemptView",
  54. "ValidationView",
  55. "PlannerDecisionView",
  56. "EvidenceRecordV1",
  57. "ScriptDirectionArtifactV1",
  58. "StructureArtifactV1",
  59. "ParagraphArtifactV1",
  60. "ElementSetArtifactV1",
  61. "ComparisonArtifactV1",
  62. "StructuredScriptArtifactV1",
  63. "CandidatePortfolioArtifactV1",
  64. "RootDeliveryManifestV1",
  65. "LegacyProjectionCanonicalV1",
  66. "Publication",
  67. "PublicationResult",
  68. "HttpCommandRecord",
  69. "InputSnapshotRow",
  70. "ArtifactVersionRow",
  71. "ParagraphRow",
  72. "ElementRow",
  73. "ParagraphElementLinkRow",
  74. }
  75. assert required <= used_models
  76. def test_manifest_and_legacy_projection_use_real_logical_closure() -> None:
  77. records = {node.record.model_name: node.record.payload for node in GRAPH.nodes}
  78. canonical = records["LegacyProjectionCanonicalV1"]
  79. manifest = records["RootDeliveryManifestV1"]
  80. assert manifest["legacy_projection_digest"] == canonical_wire_digest(canonical)
  81. assert manifest["input_closure_digest"] != manifest["legacy_projection_digest"]
  82. for paragraph in canonical["paragraphs"]:
  83. assert "logical_key" in paragraph
  84. assert "parent_logical_key" in paragraph
  85. assert not {"paragraph_id", "parent_id", "is_active"} & set(paragraph)
  86. for element in canonical["elements"]:
  87. assert "logical_key" in element
  88. assert not {"element_id", "is_active"} & set(element)
  89. for link in canonical["paragraph_element_links"]:
  90. assert set(link) == {"paragraph_logical_key", "element_logical_key"}
  91. def test_artifact_refs_pin_specific_kind_and_canonical_digest() -> None:
  92. attempts = [
  93. node.record.payload
  94. for node in GRAPH.nodes
  95. if node.record.model_name == "AttemptView"
  96. ]
  97. for attempt in attempts:
  98. for ref in attempt["submission"]["artifact_refs"]:
  99. version = int(ref["version"])
  100. assert ref["kind"] != "business-artifact"
  101. assert ref["digest"] == ARTIFACT_DIGESTS[version]
  102. node_ids = {node.id for node in GRAPH.nodes}
  103. assert "compose:alternate:artifact" in node_ids
  104. assert "compose:alternate:decision" in node_ids
  105. def test_root_task_has_no_self_parent_and_declares_children() -> None:
  106. root = next(
  107. node.record.payload
  108. for node in GRAPH.nodes
  109. if node.record.model_name == "TaskView"
  110. and node.record.payload["task_id"] == "task-root"
  111. )
  112. assert root["parent_task_id"] is None
  113. assert root["child_task_ids"] == ["direction", "portfolio"]
  114. contract = next(
  115. node.record.payload
  116. for node in GRAPH.nodes
  117. if node.id == "root-delivery:contract"
  118. )
  119. assert contract["scope_ref"] == "script-build://scope/root"
  120. assert contract["write_scope"] == []
  121. assert [item["expected_task_kind"] for item in contract["input_decision_refs"]] == [
  122. "direction",
  123. "candidate-portfolio",
  124. ]
  125. attempt = next(
  126. node.record.payload
  127. for node in GRAPH.nodes
  128. if node.id == "root-delivery:attempt"
  129. )
  130. assert attempt["accepted_child_decision_ids"] == [
  131. "decision-direction",
  132. "decision-portfolio",
  133. ]
  134. def test_input_snapshot_row_and_logical_view_share_one_canonical_payload() -> None:
  135. row = next(
  136. node.record.payload
  137. for node in GRAPH.nodes
  138. if node.record.model_name == "InputSnapshotRow"
  139. )
  140. logical = next(
  141. node.record.payload
  142. for node in GRAPH.nodes
  143. if node.record.model_name == "ScriptBuildInputSnapshotV1"
  144. )
  145. assert logical["snapshot_id"] == str(row["id"]) == "301"
  146. assert row["canonical_sha256"] == canonical_digest(row["canonical_json"])
  147. assert logical["canonical_sha256"] == f"sha256:{row['canonical_sha256']}"
  148. for key in (
  149. "script_build_id",
  150. "execution_id",
  151. "topic_build_id",
  152. "topic_id",
  153. "topic",
  154. "account",
  155. "persona_points",
  156. "section_patterns",
  157. "strategies",
  158. "prompt_manifest",
  159. "datasource_manifest",
  160. "model_manifest",
  161. ):
  162. assert logical[key] == row["canonical_json"][key]
  163. def test_final_physical_row_counts_match_canonical_counts() -> None:
  164. canonical = next(
  165. node.record.payload
  166. for node in GRAPH.nodes
  167. if node.record.model_name == "LegacyProjectionCanonicalV1"
  168. )
  169. counts = {
  170. model: sum(1 for node in GRAPH.nodes if node.record.model_name == model)
  171. for model in ("ParagraphRow", "ElementRow", "ParagraphElementLinkRow")
  172. }
  173. assert counts == {
  174. "ParagraphRow": canonical["paragraph_count"],
  175. "ElementRow": canonical["element_count"],
  176. "ParagraphElementLinkRow": canonical["link_count"],
  177. }
  178. def test_node_detail_and_not_found() -> None:
  179. node_id = GRAPH.nodes[10].id
  180. response = client.get(f"/api/runs/{RUN_ID}/nodes/{node_id}")
  181. assert response.status_code == 200
  182. assert response.json()["node"]["id"] == node_id
  183. assert client.get(f"/api/runs/999/nodes/{node_id}").status_code == 404
  184. assert client.get(f"/api/runs/{RUN_ID}/nodes/missing").status_code == 404