test(srt): split seat helper regression coverage

This commit is contained in:
Jeffrey (Dongkyu) Kim 2026-06-06 12:14:34 +09:00
commit 82f11a12bc
5 changed files with 214 additions and 191 deletions

View file

@ -11,9 +11,9 @@
"build": "npm run build --workspaces --if-present",
"build:manus-bundle": "node scripts/build-manus-bundle.js",
"generate:plugin-manifest": "node scripts/generate-plugin-manifest.js",
"lint": "node --check scripts/skill-docs.test.js scripts/korean_character_count.js scripts/test_korean_character_count.js scripts/korean_middle_korean.js scripts/test_korean_middle_korean.js scripts/build-manus-bundle.js scripts/test_build_manus_bundle.js scripts/workflow-actions.test.js scripts/generate-plugin-manifest.js scripts/test_generate_plugin_manifest.js && python3 -m py_compile scripts/k_skill_cleaner.py scripts/test_k_skill_cleaner.py corporate-registration-consulting/scripts/fill_official_hwp.py k-skill-cleaner/scripts/k_skill_cleaner.py scripts/fine_dust.py scripts/test_fine_dust.py scripts/ktx_booking.py scripts/test_ktx_booking.py scripts/srt_booking.py scripts/srt_seats.py scripts/test_srt_booking.py scripts/sillok_search.py scripts/test_sillok_search.py scripts/korean_spell_check.py scripts/test_korean_spell_check.py scripts/patent_search.py scripts/test_patent_search.py scripts/mfds_drug_safety.py scripts/test_mfds_drug_safety.py scripts/nts_business_registration.py scripts/test_nts_business_registration.py scripts/mfds_food_safety.py scripts/test_mfds_food_safety.py scripts/zipcode_search.py scripts/test_zipcode_search.py scripts/subway_lost_property.py scripts/test_subway_lost_property.py scripts/geeknews_search.py scripts/test_geeknews_search.py nts-business-registration/scripts/nts_business_registration.py scripts/test_naver_blog_search.py scripts/test_korean_slang_writing.py scripts/kakaotalk_mac.py scripts/test_kakaotalk_mac.py scripts/test_coupang_partners_mcp_wrapper.py scripts/test_ohou_today_deal.py scripts/ticket_availability.py scripts/test_ticket_availability.py scripts/test_danawa_price_search.py ticket-availability/scripts/ticket_availability.py coupang-product-search/scripts/coupang_partners_mcp.py ohou-today-deal/scripts/ohou_today_deal.py kakaotalk-mac/scripts/kakaotalk_mac.py naver-blog-research/scripts/_naver_http.py naver-blog-research/scripts/naver_search.py naver-blog-research/scripts/naver_read.py naver-blog-research/scripts/naver_download_images.py korean-slang-writing/scripts/_slang_http.py korean-slang-writing/scripts/slang_search.py korean-slang-writing/scripts/slang_lookup.py korean-scholarship-search/scripts/scholarship_filter.py korean-scholarship-search/scripts/test_scholarship_filter.py korean-scholarship-search/scripts/university_search_plan.py seoul-bike/scripts/seoul_bike.py scripts/test_seoul_bike.py danawa-price-search/scripts/danawa_search.py kosis-stats/scripts/run_kosis_stats.py kosis-stats/tests/test_run_kosis_stats.py kstartup-search/scripts/run_kstartup.py kstartup-search/tests/test_run_kstartup.py intercity-bus-booking/scripts/intercity_bus_search.py daangn-used-goods-search/scripts/daangn_used_goods.py daangn-realty-search/scripts/daangn_realty.py daangn-jobs-search/scripts/daangn_jobs.py daangn-cars-search/scripts/daangn_cars.py foresttrip-vacancy/scripts/run_foresttrip_vacancy.py foresttrip-vacancy/tests/test_run_foresttrip_vacancy.py && npm run lint --workspaces --if-present && ./scripts/validate-skills.sh && node scripts/generate-plugin-manifest.js --check",
"lint": "node --check scripts/skill-docs.test.js scripts/korean_character_count.js scripts/test_korean_character_count.js scripts/korean_middle_korean.js scripts/test_korean_middle_korean.js scripts/build-manus-bundle.js scripts/test_build_manus_bundle.js scripts/workflow-actions.test.js scripts/generate-plugin-manifest.js scripts/test_generate_plugin_manifest.js && python3 -m py_compile scripts/k_skill_cleaner.py scripts/test_k_skill_cleaner.py corporate-registration-consulting/scripts/fill_official_hwp.py k-skill-cleaner/scripts/k_skill_cleaner.py scripts/fine_dust.py scripts/test_fine_dust.py scripts/ktx_booking.py scripts/test_ktx_booking.py scripts/srt_booking.py scripts/srt_seats.py scripts/srt_booking_test_support.py scripts/test_srt_booking.py scripts/test_srt_seats.py scripts/sillok_search.py scripts/test_sillok_search.py scripts/korean_spell_check.py scripts/test_korean_spell_check.py scripts/patent_search.py scripts/test_patent_search.py scripts/mfds_drug_safety.py scripts/test_mfds_drug_safety.py scripts/nts_business_registration.py scripts/test_nts_business_registration.py scripts/mfds_food_safety.py scripts/test_mfds_food_safety.py scripts/zipcode_search.py scripts/test_zipcode_search.py scripts/subway_lost_property.py scripts/test_subway_lost_property.py scripts/geeknews_search.py scripts/test_geeknews_search.py nts-business-registration/scripts/nts_business_registration.py scripts/test_naver_blog_search.py scripts/test_korean_slang_writing.py scripts/kakaotalk_mac.py scripts/test_kakaotalk_mac.py scripts/test_coupang_partners_mcp_wrapper.py scripts/test_ohou_today_deal.py scripts/ticket_availability.py scripts/test_ticket_availability.py scripts/test_danawa_price_search.py ticket-availability/scripts/ticket_availability.py coupang-product-search/scripts/coupang_partners_mcp.py ohou-today-deal/scripts/ohou_today_deal.py kakaotalk-mac/scripts/kakaotalk_mac.py naver-blog-research/scripts/_naver_http.py naver-blog-research/scripts/naver_search.py naver-blog-research/scripts/naver_read.py naver-blog-research/scripts/naver_download_images.py korean-slang-writing/scripts/_slang_http.py korean-slang-writing/scripts/slang_search.py korean-slang-writing/scripts/slang_lookup.py korean-scholarship-search/scripts/scholarship_filter.py korean-scholarship-search/scripts/test_scholarship_filter.py korean-scholarship-search/scripts/university_search_plan.py seoul-bike/scripts/seoul_bike.py scripts/test_seoul_bike.py danawa-price-search/scripts/danawa_search.py kosis-stats/scripts/run_kosis_stats.py kosis-stats/tests/test_run_kosis_stats.py kstartup-search/scripts/run_kstartup.py kstartup-search/tests/test_run_kstartup.py intercity-bus-booking/scripts/intercity_bus_search.py daangn-used-goods-search/scripts/daangn_used_goods.py daangn-realty-search/scripts/daangn_realty.py daangn-jobs-search/scripts/daangn_jobs.py daangn-cars-search/scripts/daangn_cars.py foresttrip-vacancy/scripts/run_foresttrip_vacancy.py foresttrip-vacancy/tests/test_run_foresttrip_vacancy.py && npm run lint --workspaces --if-present && ./scripts/validate-skills.sh && node scripts/generate-plugin-manifest.js --check",
"typecheck": "tsc --noEmit",
"test": "python3 -m pip install --user --quiet beautifulsoup4 && node --test scripts/skill-docs.test.js scripts/test_korean_character_count.js scripts/test_korean_middle_korean.js scripts/test_build_manus_bundle.js scripts/workflow-actions.test.js scripts/test_generate_plugin_manifest.js && PYTHONPATH=.:scripts python3 -m unittest scripts.test_k_skill_cleaner scripts.test_fine_dust scripts.test_ktx_booking scripts.test_srt_booking scripts.test_sillok_search scripts.test_korean_spell_check scripts.test_patent_search scripts.test_mfds_drug_safety scripts.test_nts_business_registration scripts.test_mfds_food_safety scripts.test_zipcode_search scripts.test_subway_lost_property scripts.test_geeknews_search scripts.test_naver_blog_search scripts.test_korean_slang_writing scripts.test_kakaotalk_mac scripts.test_coupang_partners_mcp_wrapper scripts.test_ohou_today_deal scripts.test_ticket_availability scripts.test_seoul_bike scripts.test_danawa_price_search && PYTHONPATH=.:scripts:korean-scholarship-search/scripts python3 -m unittest discover -s korean-scholarship-search/scripts -p 'test_scholarship_filter.py' && PYTHONPATH=.:scripts:kosis-stats/scripts python3 -m unittest discover -s kosis-stats/tests -p 'test_run_kosis_stats.py' && PYTHONPATH=.:scripts:kstartup-search/scripts python3 -m unittest discover -s kstartup-search/tests -p 'test_run_kstartup.py' && PYTHONPATH=.:foresttrip-vacancy/scripts python3 -m unittest discover -s foresttrip-vacancy/tests -p 'test_run_foresttrip_vacancy.py' && npm run test --workspaces --if-present && ./scripts/validate-skills.sh",
"test": "python3 -m pip install --user --quiet beautifulsoup4 && node --test scripts/skill-docs.test.js scripts/test_korean_character_count.js scripts/test_korean_middle_korean.js scripts/test_build_manus_bundle.js scripts/workflow-actions.test.js scripts/test_generate_plugin_manifest.js && PYTHONPATH=.:scripts python3 -m unittest scripts.test_k_skill_cleaner scripts.test_fine_dust scripts.test_ktx_booking scripts.test_srt_booking scripts.test_srt_seats scripts.test_sillok_search scripts.test_korean_spell_check scripts.test_patent_search scripts.test_mfds_drug_safety scripts.test_nts_business_registration scripts.test_mfds_food_safety scripts.test_zipcode_search scripts.test_subway_lost_property scripts.test_geeknews_search scripts.test_naver_blog_search scripts.test_korean_slang_writing scripts.test_kakaotalk_mac scripts.test_coupang_partners_mcp_wrapper scripts.test_ohou_today_deal scripts.test_ticket_availability scripts.test_seoul_bike scripts.test_danawa_price_search && PYTHONPATH=.:scripts:korean-scholarship-search/scripts python3 -m unittest discover -s korean-scholarship-search/scripts -p 'test_scholarship_filter.py' && PYTHONPATH=.:scripts:kosis-stats/scripts python3 -m unittest discover -s kosis-stats/tests -p 'test_run_kosis_stats.py' && PYTHONPATH=.:scripts:kstartup-search/scripts python3 -m unittest discover -s kstartup-search/tests -p 'test_run_kstartup.py' && PYTHONPATH=.:foresttrip-vacancy/scripts python3 -m unittest discover -s foresttrip-vacancy/tests -p 'test_run_foresttrip_vacancy.py' && npm run test --workspaces --if-present && ./scripts/validate-skills.sh",
"pack:dry-run": "npm pack --workspace k-lotto --dry-run && npm pack --workspace daiso-product-search --dry-run && npm pack --workspace market-kurly-search --dry-run && npm pack --workspace blue-ribbon-nearby --dry-run && npm pack --workspace kakao-bar-nearby --dry-run && npm pack --workspace cheap-gas-nearby --dry-run && npm pack --workspace public-restroom-nearby --dry-run && npm pack --workspace parking-lot-search --dry-run && npm pack --workspace court-auction-notice-search --dry-run && npm pack --workspace donation-place-search --dry-run && npm pack --workspace gongsijiga-search --dry-run && npm pack --workspace kbl-results --dry-run && npm pack --workspace kleague-results --dry-run && npm pack --workspace lck-analytics --dry-run && npm pack --workspace toss-securities --dry-run && npm pack --workspace hipass-receipt --dry-run && npm pack --workspace used-car-price-search --dry-run && npm pack --workspace k-skill-rhwp --dry-run && npm pack --workspace korean-marathon-schedule --dry-run && npm pack --workspace gangnamunni-clinic-search --dry-run && npm pack --workspace daishin-report-search --dry-run && npm pack --workspace sh-notice-search --dry-run && npm pack --workspace emergency-room-beds --dry-run && npm pack --workspace local-election-candidate-search --dry-run",
"ci": "npm run lint && npm run typecheck && npm run test && npm run pack:dry-run",
"version-packages": "changeset version",

