`, which no page uses, and it
+ # passed only because the detector then matched the bare substring.
+ findings, tally = self.run_check(
+ {"a.md": 'See {{< relref "/operate/api#tag/Cluster/operation/x" >}}.\n'},
+ pages={"/operate/api":
+ '
'})
+ self.assertEqual(findings, [])
+ self.assertEqual(tally["unverifiable"], 1)
+
+ def test_archived_tree_is_not_scanned(self):
+ findings, _ = self.run_check(
+ {"operate/rs/7.4/old.md": "See [x](https://redis.io/docs/latest/develop/nope/).\n"})
+ self.assertEqual(findings, [])
+
+ def test_release_notes_are_not_scanned(self):
+ findings, _ = self.run_check(
+ {"operate/release-notes/x.md": "See [x](https://redis.io/docs/latest/develop/nope/).\n"})
+ self.assertEqual(findings, [])
+
+ # --- relref path shapes (regression: an earlier version saw only one) -----
+
+ def test_dot_relative_relref_is_checked(self):
+ findings, _ = self.run_check(
+ {"develop/a.md": 'See {{< relref "./thing#no-such-heading" >}}.\n'})
+ self.assertEqual(len(findings), 1, findings)
+ self.assertIn("no-such-heading", findings[0]["problem"])
+
+ def test_dot_relative_relref_valid_passes(self):
+ findings, tally = self.run_check(
+ {"develop/a.md": 'See {{< relref "./thing#real-heading" >}}.\n'})
+ self.assertEqual(findings, [])
+ self.assertEqual(tally["relref_ok"], 1)
+
+ def test_bare_relative_relref_is_checked(self):
+ findings, _ = self.run_check(
+ {"a.md": 'See {{< relref "develop/thing#no-such-heading" >}}.\n'})
+ self.assertEqual(len(findings), 1, findings)
+
+ def test_trailing_fragment_outside_shortcode_is_checked(self):
+ """{{< relref "/path" >}}#anchor puts the fragment outside the quotes."""
+ findings, _ = self.run_check(
+ {"a.md": 'See {{< relref "/develop/thing" >}}#no-such-heading here.\n'})
+ self.assertEqual(len(findings), 1, findings)
+ self.assertIn("no-such-heading", findings[0]["problem"])
+
+ def test_trailing_fragment_valid_passes_and_is_not_double_counted(self):
+ findings, tally = self.run_check(
+ {"a.md": 'See {{< relref "/develop/thing" >}}#real-heading here.\n'})
+ self.assertEqual(findings, [])
+ self.assertEqual(tally["relref_ok"], 1, "counted once, not twice")
+
+ # A leaf page beside a section _index, each with a *different* anchor, so these
+ # tests fail if the resolver picks the wrong one rather than passing by luck.
+ SAME_PAGE_TREE = {"/develop": '
S
',
+ "/develop/thing": '
P
'}
+
+ def test_same_page_relref_checks_the_current_page(self):
+ findings, tally = self.run_check(
+ {"develop/thing.md": 'See {{< relref "#page-only" >}}.\n'},
+ pages=self.SAME_PAGE_TREE)
+ self.assertEqual(findings, [], "own-page anchor must resolve")
+ self.assertEqual(tally["relref_ok"], 1)
+
+ def test_same_page_relref_does_not_check_the_section_index(self):
+ """The regression: an empty path resolved to the sibling section page."""
+ findings, _ = self.run_check(
+ {"develop/thing.md": 'See {{< relref "#section-only" >}}.\n'},
+ pages=self.SAME_PAGE_TREE)
+ self.assertEqual(len(findings), 1, "section's anchor is not on this page")
+
+ def test_same_page_relref_from_an_index_page(self):
+ """_index.md publishes at its directory, so its own page is that directory."""
+ findings, tally = self.run_check(
+ {"develop/_index.md": 'See {{< relref "#section-only" >}}.\n'},
+ pages=self.SAME_PAGE_TREE)
+ self.assertEqual(findings, [])
+ self.assertEqual(tally["relref_ok"], 1)
+
+ def test_unresolvable_relative_path_is_unhandled_not_a_finding(self):
+ """A bad relref *path* fails the build, so non-resolution is our limit."""
+ findings, tally = self.run_check(
+ {"a.md": 'See {{< relref "some/unknown/shape#anchor" >}}.\n'})
+ self.assertEqual(findings, [])
+ self.assertEqual(tally["relref_unhandled"], 1)
+
+ # --- .md paths, which Hugo accepts as page references ----------------------
+
+ def test_md_suffix_relref_is_checked(self):
+ findings, _ = self.run_check(
+ {"a.md": 'See {{< relref "/develop/thing.md#no-such-heading" >}}.\n'})
+ self.assertEqual(len(findings), 1, findings)
+
+ def test_md_suffix_relref_valid_passes(self):
+ findings, tally = self.run_check(
+ {"a.md": 'See {{< relref "/develop/thing.md#real-heading" >}}.\n'})
+ self.assertEqual(findings, [])
+ self.assertEqual(tally["relref_ok"], 1)
+
+ def test_index_md_suffix_resolves_to_its_directory(self):
+ findings, tally = self.run_check(
+ {"a.md": 'See {{< relref "/develop/thing/_index.md#real-heading" >}}.\n'},
+ pages={"/develop/thing": PAGE})
+ self.assertEqual(findings, [])
+ self.assertEqual(tally["relref_ok"], 1)
+
+ def test_self_link_to_a_literal_md_file_keeps_its_extension(self):
+ """Regression guard: stripping .md must not break the published .md twins."""
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ content, public = build_tree(
+ root, {"/develop/thing": PAGE},
+ {"a.md": "[x](https://redis.io/docs/latest/develop/thing/index.html.md)\n"})
+ (public / "develop/thing/index.html.md").write_text("# t", encoding="utf-8")
+ findings, tally = cia.check(content, public)
+ self.assertEqual(findings, [])
+ self.assertEqual(tally["self_ok"], 1)
+
+ # --- client-rendered detection must match a mount, not a mention -----------
+
+ def test_swagger_mention_in_prose_is_not_client_rendered(self):
+ findings, tally = self.run_check(
+ {"a.md": 'See {{< relref "/develop/thing#no-such-heading" >}}.\n'},
+ pages={"/develop/thing":
+ '
R
'
+ '
browse to http://localhost:8080/swagger-ui/ to see it
'
+ '
springfox-swagger-ui'})
+ self.assertEqual(len(findings), 1, "a prose mention must not skip the check")
+ self.assertEqual(tally["unverifiable"], 0)
+
+ def test_redoc_element_is_client_rendered(self):
+ findings, tally = self.run_check(
+ {"a.md": 'See {{< relref "/develop/thing#whatever" >}}.\n'},
+ pages={"/develop/thing": '
'})
+ self.assertEqual(findings, [])
+ self.assertEqual(tally["unverifiable"], 1)
+
+ # --- the anchor pool must hold only real jump targets ---------------------
+
+ def test_meta_name_is_not_a_jump_target(self):
+ """Every page carries
; #description is not an anchor."""
+ findings, _ = self.run_check(
+ {"a.md": 'See {{< relref "/develop/thing#description" >}}.\n'},
+ pages={"/develop/thing":
+ '
'
+ '
'
+ '
R
'})
+ self.assertEqual(len(findings), 1, "a meta name must not satisfy a fragment")
+
+ def test_data_prefixed_attributes_are_not_jump_targets(self):
+ findings, _ = self.run_check(
+ {"a.md": 'See {{< relref "/develop/thing#nope" >}}.\n'},
+ pages={"/develop/thing":
+ '
'
+ '
R
'})
+ self.assertEqual(len(findings), 1, "data-id/data-name must not count")
+
+ def test_url_query_parameter_is_not_a_jump_target(self):
+ findings, _ = self.run_check(
+ {"a.md": 'See {{< relref "/develop/thing#GTM-ABC123" >}}.\n'},
+ pages={"/develop/thing":
+ '
R
'})
+ self.assertEqual(len(findings), 1, "an id= query param must not count")
+
+ def test_ids_inside_script_bodies_are_not_jump_targets(self):
+ findings, _ = self.run_check(
+ {"a.md": 'See {{< relref "/develop/thing#in-a-script" >}}.\n'},
+ pages={"/develop/thing":
+ ''
+ '
R
'})
+ self.assertEqual(len(findings), 1, "script bodies are not markup")
+
+ def test_a_name_still_counts_but_only_on_anchor_tags(self):
+ findings, tally = self.run_check(
+ {"a.md": 'See {{< relref "/develop/thing#legacy" >}}.\n'},
+ pages={"/develop/thing":
+ '
R
'})
+ self.assertEqual(findings, [])
+ self.assertEqual(tally["relref_ok"], 1)
+
+ # --- resolution details ---------------------------------------------------
+
+ def test_non_page_artifact_resolves(self):
+ """sitemap.xml and docs.ndjson are real files, not pages that failed."""
+ with TemporaryDirectory() as tmp:
+ root = Path(tmp)
+ content, public = build_tree(
+ root, {"/develop/thing": PAGE},
+ {"a.md": "See [x](https://redis.io/docs/latest/sitemap.xml).\n"})
+ (public / "sitemap.xml").write_text("
", encoding="utf-8")
+ findings, tally = cia.check(content, public)
+ self.assertEqual(findings, [])
+ self.assertEqual(tally["self_ok"], 1)
+
+ def test_prefixless_and_latest_prefix_resolve_alike(self):
+ """CI rewrites baseURL to include /docs/latest; both forms must map the same."""
+ for url in ("https://redis.io/docs/develop/thing/",
+ "https://redis.io/docs/latest/develop/thing/"):
+ with self.subTest(url=url):
+ findings, tally = self.run_check({"a.md": f"See [x]({url}).\n"})
+ self.assertEqual(findings, [], url)
+ self.assertEqual(tally["self_ok"], 1)
+
+ def test_query_string_before_fragment_is_stripped(self):
+ findings, _ = self.run_check(
+ {"a.md": "[x](https://redis.io/docs/latest/develop/thing/?utm=1#real-heading)\n"})
+ self.assertEqual(findings, [])
+
+
+if __name__ == "__main__":
+ unittest.main()