@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
@@ -42,16 +42,68 @@ def _column(field_key, field_type="decimal", presentation=None, date_index=None)
42
42
  return column
43
43
 
44
44
 
45
- def _compile(columns=(), filters=(), plugins=(("kd.kdtest.report.P", "Java"),)):
46
- normalized_columns = report_ops.normalize_report_columns_input(list(columns)) if columns else []
45
+ def _compile_requirements(columns=(), filters=(), plugins=(("kd.kdtest.report.P", "Java"),)):
46
+ """仅测试原始/历史需求编译;此路径不证明输入可写,也不调用 writer。"""
47
47
  normalized_filters = report_ops.normalize_report_filters_input(list(filters)) if filters else []
48
48
  return report_ops.compile_report_plugin_contract({
49
- "reportColumns": normalized_columns,
49
+ "reportColumns": list(columns),
50
50
  "reportFilters": normalized_filters,
51
51
  "reportPlugins": [{"className": name, "type": kind} for name, kind in plugins],
52
52
  }, "kdtest_rpt")
53
53
 
54
54
 
55
+ class DefaultSortContractTest(unittest.TestCase):
56
+ def _artifact(self):
57
+ return {
58
+ "reportColumns": [{"kind": "group", "fieldKey": "group", "caption": "Group",
59
+ "children": [_column("created", "date", date_index="CreateTime"),
60
+ _column("amount")]}],
61
+ "reportSorts": [{"fieldKey": "created", "direction": "desc"},
62
+ {"fieldKey": "amount", "direction": "asc"}],
63
+ "reportPlugins": [{"className": "kd.test.report.P", "type": "Java", "enabled": True}],
64
+ }
65
+
66
+ def test_order_and_dataset_alias_reach_query_contract(self):
67
+ artifact = self._artifact()
68
+ contract = report_ops.compile_report_plugin_contract(artifact, "kdtest_rpt")
69
+ self.assertEqual(contract["defaultSorts"], [
70
+ {"fieldKey": "created", "dateIndex": "CreateTime", "direction": "desc"},
71
+ {"fieldKey": "amount", "dateIndex": "amount", "direction": "asc"},
72
+ ])
73
+ self.assertEqual(contract["defaultSortPolicy"]["priority"], "interactiveThenDefault")
74
+ self.assertFalse(contract["defaultSortPolicy"]["metadataWritten"])
75
+ self.assertFalse(contract["defaultSortPolicy"]["runtimeVerified"])
76
+ self.assertNotIn("dateIndex", artifact["reportSorts"][0])
77
+
78
+ def test_missing_or_group_sort_reference_fails(self):
79
+ for key in ("missing", "group"):
80
+ with self.subTest(key=key):
81
+ artifact = self._artifact()
82
+ artifact["reportSorts"][0]["fieldKey"] = key
83
+ with self.assertRaisesRegex(ValueError, "sort columns not found"):
84
+ report_ops.compile_report_default_sorts(artifact)
85
+
86
+ def test_missing_dataset_binding_is_an_input_error(self):
87
+ artifact = self._artifact()
88
+ del artifact["reportColumns"][0]["children"][0]["dateIndex"]
89
+ with self.assertRaisesRegex(ValueError, "dateIndex"):
90
+ report_ops.compile_report_default_sorts(artifact)
91
+
92
+ def test_sorting_requires_one_enabled_java_fetch_plugin(self):
93
+ for plugins in ([], [{"className": "P", "type": "JS"}],
94
+ [{"className": "P", "type": "KingScript"}],
95
+ [{"className": "P", "enabled": False}],
96
+ [{"className": "P"}, {"className": "Q"}]):
97
+ with self.subTest(plugins=plugins):
98
+ artifact = self._artifact()
99
+ artifact["reportPlugins"] = plugins
100
+ artifact["queryDataSource"] = {"dataSourceNumber": "ds"}
101
+ with self.assertRaisesRegex(ValueError, "REPORT_DEFAULT_SORT_PLUGIN_REQUIRED"):
102
+ report_ops.compile_report_default_sorts(artifact)
103
+ artifact.pop("reportSorts")
104
+ self.assertEqual(report_ops.compile_report_default_sorts(artifact), [])
105
+
106
+
55
107
  class PresentationInputContractTest(unittest.TestCase):
56
108
  """Presentation is validated against the column it decorates."""
57
109
 
@@ -97,11 +149,107 @@ class PresentationInputContractTest(unittest.TestCase):
97
149
  self.assertIn("scale must be an integer", self._reject(
98
150
  _column("amt", presentation={"kind": "decimal", "scale": 42})))
99
151
 
