test_app_api.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. from __future__ import annotations
  2. from pathlib import Path
  3. from uuid import uuid4
  4. from fastapi.testclient import TestClient
  5. from acquisition.domain import Query, QueryBatch
  6. from app.api import app
  7. from app.dependencies import (
  8. get_acquisition_repository,
  9. get_decode_repository,
  10. get_pipeline_repository,
  11. )
  12. from decode_content.models import DecodeJob, DecodeResult, PayloadDraft
  13. from pipeline.models import PipelineRun
  14. class FakeAcquisitionRepo:
  15. def __init__(self):
  16. self.batch_id = uuid4()
  17. self.run_id = uuid4()
  18. self.query_id = uuid4()
  19. self.item_id = uuid4()
  20. self.media_id = uuid4()
  21. self.classification_id = uuid4()
  22. def get_query_batch(self, batch_id):
  23. assert batch_id == self.batch_id
  24. return QueryBatch(
  25. id=batch_id,
  26. name="正式 query 批次",
  27. source_type="manual",
  28. target_platforms=["xiaohongshu", "weixin", "douyin"],
  29. status="ready",
  30. )
  31. def list_queries_for_batch(self, batch_id, *, keep=None):
  32. assert batch_id == self.batch_id
  33. return [
  34. Query(
  35. id=self.query_id,
  36. batch_id=batch_id,
  37. query_text="短视频脚本 开头 怎么写",
  38. keep=True,
  39. status="ready",
  40. )
  41. ]
  42. def get_run_summary(self, run_id):
  43. assert run_id == self.run_id
  44. return {
  45. "id": run_id,
  46. "run_key": "run-1",
  47. "batch_id": self.batch_id,
  48. "status": "running",
  49. "query_count": 1,
  50. "job_count": 3,
  51. "candidate_count": 5,
  52. "creation_hit_count": 2,
  53. "queries": [
  54. {
  55. "query_id": self.query_id,
  56. "query_text": "短视频脚本 开头 怎么写",
  57. "candidate_count": 5,
  58. "creation_hit_count": 2,
  59. "platforms": {"xiaohongshu": {"status": "done"}},
  60. }
  61. ],
  62. }
  63. def get_query_detail(self, *, run_id, query_id):
  64. assert run_id == self.run_id
  65. assert query_id == self.query_id
  66. return {
  67. "query": {
  68. "id": query_id,
  69. "batch_id": self.batch_id,
  70. "query_text": "短视频脚本 开头 怎么写",
  71. "axes": {},
  72. "keep": True,
  73. "filter_reason": None,
  74. "status": "ready",
  75. "sort_order": 0,
  76. },
  77. "jobs": [{"id": uuid4(), "platform": "xiaohongshu", "status": "done"}],
  78. "items": [
  79. {
  80. "id": self.item_id,
  81. "platform": "xiaohongshu",
  82. "platform_item_id": "xhs1",
  83. "canonical_url": "https://xhs.test/1",
  84. "content_type": "图文",
  85. "title": "脚本开头",
  86. "author_name": "作者",
  87. "raw_summary": "先定受众",
  88. "status": "candidate",
  89. }
  90. ],
  91. "media_assets": [
  92. {
  93. "id": self.media_id,
  94. "item_id": self.item_id,
  95. "media_type": "image",
  96. "source_url": "https://origin.test/1.jpg",
  97. "oss_url": "https://oss.test/1.jpg",
  98. "cdn_url": "https://cdn.test/1.jpg",
  99. "position": 1,
  100. "status": "done",
  101. }
  102. ],
  103. "classifications": [
  104. {
  105. "id": self.classification_id,
  106. "item_id": self.item_id,
  107. "is_creation_knowledge": True,
  108. "label": "creation",
  109. "confidence": 0.98,
  110. "reason": "可复用",
  111. "status": "done",
  112. }
  113. ],
  114. }
  115. class FakeDecodeRepo:
  116. def __init__(self):
  117. self.decode_result_id = uuid4()
  118. self.decode_job_id = uuid4()
  119. self.payload_id = uuid4()
  120. def get_decode_result_for_item(self, item_id):
  121. return DecodeResult(
  122. id=self.decode_result_id,
  123. item_id=item_id,
  124. status="decoded",
  125. read_result={"text": "读懂后的帖子内容"},
  126. gate_result={"passed": True, "reason": "具备可复用创作方法"},
  127. framing_result={"particles": []},
  128. )
  129. def create_decode_job(self, *, item_id, status="pending", metadata=None):
  130. return DecodeJob(
  131. id=self.decode_job_id,
  132. item_id=item_id,
  133. status=status,
  134. metadata=metadata or {},
  135. )
  136. def list_payload_drafts(self, item_id=None):
  137. return [
  138. PayloadDraft(
  139. id=self.payload_id,
  140. item_id=item_id or uuid4(),
  141. payload={"title": "可审核 payload"},
  142. review_status="pending",
  143. ingest_ready=False,
  144. status="draft",
  145. )
  146. ]
  147. class FakePipelineRepo:
  148. def get_pipeline_run(self, run_id):
  149. return PipelineRun(
  150. id=run_id,
  151. run_key="pipeline-run-1",
  152. status="running",
  153. current_stage="decode",
  154. )
  155. def _client(repo: FakeAcquisitionRepo):
  156. app.dependency_overrides[get_acquisition_repository] = lambda: repo
  157. return TestClient(app)
  158. def test_app_health_and_formal_acquisition_routes():
  159. repo = FakeAcquisitionRepo()
  160. client = _client(repo)
  161. try:
  162. assert client.get("/health").json()["entrypoint"] == "app.api:app"
  163. batch = client.get(f"/api/query-batches/{repo.batch_id}").json()
  164. assert batch["batch"]["name"] == "正式 query 批次"
  165. assert batch["queries"][0]["id"] == str(repo.query_id)
  166. summary = client.get(f"/api/acquisition/runs/{repo.run_id}/summary").json()
  167. assert summary["creation_hit_count"] == 2
  168. assert summary["queries"][0]["query_id"] == str(repo.query_id)
  169. detail = client.get(f"/api/acquisition/runs/{repo.run_id}/queries/{repo.query_id}").json()
  170. item = detail["platforms"]["xiaohongshu"]["items"][0]
  171. assert item["media_assets"][0]["cdn_url"] == "https://cdn.test/1.jpg"
  172. assert item["classification"]["is_creation_knowledge"] is True
  173. finally:
  174. app.dependency_overrides.clear()
  175. def test_formal_app_does_not_expose_legacy_static_mounts():
  176. paths = {route.path for route in app.routes if hasattr(route, "path")}
  177. assert "/data" not in paths
  178. assert "/frames" not in paths
  179. assert "/api/creation-search/summary" not in paths
  180. assert "/api/creation-search/query" not in paths
  181. def test_unimplemented_formal_routes_return_501_instead_of_legacy_data():
  182. client = TestClient(app)
  183. item_id = uuid4()
  184. run_id = uuid4()
  185. assert client.get(f"/api/decode/items/{item_id}/result").status_code == 501
  186. assert client.post(f"/api/decode/items/{item_id}/jobs").status_code == 501
  187. assert client.get("/api/payloads/drafts").status_code == 501
  188. assert client.get(f"/api/pipeline/runs/{run_id}").status_code == 501
  189. def test_formal_decode_payload_pipeline_routes_have_override_success_paths():
  190. decode_repo = FakeDecodeRepo()
  191. pipeline_repo = FakePipelineRepo()
  192. item_id = uuid4()
  193. run_id = uuid4()
  194. app.dependency_overrides[get_decode_repository] = lambda: decode_repo
  195. app.dependency_overrides[get_pipeline_repository] = lambda: pipeline_repo
  196. client = TestClient(app)
  197. try:
  198. result = client.get(f"/api/decode/items/{item_id}/result").json()
  199. assert result["item_id"] == str(item_id)
  200. assert result["read_result"]["text"] == "读懂后的帖子内容"
  201. job = client.post(f"/api/decode/items/{item_id}/jobs").json()["job"]
  202. assert job["item_id"] == str(item_id)
  203. assert job["status"] == "pending"
  204. drafts = client.get(f"/api/payloads/drafts?item_id={item_id}").json()["items"]
  205. assert drafts[0]["item_id"] == str(item_id)
  206. assert drafts[0]["payload"]["title"] == "可审核 payload"
  207. run = client.get(f"/api/pipeline/runs/{run_id}").json()["run"]
  208. assert run["id"] == str(run_id)
  209. assert run["current_stage"] == "decode"
  210. finally:
  211. app.dependency_overrides.clear()
  212. def test_no_legacy_api_dependencies_in_formal_app_sources():
  213. roots = [Path("app"), Path("pipeline")]
  214. text = "\n".join(
  215. path.read_text(encoding="utf-8")
  216. for root in roots
  217. for path in root.rglob("*")
  218. if path.is_file() and path.suffix in {".py", ".jsx", ".js"}
  219. )
  220. assert "acquisition.store" not in text
  221. assert "CK_SQLITE_PATH" not in text
  222. assert "/data/queries" not in text
  223. assert "/api/creation-search" not in text
  224. assert "creation_knowledge.api" not in text
  225. def test_legacy_top_level_package_is_archived():
  226. assert not Path("creation_knowledge").exists()
  227. assert Path("archive/2026-06-30-step6/legacy_creation_knowledge/creation_knowledge").exists()
  228. def test_active_sources_do_not_import_legacy_store_or_package():
  229. roots = [Path("app"), Path("pipeline"), Path("acquisition"), Path("decode_content"), Path("scripts")]
  230. text = "\n".join(
  231. path.read_text(encoding="utf-8")
  232. for root in roots
  233. for path in root.rglob("*")
  234. if path.is_file() and path.suffix in {".py", ".jsx", ".js"}
  235. )
  236. assert "from acquisition import store" not in text
  237. assert "import acquisition.store" not in text
  238. assert "from creation_knowledge" not in text
  239. assert "import creation_knowledge" not in text