@shundoo-ai/dsh-cosmic 1.0.21 → 1.0.22

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 (218) hide show
  1. package/package.json +1 -1
  2. package/skills/cosmic-build/LOCAL-PATCHES.md +43 -14
  3. package/skills/cosmic-build/SKILL.md +24 -9
  4. package/skills/cosmic-build/examples/java-rules/CosmicReportAggregation.java +26 -17
  5. package/skills/cosmic-build/examples/java-rules/CosmicReportFilters.java +96 -2
  6. package/skills/cosmic-build/knowledge/cosmic/arch/security/security_standard.md +4 -4
  7. package/skills/cosmic-build/knowledge/cosmic/platform/allimport.md +2 -1
  8. package/skills/cosmic-build/knowledge/cosmic/platform/core/dynamic-object-collection.md +6 -4
  9. package/skills/cosmic-build/knowledge/cosmic/platform/core/model.md +9 -9
  10. package/skills/cosmic-build/knowledge/cosmic/platform/core/view.md +5 -5
  11. package/skills/cosmic-build/knowledge/cosmic/platform/form-plugin.md +8 -4
  12. package/skills/cosmic-build/knowledge/cosmic/platform/list-plugin.md +5 -3
  13. package/skills/cosmic-build/knowledge/cosmic/platform/print-plugin-guide.md +1 -0
  14. package/skills/cosmic-build/knowledge/cosmic/platform/report-plugin.md +269 -57
  15. package/skills/cosmic-build/knowledge/cosmic/platform/reportlistdata-plugin.md +130 -23
  16. package/skills/cosmic-build/knowledge/cosmic/platform/reporttreedata-plugin.md +48 -28
  17. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/_index.md +12 -3
  18. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/common/attachment-name-process.md +17 -13
  19. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/common/dynamic-update-field.md +4 -4
  20. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/filter-grid/filter-grid-set-value.md +1 -1
  21. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-basedatafield.md +2 -3
  22. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-container-init.md +0 -1
  23. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-popup.md +0 -1
  24. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-value-parse.md +5 -6
  25. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/list-show-form.md +1 -1
  26. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/query-field-dynamic.md +0 -7
  27. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/business-data/delete-service.md +24 -29
  28. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/ksql/db-batch.md +4 -0
  29. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/ksql/db-direct-access.md +4 -0
  30. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/qfilter/entity-object-access.md +8 -3
  31. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/qfilter/qfilter-compose.md +6 -5
  32. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/after-bind-data-set-visible.md +5 -1
  33. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/click-return-data.md +2 -2
  34. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/closed-call-back-set-value.md +2 -2
  35. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/register-listener.md +1 -1
  36. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/set-filter-tab-org.md +3 -0
  37. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/dispatch-service/microservice-dev.md +18 -11
  38. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/dist-lock/dist-lock-basic.md +3 -6
  39. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/mq/mq-subscribe.md +5 -1
  40. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/thread-pool/async-operation.md +10 -6
  41. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/thread-pool/big-task-async.md +4 -2
  42. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/thread-pool/schedule-task.md +9 -7
  43. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/operation-interactive.md +33 -34
  44. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/operation-plugin-events.md +30 -22
  45. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-async-query.md +133 -0
  46. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-batch-query.md +26 -22
  47. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-chart.md +112 -0
  48. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-dataset-assembly.md +200 -0
  49. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-head-filter.md +177 -0
  50. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-hyperlink-drill.md +216 -0
  51. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-list-data-query.md +12 -12
  52. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-merge-column.md +44 -6
  53. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-print-data.md +163 -0
  54. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-row-data-toolbar.md +188 -0
  55. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-subtotal-total.md +139 -0
  56. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-tree-data-query.md +108 -12
  57. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-tree-list.md +181 -0
  58. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/system-service/report-plugin-interface.md +3 -0
  59. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-basedata-cascade-confirm.md +14 -11
  60. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-before-item-validate.md +1 -1
  61. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-build-filter.md +1 -1
  62. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-multi-level-filter.md +11 -11
  63. package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-refresh-node.md +22 -15
  64. package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-algo.md +2 -2
  65. package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-entity-model.md +3 -4
  66. package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-exception.md +9 -5
  67. package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-orm-access.md +4 -0
  68. package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-request-context.md +7 -3
  69. package/skills/cosmic-build/knowledge/cosmic/platform/standards.md +28 -3
  70. package/skills/cosmic-build/references/compile-guide.md +36 -1
  71. package/skills/cosmic-build/references/dcs-code-rules.md +180 -0
  72. package/skills/cosmic-build/references/delivery.md +16 -2
  73. package/skills/cosmic-build/references/execution-contract.md +15 -3
  74. package/skills/cosmic-build/references/java-plugin-delivery.md +21 -11
  75. package/skills/cosmic-build/references/plugin-handoff-input.md +82 -24
  76. package/skills/cosmic-build/references/plugin-runtime-routing.md +23 -6
  77. package/skills/cosmic-build/references/report-filter-contract.md +53 -15
  78. package/skills/cosmic-build/references/sdk-inspection.md +4 -1
  79. package/skills/cosmic-build/references/type-registry.json +26 -17
  80. package/skills/cosmic-build/schemas/data-model-authoring.schema.json +1 -1
  81. package/skills/cosmic-build/scripts/_http_client.py +3 -0
  82. package/skills/cosmic-build/scripts/compile_plugin_source.py +251 -42
  83. package/skills/cosmic-build/scripts/configure_list.py +112 -0
  84. package/skills/cosmic-build/scripts/dcs-rule-catalog.json +799 -0
  85. package/skills/cosmic-build/scripts/dcs_code_gate.py +1976 -0
  86. package/skills/cosmic-build/scripts/execution_binding.py +25 -0
  87. package/skills/cosmic-build/scripts/platform_api_contract.py +352 -7
  88. package/skills/cosmic-build/scripts/plugin_source_gate.py +9 -1
  89. package/skills/cosmic-build/scripts/plugin_target_sdk.py +12 -8
  90. package/skills/cosmic-build/scripts/prepare_plugin_handoff.py +488 -88
  91. package/skills/cosmic-build/scripts/query_plugin_handoff.py +207 -20
  92. package/skills/cosmic-build/scripts/register_plugins.py +27 -10
  93. package/skills/cosmic-build/scripts/render_output_cards.py +57 -20
  94. package/skills/cosmic-build/scripts/resolve_output_path.py +60 -26
  95. package/skills/cosmic-build/scripts/run_java_tests.py +162 -0
  96. package/skills/cosmic-build/scripts/sync_generated_locale_assets.py +28 -0
  97. package/skills/cosmic-build/scripts/test/fixtures/ReportSdkCheck.java +1 -22
  98. package/skills/cosmic-build/scripts/test/fixtures/list_column_property_declarations.json +887 -0
  99. package/skills/cosmic-build/scripts/test/fixtures/operation-entry-field-prepare.java +78 -0
  100. package/skills/cosmic-build/scripts/test/test_add_field_required_contract.py +12 -0
  101. package/skills/cosmic-build/scripts/test/test_batch5_drift_governance.py +23 -2
  102. package/skills/cosmic-build/scripts/test/test_compile_plugin_source.py +287 -4
  103. package/skills/cosmic-build/scripts/test/test_configure_list_delivery.py +132 -0
  104. package/skills/cosmic-build/scripts/test/test_cosmic_sdk_examples.py +58 -1
  105. package/skills/cosmic-build/scripts/test/test_data_model_contract.py +19 -0
  106. package/skills/cosmic-build/scripts/test/test_dcs_code_gate.py +901 -0
  107. package/skills/cosmic-build/scripts/test/test_delivery_stage_boundary.py +1 -1
  108. package/skills/cosmic-build/scripts/test/test_direct_cli_architecture.py +2 -0
  109. package/skills/cosmic-build/scripts/test/test_http_client.py +6 -0
  110. package/skills/cosmic-build/scripts/test/test_java_toolchain_feedback.py +206 -0
  111. package/skills/cosmic-build/scripts/test/test_kd_api.py +21 -0
  112. package/skills/cosmic-build/scripts/test/test_list_configuration_closure.py +184 -0
  113. package/skills/cosmic-build/scripts/test/test_mcp_cli.py +139 -2
  114. package/skills/cosmic-build/scripts/test/test_menu_creation_closure.py +16 -0
  115. package/skills/cosmic-build/scripts/test/test_modeling_input_recovery.py +55 -1
  116. package/skills/cosmic-build/scripts/test/test_open_world_cli_catalog.py +1 -1
  117. package/skills/cosmic-build/scripts/test/test_operation_field_preparation.py +84 -0
  118. package/skills/cosmic-build/scripts/test/test_platform_api_contract.py +322 -1
  119. package/skills/cosmic-build/scripts/test/test_plugin_handoff.py +951 -4
  120. package/skills/cosmic-build/scripts/test/test_plugin_source_gate.py +30 -2
  121. package/skills/cosmic-build/scripts/test/test_plugin_target_sdk.py +21 -0
  122. package/skills/cosmic-build/scripts/test/test_property_reroute.py +165 -0
  123. package/skills/cosmic-build/scripts/test/test_property_schema_first.py +35 -0
  124. package/skills/cosmic-build/scripts/test/test_query_plugin_handoff.py +221 -0
  125. package/skills/cosmic-build/scripts/test/test_register_plugins.py +76 -4
  126. package/skills/cosmic-build/scripts/test/test_render_output_cards.py +75 -3
  127. package/skills/cosmic-build/scripts/test/test_report_business_contract.py +27 -5
  128. package/skills/cosmic-build/scripts/test/test_report_doc_contract_sync.py +103 -11
  129. package/skills/cosmic-build/scripts/test/test_report_plugin_source_gate.py +46 -16
  130. package/skills/cosmic-build/scripts/test/test_report_properties.py +74 -15
  131. package/skills/cosmic-build/scripts/test/test_report_query_data_source.py +3 -1
  132. package/skills/cosmic-build/scripts/test/test_report_sdk_optimization.py +1 -2
  133. package/skills/cosmic-build/scripts/test/test_test_environment.py +76 -1
  134. package/skills/cosmic-build/scripts/test_environment.py +67 -1
  135. package/skills/cosmic-build/skills/cosmic-coding-impl/SKILL.md +52 -10
  136. package/skills/cosmic-build/skills/cosmic-meta-api/apis/ai_meta.md +16 -3
  137. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/report-write-contracts.json +59 -26
  138. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/rule-domain.json +1 -1
  139. package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/report-write.schema.json +75 -18
  140. package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/scripts/pdm_auto.py +39 -0
  141. package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/scripts/pdm_client.py +97 -10
  142. package/skills/cosmic-build/skills/cosmic-meta-api/modules/verify/SKILL.md +10 -12
  143. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_apps.py +3 -1
  144. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_formactions.py +38 -29
  145. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_listui.py +400 -233
  146. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_metaops.py +87 -45
  147. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_properties.py +42 -15
  148. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_rules.py +135 -5
  149. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/config.py +1 -1
  150. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/execution_errors.py +1 -1
  151. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/formatters.py +17 -11
  152. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/property_reroute.py +15 -5
  153. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/report_ops.py +1429 -238
  154. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/validators.py +56 -28
  155. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/cosmic_api.py +380 -134
  156. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/field_decorator.py +1 -1
  157. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/meta_export.py +12 -11
  158. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/meta_verifier.py +42 -0
  159. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_add_field_placement_closure.py +8 -8
  160. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_build_configuration_readback.py +94 -0
  161. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_build_receipt_resume.py +47 -1
  162. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_metadata_unchanged_fail_closed.py +87 -0
  163. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_dym_evidence.py +23 -23
  164. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_entity_integrity.py +279 -4
  165. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_field_guard.py +1 -1
  166. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_plugin_contract.py +509 -32
  167. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_readback_derivation.py +178 -1
  168. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_recreate_plan.py +125 -10
  169. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_serve_contract.py +208 -0
  170. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_typed_filters.py +259 -50
  171. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_rule_inventory_perf.py +29 -0
  172. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_rule_write_closure.py +20 -0
  173. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_post_build_recovery.py +18 -0
  174. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_verification_evidence.py +35 -0
  175. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_identity_guard.py +25 -0
  176. package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_time_range_rule.py +98 -0
  177. package/skills/cosmic-build/skills/cosmic-meta-knowledge/domain-model/report-model.md +20 -9
  178. package/skills/cosmic-build/skills/cosmic-meta-knowledge/generation/report-modeling-guide.md +55 -25
  179. package/skills/cosmic-build/skills/cosmic-meta-knowledge/property-guides/report-column-properties.md +15 -39
  180. package/skills/cosmic-build/skills/cosmic-meta-modeling/SKILL.md +19 -17
  181. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/SKILL.md +82 -38
  182. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-batch.md +64 -19
  183. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-plugin.md +32 -20
  184. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-tree.md +78 -31
  185. package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/report-artifact-schema.json +2 -1
  186. package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-companion-fields.md +3 -0
  187. package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-list-configuration.md +2 -0
  188. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_contract.py +3 -1
  189. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_emit.py +27 -4
  190. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/report_validator.py +99 -44
  191. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/test_report_examples.py +31 -4
  192. package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/test_report_validator.py +129 -29
  193. package/skills/cosmic-build/skills/cosmic-meta-modifier/SKILL.md +6 -2
  194. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/validation.md +2 -0
  195. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/SKILL.md +1 -1
  196. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/report-op/SKILL.md +104 -25
  197. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/SKILL.md +9 -0
  198. package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/verify/usage-guide.md +2 -2
  199. package/skills/cosmic-build/skills/cosmic-meta-modifier/references/list-filter-modification.md +39 -12
  200. package/skills/cosmic-build/skills/mcp-server/SKILL.md +39 -24
  201. package/skills/cosmic-build/skills/mcp-server/scripts/mcp_cli.py +20 -6
  202. package/skills/cosmic-dev-best-practices/SKILL.md +1 -1
  203. package/skills/cosmic-dev-best-practices/references/README.md +2 -2
  204. package/skills/cosmic-dev-best-practices/references/basedata-dev-convention.md +2 -2
  205. package/skills/cosmic-dev-best-practices/references/bill-dev-convention.md +4 -4
  206. package/skills/cosmic-dev-best-practices/references/design-document-convention.md +2 -2
  207. package/skills/cosmic-dev-best-practices/references/design-examples/basedata-design-example.md +4 -4
  208. package/skills/cosmic-dev-best-practices/references/design-examples/bill-design-example.md +13 -13
  209. package/skills/cosmic-dev-best-practices/references/elements/CustomerField.md +1 -1
  210. package/skills/cosmic-dev-best-practices/references/elements/README.md +1 -1
  211. package/skills/cosmic-dev-best-practices/references/elements/SupplierField.md +1 -1
  212. package/skills/cosmic-dev-best-practices/references/elements/common-field-property-conventions.md +109 -13
  213. package/skills/cosmic-dev-best-practices/references/layout-control-convention.md +1 -1
  214. package/skills/erp-analysis/references/delivery-contract.md +2 -1
  215. package/skills/erp-analysis/templates/AGENTS.template.md +3 -1
  216. package/skills/cosmic-build/assets/platform-report-filters/TypedReportFilterFields.java +0 -169
  217. package/skills/cosmic-build/assets/platform-report-filters/platform.patch +0 -98
  218. package/skills/cosmic-build/scripts/apply_report_filter_extension.py +0 -39
