@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
@@ -17,7 +17,7 @@ def test_root_routes_delivery_to_the_session_boundary_reference() -> None:
17
17
  assert "Repeat `--form-number`" in delivery
18
18
  assert "after the final write" in delivery
19
19
  assert "historical forms" in delivery
20
- assert "Tool output alone does not deliver a card" in delivery
20
+ assert "Ordinary renderer stdout alone does not deliver a card" in delivery
21
21
  assert "include it verbatim in the final assistant response" in delivery
22
22
  assert "Remaining executable implementation" in root
23
23
 
@@ -6,6 +6,8 @@ from pathlib import Path
6
6
 
7
7
 
8
8
  ROOT = Path(__file__).resolve().parents[2]
9
+ if ROOT.name == "zh" and ROOT.parent.name == ".locales":
10
+ ROOT = ROOT.parent.parent
9
11
  SKILLS = (
10
12
  ROOT / "SKILL.md",
11
13
  ROOT / ".locales/zh/SKILL.md",
@@ -234,6 +234,12 @@ class IsTransientTest(unittest.TestCase):
234
234
  import _http_client as m
235
235
  self.assertTrue(m._is_transient(urllib.error.URLError("timed out")))
236
236
 
237
+ def test_http_gateway_errors_only_are_transient(self):
238
+ import urllib.error
239
+ for code in (400, 401, 403, 404, 500, 502, 503, 504):
240
+ self.assertEqual(_http_client._is_transient(urllib.error.HTTPError('https://example.test', code, '', {}, None)),
241
+ code in (502, 503, 504))
242
+
237
243
  def test_non_transient_not_matched(self):
238
244
  import _http_client as m
239
245
  self.assertFalse(m._is_transient(ValueError("bad input")))
@@ -0,0 +1,206 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+
8
+ import pytest
9
+
10
+ SCRIPTS = Path(__file__).resolve().parents[1]
11
+ sys.path.insert(0, str(SCRIPTS))
12
+ import compile_plugin_source as compiler
13
+ from plugin_target_sdk import TargetSdkError, verify_target_sdk
14
+ from run_java_tests import run_tests
15
+
16
+
17
+ def test_configuration_gaps_survive_sdk_and_compile_without_writes(tmp_path, monkeypatch):
18
+ config = tmp_path / 'appconfig.json'
19
+ config.write_text(json.dumps({'compile': True, 'autoregister': False, 'password': 'must-not-return'}))
20
+ monkeypatch.setenv('KD_APPCONFIG', str(config))
21
+ before = config.read_bytes()
22
+ facts = compiler.inspect_compile_environment(str(tmp_path))
23
+ assert set(facts['missing']) == {'GRADLE_ROOT', 'JAVA_HOME'}
24
+ assert facts['invalid'] == []
25
+ assert facts['configPath'] == str(config)
26
+ assert facts['compile'] is True and facts['autoregister'] is False
27
+ assert facts['configCommands']['status'][-1] == '--status'
28
+ assert 'must-not-return' not in json.dumps(facts)
29
+ with pytest.raises(TargetSdkError) as error:
30
+ verify_target_sdk(str(tmp_path), {})
31
+ assert error.value.details['buildEnvironment'] == facts
32
+ source = tmp_path / 'src/main/java/demo/Work.java'
33
+ source.parent.mkdir(parents=True)
34
+ source.write_text('package demo; public class Work {}')
35
+ with pytest.raises(compiler.CompileError) as error:
36
+ compiler.compile_source(tmp_path, source, 'Work', 5, app_root=tmp_path / 'app',
37
+ plugin_type='task', form_number='example', operation_keys=[], description='work')
38
+ assert error.value.details['buildEnvironment'] == facts
39
+ result = run_tests(str(tmp_path))
40
+ assert result['outcome'] == 'environment-unavailable'
41
+ assert result['buildEnvironment'] == facts
42
+ assert config.read_bytes() == before
43
+ assert not (tmp_path / '.temp').exists()
44
+
45
+
46
+ def test_missing_decisions_and_invalid_paths_remain_distinct(tmp_path, monkeypatch):
47
+ config = tmp_path / 'appconfig.json'
48
+ monkeypatch.setenv('KD_APPCONFIG', str(config))
49
+ result = compiler.inspect_compile_environment(str(tmp_path))
50
+ assert result['status'] == 'missing'
51
+ assert result['missing'] == ['compile']
52
+ config.write_text(json.dumps({'compile': True, 'GRADLE_ROOT': str(tmp_path / 'missing'), 'JAVA_HOME': str(tmp_path / 'jdk')}))
53
+ result = compiler.inspect_compile_environment(str(tmp_path))
54
+ assert set(result['invalid']) == {'GRADLE_ROOT', 'JAVA_HOME'}
55
+ assert result['missing'] == []
56
+ config.write_text('{"compile":false}')
57
+ result = run_tests(str(tmp_path))
58
+ assert result['status'] is False
59
+ assert result['buildEnvironment']['status'] == 'disabled'
60
+ assert not (tmp_path / '.temp').exists()
61
+
62
+
63
+ @pytest.mark.parametrize('required,found,reason', [
64
+ ('required: String,int', 'found: String', 'reason: actual and formal argument lists differ in length'),
65
+ ('需要: String,int', '找到: String', '原因: 实际参数列表和形式参数列表长度不同'),
66
+ ])
67
+ def test_argument_diagnostics_keep_source_overload_and_reason(required, found, reason):
68
+ result = compiler.parse_compiler_errors('\n'.join([
69
+ '/project/src/Caller.java:7: error: method apply cannot be applied to given types;',
70
+ ' Helper.apply("x");', ' ^', required, found, reason,
71
+ ' constructor Helper.Helper(String) is not applicable', '1 error', 'BUILD FAILED']))
72
+ assert len(result) == 1
73
+ for detail in ['Helper.apply("x");', '^', required, found, reason,
74
+ 'constructor Helper.Helper(String) is not applicable']:
75
+ assert detail in result[0]['detail']
76
+ assert 'BUILD FAILED' not in result[0]['detail']
77
+ assert '1 error' not in result[0]['detail']
78
+
79
+
80
+ def test_diagnostic_bounds_keep_later_helper_file_and_exact_count():
81
+ text = '\n'.join([f'/project/First.java:{i}: error: first {i}' for i in range(1, 41)] +
82
+ ['/project/Helper.java:8: error: missing helper'])
83
+ mine, others, dropped = compiler.collect_compiler_errors(text)
84
+ assert mine == []
85
+ assert len(others) == compiler.DIAGNOSTIC_LIMIT
86
+ assert len(others) + dropped == 41
87
+ assert any(item['file'].endswith('Helper.java') for item in others)
88
+ result = compiler.parse_compiler_errors('/project/Call.java:2: error: overload\n' + '\n'.join(f'candidate {i}' for i in range(80)))
89
+ assert len(result[0]['detail']) == 40
90
+ assert result[0]['detailTruncated'] is True
91
+
92
+
93
+ @pytest.fixture
94
+ def java_project(tmp_path, monkeypatch):
95
+ gradle = os.environ.get('APP_BUILD_TEST_GRADLE')
96
+ home = os.environ.get('JAVA_HOME')
97
+ if not gradle or not home:
98
+ pytest.skip('Set APP_BUILD_TEST_GRADLE and JAVA_HOME to run the real Gradle integration')
99
+ root = tmp_path / 'real gradle project'
100
+ module = root / 'code' / 'app'
101
+ library = root / 'code' / 'library'
102
+ for directory in [module, library]:
103
+ (directory / 'src/main/java/sample').mkdir(parents=True)
104
+ (module / 'src/test/java/sample').mkdir(parents=True)
105
+ wrapper = root / ('gradlew.bat' if os.name == 'nt' else 'gradlew')
106
+ shutil.copy2(gradle, wrapper)
107
+ # The installed Gradle launcher resolves its libraries relative to its own path.
108
+ if os.name == 'nt':
109
+ wrapper.write_text('@echo off\r\ncall "' + gradle + '" %*\r\n')
110
+ else:
111
+ wrapper.write_text('#!/bin/sh\nexec "' + gradle.replace('"', '\\"') + '" "$@"\n')
112
+ wrapper.chmod(0o755)
113
+ (root / 'settings.gradle').write_text("include(':app', ':library')\nproject(':app').projectDir=new File('code/app')\nproject(':library').projectDir=new File('code/library')\n")
114
+ (root / 'gradle.properties').write_text('org.gradle.daemon=false\n')
115
+ (root / 'build.gradle').write_text("allprojects { apply plugin: 'java' }\n")
116
+ junit = Path(gradle).resolve().parents[1] / 'lib' / 'junit-4.13.2.jar'
117
+ hamcrest = next((Path(gradle).resolve().parents[1] / 'lib').glob('hamcrest-core-*.jar'))
118
+ assert junit.is_file()
119
+ (module / 'build.gradle').write_text("dependencies { implementation project(':library'); testImplementation files(" +
120
+ json.dumps(str(junit)) + ', ' + json.dumps(str(hamcrest)) + ') }\n')
121
+ (library / 'src/main/java/sample/Shared.java').write_text('package sample; public class Shared { public static int value() { return 7; } }')
122
+ (module / 'src/main/java/sample/Caller.java').write_text('package sample; public class Caller { public int value() { return Shared.value(); } }')
123
+ test = module / 'src/test/java/sample/CallerTest.java'
124
+ test.write_text('package sample; import org.junit.Test; import static org.junit.Assert.*; public class CallerTest { @Test public void actualProductionMethod() { assertEquals(7, new Caller().value()); } }')
125
+ config = tmp_path / 'appconfig.json'
126
+ config.write_text(json.dumps({'compile': True, 'autoregister': False, 'GRADLE_ROOT': str(root), 'JAVA_HOME': home}))
127
+ monkeypatch.setenv('KD_APPCONFIG', str(config))
128
+ return root, module, library, test
129
+
130
+
131
+ def test_real_multi_module_tests_report_pass_failure_and_keep_assertions(java_project):
132
+ root, module, library, test = java_project
133
+ original = test.read_bytes()
134
+ passed = run_tests(str(module), rerun=True)
135
+ assert passed['status'] is True, passed
136
+ assert passed['counts'] == {'total': 1, 'passed': 1, 'failed': 0, 'skipped': 0}
137
+ assert passed['task'] == ':app:test'
138
+ source = library / 'src/main/java/sample/Shared.java'
139
+ source.write_text(source.read_text().replace('return 7', 'return 8'))
140
+ failed = run_tests(str(module), rerun=True)
141
+ assert failed['status'] is False and failed['outcome'] == 'test-failed', failed
142
+ assert failed['counts']['failed'] == 1
143
+ assert failed['failures'][0]['className'] == 'sample.CallerTest'
144
+ assert failed['failures'][0]['name'] == 'actualProductionMethod'
145
+ assert 'expected:<7> but was:<8>' in json.dumps(failed['failures'])
146
+ assert test.read_bytes() == original
147
+ assert not (module / 'archive.json').exists()
148
+ assert not list((module / '.temp').glob('*-gate'))
149
+
150
+
151
+ def test_real_no_tests_skipped_missing_task_and_build_failure_are_not_pass(java_project):
152
+ root, module, library, test = java_project
153
+ assert run_tests(str(library), rerun=True)['outcome'] == 'no-tests'
154
+ build = module / 'build.gradle'
155
+ original = build.read_text()
156
+ build.write_text(original + '\ntest.enabled=false\n')
157
+ assert run_tests(str(module), rerun=True)['outcome'] == 'skipped'
158
+ root_build = root / 'build.gradle'
159
+ root_original = root_build.read_text()
160
+ root_build.write_text("project(':library') { apply plugin: 'java' }\n")
161
+ build.write_text('')
162
+ assert run_tests(str(module))['outcome'] == 'task-unavailable'
163
+ root_build.write_text(root_original)
164
+ build.write_text(original)
165
+ source = library / 'src/main/java/sample/Shared.java'
166
+ source.write_text(source.read_text().replace('return 7', 'return absent'))
167
+ result = run_tests(str(module), rerun=True)
168
+ assert result['status'] is False and result['outcome'] == 'build-failed'
169
+ assert result['counts']['total'] == 0
170
+ assert any('Shared.java' in item['file'] for item in result['diagnostics'])
171
+
172
+
173
+ def test_real_unmatched_filter_timeout_and_up_to_date_do_not_claim_pass(java_project):
174
+ root, module, library, test = java_project
175
+ assert run_tests(str(module), rerun=True)['status'] is True
176
+ assert run_tests(str(module))['outcome'] == 'up-to-date'
177
+ assert run_tests(str(module), tests=['sample.Absent'], rerun=True)['status'] is False
178
+ build = module / 'build.gradle'
179
+ build.write_text(build.read_text() + '\ntest.doFirst { Thread.sleep(5000) }\n')
180
+ result = run_tests(str(module), timeout_sec=1, rerun=True)
181
+ assert result['status'] is False and result['outcome'] == 'timed-out'
182
+
183
+
184
+ def test_config_diagnostics_reach_native_single_and_batch_cli(tmp_path, monkeypatch):
185
+ config = tmp_path / 'appconfig.json'
186
+ config.write_text('{"compile":true}')
187
+ monkeypatch.setenv('KD_APPCONFIG', str(config))
188
+ entries = []
189
+ for name in ['First', 'Second']:
190
+ source = tmp_path / 'src/main/java/demo' / (name + '.java')
191
+ source.parent.mkdir(parents=True, exist_ok=True)
192
+ source.write_text('package demo; public class ' + name + ' {}')
193
+ entries.append({'sourcePath': str(source), 'className': name, 'type': 'task',
194
+ 'formNumber': 'example', 'operationKeys': [], 'description': 'test'})
195
+ batch = tmp_path / 'sources.json'
196
+ batch.write_text(json.dumps(entries))
197
+ common = [sys.executable, str(SCRIPTS / 'compile_plugin_source.py'), '--project-root', str(tmp_path), '--app-root', str(tmp_path / 'app')]
198
+ runs = [subprocess.run([*common, '--sources-file', str(batch)], capture_output=True, text=True),
199
+ subprocess.run([*common, '--source-path', entries[0]['sourcePath'], '--class-name', 'First', '--plugin-type', 'task', '--form-number', 'example', '--description', 'test'], capture_output=True, text=True)]
200
+ assert all(run.returncode == 2 for run in runs)
201
+ batch_result, single_result = [json.loads(run.stdout) for run in runs]
202
+ for result in [*batch_result['results'], single_result]:
203
+ assert result['errorCode'] == 'PLUGIN_COMPILE_CONFIG_UNAVAILABLE'
204
+ assert set(result['buildEnvironment']['missing']) == {'GRADLE_ROOT', 'JAVA_HOME'}
205
+ assert result['buildEnvironment']['configPath'] == str(config)
206
+ assert config.read_text() == '{"compile":true}'
@@ -279,6 +279,27 @@ if __name__ == "__main__":
279
279
 
280
280
 
281
281
  class WriteTransportBoundaryTest(unittest.TestCase):
282
+ def test_gateway_get_retries_are_bounded_and_writes_never_replay(self):
283
+ # 保留真实 facade 和 retry_call,仅替换网络传输边界。
284
+ for code in (502, 503, 504):
285
+ with mock.patch.object(_http_client, 'http_get_json', side_effect=[_http_error(code), {'status': True}]) as transport, \
286
+ mock.patch.object(_http_client.time, 'sleep'):
287
+ result = kd_api._request_once('https://example.test/read', {}, 'fixture',
288
+ method='GET', body=None, params=None, style='kapi')
289
+ self.assertTrue(result['status'])
290
+ self.assertEqual(transport.call_count, 2)
291
+ with mock.patch.object(_http_client, 'http_get_json', side_effect=_http_error(code)) as transport, \
292
+ mock.patch.object(_http_client.time, 'sleep'):
293
+ with self.assertRaises(urllib.error.HTTPError):
294
+ kd_api._request_once('https://example.test/read', {}, 'fixture',
295
+ method='GET', body=None, params=None, style='kapi')
296
+ self.assertEqual(transport.call_count, 3)
297
+ with mock.patch.object(_http_client, 'http_post_json', side_effect=_http_error(code)) as transport:
298
+ with self.assertRaises(urllib.error.HTTPError):
299
+ kd_api._request_once('https://example.test/write', {}, 'fixture',
300
+ method='POST', body={}, params=None, style='kapi')
301
+ self.assertEqual(transport.call_count, 1)
302
+
282
303
  def test_lost_post_response_is_not_replayed_but_get_can_retry(self):
283
304
  import http.client
284
305
  import socket
@@ -661,3 +661,187 @@ def test_empty_list_mutation_cannot_claim_verified_noop_or_discover_identity(con
661
661
  assert result["errorCode"] == "LIST_CONFIGURATION_INPUT_REQUIRED"
662
662
  assert result["mutation"]["outcome"] == "failed_preflight"
663
663
  resolve.assert_not_called()
664
+
665
+
666
+ @pytest.fixture
667
+ def column_properties(monkeypatch):
668
+ """Stateful boundary; actual list compiler and real captured declarations are exercised."""
669
+ import copy
670
+ declarations = json.loads((Path(__file__).parent / "fixtures/list_column_property_declarations.json").read_text())
671
+ state = {"values": {}, "writes": [], "adds": [], "existing": True, "family": "integer", "fieldType": "IntegerField", "dropWrite": False}
672
+ def schema(*args):
673
+ return _schema([{"key": "typed_column", "listFieldId": "value"}] if state["existing"] else [])
674
+ def add(*args, **kwargs):
675
+ state["adds"].extend(kwargs["ops"])
676
+ state["existing"] = True
677
+ return {"status": True, "errorCode": "0"}
678
+ def set_values(_cfg, _token, _form, key, values):
679
+ state["writes"].append((key, copy.deepcopy(values)))
680
+ if not state["dropWrite"]:
681
+ state["values"].update(values)
682
+ return {"status": True, "errorCode": "0"}
683
+ monkeypatch.setattr(LISTUI, "_resolve_form_id", lambda *args: "form-id")
684
+ monkeypatch.setattr(LISTUI, "do_list_elements", lambda *args, **kwargs: {"status": True, "data": [{"key": "value", "name": "测试字段", "type": state["fieldType"]}]})
685
+ monkeypatch.setattr(LISTUI, "do_get_form_schema", schema)
686
+ monkeypatch.setattr(LISTUI, "do_load_full_metadata", lambda *args: _metadata())
687
+ monkeypatch.setattr(LISTUI, "do_get_entity_type", lambda *args: _entity_type("value"))
688
+ monkeypatch.setattr(LISTUI, "do_get_property_schema", lambda *args: {"status": True, "data": copy.deepcopy(declarations[state["family"]])})
689
+ monkeypatch.setattr(LISTUI, "do_get_properties", lambda *args: {"status": True, "data": copy.deepcopy(state["values"])})
690
+ monkeypatch.setattr(LISTUI, "do_set_properties", set_values)
691
+ monkeypatch.setattr(METAOPS, "do_modify_meta", add)
692
+ return state
693
+
694
+
695
+ @pytest.mark.parametrize("field_type,family,properties,expected", [
696
+ *[(kind, "integer", {"汇总方式": "合计", "为零显示": True}, {"Summary": 1, "ZeroShow": True}) for kind in
697
+ ["IntegerField", "BigIntField", "DecimalField", "AmountField", "QtyField", "PriceField"]],
698
+ *[(kind, "date", {"格式化": "yyyy-MM-dd", "Mask": "yyyy-MM-dd"}, {"DisplayFormatString": "yyyy-MM-dd", "Mask": "yyyy-MM-dd"}) for kind in
699
+ ["DateField", "DateTimeField", "CreateDateField", "ModifyDateField"]],
700
+ *[(kind, "combo", {"ShowStyle": 0, "Editable": True}, {"ShowStyle": 0, "Editable": True}) for kind in
701
+ ["ComboField", "MulComboField", "BillStatusField"]],
702
+ *[(kind, "text", {"宽度": "160px", "文本对齐方式": "居中"}, {"Width": "160px", "TextAlign": "center"}) for kind in
703
+ ["TextField", "TextAreaField", "BasedataField", "UserField", "CheckBoxField", "TimeField", "MuliLangTextField"]],
704
+ ])
705
+ @pytest.mark.parametrize("existing", [True, False])
706
+ def test_typed_column_properties_round_trip_for_existing_and_new_columns(column_properties, field_type, family, properties, expected, existing):
707
+ state = column_properties
708
+ state.update({"fieldType": field_type, "family": family, "existing": existing})
709
+ result = LISTUI.do_configure_list({}, "token", "demo", {"columns": [{"fieldKey": "value", "properties": properties}]})
710
+ assert result["status"] is True, result
711
+ assert state["values"] == expected
712
+ assert state["writes"][0][0] == "typed_column" # use the persisted key returned after creation
713
+ assert len(state["adds"]) == (not existing)
714
+ assert all(row["matched"] for row in result["verification"]["columns"][0]["properties"].values())
715
+ state["writes"].clear()
716
+ second = LISTUI.do_configure_list({}, "token", "demo", {"columns": [{"fieldKey": "value", "properties": properties}]})
717
+ assert second["status"] is True and state["writes"] == []
718
+
719
+
720
+ @pytest.mark.parametrize("properties", [
721
+ {"Summary": "unknown"}, {"Summary": True}, {"Summary": False}, {"ListFieldId": "other"}, {"Key": "other"},
722
+ {"FieldFontSize": "large"}, {"Width": {"bad": 1}}, {"MissingProperty": 1}, {"Style": {"border": "x"}},
723
+ ])
724
+ def test_column_property_errors_do_not_write_existing_columns(column_properties, properties):
725
+ result = LISTUI.do_configure_list({}, "token", "demo", {"columns": [{"fieldKey": "value", "properties": properties}]})
726
+ assert result["status"] is False
727
+ assert result["data"]["writeAttempted"] is False
728
+ assert column_properties["writes"] == [] and column_properties["adds"] == []
729
+
730
+
731
+ def test_non_numeric_columns_reject_aggregation(column_properties):
732
+ column_properties["family"] = "text"
733
+ result = LISTUI.do_configure_list({}, "token", "demo", {"columns": [{"fieldKey": "value", "properties": {"Summary": "合计"}}]})
734
+ assert result["status"] is False and column_properties["writes"] == []
735
+
736
+
737
+ def test_zero_and_false_are_verified_and_not_replayed(column_properties):
738
+ properties = {"Summary": 0, "ZeroShow": False, "ColumnOrderAndFilter": "0"}
739
+ result = LISTUI.do_configure_list({}, "token", "demo", {"columns": [{"fieldKey": "value", "properties": properties}]})
740
+ assert result["status"] is True
741
+ assert column_properties["values"] == properties
742
+ column_properties["writes"].clear()
743
+ result = LISTUI.do_configure_list({}, "token", "demo", {"columns": [{"fieldKey": "value", "properties": properties}]})
744
+ assert result["status"] is True and not column_properties["writes"]
745
+
746
+
747
+ def test_column_property_write_receipt_does_not_replace_readback(column_properties):
748
+ column_properties["dropWrite"] = True
749
+ result = LISTUI.do_configure_list({}, "token", "demo", {"columns": [{"fieldKey": "value", "properties": {"Summary": 1}}]})
750
+ assert result["status"] is False and result["errorCode"] == "LIST_CONFIGURATION_VERIFY_FAILED"
751
+ assert result["verification"]["columns"][0]["properties"]["Summary"]["matched"] is False
752
+
753
+
754
+ def test_conflicting_legacy_and_property_options_fail_before_writes(column_properties):
755
+ result = LISTUI.do_configure_list({}, "token", "demo", {"columns": [{"fieldKey": "value", "displayName": "Title", "properties": {"名称": "Other title"}}]})
756
+ assert result["status"] is False and column_properties["writes"] == []
757
+
758
+
759
+ def test_list_visibility_reuses_observed_list_flags_without_guessing_new_ones():
760
+ rows = [{"propertyName": "Visible", "displayName": "可见性", "editable": True, "complexity": "simple",
761
+ "editorType": "mcombo", "currentValue": "default,lookup,view",
762
+ "options": [{"id": "view", "name": "查看可见"}]}]
763
+ properties, _, error = LISTUI._compile_list_column_properties(rows, {"properties": {"Visible": ["default", "lookup"]}})
764
+ assert error is None and properties == {"Visible": "default,lookup"}
765
+ assert rows[0]["options"] == [{"id": "view", "name": "查看可见"}]
766
+ _, _, error = LISTUI._compile_list_column_properties(rows, {"properties": {"Visible": "invented_flag"}})
767
+ assert error is not None
768
+
769
+
770
+ def test_conflicting_aliases_within_one_property_map_are_rejected():
771
+ rows = [{"propertyName": "Summary", "displayName": "汇总方式", "editable": True, "complexity": "simple",
772
+ "editorType": "combo", "options": [{"id": 0, "name": "不汇总"}, {"id": 1, "name": "合计"}]}]
773
+ _, _, error = LISTUI._compile_list_column_properties(rows, {"properties": {"Summary": 0, "汇总方式": 1}})
774
+ assert error["errorCode"] == "LIST_COLUMN_PROPERTY_CONFLICT"
775
+
776
+
777
+ def test_duplicate_column_requests_and_invalid_sort_fail_without_writes(column_properties):
778
+ for columns in [[{"fieldKey": "value", "properties": {"Summary": 1}}, {"fieldKey": "value", "properties": {"Summary": 2}}],
779
+ [{"fieldKey": "value", "order": "up"}]]:
780
+ result = LISTUI.do_configure_list({}, "token", "demo", {"columns": columns})
781
+ assert result["status"] is False
782
+ assert column_properties["writes"] == [] and column_properties["adds"] == []
783
+
784
+
785
+ def test_real_2136_wrap_align_payload_maps_to_live_properties_without_discovery(column_properties):
786
+ column_properties["family"] = "text"
787
+ result = LISTUI.do_configure_list({}, "token", "demo", {"columns": [{"fieldKey": "value",
788
+ "properties": {"Width": "240px", "Wrap": True, "Align": "左"}}]})
789
+ assert result["status"] is True
790
+ assert column_properties["values"] == {"Width": "240px", "AutoTextWrap": True, "TextAlign": "left"}
791
+ column_properties["writes"].clear()
792
+ repeated = LISTUI.do_configure_list({}, "token", "demo", {"columns": [{"fieldKey": "value",
793
+ "properties": {"列宽": "240px", "自动换行": True, "文本对齐": "左对齐"}}]})
794
+ assert repeated["status"] is True and column_properties["writes"] == []
795
+ assert repeated["data"]["completion"]["nextAction"] == "respond_no_change"
796
+
797
+
798
+ def test_all_invalid_properties_have_candidates_and_exact_column_recovery(column_properties):
799
+ column_properties["family"] = "text"
800
+ result = LISTUI.do_configure_list({}, "token", "demo", {"columns": [{"fieldKey": "value",
801
+ "properties": {"AutoTextWrapp": True, "TextAliggn": "left"}}]})
802
+ assert result["status"] is False
803
+ data = result["data"]
804
+ assert len(data["propertyError"]["data"]["errors"]) == 2
805
+ assert data["recovery"]["command"] == "getPropertySchema"
806
+ assert data["recovery"]["arguments"] == ["--formNumber", "demo", "--elementKey", "typed_column"]
807
+ assert data["columnKey"] == "typed_column"
808
+ assert data["completion"]["nextAction"] == "repair_only_reported_input"
809
+ errors = data["propertyError"]["data"]["errors"]
810
+ assert all(error["data"]["suggestions"] for error in errors)
811
+ assert column_properties["writes"] == []
812
+
813
+
814
+ def test_known_unsupported_aggregation_has_terminal_no_card_result(column_properties):
815
+ column_properties["family"] = "text"
816
+ result = LISTUI.do_configure_list({}, "token", "demo", {"columns": [{"fieldKey": "value", "properties": {"Summary": "合计"}}]})
817
+ assert result["data"]["completion"]["nextAction"] == "respond_unmet_property"
818
+ assert result["data"]["completion"]["requiresArtifactFinalization"] is False
819
+ assert column_properties["writes"] == []
820
+
821
+
822
+ @pytest.mark.parametrize("literal_caption", [False, True])
823
+ def test_explicit_header_prefix_is_scoped_but_exact_captions_are_preserved(column_properties, monkeypatch, literal_caption):
824
+ state = column_properties
825
+ state["family"] = "text"
826
+ monkeypatch.setattr(LISTUI, "do_list_elements", lambda *args, **kwargs: {"status": True, "data": [
827
+ {"key": "header_note", "name": "备注", "type": "TextField", "parentKey": "demo"},
828
+ {"key": "entry_note", "name": "表头备注" if literal_caption else "备注", "type": "TextField", "parentKey": "entries"}]})
829
+ monkeypatch.setattr(LISTUI, "do_get_entity_type", lambda *args: _entity_type("header_note", "entry_note"))
830
+ monkeypatch.setattr(LISTUI, "do_get_form_schema", lambda *args: _schema([
831
+ {"key": "header_column", "listFieldId": "header_note"}, {"key": "entry_column", "listFieldId": "entry_note"}]))
832
+ result = LISTUI.do_configure_list({}, "token", "demo", {"columns": [{"fieldKey": "表头备注", "properties": {"Wrap": True}}]})
833
+ assert result["status"] is True
834
+ assert state["writes"][0][0] == ("entry_column" if literal_caption else "header_column")
835
+
836
+
837
+ def test_all_bad_column_values_are_reported_before_any_write(column_properties, monkeypatch):
838
+ monkeypatch.setattr(LISTUI, "do_list_elements", lambda *args, **kwargs: {"status": True, "data": [
839
+ {"key": "value", "name": "第一列", "type": "IntegerField"}, {"key": "other", "name": "第二列", "type": "IntegerField"}]})
840
+ monkeypatch.setattr(LISTUI, "do_get_entity_type", lambda *args: _entity_type("value", "other"))
841
+ monkeypatch.setattr(LISTUI, "do_get_form_schema", lambda *args: _schema([
842
+ {"key": "typed_column", "listFieldId": "value"}, {"key": "other_column", "listFieldId": "other"}]))
843
+ result = LISTUI.do_configure_list({}, "token", "demo", {"columns": [
844
+ {"fieldKey": "value", "properties": {"Width": 150}}, {"fieldKey": "other", "properties": {"Width": 150}}]})
845
+ assert result["status"] is False
846
+ assert {error["fieldKey"] for error in result["data"]["columnErrors"]} == {"value", "other"}
847
+ assert column_properties["writes"] == [] and column_properties["adds"] == []
@@ -34,8 +34,8 @@ def _platform_ok(data):
34
34
  return {"status": True, "errorCode": "0", "message": None, "data": data}
35
35
 
36
36
 
37
- def _platform_fail(error_code, message):
38
- return {"status": False, "errorCode": error_code, "message": message, "data": None}
37
+ def _platform_fail(error_code, message, data=None):
38
+ return {"status": False, "errorCode": error_code, "message": message, "data": data}
39
39
 
40
40
 
41
41
  class EndpointContractTest(unittest.TestCase):
@@ -296,6 +296,143 @@ class CallFoldingTest(unittest.TestCase):
296
296
  self.assertEqual(flat["outputDir"], "datamodel")
297
297
 
298
298
 
299
+ class BatchFailureTest(unittest.TestCase):
300
+ """兼容旧平台缺明细、新平台带明细及外层成功但业务失败的批量响应。"""
301
+
302
+ def test_platform_failure_preserves_counts_and_item_reasons(self):
303
+ data = {
304
+ "success": False, "successCount": 0, "failCount": 1,
305
+ "items": [{"id": 42, "success": False, "message": "当前环境不允许维护标品预置的MCP服务。"}],
306
+ }
307
+ for command in ("delete", "resetStatus", "unshelves", "importYaml"):
308
+ with self.subTest(command=command), mock.patch.object(
309
+ mcp_cli.kd_api, "post", return_value=_platform_fail("DELETE_FAIL", "成功0条,失败1条。", data)
310
+ ):
311
+ result = mcp_cli._call(command, {"ids": [42]}, cfg={})
312
+ self.assertFalse(result["status"])
313
+ self.assertEqual(result["errorCode"], "DELETE_FAIL")
314
+ self.assertEqual(result["message"], "成功0条,失败1条。")
315
+ self.assertEqual(result["data"], data)
316
+
317
+ def test_legacy_delete_failure_does_not_fabricate_details(self):
318
+ with mock.patch.object(mcp_cli.kd_api, "post", return_value=_platform_fail(
319
+ "DELETE_FAIL", "成功0条,失败1条。"
320
+ )):
321
+ result = mcp_cli._call("delete", {"ids": [42]}, cfg={})
322
+ self.assertEqual(result, mcp_cli._fail("DELETE_FAIL", "成功0条,失败1条。"))
323
+ self.assertIsNone(result["data"])
324
+
325
+ def test_outer_success_with_failed_count_is_not_full_success(self):
326
+ data = {
327
+ "success": True, "successCount": 1, "failCount": 1,
328
+ "message": "一条下架已受理,一条状态不允许。",
329
+ "items": [
330
+ {"id": 1, "success": True, "message": None},
331
+ {"id": 2, "success": False, "message": "仅已发布或待更新状态的数据可下架。"},
332
+ ],
333
+ }
334
+ for command in ("delete", "resetStatus", "unshelves", "importYaml"):
335
+ with self.subTest(command=command), mock.patch.object(
336
+ mcp_cli.kd_api, "post", return_value=_platform_ok(data)
337
+ ):
338
+ result = mcp_cli._call(command, {}, cfg={})
339
+ self.assertFalse(result["status"])
340
+ self.assertEqual(result["errorCode"], "PARTIAL_SUCCESS")
341
+ self.assertEqual(result["message"], data["message"])
342
+ self.assertEqual(result["data"], data)
343
+
344
+ def test_inner_failure_overrides_misleading_success_count(self):
345
+ data = {
346
+ "success": False, "errorCode": "DELETE_FAIL", "message": "实体删除操作失败。",
347
+ "successCount": 1, "failCount": 0, "items": [],
348
+ }
349
+ with mock.patch.object(mcp_cli.kd_api, "post", return_value=_platform_ok(data)):
350
+ result = mcp_cli._call("delete", {"ids": [42]}, cfg={})
351
+ self.assertFalse(result["status"])
352
+ self.assertEqual(result["errorCode"], "DELETE_FAIL")
353
+ self.assertEqual(result["data"], data)
354
+
355
+ def test_batch_failure_without_code_keeps_partial_and_total_distinct(self):
356
+ for success_count, expected in ((0, "PLATFORM_ERROR"), (1, "PARTIAL_SUCCESS")):
357
+ data = {"successCount": success_count, "failCount": 1}
358
+ with self.subTest(success_count=success_count), mock.patch.object(
359
+ mcp_cli.kd_api, "post", return_value=_platform_ok(data)
360
+ ):
361
+ result = mcp_cli._call("delete", {}, cfg={})
362
+ self.assertEqual(result["errorCode"], expected)
363
+ self.assertEqual(result["data"], data)
364
+
365
+ def test_zero_failure_batch_remains_successful(self):
366
+ data = {"success": True, "successCount": 1, "failCount": 0, "ignoreCount": 2, "items": []}
367
+ for command in ("delete", "resetStatus", "unshelves", "importYaml"):
368
+ with self.subTest(command=command), mock.patch.object(
369
+ mcp_cli.kd_api, "post", return_value=_platform_ok(data)
370
+ ):
371
+ result = mcp_cli._call(command, {}, cfg={})
372
+ self.assertEqual(result, mcp_cli._ok(data))
373
+
374
+ def test_gateway_registration_failure_does_not_invalidate_saved_record(self):
375
+ data = {"id": 42, "registerInfo": {"success": False, "failCount": 1}}
376
+ with mock.patch.object(mcp_cli.kd_api, "post", return_value=_platform_ok(data)):
377
+ result = mcp_cli._call("save", {}, cfg={})
378
+ self.assertEqual(result, mcp_cli._ok(data))
379
+
380
+ def test_message_sanitization_does_not_discard_failure_data(self):
381
+ data = {"items": [{"id": 42, "success": False, "message": "实体操作拒绝删除。"}]}
382
+ with mock.patch.object(mcp_cli.kd_api, "post", return_value=_platform_fail(
383
+ "DELETE_FAIL", "实体操作失败,TraceId:abc\n====Stack====\nJava stack", data
384
+ )):
385
+ result = mcp_cli._call("delete", {}, cfg={})
386
+ self.assertIn("TraceId:abc", result["message"])
387
+ self.assertNotIn("====Stack====", result["message"])
388
+ self.assertEqual(result["data"], data)
389
+
390
+ def test_main_batch_failure_exits_one_with_details_without_replaying(self):
391
+ data = {
392
+ "success": False, "errorCode": "DELETE_FAIL", "successCount": 0, "failCount": 1,
393
+ "items": [{"id": 42, "success": False, "message": "实体操作拒绝删除。"}],
394
+ }
395
+ for platform in (_platform_fail("DELETE_FAIL", "成功0条,失败1条。", data), _platform_ok(data)):
396
+ with self.subTest(platform_status=platform["status"]), \
397
+ mock.patch.object(mcp_cli, "_load_config", return_value={}), \
398
+ mock.patch.object(mcp_cli.kd_api, "post", return_value=platform) as post, \
399
+ contextlib.redirect_stdout(io.StringIO()) as output:
400
+ code = mcp_cli.main(["delete", "--ids", "42", "--confirm"])
401
+ self.assertEqual(code, 1)
402
+ self.assertEqual(len(output.getvalue().splitlines()), 1)
403
+ result = json.loads(output.getvalue())
404
+ self.assertFalse(result["status"])
405
+ self.assertEqual(result["data"], data)
406
+ post.assert_called_once()
407
+
408
+ def test_main_partial_unshelves_is_failure_without_replaying(self):
409
+ data = {"success": True, "successCount": 1, "failCount": 1, "items": []}
410
+ with mock.patch.object(mcp_cli, "_load_config", return_value={}), \
411
+ mock.patch.object(mcp_cli.kd_api, "post", return_value=_platform_ok(data)) as post, \
412
+ contextlib.redirect_stdout(io.StringIO()) as output:
413
+ code = mcp_cli.main(["unshelves", "--ids", "1,2", "--confirm"])
414
+ self.assertEqual(code, 1)
415
+ self.assertEqual(json.loads(output.getvalue())["data"], data)
416
+ post.assert_called_once()
417
+
418
+
419
+ class DeleteGuidanceTest(unittest.TestCase):
420
+ """两种语言的技能入口必须保留相同的删除条件与失败取证字段。"""
421
+
422
+ def test_delete_contract_covers_status_source_and_entity_operation(self):
423
+ with open(os.path.join(os.path.dirname(_SCRIPTS_DIR), "skills", "mcp-server", "SKILL.md"), encoding="utf-8") as handle:
424
+ text = handle.read()
425
+ paragraph = text.split("- `delete`", 1)[1].split("\n- ", 1)[0]
426
+ for token in ("FAILED", "UNSHELVESED", "source_type", "open_mcp_ext"):
427
+ self.assertIn(token, paragraph)
428
+
429
+ def test_delete_failure_guidance_requires_item_details_and_legacy_boundary(self):
430
+ with open(os.path.join(os.path.dirname(_SCRIPTS_DIR), "skills", "mcp-server", "SKILL.md"), encoding="utf-8") as handle:
431
+ text = handle.read()
432
+ for token in ("data.items[].message", "data: null", "OperationResult", "DELETE_FAIL"):
433
+ self.assertIn(token, text)
434
+
435
+
299
436
  class MessageSanitizationTest(unittest.TestCase):
300
437
  """Platform failure messages carry TraceId/WebInstance/Java stacks that can
301
438
  run kilobytes long (e.g. bos.orm.read.dataNotExist on a deleted record);