修復定期簡報漏產與補跑可靠性

This commit is contained in:
OoO
2026-06-06 15:25:20 +08:00
parent d6d8777e41
commit 8fa95b94a9
8 changed files with 475 additions and 5 deletions

View File

@@ -1,5 +1,6 @@
from datetime import datetime
import json
import os
def test_build_defined_ppt_jobs_uses_latest_date():
@@ -128,6 +129,182 @@ def test_due_schedule_kinds_include_periodic_boundaries():
]
def test_latest_schedule_occurrence_replays_missed_slots():
from services.ppt_auto_generation_service import get_latest_schedule_occurrence
assert get_latest_schedule_occurrence("daily", datetime(2026, 6, 6, 14, 0)) == datetime(2026, 6, 5, 20, 30)
assert get_latest_schedule_occurrence("daily", datetime(2026, 6, 6, 21, 0)) == datetime(2026, 6, 6, 20, 30)
assert get_latest_schedule_occurrence("weekly", datetime(2026, 6, 6, 14, 0)) == datetime(2026, 6, 1, 20, 40)
assert get_latest_schedule_occurrence("monthly", datetime(2026, 6, 6, 14, 0)) == datetime(2026, 6, 1, 20, 50)
assert get_latest_schedule_occurrence("quarterly", datetime(2026, 6, 6, 14, 0)) == datetime(2026, 4, 1, 21, 0)
assert get_latest_schedule_occurrence("half_yearly", datetime(2026, 6, 6, 14, 0)) == datetime(2026, 1, 1, 21, 10)
assert get_latest_schedule_occurrence("annual", datetime(2026, 6, 6, 14, 0)) == datetime(2026, 1, 1, 21, 20)
def test_ppt_catchup_plan_marks_missing_after_missed_slot(monkeypatch):
from services import ppt_auto_generation_service as svc
class _Rows:
def fetchall(self):
return []
class _Session:
def execute(self, *_args, **_kwargs):
return _Rows()
def close(self):
return None
monkeypatch.setattr(svc, "get_session", lambda: _Session())
monkeypatch.setattr(svc, "_latest_sales_date", lambda: "2026-06-04")
plan = svc.get_scheduled_ppt_catchup_plan(
now=datetime(2026, 6, 6, 14, 0),
schedule_kinds=["daily"],
)
assert plan[0]["scheduled_at"] == "2026-06-05 20:30"
assert plan[0]["missing_report_types"] == ["daily"]
assert plan[0]["ready"] is False
def test_ppt_catchup_plan_uses_existing_exact_report(monkeypatch, tmp_path):
from services import ppt_auto_generation_service as svc
pptx = tmp_path / "ocbot_daily_ok.pptx"
pptx.write_bytes(b"pptx")
class _Rows:
def fetchall(self):
return [
(
"daily",
json.dumps({"report_type": "daily", "date": "2026/06/04"}),
str(pptx),
)
]
class _Session:
def execute(self, *_args, **_kwargs):
return _Rows()
def close(self):
return None
monkeypatch.setattr(svc, "get_session", lambda: _Session())
monkeypatch.setattr(svc, "_latest_sales_date", lambda: "2026-06-04")
plan = svc.get_scheduled_ppt_catchup_plan(
now=datetime(2026, 6, 6, 14, 0),
schedule_kinds=["daily"],
)
assert plan[0]["ready_report_types"] == ["daily"]
assert plan[0]["missing_report_types"] == []
assert plan[0]["ready"] is True
def test_ppt_catchup_generates_only_missing_types(monkeypatch):
from services import ppt_auto_generation_service as svc
calls = []
monkeypatch.setattr(
svc,
"get_scheduled_ppt_catchup_plan",
lambda **_kwargs: [{
"schedule_kind": "weekly",
"scheduled_at": "2026-06-01 20:40",
"missing_report_types": ["market_intel"],
}],
)
def fake_generate_defined_ppt_reports(**kwargs):
calls.append(kwargs)
return {"ok": True, "ready": 1, "errors": 0, "jobs": [{"report_type": "market_intel"}]}
monkeypatch.setattr(svc, "generate_defined_ppt_reports", fake_generate_defined_ppt_reports)
result = svc.catch_up_scheduled_ppt_reports()
assert result["status"] == "completed"
assert result["generated_kinds"] == ["weekly"]
assert calls == [{
"report_types": ["market_intel"],
"schedule_kind": "weekly",
"force": False,
}]
def test_ppt_catchup_background_queues_without_main_loop_block(monkeypatch):
from services import ppt_auto_generation_service as svc
calls = []
threads = []
class _Thread:
def __init__(self, *, target, name, daemon):
self.target = target
self.name = name
self.daemon = daemon
threads.append(self)
def start(self):
self.target()
def fake_catchup(**kwargs):
calls.append(kwargs)
return {"ok": True, "status": "skipped", "runs": []}
monkeypatch.setattr(svc.threading, "Thread", _Thread)
monkeypatch.setattr(svc, "catch_up_scheduled_ppt_reports", fake_catchup)
result = svc.start_scheduled_ppt_catchup_background(schedule_kinds=["daily", "weekly"])
assert result["status"] == "queued"
assert result["schedule_kinds"] == ["daily", "weekly"]
assert threads[0].name == "ppt-auto-generation-catchup"
assert threads[0].daemon is True
assert calls == [{
"now": None,
"force": False,
"schedule_kinds": ["daily", "weekly"],
}]
def test_scheduled_generation_sets_fast_fallback_env(monkeypatch, tmp_path):
from routes import openclaw_bot_routes as bot_routes
from services import ppt_auto_generation_service as svc
output = tmp_path / "ocbot_market_intel_ok.pptx"
output.write_bytes(b"pptx")
observed = {}
job = svc.build_defined_ppt_jobs(
latest_date="2026-05-11",
report_types=["market_intel"],
)[0]
monkeypatch.delenv("PPT_SCHEDULED_FAST_FALLBACK", raising=False)
monkeypatch.delenv("MCP_FAST_STATIC_FALLBACK", raising=False)
monkeypatch.setattr(svc, "_expire_matching_ppt_cache", lambda _job: 0)
monkeypatch.setattr(bot_routes, "send_message", lambda *_args, **_kwargs: None, raising=False)
def fake_generate_ppt_cmd(*_args, **_kwargs):
observed["ppt_fast"] = os.getenv("PPT_SCHEDULED_FAST_FALLBACK")
observed["mcp_fast"] = os.getenv("MCP_FAST_STATIC_FALLBACK")
return str(output)
monkeypatch.setattr(bot_routes, "_generate_ppt_cmd", fake_generate_ppt_cmd)
path, invalidated = svc._generate_job(job, schedule_kind="weekly")
assert path == str(output)
assert invalidated == 0
assert observed == {"ppt_fast": "true", "mcp_fast": "true"}
assert os.getenv("PPT_SCHEDULED_FAST_FALLBACK") is None
assert os.getenv("MCP_FAST_STATIC_FALLBACK") is None
def test_schedule_cadence_status_exposes_all_periodic_contracts():
from services.ppt_auto_generation_service import get_schedule_cadence_status