View file

@ -0,0 +1,128 @@
from __future__ import annotations
SEAT_HTML = "\n".join([
'<li class="scar-01 off"><strong>일반실<br />1호차</strong></li>',
'<li class="scar-04 on"><a href="#none" onclick="selectScarInfo(\'0004\'); return false;"><strong>일반실<br />4호차</strong></a></li>',
'<li class="scar-05"><a href="#none" onclick="selectScarInfo(\'0005\'); return false;"><strong>일반실<br />5호차</strong></a></li>',
'<li class="scar-03 off"><strong>특실<br />3호차</strong></li>',
'<a href="#none" onclick="selectSeatInfo(this, \'23\', \'6C\'); return false;">6C<strong><em>(정방향, 내측)</em></strong></a>',
'<a href="#none" onclick="selectSeatInfo(this, \'11\', \'3A\'); return false;">3A<strong><em>(역방향, 창측)</em></strong></a>',
"<span>5C<strong><em>(정방향, 내측, 선택불가)</em></strong></span>",
])
SPECIAL_SEAT_HTML = "\n".join([
'<li class="scar-03 on"><a href="#none" onclick="selectScarInfo(\'0003\'); return false;"><strong>특실<br />3호차</strong></a></li>',
'<li class="scar-05 off"><strong>일반실<br />5호차</strong></li>',
'<a href="#none" onclick="selectSeatInfo(this, \'31\', \'1A\'); return false;">1A<strong><em>(정방향, 1인석)</em></strong></a>',
"<span>2C<strong><em>(역방향, 내측, 선택불가)</em></strong></span>",
])
class FakeTrain:
train_number = "313"
dep_date = "20260610"
dep_time = "080000"
arr_date = "20260610"
arr_time = "103400"
train_code = "17"
train_name = "SRT"
dep_station_code = "0551"
dep_station_name = "수서"
arr_station_code = "0020"
arr_station_name = "부산"
dep_station_run_order = "000001"
arr_station_run_order = "000007"
general_seat_state = "예약가능"
special_seat_state = "매진"
reserve_wait_possible_code = "-2"
def general_seat_available(self) -> bool:
return True
def special_seat_available(self) -> bool:
return False
def reserve_standby_available(self) -> bool:
return False
class FakeResponse:
def __init__(self, text: str) -> None:
self.text = text
class FakeSession:
def __init__(self) -> None:
self.calls: list[dict[str, str]] = []
def get(self, _url: str, params: dict[str, str]) -> FakeResponse:
self.calls.append(params)
car = params["scarNo1"] or "0004"
return FakeResponse(SEAT_HTML.replace("scar-04 on", f"scar-{car[-2:]} on"))
class FakeClient:
def __init__(self, train: FakeTrain) -> None:
self.train = train
self._session = FakeSession()
def search_train(
self,
_dep: str,
_arr: str,
_date: str,
_time: str,
_time_limit: str | None = None,
available_only: bool = True,
) -> list[FakeTrain]:
return [self.train]
class NoisySession(FakeSession):
def get(self, _url: str, params: dict[str, str]) -> FakeResponse:
print("접속자가 많아 대기열에 들어갑니다.")
return super().get(_url, params)
class NoisyClient(FakeClient):
def __init__(self, train: FakeTrain) -> None:
self.train = train
self._session = NoisySession()
def search_train(
self,
_dep: str,
_arr: str,
_date: str,
_time: str,
_time_limit: str | None = None,
available_only: bool = True,
) -> list[FakeTrain]:
print("대기인원: 6명")
return [self.train]
class EmptyClient(FakeClient):
def search_train(
self,
_dep: str,
_arr: str,
_date: str,
_time: str,
_time_limit: str | None = None,
available_only: bool = True,
) -> list[FakeTrain]:
return []
class SpecialSession(FakeSession):
def get(self, _url: str, params: dict[str, str]) -> FakeResponse:
self.calls.append(params)
return FakeResponse(SPECIAL_SEAT_HTML)
class SpecialClient(FakeClient):
def __init__(self, train: FakeTrain) -> None:
self.train = train
self._session = SpecialSession()