100
- def test_a_valid_presentation_survives_normalization(self):
152
+ def test_an_explicit_decimal_presentation_survives_normalization(self):
101
153
  columns = report_ops.normalize_report_columns_input([
102
- _column("amt", presentation={"kind": "amount", "currencyField": "cur", "scale": 2})])
103
- self.assertEqual(columns[0]["presentation"],
104
- {"kind": "amount", "currencyField": "cur", "scale": 2})
154
+ _column("value", presentation={"kind": "decimal", "scale": 2})])
155
+ self.assertEqual(columns[0]["presentation"], {"kind": "decimal", "scale": 2})
156
+
157
+
158
+ class DynamicPrecisionWriteGuardTest(unittest.TestCase):
159
+ """测试真实门禁和入口;HTTP/auth 仅作不应触达的边界探针。"""
160
+
161
+ def _columns(self, kind="amount", field_type="bigint", hide=True, nested=False, scale=None):
162
+ key = "measureUnitField" if kind == "qty" else "currencyField"
163
+ spec = {"kind": kind, key: "reference"}
164
+ if scale is not None:
165
+ spec["scale"] = scale
166
+ columns = [_column("value", presentation=spec),
167
+ {**_column("reference", field_type), "hide": hide}]
168
+ return [{"kind": "group", "fieldKey": "group", "caption": "Group", "children": columns}] if nested else columns
169
+
170
+ def test_scalar_targets_never_prove_a_precision_reference(self):
171
+ import copy
172
+ for kind in ("amount", "price", "qty"):
173
+ for field_type in report_ops._REPORT_COLUMN_FIELD_TYPES:
174
+ for hide in (True, False):
175
+ for nested in (True, False):
176
+ for scale in (None, 2):
177
+ with self.subTest(kind=kind, fieldType=field_type, hide=hide, nested=nested, scale=scale):
178
+ columns = self._columns(kind, field_type, hide, nested, scale)
179
+ before = copy.deepcopy(columns)
180
+ with self.assertRaisesRegex(ValueError, "REPORT_PRECISION_REFERENCE_UNVERIFIED"):
181
+ report_ops.normalize_report_columns_input(columns)
182
+ self.assertEqual(columns, before)
183
+
184
+ def test_direct_writers_and_dry_run_refuse_before_http(self):
185
+ from unittest import mock
186
+ with mock.patch.object(report_ops, "_api_call") as http:
187
+ for writer in (report_ops.set_report_columns, report_ops.add_report_columns):
188
+ for dry_run in (True, False):
189
+ with self.subTest(writer=writer.__name__, dry_run=dry_run):
190
+ with self.assertRaisesRegex(ValueError, "REPORT_PRECISION_REFERENCE_UNVERIFIED"):
191
+ writer({}, "fixture", "FID", self._columns(nested=True), dry_run=dry_run)
192
+ http.assert_not_called()
193
+
194
+ def test_serve_and_pipeline_refuse_before_writers(self):
195
+ import cosmic_api
196
+ from unittest import mock
197
+ for kind in ("amount", "price", "qty"):
198
+ with mock.patch.object(cosmic_api, "do_set_report_columns") as write, \
199
+ mock.patch.object(cosmic_api, "do_add_report_columns") as append, \
200
+ mock.patch.object(cosmic_api, "do_create_page") as create:
201
+ for cmd in ("setReportColumns", "addReportColumns"):
202
+ with self.assertRaisesRegex(ValueError, "REPORT_PRECISION_REFERENCE_UNVERIFIED"):
203
+ cosmic_api._dispatch_serve({}, "fixture", cmd, {"columns": self._columns(kind)})
204
+ with self.assertRaisesRegex(ValueError, "REPORT_PRECISION_REFERENCE_UNVERIFIED"):
205
+ cosmic_api._execute_report_pipeline({}, "fixture", {"reportColumns": self._columns(kind)}, "APP", "UNIT")
206
+ write.assert_not_called()
207
+ append.assert_not_called()
208
+ create.assert_not_called()
209
+
210
+ def test_raw_contract_keeps_requirement_without_claiming_runtime_success(self):
211
+ import copy
212
+ for kind in ("amount", "price", "qty"):
213
+ columns = self._columns(kind)
214
+ artifact = {"reportColumns": columns, "reportSorts": [{"fieldKey": "value", "direction": "asc"}],
215
+ "reportPlugins": [{"className": "kd.test.report.P", "type": "Java", "enabled": True}]}
216
+ before = copy.deepcopy(artifact)
217
+ contract = report_ops.compile_report_plugin_contract(artifact)
218
+ self.assertEqual(artifact, before)
219
+ self.assertEqual(contract["defaultSorts"], [{"fieldKey": "value", "dateIndex": "value", "direction": "asc"}])
220
+ entry = contract["columnPresentation"][0]
221
+ source = entry["precisionSource"]
222
+ self.assertEqual(source["requiredRuntimeValueType"], "DynamicObject")
223
+ self.assertEqual(source["fieldKey"], "reference")
224
+ self.assertFalse(source["rawIdIsReference"])
225
+ self.assertFalse(source["runtimeVerified"])
226
+ self.assertTrue(source["metadataWriteBlocked"])
227
+ self.assertEqual(len(source["requiredEvidence"]), 3)
228
+ self.assertEqual(columns[0]["presentation"]["kind"], kind)
229
+
230
+ def test_dynamic_precision_cannot_refresh_a_successful_authoring_baseline(self):
231
+ from unittest import mock
232
+ with tempfile.TemporaryDirectory() as root:
233
+ report_ops.write_report_authoring_state(root, "sample_rpt", "FID",
234
+ {"reportColumns": [_column("value")]}, {"reportColumns": [_column("value")]})
235
+ path = report_ops.report_authoring_path(root, "sample_rpt")
236
+ before = path.read_bytes()
237
+ for kind in ("amount", "price", "qty"):
238
+ with mock.patch.object(report_ops, "_api_call") as http:
239
+ with self.assertRaisesRegex(ValueError, "REPORT_PRECISION_REFERENCE_UNVERIFIED"):
240
+ report_ops.write_report_authoring_state(root, "sample_rpt", "FID",
241
+ {"reportColumns": self._columns(kind)}, {})
242
+ http.assert_not_called()
243
+ self.assertEqual(path.read_bytes(), before)
244
+
245
+ def test_supported_unbound_types_are_not_precision_fallbacks(self):
246
+ columns = [_column("id", "bigint"), _column("date", "date"),
247
+ _column("measure", presentation={"kind": "decimal", "scale": 3}),
248
+ _column("ref", "bigint", {"kind": "basedata", "entityId": "bd_item", "displayProp": "name"})]
249
+ normalized = report_ops.normalize_report_columns_input(columns)
250
+ self.assertEqual(normalized, columns)
251
+ for entry in _compile_requirements(columns=normalized)["columnPresentation"]:
252
+ self.assertNotIn("precisionSource", entry)
105
253
 
106
254
 
107
255
  class PresentationStaysOutOfPlatformPayloadTest(unittest.TestCase):
@@ -109,14 +257,14 @@ class PresentationStaysOutOfPlatformPayloadTest(unittest.TestCase):
109
257
 
110
258
  def test_column_payload_carries_only_contract_properties(self):
111
259
  columns = report_ops.normalize_report_columns_input([
112
- _column("amt", presentation={"kind": "amount", "currencyField": "cur", "scale": 2})])
260
+ _column("value", presentation={"kind": "decimal", "scale": 2})])
113
261
  payload = report_ops._build_columns_payload(columns)
