test_app_api.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  1. from __future__ import annotations
  2. from contextlib import nullcontext
  3. from pathlib import Path
  4. from uuid import uuid4
  5. from fastapi.testclient import TestClient
  6. from acquisition.domain import AcquisitionRun, Query, QueryBatch
  7. from app.api import app
  8. from app.dependencies import (
  9. get_acquisition_repository,
  10. get_creation_db_config,
  11. get_decode_repository,
  12. get_pipeline_repository,
  13. )
  14. from app.routes import decode as decode_routes
  15. from app.routes import manual_queries
  16. from decode_content.models import (
  17. DecodeJob,
  18. DecodeResult,
  19. IngestRecord,
  20. KnowledgeParticle,
  21. PayloadDraft,
  22. ScopeResult,
  23. )
  24. from pipeline.models import PipelineRun
  25. class FakeAcquisitionRepo:
  26. def __init__(self):
  27. self.batch_id = uuid4()
  28. self.run_id = uuid4()
  29. self.query_id = uuid4()
  30. self.item_id = uuid4()
  31. self.media_id = uuid4()
  32. self.classification_id = uuid4()
  33. def get_query_batch(self, batch_id):
  34. assert batch_id == self.batch_id
  35. return QueryBatch(
  36. id=batch_id,
  37. name="正式 query 批次",
  38. source_type="manual",
  39. target_platforms=["xiaohongshu", "weixin", "douyin"],
  40. status="ready",
  41. )
  42. def list_queries_for_batch(self, batch_id, *, keep=None):
  43. assert batch_id == self.batch_id
  44. return [
  45. Query(
  46. id=self.query_id,
  47. batch_id=batch_id,
  48. query_text="短视频脚本 开头 怎么写",
  49. keep=True,
  50. status="ready",
  51. )
  52. ]
  53. def get_run_summary(self, run_id):
  54. assert run_id == self.run_id
  55. return {
  56. "id": run_id,
  57. "run_key": "run-1",
  58. "batch_id": self.batch_id,
  59. "status": "running",
  60. "query_count": 1,
  61. "job_count": 3,
  62. "candidate_count": 5,
  63. "creation_hit_count": 2,
  64. "queries": [
  65. {
  66. "query_id": self.query_id,
  67. "query_text": "短视频脚本 开头 怎么写",
  68. "candidate_count": 5,
  69. "creation_hit_count": 2,
  70. "platforms": {"xiaohongshu": {"status": "done"}},
  71. }
  72. ],
  73. }
  74. def get_query_detail(self, *, run_id, query_id):
  75. assert run_id == self.run_id
  76. assert query_id == self.query_id
  77. return {
  78. "query": {
  79. "id": query_id,
  80. "batch_id": self.batch_id,
  81. "query_text": "短视频脚本 开头 怎么写",
  82. "axes": {},
  83. "keep": True,
  84. "filter_reason": None,
  85. "status": "ready",
  86. "sort_order": 0,
  87. },
  88. "jobs": [{"id": uuid4(), "platform": "xiaohongshu", "status": "done"}],
  89. "items": [
  90. {
  91. "id": self.item_id,
  92. "platform": "xiaohongshu",
  93. "platform_item_id": "xhs1",
  94. "unique_key": "xhs:xhs1",
  95. "canonical_url": "https://xhs.test/1",
  96. "content_type": "图文",
  97. "content_mode": "image_post",
  98. "title": "脚本开头",
  99. "author_name": "作者",
  100. "body_text": "先定受众,再写开头钩子",
  101. "raw_summary": "先定受众",
  102. "status": "candidate",
  103. }
  104. ],
  105. "media_assets": [
  106. {
  107. "id": self.media_id,
  108. "item_id": self.item_id,
  109. "media_type": "image",
  110. "source_url": "https://origin.test/1.jpg",
  111. "oss_url": "https://oss.test/1.jpg",
  112. "cdn_url": "https://cdn.test/1.jpg",
  113. "position": 1,
  114. "status": "done",
  115. }
  116. ],
  117. "classifications": [
  118. {
  119. "id": self.classification_id,
  120. "item_id": self.item_id,
  121. "is_creation_knowledge": True,
  122. "label": "creation",
  123. "confidence": 0.98,
  124. "reason": "可复用",
  125. "status": "done",
  126. "error_message": None,
  127. }
  128. ],
  129. }
  130. def get_latest_query_result_list(self, query_id):
  131. assert query_id == self.query_id
  132. return {
  133. "query": {
  134. "id": query_id,
  135. "batch_id": self.batch_id,
  136. "query_text": "短视频脚本 开头 怎么写",
  137. "axes": {},
  138. "keep": True,
  139. "filter_reason": None,
  140. "status": "ready",
  141. "sort_order": 0,
  142. },
  143. "run": {
  144. "id": self.run_id,
  145. "batch_id": self.batch_id,
  146. "run_key": "run-1",
  147. "status": "running",
  148. },
  149. "jobs": [{"id": uuid4(), "platform": "xiaohongshu", "status": "done"}],
  150. "items": [
  151. {
  152. "id": self.item_id,
  153. "platform": "xiaohongshu",
  154. "title": "脚本开头",
  155. "raw_summary": "先定受众",
  156. "status": "candidate",
  157. "content_mode": "image_post",
  158. "metadata": {
  159. "page_index": 1,
  160. "matched_candidate": {"raw": "large"},
  161. "source_payload": {"raw": "large"},
  162. },
  163. }
  164. ],
  165. "media_assets": [
  166. {
  167. "id": self.media_id,
  168. "item_id": self.item_id,
  169. "media_type": "image",
  170. "source_url": "https://origin.test/1.jpg",
  171. "oss_url": "https://oss.test/1.jpg",
  172. "cdn_url": "https://cdn.test/1.jpg",
  173. "position": 1,
  174. "status": "done",
  175. }
  176. ],
  177. "classifications": [
  178. {
  179. "id": self.classification_id,
  180. "item_id": self.item_id,
  181. "is_creation_knowledge": True,
  182. "label": "creation",
  183. "confidence": 0.98,
  184. "status": "done",
  185. "error_message": None,
  186. }
  187. ],
  188. "decode_summaries": [
  189. {
  190. "item_id": self.item_id,
  191. "decode_status": "decoded",
  192. "particle_count": 1,
  193. "payload_count": 1,
  194. }
  195. ],
  196. }
  197. def get_candidate_item(self, item_id):
  198. assert item_id == self.item_id
  199. return self.get_query_detail(run_id=self.run_id, query_id=self.query_id)["items"][0]
  200. def list_media_assets_for_item(self, item_id):
  201. assert item_id == self.item_id
  202. return self.get_query_detail(run_id=self.run_id, query_id=self.query_id)["media_assets"]
  203. class FakeDecodeRepo:
  204. def __init__(self):
  205. self.decode_result_id = uuid4()
  206. self.decode_job_id = uuid4()
  207. self.payload_id = uuid4()
  208. self.other_payload_id = uuid4()
  209. self.marked_payload_ids = []
  210. self.saved_ingest_records = []
  211. def get_decode_result_for_item(self, item_id):
  212. return DecodeResult(
  213. id=self.decode_result_id,
  214. item_id=item_id,
  215. status="decoded",
  216. read_result={"text": "读懂后的帖子内容"},
  217. gate_result={"passed": True, "reason": "具备可复用创作方法"},
  218. framing_result={"particles": []},
  219. )
  220. def create_decode_job(self, *, item_id, status="pending", metadata=None):
  221. return DecodeJob(
  222. id=self.decode_job_id,
  223. item_id=item_id,
  224. status=status,
  225. metadata=metadata or {},
  226. )
  227. def list_payload_drafts(self, item_id=None):
  228. return [
  229. PayloadDraft(
  230. id=self.payload_id,
  231. item_id=item_id or uuid4(),
  232. payload={
  233. "source": {"id": "xhs_1", "source_type": "post", "title": "脚本开头", "author": "作者"},
  234. "title": "可审核 payload",
  235. "content": "先定受众,再写开头钩子。",
  236. "dim_attributes": ["how"],
  237. "dim_creations": ["创作"],
  238. "scopes": [{"scope_type": "intent", "value": "吸引注意"}],
  239. "custom_ext": [{"key": "业务阶段", "type": "str", "value": "脚本"}],
  240. },
  241. review_status="pending",
  242. ingest_ready=False,
  243. status="draft",
  244. )
  245. ]
  246. def mark_payload_draft_ingested(self, payload_draft_id):
  247. self.marked_payload_ids.append(payload_draft_id)
  248. return PayloadDraft(
  249. id=payload_draft_id,
  250. item_id=uuid4(),
  251. payload=self.list_payload_drafts()[0].payload,
  252. review_status="approved",
  253. ingest_ready=True,
  254. status="ingested",
  255. )
  256. def save_ingest_record(self, **kwargs):
  257. record = IngestRecord(id=uuid4(), **kwargs)
  258. self.saved_ingest_records.append(record)
  259. return record
  260. def list_knowledge_particles(self, item_id=None):
  261. return [
  262. KnowledgeParticle(
  263. id=uuid4(),
  264. item_id=item_id or uuid4(),
  265. particle_type="what",
  266. title="What I know",
  267. content={"content": "一个可复用知识点"},
  268. status="draft",
  269. )
  270. ]
  271. def list_scope_results(self, item_id=None):
  272. return [
  273. ScopeResult(
  274. id=uuid4(),
  275. item_id=item_id or uuid4(),
  276. scope_type="intent",
  277. scope_value="吸引注意",
  278. status="draft",
  279. )
  280. ]
  281. def list_ingest_records(self, item_id=None):
  282. if self.saved_ingest_records:
  283. return self.saved_ingest_records
  284. return [
  285. IngestRecord(
  286. id=uuid4(),
  287. payload_draft_id=self.payload_id,
  288. target_system="dry-run",
  289. status="ingested",
  290. )
  291. ]
  292. class FakePipelineRepo:
  293. def get_pipeline_run(self, run_id):
  294. return PipelineRun(
  295. id=run_id,
  296. run_key="pipeline-run-1",
  297. status="running",
  298. current_stage="decode",
  299. )
  300. def get_pipeline_run_by_acquisition_run(self, acquisition_run_id):
  301. return PipelineRun(
  302. id=uuid4(),
  303. run_key=f"pipeline-for-{acquisition_run_id}",
  304. status="running",
  305. current_stage="decode",
  306. )
  307. def list_timeline(self, run_id):
  308. return [
  309. {
  310. "id": str(uuid4()),
  311. "pipeline_run_id": str(run_id),
  312. "stage": "decode",
  313. "event_type": "decode_item_finished",
  314. "status": "done",
  315. }
  316. ]
  317. def get_timeline_bundle(self, run_id):
  318. return {
  319. "events": self.list_timeline(run_id),
  320. "jobs": [{"id": str(uuid4()), "stage": "decode", "status": "done"}],
  321. "candidate_hits": [{"id": str(uuid4()), "platform": "xiaohongshu"}],
  322. "llm_call_traces": [{"id": str(uuid4()), "stage": "decode", "status": "success"}],
  323. }
  324. class FakeManualRepo:
  325. def __init__(self):
  326. self.batch_id = uuid4()
  327. self.run_id = uuid4()
  328. self.queries = []
  329. self.runs = []
  330. self.updates = []
  331. def create_query_batch(self, **kwargs):
  332. self.batch_kwargs = kwargs
  333. return QueryBatch(id=self.batch_id, **kwargs)
  334. def add_query(self, **kwargs):
  335. query = Query(id=uuid4(), **kwargs)
  336. self.queries.append(query)
  337. return query
  338. def create_acquisition_run(self, **kwargs):
  339. run = AcquisitionRun(id=self.run_id, **kwargs)
  340. self.runs.append(run)
  341. return run
  342. def update_acquisition_run(self, run_id, **kwargs):
  343. assert run_id == self.run_id
  344. self.updates.append(kwargs)
  345. base = self.runs[-1].model_dump()
  346. base.update(kwargs)
  347. return AcquisitionRun(**base)
  348. def _client(repo: FakeAcquisitionRepo):
  349. app.dependency_overrides[get_acquisition_repository] = lambda: repo
  350. return TestClient(app)
  351. def test_manual_query_batch_api_creates_ready_batch_and_starts_pipeline(monkeypatch, tmp_path):
  352. repo = FakeManualRepo()
  353. popen_calls = []
  354. class FakeProcess:
  355. pid = 12345
  356. def fake_popen(cmd, **kwargs):
  357. popen_calls.append((cmd, kwargs))
  358. return FakeProcess()
  359. monkeypatch.setattr(manual_queries, "transaction", lambda _config: nullcontext(object()))
  360. monkeypatch.setattr(manual_queries, "PostgresAcquisitionRepository", lambda _conn: repo)
  361. monkeypatch.setattr(manual_queries, "RUNTIME_MANUAL_DIR", tmp_path)
  362. monkeypatch.setattr(manual_queries.subprocess, "Popen", fake_popen)
  363. app.dependency_overrides[get_creation_db_config] = lambda: object()
  364. client = TestClient(app)
  365. try:
  366. response = client.post(
  367. "/api/query-batches/manual",
  368. json={
  369. "queries": [
  370. "符号 视频 灵感 怎么做",
  371. "符号 视频 灵感 怎么做",
  372. {"query_text": "叙事体裁 视频 灵感 有哪些", "axes": {"实质": "叙事体裁"}},
  373. ]
  374. },
  375. )
  376. assert response.status_code == 200
  377. data = response.json()
  378. assert data["status"] == "queued"
  379. assert data["batch_id"] == str(repo.batch_id)
  380. assert "pipeline_run_id" in data
  381. assert data["run_id"] == str(repo.run_id)
  382. assert data["pid"] == 12345
  383. assert data["query_count"] == 2
  384. assert data["summary_url"] == f"/api/acquisition/runs/{repo.run_id}/summary"
  385. assert repo.batch_kwargs["generation_method"] == "manual_query_api_v1"
  386. assert repo.batch_kwargs["target_platforms"] == ["xiaohongshu", "weixin", "douyin"]
  387. assert [query.query_text for query in repo.queries] == [
  388. "符号 视频 灵感 怎么做",
  389. "叙事体裁 视频 灵感 有哪些",
  390. ]
  391. assert all(query.keep is True and query.status == "ready" for query in repo.queries)
  392. assert all(query.metadata["family_key"] == "manual" for query in repo.queries)
  393. assert repo.runs[0].status == "pending"
  394. assert repo.runs[0].metadata["log_path"].endswith(".log")
  395. assert repo.runs[0].metadata["command"]
  396. assert popen_calls
  397. cmd, kwargs = popen_calls[0]
  398. assert "scripts/run_creation_pipeline.py" in cmd[1]
  399. assert ["--platform", "xiaohongshu"] == cmd[-6:-4]
  400. assert "--no-dry-ingest-record" not in cmd
  401. assert kwargs["cwd"] == str(manual_queries.ROOT)
  402. assert str(manual_queries.ROOT) in kwargs["env"]["PYTHONPATH"]
  403. assert repo.updates == []
  404. finally:
  405. app.dependency_overrides.clear()
  406. def test_manual_query_batch_api_extracts_family_json(monkeypatch, tmp_path):
  407. repo = FakeManualRepo()
  408. monkeypatch.setattr(manual_queries, "transaction", lambda _config: nullcontext(object()))
  409. monkeypatch.setattr(manual_queries, "PostgresAcquisitionRepository", lambda _conn: repo)
  410. monkeypatch.setattr(manual_queries, "RUNTIME_MANUAL_DIR", tmp_path)
  411. monkeypatch.setattr(manual_queries.subprocess, "Popen", lambda *args, **kwargs: type("P", (), {"pid": 1})())
  412. app.dependency_overrides[get_creation_db_config] = lambda: object()
  413. client = TestClient(app)
  414. try:
  415. response = client.post(
  416. "/api/query-batches/manual",
  417. json={
  418. "families": [
  419. {
  420. "key": "f1",
  421. "name": "实质正交",
  422. "items": [{"query": "公共安全 视频 灵感 怎么做"}],
  423. }
  424. ]
  425. },
  426. )
  427. assert response.status_code == 200
  428. assert repo.queries[0].query_text == "公共安全 视频 灵感 怎么做"
  429. assert repo.queries[0].metadata["family_key"] == "manual"
  430. assert repo.queries[0].metadata["source_family_key"] == "f1"
  431. finally:
  432. app.dependency_overrides.clear()
  433. def test_manual_query_batch_api_rejects_empty_and_invalid_platform():
  434. client = TestClient(app)
  435. assert client.post("/api/query-batches/manual", json={"queries": [" "]}).status_code == 422
  436. assert client.post(
  437. "/api/query-batches/manual",
  438. json={"queries": ["符号 视频 灵感 怎么做"], "target_platforms": ["bilibili"]},
  439. ).status_code == 422
  440. def test_app_health_and_formal_acquisition_routes():
  441. repo = FakeAcquisitionRepo()
  442. client = _client(repo)
  443. try:
  444. assert client.get("/health").json()["entrypoint"] == "app.api:app"
  445. batch = client.get(f"/api/query-batches/{repo.batch_id}").json()
  446. assert batch["batch"]["name"] == "正式 query 批次"
  447. assert batch["queries"][0]["id"] == str(repo.query_id)
  448. summary = client.get(f"/api/acquisition/runs/{repo.run_id}/summary").json()
  449. assert summary["creation_hit_count"] == 2
  450. assert summary["queries"][0]["query_id"] == str(repo.query_id)
  451. detail = client.get(f"/api/acquisition/runs/{repo.run_id}/queries/{repo.query_id}").json()
  452. item = detail["platforms"]["xiaohongshu"]["items"][0]
  453. assert item["content_mode"] == "image_post"
  454. assert item["body_text"] == "先定受众,再写开头钩子"
  455. assert item["media_assets"][0]["cdn_url"] == "https://cdn.test/1.jpg"
  456. assert item["classification"]["is_creation_knowledge"] is True
  457. latest = client.get(f"/api/query-generation/latest/queries/{repo.query_id}").json()
  458. lightweight_item = latest["items"][0]
  459. assert "body_text" not in lightweight_item
  460. assert "source_payload" not in lightweight_item
  461. assert "matched_candidate" not in lightweight_item["metadata"]
  462. assert "source_payload" not in lightweight_item["metadata"]
  463. assert lightweight_item["raw_summary"] == "先定受众"
  464. assert len(lightweight_item["media_assets"]) == 1
  465. assert lightweight_item["decode_summary"]["particle_count"] == 1
  466. assert latest["platforms"]["xiaohongshu"]["item_ids"] == [str(repo.item_id)]
  467. assert "items" not in latest["platforms"]["xiaohongshu"]
  468. finally:
  469. app.dependency_overrides.clear()
  470. def test_app_compresses_large_json_responses():
  471. repo = FakeAcquisitionRepo()
  472. client = _client(repo)
  473. try:
  474. response = client.get(
  475. f"/api/query-generation/latest/queries/{repo.query_id}",
  476. headers={"Accept-Encoding": "gzip"},
  477. )
  478. assert response.status_code == 200
  479. assert response.headers.get("content-encoding") == "gzip"
  480. finally:
  481. app.dependency_overrides.clear()
  482. def test_query_generation_preview_defaults_to_first_two_families():
  483. client = TestClient(app)
  484. data = client.get("/api/query-generation/preview?per=2").json()
  485. assert data["summary"]["family_keys"] == ["f1", "f2"]
  486. assert [family["key"] for family in data["families"]] == ["f1", "f2"]
  487. assert data["summary"]["query_count"] == 4
  488. assert data["metadata"]["active_family_keys"] == ["f1", "f2"]
  489. def test_query_generation_preview_can_return_full_cartesian_combinations():
  490. client = TestClient(app)
  491. data = client.get("/api/query-generation/preview?per=0&batch_n=1").json()
  492. assert data["summary"]["family_keys"] == ["f1", "f2"]
  493. assert data["summary"]["query_count"] == 36
  494. assert [len(family["items"]) for family in data["families"]] == [18, 18]
  495. def test_formal_app_does_not_expose_legacy_static_mounts():
  496. paths = {route.path for route in app.routes if hasattr(route, "path")}
  497. assert "/data" not in paths
  498. assert "/frames" not in paths
  499. assert "/api/creation-search/summary" not in paths
  500. assert "/api/creation-search/query" not in paths
  501. def test_pipeline_routes_return_trace_run_and_timeline():
  502. pipeline_repo = FakePipelineRepo()
  503. app.dependency_overrides[get_pipeline_repository] = lambda: pipeline_repo
  504. client = TestClient(app)
  505. run_id = uuid4()
  506. acquisition_run_id = uuid4()
  507. try:
  508. run = client.get(f"/api/pipeline/runs/{run_id}").json()["run"]
  509. assert run["id"] == str(run_id)
  510. assert run["current_stage"] == "decode"
  511. timeline = client.get(f"/api/pipeline/runs/{run_id}/timeline").json()
  512. assert timeline["events"][0]["event_type"] == "decode_item_finished"
  513. assert timeline["jobs"][0]["stage"] == "decode"
  514. assert timeline["candidate_hits"][0]["platform"] == "xiaohongshu"
  515. assert timeline["llm_call_traces"][0]["status"] == "success"
  516. by_acq = client.get(f"/api/pipeline/runs/by-acquisition/{acquisition_run_id}").json()["run"]
  517. assert by_acq["run_key"] == f"pipeline-for-{acquisition_run_id}"
  518. finally:
  519. app.dependency_overrides.clear()
  520. def test_formal_decode_payload_pipeline_routes_have_override_success_paths():
  521. acquisition_repo = FakeAcquisitionRepo()
  522. decode_repo = FakeDecodeRepo()
  523. pipeline_repo = FakePipelineRepo()
  524. item_id = acquisition_repo.item_id
  525. run_id = uuid4()
  526. app.dependency_overrides[get_acquisition_repository] = lambda: acquisition_repo
  527. app.dependency_overrides[get_decode_repository] = lambda: decode_repo
  528. app.dependency_overrides[get_pipeline_repository] = lambda: pipeline_repo
  529. client = TestClient(app)
  530. try:
  531. result = client.get(f"/api/decode/items/{item_id}/result").json()
  532. assert result["item_id"] == str(item_id)
  533. assert result["read_result"]["text"] == "读懂后的帖子内容"
  534. job = client.post(f"/api/decode/items/{item_id}/jobs").json()["job"]
  535. assert job["item_id"] == str(item_id)
  536. assert job["status"] == "pending"
  537. drafts = client.get(f"/api/payloads/drafts?item_id={item_id}").json()["items"]
  538. assert drafts[0]["item_id"] == str(item_id)
  539. assert drafts[0]["payload"]["title"] == "可审核 payload"
  540. ingest = client.post(f"/api/decode/items/{item_id}/ingest").json()
  541. assert ingest["dry_run"] is True
  542. assert ingest["total"] == 1
  543. assert ingest["ingested_count"] == 1
  544. assert ingest["failed_count"] == 0
  545. assert decode_repo.marked_payload_ids == [decode_repo.payload_id]
  546. assert decode_repo.saved_ingest_records[0].target_system == "dry-run"
  547. detail = client.get(f"/api/decode/items/{item_id}/detail").json()
  548. assert detail["item"]["id"] == str(item_id)
  549. assert detail["knowledge_particles"][0]["title"] == "What I know"
  550. assert detail["ingest_records"][0]["status"] == "ingested"
  551. run = client.get(f"/api/pipeline/runs/{run_id}").json()["run"]
  552. assert run["id"] == str(run_id)
  553. assert run["current_stage"] == "decode"
  554. finally:
  555. app.dependency_overrides.clear()
  556. def test_item_ingest_real_mode_requires_configured_url(monkeypatch):
  557. acquisition_repo = FakeAcquisitionRepo()
  558. decode_repo = FakeDecodeRepo()
  559. item_id = acquisition_repo.item_id
  560. app.dependency_overrides[get_decode_repository] = lambda: decode_repo
  561. monkeypatch.setattr(decode_routes.IngestApiConfig, "from_env", lambda _env_file: decode_routes.IngestApiConfig())
  562. client = TestClient(app)
  563. try:
  564. response = client.post(f"/api/decode/items/{item_id}/ingest", json={"dry_run": False})
  565. assert response.status_code == 400
  566. assert "CK_INGEST_API_URL" in response.json()["detail"]
  567. assert decode_repo.saved_ingest_records == []
  568. finally:
  569. app.dependency_overrides.clear()
  570. def test_no_legacy_api_dependencies_in_formal_app_sources():
  571. roots = [Path("app"), Path("pipeline")]
  572. text = "\n".join(
  573. path.read_text(encoding="utf-8")
  574. for root in roots
  575. for path in root.rglob("*")
  576. if path.is_file() and path.suffix in {".py", ".jsx", ".js"}
  577. )
  578. assert "acquisition.store" not in text
  579. assert "CK_SQLITE_PATH" not in text
  580. assert "/data/queries" not in text
  581. assert "/api/creation-search" not in text
  582. assert "creation_knowledge.api" not in text
  583. def test_legacy_top_level_package_is_archived():
  584. assert not Path("creation_knowledge").exists()
  585. assert Path("archive/2026-06-30-step6/legacy_creation_knowledge/creation_knowledge").exists()
  586. def test_active_sources_do_not_import_legacy_store_or_package():
  587. roots = [Path("app"), Path("pipeline"), Path("acquisition"), Path("decode_content"), Path("scripts")]
  588. text = "\n".join(
  589. path.read_text(encoding="utf-8")
  590. for root in roots
  591. for path in root.rglob("*")
  592. if path.is_file() and path.suffix in {".py", ".jsx", ".js"}
  593. )
  594. assert "from acquisition import store" not in text
  595. assert "import acquisition.store" not in text
  596. assert "from creation_knowledge" not in text
  597. assert "import creation_knowledge" not in text