100 lines
3.6 KiB
Python
100 lines
3.6 KiB
Python
"""tests/test_events.py —— 事件总线(发布/订阅/持久/快照/回放/seq)。"""
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import json
|
||
|
||
import pytest
|
||
|
||
from app.services.bioflow.events import EVENT_TYPES, EventBus
|
||
|
||
pytestmark = pytest.mark.bioflow_unit
|
||
|
||
|
||
def _run(coro):
|
||
return asyncio.new_event_loop().run_until_complete(coro)
|
||
|
||
|
||
def test_publish_replay_persistence(tmp_path):
|
||
bus = EventBus(tmp_path / "events")
|
||
e1 = bus.publish("job_created", {"job_id": "j1", "title": "t", "stages": []})
|
||
e2 = bus.publish("stage_started", {"job_id": "j1", "stage": "identify"})
|
||
assert e2["seq"] == e1["seq"] + 1
|
||
# 持久文件存在
|
||
f = tmp_path / "events" / "j1.jsonl"
|
||
assert f.exists()
|
||
lines = f.read_text(encoding="utf-8").splitlines()
|
||
assert len(lines) == 2
|
||
assert json.loads(lines[0])["type"] == "job_created"
|
||
# 回放
|
||
replay = bus.replay("j1")
|
||
assert [e["type"] for e in replay] == ["job_created", "stage_started"]
|
||
# after_seq
|
||
assert bus.replay("j1", after_seq=e1["seq"])[0]["type"] == "stage_started"
|
||
|
||
|
||
def test_unknown_event_type_rejected(tmp_path):
|
||
bus = EventBus(tmp_path / "events")
|
||
with pytest.raises(ValueError, match="未知事件类型"):
|
||
bus.publish("nope", {"job_id": "j"})
|
||
|
||
|
||
def test_event_types_complete():
|
||
"""TC-BIO-SSE-001:12 类事件全集。"""
|
||
expected = {
|
||
"job_created", "plan_ready", "stage_started", "stage_log", "stage_done",
|
||
"stage_failed", "stage_skipped", "stage_await_confirm", "stage_await_files",
|
||
"job_done", "job_paused", "job_archived",
|
||
}
|
||
assert expected <= set(EVENT_TYPES)
|
||
|
||
|
||
def test_subscribe_broadcast(tmp_path):
|
||
async def scenario():
|
||
bus = EventBus(tmp_path / "events")
|
||
sub1 = await bus.subscribe(job_filter="j1")
|
||
sub2 = await bus.subscribe(job_filter="j1")
|
||
sub3 = await bus.subscribe(job_filter="j2")
|
||
bus.publish("stage_started", {"job_id": "j1", "stage": "identify"})
|
||
e1 = await asyncio.wait_for(sub1.queue.get(), timeout=2)
|
||
e2 = await asyncio.wait_for(sub2.queue.get(), timeout=2)
|
||
assert e1["type"] == "stage_started"
|
||
assert e2["seq"] == e1["seq"]
|
||
assert sub3.queue.empty() # job 过滤
|
||
await bus.unsubscribe(sub1)
|
||
await bus.unsubscribe(sub2)
|
||
await bus.unsubscribe(sub3)
|
||
return True
|
||
|
||
assert _run(scenario())
|
||
|
||
|
||
def test_snapshot(tmp_path):
|
||
bus = EventBus(tmp_path / "events")
|
||
bus.publish("job_created", {"job_id": "j1", "title": "t", "stages": ["identify", "phylogeny"]})
|
||
bus.publish("stage_started", {"job_id": "j1", "stage": "identify"})
|
||
bus.publish("stage_done", {"job_id": "j1", "stage": "identify", "exit_code": 0})
|
||
snap = bus.snapshot("j1")
|
||
assert snap["type"] == "snapshot"
|
||
assert snap["job"]["status"] == "created"
|
||
stages = {s["stage"]: s["status"] for s in snap["stages"]}
|
||
assert stages["identify"] == "done"
|
||
assert stages["phylogeny"] == "waiting"
|
||
assert snap["last_seq"] == 3
|
||
assert bus.snapshot("nope") is None
|
||
|
||
|
||
def test_snapshot_job_done(tmp_path):
|
||
bus = EventBus(tmp_path / "events")
|
||
bus.publish("job_created", {"job_id": "j1", "stages": []})
|
||
bus.publish("job_done", {"job_id": "j1"})
|
||
assert bus.snapshot("j1")["job"]["status"] == "done"
|
||
|
||
|
||
def test_memory_replay_after_seq(tmp_path):
|
||
bus = EventBus(tmp_path / "events")
|
||
e1 = bus.publish("stage_started", {"job_id": "j1", "stage": "a"})
|
||
bus.publish("stage_done", {"job_id": "j1", "stage": "a"})
|
||
replay = bus.memory_replay(after_seq=e1["seq"])
|
||
assert [e["type"] for e in replay] == ["stage_done"]
|