View file

@ -8,197 +8,10 @@ from contextlib import redirect_stdout
from unittest.mock import patch
import srt_booking
import srt_seats
SEAT_HTML = "\n".join([
'<li class="scar-01 off"><strong>일반실<br />1호차</strong></li>',
'<li class="scar-04 on"><a href="#none" onclick="selectScarInfo(\'0004\'); return false;"><strong>일반실<br />4호차</strong></a></li>',
'<li class="scar-05"><a href="#none" onclick="selectScarInfo(\'0005\'); return false;"><strong>일반실<br />5호차</strong></a></li>',
'<li class="scar-03 off"><strong>특실<br />3호차</strong></li>',
'<a href="#none" onclick="selectSeatInfo(this, \'23\', \'6C\'); return false;">6C<strong><em>(정방향, 내측)</em></strong></a>',
'<a href="#none" onclick="selectSeatInfo(this, \'11\', \'3A\'); return false;">3A<strong><em>(역방향, 창측)</em></strong></a>',
"<span>5C<strong><em>(정방향, 내측, 선택불가)</em></strong></span>",
])
SPECIAL_SEAT_HTML = "\n".join([
'<li class="scar-03 on"><a href="#none" onclick="selectScarInfo(\'0003\'); return false;"><strong>특실<br />3호차</strong></a></li>',
'<li class="scar-05 off"><strong>일반실<br />5호차</strong></li>',
'<a href="#none" onclick="selectSeatInfo(this, \'31\', \'1A\'); return false;">1A<strong><em>(정방향, 1인석)</em></strong></a>',
"<span>2C<strong><em>(역방향, 내측, 선택불가)</em></strong></span>",
])
MISSING_DETAIL_HTML = "\n".join([
'<a href="#none" onclick="selectSeatInfo(this, \'41\', \'9A\'); return false;">9A<strong><em>()</em></strong></a>',
])
class FakeTrain:
train_number = "313"
dep_date = "20260610"
dep_time = "080000"
arr_date = "20260610"
arr_time = "103400"
train_code = "17"
train_name = "SRT"
dep_station_code = "0551"
dep_station_name = "수서"
arr_station_code = "0020"
arr_station_name = "부산"
dep_station_run_order = "000001"
arr_station_run_order = "000007"
general_seat_state = "예약가능"
special_seat_state = "매진"
reserve_wait_possible_code = "-2"
def general_seat_available(self) -> bool:
return True
def special_seat_available(self) -> bool:
return False
def reserve_standby_available(self) -> bool:
return False
class FakeResponse:
def __init__(self, text: str) -> None:
self.text = text
class FakeSession:
def __init__(self) -> None:
self.calls: list[dict[str, str]] = []
def get(self, _url: str, params: dict[str, str]) -> FakeResponse:
self.calls.append(params)
car = params["scarNo1"] or "0004"
return FakeResponse(SEAT_HTML.replace("scar-04 on", f"scar-{car[-2:]} on"))
class FakeClient:
def __init__(self, train: FakeTrain) -> None:
self.train = train
self._session = FakeSession()
def search_train(
self,
_dep: str,
_arr: str,
_date: str,
_time: str,
_time_limit: str | None = None,
available_only: bool = True,
) -> list[FakeTrain]:
return [self.train]
class NoisySession(FakeSession):
def get(self, _url: str, params: dict[str, str]) -> FakeResponse:
print("접속자가 많아 대기열에 들어갑니다.")
return super().get(_url, params)
class NoisyClient(FakeClient):
def __init__(self, train: FakeTrain) -> None:
self.train = train
self._session = NoisySession()
def search_train(
self,
_dep: str,
_arr: str,
_date: str,
_time: str,
_time_limit: str | None = None,
available_only: bool = True,
) -> list[FakeTrain]:
print("대기인원: 6명")
return [self.train]
class EmptyClient(FakeClient):
def search_train(
self,
_dep: str,
_arr: str,
_date: str,
_time: str,
_time_limit: str | None = None,
available_only: bool = True,
) -> list[FakeTrain]:
return []
class SpecialSession(FakeSession):
def get(self, _url: str, params: dict[str, str]) -> FakeResponse:
self.calls.append(params)
return FakeResponse(SPECIAL_SEAT_HTML)
class SpecialClient(FakeClient):
def __init__(self, train: FakeTrain) -> None:
self.train = train
self._session = SpecialSession()
from srt_booking_test_support import EmptyClient, FakeClient, FakeTrain, NoisyClient, SpecialClient
class SrtSeatTests(unittest.TestCase):
def test_normalize_car_and_seat_maps_srt_html(self) -> None:
cars = srt_seats.parse_cars(SEAT_HTML)
seats = srt_seats.parse_seats(SEAT_HTML)
self.assertEqual([car["car_no"] for car in cars if car["available"]], [4, 5])
self.assertEqual(cars[1]["room_class"], "일반실")
self.assertTrue(cars[1]["current"])
self.assertEqual([seat["seat"] for seat in seats if seat["available"]], ["6C", "3A"])
self.assertEqual([seat["seat"] for seat in seats if not seat["available"]], ["5C"])
self.assertEqual(seats[0]["direction"], "정방향")
self.assertEqual(seats[0]["position"], "내측")
self.assertEqual(seats[2]["notes"], ["선택불가"])
def test_booking_priority_sorts_middle_cars_before_end_cars(self) -> None:
cars: list[srt_seats.SrtCar] = [
{"car_no": 1, "car_no_raw": "0001", "room_class": "일반실", "available": True, "current": False},
{"car_no": 8, "car_no_raw": "0008", "room_class": "일반실", "available": True, "current": False},
{"car_no": 2, "car_no_raw": "0002", "room_class": "일반실", "available": True, "current": False},
{"car_no": 7, "car_no_raw": "0007", "room_class": "일반실", "available": True, "current": False},
{"car_no": 3, "car_no_raw": "0003", "room_class": "일반실", "available": True, "current": False},
{"car_no": 6, "car_no_raw": "0006", "room_class": "일반실", "available": True, "current": False},
{"car_no": 4, "car_no_raw": "0004", "room_class": "일반실", "available": True, "current": False},
{"car_no": 5, "car_no_raw": "0005", "room_class": "일반실", "available": True, "current": False},
]
sorted_cars = srt_seats.sort_cars_for_booking(cars)
self.assertEqual([car["car_no"] for car in sorted_cars], [4, 5, 3, 6, 2, 7, 1, 8])
def test_booking_priority_sorts_forward_window_before_other_seats(self) -> None:
seats: list[srt_seats.SrtSeat] = [
{"seat": "3A", "seat_no": "11", "available": True, "direction": "역방향", "position": "창측", "notes": []},
{"seat": "6C", "seat_no": "23", "available": True, "direction": "정방향", "position": "내측", "notes": []},
{"seat": "2A", "seat_no": "7", "available": True, "direction": "정방향", "position": "창측", "notes": []},
]
sorted_seats = srt_seats.sort_seats_for_booking(seats, "forward-window")
self.assertEqual([seat["seat"] for seat in sorted_seats], ["2A", "6C", "3A"])
def test_booking_priority_treats_single_seat_as_window_preference(self) -> None:
seats: list[srt_seats.SrtSeat] = [
{"seat": "1C", "seat_no": "3", "available": True, "direction": "정방향", "position": "내측", "notes": []},
{"seat": "2A", "seat_no": "5", "available": True, "direction": "정방향", "position": "1인석", "notes": []},
]
sorted_seats = srt_seats.sort_seats_for_booking(seats, "window-forward")
self.assertEqual([seat["seat"] for seat in sorted_seats], ["2A", "1C"])
def test_parse_seat_page_marks_missing_detail_attributes_unknown(self) -> None:
seats = srt_seats.parse_seats(MISSING_DETAIL_HTML)
self.assertEqual(seats[0]["direction"], "unknown")
self.assertEqual(seats[0]["position"], "unknown")
def test_command_seats_outputs_available_seats_by_booking_preference(self) -> None:
train = FakeTrain()
train_id = srt_booking.build_train_id(train)

