import unittest from action_executor import ActionExecutor class _FakeRCON: def __init__(self, lua_return: str): self.lua_return = lua_return self.last_lua: str | None = None def lua(self, code: str) -> str: self.last_lua = code return self.lua_return class TestPlaceEntityReuse(unittest.TestCase): def test_place_entity_treats_reused_as_success(self): """ place_entity가 REUSED:-91,-68 같은 값을 반환하면, executor는 이를 성공으로 처리해야 한다. """ rcon = _FakeRCON("REUSED:-91,-68") ex = ActionExecutor(rcon) # type: ignore[arg-type] ok, msg = ex.place_entity("stone-furnace", x=-91, y=-68, direction="north") self.assertTrue(ok, msg) self.assertIn("재사용", msg) def test_place_entity_reuse_searches_a_wider_radius(self): """ REUSED 탐지를 ±1 타일(9칸)에서 끝내지 않고, 요청 좌표 중심으로 더 넓은 반경 내 기존 엔티티도 찾아야 한다. """ rcon = _FakeRCON("BLOCKED") ex = ActionExecutor(rcon) # type: ignore[arg-type] # 실제 성공/실패는 필요 없고, place_entity에 전달된 Lua 코드에 # wider radius 탐색이 포함됐는지만 확인한다. ex.place_entity("stone-furnace", x=-92, y=-70, direction="north") assert rcon.last_lua is not None self.assertIn("radius = 3", rcon.last_lua) def test_execute_bootstraps_furnace_when_place_entity_reused_message_has_coords(self): ex = ActionExecutor(_FakeRCON("OK")) def fake_place_entity(name: str, x: int, y: int, direction: str = "north"): return True, "stone-furnace 재사용 (-91, -68)" ex.place_entity = fake_place_entity # type: ignore[assignment] called: list[dict] = [] def fake_auto_bootstrap(*, furnace_name: str, x: int, y: int, reason: str = ""): called.append({ "furnace_name": furnace_name, "x": x, "y": y, "reason": reason, }) return True, "mock_bootstrap_ok" ex._auto_bootstrap_furnace = fake_auto_bootstrap # type: ignore[attr-defined] ok, msg = ex.execute({ "action": "place_entity", "reason": "기존 제련소 재사용 후 가동", "params": { "name": "stone-furnace", "x": -91, "y": -68, "direction": "north", }, }) self.assertTrue(ok, msg) self.assertEqual(len(called), 1) self.assertEqual(called[0]["furnace_name"], "stone-furnace") self.assertEqual(called[0]["x"], -91) self.assertEqual(called[0]["y"], -68)