import sys import unittest from pathlib import Path sys.path.insert(0, str(Path(__file__).parent.parent.parent)) from app.services.state import RedisState class _FakeRedis: def __init__(self, batches): self.batches = batches self.data = {} for key in [key for batch in batches for key in batch]: index = int(key.decode("utf-8").split(":")[-1]) self.data[key] = { b"task_id": key, b"state": b"1", b"progress": str(index).encode("utf-8"), } def scan(self, cursor, count): batch_index = int(cursor) next_cursor = batch_index + 1 if next_cursor >= len(self.batches): next_cursor = 0 return next_cursor, self.batches[batch_index] def hgetall(self, key): return self.data[key] class TestRedisState(unittest.TestCase): def _build_state(self, batch_sizes): keys = [f"task:{i}".encode("utf-8") for i in range(sum(batch_sizes))] batches = [] offset = 0 for batch_size in batch_sizes: batches.append(keys[offset : offset + batch_size]) offset += batch_size state = RedisState.__new__(RedisState) state._redis = _FakeRedis(batches) return state def test_get_all_tasks_paginates_across_scan_batches(self): """ Redis SCAN 分批返回 key 时,分页切片必须按当前批次起始位置计算。 这个用例复现 PR #890 描述的 18 条任务、page_size=10 场景: 第一批 10 条,第二批 8 条。旧逻辑第一页会返回空列表,第二页 只返回 2 条;修复后第一页返回 10 条,第二页返回剩余 8 条。 """ state = self._build_state([10, 8]) first_page, first_total = state.get_all_tasks(page=1, page_size=10) second_page, second_total = state.get_all_tasks(page=2, page_size=10) self.assertEqual(first_total, 18) self.assertEqual(second_total, 18) self.assertEqual(len(first_page), 10) self.assertEqual(len(second_page), 8) self.assertEqual( [task["task_id"] for task in first_page], [f"task:{i}" for i in range(10)], ) self.assertEqual( [task["task_id"] for task in second_page], [f"task:{i}" for i in range(10, 18)], ) if __name__ == "__main__": unittest.main()