import csv import json import re import subprocess import sys import tempfile import unittest from collections import defaultdict from pathlib import Path ROOT = Path(__file__).resolve().parents[3] SKILL_ROOT = ROOT / "skills" / "tsl-api-reference" LOOKUP = SKILL_ROOT / "scripts" / "lookup.py" DICTIONARY_LOOKUP = SKILL_ROOT / "scripts" / "dictionary_lookup.py" class ValueDomainCliTest(unittest.TestCase): def run_cli(self, script, *args): return subprocess.run( [sys.executable, str(script), *args], cwd=ROOT, capture_output=True, text=True, encoding="utf-8", check=False, ) def test_dictionary_query_resolves_classification_by_label_and_code(self): for query in ("申万煤炭", "SWHY740000"): with self.subTest(query=query): result = self.run_cli(DICTIONARY_LOOKUP, "--query", query) self.assertEqual(0, result.returncode, result.stderr) self.assertIn("status: ok", result.stdout) self.assertIn("类型:参数取值", result.stdout) self.assertIn( "参数域:classification_code(分类属性代码)", result.stdout, ) self.assertIn("取值:SWHY740000", result.stdout) self.assertIn("名称:申万煤炭", result.stdout) self.assertIn("父级:SWHY(申万行业)", result.stdout) self.assertIn("层级:1", result.stdout) self.assertIn("关联表:138 股票.股票行业分类信息", result.stdout) self.assertIn("快照日期:2023-10-13", result.stdout) self.assertIn("来源:faq:31494", result.stdout) def test_dictionary_query_uses_strong_values_without_hijacking_table_names(self): for query in ("SWHY740000 是什么", "申万煤炭分类代码"): with self.subTest(query=query): result = self.run_cli(DICTIONARY_LOOKUP, "--query", query) self.assertEqual(0, result.returncode, result.stderr) self.assertIn("类型:参数取值", result.stdout) self.assertIn("取值:SWHY740000", result.stdout) table = self.run_cli(DICTIONARY_LOOKUP, "--query", "申万行业配置") self.assertEqual(0, table.returncode, table.stderr) self.assertIn("类型:table", table.stdout) self.assertIn("表 ID:629", table.stdout) self.assertNotIn("类型:参数取值", table.stdout) def test_pattern_punctuation_is_significant(self): pattern = self.run_cli(DICTIONARY_LOOKUP, "--query", ".N") single_letter = self.run_cli(DICTIONARY_LOOKUP, "--query", "N") self.assertEqual(0, pattern.returncode, pattern.stderr) self.assertIn("类型:参数取值", pattern.stdout) self.assertIn("取值:<分类属性代码>.N", pattern.stdout) self.assertEqual(0, single_letter.returncode, single_letter.stderr) self.assertNotIn("类型:参数取值", single_letter.stdout) def test_shared_board_type_enum_maps_multiple_api_parameters(self): label = self.run_cli(DICTIONARY_LOOKUP, "--query", "申万三级行业") numeric = self.run_cli(DICTIONARY_LOOKUP, "--query", "板块类别 7") bare_numeric = self.run_cli(DICTIONARY_LOOKUP, "--query", "7") api = self.run_cli(LOOKUP, "--name", "getbktypelist") bare_lookup = self.run_cli(LOOKUP, "--kw", "7") self.assertEqual(0, label.returncode, label.stderr) self.assertIn("status: ambiguous", label.stdout) self.assertIn("取值:7", label.stdout) self.assertIn("名称:申万三级行业", label.stdout) self.assertIn("绑定 API:getbktypelist.bktype; stocksbklist.bktype", label.stdout) self.assertIn("字段:申万三级行业", label.stdout) self.assertEqual(0, numeric.returncode, numeric.stderr) self.assertIn("名称:申万三级行业", numeric.stdout) self.assertEqual(0, bare_numeric.returncode, bare_numeric.stderr) self.assertNotIn("类型:参数取值", bare_numeric.stdout) self.assertEqual(0, api.returncode, api.stderr) self.assertIn("market_board_type", api.stdout) self.assertIn("完整", api.stdout) self.assertIn("`7` — 申万三级行业", api.stdout) self.assertEqual(0, bare_lookup.returncode, bare_lookup.stderr) self.assertNotIn("参数取值域:getbktypelist.bktype", bare_lookup.stdout) def test_historical_market_board_codes_use_the_shared_domain(self): dictionary = self.run_cli(DICTIONARY_LOOKUP, "--query", "TSI000001") lookup = self.run_cli(LOOKUP, "--kw", "TSI000001") self.assertEqual(0, dictionary.returncode, dictionary.stderr) self.assertIn("取值:TSI000001", dictionary.stdout) self.assertIn("名称:A股板块", dictionary.stdout) self.assertIn("绑定 API:getBkByDate.index_id", dictionary.stdout) self.assertEqual(0, lookup.returncode, lookup.stderr) self.assertIn("历史市场板块代码:TSI000001(A股板块)", lookup.stdout) self.assertNotIn("参数取值域:getbktypelist.bktype", lookup.stdout) def test_exact_code_value_ignores_incidental_dictionary_id_substrings(self): result = self.run_cli(DICTIONARY_LOOKUP, "--query", "SWHY740000") self.assertEqual(0, result.returncode, result.stderr) self.assertIn("status: ok", result.stdout) self.assertIn("取值:SWHY740000", result.stdout) self.assertNotIn("字段:截止日", result.stdout) def test_named_historical_board_domain_is_complete_and_api_specific(self): dictionary = self.run_cli(DICTIONARY_LOOKUP, "--query", "中小企业板") lookup = self.run_cli(LOOKUP, "--kw", "中小企业板") exact = self.run_cli(LOOKUP, "--name", "getAbkbyDate") self.assertEqual(0, dictionary.returncode, dictionary.stderr) self.assertIn("绑定 API:getAbkbyDate.bk_name", dictionary.stdout) self.assertIn("2021-04-06 并入主板", dictionary.stdout) self.assertEqual(0, lookup.returncode, lookup.stderr) self.assertIn("历史市场板块名:中小企业板", lookup.stdout) self.assertEqual(0, exact.returncode, exact.stderr) self.assertIn("historical_a_share_board_name", exact.stdout) self.assertIn("`中小企业板` — 中小企业板(历史兼容)", exact.stdout) def test_scope_filters_multi_table_value_domains(self): stock = self.run_cli( DICTIONARY_LOOKUP, "--query", "属性代码", "--scope", "stock" ) fund = self.run_cli( DICTIONARY_LOOKUP, "--query", "属性代码", "--scope", "fund" ) self.assertEqual(0, stock.returncode, stock.stderr) self.assertIn("关联表:138 股票.股票行业分类信息", stock.stdout) self.assertNotIn("355 基金.基金分类信息", stock.stdout) self.assertEqual(0, fund.returncode, fund.stderr) self.assertIn("关联表:355 基金.基金分类信息", fund.stdout) self.assertNotIn("138 股票.股票行业分类信息", fund.stdout) def test_keyword_query_maps_value_to_current_and_historical_apis(self): result = self.run_cli(LOOKUP, "--kw", "申万", "煤炭") self.assertEqual(0, result.returncode, result.stderr) self.assertIn("getBk\tfunction\tgetBk(marketlist)", result.stdout) self.assertIn( "getBkByDate\tfunction\tgetBkByDate(index_id, end_t, extype)", result.stdout, ) self.assertIn("getBk.marketlist", result.stdout) self.assertIn("当前板块候选:申万煤炭", result.stdout) self.assertIn("运行时核验", result.stdout) self.assertIn("getBkByDate.index_id", result.stdout) self.assertIn("历史分类代码:SWHY740000", result.stdout) def test_keyword_annotations_only_include_the_top_value_group(self): exact_code = self.run_cli(LOOKUP, "--kw", "SWHY740000") domain_query = self.run_cli(LOOKUP, "--kw", "属性", "代码") self.assertEqual(0, exact_code.returncode, exact_code.stderr) self.assertIn("历史分类代码:SWHY740000", exact_code.stdout) self.assertNotIn("历史分类代码:SWHY(申万行业)", exact_code.stdout) self.assertEqual(0, domain_query.returncode, domain_query.stderr) self.assertIn("目录选择器:属性代码", domain_query.stdout) self.assertIn("下级分类代码模式:<分类属性代码>.N", domain_query.stdout) self.assertNotIn("历史分类代码:CAPCHY", domain_query.stdout) def test_exact_getbk_query_explains_runtime_catalog_boundary(self): result = self.run_cli(LOOKUP, "--name", "getBk") self.assertEqual(0, result.returncode, result.stderr) self.assertIn("参数取值域", result.stdout) self.assertIn("marketlist", result.stdout) self.assertIn("runtime_catalog", result.stdout) self.assertIn("静态记录不完整", result.stdout) self.assertIn("getBkList2", result.stdout) self.assertIn("getUserBkList2", result.stdout) self.assertIn("按值查询", result.stdout) self.assertNotIn("`申万煤炭`", result.stdout) def test_runtime_domain_query_returns_resolvers_not_unrelated_market_codes(self): result = self.run_cli(DICTIONARY_LOOKUP, "--query", "market_board") self.assertEqual(0, result.returncode, result.stderr) self.assertIn("status: ok", result.stdout) self.assertIn("类型:参数取值域", result.stdout) self.assertIn("参数域:market_board", result.stdout) self.assertIn("绑定 API:getBk.marketlist", result.stdout) self.assertIn("getBkList2(bktype)(系统目录)", result.stdout) self.assertIn("getUserBkList2(bktype)(用户目录)", result.stdout) self.assertIn("申万煤炭", result.stdout) self.assertNotIn("TSI000001", result.stdout) def test_domain_label_query_returns_one_domain_summary(self): result = self.run_cli(DICTIONARY_LOOKUP, "--query", "分类属性代码") self.assertEqual(0, result.returncode, result.stderr) self.assertIn("status: ok", result.stdout) self.assertIn("参数域:classification_code", result.stdout) self.assertIn("绑定 API:getBkByDate.index_id", result.stdout) self.assertIn("SWHY740000(申万煤炭)", result.stdout) self.assertNotIn("类型:field", result.stdout) def test_exact_api_query_does_not_expand_versioned_catalog(self): result = self.run_cli(LOOKUP, "--name", "getBkByDate") self.assertEqual(0, result.returncode, result.stderr) self.assertIn("分类属性代码", result.stdout) self.assertIn("已记录值或候选:7 条", result.stdout) self.assertIn("dictionary_lookup.py --query", result.stdout) parameter_domain = result.stdout.split("### 参数取值域", maxsplit=1)[1] self.assertNotIn("`SWHY740000`", parameter_domain) def test_invalid_value_domain_data_is_a_deployment_error(self): with tempfile.TemporaryDirectory() as temp_dir: invalid = Path(temp_dir) / "value_domains.json" invalid.write_text('{"version": 1, "domains": []}\n', encoding="utf-8") for script, action in ( (LOOKUP, ("--kw", "申万")), (DICTIONARY_LOOKUP, ("--query", "申万")), ): with self.subTest(script=script.name): result = self.run_cli( script, *action, "--value-domains", str(invalid), ) self.assertEqual(1, result.returncode) self.assertEqual("", result.stdout) self.assertIn("domains must not be empty", result.stderr) self.assertNotIn("Traceback", result.stderr) def test_parameter_domain_binding_does_not_cross_scope_or_module(self): with tempfile.TemporaryDirectory() as temp_dir: data_dir = Path(temp_dir) / "data" data_dir.mkdir() tsv = data_dir / "function_index.tsv" tsv.write_text( "name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n" "getBk\tdotnet\tdatawarehouse\tgetBk(marketlist)\t" "dotnet/market.md\tgetbk\t\t系统板块\n" "getBk\tdotnet\tdatawarehouse\tgetBk()\t" "dotnet/market.md\tgetbk-empty\t\t无参数重载\n" "getBk\tproject\tdemo\tgetBk(name)\tproject/demo.md\t" "getbk\t\t项目函数\n", encoding="utf-8", ) domains = data_dir / "value_domains.json" domains.write_text( json.dumps( { "version": 1, "domains": [ { "id": "market_board", "label": "市场板块", "mode": "runtime_catalog", "complete": False, "bindings": [ { "scope": "dotnet", "module": "datawarehouse", "api": "getBk", "parameter": "marketlist", "role": "current_components", } ], "resolvers": [ { "scope": "dotnet", "module": "datawarehouse", "api": "getBkList2", "parameter": "bktype", "catalog": "system", "source": "net_function:28964", } ], "related_tables": [], "values": [ { "value": "申万煤炭", "label": "申万煤炭", "sources": ["faq:31494"], } ], "sources": ["faq:31494"], "as_of": "2026-08-20", } ], }, ensure_ascii=False, ), encoding="utf-8", ) result = self.run_cli( LOOKUP, "--kw", "申万煤炭", "--tsv", str(tsv), "--value-domains", str(domains), ) self.assertEqual(0, result.returncode, result.stderr) self.assertIn("dotnet/market.md#getbk", result.stdout) self.assertNotIn("dotnet/market.md#getbk-empty", result.stdout) self.assertNotIn("project/demo.md#getbk", result.stdout) def test_exact_query_attaches_domain_only_to_overload_with_bound_parameter(self): with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) data_dir = root / "data" codegen = root / "references" / "codegen" / "dotnet" data_dir.mkdir() codegen.mkdir(parents=True) tsv = data_dir / "function_index.tsv" tsv.write_text( "name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n" "getBk\tdotnet\tdatawarehouse\tgetBk(marketlist)\t" "dotnet/market.md\tgetbk\t\t系统板块\n" "getBk\tdotnet\tdatawarehouse\tgetBk()\t" "dotnet/market.md\tgetbk-1\t\t无参数重载\n", encoding="utf-8", ) (codegen / "market.md").write_text( "# Dotnet\n\n" "## `getBk(marketlist)`\n\n声明:function\n\n系统板块\n\n" "## `getBk()`\n\n声明:function\n\n无参数重载\n", encoding="utf-8", ) domains = data_dir / "value_domains.json" domains.write_text( json.dumps( { "version": 1, "domains": [ { "id": "market_board", "label": "市场板块", "mode": "runtime_catalog", "complete": False, "bindings": [ { "scope": "dotnet", "module": "datawarehouse", "api": "getBk", "parameter": "marketlist", "role": "current_components", } ], "resolvers": [ { "scope": "dotnet", "module": "datawarehouse", "api": "getBkList2", "parameter": "bktype", "catalog": "system", "source": "net_function:28964", } ], "related_tables": [], "values": [], "sources": ["net_function:28960"], "as_of": "2026-08-20", } ], }, ensure_ascii=False, ), encoding="utf-8", ) result = self.run_cli( LOOKUP, "--name", "getBk", "--tsv", str(tsv), "--value-domains", str(domains), ) self.assertEqual(0, result.returncode, result.stderr) self.assertIn("## `getBk(marketlist)`", result.stdout) self.assertIn("## `getBk()`", result.stdout) self.assertEqual(1, result.stdout.count("### 参数取值域")) def test_exact_query_does_not_attach_domain_to_same_name_in_other_scope(self): with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) data_dir = root / "data" codegen = root / "references" / "codegen" (codegen / "dotnet").mkdir(parents=True) (codegen / "project").mkdir(parents=True) data_dir.mkdir() tsv = data_dir / "function_index.tsv" tsv.write_text( "name\tscope\tmodule\tsignature\tpage\tanchor\ttags\tsummary\n" "getBk\tdotnet\tdatawarehouse\tgetBk(marketlist)\t" "dotnet/market.md\tgetbk\t\t系统板块\n" "getBk\tproject\tdemo\tgetBk(name)\tproject/demo.md\t" "getbk\t\t项目函数\n", encoding="utf-8", ) (codegen / "dotnet" / "market.md").write_text( "# Dotnet\n\n## `getBk(marketlist)`\n\n声明:function\n\n系统板块\n", encoding="utf-8", ) (codegen / "project" / "demo.md").write_text( "# Project\n\n## `getBk(name)`\n\n声明:function\n\n项目函数\n", encoding="utf-8", ) domains = data_dir / "value_domains.json" domains.write_text( json.dumps( { "version": 1, "domains": [ { "id": "market_board", "label": "市场板块", "mode": "runtime_catalog", "complete": False, "bindings": [ { "scope": "dotnet", "module": "datawarehouse", "api": "getBk", "parameter": "marketlist", "role": "current_components", } ], "resolvers": [ { "scope": "dotnet", "module": "datawarehouse", "api": "getBkList2", "parameter": "bktype", "catalog": "system", "source": "net_function:28964", } ], "related_tables": [], "values": [], "sources": ["net_function:28960"], "as_of": "2026-08-20", } ], }, ensure_ascii=False, ), encoding="utf-8", ) result = self.run_cli( LOOKUP, "--scope", "project", "--name", "getBk", "--tsv", str(tsv), "--value-domains", str(domains), ) self.assertEqual(0, result.returncode, result.stderr) self.assertIn("项目函数", result.stdout) self.assertNotIn("参数取值域", result.stdout) def test_bundled_value_domains_reference_existing_apis_parameters_and_tables(self): document = json.loads( (SKILL_ROOT / "data" / "value_domains.json").read_text(encoding="utf-8") ) with (SKILL_ROOT / "data" / "function_index.tsv").open( encoding="utf-8", newline="" ) as handle: rows = list(csv.DictReader(handle, delimiter="\t")) with (SKILL_ROOT / "data" / "dictionary_index.tsv").open( encoding="utf-8", newline="" ) as handle: dictionary_rows = list(csv.DictReader(handle, delimiter="\t")) apis = defaultdict(set) for row in rows: match = re.search(r"\((.*)\)", row["signature"]) if not match: continue key = ( row["scope"].casefold(), row["module"].casefold(), row["name"].casefold(), ) for parameter in match.group(1).split(","): parameter = parameter.strip().strip("[]") if parameter: apis[key].add(parameter.casefold()) table_pages = { row["table_id"]: row["page"] for row in dictionary_rows if row["kind"] in {"table", "source"} and row["table_id"] } for domain in document["domains"]: for binding in domain["bindings"]: self.assert_api_parameter_exists(apis, binding) for resolver in domain["resolvers"]: self.assert_api_parameter_exists(apis, resolver) for table in domain["related_tables"]: self.assertEqual(table["page"], table_pages.get(table["id"])) self.assertTrue((SKILL_ROOT / table["page"]).is_file()) for value in domain["values"]: for relation in value.get("relations", []): self.assert_api_parameter_exists(apis, relation["verification"]) def assert_api_parameter_exists(self, apis, reference): key = ( reference["scope"].casefold(), reference["module"].casefold(), (reference.get("api") or reference.get("resolver")).casefold(), ) parameter = reference.get("parameter") self.assertIn(key, apis) if parameter: self.assertIn(parameter.casefold(), apis[key]) if __name__ == "__main__": unittest.main()