feat: replace GLM API with local Ollama structured output, remove ~400 lines of JSON repair code
This commit is contained in:
@@ -1,87 +1,47 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
from ai_planner import AIPlanner
|
||||
|
||||
|
||||
class TestAIPlannerParseJson(unittest.TestCase):
|
||||
class TestAIPlannerFallback(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# AIPlanner 생성 시 ZAI_API_KEY가 필요하므로 테스트에서는 더미를 주입한다.
|
||||
os.environ.setdefault("ZAI_API_KEY", "dummy")
|
||||
self.planner = AIPlanner()
|
||||
|
||||
def test_parse_json_object(self):
|
||||
raw = (
|
||||
def test_fallback_uses_ore_anchor_from_summary(self):
|
||||
summary = "- 위치: (0, 0)\n- iron-ore: 100타일 (앵커: 50, 30)\n"
|
||||
plan = self.planner._fallback_plan_from_summary(summary)
|
||||
actions = plan["actions"]
|
||||
self.assertTrue(any(a["action"] == "mine_resource" for a in actions))
|
||||
|
||||
def test_fallback_explore_when_no_anchors(self):
|
||||
summary = "- 위치: (0, 0)\n"
|
||||
plan = self.planner._fallback_plan_from_summary(summary)
|
||||
actions = plan["actions"]
|
||||
self.assertTrue(any(a["action"] == "explore" for a in actions))
|
||||
|
||||
def test_decide_returns_actions_from_ollama(self):
|
||||
mock_response = MagicMock()
|
||||
mock_response.message.content = (
|
||||
'{"thinking":"t","current_goal":"g",'
|
||||
'"actions":[{"action":"explore","params":{"direction":"east","max_steps":1},"reason":"x"}],'
|
||||
'"after_this":"a"}'
|
||||
)
|
||||
plan = self.planner._parse_json(raw)
|
||||
self.assertEqual(plan["current_goal"], "g")
|
||||
self.assertEqual(len(plan["actions"]), 1)
|
||||
self.assertEqual(plan["actions"][0]["action"], "explore")
|
||||
with patch("ai_planner.ollama.Client") as MockClient:
|
||||
MockClient.return_value.chat.return_value = mock_response
|
||||
actions = self.planner.decide("## 스텝 1\n현재 상태: 초기")
|
||||
self.assertIsInstance(actions, list)
|
||||
self.assertTrue(len(actions) >= 1)
|
||||
self.assertEqual(actions[0]["action"], "explore")
|
||||
|
||||
def test_parse_json_array_top_level(self):
|
||||
raw = '[{"action":"explore","params":{"direction":"east","max_steps":1},"reason":"x"}]'
|
||||
plan = self.planner._parse_json(raw)
|
||||
self.assertEqual(len(plan["actions"]), 1)
|
||||
self.assertEqual(plan["actions"][0]["action"], "explore")
|
||||
self.assertIn("after_this", plan)
|
||||
def test_decide_falls_back_when_ollama_raises(self):
|
||||
summary = "- 위치: (10, 10)\n- iron-ore: 50타일 (앵커: 60, 10)\n"
|
||||
with patch("ai_planner.ollama.Client") as MockClient:
|
||||
MockClient.return_value.chat.side_effect = Exception("connection refused")
|
||||
actions = self.planner.decide(summary)
|
||||
self.assertIsInstance(actions, list)
|
||||
self.assertTrue(len(actions) >= 1)
|
||||
|
||||
def test_parse_json_array_with_code_fence(self):
|
||||
raw = (
|
||||
"```json\n"
|
||||
'[{"action":"explore","params":{"direction":"east","max_steps":1},"reason":"x"}]\n'
|
||||
"```"
|
||||
)
|
||||
plan = self.planner._parse_json(raw)
|
||||
self.assertEqual(len(plan["actions"]), 1)
|
||||
self.assertEqual(plan["actions"][0]["action"], "explore")
|
||||
|
||||
def test_extract_glm_text_prefers_content_then_reasoning(self):
|
||||
# content가 비어있고 reasoning_content에 JSON이 들어있는 케이스
|
||||
fake = {
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "length",
|
||||
"message": {
|
||||
"content": "",
|
||||
"reasoning_content": '{"thinking":"t","current_goal":"g","actions":[],"after_this":"a"}',
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
extracted = self.planner._extract_glm_assistant_text(fake)
|
||||
self.assertIn('"current_goal":"g"', extracted)
|
||||
|
||||
def test_extract_glm_text_uses_reasoning_when_content_has_no_json(self):
|
||||
fake = {
|
||||
"choices": [
|
||||
{
|
||||
"finish_reason": "length",
|
||||
"message": {
|
||||
"content": "1. **Current State Analysis:**\n- Location: (0, 0)\n- Inventory: {...}",
|
||||
"reasoning_content": '{"thinking":"t","current_goal":"g","actions":[{"action":"explore","params":{"direction":"east","max_steps":1},"reason":"x"}],"after_this":"a"}',
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
extracted = self.planner._extract_glm_assistant_text(fake)
|
||||
self.assertIn('"current_goal":"g"', extracted)
|
||||
|
||||
def test_parse_json_selects_actions_object_when_multiple_json_objects_exist(self):
|
||||
# 분석 텍스트 안에 먼저 나오는 하위 JSON({ "foo": 1 })이 있고,
|
||||
# 뒤에 실제 계획 JSON({ "actions": [...] })이 있는 경우를 검증한다.
|
||||
raw = (
|
||||
"1. Analyze...\n"
|
||||
'{"foo": 1}\n'
|
||||
"2. Continue...\n"
|
||||
'{"thinking":"t","current_goal":"g",'
|
||||
'"actions":[{"action":"explore","params":{"direction":"east","max_steps":1},"reason":"x"}],'
|
||||
'"after_this":"a"}'
|
||||
)
|
||||
plan = self.planner._parse_json(raw)
|
||||
self.assertEqual(plan["current_goal"], "g")
|
||||
self.assertEqual(len(plan["actions"]), 1)
|
||||
self.assertEqual(plan["actions"][0]["action"], "explore")
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user