@@ -86,7 +86,7 @@ def _is_standard(isv: str, standard: bool | None = None) -> bool:
86
86
  """Determine whether to use standard naming rules (no ISV prefix).
87
87
 
88
88
  本地补丁 #16:命名规则强制标准模式(preset 作者 2026-08-27)——原判定整段停用。
89
- 上游逻辑(按参数/身份判定)保留在下方注释,便于对照与回退。
89
+ 上游逻辑保留在下方注释,便于对照与回退。
90
90
  """
91
91
  # 上游原逻辑(已停用):
92
92
  # if standard is not None:
@@ -39,7 +39,7 @@ if str(_ROOT_SCRIPTS) not in sys.path:
39
39
  sys.path.insert(0, str(_ROOT_SCRIPTS))
40
40
 
41
41
  from kd_env_loader import load_effective_skill_config, get_locale
42
- from kd_auth import get_token
42
+ from kd_auth import get_token, make_auth_headers
43
43
  from _http_client import DEFAULT_USER_AGENT, with_default_user_agent, get_ssl_context
44
44
 
45
45
 
@@ -64,14 +64,11 @@ def do_auth(cfg):
64
64
  return token
65
65
 
66
66
 
67
- def _api_get(base_url, path, access_token):
68
- url = base_url.rstrip("/") + path
69
- req = urllib.request.Request(url, headers={
70
- "access_token": access_token,
71
- "api": "true",
72
- "Content-Type": "application/json",
73
- "User-Agent": DEFAULT_USER_AGENT,
74
- })
67
+ def _api_get(cfg, path, access_token):
68
+ url = cfg["base_url"].rstrip("/") + path
69
+ headers = make_auth_headers(cfg, access_token, style="legacy_api",
70
+ content_type="application/json")
71
+ req = urllib.request.Request(url, headers=headers)
75
72
  with urllib.request.urlopen(req, timeout=60, context=get_ssl_context()) as resp:
