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.
This commit is contained in:
bot-ci committed 2026-11-03 01:59:18 +08:00
1 parent f408da93c2
commit dcf37c4a1d
2 files changed
+124 -5

No files matched your search

+68 -2
View File
@@ -452,8 +452,9 @@ def test_snapshot_seed_no_source_is_non_success():
def test_defer_snapshot_seed_schedules_ten_minutes():
"""调度辅助:skip 记非成功,next_run = finished + 600。"""
"""调度辅助:skip 记非成功,next_run = finished + 600;落盘 task:state=deferred。"""
from reaction_board.logging_json import JsonLogger
from reaction_board.task_state import STATUS_DEFERRED, load_state
with tempfile.TemporaryDirectory() as tmp:
path = write_config(tmp, cookie="", csrf="")
@@ -472,9 +473,64 @@ def test_defer_snapshot_seed_schedules_ten_minutes():
)
assert ctrl.ok is False
assert ctrl.error == "snapshot_running"
assert abs(ctrl.next_run - (finished + cli._SEED_RETRY_SECONDS)) < 0.01
expected_due = finished + cli._SEED_RETRY_SECONDS
assert abs(ctrl.next_run - expected_due) < 0.01
last = json.loads(repo.kv_get("task:last:snapshot_seed"))
assert last["skipped"] is True and last["ok"] is False
state = load_state(repo, "snapshot_seed")
assert state is not None
assert state.status == STATUS_DEFERRED
assert abs(state.due_at - expected_due) < 0.01
assert state.due_at > finished
assert state.error == "snapshot_running"
finally:
db.close()
def test_quiet_window_defer_persists_deferred_state():
"""H8: 静默窗 defer 落盘 status=deferred + due_at=窗后 01:00,不得记成功。"""
from datetime import datetime
from unittest import mock
from reaction_board.logging_json import JsonLogger
from reaction_board.task_state import STATUS_DEFERRED, load_state
from reaction_board.timeutil import (
SHANGHAI_TZ,
next_after_snapshot_quiet_window,
)
with tempfile.TemporaryDirectory() as tmp:
path = write_config(tmp, cookie="", csrf="")
code, _ = run_cli(["init", "--config", str(path)])
assert code == 0
db = Database(Path(tmp) / "app.db")
db.initialize()
try:
repo = Repository(db)
logger = JsonLogger()
# 静默窗内:2026-11-03 23:30 上海
quiet = datetime(2026, 11, 3, 23, 30, tzinfo=SHANGHAI_TZ).timestamp()
expected_due = next_after_snapshot_quiet_window(quiet)
assert expected_due > quiet
with mock.patch("reaction_board.cli.time.time", return_value=quiet):
ctrl = cli._quiet_window_defer(
repo, logger, "discovery", started_at=int(quiet) - 1,
)
assert ctrl.ok is False
assert ctrl.error == cli._QUIET_SKIP_REASON
assert abs(ctrl.next_run - expected_due) < 0.01
last = json.loads(repo.kv_get("task:last:discovery"))
assert last["ok"] is False and last["skipped"] is True
assert last["error"] == cli._QUIET_SKIP_REASON
# 不得推成成功完成类 kv
assert repo.kv_get("snapshot:last_completed") is None
assert repo.kv_get("discovery:last_completed") is None
state = load_state(repo, "discovery")
assert state is not None
assert state.status == STATUS_DEFERRED
assert abs(state.due_at - expected_due) < 0.01
assert state.due_at > quiet
assert state.error == cli._QUIET_SKIP_REASON
finally:
db.close()
@@ -571,6 +627,8 @@ def test_candidate_cleanup_failure_retries_soon():
Repository.delete_expired_candidates = original_delete
assert code == 0, output
from reaction_board.task_state import STATUS_FAILED, load_state
db = Database(Path(tmp) / "app.db")
db.initialize()
try:
@@ -578,6 +636,13 @@ def test_candidate_cleanup_failure_retries_soon():
last = json.loads(repo.kv_get("task:last:candidate_cleanup"))
assert last["ok"] is False
assert "cleanup db boom" in last["error"]
state = load_state(repo, "candidate_cleanup")
assert state is not None
assert state.status == STATUS_FAILED
assert state.due_at > 0
delta_state = state.due_at - __import__("time").time()
assert 250 <= delta_state <= 360, delta_state
assert "cleanup db boom" in (state.error or "")
finally:
db.close()
scheduler = captured["scheduler"]
@@ -759,6 +824,7 @@ def main_test():
test_snapshot_seed_no_source_is_non_success()
test_snapshot_seed_same_day_completed_succeeds()
test_defer_snapshot_seed_schedules_ten_minutes()
test_quiet_window_defer_persists_deferred_state()
test_candidate_cleanup_failure_retries_soon()
test_initial_daily_next_run_uses_wall_clock_not_zero()
test_reached_daily_slot_gates_force_seed()