import ast import csv import json import re import subprocess import sys import tempfile import unittest from pathlib import Path ROOT = Path(__file__).resolve().parents[1] SKILL_ROOT = ROOT / "skills" / "tsl-api-reference" CLASS_LOOKUP = SKILL_ROOT / "scripts" / "class_lookup.py" API_LOOKUP = SKILL_ROOT / "scripts" / "lookup.py" FRAMEWORK_INDEX = SKILL_ROOT / "data" / "framework_index.json" FUNCTION_INDEX = SKILL_ROOT / "data" / "function_index.tsv" CODEGEN_ROOT = SKILL_ROOT / "references" / "codegen" def run_script(script, *args): return subprocess.run( [sys.executable, str(script), *args], cwd=ROOT, capture_output=True, text=True, ) def indexed_class_count(): with FUNCTION_INDEX.open(encoding="utf-8", newline="") as handle: return sum( row["kind"] == "class" for row in csv.DictReader(handle, delimiter="\t") ) class TslApiFrameworkTests(unittest.TestCase): def test_web_development_support_functions_are_removed(self): removed = { "Web_tb_arrow", "Web_tb_bar", "Web_tb_gradient", "Web_tb_npbar", "Web_tb_npgridbar", "Web_css_bgbar", "Web_css_bgcolor", "Web_css_bghist", "Web_richtext_zdbgcolor", "Web_spark_hist", "Web_spark_nphist", "Web_fig_box", "Web_fig_heatmap", "Web_fig_histogram", "Web_fig_radar", "Web_fig_tree_decomp", "Web_fig_violin", "Demo_framestyle_constyle_dotweb", "Demo_framestyle_constyle_net", "Demo_framestyle_heatmaptb_net", "Demo_framestyle_net", "Demo_framestyle_web", "Demo_framestyle_data", "Kerneldensityestimator", "Web_color_theme", "Web_color_triscale", "Web_dict2tabletree", "Web_format", "Web_getcolorscalebyrgbarr", "Web_getdefultcolor", "Web_getlineargradient", "Web_gettdbar", "Web_html_span", "Web_html_table", "Web_table2dicttree", "web_metric_TranValuesWithGroup", "tsplot_PerformanceSummary", "tssubplot_lines", } with FUNCTION_INDEX.open(encoding="utf-8", newline="") as handle: indexed = { row["name"].casefold() for row in csv.DictReader(handle, delimiter="\t") } headings = set() for path in CODEGEN_ROOT.rglob("*.md"): text = path.read_text(encoding="utf-8") headings.update( match.group(1).split("(", 1)[0].casefold() for match in re.finditer( r"^## `([^`]+)`", text, re.MULTILINE ) ) self.assertTrue({name.casefold() for name in removed}.isdisjoint(indexed)) self.assertTrue({name.casefold() for name in removed}.isdisjoint(headings)) self.assertTrue( {"barline_web", "fastline_web", "pieline_web"} <= indexed ) def test_recent_update_source_names_do_not_leak_into_api_taxonomy(self): fallback_pages = [ path.relative_to(CODEGEN_ROOT).as_posix() for path in CODEGEN_ROOT.rglob("recent-updates-*.md") ] with FUNCTION_INDEX.open(encoding="utf-8", newline="") as handle: fallback_rows = [ row["page"] for row in csv.DictReader(handle, delimiter="\t") if "recent-updates-" in row["page"] or row["module"].startswith("recent-updates-") ] self.assertEqual([], fallback_pages) self.assertEqual([], fallback_rows) def test_recent_financial_functions_have_normalized_signatures_and_sources(self): expected = { "adjustmentFactor": ( "AdjustmentFactor", "adjustmentFactor(rate_day)", "dotnet/financial/market-specified_date.md", ), "stockZtOrDtCloseOfMD": ( "StockZtOrDtCloseOfMD", "stockZtOrDtCloseOfMD(flag)", "dotnet/financial/market-specified_date.md", ), "getEtfListByDate": ( "GetETFListByDate", "getEtfListByDate(bk_name, end_t)", "dotnet/financial/fund-etf.md", ), "getIndexAdjustDate": ( "GetIndexAdjustDate", "getIndexAdjustDate(index_id, beg_t, end_t)", "dotnet/financial/index-index_info.md", ), "fi_report_excessYields": ( "fi_report_ExcessYields", "fi_report_excessYields(rt_data, fname_pf, fname_bm, " "fname_date, plot_type)", "dotnet/financial/financial_engineering-factor_research-" "performance_analysis-return_evaluation-intermediate-" "common_reports.md", ), "fi_report_corrAnalysis": ( "fi_report_CorrAnalysis", "fi_report_corrAnalysis(rt_data, fname_date)", "dotnet/financial/financial_engineering-factor_research-" "performance_analysis-return_evaluation-intermediate-" "common_reports.md", ), "fi_report_historicalPerformance": ( "fi_report_HistoricalPerformance", "fi_report_historicalPerformance(rt_data, fname_bm, " "fname_date, cycle, factor_roll_n_year)", "dotnet/financial/financial_engineering-factor_research-" "performance_analysis-return_evaluation-intermediate-" "common_reports.md", ), "fi_report_periodReturn": ( "fi_report_PeriodReturn", "fi_report_periodReturn(rt_data, fname_date)", "dotnet/financial/financial_engineering-factor_research-" "performance_analysis-return_evaluation-intermediate-" "common_reports.md", ), "fi_report_returnAndRisk": ( "fi_report_ReturnAndRisk", "fi_report_returnAndRisk(rt_data, fname_date, qk_type, cycle, " "days_in_1_y)", "dotnet/financial/financial_engineering-factor_research-" "performance_analysis-return_evaluation-intermediate-" "common_reports.md", ), "fi_report_riskFeature": ( "fi_report_RiskFeature", "fi_report_riskFeature(rt_data, fname_bm, fname_date, " "days_in_1_y)", "dotnet/financial/financial_engineering-factor_research-" "performance_analysis-return_evaluation-intermediate-" "common_reports.md", ), "fi_report_rollReturn": ( "fi_report_RollReturn", "fi_report_rollReturn(rt_data, fname_date, cycle)", "dotnet/financial/financial_engineering-factor_research-" "performance_analysis-return_evaluation-intermediate-" "common_reports.md", ), "financialItemIn12Month_HK": ( "FinancialItemIn12Month_HK", "financialItemIn12Month_HK(r_date, info_id, " "if_to_func_currency)", "dotnet/financial/financial_analysis-extended_" "fundamental_data.md", ), "reportType_HK": ( "ReportType_HK", "reportType_HK(r_date, r_type)", "dotnet/financial/financial_analysis-extended_" "fundamental_data.md", ), "originalToFunctionalCurrency_HK": ( "OriginalToFunctionalCurrency_HK", "originalToFunctionalCurrency_HK(end_t, original_value, " "original_currency)", "dotnet/financial/financial_analysis-extended_" "fundamental_data.md", ), } with FUNCTION_INDEX.open(encoding="utf-8", newline="") as handle: rows = list(csv.DictReader(handle, delimiter="\t")) by_name = {row["name"]: row for row in rows} self.assertEqual(set(expected), set(expected) & set(by_name)) for name, (query_name, signature, page) in expected.items(): with self.subTest(name=name): row = by_name[name] self.assertEqual("dotnet", row["scope"]) self.assertEqual("financial", row["module"]) self.assertEqual(signature, row["signature"]) self.assertEqual(page, row["page"]) result = run_script(API_LOOKUP, "--name", query_name) self.assertEqual(0, result.returncode, result.stderr) self.assertIn(f"## `{signature}`", result.stdout) self.assertIn(f"{page}#", result.stdout) parameter_text = signature.partition("(")[2].removesuffix(")") for parameter in filter(None, parameter_text.split(", ")): self.assertRegex( parameter, r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$", ) parameter_types = [] for line in result.stdout.splitlines(): match = re.fullmatch( r"\| `[^`]+`\s+\|\s+([^|]+?)\s+\|.*", line, ) if match: parameter_types.append(match.group(1).strip()) self.assertTrue(parameter_types) self.assertTrue( all(value == value.casefold() for value in parameter_types) ) return_types = [ line.removeprefix("返回:").strip() for line in result.stdout.splitlines() if line.startswith("返回:") ] self.assertEqual(1, len(return_types)) self.assertEqual(return_types[0], return_types[0].casefold()) for name in ( "financialItemIn12Month_HK", "reportType_HK", "originalToFunctionalCurrency_HK", ): self.assertIn("_HK", by_name[name]["signature"]) def test_class_index_covers_all_indexed_class_kinds(self): result = run_script(CLASS_LOOKUP, "--list", "--format", "json") self.assertEqual(0, result.returncode, result.stderr) packet = json.loads(result.stdout) self.assertEqual(indexed_class_count(), packet["count"]) classes = { (item["scope"], item["qualified_name"]): item for item in packet["classes"] } self.assertIn(("builtin", "TStringList"), classes) self.assertIn(("module", "tsBackTesting"), classes) self.assertIn(("module", "opDeltaHedging"), classes) self.assertIn(("module", "tsLargeAndSmallTurning"), classes) self.assertIn(("module", "regime_Based"), classes) self.assertIn(("module", "ml_Clock"), classes) self.assertIn(("module", "gaussianNB"), classes) self.assertIn(("module", "decisionTreeClassifier"), classes) self.assertIn(("module", "mlpClassifier"), classes) self.assertIn(("module", "svc"), classes) self.assertIn(("module", "brinson_PerforAttri"), classes) self.assertIn(("module", "cb_BinaryTree"), classes) self.assertIn(("module", "ts_ChipDistribution"), classes) self.assertIn(("module", "tsPositionMeasure"), classes) self.assertIn(("module", "ts_SameTypeFundEquityPercent"), classes) self.assertIn(("module", "ts_AIPBackTesting"), classes) self.assertIn(("dotnet", "tsOptimizer"), classes) self.assertTrue(classes[("module", "tsBackTesting")]["profile_available"]) self.assertTrue( classes[("module", "ts_SameTypeFundEquityPercent")][ "profile_available" ] ) self.assertTrue( classes[("module", "ts_AIPBackTesting")]["profile_available"] ) self.assertFalse(classes[("builtin", "TStringList")]["profile_available"]) def test_recovered_recent_update_classes_keep_member_ownership(self): with FUNCTION_INDEX.open(encoding="utf-8", newline="") as handle: rows = list(csv.DictReader(handle, delimiter="\t")) expected_owners = { "module/derivatives-analysis/option-delta-hedging.md": { "opDeltaHedging" }, "module/tinysoft-strategies/market-cap-rotation.md": { "tsLargeAndSmallTurning" }, "module/portfolio-optimization/investment-clock.md": { "regime_Based", "ml_Clock", }, } for page, owners in expected_owners.items(): with self.subTest(page=page): members = [ row for row in rows if row["page"] == page and row["kind"] != "class" ] self.assertTrue(members) self.assertTrue(all(row["owner"] in owners for row in members)) flattened_names = { "GetOptionDelta", "GetRegularAlpha", "Anova", "GetState_Division", } self.assertFalse( any( row["page"] in expected_owners and row["name"] in flattened_names and not row["owner"] for row in rows ) ) def test_recovered_domain_classes_and_wrappers_keep_their_boundaries(self): with FUNCTION_INDEX.open(encoding="utf-8", newline="") as handle: rows = list(csv.DictReader(handle, delimiter="\t")) owned = { "brinson_PerforAttri.GetBenchmark": "brinson_PerforAttri", "cb_BinaryTree.CBValuation": "cb_BinaryTree", "ts_ChipDistribution.ChipMeasure": "ts_ChipDistribution", "tsPositionMeasure.PortfolioOptimize": "tsPositionMeasure", "ts_SameTypeFundEquityPercent.GetFundArr": ( "ts_SameTypeFundEquityPercent" ), "ts_AIPBackTesting.CumReturn": "ts_AIPBackTesting", } for qualified_name, owner in owned.items(): with self.subTest(qualified_name=qualified_name): matches = [ row for row in rows if row["qualified_name"] == qualified_name ] self.assertTrue(matches) self.assertTrue(all(row["owner"] == owner for row in matches)) wrappers = { "brinson_perforAttri_demo", "op_underlying_hv", "Index_TA_ChipDistribution", "CB_CCB", "CB_GetRiskTarget_CCB", "getstockVal", } for name in wrappers: with self.subTest(name=name): matches = [row for row in rows if row["name"] == name] self.assertTrue(matches) self.assertTrue(all(not row["owner"] for row in matches)) flattened_members = { "GetBenchmark", "CBValuation", "ChipMeasure", "PortfolioOptimize", "GetFundArr", "CumReturn", } self.assertFalse( any( row["scope"] == "module" and row["name"] in flattened_members and row["page"].startswith("module/") and not row["owner"] for row in rows ) ) def test_ts_ops_indexes_runtime_class_without_example_implementation(self): with FUNCTION_INDEX.open(encoding="utf-8", newline="") as handle: rows = list(csv.DictReader(handle, delimiter="\t")) page_rows = [ row for row in rows if row["page"] == "module/ts-ops.md" ] self.assertIn( ("opsServer", "class", ""), { (row["qualified_name"], row["kind"], row["owner"]) for row in page_rows }, ) expected_members = { "opsServer.FOpsId", "opsServer.FAsyncResults", "opsServer.FLastId", "opsServer.FDataUrl", "opsServer.FOPSList", "opsServer.FOPSName", "opsServer.FCallBack", "opsServer.FSubCallBack", "opsServer.FReturnRuntimeError", "opsServer.FDebug", "opsServer.create", "opsServer.CallBack", "opsServer.WaitSubscription", "opsServer.AsyncCall", "opsServer.DoOnMessage", } self.assertTrue( expected_members <= {row["qualified_name"] for row in page_rows} ) self.assertTrue( all( row["owner"] == "opsServer" for row in page_rows if row["kind"] != "class" ) ) example_names = { "myopsserver2", "hahaha2", "getmydata", "my_multiply", } self.assertFalse( any(row["name"] in example_names for row in page_rows) ) ops_call = [ row for row in rows if row["name"] == "opsCall" and row["page"] == "module/ts-opi.md" ] self.assertTrue(ops_call) self.assertTrue( all(row["kind"] == "function" and not row["owner"] for row in ops_call) ) def test_recovered_machine_learning_classes_have_core_members(self): expected = { "gaussianNB": {"create", "Fit", "Predict", "Predict_proba"}, "decisionTreeClassifier": { "create", "Fit", "Predict", "Predict_proba", }, "mlpClassifier": {"create", "Fit", "Predict", "Score"}, "svc": {"create", "Fit", "Predict", "Predict_proba", "Score"}, } for class_name, required_members in expected.items(): with self.subTest(class_name=class_name): result = run_script( CLASS_LOOKUP, "--class", class_name, "--include-members", "--format", "json", ) self.assertEqual(0, result.returncode, result.stderr) members = { item["name"] for item in json.loads(result.stdout)["members"] } self.assertTrue(required_members <= members) def test_generic_class_uses_compact_member_view_without_fake_lifecycle(self): result = run_script( CLASS_LOOKUP, "--class", "TStringList", "--format", "json", ) self.assertEqual(0, result.returncode, result.stderr) packet = json.loads(result.stdout) self.assertEqual("resolved", packet["status"]) self.assertEqual("not_profiled", packet["profile_status"]) self.assertIsNone(packet["profile"]) self.assertEqual({"method": 25, "property": 15}, packet["member_summary"]) def test_custom_index_without_profiles_queries_generic_class(self): fieldnames = [ "name", "scope", "module", "signature", "page", "anchor", "tags", "summary", "kind", "binding", "visibility", "owner", "qualified_name", ] rows = [ { "name": "MyClass", "scope": "project", "module": "demo", "signature": "MyClass", "page": "project/demo.md", "anchor": "myclass", "tags": "示例 class", "summary": "示例类。", "kind": "class", "binding": "", "visibility": "", "owner": "", "qualified_name": "MyClass", }, { "name": "Run", "scope": "project", "module": "demo", "signature": "Run()", "page": "project/demo.md", "anchor": "run", "tags": "运行", "summary": "运行示例。", "kind": "method", "binding": "instance", "visibility": "public", "owner": "MyClass", "qualified_name": "MyClass.Run", }, ] with tempfile.TemporaryDirectory() as temp_dir: data_dir = Path(temp_dir) / "data" data_dir.mkdir() tsv_path = data_dir / "function_index.tsv" with tsv_path.open("w", encoding="utf-8", newline="") as handle: writer = csv.DictWriter( handle, fieldnames=fieldnames, delimiter="\t", lineterminator="\n", ) writer.writeheader() writer.writerows(rows) result = run_script( CLASS_LOOKUP, "--tsv", str(tsv_path), "--class", "MyClass", "--include-members", "--format", "json", ) self.assertEqual(0, result.returncode, result.stderr) packet = json.loads(result.stdout) self.assertEqual("resolved", packet["status"]) self.assertEqual("not_profiled", packet["profile_status"]) self.assertIsNone(packet["profile"]) self.assertEqual({"method": 1}, packet["member_summary"]) self.assertEqual("MyClass.Run", packet["members"][0]["qualified_name"]) def test_explicit_missing_framework_file_remains_a_data_error(self): with tempfile.TemporaryDirectory() as temp_dir: missing = Path(temp_dir) / "missing-framework-index.json" result = run_script( CLASS_LOOKUP, "--profiles", str(missing), "--class", "TStringList", ) self.assertEqual(1, result.returncode) self.assertIn("failed to load", result.stderr) def test_configuration_on_generic_class_requires_curated_profile(self): result = run_script( CLASS_LOOKUP, "--class", "TStringList", "--config", "Mode=1", "--format", "json", ) self.assertEqual(0, result.returncode, result.stderr) packet = json.loads(result.stdout) self.assertEqual("not_profiled", packet["profile_status"]) self.assertIn( "CLASS_NOT_PROFILED", {item["code"] for item in packet["diagnostics"]}, ) def test_profiled_class_adds_framework_lifecycle(self): result = run_script( CLASS_LOOKUP, "--class", "tsBackTesting", "--format", "json", ) self.assertEqual(0, result.returncode, result.stderr) packet = json.loads(result.stdout) self.assertEqual("resolved", packet["profile_status"]) self.assertEqual("resolved", packet["profile"]["scaffold_status"]) self.assertEqual("resolved", packet["profile"]["contract_status"]) self.assertEqual( ["construct", "configure", "schedule", "callback", "execute", "inspect"], [phase["phase"] for phase in packet["profile"]["lifecycle"]], ) def test_non_backtest_framework_archetypes_have_profiles(self): expected = { "tsbf_ReturnAttribution": { "phases": ["construct", "configure", "schedule", "callback", "inspect"], "contract_status": "incomplete", }, "ts_EvaluationOfRotationSkill": { "phases": ["construct", "execute", "inspect"], "contract_status": "resolved", }, "ts_IndustryAllocationOfFund": { "phases": ["construct", "configure", "execute"], "contract_status": "resolved", }, "ts_SameTypeFundEquityPercent": { "phases": [ "construct", "configure", "schedule", "callback", "execute", "inspect", ], "contract_status": "incomplete", }, "ts_AIPBackTesting": { "phases": [ "construct", "configure", "schedule", "callback", "execute", "inspect", ], "contract_status": "incomplete", }, } for class_name, contract in expected.items(): with self.subTest(class_name=class_name): result = run_script( CLASS_LOOKUP, "--class", class_name, "--format", "json", ) self.assertEqual(0, result.returncode, result.stderr) profile = json.loads(result.stdout)["profile"] self.assertEqual( contract["phases"], [phase["phase"] for phase in profile["lifecycle"]], ) self.assertEqual( contract["contract_status"], profile["contract_status"] ) def test_tsoptimizer_resolves_after_module_migration(self): unfiltered = run_script( CLASS_LOOKUP, "--class", "tsOptimizer", "--format", "json", ) filtered = run_script( CLASS_LOOKUP, "--class", "tsOptimizer", "--scope", "dotnet", "--module", "math", "--format", "json", ) self.assertEqual(0, unfiltered.returncode, unfiltered.stderr) unfiltered_packet = json.loads(unfiltered.stdout) self.assertEqual("resolved", unfiltered_packet["status"]) self.assertEqual("dotnet", unfiltered_packet["class"]["scope"]) self.assertEqual("math", unfiltered_packet["class"]["module"]) self.assertEqual(0, filtered.returncode, filtered.stderr) self.assertEqual( "dotnet", json.loads(filtered.stdout)["class"]["scope"], ) self.assertEqual( "math", json.loads(filtered.stdout)["class"]["module"], ) def test_class_check_validates_generic_and_profile_layers(self): result = run_script(CLASS_LOOKUP, "--check") self.assertEqual(0, result.returncode, result.stderr) self.assertIn(f"OK: {indexed_class_count()} classes indexed", result.stdout) self.assertIn("6 framework profile(s) validated", result.stdout) def test_framework_module_has_no_independent_cli(self): source = (SKILL_ROOT / "scripts" / "framework_lookup.py").read_text( encoding="utf-8" ) tree = ast.parse(source) functions = { node.name for node in tree.body if isinstance(node, ast.FunctionDef) } imported_modules = { alias.name for node in tree.body if isinstance(node, ast.Import) for alias in node.names } self.assertNotIn("main", functions) self.assertNotIn("argparse", imported_modules) self.assertNotIn('__name__ == "__main__"', source) def test_ts_backtesting_returns_class_first_scaffold(self): result = run_script( CLASS_LOOKUP, "--class", "tsBackTesting", "--format", "json", ) self.assertEqual(0, result.returncode, result.stderr) packet = json.loads(result.stdout) profile = packet["profile"] self.assertEqual("resolved", profile["scaffold_status"]) self.assertEqual("resolved", profile["contract_status"]) self.assertEqual( ["construct", "configure", "schedule", "callback", "execute", "inspect"], [phase["phase"] for phase in profile["lifecycle"]], ) self.assertEqual( ["tsBackTesting.GetTradeOrder"], [hook["api"] for hook in profile["hooks"] if hook["required"]], ) self.assertEqual([], profile["diagnostics"]) self.assertEqual( ["tsBackTesting.GetTradeOrder"], [contract["callback"] for contract in profile["contracts"]], ) self.assertEqual( { "api": "tsBackTesting.FGroupType", "field": "FGroupType", "values": ["1", "2"], }, profile["contracts"][0]["mode_discriminator"], ) def test_candidate_discriminator_resolves_ratio_schema(self): result = run_script( CLASS_LOOKUP, "--class", "tsBackTesting", "--config", "FGroupType=1", "--format", "json", ) self.assertEqual(0, result.returncode, result.stderr) packet = json.loads(result.stdout) profile = packet["profile"] self.assertEqual({"FGroupType": "1"}, profile["configured_values"]) self.assertEqual("resolved", profile["contract_status"]) self.assertEqual([], profile["diagnostics"]) schema = profile["contracts"][0]["selected_return_schema"] self.assertEqual("1", schema["when"]["FGroupType"]) self.assertEqual( ["截止日", "代码"], [field["name"] for field in schema["fields"] if field.get("required")], ) self.assertEqual( [-1, 1], schema["alternate_returns"][1]["documented_examples"], ) self.assertTrue( all( field.get("pdf_non_empty") for field in schema["fields"] if field["name"] in {"截止日", "代码", "方向", "比例(%)"} ) ) self.assertEqual( "FRateType", next( field["required_when"] for field in schema["fields"] if field["name"] == "比例(%)" )[0]["field"], ) def test_candidate_discriminator_resolves_quantity_schema(self): result = run_script( CLASS_LOOKUP, "--class", "tsBackTesting", "--config", "FGroupType=2", "--format", "json", ) self.assertEqual(0, result.returncode, result.stderr) profile = json.loads(result.stdout)["profile"] self.assertEqual("resolved", profile["contract_status"]) self.assertEqual([], profile["diagnostics"]) schema = profile["contracts"][0]["selected_return_schema"] self.assertEqual("2", schema["when"]["FGroupType"]) self.assertIn( "动作", [field["name"] for field in schema["fields"] if field.get("required")], ) self.assertEqual( ["empty_array"], [item["type"] for item in schema["alternate_returns"]], ) self.assertEqual( {"组 ID", "组合类型"}, { field["name"] for field in schema["fields"] if field["name"] in {"组 ID", "组合类型"} }, ) action = next(field for field in schema["fields"] if field["name"] == "动作") self.assertEqual("0", action["compat_default"]) volume = next(field for field in schema["fields"] if field["name"] == "成交量") self.assertEqual( ["32", "33"], volume["record_required_when"]["动作"], ) self.assertEqual( "FOpenVolType", next( field["required_when"] for field in schema["fields"] if field["name"] == "资金" )[0]["field"], ) def test_unknown_discriminator_value_is_reported(self): result = run_script( CLASS_LOOKUP, "--class", "tsBackTesting", "--config", "FGroupType=3", "--format", "json", ) self.assertEqual(0, result.returncode, result.stderr) profile = json.loads(result.stdout)["profile"] self.assertEqual("incomplete", profile["contract_status"]) self.assertIsNone(profile["contracts"][0]["selected_return_schema"]) self.assertIn( "CONTRACT_MODE_UNKNOWN", {item["code"] for item in profile["diagnostics"]}, ) def test_get_trade_order_reference_records_mode_specific_fields(self): result = run_script( API_LOOKUP, "--name", "tsBackTesting.GetTradeOrder", ) self.assertEqual(0, result.returncode, result.stderr) self.assertIn("FGroupType := 1", result.stdout) self.assertIn("FGroupType := 2", result.stdout) self.assertIn("`比例(%)`", result.stdout) self.assertIn("`剩余资金占比(%)`", result.stdout) self.assertIn("非数组", result.stdout) def test_quantity_mode_discriminators_are_indexed_members(self): for name in ("tsBackTesting.FOpenVolType", "tsBackTesting.FCloseVolType"): with self.subTest(name=name): result = run_script(API_LOOKUP, "--name", name) self.assertEqual(0, result.returncode, result.stderr) self.assertNotIn("No TSL API named", result.stdout) def test_unknown_configuration_is_reported(self): result = run_script( CLASS_LOOKUP, "--class", "tsBackTesting", "--config", "notAField=1", "--format", "json", ) self.assertEqual(0, result.returncode, result.stderr) packet = json.loads(result.stdout) self.assertIn( "UNKNOWN_CONFIGURATION", {item["code"] for item in packet["profile"]["diagnostics"]}, ) def test_unknown_class_uses_query_success_exit_code(self): result = run_script( CLASS_LOOKUP, "--class", "MissingFramework", "--format", "json", ) self.assertEqual(0, result.returncode, result.stderr) self.assertEqual("not_found", json.loads(result.stdout)["status"]) def test_check_rejects_stale_evidence(self): data = json.loads(FRAMEWORK_INDEX.read_text(encoding="utf-8")) data["frameworks"][0]["hooks"][0]["evidence"] = [ "module/ts-backtesting.md#missing" ] with tempfile.TemporaryDirectory() as temp_dir: index_path = Path(temp_dir) / "framework_index.json" index_path.write_text(json.dumps(data), encoding="utf-8") result = run_script( CLASS_LOOKUP, "--check", "--profiles", str(index_path), ) self.assertEqual(1, result.returncode) self.assertIn("does not identify an indexed overload", result.stderr) def test_query_rejects_invalid_profile_data(self): data = json.loads(FRAMEWORK_INDEX.read_text(encoding="utf-8")) data["frameworks"][0]["execution"][0]["evidence"] = [ "module/ts-backtesting.md#missing" ] with tempfile.TemporaryDirectory() as temp_dir: index_path = Path(temp_dir) / "framework_index.json" index_path.write_text(json.dumps(data), encoding="utf-8") result = run_script( CLASS_LOOKUP, "--class", "tsBackTesting", "--format", "json", "--profiles", str(index_path), ) self.assertEqual(1, result.returncode) self.assertEqual("", result.stdout) self.assertIn("does not identify an indexed overload", result.stderr) def test_check_rejects_profile_without_indexed_class(self): data = json.loads(FRAMEWORK_INDEX.read_text(encoding="utf-8")) data["frameworks"][0]["qualified_name"] = "MissingClass" with tempfile.TemporaryDirectory() as temp_dir: index_path = Path(temp_dir) / "framework_index.json" index_path.write_text(json.dumps(data), encoding="utf-8") result = run_script( CLASS_LOOKUP, "--check", "--profiles", str(index_path), ) self.assertEqual(1, result.returncode) self.assertIn("is not an indexed class", result.stderr) def test_ordinary_api_lookup_remains_available(self): result = run_script(API_LOOKUP, "--name", "tsBackTesting.BackTest") self.assertEqual(0, result.returncode, result.stderr) self.assertIn("### `BackTest()`", result.stdout) self.assertIn("module/ts-backtesting.md#backtest", result.stdout) if __name__ == "__main__": unittest.main()