@shundoo-ai/dsh-cosmic 1.0.19 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/cordis.patch.yml +5 -5
  2. package/package.json +1 -1
  3. package/skills/cosmic-build/CHANGELOG.md +4 -0
  4. package/skills/cosmic-build/LOCAL-PATCHES.md +91 -40
  5. package/skills/cosmic-build/SKILL.md +46 -13
  6. package/skills/cosmic-build/assets/platform-report-filters/TypedReportFilterFields.java +169 -0
  7. package/skills/cosmic-build/assets/platform-report-filters/platform.patch +98 -0
  8. package/skills/cosmic-build/assets/report-algo-benchmark/ReportAlgoBenchmark.java +48 -0
  9. package/skills/cosmic-build/assets/report-algo-benchmark/ReportBenchmarkLogFactory.java +7 -0
  10. package/skills/cosmic-build/examples/java-rules/CosmicEntryQuery.java +26 -0
  11. package/skills/cosmic-build/examples/java-rules/CosmicReportAggregation.java +35 -0
  12. package/skills/cosmic-build/examples/java-rules/CosmicReportFilters.java +77 -0
  13. package/skills/cosmic-build/examples/java-rules/CosmicTimeAdapter.java +25 -0
  14. package/skills/cosmic-build/examples/java-rules/CosmicTimeAdapterCheck.java +26 -0
  15. package/skills/cosmic-build/knowledge/cosmic/platform/api/algo/GroupbyDataSet.md +47 -0
  16. package/skills/cosmic-build/knowledge/cosmic/platform/api/entity/report/FilterInfo.md +12 -12
  17. package/skills/cosmic-build/knowledge/cosmic/platform/api/entity/report/FilterItemInfo.md +2 -0
  18. package/skills/cosmic-build/knowledge/cosmic/platform/form-plugin.md +2 -4
  19. package/skills/cosmic-build/knowledge/cosmic/platform/java-type-contracts.md +11 -2
  20. package/skills/cosmic-build/knowledge/cosmic/platform/list-plugin.md +18 -14
  21. package/skills/cosmic-build/knowledge/cosmic/platform/operation-plugin.md +4 -4
  22. package/skills/cosmic-build/knowledge/cosmic/platform/reportlistdata-plugin.md +17 -9
  23. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/cross-entity-aggregate-writeback.md +2 -0
  24. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/pre-post-operation.md +15 -7
  25. package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-algo.md +24 -68
  26. package/skills/cosmic-build/knowledge/cosmic/platform/workflow-plugin.md +2 -7
  27. package/skills/cosmic-build/references/application-context.md +35 -1
  28. package/skills/cosmic-build/references/common-reference.md +1 -1
  29. package/skills/cosmic-build/references/compile-guide.md +55 -68
  30. package/skills/cosmic-build/references/delivery.md +59 -7
  31. package/skills/cosmic-build/references/execution-contract.md +38 -0
  32. package/skills/cosmic-build/references/form-discovery.md +7 -1
  33. package/skills/cosmic-build/references/java-plugin-delivery.md +32 -162
  34. package/skills/cosmic-build/references/operation-dispatch.md +17 -0
  35. package/skills/cosmic-build/references/plugin-handoff-input.md +131 -0
  36. package/skills/cosmic-build/references/plugin-runtime-routing.md +1 -1
  37. package/skills/cosmic-build/references/report-filter-contract.md +78 -0
  38. package/skills/cosmic-build/references/sdk-inspection.md +23 -0
  39. package/skills/cosmic-build/schemas/data-model-authoring.schema.json +2 -3
  40. package/skills/cosmic-build/scripts/_http_client.py +55 -55
  41. package/skills/cosmic-build/scripts/appconfig_load.py +2 -2
  42. package/skills/cosmic-build/scripts/apply_report_filter_extension.py +39 -0
  43. package/skills/cosmic-build/scripts/bootstrap_cosmic_context.py +37 -2
  44. package/skills/cosmic-build/scripts/compile_plugin_source.py +279 -156
  45. package/skills/cosmic-build/scripts/execution_binding.py +40 -0
  46. package/skills/cosmic-build/scripts/inspect_platform_sdk.py +169 -0
  47. package/skills/cosmic-build/scripts/kd_api.py +23 -22
  48. package/skills/cosmic-build/scripts/kd_auth.py +1 -1
  49. package/skills/cosmic-build/scripts/kd_env_loader.py +19 -10
  50. package/skills/cosmic-build/scripts/kd_token_cache.py +2 -2
  51. package/skills/cosmic-build/scripts/kingscript_sdk_sync.py +350 -0
  52. package/skills/cosmic-build/scripts/kingscript_source_gate.py +11 -8
  53. package/skills/cosmic-build/scripts/platform_api_contract.py +246 -11
  54. package/skills/cosmic-build/scripts/plugin_corpus/build_index.py +403 -0
  55. package/skills/cosmic-build/scripts/plugin_corpus/jvm_classfile.py +281 -0
  56. package/skills/cosmic-build/scripts/plugin_corpus/measure_api_usage.py +446 -0
  57. package/skills/cosmic-build/scripts/plugin_source_gate.py +48 -7
  58. package/skills/cosmic-build/scripts/plugin_target_sdk.py +45 -0
  59. package/skills/cosmic-build/scripts/prepare_plugin_handoff.py +493 -47
  60. package/skills/cosmic-build/scripts/preview_url.py +35 -6
  61. package/skills/cosmic-build/scripts/query_plugin_handoff.py +250 -0
  62. package/skills/cosmic-build/scripts/register_plugins.py +28 -2
  63. package/skills/cosmic-build/scripts/render_output_cards.py +42 -9
  64. package/skills/cosmic-build/scripts/report_algo_benchmark.py +46 -0
  65. package/skills/cosmic-build/scripts/run.py +2 -2
  66. package/skills/cosmic-build/scripts/sync_generated_locale_assets.py +43 -0
  67. package/skills/cosmic-build/scripts/test/fixtures/ReportSdkCheck.java +64 -0
  68. package/skills/cosmic-build/scripts/test/fixtures/java_context_booking_native.json +541 -0
  69. package/skills/cosmic-build/scripts/test/test_add_rule_business_contract.py +85 -14
  70. package/skills/cosmic-build/scripts/test/test_batch5_drift_governance.py +12 -3
  71. package/skills/cosmic-build/scripts/test/test_build_meta_order.py +0 -16
  72. package/skills/cosmic-build/scripts/test/test_cli_continuation.py +87 -0
  73. package/skills/cosmic-build/scripts/test/test_compile_plugin_source.py +224 -3
  74. package/skills/cosmic-build/scripts/test/test_control_authoring.py +123 -4
  75. package/skills/cosmic-build/scripts/test/test_cosmic_sdk_examples.py +141 -0
  76. package/skills/cosmic-build/scripts/test/test_data_model_contract.py +30 -5
  77. package/skills/cosmic-build/scripts/test/test_delivery_stage_boundary.py +2 -10
  78. package/skills/cosmic-build/scripts/test/test_dym_sync_background.py +11 -0
  79. package/skills/cosmic-build/scripts/test/test_dynamic_form_metadata_only_contract.py +59 -1
  80. package/skills/cosmic-build/scripts/test/test_guide_skeleton_contract.py +87 -43
  81. package/skills/cosmic-build/scripts/test/test_inspect_platform_sdk.py +71 -0
  82. package/skills/cosmic-build/scripts/test/test_kd_api.py +67 -19
  83. package/skills/cosmic-build/scripts/test/test_kingscript_sdk_sync.py +221 -0
  84. package/skills/cosmic-build/scripts/test/test_kingscript_source_gate.py +10 -0
  85. package/skills/cosmic-build/scripts/test/test_list_configuration_closure.py +18 -0
  86. package/skills/cosmic-build/scripts/test/test_long_session_regressions.py +116 -0
  87. package/skills/cosmic-build/scripts/test/test_mcp_cli.py +524 -0
  88. package/skills/cosmic-build/scripts/test/test_modeling_input_recovery.py +141 -0
  89. package/skills/cosmic-build/scripts/test/test_open_world_cli_catalog.py +36 -2
  90. package/skills/cosmic-build/scripts/test/test_platform_api_contract.py +161 -1
  91. package/skills/cosmic-build/scripts/test/test_plugin_business_contract.py +91 -0
  92. package/skills/cosmic-build/scripts/test/test_plugin_corpus.py +195 -0
  93. package/skills/cosmic-build/scripts/test/test_plugin_handoff.py +346 -1
  94. package/skills/cosmic-build/scripts/test/test_plugin_target_sdk.py +70 -0
  95. package/skills/cosmic-build/scripts/test/test_preview_url.py +44 -1
  96. package/skills/cosmic-build/scripts/test/test_property_batch.py +22 -0
  97. package/skills/cosmic-build/scripts/test/test_property_input_discovery.py +13 -11
  98. package/skills/cosmic-build/scripts/test/test_query_plugin_handoff.py +160 -0
  99. package/skills/cosmic-build/scripts/test/test_register_plugins.py +31 -1
  100. package/skills/cosmic-build/scripts/test/test_render_output_cards.py +50 -4
  101. package/skills/cosmic-build/scripts/test/test_report_business_contract.py +27 -0
  102. package/skills/cosmic-build/scripts/test/test_report_doc_contract_sync.py +5 -4
  103. package/skills/cosmic-build/scripts/test/test_report_sdk_optimization.py +36 -0
  104. package/skills/cosmic-build/scripts/test/test_rule_batch_evidence.py +1 -1
  105. package/skills/cosmic-build/scripts/test/test_rule_domain_contract.py +20 -0
  106. package/skills/cosmic-build/scripts/test/test_rule_function_formula.py +16 -6
  107. package/skills/cosmic-build/scripts/test/test_skill_entry_contract.py +1 -0
  108. package/skills/cosmic-build/scripts/test/test_test_environment.py +45 -0
  109. package/skills/cosmic-build/scripts/test/test_verify_plugin_loading.py +79 -0
  110. package/skills/cosmic-build/scripts/test_environment.py +49 -0
  111. package/skills/cosmic-build/scripts/verify_plugin_loading.py +71 -0
  112. package/skills/cosmic-build/skills/cosmic-coding-impl/SKILL.md +86 -162
  113. package/skills/cosmic-build/skills/cosmic-coding-script/ATTRIBUTION.md +1 -1
  114. package/skills/cosmic-build/skills/cosmic-coding-script/SKILL.md +45 -14
  115. package/skills/cosmic-build/skills/cosmic-coding-script/references/kwc-controller-authoring.md +6 -0
  116. package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/README.md +29 -19
  117. package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/docs//350/204/232/346/234/254SDK.md +3 -4
  118. package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/indexes/package-index.md +1 -1
  119. package/skills/cosmic-build/skills/cosmic-coding-script/references/sdk/strategy.md +27 -18
  120. package/skills/cosmic-build/skills/cosmic-meta-api/SKILL.md +11 -17
  121. package/skills/cosmic-build/skills/cosmic-meta-api/apis/ai_meta.md +1 -1
  122. package/skills/cosmic-build/skills/cosmic-meta-api/apis/app_menu.md +1 -1
  123. package/skills/cosmic-build/skills/cosmic-meta-api/apis/meta_log.md +1 -1
  124. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/control-authoring.json +45 -4
  125. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/form-control-traits.json +1 -1
  126. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/layout-ui.json +18 -0
  127. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/menu-publish.json +8 -2
  128. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/property-batch.json +7 -0
  129. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/property-input-index.json +3 -3
  130. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/report-write-contracts.json +1 -0
  131. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/rule-domain.json +37 -5
  132. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/rule-event-contract.json +24 -3
  133. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/run-build-meta.json +61 -12
  134. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/dropdown-menu.schema.json +69 -0
  135. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/report-write.schema.json +2 -1
  136. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/rule-set.schema.json +1 -0
  137. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/rule-write.schema.json +14 -7
  138. package/skills/cosmic-build/skills/cosmic-meta-api/docs/api-summary.md +2 -2
  139. package/skills/cosmic-build/skills/cosmic-meta-api/modules/app-action/SKILL.md +6 -3
  140. package/skills/cosmic-build/skills/cosmic-meta-api/modules/develop/SKILL.md +1 -2
  141. package/skills/cosmic-build/skills/cosmic-meta-api/modules/export/SKILL.md +5 -6
  142. package/skills/cosmic-build/skills/cosmic-meta-api/modules/init/SKILL.md +13 -36
  143. package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/SKILL.md +8 -9
  144. package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/scripts/config.json.example +5 -15
  145. package/skills/cosmic-build/skills/cosmic-meta-api/modules/reset/SKILL.md +6 -6
  146. package/skills/cosmic-build/skills/cosmic-meta-api/modules/usage/SKILL.md +20 -148
  147. package/skills/cosmic-build/skills/cosmic-meta-api/modules/verify/SKILL.md +1 -1
  148. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/build_receipts.py +171 -0
  149. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_apps.py +25 -6
  150. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_controls.py +55 -21
  151. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_fields.py +10 -3
  152. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_formactions.py +63 -6
  153. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_forms.py +59 -2
  154. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_listui.py +8 -0
  155. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_metaops.py +36 -1
  156. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_plugins.py +115 -90
  157. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_properties.py +666 -4
  158. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_rules.py +1415 -87
  159. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/commands.py +6 -1
  160. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/config.py +28 -15
  161. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/execution_errors.py +40 -0
  162. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/formatters.py +32 -5
  163. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/property_policy_contract.py +6 -0
  164. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/report_ops.py +113 -17
  165. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/reset.py +3 -3
  166. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/rule_contract.py +107 -26
  167. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/build_meta_order.py +50 -49
  168. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/cosmic_api.py +856 -150
  169. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/field_decorator.py +47 -14
  170. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_build_receipt_resume.py +374 -0
  171. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_cli_failure_contract.py +24 -0
  172. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_creation_identity_boundary.py +4 -2
  173. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_dropdown_menu_contract.py +382 -0
  174. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_existing_form_ui.py +0 -25
  175. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_field_decorator.py +50 -6
  176. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_functional_group_resolution.py +83 -16
  177. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_identity_template_resolution.py +103 -0
  178. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_plugin_contract.py +28 -2
  179. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_readback_derivation.py +22 -0
  180. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_typed_filters.py +65 -0
  181. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_rule_write_closure.py +1041 -15
  182. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_post_build_recovery.py +65 -0
  183. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_verification_evidence.py +17 -0
  184. package/skills/cosmic-build/skills/cosmic-meta-knowledge/generation/api-param-checklist.md +1 -1
  185. package/skills/cosmic-build/skills/cosmic-meta-knowledge/generation/rule-modeling-guide.md +7 -4
  186. package/skills/cosmic-build/skills/cosmic-meta-modeling/SKILL.md +61 -13
  187. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/app-menu/SKILL.md +8 -0
  188. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/formtemplate/SKILL.md +5 -0
  189. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/formtemplate/dynamic-form-artifact-schema.md +5 -0
  190. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/SKILL.md +21 -9
  191. package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-entity-graphs.md +1 -1
  192. package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-storage-constraints.md +5 -3
  193. package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-ui-composition.md +6 -3
  194. package/skills/cosmic-build/skills/cosmic-meta-modeling/references/creation-recovery.md +13 -0
  195. package/skills/cosmic-build/skills/cosmic-meta-modeling/references/parse-core.md +3 -0
  196. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_contract.py +6 -1
  197. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_emit.py +135 -87
  198. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/field_type_registry.py +11 -1
  199. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/isv_resolver.py +1 -3
  200. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/report_validator.py +29 -0
  201. package/skills/cosmic-build/skills/cosmic-meta-modifier/SKILL.md +4 -2
  202. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/context/SKILL.md +2 -2
  203. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/execute/SKILL.md +1 -1
  204. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/SKILL.md +5 -4
  205. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/operation-write.md +6 -2
  206. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/validation.md +2 -0
  207. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/SKILL.md +20 -5
  208. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/complex-apis.md +1 -1
  209. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/policy-grammar.md +4 -1
  210. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/report-op/SKILL.md +9 -0
  211. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/SKILL.md +61 -4
  212. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/raw-rule-authoring.md +3 -1
  213. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/rule-actions.md +2 -2
  214. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/rule-reference.md +6 -6
  215. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/state.md +1 -1
  216. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/unit/SKILL.md +6 -3
  217. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/verify/usage-guide.md +4 -4
  218. package/skills/cosmic-build/skills/cosmic-meta-modifier/references/list-filter-modification.md +4 -1
  219. package/skills/cosmic-build/skills/cosmic-meta-ui/SKILL.md +9 -1
  220. package/skills/cosmic-build/skills/cosmic-meta-ui/modules/button/SKILL.md +3 -0
  221. package/skills/cosmic-build/skills/mcp-server/SKILL.md +210 -0
  222. package/skills/cosmic-build/skills/mcp-server/scripts/mcp_cli.py +431 -0
  223. package/skills/cosmic-build/skills/openapi-gen/SKILL.md +3 -3
  224. package/skills/cosmic-build/skills/openapi-gen/scripts/api/gen_runner.py +2 -2
  225. package/skills/cosmic-dev-best-practices/references/bill-dev-convention.md +19 -9
  226. package/skills/cosmic-dev-best-practices/references/cosmic-meta-api.md +14 -11
  227. package/skills/cosmic-dev-best-practices/references/design-examples/basedata-design-example.md +1 -1
  228. package/skills/cosmic-dev-best-practices/references/design-examples/bill-design-example.md +1 -1
  229. package/skills/cosmic-dev-best-practices/references/elements/AssistantField.md +1 -1
  230. package/skills/cosmic-dev-best-practices/references/elements/BasedataField.md +2 -2
  231. package/skills/cosmic-dev-best-practices/references/elements/BasedataPropField.md +3 -3
  232. package/skills/cosmic-dev-best-practices/references/elements/CreaterField.md +2 -2
  233. package/skills/cosmic-dev-best-practices/references/elements/CurrencyField.md +2 -2
  234. package/skills/cosmic-dev-best-practices/references/elements/CustomerField.md +2 -2
  235. package/skills/cosmic-dev-best-practices/references/elements/FlexField.md +1 -1
  236. package/skills/cosmic-dev-best-practices/references/elements/MaterielField.md +2 -2
  237. package/skills/cosmic-dev-best-practices/references/elements/OrgField.md +2 -2
  238. package/skills/cosmic-dev-best-practices/references/elements/SupplierField.md +2 -2
  239. package/skills/cosmic-dev-best-practices/references/elements/UnitField.md +1 -1
  240. package/skills/cosmic-dev-best-practices/references/elements/UserField.md +3 -3
  241. package/skills/cosmic-dev-best-practices/references/elements/common-field-property-conventions.md +16 -11
  242. package/skills/cosmic-dev-best-practices/references/layout-control-convention.md +5 -4
  243. package/skills/erp-analysis/references/delivery-contract.md +4 -2
  244. package/skills/erp-analysis/templates/AGENTS.template.md +1 -1
  245. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/.dsh/cosmic/.kd/config.json +0 -21
