"""SSE activity stream: resumable event ids, reconnect without duplicate/lost events, and parity with the polling fallback.""" import pytest from fastapi.testclient import TestClient from photo_pipeline.api.app import create_app from photo_pipeline.config import Config from photo_pipeline.db import run_migrations from photo_pipeline.services.jobs import ItemState, JobService, JobState @pytest.fixture def client(tmp_path): (tmp_path / "data").mkdir() config = Config.from_env({"PHOTO_PIPELINE_DATA_DIR": str(tmp_path / "data")}) run_migrations(config.database_url) app = create_app(config) with TestClient(app) as test_client: test_client.app = app yield test_client def _finished_job(client): job = client.post("/api/v1/jobs", json={"job_type": "scan", "items": ["a", "b"]}).json() service = JobService(client.app.state.session_factory) service.claim(["scan"], "w1") for item in ("a", "b"): service.set_item(job["id"], item, ItemState.RUNNING) service.set_item(job["id"], item, ItemState.SUCCEEDED) service.transition(job["id"], JobState.SUCCEEDED, worker_id="w1") return job["id"] def _read_ids(client, job_id, headers=None): ids, saw_done = [], False with client.stream( "GET", f"/api/v1/jobs/{job_id}/events/stream", headers=headers or {} ) as stream: assert stream.headers["content-type"].startswith("text/event-stream") for line in stream.iter_lines(): if line.startswith("id:"): ids.append(int(line.split(":", 1)[1])) elif line.startswith("event: done"): saw_done = True return ids, saw_done def test_sse_streams_all_events_then_closes(client): job_id = _finished_job(client) ids, saw_done = _read_ids(client, job_id) assert ids == sorted(ids) and len(ids) >= 3 assert saw_done # terminal job closes the stream def test_sse_reconnect_has_no_duplicate_or_lost_events(client): job_id = _finished_job(client) full, _ = _read_ids(client, job_id) resume_from = full[1] resumed, saw_done = _read_ids(client, job_id, headers={"Last-Event-ID": str(resume_from)}) assert resumed == [i for i in full if i > resume_from] # no dup, none lost assert saw_done def test_sse_matches_polling(client): job_id = _finished_job(client) sse_ids, _ = _read_ids(client, job_id) polled = client.get(f"/api/v1/jobs/{job_id}/events").json()["events"] assert sse_ids == [e["seq"] for e in polled]