from __future__ import annotations import pytest import yuxi.services.run_queue_service as run_queue_service class _FakeRedisClient: def __init__(self, *, ping_error: Exception | None = None): self.ping_error = ping_error self.ping_calls = 0 self.closed = False async def ping(self): self.ping_calls += 1 if self.ping_error is not None: raise self.ping_error async def aclose(self): self.closed = True class _FakeStreamRedis: def __init__(self): self.streams: dict[str, list[tuple[str, dict[str, str]]]] = {} self.expire_calls: list[tuple[str, int]] = [] async def xadd(self, key: str, fields: dict[str, str], **kwargs): del kwargs stream = self.streams.setdefault(key, []) event_id = f"{1700000000000 + len(stream)}-0" stream.append((event_id, dict(fields))) return event_id async def expire(self, key: str, ttl: int): self.expire_calls.append((key, ttl)) async def xrange(self, key: str, min: str, max: str, count: int): del max rows = list(self.streams.get(key, [])) if min.startswith("("): cursor = min[1:] rows = [(event_id, fields) for event_id, fields in rows if event_id > cursor] elif min == "-": rows = list(rows) return rows[:count] async def xrevrange(self, key: str, max: str, min: str, count: int): del max, min rows = list(reversed(self.streams.get(key, []))) return rows[:count] @pytest.mark.asyncio async def test_get_redis_client_ping_once_and_cache(monkeypatch: pytest.MonkeyPatch): redis_asyncio = pytest.importorskip("redis.asyncio") fake_client = _FakeRedisClient() monkeypatch.setattr(run_queue_service, "_redis_client", None) monkeypatch.setattr( redis_asyncio.Redis, "from_url", staticmethod(lambda *args, **kwargs: fake_client), ) client_1 = await run_queue_service.get_redis_client() client_2 = await run_queue_service.get_redis_client() assert client_1 is fake_client assert client_2 is fake_client assert fake_client.ping_calls == 1 @pytest.mark.asyncio async def test_get_redis_client_ping_fail_fast(monkeypatch: pytest.MonkeyPatch): redis_asyncio = pytest.importorskip("redis.asyncio") fake_client = _FakeRedisClient(ping_error=RuntimeError("redis unavailable")) monkeypatch.setattr(run_queue_service, "_redis_client", None) monkeypatch.setattr( redis_asyncio.Redis, "from_url", staticmethod(lambda *args, **kwargs: fake_client), ) with pytest.raises(RuntimeError, match="Redis connection failed"): await run_queue_service.get_redis_client() assert run_queue_service._redis_client is None assert fake_client.closed is True @pytest.mark.asyncio async def test_run_stream_event_roundtrip(monkeypatch: pytest.MonkeyPatch): fake_redis = _FakeStreamRedis() monkeypatch.setattr(run_queue_service, "_redis_client", fake_redis) run_id = "run-1" seq1 = await run_queue_service.append_run_stream_event(run_id, "loading", {"items": [1]}) seq2 = await run_queue_service.append_run_stream_event( run_id, "finished", {"chunk": {"status": "finished", "thread_id": "child-thread"}}, ) assert seq1 < seq2 events = await run_queue_service.list_run_stream_events(run_id, after_seq="0-0", limit=100) assert [item["event_type"] for item in events] == ["loading", "finished"] assert events[0]["payload"]["schema_version"] == 1 assert events[0]["payload"]["run_id"] == run_id assert events[0]["payload"]["payload"] == {"items": [1]} assert events[1]["payload"]["thread_id"] == "child-thread" next_events = await run_queue_service.list_run_stream_events(run_id, after_seq=seq1, limit=100) assert len(next_events) == 1 assert next_events[0]["seq"] == seq2 last_seq = await run_queue_service.get_last_run_stream_seq(run_id) assert last_seq == seq2 def test_normalize_after_seq_stream_id_only(): assert run_queue_service.normalize_after_seq(None) == "0-0" assert run_queue_service.normalize_after_seq("1700000000000-3") == "1700000000000-3" assert run_queue_service.normalize_after_seq("12") == "0-0" assert run_queue_service.normalize_after_seq("bad-value") == "0-0"