Files
Cognitive_Agent/tests/unit/test_pet_cards.py
T

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()