from __future__ import annotations import asyncio import pytest from platform_common.jobs import ( InMemoryJobStore, JobNotFoundError, JobRecord, JobStatus, JobStore, make_job_store, ) @pytest.fixture def store() -> InMemoryJobStore: return InMemoryJobStore() def test_protocol_conformance(store: InMemoryJobStore) -> None: assert isinstance(store, JobStore) async def test_create_and_get(store: InMemoryJobStore) -> None: record = await store.create("j1", {"input": "hello"}, ttl_seconds=60) assert record.id == "j1" assert record.status is JobStatus.PENDING assert record.payload == {"input": "hello"} fetched = await store.get("j1") assert fetched is not None assert fetched.payload == {"input": "hello"} async def test_create_duplicate_raises(store: InMemoryJobStore) -> None: await store.create("j1", ttl_seconds=60) with pytest.raises(ValueError, match="already exists"): await store.create("j1", ttl_seconds=60) async def test_get_missing_returns_none(store: InMemoryJobStore) -> None: assert await store.get("nope") is None async def test_update_patches_fields(store: InMemoryJobStore) -> None: await store.create("j1", {"a": 1}, ttl_seconds=60) updated = await store.update("j1", status=JobStatus.RUNNING, result={"out": 42}) assert updated.status is JobStatus.RUNNING assert updated.result == {"out": 42} assert updated.payload == {"a": 1} assert updated.updated_at >= updated.created_at async def test_update_missing_raises(store: InMemoryJobStore) -> None: with pytest.raises(JobNotFoundError): await store.update("nope", status=JobStatus.FAILED) async def test_delete(store: InMemoryJobStore) -> None: await store.create("j1", ttl_seconds=60) assert await store.delete("j1") is True assert await store.delete("j1") is False assert await store.get("j1") is None async def test_ttl_expiry(store: InMemoryJobStore) -> None: await store.create("j1", ttl_seconds=1) assert await store.exists("j1") await asyncio.sleep(1.1) assert await store.get("j1") is None assert await store.exists("j1") is False async def test_invalid_ttl(store: InMemoryJobStore) -> None: with pytest.raises(ValueError, match="positive"): await store.create("j1", ttl_seconds=0) async def test_lru_capacity() -> None: store = InMemoryJobStore(max_records=3) for i in range(5): await store.create(f"j{i}", ttl_seconds=60) ids = await store.list_ids() assert set(ids) == {"j2", "j3", "j4"} async def test_record_roundtrip() -> None: record = JobRecord(id="j1", status=JobStatus.SUCCEEDED, payload={"k": "v"}, result={"r": 1}) restored = JobRecord.from_dict(record.to_dict()) assert restored == record def test_make_job_store_default(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("JOB_STORE_URL", raising=False) assert isinstance(make_job_store(), InMemoryJobStore) def test_make_job_store_memory_explicit() -> None: assert isinstance(make_job_store("memory://"), InMemoryJobStore) def test_make_job_store_redis_not_yet_implemented() -> None: with pytest.raises(NotImplementedError, match=r"v0\.2"): make_job_store("redis://localhost:6379/0") def test_make_job_store_unknown_scheme() -> None: with pytest.raises(ValueError, match="unsupported"): make_job_store("kafka://broker")