76
73
  return json.loads(resp.read().decode("utf-8"))
77
74
 
@@ -84,13 +81,17 @@ def _resolve_cfg(raw, env="server"):
84
81
  """Compatible with nested config (server/pdm sub-nodes) and flat config"""
85
82
  section = raw.get(env, raw)
86
83
  return {
84
+ # Pass the whole section through: kd_auth routes on normalAccessToken and also
85
+ # reads loginUser / userType / acgwIdentity
86
+ **section,
87
87
  "base_url": section.get("base_url") or section.get("baseUrl", ""),
88
88
  "app_id": section.get("app_id") or section.get("appId", ""),
89
89
  "app_secret": section.get("app_secret") or section.get("appSecret", ""),
90
90
  "tenant_id": section.get("tenant_id") or section.get("tenantId", ""),
91
91
  "account_id": section.get("account_id") or section.get("accountId", ""),
92
92
  "login_user": section.get("login_user") or section.get("user", ""),
93
- "user_type": section.get("user_type") or section.get("usertype", "Mobile"),
93
+ "user_type": section.get("user_type") or section.get("userType") or "Mobile",
94
+ "normalAccessToken": section.get("normalAccessToken") is True,
94
95
  "language": section.get("language", "zh_CN"),
95
96
  }
96
97
 
