257 lines
10 KiB
Python
257 lines
10 KiB
Python
import importlib.util
|
|
import re
|
|
import unittest
|
|
from pathlib import Path, PurePosixPath
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SKILL_DIR = ROOT / "skills" / "tsl-syntax-reference"
|
|
SKILL_FILE = SKILL_DIR / "SKILL.md"
|
|
REFERENCES_DIR = SKILL_DIR / "references"
|
|
COMPATIBILITY_INDEX = ROOT / "docs" / "tsl" / "syntax" / "index.md"
|
|
RULESET_FILE = ROOT / "rulesets" / "tsl" / "index.md"
|
|
LOOKUP_SCRIPT = SKILL_DIR / "scripts" / "lookup.py"
|
|
|
|
lookup_spec = importlib.util.spec_from_file_location(
|
|
"tsl_syntax_reference_structure_lookup", LOOKUP_SCRIPT
|
|
)
|
|
lookup = importlib.util.module_from_spec(lookup_spec)
|
|
lookup_spec.loader.exec_module(lookup)
|
|
|
|
TOPIC_REFERENCES = {
|
|
"01_quickstart.md",
|
|
"02_core_model.md",
|
|
"03_values_and_literals.md",
|
|
"04_variables_and_constants.md",
|
|
"05_functions_and_calls.md",
|
|
"06_expressions_and_operators.md",
|
|
"07_control_flow.md",
|
|
"08_objects_and_classes.md",
|
|
"09_units_and_scope.md",
|
|
"10_runtime_context_and_with.md",
|
|
"11_pitfalls.md",
|
|
"12_matrix_and_collections.md",
|
|
"13_resultset_and_filters.md",
|
|
"14_ts_sql.md",
|
|
"15_debug_and_profiler.md",
|
|
"16_lexical_structure_and_compile_options.md",
|
|
"17_types_and_conversions.md",
|
|
"18_external_calls_and_threads.md",
|
|
"19_namespace_libpath_and_unit_runtime.md",
|
|
"20_object_runtime_and_introspection.md",
|
|
"21_builtin_runtime_objects.md",
|
|
"22_matrix_deep_dive.md",
|
|
"23_fmarray.md",
|
|
"24_object_overloads_and_iteration.md",
|
|
}
|
|
MARKDOWN_LINK = re.compile(r"!?\[[^\]]*\]\(([^)]+)\)")
|
|
OPERATIONAL_TOPIC_LINK = re.compile(
|
|
r"(?:跳到|跳转到|转到|回到|回看|回(?=\s*\[)|进入|移到)"
|
|
r"(?=[^。\n]*\[[^\]]+\]\((?:0[1-9]|1[0-9]|2[0-4])_[^)]+\.md(?:#[^)]+)?\))"
|
|
)
|
|
|
|
|
|
def read_text(path: Path) -> str:
|
|
return path.read_text(encoding="utf-8")
|
|
|
|
|
|
def frontmatter(path: Path) -> dict[str, str]:
|
|
text = read_text(path)
|
|
match = re.match(r"\A---\s*\n(.*?)\n---\s*\n", text, re.DOTALL)
|
|
if not match:
|
|
raise AssertionError(f"missing YAML frontmatter: {path}")
|
|
fields: dict[str, str] = {}
|
|
for line in match.group(1).splitlines():
|
|
key, separator, value = line.partition(":")
|
|
if separator:
|
|
fields[key.strip()] = value.strip().strip("\"'")
|
|
return fields
|
|
|
|
|
|
def local_link_targets(text: str) -> list[str]:
|
|
text = re.sub(r"```.*?```", "", text, flags=re.DOTALL)
|
|
text = re.sub(r"`[^`\n]*`", "", text)
|
|
targets = []
|
|
for match in MARKDOWN_LINK.finditer(text):
|
|
target = match.group(1).strip().split(maxsplit=1)[0].strip("<>")
|
|
if target.startswith(("#", "http://", "https://", "mailto:")):
|
|
continue
|
|
targets.append(target)
|
|
return targets
|
|
|
|
|
|
class TslSyntaxReferenceSkillStructureTest(unittest.TestCase):
|
|
def test_skill_discovery_covers_natural_tinysoft_terms(self) -> None:
|
|
description = frontmatter(SKILL_FILE)["description"]
|
|
|
|
for term in ("TSL", "TSF", "TS-SQL", "Tinysoft", "天软", "脚本"):
|
|
self.assertIn(term, description)
|
|
|
|
def test_skill_requires_safe_two_stage_lookup(self) -> None:
|
|
text = read_text(SKILL_FILE)
|
|
|
|
self.assertRegex(text, r"--query[^\n]*(候选|Section ID)")
|
|
self.assertRegex(text, r"--section[^\n]*(正文|事实|章节)")
|
|
self.assertRegex(text, r"shell|命令注入|安全传参|原样拼接")
|
|
self.assertRegex(text, r"询问[^\n]*运行|如何运行")
|
|
self.assertIn("AGENTS.md", text)
|
|
self.assertRegex(text, r"可直接照写[^\n]*(不等于|不代表)[^\n]*(验证|可用)")
|
|
|
|
def test_concept_map_is_plain_text_without_navigation_or_code(self) -> None:
|
|
rendered = lookup.render_concept_map(lookup.build_concept_map())
|
|
|
|
self.assertIsNone(re.search(r"\[[^\]]+\]\([^)]+\)", rendered))
|
|
self.assertNotIn("```", rendered)
|
|
self.assertNotIn("`", rendered)
|
|
|
|
def test_high_risk_reference_pages_defer_api_scope(self) -> None:
|
|
for name in (
|
|
"10_runtime_context_and_with.md",
|
|
"15_debug_and_profiler.md",
|
|
"22_matrix_deep_dive.md",
|
|
):
|
|
text = read_text(REFERENCES_DIR / name)
|
|
with self.subTest(page=name):
|
|
self.assertIn("tsl-api-reference", text)
|
|
self.assertRegex(text, r"签名|参数")
|
|
self.assertRegex(text, r"scope|环境|解释器|可用性")
|
|
|
|
def test_local_link_targets_includes_local_images(self) -> None:
|
|
text = "\n"
|
|
self.assertEqual(local_link_targets(text), ["images/router.png"])
|
|
|
|
def test_skill_name_matches_directory(self) -> None:
|
|
self.assertEqual(frontmatter(SKILL_FILE)["name"], SKILL_DIR.name)
|
|
|
|
def test_frontmatter_description_covers_tsl_and_tsf_tasks(self) -> None:
|
|
description = frontmatter(SKILL_FILE)["description"]
|
|
self.assertRegex(description, r"(?i)\bTSL\b")
|
|
self.assertRegex(description, r"(?i)\bTSF\b")
|
|
self.assertRegex(description, r"写|编写|修改|审查|解释|语法错误")
|
|
|
|
def test_reference_inventory_contains_topics_without_router(self) -> None:
|
|
actual = {path.name for path in REFERENCES_DIR.iterdir() if path.is_file()}
|
|
self.assertEqual(actual, TOPIC_REFERENCES)
|
|
self.assertFalse((REFERENCES_DIR / "index.md").exists())
|
|
|
|
def test_lookup_script_is_bundled(self) -> None:
|
|
self.assertTrue((SKILL_DIR / "scripts" / "lookup.py").is_file())
|
|
|
|
def test_manual_router_protocol_is_absent(self) -> None:
|
|
corpus = read_text(SKILL_FILE) + "\n" + "\n".join(
|
|
read_text(path) for path in REFERENCES_DIR.glob("*.md")
|
|
)
|
|
for forbidden in ("路由中心", "选择一个主专题", "候选页继续判断"):
|
|
self.assertNotIn(forbidden, corpus)
|
|
|
|
def test_topic_pages_do_not_instruct_manual_navigation(self) -> None:
|
|
for path in REFERENCES_DIR.glob("*.md"):
|
|
for line_number, line in enumerate(read_text(path).splitlines(), start=1):
|
|
with self.subTest(path=path.name, line=line_number):
|
|
self.assertIsNone(OPERATIONAL_TOPIC_LINK.search(line), line)
|
|
|
|
def test_json_router_is_absent(self) -> None:
|
|
self.assertFalse((REFERENCES_DIR / "00_agent_index.json").exists())
|
|
self.assertFalse((ROOT / "docs" / "tsl" / "syntax" / "00_agent_index.json").exists())
|
|
|
|
def test_references_do_not_escape_skill(self) -> None:
|
|
for path in REFERENCES_DIR.glob("*.md"):
|
|
for target in local_link_targets(read_text(path)):
|
|
link_path = target.split("#", 1)[0].replace("\\", "/")
|
|
self.assertNotIn("docs/tsl/", link_path, f"{path}: {target}")
|
|
self.assertFalse(link_path.startswith("/"), f"{path}: {target}")
|
|
self.assertNotIn("..", PurePosixPath(link_path).parts, f"{path}: {target}")
|
|
|
|
def test_reference_markdown_links_resolve(self) -> None:
|
|
for path in REFERENCES_DIR.glob("*.md"):
|
|
for target in local_link_targets(read_text(path)):
|
|
link_path = target.split("#", 1)[0]
|
|
self.assertTrue((REFERENCES_DIR / link_path).is_file(), f"{path}: {target}")
|
|
|
|
def test_skill_uses_lookup_as_its_only_retrieval_entry(self) -> None:
|
|
text = read_text(SKILL_FILE)
|
|
self.assertIn("scripts/lookup.py", text)
|
|
for mode in ("write", "diagnose", "explain"):
|
|
self.assertIn(f"--mode {mode}", text)
|
|
self.assertEqual(local_link_targets(text), [])
|
|
self.assertNotIn("references/index.md", text)
|
|
|
|
def test_skill_does_not_bundle_external_domains(self) -> None:
|
|
forbidden = {
|
|
"naming.md",
|
|
"code_style.md",
|
|
"toolchain.md",
|
|
"modules",
|
|
"tsl-api-reference",
|
|
"00_agent_index.json",
|
|
}
|
|
actual = {path.name for path in SKILL_DIR.rglob("*")}
|
|
self.assertTrue(forbidden.isdisjoint(actual), forbidden & actual)
|
|
|
|
def test_compatibility_index_contains_no_syntax_facts(self) -> None:
|
|
text = read_text(COMPATIBILITY_INDEX)
|
|
self.assertIn("../../../skills/tsl-syntax-reference/SKILL.md", text)
|
|
for forbidden in (
|
|
"```",
|
|
"代码块身份",
|
|
"01_quickstart.md",
|
|
"02_core_model.md",
|
|
"| 任务",
|
|
):
|
|
self.assertNotIn(forbidden, text)
|
|
|
|
def test_ruleset_file_remains_tsl_router(self) -> None:
|
|
self.assertTrue(RULESET_FILE.is_file())
|
|
text = read_text(RULESET_FILE)
|
|
for route in (
|
|
"tsl-syntax-reference",
|
|
"tsl-api-reference",
|
|
"naming.md",
|
|
"code_style.md",
|
|
"toolchain.md",
|
|
"modules/index.md",
|
|
"项目",
|
|
):
|
|
self.assertIn(route, text)
|
|
|
|
def test_ruleset_routes_both_tsl_skills(self) -> None:
|
|
text = read_text(RULESET_FILE)
|
|
self.assertRegex(text, r"语法[^\n]*tsl-syntax-reference|tsl-syntax-reference[^\n]*语法")
|
|
self.assertRegex(text, r"API|函数")
|
|
self.assertIn("tsl-api-reference", text)
|
|
|
|
def test_ruleset_syntax_constraints_moved_to_skill(self) -> None:
|
|
ruleset = read_text(RULESET_FILE)
|
|
owner = read_text(SKILL_FILE) + "\n" + "\n".join(
|
|
read_text(path) for path in REFERENCES_DIR.glob("*.md")
|
|
)
|
|
self.assertRegex(ruleset, r"缺失|不可用")
|
|
self.assertRegex(ruleset, r"停止|阻断")
|
|
for forbidden in (
|
|
"```tsl",
|
|
"代码块身份",
|
|
".tsl` / `.tsf` 后缀由用户指定时",
|
|
".tsl` 代码需要本文件内函数或类时",
|
|
):
|
|
self.assertNotIn(forbidden, ruleset)
|
|
self.assertIn("代码块身份", owner)
|
|
self.assertIn(".tsl", owner)
|
|
self.assertIn(".tsf", owner)
|
|
|
|
def test_static_docs_route_to_fact_owner(self) -> None:
|
|
routes = {
|
|
ROOT / "docs" / "tsl" / "index.md": "../../skills/tsl-syntax-reference/SKILL.md",
|
|
ROOT / "docs" / "tsl" / "naming.md": "../../skills/tsl-syntax-reference/SKILL.md",
|
|
ROOT / "docs" / "tsl" / "code_style.md": "../../skills/tsl-syntax-reference/SKILL.md",
|
|
ROOT / "docs" / "index.md": "../skills/tsl-syntax-reference/SKILL.md",
|
|
}
|
|
legacy_deep_link = re.compile(r"(?:docs/tsl/)?syntax/(?:0[1-9]|1[0-9]|2[0-4])_")
|
|
for path, expected_link in routes.items():
|
|
text = read_text(path)
|
|
self.assertIn(expected_link, text, path)
|
|
self.assertIsNone(legacy_deep_link.search(text), path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|