Expose missing KTX seat detail payloads

Constraint: PR #298 review blocker required missing seat_infos.seat_info to fail clearly instead of reporting zero seats.
Rejected: Treating absent seat_info as an empty list | this masks Korail detail endpoint failures as authoritative no-seat results.
Confidence: high
Scope-risk: narrow
Directive: Preserve explicit empty seat_info lists as valid; only absent or malformed detail schema should fail.
Tested: PYTHONPATH=.:scripts python3 -m unittest scripts.test_ktx_booking; node --test scripts/skill-docs.test.js; npm run typecheck; python3 -m compileall -q scripts/ktx_booking.py scripts/test_ktx_booking.py; ruff check scripts/ktx_booking.py scripts/test_ktx_booking.py; shellcheck scripts/validate-skills.sh; bash scripts/validate-skills.sh; PYENV_VERSION=3.11.9 npm run ci
Not-tested: plain npm run ci with local default Python 3.14 remains blocked by pyexpat/libexpat linkage before project tests
This commit is contained in:
Jeffrey (Dongkyu) Kim 2026-05-29 00:27:02 +09:00
commit 20e8f3f8f0
2 changed files with 43 additions and 9 deletions

View file

@ -928,19 +928,19 @@ def command_seats(args: argparse.Namespace) -> None:
for car in cars:
raw = client.car_seats(raw_train, str(car["car_no_raw"]), passenger_count, room_class)
seat_infos = raw.get("seat_infos") if isinstance(raw, dict) else None
seat_detail_unavailable = (
f"seat detail data is unavailable for car_no {car['car_no']}; "
"retry search or choose another train"
)
if not isinstance(seat_infos, dict):
raise SystemExit(
f"seat detail data is unavailable for car_no {car['car_no']}; "
"retry search or choose another train"
)
raw_seats = seat_infos.get("seat_info", [])
raise SystemExit(seat_detail_unavailable)
if "seat_info" not in seat_infos:
raise SystemExit(seat_detail_unavailable)
raw_seats = seat_infos["seat_info"]
if isinstance(raw_seats, dict):
raw_seats = [raw_seats]
if not isinstance(raw_seats, list):
raise SystemExit(
f"seat detail data is unavailable for car_no {car['car_no']}; "
"retry search or choose another train"
)
raise SystemExit(seat_detail_unavailable)
all_seats = [normalize_seat(seat) for seat in raw_seats if seat.get("h_con_seat_no") != "0A"]
seats = sort_seats_for_booking(all_seats)
if args.available_only:

View file

@ -831,6 +831,40 @@ class KtxBookingTests(unittest.TestCase):
self.assertIn("seat detail data is unavailable", str(exc.exception))
def test_command_seats_fails_when_seat_info_key_is_missing(self):
selected = FakeTrain(train_no="009", dep_time="090000", arr_time="113000", label="selected")
train_id = ktx_booking.normalize_train(selected, index=1)["train_id"]
client = FakeClient(
[],
train_details=[(selected, {"h_trn_no": "009"})],
cars=[{"h_srcar_no": "05", "h_psrm_cl_cd": "1", "h_seat_cnt": "48", "h_rest_seat_cnt": "9"}],
)
client.car_seats = lambda *args, **kwargs: {"seat_infos": {}}
args = argparse.Namespace(
dep="서울",
arr="부산",
date="20260328",
time="090000",
adults=1,
children=0,
toddlers=0,
seniors=0,
train_id=train_id,
room="general",
train_type="ktx",
car_no=None,
available_only=False,
power_only=False,
limit=10,
)
with patch.object(ktx_booking, "build_client", return_value=client):
with self.assertRaises(SystemExit) as exc:
with redirect_stdout(io.StringIO()):
ktx_booking.command_seats(args)
self.assertIn("seat detail data is unavailable", str(exc.exception))
def test_command_seats_fails_when_car_data_is_unavailable(self):
selected = FakeTrain(train_no="009", dep_time="090000", arr_time="113000", label="selected")
train_id = ktx_booking.normalize_train(selected, index=1)["train_id"]