@@ -114,7 +115,7 @@ class MetaExportClient:
114
115
  query = "&".join(f"{k}={v}" for k, v in params.items())
115
116
  path = f"/kapi/v2/devportal/ai-meta/getMetadataExportFiles?{query}"
116
117
 
117
- resp = _api_get(self._cfg["base_url"], path, self._get_token())
118
+ resp = _api_get(self._cfg, path, self._get_token())
118
119
  error_code = resp.get("errorCode")
119
120
  if error_code and error_code != "0":
120
121
  raise RuntimeError(
@@ -176,6 +176,12 @@ class SpecDeriver:
176
176
  "op": "fieldProperty",
177
177
  },
178
178
  ))
179
+ if field_key and fld.get("comboOptions"):
180
+ specs.append(AssertionSpec(type="combo_options_equal", target=field_key,
181
+ expected=fld["comboOptions"], metadata={"fieldType": field_type}))
182
+ if field_key and fld.get("unitFieldKey"):
183
+ specs.append(AssertionSpec(type="unit_binding_equals", target=field_key,
184
+ expected=fld["unitFieldKey"], metadata={"fieldType": field_type}))
179
185
  for setting in artifact.get("propertySettings") or []:
180
186
  if not isinstance(setting, dict):
181
187
  continue
@@ -460,6 +466,8 @@ class Verifier:
460
466
  "field_exists": self._check_field_exists,
461
467
  "field_not_exists": self._check_field_not_exists,
462
468
  "property_equals": self._check_property_equals,
469
+ "combo_options_equal": self._check_field_configuration,
470
+ "unit_binding_equals": self._check_field_configuration,
463
471
  "element_exists": self._check_element_exists,
464
472
  "element_not_exists": self._check_element_not_exists,
465
473
  "element_parent": self._check_element_parent,
@@ -765,6 +773,40 @@ class Verifier:
765
773
  message=f"Field '{target}' still exists in the form structure (expected to be deleted)",
766
774
  )
767
775
 
776
+ def _check_field_configuration(self, spec, form_id, form_number):
777
+ metadata = self._get_full_metadata(form_number)
778
+ if "_error" in metadata:
779
+ return VerifyResult(spec, None, "error", f"loadFullMetadata failed: {metadata['_error']}")
780
+ items = (metadata.get("entitymeta") or {}).get("Items") or []
781
+ matches = [item for item in items if isinstance(item, dict) and item.get("Key") == spec.target]
782
+ if len(matches) != 1:
783
+ return VerifyResult(spec, None, "error", f"Expected one exact field {spec.target}, found {len(matches)}")
784
+ field = matches[0]
785
+ if spec.type == "unit_binding_equals":
786
+ units = [item for item in items if isinstance(item, dict) and item.get("_Type_") == "UnitField"
787
+ and item.get("Id") == field.get("UnitFieldId") and field.get("UnitFieldId")]
788
+ actual = units[0].get("Key") if len(units) == 1 else None
789
+ matches = actual == spec.expected and units[0].get("ParentId") == field.get("ParentId") if units else False
790
+ else:
791
+ if spec.metadata.get("fieldType") == "RadioField":
792
+ groups = [item for item in items if isinstance(item, dict) and item.get("Id") == field.get("Group") and field.get("Group")]
793
+ if len(groups) != 1:
794
+ return VerifyResult(spec, None, "error", f"Radio group for {spec.target} is unresolved")
795
+ field = groups[0]
796
+ actual = []
797
+ for index, option in enumerate(field.get("Items") or []):
798
+ if not isinstance(option, dict):
799
+ return VerifyResult(spec, None, "error", f"Invalid option readback for {spec.target}")
800
+ caption = option.get("Caption")
801
+ if isinstance(caption, dict):
802
+ expected_label = spec.expected[index].get("label") if index < len(spec.expected) else None
803
+ caption = expected_label if expected_label in caption.values() else next(iter(caption.values()), "")
804
+ actual.append({"label": str(caption if caption is not None else ""),
805
+ "value": str(option["Value"]) if option.get("Value") is not None else ""})
806
+ matches = actual == spec.expected
807
+ return VerifyResult(spec, actual, "pass" if matches else "fail",
808
+ f"{spec.target}: {spec.type} {'verified' if matches else 'differs from requested configuration'}")
809
+
768
810
  def _check_property_equals(self, spec, form_id, form_number):
769
811
  if not form_number:
770
812
  return VerifyResult(
@@ -18,9 +18,9 @@ import cosmic_api # noqa: E402
18
18
 
19
19
  @pytest.mark.parametrize("batch", [False, True])
20
20
  def test_serve_add_field_uses_verified_destination(batch):
21
- args = {"formName": "form", "fieldName": "Field", "bizWord": "field", "panel": "panel"}
21
+ args = {"formName": "form", "fieldName": "Field", "bizWord": "field", "fieldType": "TextField", "panel": "panel"}
22
22
  if batch:
23
- args = {"formName": "form", "fields": [{"name": "Field", "bizWord": "field", "panel": "panel"}]}
23
+ args = {"formName": "form", "fields": [{"name": "Field", "bizWord": "field", "fieldType": "TextField", "panel": "panel"}]}
24
24
  prepared = {"status": True}
25
25
  placements = [{"panel": "resolved_panel"}]
26
26
  created = _field_result("field")
@@ -39,7 +39,7 @@ def test_serve_missing_destination_stops_creation():
39
39
  "cosmic_api.do_add_fields",
40
40
  ) as create:
41
41
  result = cosmic_api._dispatch_serve({}, "token", "addField", {
42
- "formName": "form", "fields": [{"name": "Field", "bizWord": "field", "panel": "missing"}],
42
+ "formName": "form", "fields": [{"name": "Field", "bizWord": "field", "fieldType": "TextField", "panel": "missing"}],
43
43
  })
44
44
  assert result is blocked
45
45
  create.assert_not_called()
@@ -66,13 +66,13 @@ def _field_result(*keys):
66
66
 
67
67
  def test_batch_contract_accepts_per_field_placement_and_rejects_two_anchors():