114
262
  self.assertEqual(sorted(payload[0]), ["caption", "dateIndex", "fieldKey", "fieldType", "kind"])
115
263
 
116
264
  def test_nested_group_children_are_also_clean(self):
117
265
  columns = report_ops.normalize_report_columns_input([{
118
266
  "kind": "group", "fieldKey": "grp", "caption": "G", "children": [
119
- _column("qty", presentation={"kind": "qty", "measureUnitField": "unit"})]}])
267
+ _column("value", presentation={"kind": "decimal", "scale": 3})]}])
120
268
  child = report_ops._build_columns_payload(columns)[0]["children"][0]
121
269
  self.assertNotIn("presentation", child)
122
270
  self.assertNotIn("measureUnitField", child)
@@ -132,13 +280,13 @@ class CompileReportPluginContractTest(unittest.TestCase):
132
280
  report handed Java delivery nothing at all, and a date column returned as text then
133
281
  failed at render time with "setvalue fail, name: <column>, Unparseable date".
134
282
  """
135
- contract = _compile(columns=[_column("amt")])
283
+ contract = _compile_requirements(columns=[_column("amt")])
136
284
  self.assertIsNotNone(contract)
137
285
  self.assertEqual(contract["columnPresentation"], [])
138
286
  self.assertEqual([row["dateIndex"] for row in contract["dataSetColumnTypes"]], ["amt"])
139
287
 
140
288
  def test_nothing_to_say_still_means_no_contract(self):
141
- self.assertIsNone(_compile(columns=[]))
289
+ self.assertIsNone(_compile_requirements(columns=[]))
142
290
 
143
291
  def test_each_kind_maps_to_a_documented_factory_call(self):
144
292
  cases = {
@@ -156,11 +304,11 @@ class CompileReportPluginContractTest(unittest.TestCase):
156
304
  }
157
305
  for kind, (spec, field_type, expected) in cases.items():
158
306
  with self.subTest(kind=kind):
159
- contract = _compile(columns=[_column("amt", field_type, presentation=spec)])
307
+ contract = _compile_requirements(columns=[_column("amt", field_type, presentation=spec)])
160
308
  self.assertEqual(contract["columnPresentation"][0]["java"], expected)
161
309
 
162
310
  def test_no_guessed_type_constant_appears_anywhere(self):
163
- contract = _compile(columns=[
311
+ contract = _compile_requirements(columns=[
164
312
  _column("amt", presentation={"kind": "amount", "currencyField": "cur"}),
165
313
  _column("qty", presentation={"kind": "qty", "measureUnitField": "unit"}),
166
314
  ])
@@ -169,7 +317,7 @@ class CompileReportPluginContractTest(unittest.TestCase):
169
317
  self.assertNotIn(guessed, rendered)
170
318
 
171
319
  def test_scale_on_a_money_column_is_executable_code_not_a_comment(self):
172
- entry = _compile(columns=[
320
+ entry = _compile_requirements(columns=[
173
321
  _column("amt", presentation={"kind": "amount", "currencyField": "cur", "scale": 2}),
174
322
  ])["columnPresentation"][0]
175
323
  self.assertEqual(entry["javaFollowUp"], ["setScale(2)"])
@@ -177,7 +325,7 @@ class CompileReportPluginContractTest(unittest.TestCase):
177
325
  self.assertNotIn("/*", entry["java"])
178
326
 
179
327
  def test_a_plain_decimal_column_takes_scale_from_the_factory(self):
180
- entry = _compile(columns=[
328
+ entry = _compile_requirements(columns=[
181
329
  _column("qty", presentation={"kind": "decimal", "scale": 4}),
182
330
  ])["columnPresentation"][0]
183
331
  self.assertIn(", 4)", entry["java"])
@@ -191,20 +339,20 @@ class CompileReportPluginContractTest(unittest.TestCase):
191
339
  R-presentation-binding now rejects an undeclared target — would only ever be reachable
192
340
  from an artifact the validator refuses.
193
341
  """
