"""Offline tests for the paper-lookup scripts.
No network. Every fixture is a trimmed copy of a real response captured on
2026-07-27, because the behavior under test *is* the shape of these payloads --
a synthetic JATS document with a tidy `
` would not exercise the case this
skill exists to catch.
One exception, flagged rather than hidden: `arxiv_error.xml` is reconstructed
from a verified live response rather than saved from one. Its `totalResults`, its
`Error`, and its `start must be an integer` summary were all
observed from the real API, but arXiv penalizes repeated malformed requests
harder than valid ones (see references/arxiv.md) and stayed throttled for the
rest of the session, so the capture could not be repeated to save the bytes.
The three exit codes are the point of the suite:
jats_to_text.py -> 2 when a document has no
arxiv_atom.py -> 3 on arXiv's HTTP-200 error feed
paginate.py -> 4 when a walk ends short of the reported total
Each is a failure the upstream API reports as success.
"""
from __future__ import annotations
import json
import subprocess
import sys
import unittest
from pathlib import Path
sys.dont_write_bytecode = True
SKILL_ROOT = Path(__file__).resolve().parents[2] / "skills" / "paper-lookup"
SCRIPTS = SKILL_ROOT / "scripts"
FIXTURES = Path(__file__).resolve().parent / "fixtures"
sys.path.insert(0, str(SCRIPTS))
import skill_contract # noqa: E402
import _common # noqa: E402
import arxiv_atom # noqa: E402
import jats_to_text # noqa: E402
import openalex_abstract # noqa: E402
import paginate # noqa: E402
CliHelpTests = skill_contract.cli.help_test_case(SKILL_ROOT)
def run_script(name: str, *args: str) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, str(SCRIPTS / name), *args],
capture_output=True,
text=True,
timeout=60,
cwd=SCRIPTS,
)
def fixture(name: str) -> str:
return str(FIXTURES / name)
class CommonTests(unittest.TestCase):
def test_collapse_ws_flattens_arxiv_hard_wrapping(self) -> None:
wrapped = "The dominant sequence\n transduction models\tare based on"
self.assertEqual(
_common.collapse_ws(wrapped), "The dominant sequence transduction models are based on"
)
def test_collapse_ws_handles_none(self) -> None:
self.assertEqual(_common.collapse_ws(None), "")
def test_strip_control_keeps_tab_and_newline(self) -> None:
self.assertEqual(_common.strip_control("a\x00b\tc\nd\x07"), "ab\tc\nd")
def test_read_input_rejects_oversized_file(self) -> None:
with self.assertRaises(_common.InputError):
_common.read_input(fixture("openalex_work.json"), max_bytes=10)
def test_read_input_rejects_missing_file(self) -> None:
with self.assertRaises(_common.InputError):
_common.read_input(str(FIXTURES / "does-not-exist.json"))
def test_load_json_attributes_the_parse_failure_to_the_source(self) -> None:
with self.assertRaises(_common.InputError) as caught:
_common.load_json(fixture("jats_no_body.xml"))
self.assertIn("jats_no_body.xml", str(caught.exception))
class ReconciliationTests(unittest.TestCase):
"""The three outcomes must stay distinguishable.
Collapsing "the caller set a bound" into "records are missing" is what makes
a bounded search look broken; collapsing it the other way is what makes a
broken search look bounded.
"""
def test_complete_walk(self) -> None:
record = _common.Reconciliation(expected=360, retrieved=360, pages=12)
self.assertTrue(record.complete)
self.assertTrue(record.ok)
self.assertNotIn("shortfall", record.as_dict())
def test_bound_is_explained_not_a_failure(self) -> None:
record = _common.Reconciliation(
expected=697030, retrieved=100, pages=2, stopped_at_limit=True
)
self.assertFalse(record.complete)
self.assertTrue(record.ok)
summary = record.as_dict()
self.assertEqual(summary["shortfall"], 696930)
self.assertIn("max-records", summary["shortfall_reason"])
def test_unexplained_shortfall_is_not_ok(self) -> None:
record = _common.Reconciliation(expected=360, retrieved=120, pages=4)
self.assertFalse(record.ok)
self.assertIn("UNEXPLAINED", record.as_dict()["shortfall_reason"])
def test_absent_total_is_a_documented_state(self) -> None:
record = _common.Reconciliation(expected=None, retrieved=5, pages=1)
self.assertTrue(record.ok)
self.assertIn("expected_total_note", record.as_dict())
class RedactionTests(unittest.TestCase):
"""Credentials must never reach the provenance output.
OpenAlex and Crossref authenticate by query string, so the URL that was
actually fetched carries the secret -- and provenance is printed.
"""
def test_api_key_value_is_replaced_but_the_parameter_survives(self) -> None:
redacted = _common.redact_url("https://api.openalex.org/works?cursor=*&api_key=SECRET")
self.assertNotIn("SECRET", redacted)
self.assertIn("api_key=REDACTED", redacted)
self.assertIn("cursor=", redacted)
def test_contact_details_are_redacted_too(self) -> None:
for param in ("mailto", "email", "tool"):
with self.subTest(param=param):
redacted = _common.redact_url(f"https://api.crossref.org/works?{param}=me@x.com")
self.assertNotIn("me@x.com", redacted)
def test_redaction_is_case_insensitive_on_the_parameter_name(self) -> None:
self.assertNotIn("SECRET", _common.redact_url("https://x.test/?API_KEY=SECRET"))
def test_urls_without_a_query_are_untouched(self) -> None:
url = "https://api.biorxiv.org/details/biorxiv/2024-01-01/2024-01-03/0/json"
self.assertEqual(_common.redact_url(url), url)
def test_dry_run_output_carries_no_key(self) -> None:
import os
environment = {**os.environ, "OPENALEX_API_KEY": "SECRET_TEST_KEY"}
result = subprocess.run(
[
sys.executable,
str(SCRIPTS / "paginate.py"),
"--api",
"openalex",
"--query",
"search=crispr",
"--dry-run",
],
capture_output=True,
text=True,
timeout=60,
cwd=SCRIPTS,
env=environment,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertNotIn("SECRET_TEST_KEY", result.stdout)
self.assertIn("api_key=REDACTED", result.stdout)
class JatsTests(unittest.TestCase):
def test_full_text_article_yields_sections(self) -> None:
result = run_script("jats_to_text.py", fixture("jats_with_body.xml"))
self.assertEqual(result.returncode, 0, result.stderr)
payload = json.loads(result.stdout)
self.assertTrue(payload["full_text_available"])
self.assertEqual(payload["metadata"]["pmcid"], "PMC7029759")
self.assertEqual(payload["metadata"]["pmid"], "32117569")
self.assertGreater(payload["section_count"], 0)
self.assertGreater(payload["word_count"], 0)
def test_first_article_id_wins_over_nested_sub_article_doi(self) -> None:
"""F1000Research-style peer-review sub-articles carry their own DOIs."""
result = run_script("jats_to_text.py", fixture("jats_with_body.xml"))
payload = json.loads(result.stdout)
self.assertEqual(payload["metadata"]["doi"], "10.12688/f1000research.22211.2")
def test_missing_body_exits_2_and_surfaces_the_xml_comment(self) -> None:
"""The whole reason this script exists: eFetch returns 200 for this."""
result = run_script("jats_to_text.py", fixture("jats_no_body.xml"))
self.assertEqual(result.returncode, 2, result.stdout)
payload = json.loads(result.stdout)
self.assertFalse(payload["full_text_available"])
self.assertIn("does not allow downloading", payload["reason"])
# The reason exists only as an XML comment, which ElementTree discards.
self.assertTrue(payload["xml_comments"])
self.assertIn("Europe PMC", payload["guidance"])
def test_metadata_still_emitted_when_body_is_missing(self) -> None:
result = run_script("jats_to_text.py", fixture("jats_no_body.xml"))
payload = json.loads(result.stdout)
self.assertEqual(payload["metadata"]["journal"], "British Medical Journal (Clinical research ed.)")
def test_allow_metadata_only_opts_out_of_the_refusal(self) -> None:
result = run_script("jats_to_text.py", fixture("jats_no_body.xml"), "--allow-metadata-only")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertFalse(json.loads(result.stdout)["full_text_available"])
def test_europepmc_bare_article_wrapper_is_accepted(self) -> None:
"""eFetch wraps in ; Europe PMC returns a bare ."""
result = run_script("jats_to_text.py", fixture("jats_europepmc.xml"))
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue(json.loads(result.stdout)["full_text_available"])
def test_section_filter_matches_case_insensitively(self) -> None:
result = run_script("jats_to_text.py", fixture("jats_with_body.xml"), "--sections", "methods")
self.assertEqual(result.returncode, 0, result.stderr)
payload = json.loads(result.stdout)
self.assertEqual(payload["section_count"], 1)
def test_unmatched_section_filter_lists_what_is_available(self) -> None:
result = run_script("jats_to_text.py", fixture("jats_with_body.xml"), "--sections", "nope")
self.assertEqual(result.returncode, 1)
self.assertIn("available:", result.stderr)
def test_non_jats_xml_is_rejected_with_a_useful_message(self) -> None:
result = run_script("jats_to_text.py", fixture("not_jats.xml"))
self.assertEqual(result.returncode, 1)
self.assertIn("no element", result.stderr)
def test_xref_text_is_dropped_but_the_sentence_survives(self) -> None:
"""Citation markers must not land mid-sentence, and the tail must stay."""
body = jats_to_text.parse(
"Results"
"
Growth increased12 after treatment.
"
""
)
section = jats_to_text.collect_sections(body)[0]
self.assertEqual(section["text"], "Results Growth increased after treatment.")
class ArxivTests(unittest.TestCase):
def test_entry_is_parsed_with_version_split(self) -> None:
result = run_script("arxiv_atom.py", fixture("arxiv_feed.xml"))
self.assertEqual(result.returncode, 0, result.stderr)
payload = json.loads(result.stdout)
entry = payload["entries"][0]
self.assertEqual(entry["arxiv_id"], "1706.03762")
self.assertEqual(entry["arxiv_id_versioned"], "1706.03762v7")
self.assertEqual(entry["version"], "7")
def test_pdf_link_is_selected_by_rel_not_position(self) -> None:
"""The feed's own precedes the entries and must not be picked up."""
result = run_script("arxiv_atom.py", fixture("arxiv_feed.xml"))
entry = json.loads(result.stdout)["entries"][0]
self.assertEqual(entry["pdf_url"], "https://arxiv.org/pdf/1706.03762v7")
self.assertEqual(entry["abstract_url"], "https://arxiv.org/abs/1706.03762v7")
self.assertNotIn("api/query", entry["pdf_url"])
def test_hard_wrapped_title_and_abstract_are_collapsed(self) -> None:
result = run_script("arxiv_atom.py", fixture("arxiv_feed.xml"))
entry = json.loads(result.stdout)["entries"][0]
self.assertEqual(entry["title"], "Attention Is All You Need")
self.assertNotIn("\n", entry["abstract"])
def test_absent_arxiv_doi_is_none_not_invented(self) -> None:
result = run_script("arxiv_atom.py", fixture("arxiv_feed.xml"))
entry = json.loads(result.stdout)["entries"][0]
self.assertIsNone(entry["doi"])
def test_error_feed_exits_3(self) -> None:
"""arXiv sends this with HTTP 200 and totalResults 1."""
result = run_script("arxiv_atom.py", fixture("arxiv_error.xml"))
self.assertEqual(result.returncode, 3, result.stdout)
self.assertIn("start must be an integer", result.stderr)
def test_rate_limited_plain_text_body_exits_5(self) -> None:
"""arXiv answers a throttled caller with 14 bytes of plain text, not XML."""
result = run_script("arxiv_atom.py", fixture("arxiv_rate_limited.txt"))
self.assertEqual(result.returncode, 5, result.stdout)
self.assertIn("throttling", result.stderr)
self.assertIn("three seconds", result.stderr)
def test_non_xml_body_reports_what_it_actually_got(self) -> None:
result = run_script("arxiv_atom.py", fixture("openalex_work.json"))
self.assertEqual(result.returncode, 1)
self.assertIn("first 100 bytes", result.stderr)
def test_empty_feed_is_reported_as_a_genuine_no_match(self) -> None:
result = run_script("arxiv_atom.py", fixture("arxiv_empty.xml"))
self.assertEqual(result.returncode, 0, result.stderr)
payload = json.loads(result.stdout)
self.assertEqual(payload["total_results"], 0)
self.assertEqual(payload["returned"], 0)
self.assertIn("not found in arXiv", payload["note"])
def test_query_as_executed_is_reported(self) -> None:
"""An unknown field prefix is silently rewritten to `all:` upstream."""
result = run_script("arxiv_atom.py", fixture("arxiv_rewritten_query.xml"))
payload = json.loads(result.stdout)
self.assertIn("all:badfield:xyz", payload["query_as_executed"])
def test_ids_only_strips_versions(self) -> None:
result = run_script("arxiv_atom.py", fixture("arxiv_feed.xml"), "--ids-only")
self.assertEqual(result.stdout.strip(), "1706.03762")
def test_split_version_leaves_unversioned_ids_alone(self) -> None:
self.assertEqual(arxiv_atom.split_version("2103.15348"), ("2103.15348", None))
self.assertEqual(arxiv_atom.split_version("2103.15348v12"), ("2103.15348", "12"))
# Old-style IDs contain letters and a slash but no version.
self.assertEqual(arxiv_atom.split_version("hep-th/9901001"), ("hep-th/9901001", None))
def test_id_from_url_is_scheme_agnostic(self) -> None:
self.assertEqual(arxiv_atom.id_from_url("http://arxiv.org/abs/1706.03762v7"), "1706.03762v7")
self.assertEqual(arxiv_atom.id_from_url("https://arxiv.org/abs/1706.03762v7"), "1706.03762v7")
class OpenAlexAbstractTests(unittest.TestCase):
def test_abstract_is_reconstructed_in_position_order(self) -> None:
text, anomalies = openalex_abstract.reconstruct(
{"Despite": [0], "growing": [1], "interest": [2], "in": [3], "OA": [4]}
)
self.assertEqual(text, "Despite growing interest in OA")
self.assertEqual(anomalies, [])
def test_duplicate_positions_keep_both_tokens(self) -> None:
"""The naive {position: word} inversion silently drops one of these."""
text, anomalies = openalex_abstract.reconstruct({"alpha": [0], "beta": [0], "gamma": [1]})
self.assertIn("alpha", text)
self.assertIn("beta", text)
self.assertIn("gamma", text)
self.assertTrue(any("more than one token" in note for note in anomalies))
def test_gaps_in_the_index_are_reported(self) -> None:
_, anomalies = openalex_abstract.reconstruct({"first": [0], "last": [5]})
self.assertTrue(any("absent from the index" in note for note in anomalies))
def test_non_integer_positions_are_reported_not_crashed_on(self) -> None:
text, anomalies = openalex_abstract.reconstruct({"word": ["x"], "real": [0]})
self.assertEqual(text, "real")
self.assertTrue(any("non-integer position" in note for note in anomalies))
def test_empty_index_yields_empty_text(self) -> None:
self.assertEqual(openalex_abstract.reconstruct({}), ("", []))
def test_single_work_payload(self) -> None:
result = run_script("openalex_abstract.py", fixture("openalex_work.json"))
self.assertEqual(result.returncode, 0, result.stderr)
payload = json.loads(result.stdout)
self.assertEqual(payload["count"], 1)
self.assertEqual(payload["with_abstract"], 1)
self.assertIn("Open Access", payload["works"][0]["abstract"])
def test_list_response_payload(self) -> None:
result = run_script("openalex_abstract.py", fixture("openalex_list.json"))
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads(result.stdout)["count"], 2)
def test_missing_index_distinguishes_no_abstract_from_select_exclusion(self) -> None:
result = run_script("openalex_abstract.py", fixture("openalex_no_abstract.json"))
self.assertEqual(result.returncode, 0, result.stderr)
work = json.loads(result.stdout)["works"][0]
self.assertIsNone(work["abstract"])
self.assertIn("`select=`", work["abstract_warnings"][0])
def test_text_only_fails_loudly_when_nothing_reconstructs(self) -> None:
result = run_script("openalex_abstract.py", fixture("openalex_no_abstract.json"), "--text-only")
self.assertEqual(result.returncode, 1)
self.assertIn("no abstracts", result.stderr)
def test_works_from_rejects_a_bare_scalar(self) -> None:
with self.assertRaises(_common.InputError):
openalex_abstract.works_from("not a work")
class PaginateAdapterTests(unittest.TestCase):
"""Adapter unit tests. The network walk is covered by manual verification;
what matters here is the arithmetic that made the walk wrong before."""
def test_biorxiv_steps_by_the_reported_count_not_100(self) -> None:
payload = {
"messages": [{"status": "ok", "cursor": 0, "count": 30, "total": "360"}],
"collection": [{"doi": f"10.1101/x{i}"} for i in range(30)],
}
page = paginate._rxiv_parse(payload, 0)
self.assertEqual(page.total, 360)
self.assertEqual(page.next_state, 30, "stepping by 100 would skip records 30-99")
self.assertEqual(len(page.records), 30)
def test_biorxiv_pubs_steps_by_100(self) -> None:
payload = {
"messages": [{"status": "ok", "cursor": 0, "count": 100, "total": "124"}],
"collection": [{"preprint_doi": f"10.1101/x{i}"} for i in range(100)],
}
self.assertEqual(paginate._rxiv_parse(payload, 0).next_state, 100)
def test_biorxiv_stops_when_the_next_offset_reaches_total(self) -> None:
payload = {
"messages": [{"status": "ok", "cursor": 330, "count": 30, "total": "360"}],
"collection": [{"doi": f"10.1101/x{i}"} for i in range(30)],
}
self.assertIsNone(paginate._rxiv_parse(payload, 330).next_state)
def test_biorxiv_reports_the_count_mismatch_it_falls_back_from(self) -> None:
payload = {
"messages": [{"status": "ok", "cursor": 0, "count": 30, "total": "360"}],
"collection": [{"doi": "10.1101/x"}],
}
page = paginate._rxiv_parse(payload, 0)
self.assertEqual(page.next_state, 1)
self.assertTrue(any("but returned" in note for note in page.notes or []))
def test_biorxiv_no_articles_found_is_surfaced(self) -> None:
"""HTTP 200 with an empty collection, indistinguishable without status."""
page = paginate._rxiv_parse(
{"messages": [{"status": "no articles found"}], "collection": []}, 0
)
self.assertEqual(page.records, [])
self.assertIsNone(page.next_state)
self.assertTrue(any("no articles found" in note for note in page.notes or []))
def test_biorxiv_endpoint_without_counts_reports_no_total(self) -> None:
"""DOI and N-most-recent lookups omit count/total entirely."""
page = paginate._rxiv_parse(
{"messages": [{"status": "ok", "category": "all"}], "collection": [{"doi": "10.1101/x"}]},
0,
)
self.assertIsNone(page.total)
self.assertEqual(page.next_state, 1)
def test_biorxiv_url_selects_details_or_pubs(self) -> None:
build = paginate._rxiv_url("biorxiv")
self.assertIn("/details/biorxiv/2024-01-01/2024-01-03/0/json", build("2024-01-01/2024-01-03", 0, 100))
self.assertIn("/pubs/biorxiv/2024-01-01/2024-01-03/30/json", build("pubs:2024-01-01/2024-01-03", 30, 100))
def test_medrxiv_never_uses_the_api_medrxiv_host(self) -> None:
url = paginate._rxiv_url("medrxiv")("2024-01-01/2024-01-03", 0, 100)
self.assertIn("api.biorxiv.org", url)
self.assertNotIn("api.medrxiv.org", url)
def test_europepmc_stops_on_an_echoed_cursor(self) -> None:
"""There is no null terminator: exhaustion echoes your own cursor back."""
page = paginate._europepmc_parse(
{"hitCount": 19, "nextCursorMark": "ABC", "resultList": {"result": []}}, "ABC"
)
self.assertIsNone(page.next_state)
self.assertEqual(page.total, 19)
def test_europepmc_continues_on_a_new_cursor(self) -> None:
page = paginate._europepmc_parse(
{"hitCount": 19, "nextCursorMark": "DEF", "resultList": {"result": [{"id": "1"}]}}, "ABC"
)
self.assertEqual(page.next_state, "DEF")
def test_europepmc_errcode_in_a_200_body_raises(self) -> None:
with self.assertRaises(RuntimeError) as caught:
paginate._europepmc_parse(
{"errCode": 404, "errMsg": "Invalid page size provided"}, "*"
)
self.assertIn("Invalid page size", str(caught.exception))
def test_europepmc_reports_the_query_as_parsed(self) -> None:
page = paginate._europepmc_parse(
{
"hitCount": 1,
"nextCursorMark": "B",
"request": {"queryString": 'SRC:"PPR"'},
"resultList": {"result": [{"id": "PPR1"}]},
},
"*",
)
self.assertTrue(any("as parsed" in note for note in page.notes or []))
def test_openalex_stops_on_an_empty_page(self) -> None:
page = paginate._openalex_parse({"meta": {"count": 5, "next_cursor": "X"}, "results": []}, "*")
self.assertIsNone(page.next_state)
def test_openalex_surfaces_the_reported_cost(self) -> None:
page = paginate._openalex_parse(
{"meta": {"count": 5, "next_cursor": "X", "cost_usd": 0.001}, "results": [{"id": "W1"}]},
"*",
)
self.assertTrue(any("cost_usd" in note for note in page.notes or []))
def test_crossref_stops_on_an_empty_page(self) -> None:
page = paginate._crossref_parse(
{"message": {"total-results": 7, "next-cursor": "X", "items": []}}, "*"
)
self.assertIsNone(page.next_state)
self.assertEqual(page.total, 7)
def test_every_api_declares_a_delay_and_a_query_format(self) -> None:
for name, api in paginate.APIS.items():
with self.subTest(api=name):
self.assertGreater(api.delay, 0, "an API with no delay would be parallelized by accident")
self.assertTrue(api.note)
def test_dry_run_makes_no_request(self) -> None:
result = run_script(
"paginate.py", "--api", "biorxiv", "--query", "2024-01-01/2024-01-03", "--dry-run"
)
self.assertEqual(result.returncode, 0, result.stderr)
payload = json.loads(result.stdout)
self.assertTrue(payload["first_url"].startswith("https://api.biorxiv.org/details/"))
def test_list_apis_needs_no_other_argument(self) -> None:
result = run_script("paginate.py", "--list-apis")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(sorted(json.loads(result.stdout)), sorted(paginate.APIS))
def test_missing_required_arguments_exit_2(self) -> None:
result = run_script("paginate.py", "--api", "biorxiv")
self.assertEqual(result.returncode, 2)
self.assertIn("--list-apis", result.stderr)
def test_nonpositive_bounds_are_rejected(self) -> None:
for flag in ("--max-records", "--max-calls", "--page-size"):
with self.subTest(flag=flag):
result = run_script(
"paginate.py", "--api", "biorxiv", "--query", "5", flag, "0", "--dry-run"
)
self.assertEqual(result.returncode, 1)
class StructureTests(unittest.TestCase):
"""Repo rules that apply to this skill specifically."""
def test_frontmatter_conforms(self) -> None:
self.assertEqual(skill_contract.structure.frontmatter_problems(SKILL_ROOT), [])
def test_skill_md_is_within_the_line_budget(self) -> None:
self.assertEqual(skill_contract.structure.length_problems(SKILL_ROOT), [])
def test_no_stray_tests_under_the_skill(self) -> None:
self.assertEqual(skill_contract.structure.stray_test_problems(SKILL_ROOT), [])
def test_no_bytecode_shipped(self) -> None:
"""Running the scripts by hand during development leaves __pycache__ behind."""
self.assertEqual(skill_contract.structure.bytecode_problems(SKILL_ROOT), [])
def test_referenced_paths_exist(self) -> None:
self.assertEqual(skill_contract.structure.link_problems(SKILL_ROOT), [])
def test_scripts_compile(self) -> None:
self.assertEqual(skill_contract.structure.compile_problems(SKILL_ROOT), [])
def test_no_dynamic_execution(self) -> None:
self.assertEqual(skill_contract.structure.dynamic_execution_problems(SKILL_ROOT), [])
def test_no_leftover_tool_call_markup(self) -> None:
"""A previous release shipped a stray at EOF."""
for path in sorted(SKILL_ROOT.rglob("*.md")):
with self.subTest(path=path.name):
text = path.read_text(encoding="utf-8")
for marker in ("", "", ""):
self.assertNotIn(marker, text)
def test_every_reference_file_is_linked_from_skill_md(self) -> None:
skill_md = (SKILL_ROOT / "SKILL.md").read_text(encoding="utf-8")
for reference in sorted((SKILL_ROOT / "references").glob("*.md")):
with self.subTest(reference=reference.name):
self.assertIn(f"references/{reference.name}", skill_md)
def test_every_script_is_documented_in_skill_md(self) -> None:
skill_md = (SKILL_ROOT / "SKILL.md").read_text(encoding="utf-8")
for script in sorted(SCRIPTS.glob("*.py")):
if script.name == "_common.py":
continue
with self.subTest(script=script.name):
self.assertIn(f"scripts/{script.name}", skill_md)
def test_paginate_covers_every_api_it_advertises(self) -> None:
skill_md = (SKILL_ROOT / "SKILL.md").read_text(encoding="utf-8")
for name in paginate.APIS:
with self.subTest(api=name):
self.assertIn(name, skill_md.lower())
if __name__ == "__main__":
unittest.main()