diff --git a/assemblymcp/config.py b/assemblymcp/config.py index c6aca68..f6e2dee 100644 --- a/assemblymcp/config.py +++ b/assemblymcp/config.py @@ -4,7 +4,7 @@ class Settings(BaseSettings): # Core Settings - assembly_api_key: str | None = Field(None, description="National Assembly API Key") + api_key: str | None = Field(None, description="National Assembly API Key") default_assembly_age: str = Field("22", description="Default Assembly Age (e.g. 22)") # Logging Settings diff --git a/assemblymcp/server.py b/assemblymcp/server.py index 31b27e1..8e77b02 100644 --- a/assemblymcp/server.py +++ b/assemblymcp/server.py @@ -40,7 +40,7 @@ # Initialize API Client globally to load specs once try: - client = AssemblyAPIClient(api_key=settings.assembly_api_key) + client = AssemblyAPIClient(api_key=settings.api_key) except Exception as e: logger.error(f"Failed to initialize client: {e}") client = None @@ -106,7 +106,7 @@ async def get_assembly_info() -> str: return "Error: API Client not initialized. Please check API key configuration." try: - api_key_status = "configured" if settings.assembly_api_key else "not configured" + api_key_status = "configured" if settings.api_key else "not configured" service_count = len(client.service_map) return ( "AssemblyMCP – 대한민국 국회 OpenAPI (Korean National Assembly Open API)\n" @@ -704,7 +704,7 @@ def main(): """Run the MCP server""" sys.stdout.reconfigure(line_buffering=True) # Validate settings on startup (but don't fail if API key is missing yet) - if not settings.assembly_api_key: + if not settings.api_key: logger.warning("ASSEMBLY_API_KEY is not configured. The server will run but tools will fail.") # Check for transport configuration diff --git a/assemblymcp/services.py b/assemblymcp/services.py index e78bd90..a633cc9 100644 --- a/assemblymcp/services.py +++ b/assemblymcp/services.py @@ -52,7 +52,7 @@ def normalize_age(val: Any) -> str: retry=retry_if_exception_type((AssemblyAPIError, httpx.RequestError)), reraise=True, ) -async def _get_data_with_retry(client: AssemblyAPIClient, service_id: str, params: dict[str, Any]) -> dict[str, Any]: +async def _get_data_with_retry(client: AssemblyAPIClient, service_id: str, params: dict[str, Any]) -> list[Any] | str: """Fetch data from API with retry logic.""" try: return await client.get_data(service_id_or_name=service_id, params=params) @@ -64,23 +64,14 @@ async def _get_data_with_retry(client: AssemblyAPIClient, service_id: str, param def _collect_rows(raw_data: Any) -> list[dict[str, Any]]: - """Walk nested API response objects and return every dict row.""" - rows: list[dict[str, Any]] = [] + """Convert get_data() result to a flat list of dicts. - def _walk(node: Any) -> None: - if isinstance(node, dict): - if "row" in node and isinstance(node["row"], list): - for entry in node["row"]: - if isinstance(entry, dict): - rows.append(entry) - for value in node.values(): - _walk(value) - elif isinstance(node, list): - for item in node: - _walk(item) - - _walk(raw_data) - return rows + get_data() returns list[BaseModel] (generated types) or list[dict] (fallback). + Both cases are handled uniformly here. + """ + if not raw_data or isinstance(raw_data, str): + return [] + return [item.model_dump() if hasattr(item, "model_dump") else item for item in raw_data] class DiscoveryService: @@ -150,15 +141,17 @@ async def list_services(self, keyword: str = "") -> list[dict[str, str]]: results.sort(key=lambda x: x["name"]) return results - async def call_raw(self, service_id_or_name: str, params: dict[str, Any]) -> dict[str, Any]: + async def call_raw(self, service_id_or_name: str, params: dict[str, Any]) -> list[dict[str, Any]]: """ Call a specific API service with raw parameters. + Returns rows as a list of dicts (JSON-serializable). """ # 파라미터 자동 보정 적용 normalized_params = self._normalize_params(params) try: - return await _get_data_with_retry(self.client, service_id_or_name, normalized_params) + raw = await _get_data_with_retry(self.client, service_id_or_name, normalized_params) + return _collect_rows(raw) except AssemblyAPIError as e: error_msg = str(e) @@ -919,68 +912,33 @@ async def get_committee_members( params["COMMITTEE_NAME"] = committee_name raw_data = await _get_data_with_retry(self.client, self.COMMITTEE_MEMBER_LIST_ID, params) - - # Explicitly check for INFO-200 (No corresponding data) from the raw API response - # Structure is usually { "service_name": [ { "head": [...] }, { "row": [...] } ] } - # or just { "RESULT": { "CODE": "...", "MESSAGE": "..." } } - service_key = list(raw_data.keys())[0] if raw_data else None - if service_key and isinstance(raw_data[service_key], list) and len(raw_data[service_key]) > 0: - head_section = raw_data[service_key][0].get("head") - if head_section and len(head_section) > 1: - result = head_section[1].get("RESULT") - if result and result.get("CODE") == "INFO-200": - msg = result.get("MESSAGE") - - error_details = { - "error_type": "DATA_NOT_FOUND", - "api_code": result.get("CODE"), - "api_message": msg, - "query_info": { - "committee_name": committee_name, - "committee_code": committee_code, - }, - "suggestion": ( - "이 위원회에 대한 위원 명단 데이터가 국회 OpenAPI에 없거나, " - "검색 조건(이름)이 정확하지 않을 수 있습니다. " - "get_committee_list()를 호출하여 정확한 committee_code를 " - "확인 후 다시 시도하거나, 다른 검색어를 사용해보세요." - ), - } - - # If searched by name and no code, try to find suggestions - if committee_name and not committee_code: - try: - candidates = await self.get_committee_list(committee_name=committee_name) - valid_candidates = [] - for c in candidates: - if c.HR_DEPT_CD and c.HR_DEPT_CD != "None": - valid_candidates.append(f"{c.COMMITTEE_NAME}(코드: {c.HR_DEPT_CD})") - - if valid_candidates: - error_details["suggestion"] = ( - f"입력하신 위원회명 '{committee_name}'에 대해 " - "위원 명단을 찾을 수 없습니다. " - f"다음과 같은 관련 위원회가 있습니다. " - f"해당 코드(committee_code)로 재시도해보세요: " - f"{', '.join(valid_candidates)}" - ) - else: - error_details["suggestion"] = ( - f"'{committee_name}'(으)로 검색된 위원회 중 " - f"유효한 코드(HR_DEPT_CD)를 가진 위원회가 없습니다. " - "이는 해당 위원회 명단이 OpenAPI에 없거나, " - "이름이 정확하지 않을 수 있습니다. " - "get_committee_list()를 호출하여 " - "전체 위원회 목록을 확인해보세요." - ) - except Exception as e: - logger.warning(f"Error generating suggestions for '{committee_name}': {e}") - # Fallback to generic suggestion if suggestion generation fails - - return {"error": error_details} - rows = _collect_rows(raw_data) + if not rows and committee_name and not committee_code: + # 빈 결과일 때 관련 위원회 코드 제안 + suggestion = ( + "이 위원회에 대한 위원 명단 데이터가 국회 OpenAPI에 없거나, " + "검색 조건(이름)이 정확하지 않을 수 있습니다. " + "get_committee_list()를 호출하여 정확한 committee_code를 " + "확인 후 다시 시도하거나, 다른 검색어를 사용해보세요." + ) + try: + candidates = await self.get_committee_list(committee_name=committee_name) + valid_candidates = [ + f"{c.COMMITTEE_NAME}(코드: {c.HR_DEPT_CD})" + for c in candidates + if c.HR_DEPT_CD and c.HR_DEPT_CD != "None" + ] + if valid_candidates: + suggestion = ( + f"입력하신 위원회명 '{committee_name}'에 대해 위원 명단을 찾을 수 없습니다. " + f"다음과 같은 관련 위원회가 있습니다. " + f"해당 코드(committee_code)로 재시도해보세요: {', '.join(valid_candidates)}" + ) + except Exception as e: + logger.warning(f"Error generating suggestions for '{committee_name}': {e}") + return {"error": {"error_type": "DATA_NOT_FOUND", "suggestion": suggestion}} + # CRITICAL FIX: The API sometimes ignores HR_DEPT_CD and returns all members. # We must manually filter by committee_code if it was provided. if committee_code: diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md new file mode 100644 index 0000000..e36c35d --- /dev/null +++ b/docs/PROJECT_STATUS.md @@ -0,0 +1,48 @@ +# AssemblyMCP Project Status + +이 문서는 현재 워크트리 상태, 최근 변경 방향, 다음 작업 우선순위를 빠르게 파악하기 위한 운영 메모입니다. + +## Current Status + +- 기준 시점: 2026-04-01 +- 개발 상태: 실행 가능, 핵심 테스트 통과 +- 현재 작업 브랜치: `refactor/pydantic-model-first` +- 핵심 연동 상태: `assembly-api-client` `v1.2.6` 계약 반영 완료 + +## Verified State + +- `AssemblyMCP`: `uv run pytest -q` -> `67 passed` +- `assembly-api-client`: `uv run pytest -q` -> `254 passed` +- 실데이터 스모크 확인 완료: + - 최근 법안 조회 + - 의원 정보 조회 + - 위원회 일정 조회 + - 위원회 목록 조회 + +## Recent Technical Changes + +- `assembly-api-client>=1.2.6`로 의존성 상향 +- `get_data()`의 반환 계약 변경 반영 + - 과거: 중첩된 raw dict 중심 응답 + - 현재: `list[BaseModel]` 또는 `list[dict]` +- `assemblymcp/services.py`에서 `_collect_rows()`로 응답 평탄화 통일 +- 관련 테스트 전반을 새 계약 기준으로 정리 + +## Operational Notes + +- `.env`의 `ASSEMBLY_API_KEY` 설정 시 실 API 호출 가능 +- `assemblymcp/config.py` 설정 필드는 `assembly_api_key`가 아니라 `api_key` +- `MeetingService.search_meetings()`는 `page_size`가 아니라 `limit` 인자를 사용 + +## Remaining Workspace Notes + +- 이 저장소에는 코드 미완료 흔적이 거의 없음 +- 별도 저장소 `assembly-api-client`에는 `uv.lock` 수정이 남아 있음 +- 이 저장소의 `.agent/` 메모는 이 문서로 통합함 + +## Suggested Next Steps + +1. `assembly-api-client`의 `uv.lock` 변경을 유지할지 결정 +2. 필요하면 Docker 빌드/런타임 검증 추가 +3. 배포 전 실제 사용 시나리오 기준 스모크 스크립트 정리 +4. 운영 문서가 더 필요하면 `docs/HANDOVER_GUIDE.md`와 역할 분리 유지 diff --git a/pyproject.toml b/pyproject.toml index 7fc98da..f96fdd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ license = "MIT" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "assembly-api-client>=1.1.0", + "assembly-api-client>=1.2.6", "beautifulsoup4>=4.14.2", "fastmcp>=2.13.1", "httpx>=0.28.1", diff --git a/tests/test_bill_service.py b/tests/test_bill_service.py index cb69da2..7645b2c 100644 --- a/tests/test_bill_service.py +++ b/tests/test_bill_service.py @@ -19,30 +19,23 @@ def bill_service(mock_client): @pytest.mark.asyncio async def test_get_bill_info_success(bill_service, mock_client): - # Mock API response - mock_response = { - "O4K6HM0012064I15889": [ + mock_client.get_data = AsyncMock( + return_value=[ { - "head": [{"RESULT": {"CODE": "INFO-000", "MESSAGE": "Success"}}], - "row": [ - { - "BILL_ID": "PRC_T2T3T4T5T6T7T8T9", - "BILL_NO": "2100001", - "BILL_NAME": "Test Bill", - "PROPOSER": "Test Proposer", - "PROPOSER_KIND": "Member", - "PROC_RESULT_NM": "Passed", - "CURR_COMMITTEE": "Legislation", - "PROPOSE_DT": "20200101", - "COMMITTEE_DT": "20200102", - "PROC_DT": "20200103", - "LINK_URL": "http://test.com", - } - ], + "BILL_ID": "PRC_T2T3T4T5T6T7T8T9", + "BILL_NO": "2100001", + "BILL_NAME": "Test Bill", + "PROPOSER": "Test Proposer", + "PROPOSER_KIND": "Member", + "PROC_RESULT_NM": "Passed", + "CURR_COMMITTEE": "Legislation", + "PROPOSE_DT": "20200101", + "COMMITTEE_DT": "20200102", + "PROC_DT": "20200103", + "LINK_URL": "http://test.com", } ] - } - mock_client.get_data = AsyncMock(return_value=mock_response) + ) bills = await bill_service.get_bill_info(age="21", limit=1) @@ -53,43 +46,32 @@ async def test_get_bill_info_success(bill_service, mock_client): assert bills[0].BILL_NAME == "Test Bill" assert bills[0].PROPOSER == "Test Proposer" - # Verify API call parameters mock_client.get_data.assert_called_once() call_args = mock_client.get_data.call_args assert call_args.kwargs["service_id_or_name"] == "O4K6HM0012064I15889" assert call_args.kwargs["params"]["AGE"] == "21" assert call_args.kwargs["params"]["pSize"] == 1 - mock_client.get_data = AsyncMock(return_value={}) + mock_client.get_data = AsyncMock(return_value=[]) bills = await bill_service.get_bill_info(age="21") assert bills == [] @pytest.mark.asyncio async def test_search_bills_fallback(bill_service, mock_client): - # Mock first call (age 22) returning empty - # Mock second call (age 21) returning results - - # We need to handle multiple calls to get_data with different params async def side_effect(service_id_or_name, params, **kwargs): if params.get("AGE") == "22": - return {} + return [] if params.get("AGE") == "21": - return { - "O4K6HM0012064I15889": [ - { - "row": [ - { - "BILL_ID": "2100001", - "BILL_NAME": "Fallback Bill", - "PROPOSE_DT": "20200101", - "LINK_URL": "http://test.com", - } - ] - } - ] - } - return {} + return [ + { + "BILL_ID": "2100001", + "BILL_NAME": "Fallback Bill", + "PROPOSE_DT": "20200101", + "LINK_URL": "http://test.com", + } + ] + return [] mock_client.get_data = AsyncMock(side_effect=side_effect) @@ -102,61 +84,40 @@ async def side_effect(service_id_or_name, params, **kwargs): @pytest.mark.asyncio async def test_get_recent_bills_sorting(bill_service, mock_client): - # Mock unsorted response - mock_response = { - "O4K6HM0012064I15889": [ - { - "row": [ - {"BILL_ID": "1", "BILL_NAME": "Old", "PROPOSE_DT": "20230101", "LINK_URL": "x"}, - {"BILL_ID": "2", "BILL_NAME": "New", "PROPOSE_DT": "20231231", "LINK_URL": "x"}, - {"BILL_ID": "3", "BILL_NAME": "Mid", "PROPOSE_DT": "20230601", "LINK_URL": "x"}, - ] - } + mock_client.get_data = AsyncMock( + return_value=[ + {"BILL_ID": "1", "BILL_NAME": "Old", "PROPOSE_DT": "20230101", "LINK_URL": "x"}, + {"BILL_ID": "2", "BILL_NAME": "New", "PROPOSE_DT": "20231231", "LINK_URL": "x"}, + {"BILL_ID": "3", "BILL_NAME": "Mid", "PROPOSE_DT": "20230601", "LINK_URL": "x"}, ] - } - mock_client.get_data = AsyncMock(return_value=mock_response) + ) bills = await bill_service.get_recent_bills(limit=3) assert len(bills) == 3 - assert bills[0].BILL_NAME == "New" # 2023-12-31 - assert bills[1].BILL_NAME == "Mid" # 2023-06-01 - assert bills[2].BILL_NAME == "Old" # 2023-01-01 + assert bills[0].BILL_NAME == "New" + assert bills[1].BILL_NAME == "Mid" + assert bills[2].BILL_NAME == "Old" @pytest.mark.asyncio async def test_get_bill_details(bill_service, mock_client): - # Mock basic info response with both BILL_ID and BILL_NO - basic_response = { - "O4K6HM0012064I15889": [ - { - "row": [ - { - "BILL_ID": "PRC_X1Y2Z3A4B5C6D7E8", - "BILL_NO": "2200001", - "BILL_NAME": "Detail Bill", - "PROPOSE_DT": "20240101", - "LINK_URL": "http://test.com", - } - ] - } - ] - } - - # Mock detail info response - detail_response = { - "OS46YD0012559515463": [{"row": [{"MAIN_CNTS": "This is the summary.", "RSON_CONT": "This is the reason."}]}] - } - async def side_effect(service_id_or_name, params, **kwargs): if service_id_or_name == bill_service.BILL_SEARCH_ID: - return basic_response + return [ + { + "BILL_ID": "PRC_X1Y2Z3A4B5C6D7E8", + "BILL_NO": "2200001", + "BILL_NAME": "Detail Bill", + "PROPOSE_DT": "20240101", + "LINK_URL": "http://test.com", + } + ] if service_id_or_name == bill_service.BILL_DETAIL_ID: - # Verify that BILL_NO is being used (not BILL_ID) assert "BILL_NO" in params assert params["BILL_NO"] == "2200001" - return detail_response - return {} + return [{"MAIN_CNTS": "This is the summary.", "RSON_CONT": "This is the reason."}] + return [] mock_client.get_data = AsyncMock(side_effect=side_effect) @@ -173,28 +134,22 @@ async def side_effect(service_id_or_name, params, **kwargs): @pytest.mark.asyncio async def test_bill_model_captures_both_ids(bill_service, mock_client): """Test that Bill model captures both BILL_ID and BILL_NO separately.""" - mock_response = { - "O4K6HM0012064I15889": [ + mock_client.get_data = AsyncMock( + return_value=[ { - "row": [ - { - "BILL_ID": "PRC_ALPHA123BETA456", - "BILL_NO": "2123709", - "BILL_NAME": "Test Bill with Both IDs", - "PROPOSE_DT": "20240101", - "LINK_URL": "http://test.com", - } - ] + "BILL_ID": "PRC_ALPHA123BETA456", + "BILL_NO": "2123709", + "BILL_NAME": "Test Bill with Both IDs", + "PROPOSE_DT": "20240101", + "LINK_URL": "http://test.com", } ] - } - mock_client.get_data = AsyncMock(return_value=mock_response) + ) bills = await bill_service.get_bill_info(age="22", limit=1) assert len(bills) == 1 bill = bills[0] - # Verify both fields are captured separately assert bill.BILL_ID == "PRC_ALPHA123BETA456" assert bill.BILL_NO == "2123709" @@ -202,27 +157,21 @@ async def test_bill_model_captures_both_ids(bill_service, mock_client): @pytest.mark.asyncio async def test_bill_model_fallback_when_bill_id_missing(bill_service, mock_client): """Test that bill_id falls back to BILL_NO when BILL_ID is missing.""" - mock_response = { - "O4K6HM0012064I15889": [ + mock_client.get_data = AsyncMock( + return_value=[ { - "row": [ - { - "BILL_NO": "2123709", - "BILL_NAME": "Test Bill without BILL_ID", - "PROPOSE_DT": "20240101", - "LINK_URL": "http://test.com", - } - ] + "BILL_NO": "2123709", + "BILL_NAME": "Test Bill without BILL_ID", + "PROPOSE_DT": "20240101", + "LINK_URL": "http://test.com", } ] - } - mock_client.get_data = AsyncMock(return_value=mock_response) + ) bills = await bill_service.get_bill_info(age="22", limit=1) assert len(bills) == 1 bill = bills[0] - # bill_id should fallback to BILL_NO value assert bill.BILL_ID == "2123709" assert bill.BILL_NO == "2123709" @@ -230,39 +179,27 @@ async def test_bill_model_fallback_when_bill_id_missing(bill_service, mock_clien @pytest.mark.asyncio async def test_get_bill_details_uses_bill_no(bill_service, mock_client): """Test that get_bill_details uses BILL_NO parameter when calling detail API.""" - basic_response = { - "O4K6HM0012064I15889": [ - { - "row": [ - { - "BILL_ID": "PRC_TEST123", - "BILL_NO": "9999999", - "BILL_NAME": "Test Bill", - "PROPOSE_DT": "20240101", - "LINK_URL": "http://test.com", - } - ] - } - ] - } - - detail_response = {"OS46YD0012559515463": [{"row": [{"MAIN_CNTS": "Summary", "RSON_CONT": "Reason"}]}]} - call_params = {} async def side_effect(service_id_or_name, params, **kwargs): if service_id_or_name == bill_service.BILL_SEARCH_ID: - return basic_response + return [ + { + "BILL_ID": "PRC_TEST123", + "BILL_NO": "9999999", + "BILL_NAME": "Test Bill", + "PROPOSE_DT": "20240101", + "LINK_URL": "http://test.com", + } + ] if service_id_or_name == bill_service.BILL_DETAIL_ID: - # Capture the params to verify later call_params.update(params) - return detail_response - return {} + return [{"MAIN_CNTS": "Summary", "RSON_CONT": "Reason"}] + return [] mock_client.get_data = AsyncMock(side_effect=side_effect) await bill_service.get_bill_details("PRC_TEST123") - # Verify the detail API was called with BILL_NO, not BILL_ID assert "BILL_NO" in call_params assert call_params["BILL_NO"] == "9999999" diff --git a/tests/test_committee_service.py b/tests/test_committee_service.py index 35bafe8..42ad73a 100644 --- a/tests/test_committee_service.py +++ b/tests/test_committee_service.py @@ -19,33 +19,26 @@ def committee_service(mock_client): @pytest.mark.asyncio async def test_get_committee_list(committee_service, mock_client): - # Mock API response - mock_response = { - "O2Q4ZT001004PV11014": [ + mock_client.get_data = AsyncMock( + return_value=[ { - "head": [{"RESULT": {"CODE": "INFO-000", "MESSAGE": "Success"}}], - "row": [ - { - "HR_DEPT_CD": "9700008", - "COMMITTEE_NAME": "법제사법위원회", - "CMT_DIV_NM": "상임위원회", - "HG_NM": "박광온", - "CURR_CNT": "18", - "LIMIT_CNT": "18", - }, - { - "HR_DEPT_CD": "9700009", - "COMMITTEE_NAME": "정무위원회", - "CMT_DIV_NM": "상임위원회", - "HG_NM": "백혜련", - "CURR_CNT": "24", - "LIMIT_CNT": "24", - }, - ], - } + "HR_DEPT_CD": "9700008", + "COMMITTEE_NAME": "법제사법위원회", + "CMT_DIV_NM": "상임위원회", + "HG_NM": "박광온", + "CURR_CNT": "18", + "LIMIT_CNT": "18", + }, + { + "HR_DEPT_CD": "9700009", + "COMMITTEE_NAME": "정무위원회", + "CMT_DIV_NM": "상임위원회", + "HG_NM": "백혜련", + "CURR_CNT": "24", + "LIMIT_CNT": "24", + }, ] - } - mock_client.get_data = AsyncMock(return_value=mock_response) + ) committees = await committee_service.get_committee_list() @@ -56,7 +49,6 @@ async def test_get_committee_list(committee_service, mock_client): assert committees[0].HG_NM == "박광온" assert committees[0].CURR_CNT == 18 - # Verify API call parameters mock_client.get_data.assert_called_once() call_args = mock_client.get_data.call_args assert call_args.kwargs["service_id_or_name"] == "O2Q4ZT001004PV11014" @@ -65,7 +57,7 @@ async def test_get_committee_list(committee_service, mock_client): @pytest.mark.asyncio async def test_get_committee_list_filter(committee_service, mock_client): - mock_client.get_data = AsyncMock(return_value={}) + mock_client.get_data = AsyncMock(return_value=[]) await committee_service.get_committee_list(committee_name="법제사법위원회") @@ -76,26 +68,12 @@ async def test_get_committee_list_filter(committee_service, mock_client): @pytest.mark.asyncio async def test_get_committee_members_by_code(committee_service, mock_client): - mock_response = { - "OCAJQ4001000LI18751": [ - { - "head": [{"RESULT": {"CODE": "INFO-000", "MESSAGE": "Success"}}], - "row": [ - { - "COMMITTEE_NAME": "법제사법위원회", - "HR_DEPT_CD": "9700008", - "HG_NM": "홍길동", - }, - { - "COMMITTEE_NAME": "법제사법위원회", - "HR_DEPT_CD": "9700008", - "HG_NM": "임꺽정", - }, - ], - } + mock_client.get_data = AsyncMock( + return_value=[ + {"COMMITTEE_NAME": "법제사법위원회", "HR_DEPT_CD": "9700008", "HG_NM": "홍길동"}, + {"COMMITTEE_NAME": "법제사법위원회", "HR_DEPT_CD": "9700008", "HG_NM": "임꺽정"}, ] - } - mock_client.get_data = AsyncMock(return_value=mock_response) + ) rows = await committee_service.get_committee_members(committee_code="9700008", limit=10) @@ -108,16 +86,14 @@ async def test_get_committee_members_by_code(committee_service, mock_client): @pytest.mark.asyncio async def test_get_committee_members_filter_by_name(committee_service, mock_client): - mock_response = { - "row": [ + mock_client.get_data = AsyncMock( + return_value=[ {"COMMITTEE_NAME": "법제사법위원회", "HG_NM": "홍길동"}, {"COMMITTEE_NAME": "정무위원회", "HG_NM": "임꺽정"}, ] - } - mock_client.get_data = AsyncMock(return_value=mock_response) + ) rows = await committee_service.get_committee_members(committee_name="법제사법위원회", limit=5) - # Should keep only the matching committee rows after post-filter assert len(rows) == 1 assert rows[0]["HG_NM"] == "홍길동" diff --git a/tests/test_committee_service_enhancement.py b/tests/test_committee_service_enhancement.py index 59b5547..ce622fc 100644 --- a/tests/test_committee_service_enhancement.py +++ b/tests/test_committee_service_enhancement.py @@ -19,86 +19,35 @@ def committee_service(mock_client): @pytest.mark.asyncio async def test_get_committee_members_info_200_no_suggestion(committee_service, mock_client): - # Mock INFO-200 response (No Data) - - mock_response = { - "OCAJQ4001000LI18751": [ - { - "head": [ - {"status": "ok"}, - {"RESULT": {"CODE": "INFO-200", "MESSAGE": "해당하는 데이터가 없습니다."}}, - ] - }, - {"row": []}, - ] - } - - mock_client.get_data = AsyncMock(return_value=mock_response) - - # Mock get_committee_list to return empty list (no suggestions) - + """Empty result with no candidate committees → generic suggestion returned.""" + mock_client.get_data = AsyncMock(return_value=[]) committee_service.get_committee_list = AsyncMock(return_value=[]) result = await committee_service.get_committee_members(committee_name="없는위원회") assert isinstance(result, dict) - assert "error" in result - - assert result["error"]["api_code"] == "INFO-200" - - assert "유효한 코드(HR_DEPT_CD)를 가진 위원회가 없습니다" in result["error"]["suggestion"] + assert result["error"]["error_type"] == "DATA_NOT_FOUND" + assert "get_committee_list" in result["error"]["suggestion"] @pytest.mark.asyncio async def test_get_committee_members_info_200_with_suggestion(committee_service, mock_client): - # Mock INFO-200 response - - mock_response = { - "OCAJQ4001000LI18751": [ - { - "head": [ - {"status": "ok"}, - {"RESULT": {"CODE": "INFO-200", "MESSAGE": "해당하는 데이터가 없습니다."}}, - ] - }, - {"row": []}, - ] - } - - mock_client.get_data = AsyncMock(return_value=mock_response) - - # Mock get_committee_list to return candidates + """Empty result with candidate committees → suggestion includes committee codes.""" + mock_client.get_data = AsyncMock(return_value=[]) mock_candidates = [ - Committee( - HR_DEPT_CD="12345", - COMMITTEE_NAME="유사위원회A", - CMT_DIV_NM="상임", - HG_NM="A", - ), - Committee( - HR_DEPT_CD="67890", - COMMITTEE_NAME="유사위원회B", - CMT_DIV_NM="상임", - HG_NM="B", - ), + Committee(HR_DEPT_CD="12345", COMMITTEE_NAME="유사위원회A", CMT_DIV_NM="상임", HG_NM="A"), + Committee(HR_DEPT_CD="67890", COMMITTEE_NAME="유사위원회B", CMT_DIV_NM="상임", HG_NM="B"), ] - committee_service.get_committee_list = AsyncMock(return_value=mock_candidates) result = await committee_service.get_committee_members(committee_name="유사위원회") assert isinstance(result, dict) - assert "error" in result - - assert result["error"]["api_code"] == "INFO-200" - + assert result["error"]["error_type"] == "DATA_NOT_FOUND" suggestion = result["error"]["suggestion"] - assert "다음과 같은 관련 위원회가 있습니다" in suggestion - assert "유사위원회A(코드: 12345)" in suggestion - assert "유사위원회B(코드: 67890)" in suggestion diff --git a/tests/test_committee_service_fixes.py b/tests/test_committee_service_fixes.py index 4aa01d2..d4c38be 100644 --- a/tests/test_committee_service_fixes.py +++ b/tests/test_committee_service_fixes.py @@ -22,46 +22,24 @@ async def test_get_committee_members_filters_incorrect_codes(committee_service, Test that get_committee_members correctly filters out members from other committees when searched by committee_code, even if the API returns them. """ - target_code = "9700006" # Target: 법제사법위원회 - other_code = "9700005" # Noise: 국회운영위원회 - - # Simulate API returning mixed results (Bug reproduction scenario) - mock_response = { - "OCAJQ4001000LI18751": [ - { - "head": [{"RESULT": {"CODE": "INFO-000", "MESSAGE": "Success"}}], - "row": [ - { - "COMMITTEE_NAME": "법제사법위원회", - "HR_DEPT_CD": target_code, - "HG_NM": "Target Member 1", - }, - { - "COMMITTEE_NAME": "국회운영위원회", # Should be filtered out - "HR_DEPT_CD": other_code, - "HG_NM": "Noise Member 1", - }, - { - "COMMITTEE_NAME": "법제사법위원회", - "HR_DEPT_CD": target_code, - "HG_NM": "Target Member 2", - }, - ], - } + target_code = "9700006" + other_code = "9700005" + + mock_client.get_data = AsyncMock( + return_value=[ + {"COMMITTEE_NAME": "법제사법위원회", "HR_DEPT_CD": target_code, "HG_NM": "Target Member 1"}, + {"COMMITTEE_NAME": "국회운영위원회", "HR_DEPT_CD": other_code, "HG_NM": "Noise Member 1"}, + {"COMMITTEE_NAME": "법제사법위원회", "HR_DEPT_CD": target_code, "HG_NM": "Target Member 2"}, ] - } - mock_client.get_data = AsyncMock(return_value=mock_response) + ) - # Call the service rows = await committee_service.get_committee_members(committee_code=target_code, limit=100) - # Verification assert len(rows) == 2, f"Expected 2 members, got {len(rows)}" for row in rows: assert row["HR_DEPT_CD"] == target_code assert row["COMMITTEE_NAME"] == "법제사법위원회" - # Ensure invalid member is NOT in the list names = [r["HG_NM"] for r in rows] assert "Noise Member 1" not in names @@ -74,21 +52,12 @@ async def test_get_committee_members_filters_incorrect_codes_with_dept_cd_key(co target_code = "9700006" other_code = "9700005" - mock_response = { - "row": [ - { - "COMMITTEE_NAME": "법제사법위원회", - "DEPT_CD": target_code, # Key variation - "HG_NM": "Target Member", - }, - { - "COMMITTEE_NAME": "국회운영위원회", - "DEPT_CD": other_code, - "HG_NM": "Noise Member", - }, + mock_client.get_data = AsyncMock( + return_value=[ + {"COMMITTEE_NAME": "법제사법위원회", "DEPT_CD": target_code, "HG_NM": "Target Member"}, + {"COMMITTEE_NAME": "국회운영위원회", "DEPT_CD": other_code, "HG_NM": "Noise Member"}, ] - } - mock_client.get_data = AsyncMock(return_value=mock_response) + ) rows = await committee_service.get_committee_members(committee_code=target_code) @@ -105,14 +74,12 @@ async def test_get_committee_members_empty_result_handling(committee_service, mo target_code = "9700006" other_code = "9700005" - # API returns only wrong data - mock_response = { - "row": [ + mock_client.get_data = AsyncMock( + return_value=[ {"DEPT_CD": other_code, "HG_NM": "Noise Member 1"}, {"DEPT_CD": other_code, "HG_NM": "Noise Member 2"}, ] - } - mock_client.get_data = AsyncMock(return_value=mock_response) + ) rows = await committee_service.get_committee_members(committee_code=target_code) @@ -124,24 +91,18 @@ async def test_get_committee_members_empty_result_handling(committee_service, mo async def test_get_committee_members_invalid_korean_name(committee_service, mock_client): """ Test behavior when an invalid Korean committee name is provided (e.g., typo). - Scenario: API returns some data (ignoring the bad name param) but client-side filtering - should remove everything because names don't match. """ - typo_name = "법제사법위훤회" # Typo - - # API ignores the name and returns data for "법제사법위원회" (Best case assumption for API behavior) - # OR API returns everything. In either case, filtering should clear it. - mock_response = { - "row": [ - {"COMMITTEE_NAME": "법제사법위원회", "HG_NM": "Member 1"}, # Mismatch - {"COMMITTEE_NAME": "국회운영위원회", "HG_NM": "Member 2"}, # Mismatch + typo_name = "법제사법위훤회" + + mock_client.get_data = AsyncMock( + return_value=[ + {"COMMITTEE_NAME": "법제사법위원회", "HG_NM": "Member 1"}, + {"COMMITTEE_NAME": "국회운영위원회", "HG_NM": "Member 2"}, ] - } - mock_client.get_data = AsyncMock(return_value=mock_response) + ) rows = await committee_service.get_committee_members(committee_name=typo_name) - # Since "법제사법위훤회" is not in any "COMMITTEE_NAME", result should be empty assert len(rows) == 0 assert isinstance(rows, list) @@ -149,23 +110,10 @@ async def test_get_committee_members_invalid_korean_name(committee_service, mock @pytest.mark.asyncio async def test_get_committee_members_api_no_data(committee_service, mock_client): """ - Test behavior when API explicitly returns INFO-200 (No Data). + Test behavior when API returns no data for a committee name search. The service should return a structured error dictionary with suggestions. """ - mock_response = { - "OCAJQ4001000LI18751": [ - { - "head": [ - {"list_total_count": 0}, - {"RESULT": {"CODE": "INFO-200", "MESSAGE": "해당하는 데이터가 없습니다."}}, - ] - } - ] - } - mock_client.get_data = AsyncMock(return_value=mock_response) - - # Mocking list_api_services for suggestion generation inside error handling - # The service calls self.get_committee_list recursively for suggestions + mock_client.get_data = AsyncMock(return_value=[]) committee_service.get_committee_list = AsyncMock(return_value=[]) result = await committee_service.get_committee_members(committee_name="없는위원회") diff --git a/tests/test_enhanced_services.py b/tests/test_enhanced_services.py index 554bb0b..3a330ab 100644 --- a/tests/test_enhanced_services.py +++ b/tests/test_enhanced_services.py @@ -40,25 +40,17 @@ def smart_service(bill_service, meeting_service, member_service): @pytest.mark.asyncio async def test_search_bills_enhanced(bill_service, mock_client): - # Mock response for bill search - mock_client.get_data.return_value = { - "O4K6HM0012064I15889": [ - {"head": [{"list_total_count": 1}, {"RESULT": {"CODE": "INFO-000", "MESSAGE": "정상"}}]}, - { - "row": [ - { - "BILL_ID": "PRC_123", - "BILL_NAME": "테스트 법안", - "PROPOSER": "김철수", - "PROPOSER_KIND": "의원", - "PROC_STATUS": "접수", - "CURR_COMMITTEE": "법사위", - "PROPOSE_DT": "20240101", - } - ] - }, - ] - } + mock_client.get_data.return_value = [ + { + "BILL_ID": "PRC_123", + "BILL_NAME": "테스트 법안", + "PROPOSER": "김철수", + "PROPOSER_KIND": "의원", + "PROC_STATUS": "접수", + "CURR_COMMITTEE": "법사위", + "PROPOSE_DT": "20240101", + } + ] bills = await bill_service.get_bill_info(age="22", bill_name="테스트") assert len(bills) == 1 @@ -68,65 +60,35 @@ async def test_search_bills_enhanced(bill_service, mock_client): @pytest.mark.asyncio async def test_smart_service_analyze(smart_service, mock_client): - # Mock for bill search mock_client.get_data.side_effect = [ # search_bills - { - "O4K6HM0012064I15889": [ - {"head": []}, - { - "row": [ - { - "BILL_ID": "PRC_1", - "BILL_NAME": "AI 법안", - "PROPOSER": "김철수", - "PROPOSER_KIND": "의원", - "PROC_STATUS": "접수", - "CURR_COMMITTEE": "과방위", - } - ] - }, - ] - }, - # get_bill_details (basic info Probe) - { - "O4K6HM0012064I15889": [ - {"head": []}, - { - "row": [ - { - "BILL_ID": "PRC_1", - "BILL_NAME": "AI 법안", - "PROPOSER": "김철수", - "PROPOSER_KIND": "의원", - "PROC_STATUS": "접수", - "CURR_COMMITTEE": "과방위", - } - ] - }, - ] - }, + [ + { + "BILL_ID": "PRC_1", + "BILL_NAME": "AI 법안", + "PROPOSER": "김철수", + "PROPOSER_KIND": "의원", + "PROC_STATUS": "접수", + "CURR_COMMITTEE": "과방위", + } + ], + # get_bill_details (basic info probe) + [ + { + "BILL_ID": "PRC_1", + "BILL_NAME": "AI 법안", + "PROPOSER": "김철수", + "PROPOSER_KIND": "의원", + "PROC_STATUS": "접수", + "CURR_COMMITTEE": "과방위", + } + ], # get_bill_details (detail) - { - "OS46YD0012559515463": [ - {"head": []}, - {"row": [{"MAIN_CNTS": "AI 진흥 내용", "RSON_CONT": "필요성"}]}, - ] - }, + [{"MAIN_CNTS": "AI 진흥 내용", "RSON_CONT": "필요성"}], # get_meeting_records - { - "OOWY4R001216HX11492": [ - {"head": []}, - {"row": [{"CONF_TITLE": "1차 회의"}]}, - ] - }, + [{"CONF_TITLE": "1차 회의"}], # get_member_info - { - "OWSSC6001134T516707": [ - {"head": []}, - {"row": [{"HG_NM": "김철수", "POLY_NM": "국민의힘"}]}, - ] - }, + [{"HG_NM": "김철수", "POLY_NM": "국민의힘"}], ] report = await smart_service.analyze_legislative_issue("AI") diff --git a/tests/test_fixes.py b/tests/test_fixes.py index 0731c18..cc0d36c 100644 --- a/tests/test_fixes.py +++ b/tests/test_fixes.py @@ -87,13 +87,7 @@ async def test_get_bill_details_numeric_id_bypass(mock_client): bill_service.get_bill_info = AsyncMock(return_value=[]) # Mock get_data for detail call - detail_response = { - "OS46YD0012559515463": [ - {"head": []}, - {"row": [{"MAIN_CNTS": "Summary", "RSON_CONT": "Reason"}]}, - ] - } - mock_client.get_data = AsyncMock(return_value=detail_response) + mock_client.get_data = AsyncMock(return_value=[{"MAIN_CNTS": "Summary", "RSON_CONT": "Reason"}]) numeric_id = "2214308" detail = await bill_service.get_bill_details(numeric_id) diff --git a/tests/test_meeting_service.py b/tests/test_meeting_service.py index 788049d..1012f5a 100644 --- a/tests/test_meeting_service.py +++ b/tests/test_meeting_service.py @@ -18,29 +18,22 @@ def meeting_service(mock_client): @pytest.mark.asyncio async def test_search_meetings_by_committee(meeting_service, mock_client): - # Mock API response - mock_response = { - "OR137O001023MZ19321": [ + mock_client.get_data = AsyncMock( + return_value=[ { - "head": [{"RESULT": {"CODE": "INFO-000", "MESSAGE": "Success"}}], - "row": [ - { - "MEETING_DATE": "2024-11-20", - "CONF_DATE": "20241120", - "COMM_NAME": "법제사법위원회", - "TITLE": "제410회국회(정기회) 제10차 법제사법위원회", - }, - { - "MEETING_DATE": "2024-11-15", - "CONF_DATE": "20241115", - "COMM_NAME": "법제사법위원회", - "TITLE": "제410회국회(정기회) 제9차 법제사법위원회", - }, - ], - } + "MEETING_DATE": "2024-11-20", + "CONF_DATE": "20241120", + "COMM_NAME": "법제사법위원회", + "TITLE": "제410회국회(정기회) 제10차 법제사법위원회", + }, + { + "MEETING_DATE": "2024-11-15", + "CONF_DATE": "20241115", + "COMM_NAME": "법제사법위원회", + "TITLE": "제410회국회(정기회) 제9차 법제사법위원회", + }, ] - } - mock_client.get_data = AsyncMock(return_value=mock_response) + ) meetings = await meeting_service.search_meetings(committee_name="법제사법위원회") @@ -48,7 +41,6 @@ async def test_search_meetings_by_committee(meeting_service, mock_client): assert meetings[0]["COMM_NAME"] == "법제사법위원회" assert meetings[0]["CONF_DATE"] == "20241120" - # Verify API call mock_client.get_data.assert_called_once() call_args = mock_client.get_data.call_args assert call_args.kwargs["service_id_or_name"] == "O27DU0000960M511942" @@ -57,7 +49,7 @@ async def test_search_meetings_by_committee(meeting_service, mock_client): @pytest.mark.asyncio async def test_search_meetings_pagination(meeting_service, mock_client): - mock_client.get_data = AsyncMock(return_value={"row": []}) + mock_client.get_data = AsyncMock(return_value=[]) await meeting_service.search_meetings(page=3) @@ -67,38 +59,27 @@ async def test_search_meetings_pagination(meeting_service, mock_client): @pytest.mark.asyncio async def test_search_meetings_by_date_range(meeting_service, mock_client): - # Mock API response with dates outside range - mock_response = { - "OR137O001023MZ19321": [ + mock_client.get_data = AsyncMock( + return_value=[ + { + "MEETING_DATE": "2024-11-20", + "CONF_DATE": "20241120", + "COMM_NAME": "법제사법위원회", + }, { - "head": [{"RESULT": {"CODE": "INFO-000", "MESSAGE": "Success"}}], - "row": [ - { - "MEETING_DATE": "2024-11-20", - "CONF_DATE": "20241120", - "COMM_NAME": "법제사법위원회", - }, - { - "MEETING_DATE": "2024-11-10", - "CONF_DATE": "20241110", # Should be filtered out - "COMM_NAME": "법제사법위원회", - }, - ], - } + "MEETING_DATE": "2024-11-10", + "CONF_DATE": "20241110", # Should be filtered out + "COMM_NAME": "법제사법위원회", + }, ] - } - mock_client.get_data = AsyncMock(return_value=mock_response) + ) - # Search for meetings after 2024-11-15 meetings = await meeting_service.search_meetings(date_start="2024-11-15") assert len(meetings) == 1 assert meetings[0]["CONF_DATE"] == "20241120" - # Verify API call mock_client.get_data.assert_called_once() call_args = mock_client.get_data.call_args - # Service ID should be the Schedule API assert call_args.kwargs["service_id_or_name"] == "O27DU0000960M511942" - # Date filtering is done in memory, so no date params sent to API assert "CONF_DATE" not in call_args.kwargs["params"] diff --git a/tests/test_meeting_service_fixes.py b/tests/test_meeting_service_fixes.py index 20c71fc..893b506 100644 --- a/tests/test_meeting_service_fixes.py +++ b/tests/test_meeting_service_fixes.py @@ -22,49 +22,37 @@ async def test_search_meetings_uses_schedule_api(meeting_service, mock_client): """ Test that search_meetings uses the Schedule API and filters by date correctly. """ - # Mock Schedule API response - mock_response = { - "nttmdfdcaakvibdar": [ + mock_client.get_data = AsyncMock( + return_value=[ { - "head": [{"RESULT": {"CODE": "INFO-000", "MESSAGE": "Success"}}], - "row": [ - { - "MEETING_DATE": "2025-12-29", - "TITLE": "전체회의", - "COMMITTEE_NAME": "법제사법위원회", - "UNIT_CD": "100022", - }, - { - "MEETING_DATE": "2025-12-01", # Out of range - "TITLE": "전체회의", - "COMMITTEE_NAME": "법제사법위원회", - "UNIT_CD": "100022", - }, - ], - } + "MEETING_DATE": "2025-12-29", + "TITLE": "전체회의", + "COMMITTEE_NAME": "법제사법위원회", + "UNIT_CD": "100022", + }, + { + "MEETING_DATE": "2025-12-01", # Out of range + "TITLE": "전체회의", + "COMMITTEE_NAME": "법제사법위원회", + "UNIT_CD": "100022", + }, ] - } - mock_client.get_data = AsyncMock(return_value=mock_response) + ) - # Configure settings default age settings.default_assembly_age = "22" - # Search range: 2025-12-20 to 2025-12-30 results = await meeting_service.search_meetings( committee_name="법제사법위원회", date_start="2025-12-20", date_end="2025-12-30" ) - # 1. Verify API Call mock_client.get_data.assert_called_once() call_args = mock_client.get_data.call_args assert call_args.kwargs["service_id_or_name"] == "O27DU0000960M511942" assert call_args.kwargs["params"]["COMMITTEE_NAME"] == "법제사법위원회" - assert call_args.kwargs["params"]["UNIT_CD"] == "100022" # Converted + assert call_args.kwargs["params"]["UNIT_CD"] == "100022" - # 2. Verify Filtering assert len(results) == 1 assert results[0]["MEETING_DATE"] == "2025-12-29" - # Verify field remapping assert results[0]["CONF_DATE"] == "20251229" assert results[0]["CONF_TITLE"] == "전체회의" @@ -75,6 +63,5 @@ async def test_unit_cd_conversion(meeting_service): assert meeting_service._convert_unit_cd("22") == "100022" assert meeting_service._convert_unit_cd(22) == "100022" assert meeting_service._convert_unit_cd("21") == "100021" - # If already converted or unknown format assert meeting_service._convert_unit_cd("100022") == "100022" assert meeting_service._convert_unit_cd("Cheolsu") == "Cheolsu" diff --git a/tests/test_member_service.py b/tests/test_member_service.py index 7728a1c..e359221 100644 --- a/tests/test_member_service.py +++ b/tests/test_member_service.py @@ -9,13 +9,10 @@ async def test_member_info_filtering(): """의원 성명 검색 시 공백 제거 및 필터링 로직 테스트""" mock_client = MagicMock() - # 실제 API 구조 시뮬레이션: {"서비스ID": [{"head": [...]}, {"row": [...]}]} - mock_data = { - "OWSSC6001134T516707": [ - {"head": [{"list_total_count": 2}, {"RESULT": {"CODE": "INFO-000", "MESSAGE": "정상"}}]}, - {"row": [{"HG_NM": "홍 길 동", "POLY_NM": "A당"}, {"HG_NM": "김철수", "POLY_NM": "B당"}]}, - ] - } + mock_data = [ + {"HG_NM": "홍 길 동", "POLY_NM": "A당"}, + {"HG_NM": "김철수", "POLY_NM": "B당"}, + ] with patch("assemblymcp.services._get_data_with_retry", new_callable=AsyncMock) as mock_retry: mock_retry.return_value = mock_data @@ -35,13 +32,9 @@ async def test_member_info_filtering(): async def test_member_committee_careers_parsing(): """의원 경력 데이터 파싱 테스트""" mock_client = MagicMock() - # ORNDP7000993P115502 - mock_data = { - "ORNDP7000993P115502": [ - {"head": [{"list_total_count": 1}]}, - {"row": [{"HG_NM": "홍길동", "PROFILE_SJ": "법제사법위원회 위원", "FRTO_DATE": "2024.06.10 ~"}]}, - ] - } + mock_data = [ + {"HG_NM": "홍길동", "PROFILE_SJ": "법제사법위원회 위원", "FRTO_DATE": "2024.06.10 ~"}, + ] with patch("assemblymcp.services._get_data_with_retry", new_callable=AsyncMock) as mock_retry: mock_retry.return_value = mock_data diff --git a/tests/test_server_discovery.py b/tests/test_server_discovery.py index 27775b2..5c7e071 100644 --- a/tests/test_server_discovery.py +++ b/tests/test_server_discovery.py @@ -55,9 +55,9 @@ async def test_list_services_filter_case_insensitive(discovery_service): @pytest.mark.asyncio async def test_call_raw_success(discovery_service, mock_client): - mock_client.get_data = AsyncMock(return_value={"result": "success"}) + mock_client.get_data = AsyncMock(return_value=[{"result": "success"}]) result = await discovery_service.call_raw("TEST_ID_1", params={"pSize": 5}) mock_client.get_data.assert_called_once_with(service_id_or_name="TEST_ID_1", params={"pSize": 5}) - assert result == {"result": "success"} + assert result == [{"result": "success"}] diff --git a/tests/test_smart_service_new.py b/tests/test_smart_service_new.py index 41f89e3..f7b7ec7 100644 --- a/tests/test_smart_service_new.py +++ b/tests/test_smart_service_new.py @@ -23,21 +23,11 @@ def smart_service(mock_client): @pytest.mark.asyncio async def test_get_legislative_reports(smart_service, mock_client): - # Mock for NABO Focus (OB5IBW001180FQ10640) - # Mock for News (O5MSQF0009823A15643) mock_client.get_data.side_effect = [ # NABO Focus - { - "OB5IBW001180FQ10640": [ - {"row": [{"SUBJECT": "보고서1", "REG_DATE": "2024-01-01", "LINK_URL": "http://nabo.go.kr/1"}]} - ] - }, + [{"SUBJECT": "보고서1", "REG_DATE": "2024-01-01", "LINK_URL": "http://nabo.go.kr/1"}], # News - { - "O5MSQF0009823A15643": [ - {"row": [{"V_TITLE": "뉴스1", "DATE_RELEASED": "2024-01-02", "URL_LINK": "http://news.go.kr/1"}]} - ] - }, + [{"V_TITLE": "뉴스1", "DATE_RELEASED": "2024-01-02", "URL_LINK": "http://news.go.kr/1"}], ] reports = await smart_service.get_legislative_reports("AI") @@ -51,27 +41,21 @@ async def test_get_legislative_reports(smart_service, mock_client): async def test_get_committee_work_summary(smart_service, mock_client): mock_client.get_data.side_effect = [ # Recent Bills - { - "O4K6HM0012064I15889": [ - { - "row": [ - { - "BILL_ID": "PRC_1", - "BILL_NAME": "법사위 법안", - "CURR_COMMITTEE": "법제사법위원회", - "PROPOSER": "의원1", - "PROPOSER_KIND": "의원", - "PROC_STATUS": "접수", - "LINK_URL": "x", - } - ] - } - ] - }, + [ + { + "BILL_ID": "PRC_1", + "BILL_NAME": "법사위 법안", + "CURR_COMMITTEE": "법제사법위원회", + "PROPOSER": "의원1", + "PROPOSER_KIND": "의원", + "PROC_STATUS": "접수", + "LINK_URL": "x", + } + ], # Reports (NABO) - {}, + [], # News - {}, + [], ] summary = await smart_service.get_committee_work_summary("법사위") @@ -83,29 +67,19 @@ async def test_get_committee_work_summary(smart_service, mock_client): @pytest.mark.asyncio async def test_analyze_committee_performance(smart_service, mock_client): mock_client.get_data.side_effect = [ - {}, # 22nd bills - { - "O4K6HM0012064I15889": [ - { - "row": [ - { - "BILL_ID": "PRC_21_1", - "BILL_NAME": "가결법안", - "CURR_COMMITTEE": "법제사법위원회", - "PROC_STATUS": "원안가결", - "LINK_URL": "x", - } - ] - } - ] - }, # 21st bills - { - "OND1KZ0009677M13515": [ - {"row": [{"BILL_ID": "PRC_21_1", "VOTE_TCNT": 100, "YES_TCNT": 90, "PROC_RESULT_CD": "가결"}]} - ] - }, # Voting summary - {}, # Reports - {}, # News + [], # 22nd bills (empty) + [ + { + "BILL_ID": "PRC_21_1", + "BILL_NAME": "가결법안", + "CURR_COMMITTEE": "법제사법위원회", + "PROC_STATUS": "원안가결", + "LINK_URL": "x", + } + ], # 21st bills + [{"BILL_ID": "PRC_21_1", "VOTE_TCNT": 100, "YES_TCNT": 90, "PROC_RESULT_CD": "가결"}], # Voting summary + [], # Reports + [], # News ] report = await smart_service.get_committee_voting_stats("법사위") diff --git a/tests/test_voting_service.py b/tests/test_voting_service.py index 975a35c..01fef23 100644 --- a/tests/test_voting_service.py +++ b/tests/test_voting_service.py @@ -38,24 +38,17 @@ def smart_service(bill_service, meeting_service, member_service): @pytest.mark.asyncio async def test_get_bill_voting_summary(bill_service, mock_client): - mock_client.get_data.return_value = { - "OND1KZ0009677M13515": [ - {"head": []}, - { - "row": [ - { - "BILL_ID": "PRC_V1", - "BILL_NAME": "테스트 법안", - "YES_TCNT": "150", - "NO_TCNT": "50", - "BLANK_TCNT": "10", - "VOTE_TCNT": "210", - "PROC_RESULT_CD": "가결", - } - ] - }, - ] - } + mock_client.get_data.return_value = [ + { + "BILL_ID": "PRC_V1", + "BILL_NAME": "테스트 법안", + "YES_TCNT": "150", + "NO_TCNT": "50", + "BLANK_TCNT": "10", + "VOTE_TCNT": "210", + "PROC_RESULT_CD": "가결", + } + ] summary = await bill_service.get_bill_voting_summary("PRC_V1") @@ -66,22 +59,15 @@ async def test_get_bill_voting_summary(bill_service, mock_client): @pytest.mark.asyncio async def test_get_member_voting_history(bill_service, mock_client): - mock_client.get_data.return_value = { - "OPR1MQ000998LC12535": [ - {"head": []}, - { - "row": [ - { - "BILL_ID": "PRC_V1", - "BILL_NAME": "테스트 법안", - "HG_NM": "홍길동", - "RESULT_VOTE_MOD": "찬성", - "POLY_NM": "가나다당", - } - ] - }, - ] - } + mock_client.get_data.return_value = [ + { + "BILL_ID": "PRC_V1", + "BILL_NAME": "테스트 법안", + "HG_NM": "홍길동", + "RESULT_VOTE_MOD": "찬성", + "POLY_NM": "가나다당", + } + ] records = await bill_service.get_member_voting_history(name="홍길동") @@ -92,39 +78,23 @@ async def test_get_member_voting_history(bill_service, mock_client): @pytest.mark.asyncio async def test_get_representative_report(smart_service, mock_client): - # Mock multiple calls inside get_representative_report - # 1. get_member_info - # 2. get_bill_info - # 3. get_member_committee_careers - # 4. get_member_voting_history - mock_client.get_data.side_effect = [ - # get_member_info (OWSSC6001134T516707) - {"OWSSC6001134T516707": [{"row": [{"HG_NM": "홍길동", "POLY_NM": "가나다당"}]}]}, - # get_bill_info (O4K6HM0012064I15889) - { - "O4K6HM0012064I15889": [ - { - "row": [ - { - "BILL_ID": "B1", - "BILL_NAME": "법안1", - "PROPOSER": "홍길동", - "PROPOSER_KIND": "의원", - "PROC_STATUS": "접수", - } - ] - } - ] - }, - # get_member_committee_careers (ORNDP7000993P115502) - {"ORNDP7000993P115502": [{"row": [{"HG_NM": "홍길동", "PROFILE_SJ": "법사위 위원"}]}]}, - # get_member_voting_history (OPR1MQ000998LC12535) - { - "OPR1MQ000998LC12535": [ - {"row": [{"BILL_ID": "V1", "BILL_NAME": "법안1", "RESULT_VOTE_MOD": "찬성", "HG_NM": "홍길동"}]} - ] - }, + # get_member_info + [{"HG_NM": "홍길동", "POLY_NM": "가나다당"}], + # get_bill_info + [ + { + "BILL_ID": "B1", + "BILL_NAME": "법안1", + "PROPOSER": "홍길동", + "PROPOSER_KIND": "의원", + "PROC_STATUS": "접수", + } + ], + # get_member_committee_careers + [{"HG_NM": "홍길동", "PROFILE_SJ": "법사위 위원"}], + # get_member_voting_history + [{"BILL_ID": "V1", "BILL_NAME": "법안1", "RESULT_VOTE_MOD": "찬성", "HG_NM": "홍길동"}], ] report = await smart_service.get_representative_report("홍길동") @@ -139,30 +109,24 @@ async def test_get_representative_report(smart_service, mock_client): async def test_get_bill_voting_results(smart_service, mock_client): mock_client.get_data.side_effect = [ # get_bill_voting_summary - {"OND1KZ0009677M13515": [{"row": [{"BILL_ID": "V1", "BILL_NAME": "법안1", "YES_TCNT": "100"}]}]}, + [{"BILL_ID": "V1", "BILL_NAME": "법안1", "YES_TCNT": "100"}], # get_member_voting_history (sample) - { - "OPR1MQ000998LC12535": [ - { - "row": [ - { - "BILL_ID": "V1", - "BILL_NAME": "법안1", - "RESULT_VOTE_MOD": "찬성", - "POLY_NM": "A당", - "HG_NM": "의원1", - }, - { - "BILL_ID": "V1", - "BILL_NAME": "법안1", - "RESULT_VOTE_MOD": "반대", - "POLY_NM": "B당", - "HG_NM": "의원2", - }, - ] - } - ] - }, + [ + { + "BILL_ID": "V1", + "BILL_NAME": "법안1", + "RESULT_VOTE_MOD": "찬성", + "POLY_NM": "A당", + "HG_NM": "의원1", + }, + { + "BILL_ID": "V1", + "BILL_NAME": "법안1", + "RESULT_VOTE_MOD": "반대", + "POLY_NM": "B당", + "HG_NM": "의원2", + }, + ], ] results = await smart_service.get_bill_voting_results("V1") diff --git a/uv.lock b/uv.lock index 0a7c0f8..aae4a14 100644 --- a/uv.lock +++ b/uv.lock @@ -27,7 +27,7 @@ wheels = [ [[package]] name = "assembly-api-client" -version = "1.1.0" +version = "1.2.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -39,14 +39,14 @@ dependencies = [ { name = "tenacity" }, { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/b4/6380d7eed75e4b90154694ac5413416b3ce2177db2c88879c116a79282f5/assembly_api_client-1.1.0.tar.gz", hash = "sha256:a5381daccb3e3a11274f7326fba775321e82885ff9e89401678a46f6ebe09e14", size = 187458 } +sdist = { url = "https://files.pythonhosted.org/packages/39/da/fe50852c4aa556d28e7a0730d184333fad56c769998d6a9fd2ed5fcf3419/assembly_api_client-1.2.6.tar.gz", hash = "sha256:3c40c13e89434488f372f08aed86742028b32770e497ef74234f4f358e56d6fa", size = 196089 } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/0a/0ca3839647db0031b255dd4f1868a65583d54eb58c7de962dba82ac20644/assembly_api_client-1.1.0-py3-none-any.whl", hash = "sha256:b030189eb8db6cb3ec8e5272b3aba7a0f377542819da9cff132bdd22628b1ee9", size = 63795 }, + { url = "https://files.pythonhosted.org/packages/91/3e/72106d792d38f2939814d592043cd7370a072ff33e74d4a9eb0f6961aee3/assembly_api_client-1.2.6-py3-none-any.whl", hash = "sha256:6061bab5c9e7d04f8a3187446624e8458492a9c5ecbf16870016ba22b4f9a85b", size = 64582 }, ] [[package]] name = "assemblymcp" -version = "0.6.0" +version = "0.6.2" source = { editable = "." } dependencies = [ { name = "assembly-api-client" }, @@ -74,7 +74,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "assembly-api-client", specifier = ">=1.1.0" }, + { name = "assembly-api-client", specifier = ">=1.2.6" }, { name = "beautifulsoup4", specifier = ">=4.14.2" }, { name = "fastmcp", specifier = ">=2.13.1" }, { name = "httpx", specifier = ">=0.28.1" },