Files
re-xxm/tests/test_task_state.py
T
bot-ci dcf37c4a1d test(scheduler): nail H7 progress keep + H8 deferred task:state
Address Michał NEED-CHANGES on !7 tip f408da93: fail/defer omit-progress
preserves progress/stat_date; seed + quiet-window defer assert
status=deferred and future due_at on task:state. Soft: seeded due≠wall;
cleanup failure reads task:state.
2026-11-03 01:59:18 +08:00

267 lines
9.1 KiB
Python

# -*- coding: utf-8 -*-
"""task:state 持久化与启动续跑: uv run python tests/test_task_state.py"""
import json
import os
import sys
import tempfile
from datetime import datetime
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.stdout.reconfigure(encoding="utf-8")
from reaction_board.database import Database # noqa: E402
from reaction_board.repository import Repository # noqa: E402
from reaction_board.scheduler import ScheduledTask, Scheduler # noqa: E402
from reaction_board.task_state import ( # noqa: E402
STATUS_DEFERRED,
STATUS_DONE,
STATUS_FAILED,
STATUS_PENDING,
STATUS_RUNNING,
ensure_pending,
load_state,
mark_deferred,
mark_done,
mark_failed,
mark_running,
recover_stale_running,
resume_stat_date,
startup_next_run,
)
from reaction_board.timeutil import SHANGHAI_TZ, next_daily_run # noqa: E402
def _repo(tmp: str) -> tuple[Database, Repository]:
db = Database(Path(tmp) / "app.db")
db.initialize()
return db, Repository(db)
def test_restart_mid_run_resumes_same_stat_date():
with tempfile.TemporaryDirectory() as tmp:
db, repo = _repo(tmp)
try:
now = datetime(2026, 11, 3, 23, 40, tzinfo=SHANGHAI_TZ).timestamp()
mark_running(
repo, "snapshot", stat_date="2026-11-03", now=now,
progress={"done": 10, "total": 100},
)
# 模拟进程崩溃:running 残留
recovered = recover_stale_running(repo, "snapshot", now=now + 60)
assert recovered is not None
assert recovered.status == STATUS_PENDING
assert recovered.stat_date == "2026-11-03"
assert recovered.progress == {"done": 10, "total": 100}
assert recovered.due_at <= now + 60
assert resume_stat_date(repo, "snapshot", fallback="2026-11-04") == "2026-11-03"
nxt = startup_next_run(
repo, "snapshot",
wall_due_at=next_daily_run(now + 60, "23:15"),
now=now + 60,
)
assert nxt <= now + 60
finally:
db.close()
def test_seeded_before_snapshot_at_does_not_force_run():
"""seed 后挂 pending;白天重启认落盘 due_at(刻意 ≠ 墙钟)且未到不跑。"""
with tempfile.TemporaryDirectory() as tmp:
db, repo = _repo(tmp)
try:
now = datetime(2026, 11, 3, 10, 30, tzinfo=SHANGHAI_TZ).timestamp()
# 刻意偏离墙钟 23:15,证明认落盘而非 fallback
slot = datetime(2026, 11, 3, 22, 0, tzinfo=SHANGHAI_TZ).timestamp()
wall = next_daily_run(now, "23:15")
assert slot != wall
ensure_pending(
repo, "snapshot", due_at=slot, stat_date="2026-11-03", now=now,
)
repo.kv_set("snapshot:seeded:2026-11-03", "1")
nxt = startup_next_run(
repo, "snapshot",
wall_due_at=wall,
now=now,
)
assert nxt == slot
assert nxt != wall
assert nxt > now
calls = []
def run_snapshot():
calls.append("ran")
return None
clock = [now]
sched = Scheduler(
[ScheduledTask("snapshot", 86400.0, run_snapshot, next_run=nxt)],
clock=lambda: clock[0],
)
assert sched.run_due() == []
assert calls == []
finally:
db.close()
def test_failed_future_due_at_does_not_run_early():
with tempfile.TemporaryDirectory() as tmp:
db, repo = _repo(tmp)
try:
now = datetime(2026, 11, 3, 12, 0, tzinfo=SHANGHAI_TZ).timestamp()
due = now + 600
mark_failed(
repo, "snapshot", due_at=due, now=now,
stat_date="2026-11-03", error="boom",
)
nxt = startup_next_run(
repo, "snapshot",
wall_due_at=next_daily_run(now, "23:15"),
now=now,
)
assert nxt == due
clock = [now]
calls = []
sched = Scheduler(
[
ScheduledTask(
"snapshot", 86400.0, lambda: calls.append(1), next_run=nxt,
)
],
clock=lambda: clock[0],
)
assert sched.run_due() == []
assert calls == []
finally:
db.close()
def test_due_at_passed_runs_including_failed_retry():
with tempfile.TemporaryDirectory() as tmp:
db, repo = _repo(tmp)
try:
now = datetime(2026, 11, 3, 12, 10, tzinfo=SHANGHAI_TZ).timestamp()
due = now - 30 # 已过重试点
mark_failed(
repo, "snapshot", due_at=due, now=due - 300,
stat_date="2026-11-03", error="boom",
)
nxt = startup_next_run(
repo, "snapshot",
wall_due_at=next_daily_run(now, "23:15"),
now=now,
)
# 不得退回今晚墙钟;应立刻可跑
assert nxt == due
assert nxt <= now
wall = next_daily_run(now, "23:15")
assert nxt != wall
calls = []
clock = [now]
sched = Scheduler(
[
ScheduledTask(
"snapshot", 86400.0, lambda: calls.append(1), next_run=nxt,
)
],
clock=lambda: clock[0],
)
results = sched.run_due()
assert len(results) == 1
assert calls == [1]
finally:
db.close()
def test_done_writes_next_due_and_blocks_until_then():
with tempfile.TemporaryDirectory() as tmp:
db, repo = _repo(tmp)
try:
now = datetime(2026, 11, 3, 23, 40, tzinfo=SHANGHAI_TZ).timestamp()
next_due = next_daily_run(now, "23:15")
mark_done(repo, "snapshot", due_at=next_due, now=now, stat_date="2026-11-03")
state = load_state(repo, "snapshot")
assert state is not None
assert state.status == STATUS_DONE
nxt = startup_next_run(
repo, "snapshot", wall_due_at=next_due, now=now + 60,
)
assert nxt == next_due
assert nxt > now + 60
finally:
db.close()
def test_no_state_falls_back_to_wall_clock():
with tempfile.TemporaryDirectory() as tmp:
db, repo = _repo(tmp)
try:
now = datetime(2026, 11, 3, 10, 0, tzinfo=SHANGHAI_TZ).timestamp()
wall = next_daily_run(now, "23:15")
nxt = startup_next_run(repo, "snapshot", wall_due_at=wall, now=now)
assert nxt == wall
finally:
db.close()
def test_fail_or_defer_preserves_progress():
"""H7: mark_failed / mark_deferred 不传 progress 时保留已有 progress 与 stat_date。"""
with tempfile.TemporaryDirectory() as tmp:
db, repo = _repo(tmp)
try:
now = datetime(2026, 11, 3, 12, 0, tzinfo=SHANGHAI_TZ).timestamp()
progress = {"done": 42, "total": 100, "cursor": "aid:99"}
mark_running(
repo, "snapshot", stat_date="2026-11-03", now=now,
progress=progress,
)
before = load_state(repo, "snapshot")
assert before is not None
assert before.progress == progress
assert before.stat_date == "2026-11-03"
due_fail = now + 300
mark_failed(
repo, "snapshot", due_at=due_fail, now=now + 1,
progress=None, error="boom",
)
failed = load_state(repo, "snapshot")
assert failed is not None
assert failed.status == STATUS_FAILED
assert failed.due_at == due_fail
assert failed.progress == progress
assert failed.stat_date == "2026-11-03"
assert failed.error == "boom"
due_defer = now + 600
mark_deferred(
repo, "snapshot", due_at=due_defer, now=now + 2,
progress=None, error="snapshot_quiet_window",
)
deferred = load_state(repo, "snapshot")
assert deferred is not None
assert deferred.status == STATUS_DEFERRED
assert deferred.due_at == due_defer
assert deferred.progress == progress
assert deferred.stat_date == "2026-11-03"
assert deferred.error == "snapshot_quiet_window"
finally:
db.close()
def main_test():
test_restart_mid_run_resumes_same_stat_date()
test_seeded_before_snapshot_at_does_not_force_run()
test_failed_future_due_at_does_not_run_early()
test_due_at_passed_runs_including_failed_retry()
test_done_writes_next_due_and_blocks_until_then()
test_no_state_falls_back_to_wall_clock()
test_fail_or_defer_preserves_progress()
print("OK: task_state persistence / resume / due_at gates")
if __name__ == "__main__":
main_test()