Files
photoanalyzer/tests/integration/test_jobs_sse.py

70 lines
2.5 KiB
Python

"""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]