mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
The helper's emitted POST example still timed out against LOST112 in live verification because the 20-second timeout budget was too short for the observed form-submit path. Raise the generated timeout to 60 seconds, lock that contract in the regression, and align the skill/doc wording so users receive guidance that matches the runnable command. Constraint: LOST112 live POST latency can exceed 20 seconds even when reachability probes succeed Rejected: Keep 20 seconds and rely on manual retry | still ships a broken default curl_example Confidence: high Scope-risk: narrow Reversibility: clean Directive: If the timeout budget changes again, rerun the exact emitted curl_example against LOST112 rather than only the lightweight reachability probe Tested: python3 -m unittest scripts.test_subway_lost_property Tested: python3 scripts/subway_lost_property.py --station 강남역 --item 지갑 --days 14 --verify-live Tested: npm run ci Not-tested: Long-term LOST112 latency drift beyond the currently observed 60-second budget
127 lines
5.1 KiB
Python
127 lines
5.1 KiB
Python
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
import shlex
|
|
from datetime import date
|
|
from pathlib import Path
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
from scripts.subway_lost_property import (
|
|
LOST112_LIST_URL,
|
|
SEOUL_METRO_LOST_CENTER_URL,
|
|
SearchQuery,
|
|
build_curl_command,
|
|
build_search_payload,
|
|
build_search_plan,
|
|
expand_station_keywords,
|
|
main,
|
|
probe_source,
|
|
)
|
|
|
|
|
|
class SubwayLostPropertyQueryTest(unittest.TestCase):
|
|
def test_build_search_payload_defaults_to_external_agency_search(self):
|
|
payload = build_search_payload(
|
|
SearchQuery(
|
|
station="강남역",
|
|
item="지갑",
|
|
start_date=date(2026, 4, 1),
|
|
end_date=date(2026, 4, 10),
|
|
)
|
|
)
|
|
|
|
self.assertEqual(payload["START_YMD"], "20260401")
|
|
self.assertEqual(payload["END_YMD"], "20260410")
|
|
self.assertEqual(payload["PRDT_NM"], "지갑")
|
|
self.assertEqual(payload["DEP_PLACE"], "강남역")
|
|
self.assertEqual(payload["SITE"], "V")
|
|
self.assertEqual(payload["pageIndex"], "1")
|
|
|
|
def test_expand_station_keywords_keeps_station_and_strips_suffix(self):
|
|
self.assertEqual(expand_station_keywords(" 강남역 "), ["강남역", "강남"])
|
|
|
|
def test_build_search_plan_serializes_official_sources_and_guidance(self):
|
|
plan = build_search_plan(
|
|
station="강남역",
|
|
item="지갑",
|
|
days=14,
|
|
today=date(2026, 4, 10),
|
|
)
|
|
|
|
self.assertEqual(plan.query.station, "강남역")
|
|
self.assertEqual(plan.query.item, "지갑")
|
|
self.assertEqual(plan.query.start_date.isoformat(), "2026-03-27")
|
|
self.assertEqual(plan.query.end_date.isoformat(), "2026-04-10")
|
|
self.assertEqual(plan.official_sources[0]["url"], LOST112_LIST_URL)
|
|
self.assertEqual(plan.official_sources[1]["url"], SEOUL_METRO_LOST_CENTER_URL)
|
|
self.assertIn("강남역", plan.suggested_keywords)
|
|
self.assertIn("강남", plan.suggested_keywords)
|
|
command = shlex.split(build_curl_command(plan.payload))
|
|
self.assertNotIn("-L", command)
|
|
self.assertIn("--max-time", command)
|
|
self.assertEqual(command[command.index("--max-time") + 1], "60")
|
|
self.assertIn("--referer", command)
|
|
self.assertEqual(command[command.index("--referer") + 1], "https://www.lost112.go.kr/")
|
|
self.assertIn("--output", command)
|
|
self.assertEqual(command[command.index("--output") + 1], "lost112-search-result.html")
|
|
self.assertIn("SITE=V", " ".join(command))
|
|
self.assertEqual(command[-1], LOST112_LIST_URL)
|
|
|
|
def test_blank_station_is_rejected(self):
|
|
with self.assertRaisesRegex(ValueError, "station"):
|
|
build_search_plan(station=" ")
|
|
|
|
|
|
class SubwayLostPropertyProbeTest(unittest.TestCase):
|
|
def test_probe_source_marks_successful_fetch_as_reachable(self):
|
|
runner = mock.Mock(return_value=mock.Mock(returncode=0, stdout="<html></html>", stderr=""))
|
|
|
|
status = probe_source("LOST112", LOST112_LIST_URL, runner=runner)
|
|
|
|
self.assertEqual(status["status"], "reachable")
|
|
command = runner.call_args.args[0]
|
|
self.assertEqual(command[0], "curl")
|
|
self.assertIn("--http1.1", command)
|
|
self.assertEqual(command[command.index("--tls-max") + 1], "1.2")
|
|
self.assertEqual(command[command.index("--max-time") + 1], "15")
|
|
self.assertEqual(command[-1], LOST112_LIST_URL)
|
|
|
|
def test_probe_source_marks_timeouts_cleanly(self):
|
|
runner = mock.Mock(side_effect=__import__("subprocess").CalledProcessError(28, ["curl"], stderr="Operation timed out"))
|
|
|
|
status = probe_source("서울교통공사", SEOUL_METRO_LOST_CENTER_URL, runner=runner)
|
|
|
|
self.assertEqual(status["status"], "timeout")
|
|
self.assertIn("timed out", status["detail"].lower())
|
|
|
|
|
|
class SubwayLostPropertyCliShapeTest(unittest.TestCase):
|
|
def test_cli_prints_json_plan(self):
|
|
stdout = io.StringIO()
|
|
with contextlib.redirect_stdout(stdout):
|
|
main(["--station", "강남역", "--item", "지갑", "--days", "14"])
|
|
|
|
payload = json.loads(stdout.getvalue())
|
|
self.assertEqual(payload["query"]["station"], "강남역")
|
|
self.assertEqual(payload["payload"]["SITE"], "V")
|
|
self.assertIn("curl", payload["curl_example"])
|
|
self.assertEqual(payload["official_sources"][0]["url"], LOST112_LIST_URL)
|
|
|
|
def test_helper_scripts_are_executable_python_entrypoints(self):
|
|
repo_root = Path(__file__).resolve().parent.parent
|
|
for helper in (
|
|
repo_root / "scripts" / "subway_lost_property.py",
|
|
repo_root / "subway-lost-property" / "scripts" / "subway_lost_property.py",
|
|
):
|
|
with self.subTest(helper=helper):
|
|
self.assertTrue(os.access(helper, os.X_OK), f"{helper} should be executable")
|
|
self.assertTrue(
|
|
helper.read_text(encoding="utf-8").startswith("#!/usr/bin/env python3\n"),
|
|
f"{helper} should start with a Python shebang",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|