194
- contract = _compile(columns=[
342
+ contract = _compile_requirements(columns=[
195
343
  _column("amt", presentation={"kind": "amount", "currencyField": "currency_id"}),
196
344
  _column("qty", presentation={"kind": "qty", "measureUnitField": "unit_id"}),
197
345
  ])
198
346
  self.assertEqual(contract["dataSetColumns"], ["amt", "qty"])
199
347
 
200
348
  def test_group_children_are_compiled_too(self):
201
- contract = _compile(columns=[{
349
+ contract = _compile_requirements(columns=[{
202
350
  "kind": "group", "fieldKey": "grp", "caption": "G", "children": [
203
351
  _column("amt", presentation={"kind": "amount", "currencyField": "cur"})]}])
204
352
  self.assertEqual(contract["columnPresentation"][0]["fieldKey"], "amt")
205
353
 
206
354
  def test_unpersisted_filter_properties_become_requirements(self):
207
- contract = _compile(
355
+ contract = _compile_requirements(
208
356
  columns=[_column("amt")],
209
357
  filters=[{"fieldPath": "flt_date", "displayName": "Date", "compare": "between",
210
358
  "mustInput": True, "defaultValue": "thismonth"}],
@@ -220,7 +368,7 @@ class CompileReportPluginContractTest(unittest.TestCase):
220
368
  between date filter does not become a date-range control. Before this entry existed the
221
369
  comparison reached Java delivery as nothing.
222
370
  """
223
- contract = _compile(
371
+ contract = _compile_requirements(
224
372
  columns=[_column("amt")],
225
373
  filters=[
226
374
  {"fieldPath": "flt_date", "displayName": "Date", "compare": "between"},
@@ -260,7 +408,7 @@ class CompileReportPluginContractTest(unittest.TestCase):
260
408
  self.assertEqual(contract["filterValueContracts"][0]["candidateValueKeys"], ["acme_region", "flt_area", "acme_area"])
261
409
 
262
410
  def test_default_valued_filter_properties_are_not_requirements(self):
263
- contract = _compile(
411
+ contract = _compile_requirements(
264
412
  columns=[_column("amt")],
265
413
  filters=[{"fieldPath": "flt_date", "displayName": "Date", "compare": "between",
266
414
  "mustInput": False, "visible": True}],
@@ -285,17 +433,17 @@ class CompileReportPluginContractTest(unittest.TestCase):
285
433
  }
286
434
  for field_type, (accessor, java_type, algo_type) in expected.items():
287
435
  with self.subTest(fieldType=field_type):
288
- row = _compile(columns=[_column("col", field_type=field_type)])[
436
+ row = _compile_requirements(columns=[_column("col", field_type=field_type)])[
289
437
  "dataSetColumnTypes"][0]
290
438
  self.assertEqual(row["rowAccessor"], accessor)
291
439
  self.assertEqual(row["javaType"], java_type)
292
440
  self.assertEqual(row["algoDataType"], algo_type)
293
- decimal = _compile(columns=[_column("amt")])["dataSetColumnTypes"][0]
441
+ decimal = _compile_requirements(columns=[_column("amt")])["dataSetColumnTypes"][0]
294
442
  self.assertEqual(decimal["rowAccessor"], "getBigDecimal")
295
443
  self.assertTrue(decimal["algoDataType"].startswith("DataType.createBigDecimalType("))
296
444
 
297
445
  def test_a_formula_column_is_not_given_a_guessed_type(self):
298
- row = _compile(columns=[_column("calc", field_type="formula")])["dataSetColumnTypes"][0]
446
+ row = _compile_requirements(columns=[_column("calc", field_type="formula")])["dataSetColumnTypes"][0]
299
447
  self.assertEqual(row["rowAccessor"], "get")
300
448
  self.assertEqual(row["javaType"], "Object")
301
449
  self.assertNotIn("algoDataType", row)
@@ -317,7 +465,7 @@ class CompileReportPluginContractTest(unittest.TestCase):
317
465
  platform cannot render: the runtime entity is built from the column list, so a value
318
466
  arriving outside any column has no property to attach to.
319
467
  """
320
- contract = _compile(columns=[
468
+ contract = _compile_requirements(columns=[
321
469
  _column("cur", field_type="bigint"),
322
470
  _column("amt", presentation={"kind": "amount", "currencyField": "cur"}),
323
471
  ])
@@ -348,13 +496,13 @@ class CompileReportPluginContractTest(unittest.TestCase):
348
496
  self.assertNotIn("unit", contract["dataSetColumns"])
349
497
 
350
498
  def test_a_kind_that_binds_nothing_reports_no_binding(self):
351
- contract = _compile(columns=[
499
+ contract = _compile_requirements(columns=[
352
500
  _column("sup", field_type="bigint", presentation={
353
501
  "kind": "basedata", "entityId": "bd_supplier", "displayProp": "name"})])
354
502
  self.assertNotIn("bindsTo", contract["columnPresentation"][0])
355
503
 
356
504
  def test_one_binding_target_lists_every_column_that_needs_it(self):
357
- contract = _compile(columns=[
505
+ contract = _compile_requirements(columns=[
358
506
  _column("unit", field_type="bigint"),
359
507
  _column("in_qty", presentation={"kind": "qty", "measureUnitField": "unit"}),
360
508
  _column("out_qty", presentation={"kind": "qty", "measureUnitField": "unit"}),
@@ -364,11 +512,11 @@ class CompileReportPluginContractTest(unittest.TestCase):
364
512
  self.assertEqual(contract["dataSetColumns"].count("unit"), 1)
365
513
 
366
514
  def test_a_column_nothing_binds_to_carries_no_back_reference(self):
367
- contract = _compile(columns=[_column("amt")])
515
+ contract = _compile_requirements(columns=[_column("amt")])
368
516
  self.assertNotIn("requiredBy", contract["dataSetColumnTypes"][0])
369
517
 
370
518
  def test_a_grouped_column_is_typed_like_any_other(self):
371
- contract = _compile(columns=[{
519
+ contract = _compile_requirements(columns=[{
372
520
  "kind": "group", "fieldKey": "grp", "caption": "G",
373
521
  "children": [_column("biz_date", field_type="date")]}])
374
522
  self.assertEqual(
@@ -377,7 +525,7 @@ class CompileReportPluginContractTest(unittest.TestCase):
377
525
 
378
526
  def test_a_column_whose_key_differs_from_its_dataset_column_keeps_the_binding(self):
379
527
  """No factory signature takes dateIndex, so setDateIndex has to be emitted."""
380
- contract = _compile(columns=[{
528
+ contract = _compile_requirements(columns=[{
381
529
  "kind": "column", "fieldKey": "material_ref", "dateIndex": "material_id",
382
530
  "fieldType": "bigint", "caption": "Material",
383
531
  "presentation": {"kind": "basedata", "entityId": "bd_material",
@@ -386,23 +534,60 @@ class CompileReportPluginContractTest(unittest.TestCase):
386
534
  ['setDateIndex("material_id")'])
387
535
 
388
536
  def test_a_column_whose_key_equals_its_dataset_column_gets_no_redundant_setter(self):
389
- contract = _compile(columns=[
537
+ contract = _compile_requirements(columns=[
390
538
  _column("amt", presentation={"kind": "amount", "currencyField": "cur", "scale": 2})])
391
539
  self.assertEqual(contract["columnPresentation"][0]["javaFollowUp"], ["setScale(2)"])
392
540
 
393
541
  def test_contract_records_the_registered_plugin_classes(self):
394
- contract = _compile(columns=[
542
+ contract = _compile_requirements(columns=[
395
543
  _column("amt", presentation={"kind": "amount", "currencyField": "cur"})])
396
544
  self.assertEqual(contract["pluginClasses"], ["kd.kdtest.report.P"])
545
+ self.assertEqual(contract["pluginTypes"], {"kd.kdtest.report.P": "reportlist"})
397
546
  self.assertEqual(contract["formNumber"], "kdtest_rpt")
398
547
  self.assertEqual(contract["schemaVersion"], report_ops.REPORT_PLUGIN_CONTRACT_VERSION)
399
548
 
549
+ def test_a_script_plugin_is_not_listed_for_java_delivery(self):
550
+ """pluginClasses feeds the card gate, which looks for an archived .java file.
551
+
552
+ A KingScript or JS registration has no Java source, so listing it made the gate refuse
553
+ the cards for a source that can never exist.
554
+ """
555
+ contract = _compile_requirements(columns=[_column("amt")], plugins=(
556
+ ("kd.kdtest.report.P", "Java"), ("kd.kdtest.report.ScriptPlugin", "KingScript"),
557
+ ("kd.kdtest.report.JsPlugin", "JS")))
558
+ self.assertEqual(contract["pluginClasses"], ["kd.kdtest.report.P"])
559
+
560
+ def test_the_left_tree_plugin_is_listed_with_its_own_type(self):
561
+ """The left tree's class sits on ReportTreeAp.ReportPlugin, a different slot and base class.
562
+
563
+ Reading only reportPlugins left it outside the card gate, and a gate message saying
564
+ reportlist would have sent its delivery to the wrong base class.
565
+ """
566
+ contract = report_ops.compile_report_plugin_contract({
567
+ "reportColumns": [_column("amt")],
568
+ "reportPlugins": [{"className": "kd.kdtest.report.CostReportPlugin", "type": "Java"}],
569
+ "reportProperties": {"tree": {"ReportPlugin": "kd.kdtest.report.CostTreePlugin"}},
570
+ }, "kdtest_rpt")
571
+ self.assertEqual(contract["pluginClasses"],
572
+ ["kd.kdtest.report.CostReportPlugin", "kd.kdtest.report.CostTreePlugin"])
573
+ self.assertEqual(contract["pluginTypes"], {
574
+ "kd.kdtest.report.CostReportPlugin": "reportlist",
575
+ "kd.kdtest.report.CostTreePlugin": "reporttreelist",
576
+ })
577
+
578
+ def test_an_unparseable_properties_section_registers_no_tree_class(self):
579
+ types = report_ops.registered_report_plugin_types({
580
+ "reportPlugins": [{"className": "kd.kdtest.report.P"}],
581
+ "reportProperties": ["not", "an", "object"],
582
+ })
583
+ self.assertEqual(types, {"kd.kdtest.report.P": "reportlist"})
584
+
400
585
 
401
586
  class WriteReportPluginContractTest(unittest.TestCase):
402
587
  """Persisting is how Java delivery picks the contract up."""
403
588
 
404
589
  def test_contract_is_written_next_to_the_run_result(self):
405
- contract = _compile(columns=[
590
+ contract = _compile_requirements(columns=[
406
591
  _column("amt", presentation={"kind": "amount", "currencyField": "cur"})])
407
592
  with tempfile.TemporaryDirectory() as tmp:
408
593
  path = report_ops.write_report_plugin_contract(tmp, "kdtest_rpt", contract)
@@ -416,5 +601,297 @@ class WriteReportPluginContractTest(unittest.TestCase):
416
601
  self.assertFalse(os.path.isdir(os.path.join(tmp, "_runtime")))
417
602
 
418
603
 
604
+ class IncrementalReportAuthoringTest(unittest.TestCase):
605
+ def baseline(self, root):
606
+ artifact = {'reportColumns': [_column('amt', presentation={'kind': 'decimal', 'scale': 3})],
607
+ 'reportFilters': [{'fieldPath': 'flt_period', 'displayName': 'Period', 'compare': '=', 'mustInput': True},
608
+ {'fieldPath': 'flt_person', 'displayName': 'Person', 'compare': '='}],
609
+ 'reportPlugins': [{'className': 'kd.sample.report.Query', 'type': 'Java'}]}
610
+ schema = {'readBackAvailable': True, 'readBackSource': 'canonical',
611
+ 'reportColumns': artifact['reportColumns'], 'reportFilters': report_ops._build_filters_payload(artifact['reportFilters'])}
612
+ report_ops.write_report_authoring_state(root, 'sample_rpt', 'FORM', artifact, schema)
613
+ return artifact, schema
614
+
615
+ def test_append_keeps_old_presentation_and_adds_basedata_contract(self):
616
+ with tempfile.TemporaryDirectory() as root:
617
+ artifact, before = self.baseline(root)
618
+ person = _column('person_id', 'bigint', {'kind': 'basedata', 'entityId': 'bos_user', 'displayProp': 'name'})
619
+ after = {**before, 'reportColumns': [*artifact['reportColumns'], person]}
620
+ # 已有同键列被平台跳过,请求的 scale=8 不能覆盖原有 scale=3。
621
+ requested = [_column('amt', presentation={'kind': 'decimal', 'scale': 8}), person]
622
+ result = report_ops.sync_report_authoring_state(root, 'sample_rpt', 'FORM', 'columnsAppend',
623
+ {'before': before['reportColumns'], 'requested': requested}, before, after, {'verified': True})
624
+ with open(result['contractPath'], encoding='utf-8') as handle:
625
+ contract = json.load(handle)
626
+ self.assertEqual(contract['dataSetColumns'], ['amt', 'person_id'])
627
+ self.assertEqual(contract['columnPresentation'][0]['scale'], 3)
628
+ self.assertEqual(contract['columnPresentation'][1]['entityId'], 'bos_user')
629
+ again = report_ops.sync_report_authoring_state(root, 'sample_rpt', 'FORM', 'columnsAppend',
630
+ {'before': after['reportColumns'], 'requested': requested}, after, after, {'verified': True})
631
+ with open(again['artifactPath'], encoding='utf-8') as handle:
632
+ self.assertEqual(len(json.load(handle)['reportColumns']), 2)
633
+
634
+ def test_filter_replacement_removes_withdrawn_requirement_and_preserves_columns(self):
635
+ with tempfile.TemporaryDirectory() as root:
636
+ artifact, before = self.baseline(root)
637
+ filters = artifact['reportFilters'][:1]
638
+ after = {**before, 'reportFilters': filters}
639
+ result = report_ops.sync_report_authoring_state(root, 'sample_rpt', 'FORM', 'filters',
640
+ filters, before, after, {'verified': True})
641
+ with open(result['contractPath'], encoding='utf-8') as handle:
642
+ contract = json.load(handle)
643
+ self.assertEqual([row['filterKey'] for row in contract['filterValueContracts']], ['flt_period'])
644
+ self.assertEqual(contract['dataSetColumns'], ['amt'])
645
+ self.assertEqual(contract['columnPresentation'][0]['scale'], 3)
646
+
647
+ def test_missing_baseline_readback_failure_and_designer_drift_fail_closed(self):
648
+ for mode in ('missing', 'unverified', 'drift', 'identity'):
649
+ with self.subTest(mode=mode), tempfile.TemporaryDirectory() as root:
650
+ artifact, before = self.baseline(root) if mode != 'missing' else ({}, {})
651
+ if mode == 'drift':
652
+ before = {**before, 'reportColumns': [_column('unknown')]}
653
+ result = report_ops.sync_report_authoring_state(root, 'sample_rpt', 'OTHER' if mode == 'identity' else 'FORM',
654
+ 'filters', [], before, {**before, 'reportFilters': []}, {'verified': mode != 'unverified'})
655
+ self.assertEqual(result['status'], 'incomplete')
656
+ self.assertTrue(result['platformMutationApplied'])
657
+ with open(result['path'], encoding='utf-8') as handle:
658
+ self.assertEqual(json.load(handle)['status'], 'incomplete')
659
+
660
+ def test_write_receipt_survives_failed_readback_without_replaying_mutation(self):
661
+ import cosmic_api
662
+ from unittest import mock
663
+ with tempfile.TemporaryDirectory() as root:
664
+ artifact, before = self.baseline(root)
665
+ with mock.patch.object(cosmic_api, 'do_get_report_schema', side_effect=OSError('gateway unavailable')) as read:
666
+ result = cosmic_api._attach_report_readback({}, 'fixture', 'FORM', 'filters',
667
+ artifact['reportFilters'][:1], {'formNumber': 'sample_rpt', 'formId': 'FORM'},
668
+ app_root=root, schema_before=before)
669
+ read.assert_called_once()
670
+ self.assertTrue(result['status'])
671
+ self.assertTrue(result['data']['platformMutationApplied'])
672
+ self.assertFalse(result['verification']['verified'])
673
+ self.assertEqual(result['data']['reportAuthoringState']['status'], 'incomplete')
674
+
675
+ def test_cli_readback_refreshes_contract_using_real_verifier(self):
676
+ import cosmic_api
677
+ from unittest import mock
678
+ with tempfile.TemporaryDirectory() as root:
679
+ artifact, before = self.baseline(root)
680
+ filters = artifact['reportFilters'][:1]
681
+ after = {**before, 'reportFilters': report_ops._build_filters_payload(filters)}
682
+ with mock.patch.object(cosmic_api, 'do_get_report_schema', return_value=after):
683
+ result = cosmic_api._attach_report_readback({}, 'fixture', 'FORM', 'filters', filters,
684
+ {'formNumber': 'sample_rpt', 'formId': 'FORM'}, app_root=root, schema_before=before)
685
+ self.assertTrue(result['verification']['verified'])
686
+ self.assertEqual(result['data']['reportAuthoringState']['status'], 'current')
687
+
688
+ def test_scoped_serve_mutation_invalidates_old_contract_but_dry_run_does_not(self):
689
+ import cosmic_api
690
+ from unittest import mock
691
+ with tempfile.TemporaryDirectory() as root:
692
+ artifact, before = self.baseline(root)
693
+ path = report_ops.report_authoring_path(root, 'sample_rpt')
694
+ args = {'appRoot': root, 'formNumber': 'sample_rpt', 'plugin': {'className': 'kd.sample.report.New'}}
695
+ with mock.patch.object(cosmic_api, 'do_register_report_plugin', return_value='kd.sample.report.New'):
696
+ cosmic_api._dispatch_serve({}, 'fixture', 'registerReportPlugin', {**args, 'dryRun': True})
697
+ self.assertEqual(json.loads(path.read_text())['status'], 'current')
698
+ cosmic_api._dispatch_serve({}, 'fixture', 'registerReportPlugin', args)
699
+ self.assertEqual(json.loads(path.read_text())['status'], 'incomplete')
700
+
701
+ def test_property_writes_invalidate_only_successful_unmerged_baseline(self):
702
+ import cosmic_api
703
+ with tempfile.TemporaryDirectory() as root:
704
+ artifact, before = self.baseline(root)
705
+ path = report_ops.report_authoring_path(root, 'sample_rpt')
706
+ cosmic_api._invalidate_unmerged_report_state({'status': False}, 'setReportFilterFieldConfig', root, 'sample_rpt')
707
+ self.assertEqual(json.loads(path.read_text())['status'], 'current')
708
+ result = cosmic_api._invalidate_unmerged_report_state({'status': True, 'data': {}},
709
+ 'setReportFilterFieldConfig', root, 'sample_rpt')
710
+ self.assertEqual(result['data']['reportAuthoringState']['status'], 'incomplete')
711
+ self.assertEqual(json.loads(path.read_text())['status'], 'incomplete')
712
+
713
+ def test_scoped_failed_writes_distinguish_uncertain_from_applied(self):
714
+ import cosmic_api
715
+ from unittest import mock
716
+ for mode in ('rejected', 'transport', 'readback', 'mismatch'):
717
+ with self.subTest(mode=mode), tempfile.TemporaryDirectory() as root:
718
+ self.baseline(root)
719
+ response = {'status': False, 'message': 'save rejected'} if mode == 'rejected' else {'status': True}
720
+ observed = {'ReportFilterFieldConfig': {'reportFilterFields': [{'key': 'other'}]}}
721
+ with mock.patch.object(report_ops, '_api_call', return_value=response,
722
+ side_effect=TimeoutError('gateway') if mode == 'transport' else None) as write, \
723
+ mock.patch.object(report_ops, 'read_report_element_properties', return_value=observed,
724
+ side_effect=OSError('gateway') if mode == 'readback' else None) as read:
725
+ result = cosmic_api._dispatch_serve({}, 'fixture', 'setReportFilterFieldConfig',
726
+ {'appRoot': root, 'formNumber': 'sample_rpt', 'fields': [{'key': 'flt_period'}]})
727
+ write.assert_called_once()
728
+ self.assertFalse(result['status'])
729
+ self.assertFalse(cosmic_api._serve_result_success(result))
730
+ state = json.loads(report_ops.report_authoring_path(root, 'sample_rpt').read_text())
731
+ self.assertEqual(state['status'], 'incomplete')
732
+ evidence = 'platformMutationApplied' if mode in ('readback', 'mismatch') else 'platformMutationPossible'
733
+ absent = 'platformMutationPossible' if evidence == 'platformMutationApplied' else 'platformMutationApplied'
734
+ self.assertTrue(state[evidence])
735
+ self.assertNotIn(absent, state)
736
+ self.assertTrue(result['data'][evidence])
737
+ self.assertFalse(result['data']['runtimeVerified'])
738
+ self.assertEqual(read.call_count, int(mode in ('readback', 'mismatch')))
739
+
740
+ def test_preflight_dry_run_and_unscoped_failure_do_not_invalidate_other_baselines(self):
741
+ import cosmic_api
742
+ from unittest import mock
743
+ with tempfile.TemporaryDirectory() as root:
744
+ self.baseline(root)
745
+ args = {'appRoot': root, 'formNumber': 'sample_rpt', 'fields': [{'key': 'flt_period'}]}
746
+ path = report_ops.report_authoring_path(root, 'sample_rpt')
747
+ with mock.patch.object(report_ops, '_api_call', side_effect=TimeoutError('gateway')) as write:
748
+ with self.assertRaises(ValueError):
749
+ cosmic_api._dispatch_serve({}, 'fixture', 'setReportFilterFieldConfig', {**args, 'fields': []})
750
+ cosmic_api._dispatch_serve({}, 'fixture', 'setReportFilterFieldConfig', {**args, 'dryRun': True})
751
+ write.assert_not_called()
752
+ self.assertEqual(json.loads(path.read_text())['status'], 'current')
753
+ unscoped = {key: value for key, value in args.items() if key != 'appRoot'}
754
+ result = cosmic_api._dispatch_serve({}, 'fixture', 'setReportFilterFieldConfig', unscoped)
755
+ write.assert_called_once()
756
+ self.assertFalse(result['status'])
757
+ self.assertNotIn('reportAuthoringState', result['data'])
758
+ self.assertEqual(json.loads(path.read_text())['status'], 'current')
759
+
760
+ def test_cli_failure_keeps_report_state_evidence_and_restores_stdout(self):
761
+ import builtins
762
+ import cosmic_api
763
+ import io
764
+ from contextlib import redirect_stdout, redirect_stderr
765
+ from types import SimpleNamespace
766
+ from unittest import mock
767
+ for applied in (None, True):
768
+ with self.subTest(applied=applied), tempfile.TemporaryDirectory() as root:
769
+ self.baseline(root)
770
+ original_print = builtins.print
771
+ output, errors = io.StringIO(), io.StringIO()
772
+ def execute():
773
+ cosmic_api._CLI_DELIVERY_ARGS = SimpleNamespace(
774
+ cmd='setReportFilterFieldConfig', app_root=root, formNumber='sample_rpt', dry_run=False)
775
+ raise report_ops.ReportMutationError('REPORT_WRITE_OUTCOME_UNKNOWN: fixture', applied)
776
+ with mock.patch.object(cosmic_api, '_main_impl', side_effect=execute), \
777
+ mock.patch.object(sys, 'argv', ['cosmic_api.py', '--stdout']), \
778
+ redirect_stdout(output), redirect_stderr(errors):
779
+ self.assertEqual(cosmic_api.main(), 1)
780
+ self.assertIs(builtins.print, original_print)
781
+ print('CARD_STDOUT_RESTORED')
782
+ lines = output.getvalue().splitlines()
783
+ result = json.loads('\n'.join(lines[:-1]))
784
+ self.assertEqual(lines[-1], 'CARD_STDOUT_RESTORED')
785
+ self.assertFalse(result['status'])
786
+ self.assertEqual(result['data']['reportAuthoringState']['status'], 'incomplete')
787
+ self.assertTrue(result['data']['platformMutationApplied' if applied else 'platformMutationPossible'])
788
+
789
+ def test_cli_success_restores_print_and_output_context_for_following_cards(self):
790
+ import builtins
791
+ import cosmic_api
792
+ import io
793
+ from _core import formatters
794
+ from contextlib import redirect_stdout, redirect_stderr
795
+ from unittest import mock
796
+ original_print = builtins.print
797
+ previous = (formatters._stdout_mode, formatters._output_file,
798
+ formatters._current_cmd, formatters._current_arg_hash)
799
+ output, errors = io.StringIO(), io.StringIO()
800
+ with mock.patch.object(sys, 'argv', ['cosmic_api.py', '--stdout', 'describeCommand', 'setReportFilters']), \
801
+ redirect_stdout(output), redirect_stderr(errors):
802
+ self.assertEqual(cosmic_api.main(), 0)
803
+ self.assertIs(builtins.print, original_print)
804
+ self.assertEqual((formatters._stdout_mode, formatters._output_file,
805
+ formatters._current_cmd, formatters._current_arg_hash), previous)
806
+ print('CARD_STDOUT_RESTORED')
807
+ lines = output.getvalue().splitlines()
808
+ self.assertEqual(lines[-1], 'CARD_STDOUT_RESTORED')
809
+ self.assertEqual(json.loads('\n'.join(lines[:-1]))['action']['command'], 'setReportFilters')
810
+
811
+ def test_internal_executor_does_not_install_a_process_wide_print_router(self):
812
+ import builtins
813
+ import cosmic_api
814
+ from _core import formatters
815
+ from unittest import mock
816
+ original_print = builtins.print
817
+ with mock.patch.object(sys, 'argv', ['cosmic_api.py', '--stdout', 'auth']), \
818
+ mock.patch.object(builtins, 'print', original_print), \
819
+ mock.patch.object(formatters, '_stdout_mode', False), \
820
+ mock.patch.object(cosmic_api, '_CLI_MACHINE_MODE', False), \
821
+ mock.patch.object(cosmic_api, '_CLI_DELIVERY_ARGS', None), \
822
+ mock.patch.object(cosmic_api, '_resolve_app_root_arg', side_effect=RuntimeError('fixture-stop')):
823
+ with self.assertRaisesRegex(RuntimeError, 'fixture-stop'):
824
+ cosmic_api._main_impl()
825
+ self.assertIs(builtins.print, original_print)
826
+
827
+ def test_unexpected_write_response_and_rejected_save_are_not_preflight_failures(self):
828
+ for response in (None, [], {'status': False}, {'errorCode': 'SAVE_REJECTED'}, {'data': {'success': False}}):
829
+ with self.subTest(response=response):
830
+ with self.assertRaises(report_ops.ReportMutationError) as failure:
831
+ report_ops._check_result(response, 'setReportFilters', form_mutation_possible=True)
832
+ self.assertIsNone(failure.exception.mutation_applied)
833
+ self.assertIn('do not repeat the same write blindly', str(failure.exception))
834
+ with self.assertRaises(RuntimeError) as failure:
835
+ report_ops._check_result({'status': False}, 'getFormSchema')
836
+ self.assertNotIsInstance(failure.exception, report_ops.ReportMutationError)
837
+
838
+ def test_malformed_readback_preserves_successful_write_and_invalidates_state(self):
839
+ import cosmic_api
840
+ from unittest import mock
841
+ for schema in (None, [], {'readBackAvailable': True, 'reportFilters': [None]}):
842
+ with self.subTest(schema=schema), tempfile.TemporaryDirectory() as root:
843
+ artifact, before = self.baseline(root)
844
+ with mock.patch.object(cosmic_api, 'do_get_report_schema', return_value=schema):
845
+ result = cosmic_api._attach_report_readback({}, 'fixture', 'FORM', 'filters',
846
+ artifact['reportFilters'][:1], {'formNumber': 'sample_rpt', 'formId': 'FORM'},
847
+ app_root=root, schema_before=before)
848
+ self.assertTrue(result['data']['platformMutationApplied'])
849
+ self.assertFalse(result['verification']['verified'])
850
+ self.assertEqual(json.loads(report_ops.report_authoring_path(root, 'sample_rpt').read_text())['status'], 'incomplete')
851
+
852
+ def test_design_state_mismatch_preserves_successful_property_write(self):
853
+ import cosmic_api
854
+ from unittest import mock
855
+ result = {'designStateExpectation': {'hostType': 'ReportListAp', 'expected': {'MaxRowHeight': 32}}}
856
+ with mock.patch.object(cosmic_api, '_export_report_dym_evidence', return_value='fixture.dym'), \
857
+ mock.patch.object(cosmic_api, 'attach_report_property_design_state',
858
+ side_effect=RuntimeError('REPORT_PROPERTY_MISMATCH: fixture')):
859
+ with self.assertRaises(report_ops.ReportMutationError) as failure:
860
+ cosmic_api._settle_report_property_design_state({}, 'fixture', result, 'sample_rpt', 'app', 'setReportProperties')
861
+ self.assertTrue(failure.exception.mutation_applied)
862
+ self.assertTrue(cosmic_api._report_mutation_failure(failure.exception)['data']['platformMutationApplied'])
863
+
864
+ def test_atomic_local_save_failure_cleans_only_its_temporary_files(self):
865
+ from pathlib import Path
866
+ from unittest import mock
867
+ with tempfile.TemporaryDirectory() as root:
868
+ artifact, schema = self.baseline(root)
869
+ runtime = Path(root) / '_runtime'
870
+ unrelated = runtime / 'unrelated.tmp'
871
+ unrelated.write_text('keep')
872
+ with mock.patch.object(report_ops.os, 'replace', side_effect=OSError('save failed')):
873
+ with self.assertRaises(OSError):
874
+ report_ops.write_report_authoring_state(root, 'sample_rpt', 'FORM', artifact, schema)
875
+ self.assertEqual(list(runtime.glob('*.tmp')), [unrelated])
876
+ self.assertEqual(json.loads(report_ops.report_authoring_path(root, 'sample_rpt').read_text())['status'], 'incomplete')
877
+
878
+ def test_contract_compile_failure_leaves_state_incomplete(self):
879
+ from unittest import mock
880
+ with tempfile.TemporaryDirectory() as root:
881
+ artifact, before = self.baseline(root)
882
+ with mock.patch.object(report_ops, 'compile_report_plugin_contract', side_effect=ValueError('invalid contract')):
883
+ with self.assertRaises(ValueError):
884
+ report_ops.write_report_authoring_state(root, 'sample_rpt', 'FORM', artifact, before)
885
+ self.assertEqual(json.loads(report_ops.report_authoring_path(root, 'sample_rpt').read_text())['status'], 'incomplete')
886
+
887
+ def test_query_data_source_only_does_not_create_a_java_contract(self):
888
+ with tempfile.TemporaryDirectory() as root:
889
+ artifact = {'queryDataSource': {'number': 'sample_data'}, 'reportColumns': [_column('amt')]}
890
+ result = report_ops.write_report_authoring_state(root, 'sample_rpt', 'FORM', artifact,
891
+ {'reportColumns': artifact['reportColumns']})
892
+ self.assertEqual(result['contractPath'], '')
893
+ self.assertFalse(os.path.isfile(os.path.join(root, '_runtime', 'sample_rpt.report-plugin-contract.json')))
894
+
895
+
419
896
  if __name__ == "__main__":
420
897
  unittest.main()