892 lines
29 KiB
Python
892 lines
29 KiB
Python
import io
|
|
import importlib.util
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from contextlib import redirect_stdout
|
|
from pathlib import Path
|
|
|
|
|
|
SCRIPT = (
|
|
Path(__file__).resolve().parents[3]
|
|
/ "skills"
|
|
/ "tsl-api-reference"
|
|
/ "scripts"
|
|
/ "lookup.py"
|
|
)
|
|
SKILL_MD = SCRIPT.parents[1] / "SKILL.md"
|
|
API_WORKFLOW = SCRIPT.parents[1] / "workflows" / "api-lookup.md"
|
|
|
|
|
|
def load_script():
|
|
spec = importlib.util.spec_from_file_location("tsl_lookup", SCRIPT)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
class LookupTest(unittest.TestCase):
|
|
def write_unified_api_fixture(self, skill_root):
|
|
data_dir = skill_root / "data"
|
|
codegen_dir = skill_root / "references" / "codegen"
|
|
data_dir.mkdir()
|
|
(codegen_dir / "project").mkdir(parents=True)
|
|
|
|
header = [
|
|
"name",
|
|
"scope",
|
|
"module",
|
|
"signature",
|
|
"page",
|
|
"anchor",
|
|
"tags",
|
|
"summary",
|
|
"kind",
|
|
"binding",
|
|
"visibility",
|
|
"owner",
|
|
"qualified_name",
|
|
]
|
|
|
|
def row(
|
|
name,
|
|
signature,
|
|
page,
|
|
anchor,
|
|
summary,
|
|
kind,
|
|
binding="",
|
|
visibility="",
|
|
owner="",
|
|
qualified_name="",
|
|
):
|
|
return [
|
|
name,
|
|
"project",
|
|
"demo",
|
|
signature,
|
|
page,
|
|
anchor,
|
|
"",
|
|
summary,
|
|
kind,
|
|
binding,
|
|
visibility,
|
|
owner,
|
|
qualified_name or name,
|
|
]
|
|
|
|
rows = [
|
|
row(
|
|
"DemoUnit",
|
|
"DemoUnit",
|
|
"project/demo_unit.md",
|
|
"demounit",
|
|
"接口。",
|
|
"unit",
|
|
),
|
|
row(
|
|
"Open",
|
|
"Open()",
|
|
"project/demo_unit.md",
|
|
"open",
|
|
"打开。",
|
|
"function",
|
|
"unit",
|
|
"public",
|
|
"DemoUnit",
|
|
"DemoUnit.Open",
|
|
),
|
|
row(
|
|
"Document",
|
|
"Document",
|
|
"project/demo_unit.md",
|
|
"document",
|
|
"文档。",
|
|
"class",
|
|
"unit",
|
|
"public",
|
|
"DemoUnit",
|
|
"DemoUnit.Document",
|
|
),
|
|
row(
|
|
"Save",
|
|
"Save(path)",
|
|
"project/demo_unit.md",
|
|
"savepath",
|
|
"按路径保存。",
|
|
"method",
|
|
"instance",
|
|
"public",
|
|
"DemoUnit.Document",
|
|
"DemoUnit.Document.Save",
|
|
),
|
|
row(
|
|
"Save",
|
|
"Save(mode)",
|
|
"project/demo_unit.md",
|
|
"savemode",
|
|
"按模式保存。",
|
|
"method",
|
|
"instance",
|
|
"protected",
|
|
"DemoUnit.Document",
|
|
"DemoUnit.Document.Save",
|
|
),
|
|
row(
|
|
"Name",
|
|
"Name",
|
|
"project/demo_unit.md",
|
|
"name",
|
|
"文档名称。",
|
|
"field",
|
|
"instance",
|
|
"public",
|
|
"DemoUnit.Document",
|
|
"DemoUnit.Document.Name",
|
|
),
|
|
row(
|
|
"Other",
|
|
"Other",
|
|
"project/demo_unit.md",
|
|
"other",
|
|
"其他对象。",
|
|
"class",
|
|
"unit",
|
|
"public",
|
|
"DemoUnit",
|
|
"DemoUnit.Other",
|
|
),
|
|
row(
|
|
"Save",
|
|
"Save()",
|
|
"project/demo_unit.md",
|
|
"save",
|
|
"保存其他对象。",
|
|
"method",
|
|
"instance",
|
|
"public",
|
|
"DemoUnit.Other",
|
|
"DemoUnit.Other.Save",
|
|
),
|
|
row(
|
|
"Widget",
|
|
"Widget",
|
|
"project/widget.md",
|
|
"widget",
|
|
"组件。",
|
|
"class",
|
|
),
|
|
row(
|
|
"Create",
|
|
"Create()",
|
|
"project/widget.md",
|
|
"create",
|
|
"创建组件。",
|
|
"method",
|
|
"class",
|
|
"public",
|
|
"Widget",
|
|
"Widget.Create",
|
|
),
|
|
row(
|
|
"Count",
|
|
"Count",
|
|
"project/widget.md",
|
|
"count",
|
|
"组件数量。",
|
|
"field",
|
|
"static",
|
|
"protected",
|
|
"Widget",
|
|
"Widget.Count",
|
|
),
|
|
]
|
|
index_text = "\n".join(
|
|
["\t".join(header), *("\t".join(values) for values in rows)]
|
|
)
|
|
tsv_path = data_dir / "function_index.tsv"
|
|
tsv_path.write_text(index_text + "\n", encoding="utf-8")
|
|
|
|
(codegen_dir / "project" / "demo_unit.md").write_text(
|
|
"# Demo / Unit\n\n"
|
|
"## `DemoUnit`\n\n"
|
|
"声明:unit\n\n"
|
|
"接口。\n\n"
|
|
"### `Open()`\n\n"
|
|
"声明:function\n\n"
|
|
"打开。\n\n"
|
|
"返回:Document\n\n"
|
|
"### `Document`\n\n"
|
|
"声明:class\n\n"
|
|
"文档。\n\n"
|
|
"#### `Save(path)`\n\n"
|
|
"声明:function\n\n"
|
|
"按路径保存。\n\n"
|
|
"可见性:`public`\n\n"
|
|
"#### `Save(mode)`\n\n"
|
|
"声明:function\n\n"
|
|
"按模式保存。\n\n"
|
|
"可见性:`protected`\n\n"
|
|
"#### `Name`\n\n"
|
|
"声明:field\n\n"
|
|
"文档名称。\n\n"
|
|
"可见性:`public`\n\n"
|
|
"类型:string\n\n"
|
|
"### `Other`\n\n"
|
|
"声明:class\n\n"
|
|
"其他对象。\n\n"
|
|
"#### `Save()`\n\n"
|
|
"声明:function\n\n"
|
|
"保存其他对象。\n\n"
|
|
"可见性:`public`\n",
|
|
encoding="utf-8",
|
|
)
|
|
(codegen_dir / "project" / "widget.md").write_text(
|
|
"# Demo / Class\n\n"
|
|
"## `Widget`\n\n"
|
|
"声明:class\n\n"
|
|
"组件。\n\n"
|
|
"### `Create()`\n\n"
|
|
"声明:class function\n\n"
|
|
"创建组件。\n\n"
|
|
"可见性:`public`\n\n"
|
|
"### `Count`\n\n"
|
|
"声明:static field\n\n"
|
|
"组件数量。\n\n"
|
|
"可见性:`protected`\n\n"
|
|
"类型:integer\n",
|
|
encoding="utf-8",
|
|
)
|
|
return tsv_path
|
|
|
|
def write_mixed_api_fixture(self, skill_root):
|
|
data_dir = skill_root / "data"
|
|
codegen_dir = skill_root / "references" / "codegen" / "project"
|
|
data_dir.mkdir()
|
|
codegen_dir.mkdir(parents=True)
|
|
header = [
|
|
"name",
|
|
"scope",
|
|
"module",
|
|
"signature",
|
|
"page",
|
|
"anchor",
|
|
"tags",
|
|
"summary",
|
|
"kind",
|
|
"binding",
|
|
"visibility",
|
|
"owner",
|
|
"qualified_name",
|
|
]
|
|
page = "project/mixed.md"
|
|
rows = [
|
|
[
|
|
"Widget",
|
|
"project",
|
|
"mixed",
|
|
"Widget",
|
|
page,
|
|
"widget",
|
|
"",
|
|
"组件。",
|
|
"class",
|
|
"",
|
|
"",
|
|
"",
|
|
"Widget",
|
|
],
|
|
[
|
|
"Open",
|
|
"project",
|
|
"mixed",
|
|
"Open()",
|
|
page,
|
|
"open",
|
|
"",
|
|
"打开。",
|
|
"method",
|
|
"instance",
|
|
"public",
|
|
"Widget",
|
|
"Widget.Open",
|
|
],
|
|
[
|
|
"Parse",
|
|
"project",
|
|
"mixed",
|
|
"Parse(text)",
|
|
page,
|
|
"parse",
|
|
"",
|
|
"解析。",
|
|
"function",
|
|
"",
|
|
"",
|
|
"",
|
|
"Parse",
|
|
],
|
|
[
|
|
"Runtime",
|
|
"project",
|
|
"mixed",
|
|
"Runtime",
|
|
page,
|
|
"runtime",
|
|
"",
|
|
"接口。",
|
|
"unit",
|
|
"",
|
|
"",
|
|
"",
|
|
"Runtime",
|
|
],
|
|
[
|
|
"Document",
|
|
"project",
|
|
"mixed",
|
|
"Document",
|
|
page,
|
|
"document",
|
|
"",
|
|
"文档。",
|
|
"class",
|
|
"unit",
|
|
"public",
|
|
"Runtime",
|
|
"Runtime.Document",
|
|
],
|
|
[
|
|
"Save",
|
|
"project",
|
|
"mixed",
|
|
"Save()",
|
|
page,
|
|
"save",
|
|
"",
|
|
"保存。",
|
|
"method",
|
|
"instance",
|
|
"public",
|
|
"Runtime.Document",
|
|
"Runtime.Document.Save",
|
|
],
|
|
[
|
|
"Other",
|
|
"project",
|
|
"mixed",
|
|
"Other",
|
|
page,
|
|
"other",
|
|
"",
|
|
"其他组件。",
|
|
"class",
|
|
"",
|
|
"",
|
|
"",
|
|
"Other",
|
|
],
|
|
]
|
|
tsv_path = data_dir / "function_index.tsv"
|
|
tsv_path.write_text(
|
|
"\n".join(
|
|
["\t".join(header), *("\t".join(row) for row in rows)]
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
(codegen_dir / "mixed.md").write_text(
|
|
"# Mixed\n\n"
|
|
"## `Widget`\n\n声明:class\n\n组件。\n\n"
|
|
"### `Open()`\n\n声明:function\n\n打开。\n\n可见性:public\n\n"
|
|
"## `Parse(text)`\n\n声明:function\n\n解析。\n\n"
|
|
"| 参数 | 类型 | 说明 |\n"
|
|
"| --- | --- | --- |\n"
|
|
"| `text` | string | 文本 |\n\n"
|
|
"返回:Widget\n\n"
|
|
"### 示例\n\n"
|
|
"```markdown\n## `NotBoundary`\n```\n\n"
|
|
"## `Runtime`\n\n声明:unit\n\n接口。\n\n"
|
|
"### `Document`\n\n声明:class\n\n文档。\n\n"
|
|
"#### `Save()`\n\n声明:function\n\n保存。\n\n可见性:public\n\n"
|
|
"## `Other`\n\n声明:class\n\n其他组件。\n",
|
|
encoding="utf-8",
|
|
)
|
|
return tsv_path
|
|
|
|
def run_lookup(self, tsv_path, *args):
|
|
return subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
*args,
|
|
"--tsv",
|
|
str(tsv_path),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
check=False,
|
|
)
|
|
|
|
def test_tsv_without_tags_is_rejected(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
tsv_path = Path(temp_dir) / "function_index.tsv"
|
|
tsv_path.write_text(
|
|
"name\tscope\tmodule\tsignature\tpage\tanchor\tsummary\n"
|
|
"foo\tbuiltin\tmisc\tfoo()\tmisc.md\tfoo\tLegacy entry\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--kw",
|
|
"Legacy",
|
|
"--tsv",
|
|
str(tsv_path),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertEqual("", result.stdout)
|
|
self.assertIn("missing required columns: tags", result.stderr)
|
|
self.assertNotIn("Traceback", result.stderr)
|
|
|
|
def test_missing_required_tsv_columns_are_rejected(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
tsv_path = Path(temp_dir) / "function_index.tsv"
|
|
tsv_path.write_text("name\nfoo\n", encoding="utf-8")
|
|
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--name",
|
|
"foo",
|
|
"--tsv",
|
|
str(tsv_path),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("missing required columns", result.stderr)
|
|
self.assertNotIn("Traceback", result.stderr)
|
|
|
|
def test_tsv_row_width_is_validated(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
tsv_path = Path(temp_dir) / "function_index.tsv"
|
|
tsv_path.write_text(
|
|
"name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n"
|
|
"foo\tbuiltin\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--name",
|
|
"foo",
|
|
"--tsv",
|
|
str(tsv_path),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertIn("line 2 has 2 columns; expected 8", result.stderr)
|
|
self.assertNotIn("Traceback", result.stderr)
|
|
|
|
def test_missing_entry_body_is_rejected(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
skill_root = Path(temp_dir)
|
|
data_dir = skill_root / "data"
|
|
codegen_dir = skill_root / "references" / "codegen"
|
|
data_dir.mkdir()
|
|
codegen_dir.mkdir(parents=True)
|
|
(data_dir / "function_index.tsv").write_text(
|
|
"name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n"
|
|
"foo\tbuiltin\tmisc\tfoo()\tmissing.md\tfoo\t\tFallback only\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--name",
|
|
"foo",
|
|
"--tsv",
|
|
str(data_dir / "function_index.tsv"),
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(1, result.returncode)
|
|
self.assertEqual("", result.stdout)
|
|
self.assertIn("entry body not found", result.stderr)
|
|
self.assertIn("missing.md#foo", result.stderr)
|
|
|
|
def test_empty_queries_are_rejected(self):
|
|
for option in ("--name", "--kw"):
|
|
with self.subTest(option=option):
|
|
result = subprocess.run(
|
|
[sys.executable, str(SCRIPT), option, ""],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(2, result.returncode)
|
|
self.assertIn("must not be empty", result.stderr)
|
|
|
|
def test_non_positive_limit_is_rejected(self):
|
|
for limit in ("0", "-1"):
|
|
with self.subTest(limit=limit):
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(SCRIPT),
|
|
"--kw",
|
|
"数组",
|
|
"--limit",
|
|
limit,
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(2, result.returncode)
|
|
self.assertIn("--limit must be >= 1", result.stderr)
|
|
|
|
def test_help_derives_scopes_from_selected_index(self):
|
|
result = subprocess.run(
|
|
[sys.executable, str(SCRIPT), "--help"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertIn("可用值从当前 function_index.tsv 读取", result.stdout)
|
|
self.assertNotIn("deprecated", result.stdout)
|
|
self.assertNotIn("--scope third", result.stdout)
|
|
|
|
def test_exact_search_uses_anchor_for_duplicate_signatures(self):
|
|
module = load_script()
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
skill_root = Path(temp_dir)
|
|
data_dir = skill_root / "data"
|
|
codegen_dir = skill_root / "references" / "codegen"
|
|
data_dir.mkdir()
|
|
codegen_dir.mkdir(parents=True)
|
|
|
|
header = [
|
|
"name",
|
|
"scope",
|
|
"module",
|
|
"signature",
|
|
"page",
|
|
"anchor",
|
|
"tags",
|
|
"summary",
|
|
]
|
|
rows = [
|
|
[
|
|
"CAPA",
|
|
"builtin",
|
|
"language",
|
|
"CAPA()",
|
|
"objects.md",
|
|
"capa",
|
|
"",
|
|
"IMAP summary",
|
|
],
|
|
[
|
|
"CAPA",
|
|
"builtin",
|
|
"language",
|
|
"CAPA()",
|
|
"objects.md",
|
|
"capa-1",
|
|
"",
|
|
"Pop3 summary",
|
|
],
|
|
]
|
|
index_text = "\n".join(
|
|
["\t".join(header), *("\t".join(row) for row in rows)]
|
|
)
|
|
(data_dir / "function_index.tsv").write_text(
|
|
index_text + "\n", encoding="utf-8"
|
|
)
|
|
(codegen_dir / "objects.md").write_text(
|
|
"# Objects\n\n"
|
|
"## `CAPA()`\n\n"
|
|
"声明:function\n\n"
|
|
"对象:IMAP\n\n"
|
|
"## `CAPA()`\n\n"
|
|
"声明:function\n\n"
|
|
"对象:Pop3\n\n"
|
|
"",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
stdout = io.StringIO()
|
|
with redirect_stdout(stdout):
|
|
status = module.main(
|
|
[
|
|
"--name",
|
|
"CAPA",
|
|
"--tsv",
|
|
str(data_dir / "function_index.tsv"),
|
|
]
|
|
)
|
|
|
|
output = stdout.getvalue()
|
|
self.assertEqual(0, status)
|
|
self.assertEqual(1, output.count("对象:IMAP"))
|
|
self.assertEqual(1, output.count("对象:Pop3"))
|
|
self.assertIn("objects.md#capa -->", output)
|
|
self.assertIn("objects.md#capa-1 -->", output)
|
|
|
|
def test_exact_qualified_member_query_returns_only_member_overloads(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
|
|
|
|
result = self.run_lookup(
|
|
tsv_path, "--name", "demounit.document.save"
|
|
)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(2, result.stdout.count("#### `Save("))
|
|
self.assertNotIn("#### `Name`", result.stdout)
|
|
self.assertNotIn("### `Other`", result.stdout)
|
|
|
|
def test_exact_unit_class_query_returns_its_h4_subtree_only(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
|
|
|
|
result = self.run_lookup(
|
|
tsv_path, "--name", "DemoUnit.Document"
|
|
)
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertIn("### `Document`", result.stdout)
|
|
self.assertIn("#### `Save(path)`", result.stdout)
|
|
self.assertIn("#### `Name`", result.stdout)
|
|
self.assertNotIn("### `Other`", result.stdout)
|
|
|
|
def test_exact_unit_query_returns_the_complete_unit_subtree(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
|
|
|
|
result = self.run_lookup(tsv_path, "--name", "DemoUnit")
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertIn("## `DemoUnit`", result.stdout)
|
|
self.assertIn("### `Open()`", result.stdout)
|
|
self.assertIn("### `Document`", result.stdout)
|
|
self.assertIn("#### `Save(path)`", result.stdout)
|
|
self.assertIn("### `Other`", result.stdout)
|
|
self.assertNotIn("## `Widget`", result.stdout)
|
|
|
|
def test_exact_class_query_returns_all_class_members(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
|
|
|
|
result = self.run_lookup(tsv_path, "--name", "widget")
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertIn("## `Widget`", result.stdout)
|
|
self.assertIn("### `Create()`\n\n声明:class function", result.stdout)
|
|
self.assertIn("### `Count`\n\n声明:static field", result.stdout)
|
|
self.assertNotIn("## `DemoUnit`", result.stdout)
|
|
|
|
def test_exact_class_in_mixed_page_stops_at_next_h2(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
tsv_path = self.write_mixed_api_fixture(Path(temp_dir))
|
|
|
|
result = self.run_lookup(tsv_path, "--name", "Widget")
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertIn("### `Open()`", result.stdout)
|
|
self.assertNotIn("## `Parse(text)`", result.stdout)
|
|
|
|
def test_exact_function_in_mixed_page_keeps_h3_example_only(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
tsv_path = self.write_mixed_api_fixture(Path(temp_dir))
|
|
|
|
result = self.run_lookup(tsv_path, "--name", "Parse")
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertIn("### 示例", result.stdout)
|
|
self.assertIn("## `NotBoundary`", result.stdout)
|
|
self.assertNotIn("## `Runtime`", result.stdout)
|
|
|
|
def test_exact_unit_in_mixed_page_stops_before_following_class(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
tsv_path = self.write_mixed_api_fixture(Path(temp_dir))
|
|
|
|
result = self.run_lookup(tsv_path, "--name", "Runtime")
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertIn("#### `Save()`", result.stdout)
|
|
self.assertNotIn("## `Other`", result.stdout)
|
|
|
|
def test_unqualified_member_query_returns_all_owners(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
|
|
|
|
result = self.run_lookup(tsv_path, "--name", "Save")
|
|
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
self.assertEqual(3, result.stdout.count("#### `Save("))
|
|
self.assertIn("按路径保存。", result.stdout)
|
|
self.assertIn("按模式保存。", result.stdout)
|
|
self.assertIn("保存其他对象。", result.stdout)
|
|
|
|
def test_keyword_search_includes_unified_fields_and_declaration_term(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
tsv_path = self.write_unified_api_fixture(Path(temp_dir))
|
|
|
|
qualified = self.run_lookup(
|
|
tsv_path, "--kw", "Widget.Create"
|
|
)
|
|
visibility = self.run_lookup(
|
|
tsv_path, "--kw", "protected", "method"
|
|
)
|
|
|
|
self.assertEqual(0, qualified.returncode, qualified.stderr)
|
|
self.assertIn("Widget.Create\tclass function\tCreate()", qualified.stdout)
|
|
self.assertNotIn("static function", qualified.stdout)
|
|
self.assertEqual(0, visibility.returncode, visibility.stderr)
|
|
self.assertIn("DemoUnit.Document.Save", visibility.stdout)
|
|
|
|
def test_skill_documents_qualified_queries_and_legacy_index_compatibility(self):
|
|
skill_text = SKILL_MD.read_text(encoding="utf-8")
|
|
text = API_WORKFLOW.read_text(encoding="utf-8")
|
|
|
|
self.assertIn("workflows/api-lookup.md", skill_text)
|
|
self.assertIn("完全限定名称", text)
|
|
self.assertIn("--scope module", text)
|
|
self.assertIn("qualified_name", text)
|
|
self.assertIn("owner、kind、", text)
|
|
self.assertIn("binding 和 visibility", text)
|
|
self.assertIn("class function", text)
|
|
self.assertIn("8 列索引", text)
|
|
self.assertIn("13 列索引", text)
|
|
|
|
def test_skill_routes_workflows_and_keeps_stop_resident(self):
|
|
skill_text = SKILL_MD.read_text(encoding="utf-8")
|
|
skill_root = SKILL_MD.parent
|
|
instruction_paths = sorted((skill_root / "workflows").glob("*.md"))
|
|
|
|
for path in instruction_paths:
|
|
self.assertIn(path.relative_to(skill_root).as_posix(), skill_text)
|
|
self.assertIn("🔴 CHECKPOINT · 🛑 STOP", skill_text)
|
|
self.assertIn("rc=1 不进入上述“API 缺失”分支", skill_text)
|
|
self.assertIn("原样名称或 `qualified_name`", skill_text)
|
|
self.assertIn("`profile_status=not_profiled`", skill_text)
|
|
|
|
def test_skill_instruction_package_has_no_reverse_dependencies(self):
|
|
skill_root = SKILL_MD.parent
|
|
package_text = "\n".join(
|
|
path.read_text(encoding="utf-8")
|
|
for path in [SKILL_MD, *sorted((skill_root / "workflows").glob("*.md"))]
|
|
)
|
|
|
|
for forbidden in (
|
|
"tsl-syntax-reference",
|
|
"references/maintenance.md",
|
|
"tools/tsl-codegen",
|
|
"scripts/build_dictionary.py",
|
|
"AGENTS.md",
|
|
"CONTEXT.md",
|
|
"docs/adr/",
|
|
):
|
|
self.assertNotIn(forbidden, package_text)
|
|
|
|
def test_repository_maintenance_doc_owns_api_data_maintenance(self):
|
|
readme = (
|
|
SKILL_MD.parents[2] / "tools" / "tsl-codegen" / "MAINTENANCE.md"
|
|
).read_text(encoding="utf-8")
|
|
for required in (
|
|
"TSL API 数据维护",
|
|
"class_lookup.py --check",
|
|
"tools/tsl-codegen/scripts/build_index.py",
|
|
"--skill-dir skills/tsl-api-reference",
|
|
"tools/tsl-codegen/scripts/build_dictionary.py",
|
|
"beautifulsoup4",
|
|
"profile_status=not_profiled",
|
|
):
|
|
self.assertIn(required, readme)
|
|
|
|
user_readme = (
|
|
SKILL_MD.parents[2] / "tools" / "tsl-codegen" / "README.md"
|
|
).read_text(encoding="utf-8")
|
|
for maintainer_only in (
|
|
"class_lookup.py --check",
|
|
"beautifulsoup4",
|
|
"--source-root <exported-html-dir>",
|
|
):
|
|
self.assertNotIn(maintainer_only, user_readme)
|
|
self.assertIn("profile_status=not_profiled", user_readme)
|
|
|
|
def test_public_cli_help_owns_status_and_exit_contracts(self):
|
|
skill_root = SKILL_MD.parent
|
|
expected = {
|
|
"lookup.py": ("退出码:", "--name 无匹配", "page#anchor"),
|
|
"class_lookup.py": ("输出状态:", "contract_status", "退出码:"),
|
|
"dictionary_lookup.py": ("status: ambiguous", "status: no_match", "退出码:"),
|
|
}
|
|
|
|
for script_name, fragments in expected.items():
|
|
with self.subTest(script=script_name):
|
|
result = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
str(skill_root / "scripts" / script_name),
|
|
"--help",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
self.assertEqual(0, result.returncode, result.stderr)
|
|
for fragment in fragments:
|
|
self.assertIn(fragment, result.stdout)
|
|
|
|
def test_keyword_search_includes_tags_and_summary(self):
|
|
module = load_script()
|
|
rows = [
|
|
{
|
|
"name": "demo",
|
|
"signature": "demo()",
|
|
"module": "base",
|
|
"tags": "数组 列表",
|
|
"summary": "返回示例值",
|
|
}
|
|
]
|
|
|
|
self.assertEqual(rows, module.search_keyword(rows, ["数组"]))
|
|
self.assertEqual(rows, module.search_keyword(rows, ["返回示例"]))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|