@@ -0,0 +1,524 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for mcp_cli.py — endpoint contract, payload builders, confirm gate, export writeback.
3
+
4
+ kd_api.post is mocked throughout: these tests pin the *contract* (endpoint
5
+ paths, payload shapes, envelope folding, destructive gating) rather than
6
+ platform behavior. The platform family lives under ai-meta/mcpserver (the
7
+ plain-`mcp` prefix was renamed server-side; requests to the old prefix fail
8
+ gateway routing).
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import contextlib
13
+ import io
14
+ import json
15
+ import os
16
+ import sys
17
+ import tempfile
18
+ import unittest
19
+ import urllib.error
20
+ from unittest import mock
21
+
22
+ _SCRIPTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
23
+ if _SCRIPTS_DIR not in sys.path:
24
+ sys.path.insert(0, _SCRIPTS_DIR)
25
+
26
+ _MCP_SKILL_DIR = os.path.join(os.path.dirname(_SCRIPTS_DIR), "skills", "mcp-server", "scripts")
27
+ if _MCP_SKILL_DIR not in sys.path:
28
+ sys.path.insert(0, _MCP_SKILL_DIR)
29
+
30
+ import mcp_cli
31
+
32
+
33
+ def _platform_ok(data):
34
+ return {"status": True, "errorCode": "0", "message": None, "data": data}
35
+
36
+
37
+ def _platform_fail(error_code, message):
38
+ return {"status": False, "errorCode": error_code, "message": message, "data": None}
39
+
40
+
41
+ class EndpointContractTest(unittest.TestCase):
42
+ """Endpoint paths must mirror McpServerApiController (@ApiMapping /ai-meta/mcpserver)."""
43
+
44
+ def test_all_nine_endpoints_share_the_ai_meta_mcpserver_prefix(self):
45
+ for name, path in mcp_cli.ENDPOINTS.items():
46
+ self.assertTrue(path.startswith("/kapi/v2/devportal/ai-meta/mcpserver/"), f"{name}: {path}")
47
+ self.assertEqual(
48
+ set(mcp_cli.ENDPOINTS),
49
+ {"list", "listByForm", "get", "save", "delete", "importYaml", "resetStatus", "unshelves", "export"},
50
+ )
51
+
52
+ def test_register_status_covers_platform_enum(self):
53
+ # McpRegisterStatusEnum: SUCCESS/FAILED/UNPUBLISH/UNSHELVES/UNSHELVESED/PENDING_UPDATE
54
+ self.assertEqual(
55
+ set(mcp_cli.REGISTER_STATUS),
56
+ {"SUCCESS", "FAILED", "UNPUBLISH", "UNSHELVES", "UNSHELVESED", "PENDING_UPDATE"},
57
+ )
58
+
59
+
60
+ class HttpMethodRoutingTest(unittest.TestCase):
61
+ """listByForm/get are served via GET (query params); the platform answers
62
+ POST on these two paths with HTTP 405 HttpMethod POST is not allowed.
63
+ The other seven endpoints remain POST + JSON body."""
64
+
65
+ def test_get_endpoints_set_is_exactly_listByForm_and_get(self):
66
+ self.assertEqual(set(mcp_cli.GET_ENDPOINTS), {"listByForm", "get"})
67
+
68
+ def test_listByForm_dispatches_to_kd_api_get_with_query_params(self):
69
+ with mock.patch.object(mcp_cli.kd_api, "get",
70
+ return_value=_platform_ok({"formNumber": "x"})) as get_mock, \
71
+ mock.patch.object(mcp_cli.kd_api, "post") as post_mock:
72
+ mcp_cli._call("listByForm", {"formNumber": "kdtest_food"}, cfg={})
73
+ get_mock.assert_called_once()
74
+ post_mock.assert_not_called()
75
+ # kd_api.get(base_url, path, config, params=…) — payload rides as query params
76
+ self.assertEqual(get_mock.call_args.kwargs.get("params"), {"formNumber": "kdtest_food"})
77
+ self.assertTrue(get_mock.call_args[0][1].endswith("/listByForm"))
78
+
79
+ def test_get_command_dispatches_to_kd_api_get_with_query_params(self):
80
+ with mock.patch.object(mcp_cli.kd_api, "get",
81
+ return_value=_platform_ok({"id": 1})) as get_mock, \
82
+ mock.patch.object(mcp_cli.kd_api, "post") as post_mock:
83
+ mcp_cli._call("get", {"id": 42}, cfg={})
84
+ get_mock.assert_called_once()
85
+ post_mock.assert_not_called()
86
+ self.assertEqual(get_mock.call_args.kwargs.get("params"), {"id": 42})
87
+ self.assertTrue(get_mock.call_args[0][1].endswith("/get"))
88
+
89
+ def test_write_and_list_endpoints_stay_on_post(self):
90
+ # A representative sample across the POST family — list (paginated read),
91
+ # save (write), unshelves (destructive). All must go through kd_api.post.
92
+ for command in ("list", "save", "unshelves", "importYaml", "resetStatus", "delete", "export"):
93
+ with self.subTest(command=command), \
94
+ mock.patch.object(mcp_cli.kd_api, "post",
95
+ return_value=_platform_ok({})) as post_mock, \
96
+ mock.patch.object(mcp_cli.kd_api, "get") as get_mock:
97
+ mcp_cli._call(command, {"any": "payload"}, cfg={})
98
+ post_mock.assert_called_once()
99
+ get_mock.assert_not_called()
100
+
101
+
102
+ class ListPayloadTest(unittest.TestCase):
103
+ def test_defaults_and_filters(self):
104
+ args = mcp_cli.build_parser().parse_args([
105
+ "list", "--status", "SUCCESS", "--api-type", "openapi_apilist", "--tool-name", "query",
106
+ ])
107
+ payload = mcp_cli.build_list_payload(args)
108
+ self.assertEqual(payload["pageIndex"], 1)
109
+ self.assertEqual(payload["pageSize"], 20)
110
+ self.assertEqual(payload["registerStatus"], "SUCCESS")
111
+ self.assertEqual(payload["apiType"], "openapi_apilist")
112
+ self.assertEqual(payload["toolName"], "query")
113
+ # untouched filters stay absent so the server applies no filter
114
+ for absent in ("sourceType", "number", "apiNumber"):
115
+ self.assertNotIn(absent, payload)
116
+
117
+ def test_invalid_status_rejected_by_choices(self):
118
+ with self.assertRaises(SystemExit):
119
+ mcp_cli.build_parser().parse_args(["list", "--status", "NOT_A_STATUS"])
120
+
121
+
122
+ class ListByFormTest(unittest.TestCase):
123
+ """listByForm: one form number → its APIs plus registered MCP tools."""
124
+
125
+ def test_payload_is_flat_form_number(self):
126
+ args = mcp_cli.build_parser().parse_args(["listByForm", "--form-number", "kdtest_food"])
127
+ with mock.patch.object(mcp_cli, "_load_config", return_value={}):
128
+ with mock.patch.object(mcp_cli, "_call",
129
+ return_value=(mcp_cli._ok({"formNumber": "kdtest_food", "apis": [], "items": []}))) as call:
130
+ result, code = mcp_cli.run_command(args)
131
+ self.assertEqual(code, 0)
132
+ self.assertEqual(call.call_args[0][0], "listByForm")
133
+ self.assertEqual(call.call_args[0][1], {"formNumber": "kdtest_food"})
134
+
135
+ def test_form_number_is_required(self):
136
+ with self.assertRaises(SystemExit):
137
+ mcp_cli.build_parser().parse_args(["listByForm"])
138
+
139
+
140
+ class SavePayloadTest(unittest.TestCase):
141
+ def test_create_payload_omits_id_and_empty_optionals(self):
142
+ args = mcp_cli.build_parser().parse_args([
143
+ "save", "--api-type", "open_rest_api", "--api-id", "42",
144
+ "--description", "查询在养批次", "--domain", "养殖", "--module", "批次管理",
145
+ ])
146
+ payload = mcp_cli.build_save_payload(args)
147
+ self.assertNotIn("id", payload)
148
+ self.assertEqual(payload["apiType"], "open_rest_api")
149
+ self.assertEqual(payload["apiId"], 42)
150
+ self.assertEqual(payload["description"], "查询在养批次")
151
+ self.assertEqual(payload["domain"], "养殖")
152
+ for absent in ("operateType", "toolNameNew", "sourceYaml", "cusParamExtCfg"):
153
+ self.assertNotIn(absent, payload)
154
+
155
+ def test_update_payload_keeps_id_and_maps_every_field(self):
156
+ args = mcp_cli.build_parser().parse_args([
157
+ "save", "--id", "7", "--description", "d", "--operate-type", "查询操作",
158
+ "--module", "m", "--before-condition", "pre", "--after-condition", "post",
159
+ "--err-recovery", "rec", "--cus-param-ext-cfg", '{"k": "v"}',
160
+ "--tool-name", "tool_x", "--source-yaml", "yaml-body",
161
+ ])
162
+ payload = mcp_cli.build_save_payload(args)
163
+ self.assertEqual(payload["id"], 7)
164
+ self.assertEqual(payload["operateType"], "查询操作")
165
+ self.assertEqual(payload["beforeCondition"], "pre")
166
+ self.assertEqual(payload["afterCondition"], "post")
167
+ self.assertEqual(payload["errRecovery"], "rec")
168
+ self.assertEqual(payload["cusParamExtCfg"], '{"k": "v"}')
169
+ self.assertEqual(payload["toolNameNew"], "tool_x")
170
+ self.assertEqual(payload["sourceYaml"], "yaml-body")
171
+
172
+ def test_invalid_api_type_rejected(self):
173
+ with self.assertRaises(SystemExit):
174
+ mcp_cli.build_parser().parse_args(["save", "--api-type", "not_a_entity", "--api-id", "1"])
175
+
176
+ def test_invalid_cus_param_ext_cfg_json_is_cli_argument_error(self):
177
+ # Client-side JSON validation: a malformed --cus-param-ext-cfg must fail
178
+ # fast as CLI_ARGUMENT_ERROR (exit 2) instead of a server round-trip.
179
+ args = mcp_cli.build_parser().parse_args([
180
+ "save", "--api-type", "openapi_apilist", "--api-id", "1",
181
+ "--description", "d", "--cus-param-ext-cfg", "{not json",
182
+ ])
183
+ with mock.patch.object(mcp_cli, "_call") as call:
184
+ result, code = mcp_cli.run_command(args)
185
+ call.assert_not_called()
186
+ self.assertEqual(result["errorCode"], "CLI_ARGUMENT_ERROR")
187
+ self.assertIn("cus-param-ext-cfg", result["message"])
188
+ self.assertEqual(code, 2)
189
+
190
+ def test_non_object_cus_param_ext_cfg_is_cli_argument_error(self):
191
+ # Valid JSON but not an object (array/scalar) must also fail client-side:
192
+ # the server contract demands an object, so sending it would only yield
193
+ # a CUS_PARAM_INVALID round-trip (exit 1) instead of exit 2.
194
+ args = mcp_cli.build_parser().parse_args([
195
+ "save", "--api-type", "openapi_apilist", "--api-id", "1",
196
+ "--description", "d", "--cus-param-ext-cfg", "[1, 2]",
197
+ ])
198
+ with mock.patch.object(mcp_cli, "_call") as call:
199
+ result, code = mcp_cli.run_command(args)
200
+ call.assert_not_called()
201
+ self.assertEqual(result["errorCode"], "CLI_ARGUMENT_ERROR")
202
+ self.assertIn("JSON object", result["message"])
203
+ self.assertEqual(code, 2)
204
+
205
+ def test_argparse_error_emits_json_line(self):
206
+ # argparse-level errors (bad enum, missing flag, unknown subcommand) must
207
+ # also print a single JSON line — the CLI has no human mode.
208
+ with contextlib.redirect_stdout(io.StringIO()) as out:
209
+ with self.assertRaises(SystemExit) as ctx:
210
+ mcp_cli.build_parser().parse_args(["list", "--status", "BOGUS"])
211
+ result = json.loads(out.getvalue())
212
+ self.assertEqual(result["errorCode"], "CLI_ARGUMENT_ERROR")
213
+ self.assertEqual(ctx.exception.code, 2)
214
+
215
+
216
+ class ConfirmGateTest(unittest.TestCase):
217
+ """delete/resetStatus/unshelves must refuse to run without --confirm."""
218
+
219
+ def test_destructive_commands_require_confirm(self):
220
+ for command in ("delete", "resetStatus", "unshelves"):
221
+ args = mcp_cli.build_parser().parse_args([command, "--ids", "1,2"])
222
+ with mock.patch.object(mcp_cli, "_call") as call:
223
+ result, code = mcp_cli.run_command(args)
224
+ call.assert_not_called()
225
+ self.assertFalse(result["status"])
226
+ self.assertEqual(result["errorCode"], "CONFIRM_REQUIRED")
227
+ self.assertEqual(code, 2)
228
+
229
+ def test_confirmed_delete_calls_platform_with_ids(self):
230
+ args = mcp_cli.build_parser().parse_args(["delete", "--ids", "1,2,3", "--confirm"])
231
+ with mock.patch.object(mcp_cli, "_call", return_value=(mcp_cli._ok({"successCount": 3}))) as call:
232
+ result, code = mcp_cli.run_command(args)
233
+ call.assert_called_once()
234
+ # _call(command, payload, cfg): payload carries the parsed id list
235
+ self.assertEqual(call.call_args[0][1]["ids"], [1, 2, 3])
236
+ self.assertEqual(code, 0)
237
+ self.assertTrue(result["status"])
238
+
239
+
240
+ class IdParsingTest(unittest.TestCase):
241
+ def test_comma_and_space_both_accepted(self):
242
+ self.assertEqual(mcp_cli._parse_ids("1, 2 3"), [1, 2, 3])
243
+
244
+ def test_non_numeric_rejected(self):
245
+ with self.assertRaises(ValueError):
246
+ mcp_cli._parse_ids("1,abc")
247
+
248
+ def test_empty_rejected(self):
249
+ with self.assertRaises(ValueError):
250
+ mcp_cli._parse_ids(" , ")
251
+
252
+
253
+ class CallFoldingTest(unittest.TestCase):
254
+ """CustomApiResult → CLI envelope folding, both directions."""
255
+
256
+ def test_platform_success_folds_to_ok_with_data(self):
257
+ with mock.patch.object(mcp_cli.kd_api, "post", return_value=_platform_ok({"total": 5})):
258
+ result = mcp_cli._call("list", {}, cfg={})
259
+ self.assertTrue(result["status"])
260
+ self.assertEqual(result["data"], {"total": 5})
261
+
262
+ def test_platform_failure_keeps_error_code_and_message(self):
263
+ platform = _platform_fail("STATUS_LOCKED", "该MCP服务当前为已发布状态,请先下架后再编辑。")
264
+ with mock.patch.object(mcp_cli.kd_api, "post", return_value=platform):
265
+ result = mcp_cli._call("save", {}, cfg={})
266
+ self.assertFalse(result["status"])
267
+ self.assertEqual(result["errorCode"], "STATUS_LOCKED")
268
+ self.assertIn("下架", result["message"])
269
+
270
+ def test_platform_failure_without_code_falls_back(self):
271
+ platform = {"status": False, "errorCode": None, "message": "boom"}
272
+ with mock.patch.object(mcp_cli.kd_api, "get", return_value=platform):
273
+ result = mcp_cli._call("get", {}, cfg={})
274
+ self.assertEqual(result["errorCode"], "PLATFORM_ERROR")
275
+
276
+ def test_transport_exception_folds_to_platform_error(self):
277
+ # Misrouted or undeployed endpoints answer with a gateway 400/NPE at
278
+ # the HTTP layer; the CLI must emit a clean JSON envelope instead of
279
+ # leaking a traceback into the machine output.
280
+ with mock.patch.object(mcp_cli.kd_api, "post", side_effect=urllib.error.HTTPError(
281
+ None, 400, "Bad Request", None, None)):
282
+ result = mcp_cli._call("list", {}, cfg={})
283
+ self.assertFalse(result["status"])
284
+ self.assertEqual(result["errorCode"], "PLATFORM_ERROR")
285
+ self.assertIn("list call failed", result["message"])
286
+
287
+ def test_load_config_flattens_server_section(self):
288
+ # load_effective_skill_config returns {server: {baseUrl, appId, ...}};
289
+ # kd_api reads flat top-level keys. _load_config must bridge the shape.
290
+ nested = {"server": {"baseUrl": "https://x/ierp", "appId": "a", "appSecret": "s"},
291
+ "pdm": {"outputDir": "datamodel"}}
292
+ with mock.patch.object(mcp_cli, "load_effective_skill_config", return_value=nested):
293
+ flat = mcp_cli._load_config()
294
+ self.assertEqual(flat["baseUrl"], "https://x/ierp")
295
+ self.assertEqual(flat["appId"], "a")
296
+ self.assertEqual(flat["outputDir"], "datamodel")
297
+
298
+
299
+ class MessageSanitizationTest(unittest.TestCase):
300
+ """Platform failure messages carry TraceId/WebInstance/Java stacks that can
301
+ run kilobytes long (e.g. bos.orm.read.dataNotExist on a deleted record);
302
+ the single-JSON machine output must stay compact while TraceId survives."""
303
+
304
+ def test_stack_trace_is_stripped_trace_id_survives(self):
305
+ platform = _platform_fail(
306
+ "bos.orm.read.dataNotExist",
307
+ "MCP服务 数据在系统中不存在,可能已经被删除。,TraceId:3ecc3add3b8aaee \n"
308
+ "WebInstance:feature-vb-test-web-082415920 \nTime:2026/09/23 10:42:13 \n"
309
+ "====Stack==== \nkd.bos.exception.KDException: MCP服务数据在系统中不存在\n"
310
+ "\tat kd.bos.orm.dataentity.DataManagerImplement.read(DataManagerImplement.java:1875)",
311
+ )
312
+ with mock.patch.object(mcp_cli.kd_api, "get", return_value=platform):
313
+ result = mcp_cli._call("get", {"id": 1}, cfg={})
314
+ self.assertFalse(result["status"])
315
+ self.assertEqual(result["errorCode"], "bos.orm.read.dataNotExist")
316
+ self.assertIn("TraceId", result["message"])
317
+ self.assertNotIn("====Stack====", result["message"])
318
+ self.assertNotIn("KDException", result["message"])
319
+
320
+ def test_overlong_message_without_stack_is_truncated(self):
321
+ platform = _platform_fail("STATUS_LOCKED", "x" * 3000)
322
+ with mock.patch.object(mcp_cli.kd_api, "post", return_value=platform):
323
+ result = mcp_cli._call("save", {}, cfg={})
324
+ self.assertTrue(result["message"].endswith("...(truncated)"))
325
+ self.assertLessEqual(len(result["message"]), mcp_cli._MESSAGE_LIMIT + len("...(truncated)"))
326
+
327
+ def test_short_message_without_stack_passes_through(self):
328
+ message = "该MCP服务当前为已发布状态,请先下架后再编辑。"
329
+ platform = _platform_fail("STATUS_LOCKED", message)
330
+ with mock.patch.object(mcp_cli.kd_api, "post", return_value=platform):
331
+ result = mcp_cli._call("save", {}, cfg={})
332
+ self.assertEqual(result["message"], message)
333
+
334
+
335
+ class ReleaseDecouplingTest(unittest.TestCase):
336
+ """灵基/苍穹发版解耦:接口族缺失时给出 MCP_API_NOT_DEPLOYED 而非笼统 PLATFORM_ERROR。
337
+
338
+ Lingee and Cosmic release independently; an updated skill tree may run
339
+ against an older platform without the ai-meta/mcpserver family. Both
340
+ not-deployed signatures must map to a dedicated, actionable error code.
341
+ """
342
+
343
+ def _http_error(self, code, body_text):
344
+ return urllib.error.HTTPError(
345
+ None, code, "Error", None,
346
+ io.BytesIO(body_text.encode("utf-8")) if body_text else None)
347
+
348
+ def test_gateway_npe_without_trace_id_is_not_deployed(self):
349
+ # API entry registered in the platform list, backend controller missing:
350
+ # gateway answers HTTP 400 + NPE with TraceId:null (never reaches a business instance).
351
+ body = ('{"data":null,"errorCode":"500",'
352
+ '"message":"java.lang.NullPointerException,TraceId:null","status":false}')
353
+ with mock.patch.object(mcp_cli.kd_api, "post", side_effect=self._http_error(400, body)):
354
+ result = mcp_cli._call("list", {}, cfg={})
355
+ self.assertFalse(result["status"])
356
+ self.assertEqual(result["errorCode"], "MCP_API_NOT_DEPLOYED")
357
+ self.assertIn("not deployed", result["message"])
358
+
359
+ def test_http_404_is_not_deployed(self):
360
+ body = '{"data":null,"errorCode":"404","message":"Cannot found OpenAPI","status":false}'
361
+ with mock.patch.object(mcp_cli.kd_api, "get", side_effect=self._http_error(404, body)):
362
+ result = mcp_cli._call("get", {}, cfg={})
363
+ self.assertEqual(result["errorCode"], "MCP_API_NOT_DEPLOYED")
364
+
365
+ def test_body_level_404_envelope_is_not_deployed(self):
366
+ # The platform often answers HTTP 200 with a body-level errorCode=404
367
+ # ("Cannot found OpenAPI"); kd_auth surfaces it as an exception
368
+ # mentioning 404 — both shapes mean "no API entry on this environment".
369
+ platform = _platform_fail("404", "Cannot found OpenAPI(or disabled)")
370
+ with mock.patch.object(mcp_cli.kd_api, "post", return_value=platform):
371
+ result = mcp_cli._call("list", {}, cfg={})
372
+ self.assertEqual(result["errorCode"], "MCP_API_NOT_DEPLOYED")
373
+
374
+ def test_business_npe_with_real_trace_id_stays_platform_error(self):
375
+ # An NPE WITH a TraceId comes from a live business instance (a genuine
376
+ # server bug), not a missing deployment — must not be misclassified.
377
+ body = ('{"data":null,"errorCode":"500",'
378
+ '"message":"java.lang.NullPointerException,TraceId:a41f8c81fe2eec37","status":false}')
379
+ with mock.patch.object(mcp_cli.kd_api, "post", side_effect=self._http_error(400, body)):
380
+ result = mcp_cli._call("save", {}, cfg={})
381
+ self.assertEqual(result["errorCode"], "PLATFORM_ERROR")
382
+
383
+ def test_trace_id_hex_containing_404_substring_stays_platform_error(self):
384
+ # A REAL TraceId whose hex happens to contain "404" (e.g. a41f404c81fe2eec37)
385
+ # must NOT trip the not-deployed classifier: bare substring matching would
386
+ # misreport a genuine server-side defect as a deployment gap AND drop the
387
+ # TraceId the user needs for the defect report. Word-boundary matching keeps
388
+ # the original detail (TraceId included) in the PLATFORM_ERROR message.
389
+ body = ('{"data":null,"errorCode":"500",'
390
+ '"message":"java.lang.NullPointerException,TraceId:a41f404c81fe2eec37","status":false}')
391
+ with mock.patch.object(mcp_cli.kd_api, "post", side_effect=self._http_error(400, body)):
392
+ result = mcp_cli._call("list", {}, cfg={})
393
+ self.assertEqual(result["errorCode"], "PLATFORM_ERROR")
394
+ self.assertIn("a41f404c81fe2eec37", result["message"])
395
+
396
+ def test_standalone_404_in_token_expired_message_is_not_deployed(self):
397
+ # Regression guard for the word-boundary switch: "404" as a standalone
398
+ # token (not embedded in a longer hex/number) still classifies as
399
+ # not-deployed, exactly like the old substring behavior.
400
+ with mock.patch.object(mcp_cli.kd_api, "get",
401
+ side_effect=RuntimeError("Business code token expired: 404")):
402
+ result = mcp_cli._call("listByForm", {}, cfg={})
403
+ self.assertEqual(result["errorCode"], "MCP_API_NOT_DEPLOYED")
404
+
405
+ def test_not_deployed_error_fails_with_exit_code_one(self):
406
+ # MCP_API_NOT_DEPLOYED flows through run_command as a platform failure (exit 1)
407
+ args = mcp_cli.build_parser().parse_args(["listByForm", "--form-number", "kdtest_food"])
408
+ with mock.patch.object(mcp_cli, "_load_config", return_value={}):
409
+ with mock.patch.object(mcp_cli, "_call",
410
+ return_value=(mcp_cli._fail("MCP_API_NOT_DEPLOYED", "not deployed"))):
411
+ result, code = mcp_cli.run_command(args)
412
+ self.assertEqual(code, 1)
413
+ self.assertEqual(result["errorCode"], "MCP_API_NOT_DEPLOYED")
414
+
415
+
416
+ class ImportYamlTest(unittest.TestCase):
417
+ def test_payload_reads_file_and_defaults_mode_override(self):
418
+ with tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False, encoding="utf-8") as handle:
419
+ handle.write("paths:\n /v2/open/x/query:\n")
420
+ path = handle.name
421
+ try:
422
+ args = mcp_cli.build_parser().parse_args(["importYaml", "--file", path])
423
+ payload = mcp_cli.build_import_payload(args)
424
+ self.assertIn("paths:", payload["yamlContent"])
425
+ self.assertEqual(payload["fileName"], os.path.basename(path))
426
+ self.assertEqual(payload["mode"], "override")
427
+ self.assertIs(payload["saveYaml"], False)
428
+ finally:
429
+ os.unlink(path)
430
+
431
+ def test_missing_file_is_cli_argument_error(self):
432
+ args = mcp_cli.build_parser().parse_args(["importYaml", "--file", "/nonexistent/x.yaml"])
433
+ result, code = mcp_cli.run_command(args)
434
+ self.assertEqual(result["errorCode"], "CLI_ARGUMENT_ERROR")
435
+ self.assertEqual(code, 2)
436
+
437
+
438
+ class ExportTest(unittest.TestCase):
439
+ def test_export_writes_content_and_strips_it_from_stdout_json(self):
440
+ platform_data = {
441
+ "success": True, "format": "yaml", "fileName": "mcp_1.yaml",
442
+ "content": "paths: {}\n", "count": 1,
443
+ }
444
+ args = mcp_cli.build_parser().parse_args(["export", "--ids", "1", "--format", "yaml"])
445
+ with mock.patch.object(mcp_cli, "_call", return_value=(mcp_cli._ok(platform_data))):
446
+ with tempfile.TemporaryDirectory() as tmp:
447
+ args.output = os.path.join(tmp, "out.yaml")
448
+ result, code = mcp_cli.run_command(args)
449
+ self.assertEqual(code, 0)
450
+ self.assertEqual(result["data"]["count"], 1)
451
+ self.assertEqual(result["data"]["savedTo"], args.output)
452
+ self.assertNotIn("content", result["data"])
453
+ with open(args.output, encoding="utf-8") as handle:
454
+ self.assertEqual(handle.read(), "paths: {}\n")
455
+
456
+ def test_export_rejects_unknown_format(self):
457
+ with self.assertRaises(SystemExit):
458
+ mcp_cli.build_parser().parse_args(["export", "--ids", "1", "--format", "xlsx"])
459
+
460
+
461
+ class MainOutputTest(unittest.TestCase):
462
+ def test_main_prints_single_json_line_and_exit_code(self):
463
+ with mock.patch.object(mcp_cli, "_load_config", return_value={}):
464
+ with mock.patch.object(mcp_cli.kd_api, "post", return_value=_platform_ok({"items": []})):
465
+ with mock.patch("sys.stdout", new_callable=lambda: _StdoutCapture()) as fake:
466
+ code = mcp_cli.main(["list", "--page-size", "5"])
467
+ self.assertEqual(code, 0)
468
+ self.assertEqual(len(fake.lines), 1)
469
+ parsed = json.loads(fake.lines[0])
470
+ self.assertTrue(parsed["status"])
471
+ self.assertEqual(parsed["data"], {"items": []})
472
+
473
+ def test_main_argument_error_exit_code_two(self):
474
+ with mock.patch("sys.stdout", new_callable=lambda: _StdoutCapture()) as fake:
475
+ code = mcp_cli.main(["importYaml", "--file", "/nonexistent/y.yaml"])
476
+ self.assertEqual(code, 2)
477
+ parsed = json.loads(fake.lines[0])
478
+ self.assertEqual(parsed["errorCode"], "CLI_ARGUMENT_ERROR")
479
+
480
+ def test_main_folds_unexpected_exception_to_cli_execution_error(self):
481
+ # kd_env_loader failures (e.g. no default environment configured) surface
482
+ # as RuntimeError from _load_config, outside run_command's own catches.
483
+ # The single-JSON-line contract must still hold: one JSON line, exit 1,
484
+ # CLI_EXECUTION_ERROR envelope — never a bare traceback on stdout.
485
+ with mock.patch.object(mcp_cli, "_load_config", side_effect=RuntimeError("no default kd env")):
486
+ with mock.patch("sys.stdout", new_callable=lambda: _StdoutCapture()) as fake:
487
+ code = mcp_cli.main(["list"])
488
+ self.assertEqual(code, 1)
489
+ self.assertEqual(len(fake.lines), 1)
490
+ parsed = json.loads(fake.lines[0])
491
+ self.assertFalse(parsed["status"])
492
+ self.assertEqual(parsed["errorCode"], "CLI_EXECUTION_ERROR")
493
+ self.assertIn("no default kd env", parsed["message"])
494
+
495
+ def test_unreadable_import_file_is_cli_argument_error(self):
496
+ # PermissionError (an OSError) from _read_file must fold into
497
+ # CLI_ARGUMENT_ERROR exit 2 — the --file argument points to a file
498
+ # the process cannot read; no traceback may escape either.
499
+ args = mcp_cli.build_parser().parse_args(["importYaml", "--file", "/etc/shadow"])
500
+ with mock.patch.object(mcp_cli, "_read_file",
501
+ side_effect=PermissionError(13, "Permission denied", "/etc/shadow")):
502
+ result, code = mcp_cli.run_command(args)
503
+ self.assertEqual(result["errorCode"], "CLI_ARGUMENT_ERROR")
504
+ self.assertIn("Permission denied", result["message"])
505
+ self.assertEqual(code, 2)
506
+
507
+
508
+ class _StdoutCapture:
509
+ """Minimal stdout double that records whole printed lines."""
510
+
511
+ def __init__(self):
512
+ self.lines = []
513
+
514
+ def write(self, text):
515
+ if text.strip():
516
+ self.lines.append(text.strip())
517
+ return len(text)
518
+
519
+ def flush(self):
520
+ pass
521
+
522
+
523
+ if __name__ == "__main__":
524
+ unittest.main()