173 lines
6.3 KiB
Python
173 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from cognitive_agent.application.pet_cards import PetCardService, normalize_nfc_id
|
|
|
|
|
|
def wait_until(predicate, timeout: float = 1.0) -> bool:
|
|
deadline = time.monotonic() + timeout
|
|
while time.monotonic() < deadline:
|
|
if predicate():
|
|
return True
|
|
time.sleep(0.01)
|
|
return predicate()
|
|
|
|
|
|
def test_pet_cards_are_durable_and_nfc_id_only_comes_from_current_reader() -> None:
|
|
nfc = {"reader": {"online": True}, "card": None}
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
database = Path(directory, "pet-cards.sqlite3")
|
|
service = PetCardService(
|
|
database_path=database,
|
|
nfc_provider=lambda: nfc,
|
|
)
|
|
first = service.create_card("团团", "摇尾巴")
|
|
second = service.create_card("圆圆", "趴下")
|
|
|
|
assert first["nfc_id"] is None
|
|
assert service.update_card(
|
|
first["id"],
|
|
pet_name="团团 2",
|
|
action="向前走",
|
|
)["action"] == "向前走"
|
|
with pytest.raises(ValueError, match="当前未识别"):
|
|
service.bind_current_nfc(first["id"])
|
|
|
|
nfc["card"] = {"uid": "e1:13-b2 03"}
|
|
bound = service.bind_current_nfc(first["id"])
|
|
assert bound["nfc_id"] == "E113B203"
|
|
|
|
with pytest.raises(ValueError, match="先保存"):
|
|
service.bind_current_nfc(second["id"])
|
|
service.update_card(
|
|
second["id"],
|
|
pet_name="圆圆",
|
|
action="趴下",
|
|
)
|
|
rebound = service.bind_current_nfc(second["id"])
|
|
assert rebound["nfc_id"] == "E113B203"
|
|
cards = {card["id"]: card for card in service.snapshot()["cards"]}
|
|
assert cards[first["id"]]["nfc_id"] is None
|
|
assert cards[second["id"]]["nfc_id"] == "E113B203"
|
|
|
|
restored = PetCardService(database_path=database)
|
|
restored_cards = {card["id"]: card for card in restored.snapshot()["cards"]}
|
|
assert restored_cards[first["id"]]["pet_name"] == "团团 2"
|
|
assert restored_cards[second["id"]]["nfc_id"] == "E113B203"
|
|
assert restored.delete_card(first["id"])["pet_name"] == "团团 2"
|
|
assert restored.snapshot()["count"] == 1
|
|
|
|
|
|
def test_bound_present_nfc_publishes_one_three_frame_burst_per_detection() -> None:
|
|
nfc = {"reader": {"online": True}, "card": {"uid": "E113B203"}}
|
|
publications: list[tuple[float, dict[str, str]]] = []
|
|
|
|
def publish(payload: dict[str, str]) -> dict[str, object]:
|
|
publications.append((time.monotonic(), dict(payload)))
|
|
return {"delivery_status": "queued", "message_id": len(publications)}
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
service = PetCardService(
|
|
database_path=Path(directory, "pet-cards.sqlite3"),
|
|
nfc_provider=lambda: nfc,
|
|
publisher=publish,
|
|
burst_window_s=0.15,
|
|
)
|
|
card = service.create_card("Lucky", "向前跑")
|
|
service.update_card(
|
|
card["id"],
|
|
pet_name="Lucky",
|
|
action="向前跑",
|
|
)
|
|
service.bind_current_nfc(card["id"])
|
|
service.start()
|
|
try:
|
|
assert wait_until(lambda: len(publications) == 3)
|
|
assert all(
|
|
payload == {
|
|
"pet_name": "Lucky",
|
|
"nfc_id": "E113B203",
|
|
"action": "向前跑",
|
|
}
|
|
for _captured_at, payload in publications
|
|
)
|
|
assert publications[-1][0] - publications[0][0] <= 0.18
|
|
status = service.snapshot()["publisher"]
|
|
assert status["state"] == "burst_complete"
|
|
assert status["topic"] == "n7/pet"
|
|
assert status["burst_count"] == 3
|
|
assert status["burst_attempt_count"] == 3
|
|
assert status["burst_published_count"] == 3
|
|
assert status["publish_count"] == 3
|
|
|
|
time.sleep(0.18)
|
|
assert len(publications) == 3
|
|
|
|
nfc["card"] = None
|
|
assert wait_until(
|
|
lambda: service.snapshot()["publisher"]["state"]
|
|
== "waiting_for_nfc"
|
|
)
|
|
stopped_at = len(publications)
|
|
time.sleep(0.18)
|
|
assert len(publications) == stopped_at
|
|
|
|
nfc["card"] = {"uid": "E113B203"}
|
|
assert wait_until(lambda: len(publications) == 6)
|
|
assert service.snapshot()["publisher"]["state"] == "burst_complete"
|
|
finally:
|
|
service.close()
|
|
|
|
|
|
def test_invalid_pet_fields_and_nfc_uids_are_rejected() -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
service = PetCardService(
|
|
database_path=Path(directory, "pet-cards.sqlite3")
|
|
)
|
|
with pytest.raises(ValueError, match="宠物名"):
|
|
service.create_card("", "走动")
|
|
with pytest.raises(ValueError, match="宠物动作"):
|
|
service.create_card("Lucky", "")
|
|
|
|
assert normalize_nfc_id("04:a1-b2 c3") == "04A1B2C3"
|
|
assert normalize_nfc_id("not-a-card") == ""
|
|
|
|
|
|
def test_failed_pet_burst_stops_after_three_attempts_until_redetected() -> None:
|
|
nfc = {"reader": {"online": True}, "card": {"uid": "E113B203"}}
|
|
attempts: list[float] = []
|
|
|
|
def fail_publish(_payload: dict[str, str]) -> dict[str, object]:
|
|
attempts.append(time.monotonic())
|
|
raise RuntimeError("broker unavailable")
|
|
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
service = PetCardService(
|
|
database_path=Path(directory, "pet-cards.sqlite3"),
|
|
nfc_provider=lambda: nfc,
|
|
publisher=fail_publish,
|
|
burst_window_s=0.09,
|
|
)
|
|
card = service.create_card("Lucky", "坐下")
|
|
service.update_card(card["id"], pet_name="Lucky", action="坐下")
|
|
service.bind_current_nfc(card["id"])
|
|
service.start()
|
|
try:
|
|
assert wait_until(lambda: len(attempts) == 3)
|
|
status = service.snapshot()["publisher"]
|
|
assert status["state"] == "burst_failed"
|
|
assert status["burst_attempt_count"] == 3
|
|
assert status["burst_published_count"] == 0
|
|
assert status["publish_count"] == 0
|
|
assert status["last_error"] == "broker unavailable"
|
|
|
|
time.sleep(0.15)
|
|
assert len(attempts) == 3
|
|
finally:
|
|
service.close()
|