68
68
  cosmic_api.validate_add_fields_input([
69
- {"name": "验收字段A", "bizWord": "acceptanceFieldA", "required": True},
70
- {"name": "验收字段B", "bizWord": "acceptanceFieldB", "panel": "高级信息"},
69
+ {"name": "验收字段A", "bizWord": "acceptanceFieldA", "fieldType": "TextField", "required": True},
70
+ {"name": "验收字段B", "bizWord": "acceptanceFieldB", "fieldType": "TextField", "panel": "高级信息"},
71
71
  ])
72
72
 
73
73
  try:
74
74
  cosmic_api.validate_add_fields_input([{
75
- "name": "验收字段B", "bizWord": "acceptanceFieldB",
75
+ "name": "验收字段B", "bizWord": "acceptanceFieldB", "fieldType": "TextField",
76
76
  "before": "字段一", "after": "字段二",
77
77
  }])
78
78
  except ValueError as exc:
@@ -84,8 +84,8 @@ def test_batch_contract_accepts_per_field_placement_and_rejects_two_anchors():
84
84
  def test_mixed_batch_does_not_apply_shared_panel_to_unplaced_fields():
85
85
  args = SimpleNamespace(
86
86
  fields=[
87
- {"name": "验收字段A", "bizWord": "acceptanceFieldA", "required": True},
88
- {"name": "验收字段B", "bizWord": "acceptanceFieldB", "panel": "高级信息"},
87
+ {"name": "验收字段A", "bizWord": "acceptanceFieldA", "fieldType": "TextField", "required": True},
88
+ {"name": "验收字段B", "bizWord": "acceptanceFieldB", "fieldType": "TextField", "panel": "高级信息"},
89
89
  ],
90
90
  panel=None, before=None, after=None,
91
91
  )
@@ -0,0 +1,94 @@
1
+ import copy
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
8
+ import meta_verifier as verifier
9
+ import cosmic_api
10
+
11
+
12
+ def artifact():
13
+ return {"entities": [{"entityKey": "example", "fields": [
14
+ {"fieldKey": "mode", "dataType": "ComboField", "comboOptions": [
15
+ {"label": "Pending", "value": "0"}, {"label": "Accepted", "value": "accepted"}]},
16
+ {"fieldKey": "quantity", "dataType": "QtyField", "unitFieldKey": "measure"},
17
+ {"fieldKey": "measure", "dataType": "UnitField"},
18
+ ]}]}
19
+
20
+
21
+ def metadata():
22
+ return {"entitymeta": {"Items": [
23
+ {"Key": "mode", "_Type_": "ComboField", "Items": [
24
+ {"Caption": {"zh_CN": "待处理", "en_US": "Pending"}, "Value": 0},
25
+ {"Caption": "Accepted", "Value": "accepted"}]},
26
+ {"Key": "quantity", "_Type_": "QtyField", "UnitFieldId": "UNIT", "ParentId": "ENTRY"},
27
+ {"Key": "measure", "_Type_": "UnitField", "Id": "UNIT", "ParentId": "ENTRY"},
28
+ ]}}
29
+
30
+
31
+ def configuration_specs():
32
+ return [spec for spec in verifier.SpecDeriver.from_build_artifact(artifact())
33
+ if spec.type in {"combo_options_equal", "unit_binding_equals"}]
34
+
35
+
36
+ def test_configuration_receipts_use_one_shared_native_read(monkeypatch):
37
+ calls = []
38
+ def read(*args):
39
+ calls.append(args)
40
+ return {"status": True, "data": metadata()}
41
+ monkeypatch.setattr(verifier, "do_load_full_metadata", read)
42
+ report = verifier.Verifier({}, "test").verify("FORM", "example", configuration_specs())
43
+ assert report.status == "pass"
44
+ assert len(calls) == 1
45
+ receipt = cosmic_api._compact_verified_targets(report)
46
+ assert receipt[0]["actual"] == artifact()["entities"][0]["fields"][0]["comboOptions"]
47
+ assert receipt[1]["expected"] == receipt[1]["actual"] == "measure"
48
+
49
+
50
+ @pytest.mark.parametrize("change", ["stored-value", "label", "order", "extra", "wrong-unit", "other-entry"])
51
+ def test_mismatched_configuration_never_becomes_verified(monkeypatch, change):
52
+ data = metadata()
53
+ items = data["entitymeta"]["Items"]
54
+ if change == "stored-value":
55
+ items[0]["Items"][0]["Value"] = "generated_1"
56
+ if change == "label":
57
+ items[0]["Items"][0]["Caption"] = "Different"
58
+ if change == "order":
59
+ items[0]["Items"].reverse()
60
+ if change == "extra":
61
+ items[0]["Items"].append({"Caption": "Extra", "Value": "extra"})
62
+ if change == "wrong-unit":
63
+ items[1]["UnitFieldId"] = "MISSING"
64
+ if change == "other-entry":
65
+ items[2]["ParentId"] = "OTHER"
66
+ monkeypatch.setattr(verifier, "do_load_full_metadata", lambda *args: {"status": True, "data": data})
67
+ report = verifier.Verifier({}, "test").verify("FORM", "example", configuration_specs())
68
+ assert report.status == "fail"
69
+ assert report.summary["failed"] == 1
70
+ assert len(cosmic_api._compact_verified_targets(report)) == 1
71
+ assert any(result.actual != result.spec.expected or change == "other-entry"
72
+ for result in report.results if result.status == "fail")
73
+
74
+
75
+ def test_unavailable_readback_is_an_error_and_never_a_pass(monkeypatch):
76
+ monkeypatch.setattr(verifier, "do_load_full_metadata", lambda *args: {"status": False, "message": "Unavailable"})
77
+ report = verifier.Verifier({}, "test").verify("FORM", "example", configuration_specs())
78
+ assert report.status == "partial"
79
+ assert report.summary["errors"] == 2
80
+ assert cosmic_api._compact_verified_targets(report) == []
81
+
82
+
83
+ def test_requested_shared_unit_is_legal_and_preserved(monkeypatch):
84
+ data = artifact()
85
+ second = copy.deepcopy(data["entities"][0]["fields"][1])
86
+ second["fieldKey"] = "other_quantity"
87
+ data["entities"][0]["fields"].append(second)
88
+ observed = metadata()
89
+ observed["entitymeta"]["Items"].append({"Key": "other_quantity", "_Type_": "QtyField", "UnitFieldId": "UNIT", "ParentId": "ENTRY"})
90
+ monkeypatch.setattr(verifier, "do_load_full_metadata", lambda *args: {"status": True, "data": observed})
91
+ specs = [spec for spec in verifier.SpecDeriver.from_build_artifact(data) if spec.type == "unit_binding_equals"]
92
+ report = verifier.Verifier({}, "test").verify("FORM", "example", specs)
93
+ assert report.status == "pass"
94
+ assert len(report.results) == 2
@@ -9,6 +9,9 @@ from unittest.mock import patch
9
9
  import pytest
