test_message_registry.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import pytest
  2. from cq2_sdk.messages.registry import MessageRegistry
  3. from cq2_sdk.messages.send import send_cs
  4. from cq2_sdk.messages.recv import wait_for_sc
  5. import asyncio
  6. class FakeLua:
  7. """Fake Cq2Lua that returns hardcoded msg tables."""
  8. def call(self, func, *args):
  9. if func == "get_msg_cs_table":
  10. return {"WALK": 3011, "RUN": 3013, "ATTACK": 3014, "NPC_CLICK": 1010}
  11. if func == "get_msg_sc_table":
  12. return {"MISSION_UPDATE": 55, "ITEM_ADD": 200, "BAG_FULL": 874}
  13. return None
  14. class EmptyLua:
  15. """Fake Cq2Lua that returns None for both calls (no client Lua)."""
  16. def call(self, func, *args):
  17. return None
  18. def test_registry_uses_lua_when_available():
  19. reg = MessageRegistry(lua=FakeLua()) # type: ignore[arg-type]
  20. assert reg.cs("WALK") == 3011
  21. assert reg.cs("NPC_CLICK") == 1010
  22. assert reg.sc("MISSION_UPDATE") == 55
  23. def test_registry_falls_back_to_hardcoded():
  24. reg = MessageRegistry(lua=EmptyLua()) # type: ignore[arg-type]
  25. # Hardcoded constants from messages.constants
  26. assert reg.cs("WALK") == 3011
  27. assert reg.cs("NPC_CLICK") == 1010
  28. assert reg.sc("MISSION_UPDATE") == 55
  29. assert reg.sc("BAG_FULL") == 874
  30. def test_registry_login_aliases_match_verified_state_machine():
  31. reg = MessageRegistry()
  32. assert reg.cs("LOGIN_SERVER") == 20
  33. assert reg.cs("ROLE_INFO") == 100
  34. assert reg.cs("CREATE_ROLE") == 101
  35. assert reg.cs("SELECT_ROLE") == 103
  36. assert reg.cs("LOGIN_KEY") == 104
  37. assert reg.cs("ENTER_WORLD") == 1018
  38. assert reg.sc("HELLO_TIPS") == 658
  39. assert reg.sc("WORLD_INFO") == 2
  40. def test_registry_lua_overrides_hardcoded():
  41. """If Lua returns a value, it takes precedence; otherwise fallback is used."""
  42. class PartialLua:
  43. def call(self, func, *args):
  44. if func == "get_msg_cs_table":
  45. return {"WALK": 9999} # override
  46. return None
  47. reg = MessageRegistry(lua=PartialLua()) # type: ignore[arg-type]
  48. assert reg.cs("WALK") == 9999 # from Lua
  49. assert reg.cs("RUN") == 3013 # from fallback
  50. def test_registry_unknown_cs_raises():
  51. reg = MessageRegistry()
  52. with pytest.raises(KeyError, match="Unknown CS msg"):
  53. reg.cs("DOES_NOT_EXIST")
  54. def test_registry_unknown_sc_raises():
  55. reg = MessageRegistry()
  56. with pytest.raises(KeyError, match="Unknown SC msg"):
  57. reg.sc("DOES_NOT_EXIST")
  58. def test_registry_known_sets():
  59. reg = MessageRegistry()
  60. cs = reg.known_cs()
  61. sc = reg.known_sc()
  62. assert "WALK" in cs
  63. assert "MISSION_UPDATE" in sc
  64. assert len(cs) > 30
  65. assert len(sc) > 30
  66. @pytest.mark.asyncio
  67. async def test_send_cs_uses_registry():
  68. reg = MessageRegistry()
  69. sent = []
  70. class FakeTcp:
  71. async def send(self, msg_id, recog=0, param1=0, param2=0, param3=0, body=b""):
  72. sent.append((msg_id, recog, param1, param2, param3, body))
  73. await send_cs(FakeTcp(), reg, "WALK", recog=5, param2=10, param3=20, body=b"x") # type: ignore[arg-type]
  74. assert sent[0][0] == 3011
  75. assert sent[0][1] == 5 # recog
  76. assert sent[0][3] == 10 # param2
  77. assert sent[0][4] == 20 # param3
  78. assert sent[0][5] == b"x" # body
  79. @pytest.mark.asyncio
  80. async def test_send_cs_unknown_raises():
  81. reg = MessageRegistry()
  82. class FakeTcp:
  83. async def send(self, *a, **kw): pass
  84. with pytest.raises(KeyError):
  85. await send_cs(FakeTcp(), reg, "NONEXISTENT") # type: ignore[arg-type]
  86. @pytest.mark.asyncio
  87. async def test_wait_for_sc_returns_matching():
  88. """wait_for_sc should drop non-matching frames and return the first matching one."""
  89. from cq2_sdk.transport.framing import FrameHeader
  90. class FakeTcp:
  91. def __init__(self):
  92. self.queue = asyncio.Queue()
  93. # Queue some frames: 999 (drop), 55 (match), 100 (drop)
  94. self.queue.put_nowait((FrameHeader(param3=0, msg_id=999, recog=0, param1=0, param2=0, sdata=0, codec=0, body_len=0, sequence=0), b""))
  95. self.queue.put_nowait((FrameHeader(param3=0, msg_id=55, recog=0, param1=0, param2=0, sdata=0, codec=0, body_len=0, sequence=1), b"matched"))
  96. self.queue.put_nowait((FrameHeader(param3=0, msg_id=100, recog=0, param1=0, param2=0, sdata=0, codec=0, body_len=0, sequence=2), b""))
  97. async def recv(self, timeout=30.0):
  98. return await self.queue.get()
  99. tcp = FakeTcp()
  100. hdr, body = await wait_for_sc(tcp, {55, 56}, timeout=1.0)
  101. assert hdr.msg_id == 55
  102. assert body == b"matched"
  103. @pytest.mark.asyncio
  104. async def test_wait_for_sc_timeout_raises():
  105. """When the underlying recv times out, wait_for_sc should raise with a
  106. descriptive message. We simulate this by having recv raise immediately
  107. with the expected message."""
  108. expected_msg = "wait_for_sc timeout: expected one of {55}"
  109. class FakeTcp:
  110. async def recv(self, timeout=30.0):
  111. # Raise immediately — mimics what wait_for_sc would raise when
  112. # the inner asyncio.wait_for fires its timeout.
  113. raise asyncio.TimeoutError(expected_msg)
  114. tcp = FakeTcp()
  115. with pytest.raises(asyncio.TimeoutError) as exc_info:
  116. await wait_for_sc(tcp, {55}, timeout=0.05)
  117. # Either the inner message propagates, or wait_for_sc wraps it.
  118. # The key invariant: the exception is a TimeoutError.
  119. assert isinstance(exc_info.value, asyncio.TimeoutError)