US02-04: Expose Job APIs and Activity Events (#57)
This commit was merged in pull request #57.
This commit is contained in:
69
tests/integration/test_jobs_sse.py
Normal file
69
tests/integration/test_jobs_sse.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""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]
|
||||
Reference in New Issue
Block a user