10
10
 
11
11
  SCRIPTS = Path(__file__).resolve().parents[1]
12
+ PACKAGE = SCRIPTS.parents[2]
13
+ if PACKAGE.parent.name == ".locales":
14
+ PACKAGE = PACKAGE.parent.parent
12
15
  sys.path.insert(0, str(SCRIPTS))
13
16
  import cosmic_api
14
17
  from _core.build_receipts import BuildReceipts
@@ -17,6 +20,32 @@ from _core.execution_errors import attach_execution_error
17
20
  from execution_binding import execution_binding, validate_execution_binding
18
21
 
19
22
 
23
+ @pytest.mark.parametrize("selected_locale,current_locale", [("", ".locales/zh"), (".locales/zh", "")])
24
+ def test_locale_entry_reuses_exact_selected_source_without_rebinding(tmp_path, selected_locale, current_locale):
25
+ import execution_binding as binding
26
+ package = tmp_path / "installed"
27
+ current = package / current_locale
28
+ selected = package / selected_locale
29
+ entry = selected / "skills/cosmic-meta-api/scripts/cosmic_api.py"
30
+ entry.parent.mkdir(parents=True)
31
+ entry.write_text("# selected runtime")
32
+ app = tmp_path / "app"
33
+ (app / "_runtime").mkdir(parents=True)
34
+ context = app / "_runtime/context.json"
35
+ original = json.dumps({"executionBinding": {"rootDir": str(selected), "revision": "unchanged"}})
36
+ context.write_text(original)
37
+ with patch.object(binding, "__file__", str(current / "scripts/execution_binding.py")):
38
+ assert binding.bound_entrypoint(app, "skills/cosmic-meta-api/scripts/cosmic_api.py") == entry
39
+ with pytest.raises(ValueError, match="SKILL_BOUND_ENTRY_MISSING"):
40
+ binding.bound_entrypoint(app, "skills/missing.py")
41
+ context.write_text(json.dumps({"executionBinding": {"rootDir": str(tmp_path / "another-install")}}))
42
+ assert binding.bound_entrypoint(app, "skills/cosmic-meta-api/scripts/cosmic_api.py") is None
43
+ context.write_text(original)
44
+ with patch.object(binding, "__file__", str(selected / "scripts/execution_binding.py")):
45
+ assert binding.bound_entrypoint(app, "skills/cosmic-meta-api/scripts/cosmic_api.py") is None
46
+ assert context.read_text() == original
47
+
48
+
20
49
  @contextmanager
21
50
  def _isolated_cli():
22
51
  from _core import config, formatters
@@ -43,6 +72,23 @@ def _artifacts(tmp_path):
43
72
  return data
44
73
 
45
74
 
75
+ def test_cli_forwards_once_and_preserves_selected_runtime_failure(tmp_path, capfd):
76
+ selected = tmp_path / "selected cli.py"
77
+ selected.write_text('import json,sys\nprint(json.dumps({"status":False,"errorCode":"SKILL_REVISION_MISMATCH","argv":sys.argv[1:]}))\nsys.exit(3)\n')
78
+ arguments = ["--stdout", "getProperties", "--app-root", str(tmp_path),
79
+ "--formNumber", "example", "--elementKey", "amount", "--propertyNames", "Scale"]
80
+ with _isolated_cli(), patch.object(sys, "argv", ["cosmic_api.py", *arguments]), \
81
+ patch.object(cosmic_api, "_resolve_app_root_arg", return_value=str(tmp_path)), \
82
+ patch.object(cosmic_api, "bound_entrypoint", return_value=selected), \
83
+ patch.object(cosmic_api, "validate_execution_binding", side_effect=AssertionError("must validate in selected runtime")), \
84
+ patch.object(cosmic_api, "load_config", return_value={}), \
85
+ patch.object(cosmic_api, "do_auth", side_effect=AssertionError("must not authenticate twice")):
86
+ assert cosmic_api.main() == 3
87
+ result = json.loads(capfd.readouterr().out)
88
+ assert result["errorCode"] == "SKILL_REVISION_MISMATCH"
89
+ assert result["argv"] == arguments
90
+
91
+
46
92
  def test_verified_dependency_receipt_survives_restart_and_only_pending_target_runs(tmp_path):
47
93
  artifacts = _artifacts(tmp_path)
48
94
  base, bill = artifacts
@@ -132,7 +178,7 @@ def test_reference_or_input_errors_do_not_claim_platform_limit(code, category):
132
178
  @pytest.mark.parametrize("locale", ["", ".locales/zh"])
133
179
  def test_cli_stdout_and_result_file_are_the_same_json_in_path_with_spaces(tmp_path, locale):
134
180
  output = tmp_path / "result with spaces.json"
