test_p0d_p0g.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. from __future__ import annotations
  2. import json
  3. from pathlib import Path
  4. from typing import Any
  5. from fastapi.testclient import TestClient
  6. from content_agent import api
  7. from content_agent.errors import ErrorCode
  8. from content_agent.integrations.composite_runtime import CompositeRuntimeStore
  9. from content_agent.integrations.database_runtime import ContentSupplyDbConfig
  10. from content_agent.integrations.demand_source import DemandSourceService
  11. from content_agent.integrations.mock_platform import MockPlatformClient
  12. from content_agent.integrations.runtime_files import LocalRuntimeFileStore
  13. from content_agent.run_service import RunService
  14. from content_agent.schemas import RunStartRequest
  15. from tests.p1_helpers import (
  16. FakeDemandSource,
  17. FakeQueryVariantClient,
  18. REAL_SOURCE_FIXTURE,
  19. real_source_payload,
  20. )
  21. def test_composite_runtime_writes_primary_before_local(tmp_path):
  22. primary = _FakeRuntimeStore()
  23. export = _FakeRuntimeStore(run_dir=tmp_path / "export")
  24. store = CompositeRuntimeStore(primary, export)
  25. store.write_json("run_001", "final_output.json", {"run_id": "run_001"})
  26. store.append_jsonl("run_001", "run_events.jsonl", [{"run_id": "run_001"}])
  27. assert primary.calls == [
  28. ("write_json", "final_output.json"),
  29. ("append_jsonl", "run_events.jsonl"),
  30. ]
  31. assert export.calls == [
  32. ("write_json", "final_output.json"),
  33. ("append_jsonl", "run_events.jsonl"),
  34. ]
  35. def test_composite_runtime_db_failure_blocks_local_export(tmp_path):
  36. primary = _FakeRuntimeStore(fail_writes=True)
  37. export = _FakeRuntimeStore(run_dir=tmp_path / "export")
  38. store = CompositeRuntimeStore(primary, export)
  39. try:
  40. store.write_json("run_001", "final_output.json", {"run_id": "run_001"})
  41. except RuntimeError as exc:
  42. assert "primary failed" in str(exc)
  43. else:
  44. raise AssertionError("expected primary write failure")
  45. assert primary.calls == [("write_json", "final_output.json")]
  46. assert export.calls == []
  47. def test_run_service_success_records_run_policy_and_lifecycle_events(tmp_path):
  48. runtime = _SpyRuntimeStore(tmp_path / "runtime")
  49. demand_source = FakeDemandSource(real_source_payload(demand_content_id=123))
  50. service = RunService(
  51. runtime=runtime,
  52. demand_source=demand_source,
  53. query_variant_client=FakeQueryVariantClient(),
  54. )
  55. state = service.start_run(RunStartRequest(platform_mode="mock"))
  56. assert state["status"] == "success"
  57. assert demand_source.calls == ["get_default_pg_pattern_source"]
  58. assert service.read_json(state["run_id"], "source_context.json")["demand_content_id"] == "123"
  59. assert runtime.run_records[0]["status"] == "running"
  60. assert runtime.run_records[0]["source_ref"]["source_type"] == "demand_content_default"
  61. assert runtime.run_updates[0]["updates"]["demand_content_id"] == 123
  62. assert runtime.run_updates[0]["updates"]["source_ref"]["demand_content_id"] == 123
  63. assert runtime.run_updates[-1]["updates"]["status"] == "success"
  64. assert runtime.policy_runs[0]["policy_bundle_id"] == "douyin_policy_bundle_v1"
  65. assert runtime.policy_runs[0]["decision_summary"]["decision_action_counts"]
  66. event_ids = [event["event_id"] for event in runtime.lifecycle_events]
  67. assert event_ids == ["lifecycle_start", "lifecycle_success"]
  68. assert not any(event_id.startswith("evt_") for event_id in event_ids)
  69. assert runtime.lifecycle_events[0]["raw_payload"]["source_ref"]["demand_content_id"] == 123
  70. def test_run_service_partial_platform_failure_records_partial_success(tmp_path):
  71. runtime = _SpyRuntimeStore(tmp_path / "runtime")
  72. demand_source = FakeDemandSource(real_source_payload(demand_content_id=123))
  73. service = RunService(
  74. runtime=runtime,
  75. demand_source=demand_source,
  76. query_variant_client=FakeQueryVariantClient(),
  77. )
  78. service._platform_client = lambda platform, platform_mode: _PartialFailurePlatformClient()
  79. state = service.start_run(RunStartRequest(platform_mode="real"))
  80. assert state["status"] == "partial_success"
  81. assert state["query_failures"][0]["search_query_id"] == "q_002"
  82. assert runtime.run_updates[-1]["updates"]["status"] == "partial_success"
  83. assert runtime.policy_runs[0]["status"] == "partial_success"
  84. assert runtime.lifecycle_events[-1]["event_id"] == "lifecycle_success"
  85. assert runtime.lifecycle_events[-1]["status"] == "partial_success"
  86. assert runtime.lifecycle_events[-1]["raw_payload"]["query_failures"]
  87. run_events = service.read_jsonl(state["run_id"], "run_events.jsonl")
  88. assert any(event["event_type"] == "platform_query_failed" for event in run_events)
  89. search_clues = service.read_jsonl(state["run_id"], "search_clues.jsonl")
  90. failed_clue = next(
  91. clue for clue in search_clues if clue["search_query_id"] == "q_002"
  92. )
  93. assert failed_clue["search_query_effect_status"] == "failed"
  94. def test_run_service_all_platform_queries_fail_records_failed_query_details(tmp_path):
  95. runtime = _SpyRuntimeStore(tmp_path / "runtime")
  96. demand_source = FakeDemandSource(real_source_payload(demand_content_id=123))
  97. service = RunService(
  98. runtime=runtime,
  99. demand_source=demand_source,
  100. query_variant_client=FakeQueryVariantClient(),
  101. )
  102. service._platform_client = lambda platform, platform_mode: _AllFailurePlatformClient()
  103. state = service.start_run(RunStartRequest(platform_mode="real"))
  104. assert state["status"] == "failed"
  105. assert state["error_code"] == ErrorCode.PLATFORM_REQUEST_FAILED.value
  106. failed_query_ids = [failure["search_query_id"] for failure in state["error_detail"]["query_failures"]]
  107. assert failed_query_ids == ["q_001", "q_002", "q_003", "q_004"]
  108. assert runtime.run_updates[-1]["updates"]["status"] == "failed"
  109. assert runtime.run_updates[-1]["updates"]["error_detail"]["query_failures"]
  110. assert runtime.lifecycle_events[-1]["event_id"] == "lifecycle_failed"
  111. assert runtime.lifecycle_events[-1]["raw_payload"]["error_detail"]["query_failures"]
  112. search_queries = service.read_jsonl(state["run_id"], "search_queries.jsonl")
  113. assert {query["search_query_effect_status"] for query in search_queries} == {"failed"}
  114. assert all(query["raw_payload"]["query_failure"]["status"] == "failed" for query in search_queries)
  115. search_clues = service.read_jsonl(state["run_id"], "search_clues.jsonl")
  116. assert [clue["search_query_id"] for clue in search_clues] == failed_query_ids
  117. assert {clue["search_query_effect_status"] for clue in search_clues} == {"failed"}
  118. assert {clue["walk_next_step"] for clue in search_clues} == {"stop_search_query"}
  119. run_events = service.read_jsonl(state["run_id"], "run_events.jsonl")
  120. platform_failures = [
  121. event for event in run_events if event["event_type"] == "platform_query_failed"
  122. ]
  123. assert [event["input_ref"] for event in platform_failures] == [
  124. f"search_queries.jsonl:{query_id}" for query_id in failed_query_ids
  125. ]
  126. assert {event["status"] for event in platform_failures} == {"failed"}
  127. def test_run_service_query_generation_failure_records_error_code(tmp_path):
  128. runtime = _SpyRuntimeStore(tmp_path / "runtime")
  129. demand_source = FakeDemandSource(real_source_payload(demand_content_id=123))
  130. service = RunService(
  131. runtime=runtime,
  132. demand_source=demand_source,
  133. query_variant_client=FakeQueryVariantClient(error=RuntimeError("model unavailable")),
  134. )
  135. state = service.start_run(RunStartRequest(platform_mode="mock"))
  136. assert state["status"] == "failed"
  137. assert state["error_code"] == ErrorCode.QUERY_GENERATION_FAILED.value
  138. assert state["error_detail"]["reason"] == "llm_variant_exception"
  139. assert runtime.run_updates[-1]["updates"]["status"] == "failed"
  140. assert (
  141. runtime.run_updates[-1]["updates"]["error_code"]
  142. == ErrorCode.QUERY_GENERATION_FAILED.value
  143. )
  144. assert runtime.lifecycle_events[-1]["event_id"] == "lifecycle_failed"
  145. assert runtime.lifecycle_events[-1]["error_code"] == ErrorCode.QUERY_GENERATION_FAILED.value
  146. def test_run_service_duplicate_query_variant_records_query_generation_failed(tmp_path):
  147. runtime = _SpyRuntimeStore(tmp_path / "runtime")
  148. demand_source = FakeDemandSource(real_source_payload(demand_content_id=123))
  149. service = RunService(
  150. runtime=runtime,
  151. demand_source=demand_source,
  152. query_variant_client=FakeQueryVariantClient({"爱国情感": "爱国情感"}),
  153. )
  154. state = service.start_run(RunStartRequest(platform_mode="mock"))
  155. assert state["status"] == "failed"
  156. assert state["error_code"] == ErrorCode.QUERY_GENERATION_FAILED.value
  157. assert state["error_detail"]["reason"] == "llm_variant_same_as_seed"
  158. assert runtime.run_updates[-1]["updates"]["error_code"] == ErrorCode.QUERY_GENERATION_FAILED.value
  159. def test_run_service_generic_query_variant_records_query_generation_failed(tmp_path):
  160. runtime = _SpyRuntimeStore(tmp_path / "runtime")
  161. demand_source = FakeDemandSource(real_source_payload(demand_content_id=123))
  162. service = RunService(
  163. runtime=runtime,
  164. demand_source=demand_source,
  165. query_variant_client=FakeQueryVariantClient({"爱国情感": "热门视频"}),
  166. )
  167. state = service.start_run(RunStartRequest(platform_mode="mock"))
  168. assert state["status"] == "failed"
  169. assert state["error_code"] == ErrorCode.QUERY_GENERATION_FAILED.value
  170. assert state["error_detail"]["reason"] == "llm_variant_generic"
  171. assert runtime.run_updates[-1]["updates"]["error_code"] == ErrorCode.QUERY_GENERATION_FAILED.value
  172. def test_run_service_malformed_query_variant_result_records_query_generation_failed(tmp_path):
  173. runtime = _SpyRuntimeStore(tmp_path / "runtime")
  174. demand_source = FakeDemandSource(real_source_payload(demand_content_id=123))
  175. service = RunService(
  176. runtime=runtime,
  177. demand_source=demand_source,
  178. query_variant_client=_MalformedQueryVariantClient(),
  179. )
  180. state = service.start_run(RunStartRequest(platform_mode="mock"))
  181. assert state["status"] == "failed"
  182. assert state["error_code"] == ErrorCode.QUERY_GENERATION_FAILED.value
  183. assert state["error_detail"]["reason"] == "llm_variant_result_invalid"
  184. assert runtime.run_updates[-1]["updates"]["error_code"] == ErrorCode.QUERY_GENERATION_FAILED.value
  185. def test_run_service_missing_query_variant_client_fails_at_p2(tmp_path):
  186. runtime = _SpyRuntimeStore(tmp_path / "runtime")
  187. service = RunService(runtime=runtime)
  188. state = service.start_run(
  189. RunStartRequest(platform_mode="mock", source=str(REAL_SOURCE_FIXTURE))
  190. )
  191. assert state["status"] == "failed"
  192. assert state["error_code"] == ErrorCode.QUERY_GENERATION_FAILED.value
  193. assert state["error_detail"]["reason"] == "query variant client is not configured"
  194. assert runtime.run_updates[-1]["updates"]["error_code"] == ErrorCode.QUERY_GENERATION_FAILED.value
  195. def test_run_service_without_selector_requires_configured_demand_source(tmp_path):
  196. runtime = _SpyRuntimeStore(tmp_path / "runtime")
  197. service = RunService(runtime=runtime)
  198. state = service.start_run(RunStartRequest(platform_mode="mock"))
  199. assert state["status"] == "failed"
  200. assert state["error_code"] == ErrorCode.DB_CONFIG_MISSING.value
  201. assert state["error_detail"]["selector"] == "default_pg_pattern_v2_passed"
  202. assert runtime.run_records[0]["source_ref"]["source_type"] == "demand_content_default"
  203. assert runtime.run_updates[-1]["updates"]["status"] == "failed"
  204. assert runtime.lifecycle_events[-1]["event_id"] == "lifecycle_failed"
  205. def test_run_service_failure_records_error_code_and_failed_lifecycle(tmp_path):
  206. runtime = _SpyRuntimeStore(tmp_path / "runtime")
  207. service = RunService(runtime=runtime)
  208. state = service.start_run(
  209. RunStartRequest(platform_mode="mock", source=str(tmp_path / "missing.json"))
  210. )
  211. assert state["status"] == "failed"
  212. assert state["error_code"] == ErrorCode.INVALID_SOURCE.value
  213. assert state["http_status_code"] == 400
  214. assert runtime.run_updates[-1]["updates"]["status"] == "failed"
  215. assert runtime.run_updates[-1]["updates"]["error_code"] == ErrorCode.INVALID_SOURCE.value
  216. assert runtime.lifecycle_events[-1]["event_id"] == "lifecycle_failed"
  217. assert runtime.lifecycle_events[-1]["error_code"] == ErrorCode.INVALID_SOURCE.value
  218. def test_demand_source_service_maps_demand_content_row_to_source_payload():
  219. connection = _DemandConnection(
  220. {
  221. "id": 123,
  222. "merge_leve2": "PG Pattern",
  223. "name": "爱国情感,人物故事",
  224. "reason": "reason",
  225. "suggestion": "suggestion",
  226. "score": 0.91,
  227. "dt": "2026-06-07",
  228. "ext_data": json.dumps({"evidence_pack": {"pattern_source_system": "pg_pattern_v2"}}),
  229. }
  230. )
  231. service = DemandSourceService(_config(), connection_factory=lambda: connection)
  232. payload = service.get_by_id(123)
  233. assert payload["demand_content_id"] == "123"
  234. assert payload["ext_data"]["evidence_pack"]["pattern_source_system"] == "pg_pattern_v2"
  235. assert payload["raw_demand_content"]["id"] == 123
  236. assert "FROM demand_content" in connection.statements[0][0]
  237. assert connection.statements[0][1] == [123]
  238. def test_demand_source_service_run_label_uses_deterministic_selector():
  239. connection = _DemandConnection(None)
  240. service = DemandSourceService(_config(), connection_factory=lambda: connection)
  241. try:
  242. service.get_by_run_label("smoke")
  243. except Exception:
  244. pass
  245. sql, params = connection.statements[0]
  246. assert "JSON_UNQUOTE(JSON_EXTRACT(ext_data, '$.run_label'))" in sql
  247. assert "ORDER BY id ASC" in sql
  248. assert "LIMIT 1" in sql
  249. assert params == ["smoke"]
  250. def test_demand_source_service_default_uses_real_pg_pattern_selector():
  251. connection = _DemandConnection(None)
  252. service = DemandSourceService(_config(), connection_factory=lambda: connection)
  253. try:
  254. service.get_default_pg_pattern_source()
  255. except Exception:
  256. pass
  257. sql, params = connection.statements[0]
  258. assert "$.evidence_pack.pattern_source_system" in sql
  259. assert "$.evidence_pack.validation_status" in sql
  260. assert "pg_pattern_v2" in sql
  261. assert "passed" in sql
  262. assert "ORDER BY id ASC" in sql
  263. assert "LIMIT 1" in sql
  264. assert params == []
  265. def test_api_mutual_exclusion_returns_invalid_request(tmp_path, monkeypatch):
  266. monkeypatch.setattr(api, "service", RunService(runtime_root=tmp_path / "runtime" / "v1"))
  267. client = TestClient(api.app)
  268. response = client.post(
  269. "/runs",
  270. json={
  271. "platform_mode": "mock",
  272. "source": "source.json",
  273. "demand_content_id": 123,
  274. },
  275. )
  276. assert response.status_code == 422
  277. assert response.json()["detail"]["error_code"] == ErrorCode.INVALID_REQUEST.value
  278. def test_api_missing_source_returns_invalid_source(tmp_path, monkeypatch):
  279. monkeypatch.setattr(api, "service", RunService(runtime_root=tmp_path / "runtime" / "v1"))
  280. client = TestClient(api.app)
  281. response = client.post(
  282. "/runs",
  283. json={"platform_mode": "mock", "source": str(tmp_path / "missing.json")},
  284. )
  285. assert response.status_code == 400
  286. assert response.json()["detail"]["error_code"] == ErrorCode.INVALID_SOURCE.value
  287. def test_api_404_returns_structured_run_not_found(tmp_path, monkeypatch):
  288. monkeypatch.setattr(api, "service", RunService(runtime_root=tmp_path / "runtime" / "v1"))
  289. client = TestClient(api.app)
  290. response = client.get("/runs/missing_run")
  291. assert response.status_code == 404
  292. assert response.json()["detail"]["error_code"] == ErrorCode.RUN_NOT_FOUND.value
  293. class _SpyRuntimeStore:
  294. def __init__(self, base_dir: Path) -> None:
  295. self.local = LocalRuntimeFileStore(base_dir)
  296. self.run_records: list[dict[str, Any]] = []
  297. self.run_updates: list[dict[str, Any]] = []
  298. self.policy_runs: list[dict[str, Any]] = []
  299. self.lifecycle_events: list[dict[str, Any]] = []
  300. self.publish_jobs: list[dict[str, Any]] = []
  301. self.author_assets: list[dict[str, Any]] = []
  302. self.author_asset_roles: list[dict[str, Any]] = []
  303. self.search_clue_assets: list[dict[str, Any]] = []
  304. self.search_clue_asset_evidence: list[dict[str, Any]] = []
  305. def prepare_run(self, run_id: str) -> Path:
  306. return self.local.prepare_run(run_id)
  307. def run_dir(self, run_id: str) -> Path:
  308. return self.local.run_dir(run_id)
  309. def write_json(self, run_id: str, filename: str, data: dict[str, Any]) -> Path:
  310. return self.local.write_json(run_id, filename, data)
  311. def update_json(self, run_id: str, filename: str, data: dict[str, Any]) -> Path:
  312. return self.local.update_json(run_id, filename, data)
  313. def append_jsonl(self, run_id: str, filename: str, rows: list[dict[str, Any]]) -> Path:
  314. return self.local.append_jsonl(run_id, filename, rows)
  315. def read_json(self, run_id: str, filename: str) -> dict[str, Any]:
  316. return self.local.read_json(run_id, filename)
  317. def read_jsonl(self, run_id: str, filename: str) -> list[dict[str, Any]]:
  318. return self.local.read_jsonl(run_id, filename)
  319. def file_status(self, run_id: str) -> dict[str, bool]:
  320. return self.local.file_status(run_id)
  321. def create_run_record(self, record: dict[str, Any]) -> None:
  322. self.run_records.append(dict(record))
  323. def update_run_record(self, run_id: str, updates: dict[str, Any]) -> None:
  324. self.run_updates.append({"run_id": run_id, "updates": dict(updates)})
  325. def record_policy_run(self, record: dict[str, Any]) -> None:
  326. self.policy_runs.append(dict(record))
  327. def append_run_event_records(
  328. self,
  329. run_id: str,
  330. policy_run_id: str,
  331. rows: list[dict[str, Any]],
  332. ) -> None:
  333. self.lifecycle_events.extend(dict(row) for row in rows)
  334. def write_publish_jobs(
  335. self,
  336. run_id: str,
  337. policy_run_id: str,
  338. rows: list[dict[str, Any]],
  339. ) -> None:
  340. self.publish_jobs.extend(dict(row) for row in rows)
  341. def write_author_assets(self, rows: list[dict[str, Any]]) -> None:
  342. self.author_assets.extend(dict(row) for row in rows)
  343. def write_author_asset_roles(self, rows: list[dict[str, Any]]) -> None:
  344. self.author_asset_roles.extend(dict(row) for row in rows)
  345. def write_search_clue_assets(self, rows: list[dict[str, Any]]) -> None:
  346. self.search_clue_assets.extend(dict(row) for row in rows)
  347. def write_search_clue_asset_evidence(self, rows: list[dict[str, Any]]) -> None:
  348. self.search_clue_asset_evidence.extend(dict(row) for row in rows)
  349. def read_performance_feedback(
  350. self,
  351. run_id: str,
  352. policy_run_id: str,
  353. ) -> list[dict[str, Any]]:
  354. return []
  355. class _FakeRuntimeStore(_SpyRuntimeStore):
  356. def __init__(self, run_dir: Path | None = None, fail_writes: bool = False) -> None:
  357. super().__init__(run_dir or Path("fake_runtime"))
  358. self.calls: list[tuple[str, str]] = []
  359. self.fail_writes = fail_writes
  360. def write_json(self, run_id: str, filename: str, data: dict[str, Any]) -> Path:
  361. self.calls.append(("write_json", filename))
  362. if self.fail_writes:
  363. raise RuntimeError("primary failed")
  364. return self.run_dir(run_id) / filename
  365. def update_json(self, run_id: str, filename: str, data: dict[str, Any]) -> Path:
  366. self.calls.append(("update_json", filename))
  367. if self.fail_writes:
  368. raise RuntimeError("primary failed")
  369. return self.run_dir(run_id) / filename
  370. def append_jsonl(self, run_id: str, filename: str, rows: list[dict[str, Any]]) -> Path:
  371. self.calls.append(("append_jsonl", filename))
  372. if self.fail_writes:
  373. raise RuntimeError("primary failed")
  374. return self.run_dir(run_id) / filename
  375. class _MalformedQueryVariantClient:
  376. def generate_variant(self, *, seed_term: str, evidence_context: dict[str, Any]):
  377. return None
  378. class _PartialFailurePlatformClient:
  379. def __init__(self) -> None:
  380. self.mock = MockPlatformClient()
  381. def search(self, search_query: dict[str, Any]) -> list[dict[str, Any]]:
  382. if search_query["search_query_id"] == "q_002":
  383. raise RuntimeError("temporary platform failure")
  384. return self.mock.search(search_query)
  385. class _AllFailurePlatformClient:
  386. def search(self, search_query: dict[str, Any]) -> list[dict[str, Any]]:
  387. raise RuntimeError("platform unavailable")
  388. class _DemandCursor:
  389. def __init__(self, connection: "_DemandConnection") -> None:
  390. self.connection = connection
  391. def __enter__(self) -> "_DemandCursor":
  392. return self
  393. def __exit__(self, *_args) -> None:
  394. return None
  395. def execute(self, sql: str, params=None) -> None:
  396. self.connection.statements.append((sql, list(params or [])))
  397. def fetchone(self):
  398. return self.connection.row
  399. class _DemandConnection:
  400. def __init__(self, row: dict[str, Any] | None) -> None:
  401. self.row = row
  402. self.statements: list[tuple[str, list[Any]]] = []
  403. def __enter__(self) -> "_DemandConnection":
  404. return self
  405. def __exit__(self, *_args) -> None:
  406. return None
  407. def cursor(self) -> _DemandCursor:
  408. return _DemandCursor(self)
  409. def _config() -> ContentSupplyDbConfig:
  410. return ContentSupplyDbConfig(
  411. host="127.0.0.1",
  412. port=3306,
  413. user="content_rw",
  414. password="dummy_password",
  415. database="content-deconstruction-supply",
  416. )