82
scripts/test_srt_seats.py Normal file
View file

@ -0,0 +1,82 @@
from __future__ import annotations
import unittest
import srt_seats
SEAT_HTML = "\n".join([
'<li class="scar-01 off"><strong>일반실<br />1호차</strong></li>',
'<li class="scar-04 on"><a href="#none" onclick="selectScarInfo(\'0004\'); return false;"><strong>일반실<br />4호차</strong></a></li>',
'<li class="scar-05"><a href="#none" onclick="selectScarInfo(\'0005\'); return false;"><strong>일반실<br />5호차</strong></a></li>',
'<li class="scar-03 off"><strong>특실<br />3호차</strong></li>',
'<a href="#none" onclick="selectSeatInfo(this, \'23\', \'6C\'); return false;">6C<strong><em>(정방향, 내측)</em></strong></a>',
'<a href="#none" onclick="selectSeatInfo(this, \'11\', \'3A\'); return false;">3A<strong><em>(역방향, 창측)</em></strong></a>',
"<span>5C<strong><em>(정방향, 내측, 선택불가)</em></strong></span>",
])
MISSING_DETAIL_HTML = "\n".join([
'<a href="#none" onclick="selectSeatInfo(this, \'41\', \'9A\'); return false;">9A<strong><em>()</em></strong></a>',
])
class SrtSeatParserTests(unittest.TestCase):
def test_normalize_car_and_seat_maps_srt_html(self) -> None:
cars = srt_seats.parse_cars(SEAT_HTML)
seats = srt_seats.parse_seats(SEAT_HTML)
self.assertEqual([car["car_no"] for car in cars if car["available"]], [4, 5])
self.assertEqual(cars[1]["room_class"], "일반실")
self.assertTrue(cars[1]["current"])
self.assertEqual([seat["seat"] for seat in seats if seat["available"]], ["6C", "3A"])
self.assertEqual([seat["seat"] for seat in seats if not seat["available"]], ["5C"])
self.assertEqual(seats[0]["direction"], "정방향")
self.assertEqual(seats[0]["position"], "내측")
self.assertEqual(seats[2]["notes"], ["선택불가"])
def test_booking_priority_sorts_middle_cars_before_end_cars(self) -> None:
cars: list[srt_seats.SrtCar] = [
{"car_no": 1, "car_no_raw": "0001", "room_class": "일반실", "available": True, "current": False},
{"car_no": 8, "car_no_raw": "0008", "room_class": "일반실", "available": True, "current": False},
{"car_no": 2, "car_no_raw": "0002", "room_class": "일반실", "available": True, "current": False},
{"car_no": 7, "car_no_raw": "0007", "room_class": "일반실", "available": True, "current": False},
{"car_no": 3, "car_no_raw": "0003", "room_class": "일반실", "available": True, "current": False},
{"car_no": 6, "car_no_raw": "0006", "room_class": "일반실", "available": True, "current": False},
{"car_no": 4, "car_no_raw": "0004", "room_class": "일반실", "available": True, "current": False},
{"car_no": 5, "car_no_raw": "0005", "room_class": "일반실", "available": True, "current": False},
]
sorted_cars = srt_seats.sort_cars_for_booking(cars)
self.assertEqual([car["car_no"] for car in sorted_cars], [4, 5, 3, 6, 2, 7, 1, 8])
def test_booking_priority_sorts_forward_window_before_other_seats(self) -> None:
seats: list[srt_seats.SrtSeat] = [
{"seat": "3A", "seat_no": "11", "available": True, "direction": "역방향", "position": "창측", "notes": []},
{"seat": "6C", "seat_no": "23", "available": True, "direction": "정방향", "position": "내측", "notes": []},
{"seat": "2A", "seat_no": "7", "available": True, "direction": "정방향", "position": "창측", "notes": []},
]
sorted_seats = srt_seats.sort_seats_for_booking(seats, "forward-window")
self.assertEqual([seat["seat"] for seat in sorted_seats], ["2A", "6C", "3A"])
def test_booking_priority_treats_single_seat_as_window_preference(self) -> None:
seats: list[srt_seats.SrtSeat] = [
{"seat": "1C", "seat_no": "3", "available": True, "direction": "정방향", "position": "내측", "notes": []},
{"seat": "2A", "seat_no": "5", "available": True, "direction": "정방향", "position": "1인석", "notes": []},
]
sorted_seats = srt_seats.sort_seats_for_booking(seats, "window-forward")
self.assertEqual([seat["seat"] for seat in sorted_seats], ["2A", "1C"])
def test_parse_seat_page_marks_missing_detail_attributes_unknown(self) -> None:
seats = srt_seats.parse_seats(MISSING_DETAIL_HTML)
self.assertEqual(seats[0]["direction"], "unknown")
self.assertEqual(seats[0]["position"], "unknown")
if __name__ == "__main__":
unittest.main()

View file

@ -12,7 +12,7 @@ metadata:
## What this skill does
`SRTrain` 위에 `scripts/srt_booking.py` helper 를 얹어 SRT 조회, 호차별 좌석번호 확인, 예약과 취소를 처리한다.
`SRTrain` 위에 `scripts/srt_booking.py` helper 를 얹어 SRT 조회와 호차별 좌석번호 확인을 처리하고, 예약과 취소는 고정된 열차/예약을 다시 식별한 뒤 `SRTrain`으로 진행한다.
## When to use