Clarify KTX seat filter failures

Constraint: PR #295 review found power-only summaries included non-power seats and empty car-list payloads looked successful.
Rejected: Document available_seats as unfiltered | would preserve a surprising JSON contract for power-only callers.
Rejected: Ignore local hidden directories one by one | hidden runtime/cache directories can keep reappearing outside git.
Confidence: high
Scope-risk: narrow
Directive: Keep seats output summaries aligned with active display filters, while --limit remains a detailed seats display cap.
Tested: PYTHONPATH=.:scripts python3 -m unittest scripts.test_ktx_booking; python3 -m compileall -q scripts/ktx_booking.py scripts/test_ktx_booking.py; git diff --check; node --test scripts/skill-docs.test.js; npm run typecheck; PIP_BREAK_SYSTEM_PACKAGES=1 npm run ci
Not-tested: live Korail production seat endpoint behavior
This commit is contained in:
iamiks 2026-05-28 23:07:09 +09:00
commit 70b92d6b03
3 changed files with 97 additions and 4 deletions

View file

@ -915,6 +915,8 @@ def command_seats(args: argparse.Namespace) -> None:
train, raw_train = match
room_class = ROOM_CLASS_MAP[args.room]
cars = [normalize_car(car) for car in client.train_cars(raw_train, passenger_count, room_class)]
if not cars:
raise SystemExit(f"seat car data is unavailable for {args.room}; retry search or choose another train")
if args.car_no is not None:
cars = [car for car in cars if car["car_no"] == args.car_no]
if not cars:
@ -929,12 +931,12 @@ def command_seats(args: argparse.Namespace) -> None:
if isinstance(raw_seats, dict):
raw_seats = [raw_seats]
all_seats = [normalize_seat(seat) for seat in raw_seats if seat.get("h_con_seat_no") != "0A"]
available_seats = sort_seats_for_booking([seat for seat in all_seats if seat["available"]])
seats = sort_seats_for_booking(all_seats)
if args.available_only:
seats = available_seats
seats = [seat for seat in seats if seat["available"]]
if args.power_only:
seats = [seat for seat in seats if seat["power_outlet"] != "none"]
available_seats = [seat for seat in seats if seat["available"]]
seats = seats[: args.limit]
car_payload = dict(car)
car_payload["available_seat_count"] = len(available_seats)

View file

@ -547,8 +547,8 @@ class KtxBookingTests(unittest.TestCase):
self.assertEqual(len(result["cars"]), 1)
self.assertEqual(result["cars"][0]["car_no"], 5)
self.assertEqual(result["cars"][0]["remaining_seats"], 3)
self.assertEqual(result["cars"][0]["available_seat_count"], 2)
self.assertEqual(result["cars"][0]["available_seats"], ["1A", "2A"])
self.assertEqual(result["cars"][0]["available_seat_count"], 1)
self.assertEqual(result["cars"][0]["available_seats"], ["1A"])
self.assertEqual(result["cars"][0]["shown_seat_count"], 1)
self.assertEqual(result["cars"][0]["seats"][0]["seat"], "1A")
self.assertEqual(result["cars"][0]["seats"][0]["power_outlet"], "direct")
@ -673,6 +673,63 @@ class KtxBookingTests(unittest.TestCase):
self.assertEqual(car["available_seats"], ["3A", "1A", "1C", "2A"])
self.assertEqual([seat["seat"] for seat in car["seats"]], ["3A", "1A", "1C", "2A"])
def test_command_seats_available_summary_matches_power_filter(self):
selected = FakeTrain(train_no="009", dep_time="090000", arr_time="113000", label="selected")
raw_train = {"h_trn_no": "009", "h_dpt_dt": "20260328"}
train_id = ktx_booking.normalize_train(selected, index=1)["train_id"]
client = FakeClient(
[],
train_details=[(selected, raw_train)],
cars=[{"h_srcar_no": "05", "h_psrm_cl_cd": "1", "h_seat_cnt": "48", "h_rest_seat_cnt": "4"}],
seats_by_car={
"05": [
{
"h_con_seat_no": "1A",
"h_seat_no": "001001",
"h_sale_psb_flg": "Y",
"h_for_rev_dir_dv": "009",
"h_sigl_win_in_dv": "012",
"h_dmd_seat_att": "015",
},
{
"h_con_seat_no": "2C",
"h_seat_no": "002003",
"h_sale_psb_flg": "Y",
"h_for_rev_dir_dv": "009",
"h_sigl_win_in_dv": "013",
"h_dmd_seat_att": "015",
},
],
},
)
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=True,
power_only=True,
limit=10,
)
output = io.StringIO()
with patch.object(ktx_booking, "build_client", return_value=client):
with redirect_stdout(output):
ktx_booking.command_seats(args)
car = json.loads(output.getvalue())["cars"][0]
self.assertEqual(car["available_seat_count"], 1)
self.assertEqual(car["available_seats"], ["1A"])
self.assertEqual([seat["seat"] for seat in car["seats"]], ["1A"])
def test_command_seats_supports_special_room_and_stale_train_error(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"]
@ -740,6 +797,39 @@ class KtxBookingTests(unittest.TestCase):
self.assertIn("car_no 5", 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"]
client = FakeClient(
[],
train_details=[(selected, {"h_trn_no": "009"})],
cars=[],
)
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 car data is unavailable", str(exc.exception))
def test_build_parser_has_ncard_commands(self):
parser = ktx_booking.build_parser()
help_text = parser.format_help()

View file

@ -36,6 +36,7 @@ while IFS= read -r -d '' skill_dir; do
fi
done < <(
find "$root" -mindepth 1 -maxdepth 1 -type d \
! -name '.*' \
! -name .git \
! -name .github \
! -name .codex \