mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
* feat(srt): 좌석 조회와 탐색 우선순위 추가 SRT search 결과의 stable train_id로 객차별 좌석을 조회하고, 특정 호차/좌석 확인과 탐색 우선순위 옵션을 제공한다. Constraint: SRT와 KTX는 별도 upstream 표면이므로 SRT HTML 파서와 테스트를 분리함 Rejected: KTX 좌석 helper 공유 | Korail API와 SRT 웹 좌석선택 HTML 계약이 달라 혼용하면 파서 안정성이 낮아짐 Confidence: medium Scope-risk: moderate Directive: SRT 좌석선택 HTML에서 노출되지 않는 속성은 추정하지 말고 명시적으로 처리할 것 Tested: PYTHONPATH=.:scripts python3 -m unittest scripts.test_srt_booking scripts.test_ktx_booking; python3 -m py_compile scripts/srt_booking.py scripts/srt_seats.py scripts/test_srt_booking.py Not-tested: 실제 예약 API에 우선순위 좌석 선택을 연결하는 흐름 * fix(srt): 좌석 조회 JSON 출력 안정화 SRT 대기열 메시지가 stdout에 섞여 seats JSON을 깨는 실제 표면 문제를 막고, 누락된 좌석 방향/위치 속성을 unknown으로 정규화한다. Constraint: issue #303 범위는 예약 부작용이 없는 좌석 조회 보조 흐름으로 제한됨 Rejected: 실제 예약 subcommand 추가 | 좌석 선점/예약은 외부 부작용이라 이번 acceptance criteria에 포함되지 않음 Confidence: high Scope-risk: narrow Directive: SRTrain upstream 출력이 추가되더라도 helper stdout은 JSON 전용으로 유지할 것 Tested: RED→GREEN in .omo/ulw-loop/evidence/srt-c002-red-green-tests.txt; live SRT tmux QA in .omo/ulw-loop/evidence/srt-c001-live-search-seats.txt; npm run ci in .omo/ulw-loop/evidence/srt-c003-regression-ci.txt Not-tested: 실제 예약/결제/취소 부작용 흐름 * test(srt): split seat helper regression coverage --------- Co-authored-by: Jeffrey (Dongkyu) Kim <vkehfdl1@gmail.com>
209 lines
6.7 KiB
Python
209 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import io
|
|
import json
|
|
import unittest
|
|
from contextlib import redirect_stdout
|
|
from unittest.mock import patch
|
|
|
|
import srt_booking
|
|
from srt_booking_test_support import EmptyClient, FakeClient, FakeTrain, NoisyClient, SpecialClient
|
|
|
|
|
|
class SrtSeatTests(unittest.TestCase):
|
|
def test_command_seats_outputs_available_seats_by_booking_preference(self) -> None:
|
|
train = FakeTrain()
|
|
train_id = srt_booking.build_train_id(train)
|
|
client = FakeClient(train)
|
|
args = argparse.Namespace(
|
|
dep="수서",
|
|
arr="부산",
|
|
date="20260610",
|
|
time="080000",
|
|
time_limit=None,
|
|
train_id=train_id,
|
|
room="general",
|
|
car_no=4,
|
|
seat="6C",
|
|
available_only=False,
|
|
car_priority="center",
|
|
seat_priority="forward-window",
|
|
limit=10,
|
|
)
|
|
output = io.StringIO()
|
|
|
|
with patch.object(srt_booking, "build_client", return_value=client):
|
|
with redirect_stdout(output):
|
|
srt_booking.command_seats(args)
|
|
|
|
result = json.loads(output.getvalue())
|
|
car = result["cars"][0]
|
|
self.assertEqual(car["car_no"], 4)
|
|
self.assertTrue(car["requested_seat_available"])
|
|
self.assertEqual(car["available_seats"], ["6C"])
|
|
self.assertEqual(client._session.calls[-1]["scarNo1"], "0004")
|
|
|
|
def test_command_seats_filters_unavailable_when_available_only(self) -> None:
|
|
train = FakeTrain()
|
|
train_id = srt_booking.build_train_id(train)
|
|
client = FakeClient(train)
|
|
args = argparse.Namespace(
|
|
dep="수서",
|
|
arr="부산",
|
|
date="20260610",
|
|
time="080000",
|
|
time_limit=None,
|
|
train_id=train_id,
|
|
room="general",
|
|
car_no=4,
|
|
seat=None,
|
|
available_only=True,
|
|
car_priority="center",
|
|
seat_priority="forward-window",
|
|
limit=10,
|
|
)
|
|
output = io.StringIO()
|
|
|
|
with patch.object(srt_booking, "build_client", return_value=client):
|
|
with redirect_stdout(output):
|
|
srt_booking.command_seats(args)
|
|
|
|
result = json.loads(output.getvalue())
|
|
shown_seats = result["cars"][0]["seats"]
|
|
self.assertEqual([seat["seat"] for seat in shown_seats], ["6C", "3A"])
|
|
self.assertTrue(all(seat["available"] for seat in shown_seats))
|
|
|
|
def test_command_seats_returns_special_room_cars(self) -> None:
|
|
train = FakeTrain()
|
|
train_id = srt_booking.build_train_id(train)
|
|
client = SpecialClient(train)
|
|
args = argparse.Namespace(
|
|
dep="수서",
|
|
arr="부산",
|
|
date="20260610",
|
|
time="080000",
|
|
time_limit=None,
|
|
train_id=train_id,
|
|
room="special",
|
|
car_no=3,
|
|
seat=None,
|
|
available_only=True,
|
|
car_priority="center",
|
|
seat_priority="window-forward",
|
|
limit=10,
|
|
)
|
|
output = io.StringIO()
|
|
|
|
with patch.object(srt_booking, "build_client", return_value=client):
|
|
with redirect_stdout(output):
|
|
srt_booking.command_seats(args)
|
|
|
|
result = json.loads(output.getvalue())
|
|
self.assertEqual(result["room"], "special")
|
|
self.assertEqual(result["cars"][0]["room_class"], "특실")
|
|
self.assertEqual(result["cars"][0]["available_seats"], ["1A"])
|
|
|
|
def test_command_seats_fails_when_train_id_is_stale(self) -> None:
|
|
train = FakeTrain()
|
|
args = argparse.Namespace(
|
|
dep="수서",
|
|
arr="부산",
|
|
date="20260610",
|
|
time="080000",
|
|
time_limit=None,
|
|
train_id=srt_booking.build_train_id(train),
|
|
room="general",
|
|
car_no=4,
|
|
seat=None,
|
|
available_only=False,
|
|
car_priority="center",
|
|
seat_priority="forward-window",
|
|
limit=10,
|
|
)
|
|
|
|
with patch.object(srt_booking, "build_client", return_value=EmptyClient(train)):
|
|
with self.assertRaises(SystemExit) as exc:
|
|
with redirect_stdout(io.StringIO()):
|
|
srt_booking.command_seats(args)
|
|
|
|
self.assertIn("train_id", str(exc.exception))
|
|
|
|
def test_command_seats_keeps_json_stdout_when_upstream_prints_queue_messages(self) -> None:
|
|
train = FakeTrain()
|
|
train_id = srt_booking.build_train_id(train)
|
|
client = NoisyClient(train)
|
|
args = argparse.Namespace(
|
|
dep="수서",
|
|
arr="부산",
|
|
date="20260610",
|
|
time="080000",
|
|
time_limit=None,
|
|
train_id=train_id,
|
|
room="general",
|
|
car_no=4,
|
|
seat=None,
|
|
available_only=True,
|
|
car_priority="center",
|
|
seat_priority="forward-window",
|
|
limit=10,
|
|
)
|
|
output = io.StringIO()
|
|
|
|
with patch.object(srt_booking, "build_client", return_value=client):
|
|
with redirect_stdout(output):
|
|
srt_booking.command_seats(args)
|
|
|
|
result = json.loads(output.getvalue())
|
|
self.assertEqual(result["cars"][0]["available_seats"], ["6C", "3A"])
|
|
|
|
def test_command_seats_explores_middle_cars_first(self) -> None:
|
|
train = FakeTrain()
|
|
train_id = srt_booking.build_train_id(train)
|
|
client = FakeClient(train)
|
|
args = argparse.Namespace(
|
|
dep="수서",
|
|
arr="부산",
|
|
date="20260610",
|
|
time="080000",
|
|
time_limit=None,
|
|
train_id=train_id,
|
|
room="general",
|
|
car_no=None,
|
|
seat=None,
|
|
available_only=True,
|
|
car_priority="center",
|
|
seat_priority="forward-window",
|
|
limit=10,
|
|
)
|
|
|
|
with patch.object(srt_booking, "build_client", return_value=client):
|
|
with redirect_stdout(io.StringIO()):
|
|
srt_booking.command_seats(args)
|
|
|
|
self.assertEqual([call["scarNo1"] for call in client._session.calls], ["", "0004", "0005"])
|
|
|
|
def test_build_parser_accepts_seats_filters(self) -> None:
|
|
args = srt_booking.build_parser().parse_args([
|
|
"seats",
|
|
"수서",
|
|
"부산",
|
|
"20260610",
|
|
"080000",
|
|
"--train-id",
|
|
"srt:v1:test",
|
|
"--car-no",
|
|
"5",
|
|
"--seat",
|
|
"11A",
|
|
"--seat-priority",
|
|
"window-forward",
|
|
])
|
|
|
|
self.assertEqual(args.car_no, 5)
|
|
self.assertEqual(args.seat, "11A")
|
|
self.assertEqual(args.seat_priority, "window-forward")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|