135
- result = subprocess.run([sys.executable, str(SCRIPTS.parents[2] / locale / "skills" / "cosmic-meta-api" / "scripts" / "cosmic_api.py"), "--stdout", "--output", str(output),
181
+ result = subprocess.run([sys.executable, str(PACKAGE / locale / "skills" / "cosmic-meta-api" / "scripts" / "cosmic_api.py"), "--stdout", "--output", str(output),
136
182
  "describeCommand", "runBatch"], capture_output=True, text=True)
137
183
  assert result.returncode == 0, result.stderr
138
184
  assert json.loads(result.stdout) == json.loads(output.read_text())
@@ -37,9 +37,12 @@ so rewording the messages keeps them passing.
37
37
 
38
38
  from __future__ import annotations
39
39
 
40
+ import copy
40
41
  import sys
41
42
  from pathlib import Path
42
43
 
44
+ import pytest
45
+
43
46
  SCRIPTS = Path(__file__).resolve().parents[1]
44
47
  if str(SCRIPTS) not in sys.path:
45
48
  sys.path.insert(0, str(SCRIPTS))
@@ -121,6 +124,90 @@ def test_change_decision_is_one_function() -> None:
121
124
  assert workspace_gate(NOOP_RESULT) is _effected_metadata_change(NOOP_RESULT) is False
122
125
 
123
126
 
127
+ COMBO_OP = {"op": "modify", "target": {"treeType": "entity", "elementType": "field", "value": "status"},
128
+ "path": "comboItems.add", "value": [{"value": "A", "caption": "Draft"}]}
129
+
130
+
131
+ def _receipt_result(ops, statuses, changed=False):
132
+ return {"status": True, "errorCode": "0", "data": {
133
+ "success": True, "formId": "60HJ2YS9I1R5", "opsCount": len(ops), "stage": "committed",
134
+ "writeAttempted": True, "rolledBack": False, "metadataChanged": changed,
135
+ "opResults": [{"opIndex": index, "op": op["op"], "treeType": op["target"]["treeType"],
136
+ "elementType": op["target"]["elementType"], "target": op["target"]["value"],
137
+ "path": op.get("path"), "status": status}
138
+ for index, (op, status) in enumerate(zip(ops, statuses))]}}
139
+
140
+
141
+ def _run_receipt(monkeypatch, result, ops=None):
142
+ monkeypatch.setattr(cmd_metaops, "_api_call", lambda *a, **k: result)
143
+ return cmd_metaops.do_modify_meta({}, "token", "60HJ2YS9I1R5", ops or [COMBO_OP],
144
+ form_number="kdtest_bill")
145
+
146
+
147
+ def test_complete_matching_noop_is_success_without_claiming_change(monkeypatch):
148
+ result = _receipt_result([COMBO_OP], ["no_op"])
149
+ before = copy.deepcopy(result)
150
+ outcome = _run_receipt(monkeypatch, result)
151
+ assert outcome["status"] is True
152
+ assert outcome["data"]["noOp"] is True
153
+ assert outcome["data"]["metadataChanged"] is False
154
+ assert outcome["data"]["noOpEvidence"]["scope"] == "handler"
155
+ assert _effected_metadata_change(outcome) is False # 不因合法 no_op 导出/同步 DYM。
156
+ assert result == before
157
+
158
+
159
+ @pytest.mark.parametrize("changes", [
160
+ {"status": "no_channel"}, {"status": "rejected"}, {"status": "unknown"},
161
+ {"opIndex": -1}, {"opIndex": True}, {"opIndex": "0"}, {"target": "other"},
162
+ {"path": "comboItems.remove"}, {"op": "remove"}, {"treeType": "form"},
163
+ {"errorCode": "META_PATH_NO_CHANNEL"}])
164
+ @pytest.mark.parametrize("changed", [False, True])
165
+ def test_incomplete_or_wrong_op_receipt_fails_even_if_metadata_changed(monkeypatch, changes, changed):
166
+ result = _receipt_result([COMBO_OP], ["no_op"], changed)
167
+ result["data"]["opResults"][0].update(changes)
168
+ outcome = _run_receipt(monkeypatch, result)
169
+ assert outcome["status"] is False
170
+ assert outcome["errorCode"] == "METADATA_OPS_UNCONFIRMED"
171
+ assert outcome["data"]["platformResult"] is result
172
+
173
+
174
+ @pytest.mark.parametrize("changes", [
175
+ {"opResults": []}, {"opResults": None}, {"stage": "precheck"}, {"stage": "post_commit"},
176
+ {"rolledBack": True}, {"formId": "other"}, {"opsCount": 2}])
177
+ def test_missing_coverage_or_unconfirmed_stage_is_not_noop_success(monkeypatch, changes):
178
+ result = _receipt_result([COMBO_OP], ["no_op"])
179
+ result["data"].update(changes)
180
+ assert _run_receipt(monkeypatch, result)["errorCode"] == "METADATA_OPS_UNCONFIRMED"
181
+
182
+
183
+ def test_duplicate_and_partial_receipts_cannot_confirm_whole_batch(monkeypatch):
184
+ ops = [COMBO_OP, {**COMBO_OP, "target": {**COMBO_OP["target"], "value": "status2"}}]
185
+ result = _receipt_result(ops, ["no_op", "no_op"])
186
+ result["data"]["opResults"][1]["opIndex"] = 0
187
+ assert _run_receipt(monkeypatch, result, ops)["errorCode"] == "METADATA_OPS_UNCONFIRMED"
188
+ result = _receipt_result(ops, ["no_op", "submitted"])
189
+ assert _run_receipt(monkeypatch, result, ops)["errorCode"] == "METADATA_UNCHANGED"
190
+
191
+
192
+ def test_applied_false_snapshot_stays_unconfirmed_and_real_update_passes(monkeypatch):
193
+ result = _receipt_result([COMBO_OP], ["applied"])
194
+ assert _run_receipt(monkeypatch, result)["errorCode"] == "METADATA_UNCHANGED"
195
+ result["data"]["metadataChanged"] = True
196
+ assert _run_receipt(monkeypatch, result) is result
197
+ result = _receipt_result([COMBO_OP], ["no_op"], changed=True)
198
+ assert _run_receipt(monkeypatch, result)["errorCode"] == "METADATA_OPS_UNCONFIRMED"
199
+
200
+
201
+ def test_batch_verified_noop_is_not_unknown_after_write(monkeypatch):
202
+ result = _receipt_result([COMBO_OP], ["no_op"])
203
+ monkeypatch.setattr(cmd_metaops, "_api_call", lambda *a, **k: result)
204
+ batch = cmd_metaops.do_batch_modify({}, "token", [
205
+ {"formId": "60HJ2YS9I1R5", "formNumber": "kdtest_bill", "ops": [COMBO_OP]}])
206
+ assert batch["status"] is True
207
+ assert batch["data"]["results"][0]["outcome"] == "succeeded"
208
+ assert batch["data"]["results"][0]["response"]["data"]["noOp"] is True
209
+
210
+
124
211
  # ── layer 2: the MetaOps preflight ──────────────────────────────────────────────────
125
212
 
126
213
  def _move(extra):
@@ -1,15 +1,10 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """The platform design-state export is a second read-back source for reports.
3
3
 
4
- Verified live: there is no `data.reportSchema`, and `data.formLayout` carries the
5
- control tree only. The default sort lives in the ReportListAp `InitField` property and the
6
- data-fetch plugin lives in `ReportQueryPlugin`, so neither had any observable read-back and
7
- both were reported as `unverifiableSections`.
8
-
9
- syncDym exports the platform's own design-state metadata, and that export does carry both.
10
- These tests pin the parse and the promotion rules, including the two conservative ones: an
11
- empty section in the export never proves absence, and a canonical projection is never
12
- overwritten by a file that may predate the last write.
4
+ formLayout 只提供控件树;syncDym 可以补充 ReportQueryPlugin 和过滤属性证据。
5
+ InitField 是默认显示列配置,其中 purpose:sort 是旧 API 污染,不证明排序持久化或运行生效。
6
+ 这些测试保留污染夹具作为负向回归:不解析或晋升排序;空插件不证明不存在;
7
+ 旧导出不能覆盖 canonical 数据,而 canonical 排序投影同样不能证明运行排序。
13
8
  """
14
9
 
15
10
  import json
@@ -101,15 +96,12 @@ def _derived_schema(filters=()):
101
96
  class ParseDesignStateExportTest(unittest.TestCase):
102
97
  """The export is parsed into exactly the sections it can prove."""
103
98
 
104
- def test_sorts_come_from_init_field_in_order(self):
99
+ def test_polluted_init_field_is_not_sort_evidence(self):
105
100
  with tempfile.TemporaryDirectory() as tmp:
106
101
  path = _write_dym(tmp, "kdtest_rpt",
107
102
  _dym(sorts=(("order_date", "desc"), ("order_no", "asc"))))
108
103
  evidence = report_ops.parse_report_dym(path)
109
- self.assertEqual(evidence["reportSorts"], [
110
- {"fieldKey": "order_date", "direction": "desc"},
111
- {"fieldKey": "order_no", "direction": "asc"},
112
- ])
104
+ self.assertNotIn("reportSorts", evidence)
113
105
 
114
106
  def test_non_sort_init_field_entries_are_ignored(self):
115
107
  with tempfile.TemporaryDirectory() as tmp:
@@ -118,7 +110,7 @@ class ParseDesignStateExportTest(unittest.TestCase):
118
110
  '"purpose": "freeze", "direction": "desc"')
119
111
  path = _write_dym(tmp, "kdtest_rpt", body)
120
112
  evidence = report_ops.parse_report_dym(path)
121
- self.assertEqual(evidence["reportSorts"], [])
113
+ self.assertNotIn("reportSorts", evidence)
122
114
 
123
115
  def test_only_the_report_query_plugin_slot_counts(self):
124
116
  with tempfile.TemporaryDirectory() as tmp:
@@ -163,21 +155,20 @@ class PromoteDesignStateEvidenceTest(unittest.TestCase):
163
155
  report_ops.attach_dym_evidence(target, evidence)
164
156
  return target
165
157
 
166
- def test_both_sections_become_verifiable(self):
158
+ def test_only_plugins_become_verifiable(self):
167
159
  schema = self._attach(sorts=(("order_date", "desc"),),
168
160
  plugins=(("kd.kdtest.report.P", True, "fetch"),))
169
161
  self.assertEqual(schema["readBackSource"], "formLayout+dym")
170
- self.assertEqual(schema["unverifiableSections"], [])
171
- self.assertEqual(schema["dymEvidence"]["provedSections"],
172
- ["reportSorts", "reportPlugins"])
162
+ self.assertEqual(schema["unverifiableSections"], ["reportSorts"])
163
+ self.assertEqual(schema["dymEvidence"]["provedSections"], ["reportPlugins"])
173
164
  self.assertEqual(schema["undecidablePluginProperties"],
174
165
  ["type", "bizAppId", "fpk", "description"])
175
166
 
176
167
  def test_an_empty_section_never_proves_absence(self):
177
168
  schema = self._attach(sorts=(("order_date", "desc"),))
178
169
  # No plugin in the export: that is missing evidence, not proof of no plugin.
179
- self.assertEqual(schema["unverifiableSections"], ["reportPlugins"])
180
- self.assertEqual(schema["dymEvidence"]["provedSections"], ["reportSorts"])
170
+ self.assertEqual(schema["unverifiableSections"], ["reportSorts", "reportPlugins"])
171
+ self.assertNotIn("dymEvidence", schema)
181
172
 
182
173
  def test_canonical_projection_is_never_overwritten(self):
183
174
  canonical = {
@@ -289,16 +280,25 @@ class VerifyAgainstDesignStateEvidenceTest(unittest.TestCase):
289
280
  report_ops.attach_dym_evidence(schema, evidence)
290
281
  return schema
291
282
 
292
- def test_sorts_are_fully_verified(self):
283
+ def test_matching_pollution_never_verifies_sorting(self):
293
284
  result = report_ops.report_readback_verification(
294
285
  "sorts",
295
286
  [{"fieldKey": "order_date", "direction": "desc"},
296
287
  {"fieldKey": "order_no", "direction": "asc"}],
297
288
  self._promoted(),
298
289
  )
299
- self.assertTrue(result["verified"], result)
290
+ self.assertFalse(result["verified"])
291
+ self.assertEqual(result["errorCode"], "REPORT_SORTS_API_UNSAFE")
300
292
  self.assertEqual(result["source"], "formLayout+dym")
301
293
 
294
+ def test_matching_canonical_sorts_are_still_unverified(self):
295
+ sorts = [{"fieldKey": "order_date", "direction": "desc"}]
296
+ result = report_ops.report_readback_verification("sorts", sorts, {
297
+ "readBackAvailable": True, "readBackSource": "canonical", "reportSorts": sorts,
298
+ })
299
+ self.assertFalse(result["verified"])
300
+ self.assertEqual(result["errorCode"], "REPORT_SORTS_API_UNSAFE")
301
+
302
302
  def test_wrong_sort_direction_is_a_mismatch(self):
303
303
  result = report_ops.report_readback_verification(
304
304
  "sorts", [{"fieldKey": "order_date", "direction": "asc"}], self._promoted())