test_skill_executor.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. from dataclasses import replace
  2. import pandas as pd
  3. import pytest
  4. from data_query_agent.config import Settings
  5. from data_query_agent.models import SkillParameters
  6. from data_query_agent.skill_executor import SkillExecutor
  7. def parameters(**overrides) -> SkillParameters:
  8. values = {
  9. "user_id": None,
  10. "date": None,
  11. "apptype": None,
  12. "realtime": None,
  13. "app_type": None,
  14. "date_from": None,
  15. "date_to": None,
  16. "data_mode": None,
  17. "bucket_position_from_end": None,
  18. "experiment_buckets": None,
  19. "control_buckets": None,
  20. "version": None,
  21. "first_layer_rule": None,
  22. "exclude_qywx": None,
  23. }
  24. values.update(overrides)
  25. return SkillParameters(**values)
  26. @pytest.mark.asyncio
  27. async def test_user_timeline_without_apptype_omits_product_argument(tmp_path, monkeypatch) -> None:
  28. executor = SkillExecutor(replace(Settings.load(), runtime_dir=tmp_path))
  29. async def fake_run(args, *, env=None):
  30. assert args[1].endswith("query-user-behavior-path/scripts/user_timeline.py")
  31. assert args[2:4] == ["mid_123", "20260806"]
  32. assert args[4:] == ["--output-dir", str(tmp_path)]
  33. assert env and "ODPS_ACCESS_ID" in env and "FEISHU_APP_SECRET" not in env
  34. pd.DataFrame({"北京时间": ["2026-08-06 10:00:00"], "来源": ["video"]}).to_excel(
  35. tmp_path / "timeline_test.xlsx", sheet_name="行为路径", index=False
  36. )
  37. return "[ODPS] InstanceId: i-test\n[XLSX] done"
  38. monkeypatch.setattr(executor, "_run", fake_run)
  39. artifact = await executor.run_user_timeline(
  40. parameters(user_id="mid_123", date="20260806", apptype=None, realtime=False),
  41. tmp_path,
  42. )
  43. assert artifact.instance_id == "i-test"
  44. assert len(artifact.dataframe) == 1
  45. assert artifact.xlsx_path.name == "timeline_test.xlsx"
  46. @pytest.mark.asyncio
  47. async def test_user_timeline_with_explicit_apptype_passes_exact_filter(tmp_path, monkeypatch) -> None:
  48. executor = SkillExecutor(replace(Settings.load(), runtime_dir=tmp_path))
  49. async def fake_run(args, *, env=None):
  50. assert args[2:5] == ["mid_123", "20260806", "4"]
  51. pd.DataFrame({"来源": ["video"]}).to_excel(
  52. tmp_path / "timeline_filtered.xlsx", sheet_name="行为路径", index=False
  53. )
  54. return "[ODPS] InstanceId: i-filtered"
  55. monkeypatch.setattr(executor, "_run", fake_run)
  56. artifact = await executor.run_user_timeline(
  57. parameters(user_id="mid_123", date="20260806", apptype="4", realtime=False),
  58. tmp_path,
  59. )
  60. assert artifact.instance_id == "i-filtered"
  61. @pytest.mark.asyncio
  62. async def test_user_timeline_rejects_invalid_host_parameters(tmp_path) -> None:
  63. executor = SkillExecutor(replace(Settings.load(), runtime_dir=tmp_path))
  64. with pytest.raises(ValueError, match="mid/machinecode"):
  65. await executor.run_user_timeline(
  66. parameters(user_id="bad value", date="20260806", apptype="0", realtime=False),
  67. tmp_path,
  68. )
  69. @pytest.mark.asyncio
  70. async def test_generic_sql_result_uses_existing_workbook_path(tmp_path) -> None:
  71. executor = SkillExecutor(replace(Settings.load(), runtime_dir=tmp_path))
  72. artifact = await executor.format_report(
  73. "query-odps-data",
  74. parameters(),
  75. tmp_path,
  76. pd.DataFrame({"dau": [10]}),
  77. "SELECT 10 AS dau",
  78. {"ODPS instance_id": "i-generic"},
  79. )
  80. assert artifact.xlsx_path.is_file()
  81. assert artifact.instance_id == "i-generic"
  82. @pytest.mark.asyncio
  83. async def test_total_only_product_efficiency_uses_direct_workbook(tmp_path, monkeypatch) -> None:
  84. executor = SkillExecutor(replace(Settings.load(), runtime_dir=tmp_path))
  85. async def unexpected_run(*args, **kwargs):
  86. raise AssertionError("total-only report must not invoke the bucket formatter")
  87. monkeypatch.setattr(executor, "_run", unexpected_run)
  88. frame = pd.DataFrame(
  89. {
  90. "stat_date": ["20260812"],
  91. "app_type": ["0"],
  92. "version_code": ["all"],
  93. "dau": [100],
  94. "head_exposure_pv": [50],
  95. "recommend_return_uv_per_dau": [0.2],
  96. "all_rov": [0.1],
  97. }
  98. )
  99. artifact = await executor.format_report(
  100. "odps-product-efficiency-report",
  101. parameters(
  102. app_type="0",
  103. date_from="20260812",
  104. date_to="20260812",
  105. data_mode="realtime",
  106. bucket_position_from_end=None,
  107. experiment_buckets=None,
  108. version="all",
  109. ),
  110. tmp_path,
  111. frame,
  112. "SELECT 1",
  113. {"ODPS instance_id": "i-total"},
  114. )
  115. assert list(artifact.dataframe.columns) == [
  116. "日期",
  117. "产品类型",
  118. "版本号",
  119. "DAU",
  120. "头部曝光PV",
  121. "推荐回流UV/DAU",
  122. "全部流量ROV(回流UV/曝光PV)",
  123. ]
  124. assert artifact.xlsx_path.is_file()
  125. assert artifact.instance_id == "i-total"