from __future__ import annotations import re import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[1] HTML_PATH = ROOT / "data" / "wiki" / "guanghan_topology_demo.html" TIMELINE_PATH = ROOT / "user_input" / "广寒计划" / "Timeline.md" CLICKABLE_MODULES = { "core", "habitat-1", "habitat-2", "greenhouse", "laboratory", "power-node", "power-north", "power-east", "power-south", "power-west", "wugang-2", "binglun-1", "binglun-2", "binglun-3", } class GuanghanTopologySourceTests(unittest.TestCase): @classmethod def setUpClass(cls) -> None: cls.html = HTML_PATH.read_text(encoding="utf-8") timeline = TIMELINE_PATH.read_text(encoding="utf-8") cls.ghc05 = timeline.split("GHC-05任务", 1)[1].split("JC-02任务", 1)[0] def test_clickable_module_inventory_is_exact(self) -> None: found = set(re.findall(r'data-module="([^"]+)"', self.html)) self.assertEqual(found, CLICKABLE_MODULES) def test_connection_inventory_matches_approved_topology(self) -> None: self.assertEqual(self.html.count('data-connection="pressurized"'), 2) self.assertIn('data-link="wugang-2--greenhouse"', self.html) self.assertIn('data-link="habitat-1--power-north"', self.html) self.assertNotIn("perimeter-corridor", self.html) def test_walkways_and_radiators_are_not_clickable(self) -> None: self.assertNotRegex( self.html, r'<(?:line|path|rect)[^>]+data-connection="pressurized"[^>]+data-module=', ) self.assertNotRegex( self.html, r'<(?:g|rect)[^>]+class="[^"]*radiator[^"]*"[^>]+data-module=', ) def test_each_radiator_has_a_visible_text_label(self) -> None: radiator_groups = re.findall( r']*>(.*?)', self.html, flags=re.S, ) self.assertEqual(len(radiator_groups), 4) for group in radiator_groups: self.assertIn(">散热器", group) rect = re.search(r']+x="(\d+)" y="(\d+)"[^>]*>散热器', group) self.assertIsNotNone(rect) self.assertIsNotNone(label) rect_x, rect_y, rect_width = map(int, rect.groups()) label_x, label_y = map(int, label.groups()) self.assertGreaterEqual(label_x, rect_x) self.assertLessEqual(label_x, rect_x + rect_width) self.assertLess(label_y, rect_y) def test_future_extensions_exist_only_as_source_comments(self) -> None: comments = "\n".join(re.findall(r"", self.html, flags=re.S)) visible_source = re.sub(r"", "", self.html, flags=re.S) for direction in ("north", "east", "west"): self.assertIn(f"FUTURE_EXPANSION_ANCHOR:{direction}", comments) self.assertNotIn("未来北部十字结构", visible_source) self.assertNotIn("未来东部十字结构", visible_source) self.assertNotIn("未来西部十字结构", visible_source) def test_every_clickable_module_has_dimensions_field(self) -> None: for module_id in CLICKABLE_MODULES: match = re.search( rf'"{re.escape(module_id)}"\s*:\s*\{{(.*?)\n \}}(?:,|\n)', self.html, flags=re.S, ) self.assertIsNotNone(match, msg=f"{module_id} detail object is missing") self.assertIn("dimensions:", match.group(1), msg=f"{module_id} is missing dimensions") def test_timeline_contains_only_the_two_real_corridors(self) -> None: for incorrect in ( "4段加压走道", "居住舱#1↔居住舱#2", "居住舱#2↔实验舱", "实验舱↔温室", "温室↔居住舱#1", "环形加压通路", "8个对接面", "4段加压走道48小时", ): self.assertNotIn(incorrect, self.ghc05) self.assertIn("居住舱#1远端对接口", self.ghc05) self.assertIn("电力模块北部", self.ghc05) self.assertIn("温室远端对接口", self.ghc05) self.assertIn("吴刚#2", self.ghc05) self.assertIn("从温室模块远端接口断开", self.ghc05) if __name__ == "__main__": unittest.main()