diff --git a/tests/test_build_explainers.py b/tests/test_build_explainers.py
index 557319a..a41309c 100644
--- a/tests/test_build_explainers.py
+++ b/tests/test_build_explainers.py
@@ -149,6 +149,53 @@ def test_render_markdown_offsets_heading_levels_by_one():
assert '
Deep
' in html # h6 + 1 stays h6, not h7
+def test_render_markdown_flushes_lists_quotes_rules_and_code():
+ script = importlib.import_module("scripts.build_explainers")
+ source = (
+ "Paragraph before a list.\n\n"
+ "- first item\n- second item\n\n"
+ "1. numbered item\n\n"
+ "> quoted line\n\n"
+ "---\n\n"
+ "```python\nprint('ok')\n```\n"
+ )
+
+ html = script.render_markdown(source, set())
+
+ assert "Paragraph before a list.
" in html
+ assert "" in html
+ assert "- numbered item
" in html
+ assert "quoted line
" in html
+ assert "
" in html
+ assert 'print('ok')
' in html
+
+
+def test_resolve_link_target_handles_unknown_markdown_and_project_paths():
+ script = importlib.import_module("scripts.build_explainers")
+
+ assert script.resolve_link_target("unknown-topic.md?view=full#intro", {"known-topic"}) == "unknown-topic.html?view=full#intro"
+ assert script.resolve_link_target("../Benefits%20Denial/", set()) == "../index.html#project-benefits"
+ assert script.resolve_link_target("../notebooks/example.ipynb", set()) == "../notebooks/example.ipynb"
+
+
+def test_faq_skips_nonparagraph_content_and_truncates_long_why_answer():
+ script = importlib.import_module("scripts.build_explainers")
+ why = "First sentence. " + ("additional context " * 40)
+ markdown = (
+ "## Definition\n\n| Term | Meaning |\n| --- | --- |\n\n"
+ "A definition paragraph.\n\n"
+ "## Why It Matters\n\n- a list item\n\n"
+ f"{why}\n\n## Example\n\nLater text.\n"
+ )
+
+ pairs = script._faq(_entry("sample", "Sample Topic"), markdown)
+
+ assert pairs[0] == ("What is Sample Topic?", "A definition paragraph.")
+ assert pairs[1][0] == "Why does this matter for fairness?"
+ assert len(pairs[1][1]) <= 501
+ assert pairs[1][1].startswith("First sentence.")
+
+
def test_seo_title_uses_the_short_title_and_rejects_a_long_one():
# #1105: a over 65 characters is cut in search results.
script = importlib.import_module("scripts.build_explainers")
@@ -349,6 +396,15 @@ def test_the_build_refuses_a_missing_or_empty_og_image(site):
script.main()
+def test_the_build_rejects_a_data_entry_without_a_slug(site):
+ script, root, entries = site
+ del entries[0]["slug"]
+ (root / "assets" / "explainers-data.json").write_text(json.dumps(entries), encoding="utf-8")
+
+ with pytest.raises(KeyError, match="slug"):
+ script.main()
+
+
def test_the_mirror_drops_a_stale_explainer(site):
script, root, _ = site
stale = root / "faircode" / "_explainers" / "removed-topic.md"
diff --git a/tests/test_check_issue_refs.py b/tests/test_check_issue_refs.py
index 05fc53e..3a11dce 100644
--- a/tests/test_check_issue_refs.py
+++ b/tests/test_check_issue_refs.py
@@ -116,6 +116,51 @@ def test_the_closes_of_a_pull_request_come_from_the_event_payload(tmp_path):
assert script.pull_request_closes({"GITHUB_EVENT_NAME": "pull_request", "GITHUB_EVENT_PATH": str(tmp_path / "missing.json")}) == set()
+def test_a_malformed_pull_request_event_is_ignored(tmp_path):
+ payload = tmp_path / "event.json"
+ payload.write_text("not json", encoding="utf-8")
+ assert script.pull_request_closes({"GITHUB_EVENT_NAME": "pull_request", "GITHUB_EVENT_PATH": str(payload)}) == set()
+
+
+def test_load_states_skips_when_gh_is_missing_or_the_issue_query_fails(monkeypatch):
+ monkeypatch.setattr(script.shutil, "which", lambda _name: None)
+ assert script.load_states(None) is None
+
+ class Failed:
+ returncode = 1
+
+ monkeypatch.setattr(script.shutil, "which", lambda _name: "/usr/bin/gh")
+ monkeypatch.setattr(script.subprocess, "run", lambda *args, **kwargs: Failed())
+ assert script.load_states(None) is None
+
+
+def test_load_states_merges_pull_requests_when_available(monkeypatch):
+ class Result:
+ def __init__(self, returncode, stdout):
+ self.returncode = returncode
+ self.stdout = stdout
+
+ results = iter((
+ Result(0, '[{"number": 1, "state": "OPEN"}]'),
+ Result(0, '[{"number": 2, "state": "MERGED"}]'),
+ ))
+ monkeypatch.setattr(script.shutil, "which", lambda _name: "/usr/bin/gh")
+ monkeypatch.setattr(script.subprocess, "run", lambda *args, **kwargs: next(results))
+ assert script.load_states(None) == {1: "open", 2: "merged"}
+
+
+def test_load_states_keeps_issue_states_when_the_pull_request_query_fails(monkeypatch):
+ class Result:
+ def __init__(self, returncode, stdout):
+ self.returncode = returncode
+ self.stdout = stdout
+
+ results = iter((Result(0, '[{"number": 1, "state": "CLOSED"}]'), Result(1, "")))
+ monkeypatch.setattr(script.shutil, "which", lambda _name: "/usr/bin/gh")
+ monkeypatch.setattr(script.subprocess, "run", lambda *args, **kwargs: next(results))
+ assert script.load_states(None) == {1: "closed"}
+
+
def _docs(tmp_path, monkeypatch):
issues = tmp_path / "issues.json"
issues.write_text(json.dumps([{"number": n, "state": s.upper()} for n, s in STATES.items()]))