| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- import os
- import unittest
- from datetime import date, datetime
- from decimal import Decimal
- from unittest.mock import Mock, patch
- import pandas as pd
- from operator_commands import ACTION_CONFIRM, ACTION_REJECT, parse_command
- from roi_control.config import RoiConfig
- from roi_control.execution import _execute_pause_ad, plan_scale_bid
- from roi_control.metrics import ENTITY_GZH, ENTITY_SELF, ENTITY_SELF_AD
- from roi_control.policy import (
- ACTION_PAUSE_AD,
- ACTION_PAUSE_CREATIVE,
- ACTION_SCALE_BID,
- MODE_ACTIONABLE,
- MODE_NOTIFY_ONLY,
- annotate_execution,
- )
- from roi_control.repository import _is_expired
- from roi_control.sheet_approval import RoiSheetClient, parse_approval_rows
- from tencent_client import ACTIVE_STATUS, SUSPEND_STATUS
- class RoiControlPolicyTest(unittest.TestCase):
- def test_mysql_naive_expiry_compares_with_aware_service_time(self):
- expires_at = datetime(2026, 7, 28, 12, 0)
- now = datetime.fromisoformat("2026-07-28T12:01:00+08:00")
- self.assertTrue(_is_expired(expires_at, now))
- def test_actionability_is_separate_from_metric_recommendation(self):
- summary = pd.DataFrame(
- [
- {
- "entity_type": ENTITY_SELF,
- "channel": "小程序投流-稳定",
- "账号id": "84502354",
- "广告id": "1001",
- "创意id": "2001",
- "动作": "关停",
- "动作原因": "低ROI",
- },
- {
- "entity_type": ENTITY_SELF,
- "channel": "小程序投流-稳定",
- "账号id": "84502354",
- "广告id": "1001",
- "创意id": "2002",
- "动作": "扩量",
- "动作原因": "高ROI",
- },
- {
- "entity_type": ENTITY_SELF,
- "channel": "小程序投流-稳定",
- "账号id": "99999999",
- "广告id": "3001",
- "创意id": "4001",
- "动作": "关停",
- "动作原因": "低ROI",
- },
- {
- "entity_type": ENTITY_GZH,
- "channel": "公众号合作-即转-稳定",
- "合作方名": "合作方",
- "公众号名": "公众号",
- "动作": "扩量",
- "动作原因": "高ROI",
- },
- {
- "entity_type": ENTITY_SELF_AD,
- "channel": "小程序投流-稳定",
- "账号id": "84502354",
- "广告id": "5001",
- "创意id": "",
- "动作": "关停",
- "动作原因": "广告级低ROI",
- },
- ]
- )
- annotated, snapshots, actions = annotate_execution(
- summary,
- {84502354},
- "roi_20260725_north_star_roi_v1_roi_policy_v1",
- )
- self.assertEqual(len(snapshots), 5)
- self.assertEqual(
- {action["action_type"] for action in actions},
- {ACTION_PAUSE_CREATIVE, ACTION_PAUSE_AD, ACTION_SCALE_BID},
- )
- self.assertEqual(
- annotated.iloc[0]["执行模式"],
- MODE_ACTIONABLE,
- )
- self.assertEqual(annotated.iloc[2]["执行模式"], MODE_NOTIFY_ONLY)
- self.assertEqual(annotated.iloc[3]["执行模式"], MODE_NOTIFY_ONLY)
- self.assertEqual(annotated.iloc[0]["审批选择"], "")
- self.assertEqual(annotated.iloc[0]["执行状态"], "待审批")
- self.assertEqual(
- annotated.iloc[0]["动作幂等键"], actions[0]["idempotency_key"]
- )
- self.assertEqual(annotated.iloc[2]["审批选择"], "不可执行")
- self.assertEqual(annotated.iloc[4]["执行模式"], MODE_ACTIONABLE)
- self.assertEqual(annotated.iloc[4]["审批选择"], "")
- ad_action = next(
- action for action in actions if action["action_type"] == ACTION_PAUSE_AD
- )
- self.assertIsNone(ad_action["dynamic_creative_id"])
- self.assertTrue(ad_action["idempotency_key"].endswith(":5001"))
- def test_sheet_approval_parser_uses_hidden_idempotency_key(self):
- rows = [
- ["账号id", "审批选择", "动作幂等键", "执行状态", "执行结果"],
- ["99999999", "批准", "roi_x:PAUSE_CREATIVE:1:2", "待审批", ""],
- ["1", "拒绝", "roi_x:PAUSE_CREATIVE:1:3", "待审批", ""],
- ["1", "随便填", "roi_x:PAUSE_CREATIVE:1:4", "待审批", ""],
- ]
- decisions = parse_approval_rows(rows)
- self.assertEqual(
- [(row["decision"], row["idempotency_key"]) for row in decisions],
- [
- ("APPROVED", "roi_x:PAUSE_CREATIVE:1:2"),
- ("REJECTED", "roi_x:PAUSE_CREATIVE:1:3"),
- ],
- )
- def test_sheet_client_reads_both_current_approval_sheets(self):
- client = RoiSheetClient.__new__(RoiSheetClient)
- client._token = Mock(return_value="token")
- client._sheet_ids = Mock(
- return_value=[
- ("小程序创意级三日汇总", "creative-sheet"),
- ("小程序广告级三日汇总", "ad-sheet"),
- ]
- )
- client._read_sheet_approvals = Mock(
- side_effect=[
- [{"sheet_id": "creative-sheet", "idempotency_key": "creative"}],
- [{"sheet_id": "ad-sheet", "idempotency_key": "ad"}],
- ]
- )
- decisions = client.read_approvals("spreadsheet")
- self.assertEqual(
- [decision["sheet_id"] for decision in decisions],
- ["creative-sheet", "ad-sheet"],
- )
- def test_pause_ad_marks_prepared_then_success_after_readback(self):
- now = datetime.fromisoformat("2026-08-03T12:00:00+08:00")
- class FakeTencent:
- def get_ad(self, account_id, adgroup_id):
- self.get_args = (account_id, adgroup_id)
- return {
- "adgroup_id": adgroup_id,
- "configured_status": ACTIVE_STATUS,
- }
- def update_ad(self, account_id, adgroup_id, **kwargs):
- self.update_args = (account_id, adgroup_id, kwargs)
- return {
- "adgroup_id": adgroup_id,
- "configured_status": SUSPEND_STATUS,
- }
- client = FakeTencent()
- item = {
- "id": 99,
- "account_id": 84502354,
- "adgroup_id": 5001,
- "execution_status": "PENDING",
- }
- with patch("roi_control.execution.update_action_item") as update_item:
- _execute_pause_ad(item, client=client, now=now)
- self.assertEqual(client.get_args, (84502354, 5001))
- self.assertEqual(
- client.update_args,
- (84502354, 5001, {"target_status": SUSPEND_STATUS}),
- )
- self.assertEqual(update_item.call_count, 2)
- self.assertEqual(
- update_item.call_args_list[0].kwargs["execution_status"], "PREPARED"
- )
- self.assertEqual(
- update_item.call_args_list[1].kwargs["execution_status"], "SUCCESS"
- )
- self.assertEqual(
- update_item.call_args_list[1].kwargs["readback_status"], SUSPEND_STATUS
- )
- def test_scale_actions_are_deduplicated_by_ad(self):
- rows = [
- {
- "entity_type": ENTITY_SELF,
- "channel": "小程序投流-稳定",
- "账号id": "84502354",
- "广告id": "1001",
- "创意id": str(2000 + index),
- "动作": "扩量",
- "动作原因": "高ROI",
- }
- for index in range(3)
- ]
- _, _, actions = annotate_execution(
- pd.DataFrame(rows),
- {84502354},
- "roi_20260725_north_star_roi_v1_roi_policy_v1",
- )
- self.assertEqual(len(actions), 1)
- self.assertEqual(actions[0]["action_type"], ACTION_SCALE_BID)
- def test_observe_recommendation_never_creates_tencent_action(self):
- summary = pd.DataFrame(
- [
- {
- "entity_type": ENTITY_SELF,
- "channel": "小程序投流-稳定",
- "账号id": "84502354",
- "广告id": "1001",
- "创意id": "2001",
- "动作": "观察",
- "动作原因": "连续两天表现不一致",
- }
- ]
- )
- annotated, _, actions = annotate_execution(
- summary,
- {84502354},
- "roi_20260730_m6_p3",
- )
- self.assertEqual(actions, [])
- self.assertEqual(annotated.iloc[0]["执行模式"], MODE_NOTIFY_ONLY)
- self.assertEqual(
- annotated.iloc[0]["执行说明"],
- "观察行仅展示,不执行腾讯写操作",
- )
- def test_invalid_apply_without_daily_job_is_rejected(self):
- environment = {
- "DAILY_ROI_ENABLED": "0",
- "ROI_APPLY_ENABLED": "1",
- }
- with patch.dict(os.environ, environment, clear=False):
- with self.assertRaisesRegex(ValueError, "requires DAILY_ROI_ENABLED"):
- RoiConfig.from_env()
- def test_sheet_approval_requires_roi_writes(self):
- environment = {
- "DAILY_ROI_ENABLED": "1",
- "ROI_APPLY_ENABLED": "0",
- "ROI_SHEET_APPROVAL_ENABLED": "1",
- }
- with patch.dict(os.environ, environment, clear=False):
- with self.assertRaisesRegex(ValueError, "requires ROI_APPLY_ENABLED"):
- RoiConfig.from_env()
- def test_roi_batch_commands_are_deterministic(self):
- confirm = parse_command("确认 roi_20260725_north_star_roi_v1")
- reject = parse_command("拒绝 roi_20260725_north_star_roi_v1")
- old_confirm = parse_command("确认 cmd_20260725120000_abcd1234")
- self.assertEqual(confirm.action, ACTION_CONFIRM)
- self.assertEqual(reject.action, ACTION_REJECT)
- self.assertEqual(confirm.command_id, "roi_20260725_north_star_roi_v1")
- self.assertEqual(old_confirm.action, ACTION_CONFIRM)
- def test_restored_intraday_boost_is_not_reapplied_by_roi(self):
- plan = plan_scale_bid(
- current_bid_fen=100,
- base_bid_fen=100,
- initial_base_bid_fen=100,
- boosted_date=date(2026, 7, 26),
- control_date=date(2026, 7, 26),
- intraday_ratio=Decimal("1.25"),
- scale_ratio=Decimal("1.10"),
- max_base_ratio=Decimal("2.00"),
- )
- self.assertTrue(plan["allowed"])
- self.assertEqual(plan["new_base_bid_fen"], 110)
- self.assertEqual(plan["target_bid_fen"], 110)
- self.assertTrue(plan["boosted_date_today"])
- self.assertFalse(plan["preserve_intraday_boost"])
- def test_active_intraday_boost_is_preserved_relative_to_new_base(self):
- plan = plan_scale_bid(
- current_bid_fen=125,
- base_bid_fen=100,
- initial_base_bid_fen=100,
- boosted_date=date(2026, 7, 26),
- control_date=date(2026, 7, 26),
- intraday_ratio=Decimal("1.25"),
- scale_ratio=Decimal("1.10"),
- max_base_ratio=Decimal("2.00"),
- )
- self.assertEqual(plan["target_bid_fen"], 138)
- self.assertTrue(plan["preserve_intraday_boost"])
- if __name__ == "__main__":
- unittest.main()
|