mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Issue #108 needs a dedicated read-only skill that can browse, search, and inspect GeekNews posts using the public feed alone. The implementation adds a fixture-backed Atom helper, skill/docs surfaces, and install/README wiring, then verifies the helper against the live GeekNews feed. Constraint: Must stay RSS-first and avoid new dependencies or unofficial APIs Constraint: Skill development requires syncing the skill into ~/.claude/skills and ~/.agents/skills during verification Rejected: Fetch article pages directly for v1 | expands scope beyond the approved RSS-driven workflow Rejected: Use XML parser modules | current python3 environment has expat issues, so regex + HTML parsing is safer here Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep the root helper and geeknews-search/scripts copy behaviorally identical because the installed skill must remain self-contained Tested: PYTHONPATH=.:scripts python3 -m unittest scripts.test_geeknews_search; node --test scripts/skill-docs.test.js; python3 scripts/geeknews_search.py list --limit 3; python3 scripts/geeknews_search.py search --query Claude --limit 3; python3 scripts/geeknews_search.py detail --id 28439; npm run ci Not-tested: Non-default feed mirrors or future Atom schema changes beyond the current public GeekNews feed shape Related: Issue #108
128 lines
5.4 KiB
Python
128 lines
5.4 KiB
Python
import contextlib
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import unittest
|
|
|
|
from scripts.geeknews_search import (
|
|
GeekNewsFeed,
|
|
build_detail_payload,
|
|
build_list_payload,
|
|
build_search_payload,
|
|
get_item_detail,
|
|
list_items,
|
|
load_feed,
|
|
main,
|
|
search_items,
|
|
)
|
|
|
|
FIXTURE_PATH = Path(__file__).resolve().parent / "fixtures" / "geeknews-feed.xml"
|
|
|
|
|
|
class GeekNewsFeedParseTest(unittest.TestCase):
|
|
def test_load_feed_parses_atom_entries_into_normalized_items(self):
|
|
feed = load_feed(FIXTURE_PATH.read_text(encoding="utf-8"))
|
|
|
|
self.assertIsInstance(feed, GeekNewsFeed)
|
|
self.assertEqual(feed.title, "GeekNews - 개발/기술/스타트업 뉴스 서비스")
|
|
self.assertEqual(feed.updated, "2026-04-12T22:53:56+09:00")
|
|
self.assertEqual(feed.home_url, "https://news.hada.io")
|
|
self.assertEqual(feed.feed_url, "https://news.hada.io/rss/news")
|
|
self.assertEqual(len(feed.items), 3)
|
|
|
|
first = feed.items[0]
|
|
self.assertEqual(first.title, "Ask GN: 기억이 안나는 웹사이트를 찾고 있습니다.")
|
|
self.assertEqual(first.link, "https://news.hada.io/topic?id=28441")
|
|
self.assertEqual(first.id, "https://news.hada.io/topic?id=28441")
|
|
self.assertEqual(first.author_name, "princox")
|
|
self.assertEqual(first.author_url, "https://news.hada.io/user/princox")
|
|
self.assertEqual(first.published, "2026-04-12T22:53:56+09:00")
|
|
self.assertIn("시각화 사이트", first.summary)
|
|
self.assertNotIn("<p>", first.summary)
|
|
self.assertIn("<p>", first.content_html)
|
|
|
|
def test_list_items_keeps_feed_order_and_applies_limit(self):
|
|
feed = load_feed(FIXTURE_PATH.read_text(encoding="utf-8"))
|
|
|
|
items = list_items(feed, limit=2)
|
|
|
|
self.assertEqual([item.id for item in items], [
|
|
"https://news.hada.io/topic?id=28441",
|
|
"https://news.hada.io/topic?id=28440",
|
|
])
|
|
|
|
def test_search_items_matches_title_summary_and_author_case_insensitively(self):
|
|
feed = load_feed(FIXTURE_PATH.read_text(encoding="utf-8"))
|
|
|
|
ai_matches = search_items(feed, query="agent", limit=5)
|
|
author_matches = search_items(feed, query="WORKDRIVER", limit=5)
|
|
|
|
self.assertEqual([item.id for item in ai_matches], ["https://news.hada.io/topic?id=28440"])
|
|
self.assertEqual([item.id for item in author_matches], ["https://news.hada.io/topic?id=28439"])
|
|
|
|
def test_get_item_detail_resolves_by_id_or_link_and_errors_cleanly(self):
|
|
feed = load_feed(FIXTURE_PATH.read_text(encoding="utf-8"))
|
|
|
|
item = get_item_detail(feed, "https://news.hada.io/topic?id=28439")
|
|
same_item = get_item_detail(feed, "28439")
|
|
|
|
self.assertEqual(item.title, "Show GN: [GN] 비개발자 + Claude로 프로덕션 운영 238일 — 무엇이 됐고 무엇이 안 됐나?")
|
|
self.assertEqual(same_item.id, item.id)
|
|
with self.assertRaisesRegex(LookupError, "No GeekNews entry matched"):
|
|
get_item_detail(feed, "missing-topic")
|
|
|
|
|
|
class GeekNewsPayloadShapeTest(unittest.TestCase):
|
|
def test_list_search_and_detail_payloads_have_stable_json_shape(self):
|
|
feed = load_feed(FIXTURE_PATH.read_text(encoding="utf-8"))
|
|
|
|
list_payload = build_list_payload(feed, limit=2)
|
|
search_payload = build_search_payload(feed, query="claude", limit=5)
|
|
detail_payload = build_detail_payload(feed, lookup="28439")
|
|
|
|
self.assertEqual(list_payload["source"]["title"], feed.title)
|
|
self.assertEqual(list_payload["count"], 2)
|
|
self.assertEqual(search_payload["query"], "claude")
|
|
self.assertEqual(search_payload["count"], 1)
|
|
self.assertEqual(detail_payload["item"]["id"], "https://news.hada.io/topic?id=28439")
|
|
self.assertIn("summary", detail_payload["item"])
|
|
self.assertIn("content_html", detail_payload["item"])
|
|
|
|
|
|
class GeekNewsCliShapeTest(unittest.TestCase):
|
|
def test_cli_prints_json_for_each_subcommand(self):
|
|
stdout = io.StringIO()
|
|
with contextlib.redirect_stdout(stdout):
|
|
main(["list", "--feed-file", str(FIXTURE_PATH), "--limit", "2"])
|
|
listed = json.loads(stdout.getvalue())
|
|
self.assertEqual(listed["count"], 2)
|
|
|
|
stdout = io.StringIO()
|
|
with contextlib.redirect_stdout(stdout):
|
|
main(["search", "--feed-file", str(FIXTURE_PATH), "--query", "claude"])
|
|
searched = json.loads(stdout.getvalue())
|
|
self.assertEqual(searched["count"], 1)
|
|
|
|
stdout = io.StringIO()
|
|
with contextlib.redirect_stdout(stdout):
|
|
main(["detail", "--feed-file", str(FIXTURE_PATH), "--id", "28439"])
|
|
detail = json.loads(stdout.getvalue())
|
|
self.assertEqual(detail["item"]["author_name"], "workdriver")
|
|
|
|
def test_helper_scripts_are_executable_python_entrypoints(self):
|
|
repo_root = Path(__file__).resolve().parent.parent
|
|
for helper in (
|
|
repo_root / "scripts" / "geeknews_search.py",
|
|
repo_root / "geeknews-search" / "scripts" / "geeknews_search.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()
|