import importlib.util import subprocess import sys import tempfile import textwrap import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SKILL_ROOT = ROOT / "skills" / "tsl-syntax-reference" LOOKUP_PATH = SKILL_ROOT / "scripts" / "lookup.py" SKILL_PATH = SKILL_ROOT / "SKILL.md" CI_PATH = ROOT / ".gitea" / "workflows" / "checks.yml" PREPARE_PATH = ROOT / ".gitea" / "workflows" / "prepare.yml" def load_lookup_module(): spec = importlib.util.spec_from_file_location("tsl_syntax_lookup", LOOKUP_PATH) if spec is None or spec.loader is None: raise RuntimeError(f"cannot load {LOOKUP_PATH}") module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module lookup = load_lookup_module() def write_reference(directory: Path, name: str, content: str) -> Path: page = directory / name page.write_text(textwrap.dedent(content).lstrip(), encoding="utf-8", newline="\n") return page def run_lookup(*args: str) -> subprocess.CompletedProcess[str]: return subprocess.run( [sys.executable, str(LOOKUP_PATH), *args], cwd=ROOT, capture_output=True, text=True, ) class TslSyntaxReferenceTests(unittest.TestCase): def test_bundled_references_pass_structural_check(self): self.assertEqual([], lookup.validate_references()) def test_bundled_sections_use_unique_explicit_ids(self): sections = lookup.load_sections() ids = [section.id for section in sections] self.assertEqual(len(ids), len(set(ids))) self.assertTrue(ids) for section_id in ids: with self.subTest(section_id=section_id): self.assertRegex(section_id, r"^syntax-\d{2}-\d{3}$") def test_explicit_section_id_survives_heading_rename(self): with tempfile.TemporaryDirectory() as temp_dir: references = Path(temp_dir) page = write_reference( references, "99_fixture.md", """ # Fixture ## 本篇职责 测试职责。 ## 原标题 事实正文。 """, ) before = lookup.load_sections(references)[1].id page.write_text( page.read_text(encoding="utf-8").replace("## 原标题", "## 新标题"), encoding="utf-8", newline="\n", ) after = lookup.load_sections(references)[1].id self.assertEqual("syntax-99-002", before) self.assertEqual(before, after) def test_missing_explicit_section_id_fails_structure_check(self): with tempfile.TemporaryDirectory() as temp_dir: references = Path(temp_dir) write_reference( references, "99_fixture.md", """ # Fixture ## 本篇职责 测试职责。 """, ) messages = [item.message for item in lookup.validate_references(references)] self.assertTrue(any("缺少显式 section ID" in message for message in messages)) def test_duplicate_explicit_section_id_fails_check_and_loading(self): with tempfile.TemporaryDirectory() as temp_dir: references = Path(temp_dir) write_reference( references, "99_fixture.md", """ # Fixture ## 本篇职责 测试职责。 ## 重复 ID 事实正文。 """, ) messages = [item.message for item in lookup.validate_references(references)] with self.assertRaisesRegex(lookup.ReferenceStructureError, "重复 section ID"): lookup.load_sections(references) self.assertTrue(any("重复 section ID" in message for message in messages)) def test_orphan_identity_line_fails_structure_check(self): with tempfile.TemporaryDirectory() as temp_dir: references = Path(temp_dir) write_reference( references, "99_fixture.md", """ # Fixture ## 本篇职责 测试职责。 ## 示例 代码块身份:可直接照写示例 """, ) messages = [item.message for item in lookup.validate_references(references)] self.assertTrue(any("孤立的代码块身份" in message for message in messages)) def test_h5_and_h6_headings_fail_structure_check(self): with tempfile.TemporaryDirectory() as temp_dir: references = Path(temp_dir) write_reference( references, "99_fixture.md", """ # Fixture ## 本篇职责 测试职责。 ##### 不会被索引的标题 隐藏事实。 """, ) messages = [item.message for item in lookup.validate_references(references)] self.assertTrue(any("H5/H6" in message for message in messages)) def test_quickstart_rule_drift_fails_structure_check(self): with tempfile.TemporaryDirectory() as temp_dir: references = Path(temp_dir) write_reference( references, "01_quickstart.md", """ # Quickstart ## 本篇职责 派生摘要。 ## 语言核心事实速查 - 普通赋值使用 `=`。 """, ) write_reference( references, "02_topic.md", """ # Topic ## 本篇职责 完整事实源。 ## 核心规则 - 普通赋值使用 `:=`。 """, ) messages = [item.message for item in lookup.validate_references(references)] self.assertTrue( any("派生摘要规则与专题事实不一致" in message for message in messages) ) def test_complete_quickstart_contract_detects_rule_deleted_from_both_pages(self): with tempfile.TemporaryDirectory() as temp_dir: references = Path(temp_dir) quickstart = write_reference( references, "01_quickstart.md", """ # Quickstart ## 本篇职责 派生摘要。 ## 语言核心事实速查 没有任何规则。 """, ) pages = {quickstart: quickstart.read_text(encoding="utf-8").splitlines()} messages = [ item.message for item in lookup._quickstart_rule_problems( pages, require_complete=True, ) ] self.assertTrue(any("assignment" in message for message in messages)) def test_missing_references_are_installation_errors_for_every_action(self): with tempfile.TemporaryDirectory() as temp_dir: missing = Path(temp_dir) / "missing" actions = ( ("--map",), ("--query", "数组下标", "--mode", "explain"), ("--section", "syntax-03-002"), ("--check",), ) for action in actions: with self.subTest(action=action): result = run_lookup( *action, "--references-dir", str(missing), ) self.assertEqual(1, result.returncode) self.assertEqual("", result.stdout) self.assertIn("参考目录不可用", result.stderr) def test_documented_array_index_query_ranks_basic_array_section_first(self): for query in ("数组下标", "请帮我解释数组的下标"): with self.subTest(query=query): result = lookup.query_sections(query, "explain", limit=1) self.assertEqual("11_matrix_and_collections.md", result.matches[0].section.page.name) self.assertEqual("基础数组与键表", result.matches[0].section.heading_path[-1]) def test_documented_write_and_diagnose_queries_rank_expected_sections(self): write_result = lookup.query_sections("命名参数", "write", limit=1) diagnose_result = lookup.query_sections( "invalid statement 声明区", "diagnose", limit=1, ) self.assertEqual( ["syntax-02-002", "syntax-01-002"], [section.id for section in write_result.prelude], ) self.assertEqual("syntax-05-008", write_result.matches[0].section.id) self.assertEqual("syntax-02-006", diagnose_result.matches[0].section.id) def test_external_call_queries_retrieve_platform_and_abi_boundaries(self): cases = { "动态库常驻": "syntax-17-012", "外部出参": "syntax-17-013", "Linux 动态加载": "syntax-17-014", "函数指针释放": "syntax-17-008", } for query, expected_id in cases.items(): with self.subTest(query=query): result = lookup.query_sections(query, "write", limit=1) self.assertEqual(expected_id, result.matches[0].section.id) def test_index_origin_variants_rank_value_rules_first(self): for query in ("下标从几开始", "下标是从几开始", "请问下标是从几开始"): with self.subTest(query=query): result = lookup.query_sections(query, "explain", limit=1) self.assertEqual("03_values_and_literals.md", result.matches[0].section.page.name) self.assertEqual("核心规则", result.matches[0].section.heading_path[-1]) def test_every_curated_page_alias_still_ranks_its_page_first(self): for page_name, aliases in lookup.PAGE_INTENT_ALIASES.items(): for alias in aliases: with self.subTest(page=page_name, alias=alias): result = lookup.query_sections(alias, "explain", limit=1) self.assertTrue(result.matches, alias) self.assertEqual(page_name, result.matches[0].section.page.name) def test_weak_only_query_remains_a_no_match(self): result = run_lookup("--query", "只能确认", "--mode", "explain") self.assertEqual(2, result.returncode) self.assertIn("only weak candidates", result.stderr) def test_batch_section_retrieval_is_atomic(self): ids = [section.id for section in lookup.load_sections()[:2]] success = run_lookup("--section", *ids) failure = run_lookup("--section", ids[0], "syntax-99-999") self.assertEqual(0, success.returncode) self.assertTrue(all(section_id in success.stdout for section_id in ids)) self.assertEqual(2, failure.returncode) self.assertEqual("", failure.stdout) self.assertIn("section not found: syntax-99-999", failure.stderr) def test_structural_metadata_is_not_exposed_in_lookup_output(self): sections = {section.id: section for section in lookup.load_sections()} candidates = lookup.render_candidates( lookup.query_sections("命名参数", "write", limit=1) ) quickstart = lookup.render_section(sections["syntax-01-002"]) for output in (candidates, quickstart): with self.subTest(output=output[:40]): self.assertNotIn("