@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
@@ -622,6 +622,20 @@ def test_report_handoff_carries_the_compiled_presentation_contract(tmp_path):
622
622
  assert bound[0]["requiredBy"] == ["amount"]
623
623
 
624
624
 
625
+ def test_report_handoff_preserves_default_sorts_and_interactive_priority(tmp_path):
626
+ artifact = _report_artifact()
627
+ artifact["reportColumns"][1]["dateIndex"] = "TotalAmount"
628
+ artifact["reportSorts"] = [{"fieldKey": "amount", "direction": "desc"},
629
+ {"fieldKey": "org_name", "direction": "asc"}]
630
+ contract = _report_handoff(tmp_path, artifact)["reportContract"]
631
+ assert contract["defaultSorts"] == [
632
+ {"fieldKey": "amount", "dateIndex": "TotalAmount", "direction": "desc"},
633
+ {"fieldKey": "org_name", "dateIndex": "org_name", "direction": "asc"},
634
+ ]
635
+ assert contract["defaultSortPolicy"]["priority"] == "interactiveThenDefault"
636
+ assert contract["defaultSortPolicy"]["runtimeVerified"] is False
637
+
638
+
625
639
  def test_report_handoff_carries_filter_requirements_the_platform_dropped(tmp_path):
626
640
  payload = _report_handoff(tmp_path, _report_artifact(must_input=True))
627
641
 
@@ -691,6 +705,79 @@ def test_report_handoff_binds_a_column_whose_key_differs_from_its_dataset_column
691
705
  assert binding["javaFollowUp"] == ['setDateIndex("material_id")']
692
706
 
693
707
 
708
+ def test_report_handoff_prepares_the_left_tree_plugin_from_its_own_slot(tmp_path):
709
+ """The left tree's class is registered as reportProperties.tree.ReportPlugin, not reportPlugins.
710
+
711
+ Reading only reportPlugins made it unpreparable: the list plugin was registered, so naming the
712
+ tree class failed with PLUGIN_CLASS_NOT_REGISTERED, and its registered package was never the
713
+ one the source landed in.
714
+ """
715
+ artifact = _report_artifact()
716
+ artifact["reportProperties"] = {"tree": {"ReportPlugin": "kd.uhyl.report.tree.KpiTreePlugin"}}
717
+ app_root = tmp_path / "app" / "uhyl_demo"
718
+ _write(app_root / "archive.json", {"appCode": "uhyl_demo"})
719
+ _java_project(tmp_path)
720
+ artifact_path = tmp_path / "kpi_rpt.artifact.json"
721
+ _write(artifact_path, artifact)
722
+ intent = {
723
+ "form": "KPI 报表", "trigger": "报表左树加载时", "className": "KpiTreePlugin",
724
+ "coreLogic": ["按组织构建左树"],
725
+ "specialized": {"externalContracts": [], "unresolvedDependencies": []},
726
+ }
727
+ payload = prepare_handoff(
728
+ app_root=str(app_root), project_root=str(tmp_path), plugin_type="reporttreelist",
729
+ intent=intent, artifact_paths=[str(artifact_path)],
730
+ run_result=_report_run_result(artifact_path.name),
731
+ )["handoff"]
732
+
733
+ assert payload["packagePath"] == "kd/uhyl/report/tree"
734
+ assert payload["reportContract"]["pluginTypes"] == {
735
+ "kd.uhyl.report.KpiReportPlugin": "reportlist",
736
+ "kd.uhyl.report.tree.KpiTreePlugin": "reporttreelist",
737
+ }
738
+
739
+
740
+ def _report_slot_handoff(tmp_path, artifact, plugin_type, intent):
741
+ app_root = tmp_path / "app" / "uhyl_demo"
742
+ _write(app_root / "archive.json", {"appCode": "uhyl_demo"})
743
+ _java_project(tmp_path)
744
+ artifact_path = tmp_path / "kpi_rpt.artifact.json"
745
+ _write(artifact_path, artifact)
746
+ return prepare_handoff(
747
+ app_root=str(app_root), project_root=str(tmp_path), plugin_type=plugin_type,
748
+ intent={"form": "KPI 报表", "coreLogic": ["按组织汇总金额"],
749
+ "specialized": {"externalContracts": [], "unresolvedDependencies": []}, **intent},
750
+ artifact_paths=[str(artifact_path)], run_result=_report_run_result(artifact_path.name),
751
+ )["handoff"]
752
+
753
+
754
+ def test_report_handoff_prepares_the_report_form_plugin_beside_its_data_plugin(tmp_path):
755
+ """A reportform plugin is the report form's own plugin and sits in neither registered slot.
756
+
757
+ Matching it against the slot classes refused its className with PLUGIN_CLASS_NOT_REGISTERED,
758
+ and with no className the coding step was handed the data plugin's class and package.
759
+ """
760
+ payload = _report_slot_handoff(tmp_path, _report_artifact(), "reportform", {
761
+ "trigger": "点击超链接时", "className": "KpiDrillReportPlugin"})
762
+
763
+ assert payload["className"] == "KpiDrillReportPlugin"
764
+ assert payload["packagePathSource"] == "appCode convention"
765
+ # The form plugin still receives the column contract: it reads each row by column name.
766
+ assert "org_name" in payload["reportContract"]["dataSetColumns"]
767
+
768
+
769
+ def test_report_handoff_finds_the_data_plugin_by_its_own_slot(tmp_path):
770
+ """With a left tree registered as well, the data-fetch handoff needs no className."""
771
+ artifact = _report_artifact()
772
+ artifact["reportProperties"] = {"tree": {"ReportPlugin": "kd.uhyl.report.tree.KpiTreePlugin"}}
773
+
774
+ payload = _report_slot_handoff(tmp_path, artifact, "reportlist", {"trigger": "报表查询时"})
775
+
776
+ assert payload["className"] == "KpiReportPlugin"
777
+ assert payload["packagePath"] == "kd/uhyl/report"
778
+ assert payload["packagePathSource"] == "registered plugin className"
779
+
780
+
694
781
  def _bos_user_schema():
695
782
  """A getFormSchema read-back for an entity no artifact describes."""
696
783
  return {"errorCode": "0", "data": {"formNumber": "bos_user", "entities": [{
@@ -1187,7 +1274,7 @@ def test_recorded_runtime_graph_resolves_header_entries_and_subentries_without_a
1187
1274
  native = json.loads((Path(__file__).parent / "fixtures/java_context_booking_native.json").read_text())
1188
1275
  _java_project(tmp_path)
1189
1276
  app = tmp_path / "app" / "sample_app"
1190
- _write(app / "archive.json", {"appCode": "sample_app"})
1277
+ _write(app / "archive.json", {"appCode": "sample_app", "appId": "SAMPLE-ID"})
1191
1278
  intent = {"form": "定场单", "trigger": "保存时", "operationKeys": ["save"], "coreLogic": ["校验场地时段冲突"],
1192
1279
  "fields": {"date": {"form": "定场单", "entity": "header", "field": "定场日期"},
1193
1280
  "price": {"form": "定场单", "entity": "entries", "field": "单价"},
@@ -1222,7 +1309,7 @@ def test_runtime_context_without_field_declarations_still_has_the_complete_graph
1222
1309
  native = json.loads((Path(__file__).parent / "fixtures/java_context_booking_native.json").read_text())
1223
1310
  _java_project(tmp_path)
1224
1311
  app = tmp_path / "app" / "sample_app"
1225
- _write(app / "archive.json", {"appCode": "sample_app"})
1312
+ _write(app / "archive.json", {"appCode": "sample_app", "appId": "SAMPLE-ID"})
1226
1313
  result = prepare_handoff(app_root=str(app), project_root=str(tmp_path), plugin_type="operation",
1227
1314
  intent={"form": "定场单", "trigger": "保存时", "operationKeys": ["save"], "coreLogic": ["校验场地状态"],
1228
1315
  "specialized": {"unresolvedDependencies": []}}, artifact_paths=[], runtime_metadata=[native],
@@ -1285,7 +1372,7 @@ def test_independent_cli_reads_native_metadata_without_modeling_receipts(tmp_pat
1285
1372
  _write(config, {"compile": False})
1286
1373
  monkeypatch.setenv("KD_APPCONFIG", str(config))
1287
1374
  app = tmp_path / "app" / "sample_app"
1288
- _write(app / "archive.json", {"appCode": "sample_app"})
1375
+ _write(app / "archive.json", {"appCode": "sample_app", "appId": "SAMPLE-ID"})
1289
1376
  responses = tmp_path / "responses.json"
1290
1377
  _write(responses, {"getEntityType": {"sample_booking_order": native,
1291
1378
  "sample_court": {"status": True, "data": {"Name": "sample_court", "DisplayName": {"zh_CN": "场地档案"},
@@ -1329,6 +1416,11 @@ def test_independent_cli_reads_native_metadata_without_modeling_receipts(tmp_pat
1329
1416
  assert result["handoff"] == saved["handoff"]
1330
1417
  else:
1331
1418
  assert result["contextMode"] == "reference-only"
1419
+ assert result["codingStartCommand"][-3:] == ["coding-start", "--handoff", result["handoffPath"]]
1420
+ import subprocess
1421
+ started = subprocess.run(result["codingStartCommand"], capture_output=True, text=True)
1422
+ assert started.returncode == 0, started.stdout + started.stderr
1423
+ assert json.loads(started.stdout)["handoffPath"] == result["handoffPath"]
1332
1424
  assert "formGraph" not in result["handoff"]
1333
1425
  assert result["metadata"]["forms"][0]["metadataSource"] == "getEntityType"
1334
1426
  recorded = [json.loads(line) for line in calls.read_text().splitlines()]
@@ -1349,7 +1441,7 @@ def test_large_native_coding_context_is_saved_completely_without_truncating_stdo
1349
1441
  ]
1350
1442
  _java_project(tmp_path)
1351
1443
  app = tmp_path / "app" / "sample_app"
1352
- _write(app / "archive.json", {"appCode": "sample_app"})
1444
+ _write(app / "archive.json", {"appCode": "sample_app", "appId": "SAMPLE-ID"})
1353
1445
  handoff = prepare_handoff(
1354
1446
  app_root=str(app), project_root=str(tmp_path), plugin_type="operation", artifact_paths=[],
1355
1447
  runtime_metadata=[native], operation_result={"status": True, "data": [{"operationKey": "save"}]},
@@ -1381,3 +1473,858 @@ def test_prepared_class_assignment_cannot_be_silently_overwritten(tmp_path):
1381
1473
  assert error.value.error_code == "PLUGIN_HANDOFF_CONFLICT"
1382
1474
  assert target.read_bytes() == original
1383
1475
  assert_handoff_replace(target, revised, replace=True)
1476
+
1477
+
1478
+ @pytest.mark.parametrize('key,value', [('reportContract', {'dataSetColumns': ['person_id']}),
1479
+ ('sdkUsage', [{'class': 'kd.bos.entity.report.ReportColumn'}]),
1480
+ ('specialized', {'externalContracts': [{'entity': 'bos_user'}]})])
1481
+ def test_handoff_replace_protects_contract_and_sdk_changes(tmp_path, key, value):
1482
+ from prepare_plugin_handoff import assert_handoff_replace
1483
+ destination = tmp_path / 'handoff.json'
1484
+ result = {'status': True, 'handoff': {'fqcn': 'sample.Plugin', 'coreLogic': ['query']}}
1485
+ _write(destination, result)
1486
+ with pytest.raises(HandoffError) as error:
1487
+ assert_handoff_replace(destination, {'status': True, 'handoff': {**result['handoff'], key: value}})
1488
+ assert error.value.error_code == 'PLUGIN_HANDOFF_CONFLICT'
1489
+ assert json.loads(destination.read_text()) == result
1490
+
1491
+
1492
+ @pytest.mark.parametrize('compile,prepare,expected', [(False, True, '0'),
1493
+ (False, False, 'PLUGIN_WORKSPACE_UNRESOLVED'), (True, True, 'PLUGIN_WORKSPACE_UNRESOLVED')])
1494
+ def test_explicit_source_only_workspace_is_prepared_only_when_requested(tmp_path, monkeypatch, compile, prepare, expected):
1495
+ config = tmp_path / 'appconfig.json'
1496
+ _write(config, {'compile': compile})
1497
+ monkeypatch.setenv('KD_APPCONFIG', str(config))
1498
+ app = tmp_path / 'app' / 'sample_demo'
1499
+ _write(app / 'archive.json', {'appCode': 'sample_demo'})
1500
+ source = tmp_path / 'explicit-sources'
1501
+ result = resolve_output_path(str(app), 'reportlist', str(source), prepare_source=prepare)
1502
+ assert result['errorCode'] == expected
1503
+ assert (source / 'src' / 'main' / 'java').exists() == (expected == '0')
1504
+ assert not (source / 'build.gradle').exists()
1505
+ unsupported = tmp_path / 'unsupported'
1506
+ assert resolve_output_path(str(app), 'unknown', str(unsupported), prepare_source=True)['errorCode'] == 'PLUGIN_TYPE_UNSUPPORTED'
1507
+ assert not unsupported.exists()
1508
+
1509
+
1510
+ def test_report_incremental_state_rejects_old_artifact_and_reuses_current_filter_evidence(tmp_path):
1511
+ from _core.report_ops import write_report_authoring_state, sync_report_authoring_state
1512
+ app = tmp_path / 'app' / 'uhyl_demo'
1513
+ _write(app / 'archive.json', {'appCode': 'uhyl_demo'})
1514
+ _java_project(tmp_path)
1515
+ artifact = _report_artifact(must_input=True)
1516
+ before = {'readBackAvailable': True, 'readBackSource': 'canonical',
1517
+ 'reportColumns': artifact['reportColumns'], 'reportFilters': artifact['reportFilters']}
1518
+ write_report_authoring_state(str(app), 'uhyl_kpi_rpt', 'FORM', artifact, before)
1519
+ after = {**before, 'reportFilters': []}
1520
+ saved = sync_report_authoring_state(str(app), 'uhyl_kpi_rpt', 'FORM', 'filters', [], before, after, {'verified': True})
1521
+ path = tmp_path / 'old-artifact.json'
1522
+ _write(path, artifact)
1523
+ intent = {'form': 'KPI 报表', 'trigger': '报表查询', 'coreLogic': ['汇总金额'],
1524
+ 'specialized': {'unresolvedDependencies': []}}
1525
+ with pytest.raises(HandoffError) as error:
1526
+ prepare_handoff(app_root=str(app), project_root=str(tmp_path), plugin_type='reportlist',
1527
+ artifact_paths=[str(path)], run_result=_report_run_result(path.name), intent=intent)
1528
+ assert error.value.error_code == 'PLUGIN_REPORT_STATE_STALE'
1529
+ result = prepare_handoff(app_root=str(app), project_root=str(tmp_path), plugin_type='reportlist',
1530
+ artifact_paths=[saved['artifactPath']], run_result=_report_run_result(Path(saved['artifactPath']).name), intent=intent)
1531
+ assert result['handoff']['reportContract']['filterEnforcement'] == []
1532
+ assert result['handoff']['reportContract']['filterValueContracts'] == []
1533
+ assert result['reportStatePath'] == saved['path']
1534
+
1535
+
1536
+ def test_report_reconciliation_reads_current_metadata_and_never_writes_platform(tmp_path, monkeypatch):
1537
+ import prepare_plugin_handoff as module
1538
+ from _core.report_ops import report_authoring_path, _build_filters_payload
1539
+ artifact = _report_artifact()
1540
+ app = tmp_path / 'app' / 'uhyl_demo'
1541
+ _write(app / 'archive.json', {'appCode': 'uhyl_demo'})
1542
+ _java_project(tmp_path)
1543
+ path = tmp_path / 'artifact.json'
1544
+ _write(path, artifact)
1545
+ _write(report_authoring_path(str(app), 'uhyl_kpi_rpt'), {'status': 'incomplete'})
1546
+ calls = []
1547
+ def read(root, cmd, selector):
1548
+ calls.append((cmd, selector))
1549
+ return {'readBackAvailable': True, 'readBackSource': 'canonical', 'formNumber': 'uhyl_kpi_rpt',
1550
+ 'formId': 'FORM', 'reportColumns': artifact['reportColumns'], 'reportFilters': _build_filters_payload(artifact['reportFilters'])}
1551
+ monkeypatch.setattr(module, 'read_platform', read)
1552
+ result = prepare_handoff(app_root=str(app), project_root=str(tmp_path), plugin_type='reportlist',
1553
+ artifact_paths=[str(path)], run_result=_report_run_result(path.name), refresh_report_state=True,
1554
+ intent={'form': 'KPI 报表', 'trigger': '报表查询', 'coreLogic': ['汇总金额'], 'specialized': {'unresolvedDependencies': []}})
1555
+ assert calls == [('getReportSchema', 'uhyl_kpi_rpt')]
1556
+ assert result['handoff']['reportContract']['dataSetColumns'] == ['org_name', 'amount', 'currency_id']
1557
+ assert result['inputFiles'][0]['path'] == str(report_authoring_path(str(app), 'uhyl_kpi_rpt'))
1558
+ with pytest.raises(HandoffError) as error:
1559
+ prepare_handoff(app_root=str(app), project_root=str(tmp_path), plugin_type='reportlist',
1560
+ artifact_paths=[str(path)], run_result={**_report_run_result(path.name), 'formId': 'OTHER'},
1561
+ intent={'form': 'KPI 报表', 'trigger': '报表查询', 'coreLogic': ['汇总金额'], 'specialized': {'unresolvedDependencies': []}})
1562
+ assert error.value.error_code == 'PLUGIN_REPORT_STATE_STALE'
1563
+
1564
+
1565
+ @pytest.mark.parametrize('mode', ['changed-input', 'save-failed', 'gate-failed', 'success'])
1566
+ def test_prepare_does_not_advance_gate_when_inputs_change_or_snapshot_cannot_be_saved(tmp_path, monkeypatch, capsys, mode):
1567
+ import os
1568
+ import prepare_plugin_handoff as module
1569
+ import plugin_source_gate
1570
+ app = tmp_path / 'app' / 'uhyl_demo'
1571
+ _write(app / 'archive.json', {'appCode': 'uhyl_demo'})
1572
+ _java_project(tmp_path)
1573
+ config = tmp_path / 'appconfig.json'
1574
+ _write(config, {'compile': False})
1575
+ monkeypatch.setenv('KD_APPCONFIG', str(config))
1576
+ artifact = tmp_path / 'artifact.json'
1577
+ _write(artifact, _report_artifact())
1578
+ intent = tmp_path / 'intent.json'
1579
+ _write(intent, {'form': 'KPI 报表', 'trigger': '报表查询', 'coreLogic': ['汇总金额'], 'specialized': {'unresolvedDependencies': []}})
1580
+ original = module.prepare_handoff
1581
+ def prepare(**kwargs):
1582
+ result = original(**kwargs)
1583
+ if mode == 'changed-input':
1584
+ _write(intent, {'form': 'KPI 报表', 'trigger': '报表查询', 'coreLogic': ['新需求']})
1585
+ return result
1586
+ monkeypatch.setattr(module, 'prepare_handoff', prepare)
1587
+ monkeypatch.setattr(module, 'prepare_inputs', lambda **kwargs: (None, []))
1588
+ calls = []
1589
+ def gate(*args, **kwargs):
1590
+ calls.append(args)
1591
+ if mode == 'gate-failed':
1592
+ raise OSError('gate unavailable')
1593
+ monkeypatch.setattr(plugin_source_gate, 'write_state', gate)
1594
+ if mode == 'save-failed':
1595
+ def refuse(*args):
1596
+ raise OSError('snapshot unavailable')
1597
+ monkeypatch.setattr(os, 'replace', refuse)
1598
+ monkeypatch.setattr(sys, 'argv', ['prepare_plugin_handoff.py', '--app-root', str(app),
1599
+ '--java-project-root', str(tmp_path), '--type', 'reportlist', '--artifact', str(artifact),
1600
+ '--form-number', 'uhyl_kpi_rpt', '--intent-file', str(intent), '--prepare-source'])
1601
+ if mode == 'success':
1602
+ module.main()
1603
+ else:
1604
+ with pytest.raises(SystemExit):
1605
+ module.main()
1606
+ outcome = json.loads(capsys.readouterr().out)
1607
+ destination = app / '_runtime' / 'plugin-handoffs' / 'KpiReportPlugin.json'
1608
+ assert list(destination.parent.glob('*.tmp')) == []
1609
+ if mode in {'success', 'gate-failed'}:
1610
+ assert len(calls) == 1
1611
+ saved = json.loads(destination.read_text())
1612
+ assert saved['handoff']['sourceGate']['requestedState'] == 'coding'
1613
+ assert 'state' not in saved['handoff']['sourceGate']
1614
+ assert outcome['status'] is (mode == 'success')
1615
+ if mode == 'success':
1616
+ assert outcome['sourcePreparation'] == {'state': 'coding', 'verified': True, 'path': saved['handoff']['sourceGate']['path']}
1617
+ assert outcome['handoff']['sourceGate'] == saved['handoff']['sourceGate']
1618
+ else:
1619
+ assert outcome['errorCode'] == 'PLUGIN_SOURCE_GATE_UNAVAILABLE'
1620
+ assert outcome['handoffPath'] == str(destination)
1621
+ assert outcome['codingStartCommand'][-1] == str(destination)
1622
+ assert outcome['gatePath'] == saved['handoff']['sourceGate']['path']
1623
+ assert 'Do not probe' in outcome['recovery']
1624
+ return
1625
+ assert outcome['status'] is False and calls == []
1626
+ if mode == 'changed-input':
1627
+ assert outcome['errorCode'] == 'HANDOFF_INPUT_STALE'
1628
+ assert not destination.exists()
1629
+
1630
+
1631
+ def test_report_refresh_requires_authoring_artifact_before_any_platform_lookup(tmp_path):
1632
+ with pytest.raises(HandoffError) as error:
1633
+ prepare_handoff(app_root=str(tmp_path), project_root=None, plugin_type='reportlist',
1634
+ artifact_paths=[], refresh_report_state=True, intent={})
1635
+ assert error.value.error_code == 'PLUGIN_REPORT_STATE_INPUT_INVALID'
1636
+
1637
+
1638
+ def _runtime_preparation_cli(tmp_path, monkeypatch, responses, *, external=None, operations=None, compile=False):
1639
+ """Exercise real preparation and its child CLI using bounded native response snapshots."""
1640
+ import prepare_plugin_handoff as module
1641
+ app = tmp_path / 'app' / 't9k2_gl_ext'
1642
+ _write(app / 'archive.json', {'appCode': 't9k2_gl_ext', 'appId': 'APP-ID'})
1643
+ config = tmp_path / 'appconfig.json'
1644
+ _write(config, {'compile': compile})
1645
+ monkeypatch.setenv('KD_APPCONFIG', str(config))
1646
+ fixture = tmp_path / 'responses.json'
1647
+ _write(fixture, responses)
1648
+ calls = tmp_path / 'calls.jsonl'
1649
+ api = tmp_path / 'fixture_api.py'
1650
+ api.write_text('import sys,json\nfrom pathlib import Path\n'
1651
+ f'responses=json.loads(Path({str(fixture)!r}).read_text())\n'
1652
+ f'with Path({str(calls)!r}).open("a") as f: f.write(json.dumps(sys.argv[1:])+"\\n")\n'
1653
+ 'flags=["--entityId","--keyword","--formNumber","--bizAppNumber","--bizAppId","--appNumber"]\n'
1654
+ 'flag=next(v for v in flags if v in sys.argv)\n'
1655
+ 'command=sys.argv[sys.argv.index("--output")+2]\n'
1656
+ 'value=responses[command+":"+sys.argv[sys.argv.index(flag)+1]]\n'
1657
+ 'Path(sys.argv[sys.argv.index("--output")+1]).write_text(json.dumps(value))\n')
1658
+ monkeypatch.setattr(module, 'COSMIC_API', api)
1659
+ intent = tmp_path / 'intent.json'
1660
+ _write(intent, {'form': 't9k2_fetch_param', 'trigger': '处理前', 'className': 'FetchParameterPlugin',
1661
+ 'operationKeys': ['process'], 'coreLogic': ['按已确认的映射生成凭证'], 'specialized': {
1662
+ 'unresolvedDependencies': [], 'externalContracts': [
1663
+ {'entity': 't9k2_gl_voucher_ext', 'fields': ['edescription', 'debitori']}] if external else []}})
1664
+ args = ['prepare_plugin_handoff.py', '--app-root', str(app), '--project-root', str(_java_project(tmp_path)), '--type', 'operation',
1665
+ '--form-number', 't9k2_fetch_param', '--intent-file', str(intent), '--prepare-source']
1666
+ if external == 'file':
1667
+ schema = tmp_path / 'voucher.schema.json'
1668
+ _write(schema, _runtime_schema())
1669
+ args.extend(['--external-schema', str(schema)])
1670
+ if operations is not None:
1671
+ opfile = tmp_path / 'operations.json'
1672
+ _write(opfile, operations)
1673
+ args.extend(['--operations-file', str(opfile)])
1674
+ monkeypatch.setattr(sys, 'argv', args)
1675
+ return module, app, calls, intent
1676
+
1677
+
1678
+ def _runtime_native(number='t9k2_fetch_param', app='gl'):
1679
+ return {'status': True, 'data': json.dumps({'Name': number, 'DisplayName': '参数表',
1680
+ 'BizAppNumber': app, 'Properties': [{'_Type_': 'TextProp', 'Name': 'number', 'DisplayName': '编码'}]})}
1681
+
1682
+
1683
+ def _runtime_schema():
1684
+ return {'status': True, 'data': {'formNumber': 't9k2_gl_voucher_ext', 'formName': '凭证扩展', 'entities': [
1685
+ {'entityKey': 'entryentity', 'entityName': '凭证分录', 'fields': [
1686
+ {'fieldKey': 'edescription', 'fieldName': '摘要', 'fieldType': 'TextField'},
1687
+ {'fieldKey': 'debitori', 'fieldName': '原币借方', 'fieldType': 'AmountField'}]}]}}
1688
+
1689
+
1690
+ def _runtime_responses():
1691
+ return {'getEntityType:t9k2_fetch_param': _runtime_native(),
1692
+ 'getApp:t9k2_gl_ext': {'status': True, 'data': {'appInfo': {'number': 't9k2_gl_ext', 'id': 'APP-ID', 'devType': '2', 'parentId': 'BASE-ID'}}},
1693
+ 'getApp:BASE-ID': {'status': True, 'data': {'appInfo': {'number': 'gl', 'id': 'BASE-ID'}}},
1694
+ 'queryFormsByApp:t9k2_gl_ext': {'status': True, 'queryScope': {'appNumber': 't9k2_gl_ext'}, 'data': []},
1695
+ 'queryFormsByApp:gl': {'status': True,
1696
+ 'queryScope': {'source': 'explicit', 'appNumber': 'gl', 'cloudNumber': None},
1697
+ 'data': [{'formNumber': 't9k2_fetch_param', 'appNumber': 'gl', 'appId': 'BASE-ID'}]},
1698
+ 'listOperations:t9k2_fetch_param': {'status': True,
1699
+ 'data': {'formNumber': 't9k2_fetch_param', 'operations': [{'operationKey': 'process'}]}}}
1700
+
1701
+
1702
+ def test_runtime_native_owner_does_not_restrict_selected_application(tmp_path, monkeypatch, capsys):
1703
+ responses = _runtime_responses()
1704
+ module, app, calls, _ = _runtime_preparation_cli(tmp_path, monkeypatch, responses)
1705
+ module.main()
1706
+ outcome = json.loads(capsys.readouterr().out)
1707
+ assert outcome["status"] is True
1708
+ saved = json.loads(Path(outcome["handoffPath"]).read_text())
1709
+ assert saved["handoff"]["formGraph"]["formNumber"] == "t9k2_fetch_param"
1710
+ assert saved["handoff"]["formGraph"]["appCode"] == "gl"
1711
+ recorded = [json.loads(line) for line in calls.read_text().splitlines()]
1712
+ assert not any("getApp" in call or "queryFormsByApp" in call for call in recorded)
1713
+
1714
+
1715
+ def test_runtime_metadata_cannot_substitute_another_form(tmp_path, monkeypatch, capsys):
1716
+ responses = _runtime_responses()
1717
+ responses["getEntityType:t9k2_fetch_param"] = _runtime_native("another_form", "gl")
1718
+ module, app, calls, _ = _runtime_preparation_cli(tmp_path, monkeypatch, responses)
1719
+ with pytest.raises(SystemExit):
1720
+ module.main()
1721
+ outcome = json.loads(capsys.readouterr().out)
1722
+ assert outcome["status"] is False
1723
+ assert outcome["errorCode"] == "PLUGIN_CONTEXT_INCOMPLETE"
1724
+ assert not list(app.rglob("*Plugin.json"))
1725
+
1726
+
1727
+ @pytest.mark.parametrize('related', ['native', 'metadata-missing', 'auth-failed', 'transport-failed', 'wrong-schema'])
1728
+ def test_runtime_related_graph_only_falls_back_for_entity_metadata_miss(tmp_path, monkeypatch, capsys, related):
1729
+ responses = _runtime_responses()
1730
+ native = _runtime_native('t9k2_gl_voucher_ext')
1731
+ native['data'] = json.dumps({'Name': 't9k2_gl_voucher_ext', 'DisplayName': '凭证扩展', 'BizAppNumber': 'gl',
1732
+ 'Properties': [{'Name': key, '_Type_': 'TextProp', 'DisplayName': key} for key in ['edescription', 'debitori']]})
1733
+ responses['getEntityType:t9k2_gl_voucher_ext'] = native if related == 'native' else {
1734
+ 'status': False, 'errorCode': 'bos.entityMetaDataNotExist', 'message': '实体t9k2_gl_voucher_ext元数据不存在。'}
1735
+ if related == 'auth-failed':
1736
+ responses['getEntityType:t9k2_gl_voucher_ext'] = {'status': False, 'errorCode': 'AUTH_FAILED', 'message': '访问令牌无效'}
1737
+ if related == 'transport-failed':
1738
+ responses['getEntityType:t9k2_gl_voucher_ext'] = {'status': False, 'errorCode': 'CONNECTION_REFUSED', 'message': 'Connection refused'}
1739
+ responses['getFormSchema:t9k2_gl_voucher_ext'] = _runtime_schema()
1740
+ if related == 'wrong-schema':
1741
+ responses['getFormSchema:t9k2_gl_voucher_ext']['data']['formNumber'] = 'another_form'
1742
+ module, _app, calls, _ = _runtime_preparation_cli(tmp_path, monkeypatch, responses, external='read')
1743
+ good = related in {'native', 'metadata-missing'}
1744
+ if good:
1745
+ module.main()
1746
+ else:
1747
+ with pytest.raises(SystemExit):
1748
+ module.main()
1749
+ outcome = json.loads(capsys.readouterr().out)
1750
+ assert outcome['status'] is good
1751
+ recorded = [json.loads(line) for line in calls.read_text().splitlines()]
1752
+ assert sum('getFormSchema' in args for args in recorded) == (related in {'metadata-missing', 'wrong-schema'})
1753
+ if good:
1754
+ saved = json.loads(Path(outcome['handoffPath']).read_text())
1755
+ assert saved['handoff']['specialized']['externalContracts'][0]['fields'] == ['edescription', 'debitori']
1756
+ assert len(saved['handoff']['relatedForms']) == (related == 'native')
1757
+ else:
1758
+ assert outcome['errorCode'] == ('PLUGIN_EXTERNAL_CONTRACT_UNVERIFIED' if related == 'wrong-schema' else 'PLUGIN_EVIDENCE_READ_FAILED')
1759
+
1760
+
1761
+ def test_runtime_explicit_evidence_is_reused_and_inputs_remain_hash_bound(tmp_path, monkeypatch, capsys):
1762
+ operations = _runtime_responses()['listOperations:t9k2_fetch_param']
1763
+ module, _app, calls, _ = _runtime_preparation_cli(tmp_path, monkeypatch, _runtime_responses(), external='file', operations=operations)
1764
+ module.main()
1765
+ outcome = json.loads(capsys.readouterr().out)
1766
+ assert outcome['status'] is True
1767
+ recorded = [json.loads(line) for line in calls.read_text().splitlines()]
1768
+ assert len(recorded) == 1 # only the current entity read; operation and related evidence supplied
1769
+ saved = json.loads(Path(outcome['handoffPath']).read_text())
1770
+ assert {Path(row['path']).name for row in saved['inputFiles']} >= {'intent.json', 'operations.json', 'voucher.schema.json'}
1771
+ from query_plugin_handoff import validate_input_files
1772
+ validate_input_files(saved)
1773
+ _write(tmp_path / 'voucher.schema.json', {'status': False})
1774
+ with pytest.raises(ValueError, match='changed'):
1775
+ validate_input_files(saved)
1776
+
1777
+
1778
+ @pytest.mark.parametrize('invalid', ['operations', 'fields', 'compile'])
1779
+ def test_runtime_explicit_inputs_do_not_bypass_verification(tmp_path, monkeypatch, capsys, invalid):
1780
+ operations = _runtime_responses()['listOperations:t9k2_fetch_param']
1781
+ if invalid == 'operations':
1782
+ operations['data']['formNumber'] = 'another_form'
1783
+ module, app, _calls, intent = _runtime_preparation_cli(tmp_path, monkeypatch, _runtime_responses(),
1784
+ external='file', operations=operations, compile=invalid == 'compile')
1785
+ if invalid == 'fields':
1786
+ data = json.loads(intent.read_text())
1787
+ data['specialized']['externalContracts'][0]['fields'] = ['debit', 'credit', 'creditlocal']
1788
+ _write(intent, data)
1789
+ with pytest.raises(SystemExit):
1790
+ module.main()
1791
+ outcome = json.loads(capsys.readouterr().out)
1792
+ assert outcome['status'] is False
1793
+ assert not list(app.rglob('*Plugin.json'))
1794
+ if invalid == 'fields':
1795
+ assert all(key in outcome['message'] for key in ['debit', 'credit', 'creditlocal']), outcome
1796
+ if invalid == 'compile':
1797
+ assert outcome['errorCode'] == 'PLUGIN_SDK_ENVIRONMENT_UNVERIFIED'
1798
+
1799
+
1800
+ @pytest.mark.parametrize('explicit', [False, True])
1801
+ def test_runtime_invalid_operation_stops_before_external_reads(tmp_path, monkeypatch, capsys, explicit):
1802
+ responses = _runtime_responses()
1803
+ responses['listOperations:t9k2_fetch_param']['data']['operations'] = [
1804
+ {'operationKey': 'close'}, {'operationKey': 'fetchsync', 'operationName': '开始同步', 'operationType': 'donothing'}]
1805
+ operations = responses['listOperations:t9k2_fetch_param'] if explicit else None
1806
+ module, _app, calls, intent = _runtime_preparation_cli(tmp_path, monkeypatch, responses, external='read', operations=operations)
1807
+ data = json.loads(intent.read_text())
1808
+ data['operationKeys'] = ['confirm']
1809
+ _write(intent, data)
1810
+ with pytest.raises(SystemExit):
1811
+ module.main()
1812
+ outcome = json.loads(capsys.readouterr().out)
1813
+ assert outcome['status'] is False
1814
+ assert 'confirm' in outcome['message'] and 'available operationKeys: close, fetchsync' in outcome['message']
1815
+ recorded = [json.loads(line) for line in calls.read_text().splitlines()]
1816
+ assert all('t9k2_gl_voucher_ext' not in args for args in recorded)
1817
+ assert sum('listOperations' in args for args in recorded) == (not explicit)
1818
+
1819
+
1820
+ def test_external_diagnostics_collect_all_contracts_and_remain_bounded():
1821
+ from prepare_plugin_handoff import _verify_external_contracts
1822
+ catalog = [{'formNumber': 'voucher', 'entityKey': 'entryentity', 'entityName': '分录',
1823
+ 'fields': {'debitori': {'fieldKey': 'debitori', 'fieldName': '原币借方'}}}]
1824
+ with pytest.raises(HandoffError) as error:
1825
+ _verify_external_contracts([{'entity': 'voucher', 'fields': ['debit', 'credit']},
1826
+ {'entity': 'account_mapping', 'fields': ['account']},
1827
+ {'entity': 'org_mapping', 'fields': ['org']}], catalog)
1828
+ assert error.value.error_code == 'PLUGIN_EXTERNAL_CONTRACT_UNVERIFIED'
1829
+ assert all(value in str(error.value) for value in ['3 external contract(s)', 'debit', 'credit', 'account_mapping', 'org_mapping'])
1830
+ with pytest.raises(HandoffError) as many:
1831
+ _verify_external_contracts([{'entity': 'missing_' + str(index)} for index in range(40)], catalog)
1832
+ assert '40 external contract(s)' in str(many.value)
1833
+ assert '32 additional failed contracts omitted' in str(many.value)
1834
+ assert len(str(many.value)) < 6500
1835
+
1836
+
1837
+ def test_explicit_external_schema_cannot_satisfy_another_form_with_same_entity_key():
1838
+ from prepare_plugin_handoff import _verify_external_contracts
1839
+ catalog = [{'formNumber': 'wrong_form', 'entityKey': 'entryentity', 'entityName': '分录',
1840
+ 'fields': {'debitori': {'fieldKey': 'debitori'}}}]
1841
+ with pytest.raises(HandoffError):
1842
+ _verify_external_contracts([{'entity': 'entryentity', 'formNumber': 'voucher', 'fields': ['debitori']}], catalog)
1843
+
1844
+
1845
+ def test_session_voucher_diagnostic_keeps_all_eleven_failed_selectors():
1846
+ from prepare_plugin_handoff import _verify_external_contracts
1847
+ missing = ["entries." + key for key in ["currency", "account", "debit", "credit", "localdebit",
1848
+ "localcredit", "description", "seq", "t9k2_chargeperson", "t9k2_country", "t9k2_province"]]
1849
+ catalog = [{"formNumber": "voucher", "entityKey": "entryentity", "fields": {
1850
+ "debitori": {"fieldName": "原币借方"}, "edescription": {"fieldName": "摘要"}}}]
1851
+ with pytest.raises(HandoffError) as error:
1852
+ _verify_external_contracts([{"entity": "voucher", "fields": missing}], catalog)
1853
+ assert all(selector in str(error.value) for selector in missing), str(error.value)
1854
+ assert "unverified field selectors (11)" in str(error.value)
1855
+ assert len(str(error.value)) < 1000
1856
+
1857
+
1858
+ def test_page_only_handoff_compiles_without_entity_fields(tmp_path, monkeypatch, capsys):
1859
+ import prepare_plugin_handoff as module
1860
+ import compile_plugin_source as compiler
1861
+ from argparse import Namespace
1862
+ from query_plugin_handoff import query
1863
+
1864
+ app = tmp_path / "app" / "sample_app"
1865
+ _write(app / "archive.json", {"appCode": "sample_app", "appId": "SAMPLE-ID"})
1866
+ project = _java_project(tmp_path)
1867
+ config = tmp_path / "appconfig.json"
1868
+ _write(config, {"compile": False})
1869
+ monkeypatch.setenv("KD_APPCONFIG", str(config))
1870
+ xml = """<FormMetadata><Key>sample_page</Key><Name>Filter Page</Name><BizAppNumber>sample_app</BizAppNumber><Items>
1871
+ <BasedataFormAp><Plugins><Plugin><ClassName>kd.bos.app.sample.app.form.SamplePlugin</ClassName><Enabled>true</Enabled></Plugin></Plugins></BasedataFormAp>
1872
+ <FormAp><Id>root</Id><Key>sample_page</Key></FormAp><FilterGridAp><Id>filter</Id><ParentId>root</ParentId><Key>filtergrid</Key></FilterGridAp>
1873
+ </Items></FormMetadata>"""
1874
+ calls = []
1875
+ def read(app_root, command, selector, flag="--formNumber"):
1876
+ calls.append((command, selector, flag))
1877
+ assert command == "getFormMetadata"
1878
+ return {"status": True, "data": xml}
1879
+ monkeypatch.setattr(module, "read_platform", read)
1880
+ source = project / "src/main/java/kd/bos/app/sample/app/form/SamplePlugin.java"
1881
+ source.parent.mkdir(parents=True)
1882
+ source.write_text("package kd.bos.app.sample.app.form;\nimport kd.bos.form.plugin.AbstractFormPlugin;\npublic class SamplePlugin extends AbstractFormPlugin {}\n")
1883
+ intent = {"form": "sample_page", "trigger": "open", "metadataNeeds": ["form"],
1884
+ "coreLogic": ["initialize the filter"], "ownershipMode": "modify", "sourcePath": str(source),
1885
+ "scenes": ["control/filter-grid/filter-grid-set-value"],
1886
+ "specialized": {"unresolvedDependencies": [], "externalContracts": []}}
1887
+ monkeypatch.setattr(sys, "argv", ["prepare_plugin_handoff.py", "--app-root", str(app),
1888
+ "--project-root", str(project), "--form-number", "sample_page", "--type", "form",
1889
+ "--intent-json", json.dumps(intent), "--prepare-source"])
1890
+ module.main()
1891
+ prepared = json.loads(capsys.readouterr().out)
1892
+ assert prepared["status"] is True
1893
+ assert prepared["metadata"]["formCount"] == 1
1894
+ assert prepared["metadata"]["forms"][0]["metadataSource"] == "getFormMetadata"
1895
+ assert "entityCount" not in prepared["metadata"]["forms"][0]
1896
+ assert "handoff.uiContext" in prepared["contextSections"] and "handoff.formGraph" not in prepared["contextSections"]
1897
+ assert calls == [("getFormMetadata", "sample_page", "--formId")]
1898
+ document = json.loads(Path(prepared["handoffPath"]).read_text())
1899
+ handoff = document["handoff"]
1900
+ assert "formGraph" not in handoff
1901
+ assert handoff["uiContext"]["formNumber"] == "sample_page"
1902
+ assert any(control["key"] == "filtergrid" for control in handoff["uiContext"]["controls"])
1903
+ assert handoff["fqcn"] == "kd.bos.app.sample.app.form.SamplePlugin"
1904
+ for operation in ("coding-start", "coding-context"):
1905
+ view = query(document, Namespace(operation=operation))
1906
+ assert view["uiContext"]["formNumber"] == "sample_page"
1907
+ assert view["codingEvidence"]["scenes"][0].endswith("filter-grid-set-value.md")
1908
+ assert "formGraph" not in view and "entities" not in view
1909
+ assert "unverified" in view["entityEvidenceScope"]
1910
+ context = compiler.compile_handoff(prepared["handoffPath"])
1911
+ result = compiler.compile_source(project, source, "SamplePlugin", 30, app_root=app,
1912
+ plugin_type="form", form_number=context["formNumber"], operation_keys=[],
1913
+ description=context["description"], field_context=context)
1914
+ assert result["status"] is True and result["compileSkipped"] is True
1915
+ assert json.loads((app / "archive.json").read_text())["javas"][0]["formNumber"] == "sample_page"
1916
+ import register_plugins as registration
1917
+ monkeypatch.setattr(registration, "_load_config", lambda root: {"baseUrl": "https://example.test"})
1918
+ sent = []
1919
+ def post(*args, **kwargs):
1920
+ sent.append(args[3])
1921
+ return {"status": True, "data": {"success": True}}
1922
+ monkeypatch.setattr(registration.kd_api, "post", post)
1923
+ monkeypatch.setattr(registration.kd_api, "get", lambda *args, **kwargs: {"status": True, "data": [
1924
+ {"className": "kd.bos.app.sample.app.form.SamplePlugin", "location": "BILL_FORM"}]})
1925
+ monkeypatch.setattr(sys, "argv", ["register_plugins.py", "--app-root", str(app), "--verify"])
1926
+ assert registration.main() == 0
1927
+ registered = json.loads(capsys.readouterr().out)
1928
+ assert registered["verification"]["status"] == "pass"
1929
+ assert registered["results"][0]["verifiedHere"] is True
1930
+ assert sent[0]["formNumber"] == "sample_page" and sent[0]["targetType"] == "BILL_FORM"
1931
+
1932
+
1933
+ @pytest.mark.parametrize("xml", ["", "<bad", "<FormMetadata><Key>other</Key><Items/></FormMetadata>",
1934
+ "<FormMetadata><Key>sample_page</Key></FormMetadata>"])
1935
+ def test_invalid_page_metadata_never_proves_an_empty_page(tmp_path, monkeypatch, xml):
1936
+ import prepare_plugin_handoff as module
1937
+ monkeypatch.setattr(module, "read_platform", lambda *args, **kwargs: {"status": True, "data": xml})
1938
+ with pytest.raises(module.HandoffError) as error:
1939
+ module.read_ui_context(str(tmp_path), "sample_page", "form")
1940
+ assert error.value.error_code == "PLUGIN_CONTEXT_INCOMPLETE"
1941
+
1942
+
1943
+ def test_entity_selectors_cannot_skip_entity_metadata(tmp_path, monkeypatch, capsys):
1944
+ import prepare_plugin_handoff as module
1945
+ intent = {"form": "sample_page", "trigger": "open", "metadataNeeds": ["form"],
1946
+ "coreLogic": ["read an entity field"], "entities": {"value": "field"},
1947
+ "specialized": {"unresolvedDependencies": []}}
1948
+ monkeypatch.setattr(sys, "argv", ["prepare_plugin_handoff.py", "--app-root", str(tmp_path),
1949
+ "--project-root", str(tmp_path), "--form-number", "sample_page", "--type", "form",
1950
+ "--intent-json", json.dumps(intent)])
1951
+ with pytest.raises(SystemExit):
1952
+ module.main()
1953
+ assert json.loads(capsys.readouterr().out)["errorCode"] == "PLUGIN_CONTEXT_INCOMPLETE"
1954
+
1955
+
1956
+ def test_source_only_modify_requires_no_platform_metadata(tmp_path, monkeypatch, capsys):
1957
+ import prepare_plugin_handoff as module
1958
+ import compile_plugin_source as compiler
1959
+ from argparse import Namespace
1960
+ from query_plugin_handoff import query
1961
+ app = tmp_path / "app" / "sample_app"
1962
+ _write(app / "archive.json", {"appCode": "sample_app", "appId": "SAMPLE-ID"})
1963
+ project = _java_project(tmp_path)
1964
+ config = tmp_path / "appconfig.json"
1965
+ _write(config, {"compile": False})
1966
+ monkeypatch.setenv("KD_APPCONFIG", str(config))
1967
+ source = project / "src/main/java/kd/bos/app/sample/app/service/SampleService.java"
1968
+ source.parent.mkdir(parents=True)
1969
+ source.write_text("package kd.bos.app.sample.app.service; public class SampleService {}")
1970
+ def unexpected(*args, **kwargs):
1971
+ raise AssertionError("source-only task must not read platform metadata")
1972
+ monkeypatch.setattr(module, "read_platform", unexpected)
1973
+ intent = {"trigger": "update service", "metadataNeeds": [], "coreLogic": ["improve service"],
1974
+ "ownershipMode": "modify", "sourcePath": str(source),
1975
+ "specialized": {"unresolvedDependencies": []}}
1976
+ monkeypatch.setattr(sys, "argv", ["prepare_plugin_handoff.py", "--app-root", str(app),
1977
+ "--project-root", str(project), "--type", "service", "--intent-json", json.dumps(intent), "--prepare-source"])
1978
+ module.main()
1979
+ prepared = json.loads(capsys.readouterr().out)
1980
+ document = json.loads(Path(prepared["handoffPath"]).read_text())
1981
+ handoff = document["handoff"]
1982
+ assert handoff["formNumber"] == "" and "formGraph" not in handoff and "uiContext" not in handoff
1983
+ for operation in ("coding-start", "coding-context"):
1984
+ view = query(document, Namespace(operation=operation))
1985
+ assert view["sourceContext"]["scope"] == "source-only" and "formGraph" not in view
1986
+ context = compiler.compile_handoff(prepared["handoffPath"])
1987
+ monkeypatch.setattr(sys, "argv", ["compile_plugin_source.py", "--handoff", prepared["handoffPath"]])
1988
+ assert compiler.main() == 0
1989
+ result = json.loads(capsys.readouterr().out)
1990
+ assert result["status"] is True and result["compileSkipped"] is True
1991
+ assert json.loads((app / "archive.json").read_text())["javas"][0]["formNumber"] == ""
1992
+
1993
+
1994
+ def test_list_context_keeps_list_plugin_binding_and_controls(tmp_path, monkeypatch):
1995
+ import prepare_plugin_handoff as module
1996
+ xml = """<FormMetadata><Key>sample_page</Key><Items><FormAp><Id>main</Id><Key>sample_page</Key></FormAp>
1997
+ <ListMeta><FormMetadata><Key>sample_list</Key><Items><FormAp><Id>list</Id><Key>sample_list</Key>
1998
+ <Plugins><Plugin><ClassName>example.ListPlugin</ClassName><Enabled>true</Enabled></Plugin></Plugins></FormAp>
1999
+ <FilterGridAp><Id>filter</Id><ParentId>list</ParentId><Key>filtergrid</Key></FilterGridAp></Items></FormMetadata></ListMeta>
2000
+ </Items></FormMetadata>"""
2001
+ monkeypatch.setattr(module, "read_platform", lambda *args, **kwargs: {"status": True, "data": xml})
2002
+ context = module.read_ui_context(str(tmp_path), "sample_page", "listform", include_list=True)
2003
+ assert context["plugins"] == [{"className": "example.ListPlugin", "type": "listform"}]
2004
+ assert any(control["key"] == "filtergrid" for control in context["controls"])
2005
+
2006
+
2007
+ def test_page_field_selector_uses_explicit_ui_binding_not_control_key(tmp_path, monkeypatch):
2008
+ import prepare_plugin_handoff as module
2009
+ app = tmp_path / "app" / "sample_app"
2010
+ _write(app / "archive.json", {"appCode": "sample_app", "appId": "SAMPLE-ID"})
2011
+ project = _java_project(tmp_path)
2012
+ xml = """<FormMetadata><Key>sample_page</Key><Items><FormAp><Id>root</Id><Key>sample_page</Key></FormAp>
2013
+ <TextFieldAp><Id>control</Id><ParentId>root</ParentId><Key>visible_control</Key><FieldKey>temporary_value</FieldKey></TextFieldAp>
2014
+ </Items></FormMetadata>"""
2015
+ monkeypatch.setattr(module, "read_platform", lambda *args, **kwargs: {"status": True, "data": xml})
2016
+ context = module.read_ui_context(str(tmp_path), "sample_page", "form")
2017
+ assert next(row for row in context["controls"] if row["key"] == "visible_control")["fieldKey"] == "temporary_value"
2018
+ intent = {"form": "sample_page", "trigger": "open", "coreLogic": ["read temporary input"],
2019
+ "fields": {"input": "temporary_value"}, "specialized": {"unresolvedDependencies": []}}
2020
+ prepared = module.prepare_handoff(app_root=str(app), project_root=str(project), plugin_type="form",
2021
+ intent=intent, artifact_paths=[], ui_context=context)["handoff"]
2022
+ assert prepared["fields"] == {"input": "temporary_value"} and "formGraph" not in prepared
2023
+ intent["fields"] = {"input": "visible_control"}
2024
+ with pytest.raises(module.HandoffError):
2025
+ module.prepare_handoff(app_root=str(app), project_root=str(project), plugin_type="form",
2026
+ intent=intent, artifact_paths=[], ui_context=context)
2027
+
2028
+
2029
+ def test_dynamic_form_ap_plugin_binding_is_discovered(tmp_path, monkeypatch):
2030
+ import prepare_plugin_handoff as module
2031
+ xml = """<FormMetadata><Key>sample_page</Key><Items><FormAp><Id>root</Id><Key>sample_page</Key>
2032
+ <Plugins><Plugin><ClassName>example.DynamicPlugin</ClassName><Enabled>true</Enabled></Plugin></Plugins>
2033
+ </FormAp></Items></FormMetadata>"""
2034
+ monkeypatch.setattr(module, "read_platform", lambda *args, **kwargs: {"status": True, "data": xml})
2035
+ context = module.read_ui_context(str(tmp_path), "sample_page", "form")
2036
+ assert context["plugins"] == [{"className": "example.DynamicPlugin", "type": "form"}]
2037
+ context = module.read_ui_context(str(tmp_path), "sample_page", "operation")
2038
+ assert context["plugins"][0]["type"] == "form"
2039
+
2040
+
2041
+ def test_operation_without_field_dependency_reads_only_operation_and_ownership(tmp_path, monkeypatch, capsys):
2042
+ import prepare_plugin_handoff as module
2043
+ from argparse import Namespace
2044
+ from query_plugin_handoff import query
2045
+ app = tmp_path / "app" / "sample_app"
2046
+ _write(app / "archive.json", {"appCode": "sample_app", "appId": "SAMPLE-ID"})
2047
+ project = _java_project(tmp_path)
2048
+ config = tmp_path / "appconfig.json"
2049
+ _write(config, {"compile": False})
2050
+ monkeypatch.setenv("KD_APPCONFIG", str(config))
2051
+ calls = []
2052
+ def read(app_root, command, selector, flag="--formNumber"):
2053
+ calls.append(command)
2054
+ if command == "getApp":
2055
+ return {"status": True, "data": {"appInfo": {"number": "sample_app", "id": "SAMPLE-ID"}}}
2056
+ if command == "listOperations":
2057
+ return {"status": True, "data": {"formNumber": "sample_page", "operations": [{"operationKey": "submit"}]}}
2058
+ if command == "queryFormsByApp":
2059
+ return {"status": True, "queryScope": {"appNumber": "sample_app"},
2060
+ "data": [{"formNumber": "sample_page", "appNumber": "sample_app", "appId": "SAMPLE-ID"}]}
2061
+ raise AssertionError(command)
2062
+ monkeypatch.setattr(module, "read_platform", read)
2063
+ intent = {"form": "sample_page", "trigger": "submit", "operationKeys": ["submit"],
2064
+ "metadataNeeds": ["operate"], "coreLogic": ["validate operation context"],
2065
+ "specialized": {"unresolvedDependencies": []}}
2066
+ monkeypatch.setattr(sys, "argv", ["prepare_plugin_handoff.py", "--app-root", str(app),
2067
+ "--project-root", str(project), "--form-number", "sample_page", "--type", "operation",
2068
+ "--intent-json", json.dumps(intent), "--prepare-source"])
2069
+ module.main()
2070
+ result = json.loads(capsys.readouterr().out)
2071
+ document = json.loads(Path(result["handoffPath"]).read_text())
2072
+ assert calls == ["listOperations"]
2073
+ assert "formGraph" not in document["handoff"]
2074
+ view = query(document, Namespace(operation="coding-start"))
2075
+ assert view["operationContext"]["source"] == "listOperations"
2076
+ assert view["operationContracts"][0]["operationKey"] == "submit"
2077
+
2078
+
2079
+ def test_namespaced_page_metadata_preserves_identity_and_plugin(tmp_path, monkeypatch):
2080
+ import prepare_plugin_handoff as module
2081
+ xml = """<FormMetadata xmlns="urn:test"><Key>sample_page</Key><Items><FormAp><Id>root</Id><Key>sample_page</Key><Plugins><Plugin><ClassName>example.PagePlugin</ClassName></Plugin></Plugins></FormAp></Items></FormMetadata>"""
2082
+ monkeypatch.setattr(module, "read_platform", lambda *args, **kwargs: {"status": True, "data": xml})
2083
+ context = module.read_ui_context(str(tmp_path), "sample_page", "form")
2084
+ assert context["formNumber"] == "sample_page"
2085
+ assert context["plugins"][0]["className"] == "example.PagePlugin"
2086
+
2087
+
2088
+ def test_page_operation_reuses_explicit_operation_evidence_without_entity_read(tmp_path, monkeypatch, capsys):
2089
+ import prepare_plugin_handoff as module
2090
+ app = tmp_path / "app" / "sample_app"
2091
+ _write(app / "archive.json", {"appCode": "sample_app", "appId": "SAMPLE-ID"})
2092
+ project = _java_project(tmp_path)
2093
+ config = tmp_path / "appconfig.json"
2094
+ _write(config, {"compile": False})
2095
+ monkeypatch.setenv("KD_APPCONFIG", str(config))
2096
+ operations = tmp_path / "operations.json"
2097
+ _write(operations, {"status": True, "data": {"formNumber": "sample_page", "appRoot": str(app),
2098
+ "operations": [{"operationKey": "refresh", "operationName": "Refresh"}]}})
2099
+ xml = """<FormMetadata><Key>sample_page</Key><BizAppNumber>sample_app</BizAppNumber><Items>
2100
+ <FormAp><Id>root</Id><Key>sample_page</Key></FormAp></Items></FormMetadata>"""
2101
+ calls = []
2102
+ def read(app_root, command, selector, flag="--formNumber"):
2103
+ calls.append(command)
2104
+ assert command == "getFormMetadata"
2105
+ return {"status": True, "data": xml}
2106
+ monkeypatch.setattr(module, "read_platform", read)
2107
+ intent = {"form": "sample_page", "trigger": "click", "metadataNeeds": ["form", "operate"],
2108
+ "coreLogic": ["invoke the existing refresh operation"],
2109
+ "specialized": {"unresolvedDependencies": []}}
2110
+ monkeypatch.setattr(sys, "argv", ["prepare_plugin_handoff.py", "--app-root", str(app),
2111
+ "--project-root", str(project), "--form-number", "sample_page", "--type", "form",
2112
+ "--intent-json", json.dumps(intent), "--operations-file", str(operations), "--prepare-source"])
2113
+ module.main()
2114
+ result = json.loads(capsys.readouterr().out)
2115
+ document = json.loads(Path(result["handoffPath"]).read_text())
2116
+ assert calls == ["getFormMetadata"]
2117
+ assert document["handoff"]["operationKeys"] == []
2118
+ assert document["handoff"]["availableOperations"][0]["operationKey"] == "refresh"
2119
+ assert "formGraph" not in document["handoff"]
2120
+ assert any(item["path"] == str(operations) for item in document["inputFiles"])
2121
+
2122
+
2123
+ def test_source_only_uses_existing_package_without_application_identity(tmp_path, monkeypatch, capsys):
2124
+ import prepare_plugin_handoff as module
2125
+ app = tmp_path / "app" / "unbound"
2126
+ app.mkdir(parents=True)
2127
+ project = _java_project(tmp_path)
2128
+ config = tmp_path / "appconfig.json"
2129
+ _write(config, {"compile": False})
2130
+ monkeypatch.setenv("KD_APPCONFIG", str(config))
2131
+ source = project / "src/main/java/example/SampleService.java"
2132
+ source.parent.mkdir(parents=True)
2133
+ source.write_text("package example; public class SampleService {}")
2134
+ def unexpected(*args, **kwargs):
2135
+ raise AssertionError("source-only task must not read metadata")
2136
+ monkeypatch.setattr(module, "read_platform", unexpected)
2137
+ intent = {"trigger": "edit", "metadataNeeds": [], "coreLogic": ["edit existing service"],
2138
+ "ownershipMode": "modify", "sourcePath": str(source), "specialized": {"unresolvedDependencies": []}}
2139
+ monkeypatch.setattr(sys, "argv", ["prepare_plugin_handoff.py", "--app-root", str(app),
2140
+ "--project-root", str(project), "--type", "service", "--intent-json", json.dumps(intent), "--prepare-source"])
2141
+ module.main()
2142
+ result = json.loads(capsys.readouterr().out)
2143
+ handoff = json.loads(Path(result["handoffPath"]).read_text())["handoff"]
2144
+ assert handoff["fqcn"] == "example.SampleService"
2145
+ assert handoff["packagePathSource"] == "existing source declaration"
2146
+ assert handoff["formNumber"] == ""
2147
+
2148
+
2149
+ def test_page_can_create_a_distinct_plugin_without_overwriting_registered_class(tmp_path):
2150
+ from prepare_plugin_handoff import prepare_handoff, HandoffError
2151
+ app = tmp_path / "app" / "sample_app"
2152
+ _write(app / "archive.json", {"appCode": "sample_app", "appId": "SAMPLE-ID"})
2153
+ project = _java_project(tmp_path)
2154
+ context = {"formNumber": "sample_page", "formName": "sample_page", "source": "getFormMetadata",
2155
+ "controls": [], "plugins": [{"className": "example.ExistingPlugin", "type": "form"}]}
2156
+ intent = {"form": "sample_page", "trigger": "click", "className": "NewPlugin",
2157
+ "ownershipMode": "create-new", "coreLogic": ["handle click"],
2158
+ "specialized": {"unresolvedDependencies": []}}
2159
+ result = prepare_handoff(app_root=str(app), project_root=str(project), plugin_type="form",
2160
+ intent=intent, artifact_paths=[], ui_context=context)
2161
+ assert result["handoff"]["className"] == "NewPlugin"
2162
+ assert result["evidence"]["registeredPluginClasses"] == ["example.ExistingPlugin"]
2163
+ intent["className"] = "ExistingPlugin"
2164
+ with pytest.raises(HandoffError) as error:
2165
+ prepare_handoff(app_root=str(app), project_root=str(project), plugin_type="form",
2166
+ intent=intent, artifact_paths=[], ui_context=context)
2167
+ assert error.value.error_code == "PLUGIN_OWNERSHIP_CONFLICT"
2168
+
2169
+
2170
+ def test_fieldless_page_uses_real_javac_and_native_compile_archive(tmp_path, monkeypatch, capsys):
2171
+ import os
2172
+ import shlex
2173
+ import shutil
2174
+ import subprocess
2175
+ import zipfile
2176
+ import prepare_plugin_handoff as module
2177
+ import compile_plugin_source as compiler
2178
+ javac = shutil.which("javac")
2179
+ java = shutil.which("java")
2180
+ if not javac or not java or not shutil.which("javap"):
2181
+ pytest.skip("JDK tools unavailable")
2182
+ app = tmp_path / "app" / "sample_app"
2183
+ _write(app / "archive.json", {"appCode": "sample_app", "appId": "SAMPLE-ID"})
2184
+ gradle = tmp_path / "gradle"
2185
+ project = gradle / "code" / "sample"
2186
+ (project / "src/main/java").mkdir(parents=True)
2187
+ (gradle / "settings.gradle").write_text("include(':sample')\nproject(':sample').projectDir=new File('code/sample')\n")
2188
+ sdk = tmp_path / "sdk"
2189
+ lib = sdk / "mservice-cosmic/lib/bos"
2190
+ lib.mkdir(parents=True)
2191
+ stub = tmp_path / "AbstractFormPlugin.java"
2192
+ stub.write_text("package kd.bos.form.plugin; public abstract class AbstractFormPlugin {}")
2193
+ subprocess.run([javac, "-d", str(tmp_path / "sdk-classes"), str(stub)], check=True, capture_output=True)
2194
+ jar = lib / "fixture.jar"
2195
+ with zipfile.ZipFile(jar, "w") as archive:
2196
+ archive.write(tmp_path / "sdk-classes/kd/bos/form/plugin/AbstractFormPlugin.class",
2197
+ "kd/bos/form/plugin/AbstractFormPlugin.class")
2198
+ (gradle / "gradle.properties").write_text(f"systemProp.cosmic_home={sdk}\n")
2199
+ (gradle / "config.gradle").write_text('cosmic_libs_path="${cosmic_home}/mservice-cosmic/lib"\nbos="${cosmic_libs_path}/bos"')
2200
+ config = tmp_path / "appconfig.json"
2201
+ import re
2202
+ settings = subprocess.run([java, "-XshowSettings:properties", "-version"], capture_output=True, text=True, timeout=15, check=True)
2203
+ match = re.search(r"(?m)^\s*java\.home\s*=\s*(.+)$", settings.stderr + settings.stdout)
2204
+ if not match:
2205
+ pytest.skip("Java home cannot be resolved")
2206
+ _write(config, {"compile": True, "GRADLE_ROOT": str(gradle), "JAVA_HOME": match.group(1).strip()})
2207
+ monkeypatch.setenv("KD_APPCONFIG", str(config))
2208
+ source = project / "src/main/java/kd/bos/app/sample/app/form/PagePlugin.java"
2209
+ output = project / "build/classes/java/main"
2210
+ wrapper = gradle / ("gradlew.bat" if os.name == "nt" else "gradlew")
2211
+ if os.name == "nt":
2212
+ wrapper.write_text(f'@echo off\nif not exist "{output}" mkdir "{output}"\n"{javac}" -cp "{jar}" -d "{output}" "{source}"\n')
2213
+ else:
2214
+ wrapper.write_text('#!/bin/sh\nset -e\nmkdir -p ' + shlex.quote(str(output)) + '\nexec ' +
2215
+ ' '.join(shlex.quote(str(value)) for value in (javac, '-cp', jar, '-d', output, source)) + '\n')
2216
+ wrapper.chmod(0o755)
2217
+ xml = """<FormMetadata><Key>sample_page</Key><BizAppNumber>sample_app</BizAppNumber>
2218
+ <Items><FormAp><Id>root</Id><Key>sample_page</Key></FormAp></Items></FormMetadata>"""
2219
+ monkeypatch.setattr(module, "read_platform", lambda *args, **kwargs: {"status": True, "data": xml})
2220
+ intent = {"form": "sample_page", "trigger": "open", "metadataNeeds": ["form"], "className": "PagePlugin",
2221
+ "coreLogic": ["initialize page"], "specialized": {"unresolvedDependencies": []}}
2222
+ monkeypatch.setattr(sys, "argv", ["prepare_plugin_handoff.py", "--app-root", str(app), "--project-root", str(project),
2223
+ "--form-number", "sample_page", "--type", "form", "--intent-json", json.dumps(intent), "--prepare-source"])
2224
+ module.main()
2225
+ prepared = json.loads(capsys.readouterr().out)
2226
+ assert prepared["status"] is True
2227
+ source.write_text("package kd.bos.app.sample.app.form; import kd.bos.form.plugin.AbstractFormPlugin; public class PagePlugin extends AbstractFormPlugin {}")
2228
+ monkeypatch.setattr(sys, "argv", ["compile_plugin_source.py", "--handoff", prepared["handoffPath"], "--timeout-sec", "20"])
2229
+ assert compiler.main() == 0
2230
+ result = json.loads(capsys.readouterr().out)
2231
+ assert result["compileSkipped"] is False
2232
+ assert result["compiledArtifact"]["fqcn"] == "kd.bos.app.sample.app.form.PagePlugin"
2233
+ assert json.loads((app / "archive.json").read_text())["javas"][0]["compileStatus"] == "passed"
2234
+
2235
+
2236
+ def test_page_and_entity_reads_run_independently_in_parallel(tmp_path, monkeypatch, capsys):
2237
+ import threading
2238
+ import prepare_plugin_handoff as module
2239
+ app = tmp_path / "app" / "sample_app"
2240
+ _write(app / "archive.json", {"appCode": "sample_app", "appId": "SAMPLE-ID"})
2241
+ project = _java_project(tmp_path)
2242
+ barrier = threading.Barrier(2)
2243
+ calls = []
2244
+ xml = "<FormMetadata><Key>sample_page</Key><BizAppNumber>sample_app</BizAppNumber><Items><FormAp><Id>root</Id><Key>sample_page</Key></FormAp></Items></FormMetadata>"
2245
+ def read(app_root, command, selector, flag="--formNumber"):
2246
+ calls.append(command)
2247
+ barrier.wait(timeout=5)
2248
+ if command == "getFormMetadata":
2249
+ return {"status": True, "data": xml}
2250
+ if command == "getEntityType":
2251
+ return {"status": True, "data": {"Name": "sample_page", "BizAppNumber": "sample_app",
2252
+ "Properties": [{"Name": "amount", "_Type_": "DecimalProp"}]}}
2253
+ raise AssertionError(command)
2254
+ monkeypatch.setattr(module, "read_platform", read)
2255
+ intent = {"form": "sample_page", "trigger": "change", "metadataNeeds": ["form", "entity"],
2256
+ "fields": {"amount": "amount"}, "coreLogic": ["update the amount"],
2257
+ "specialized": {"unresolvedDependencies": []}}
2258
+ monkeypatch.setattr(sys, "argv", ["prepare_plugin_handoff.py", "--app-root", str(app),
2259
+ "--project-root", str(project), "--form-number", "sample_page", "--type", "form",
2260
+ "--intent-json", json.dumps(intent)])
2261
+ module.main()
2262
+ result = json.loads(capsys.readouterr().out)
2263
+ saved = json.loads(Path(result["handoffPath"]).read_text())["handoff"]
2264
+ assert sorted(calls) == ["getEntityType", "getFormMetadata"]
2265
+ assert saved["uiContext"]["formNumber"] == "sample_page"
2266
+ assert saved["formGraph"]["entities"][0]["fields"][0]["fieldKey"] == "amount"
2267
+
2268
+
2269
+ def test_cli_without_selected_java_directory_stops_before_platform_reads(tmp_path, monkeypatch, capsys):
2270
+ import prepare_plugin_handoff as module
2271
+ app = tmp_path / "app"
2272
+ _write(app / "archive.json", {"appCode": "sample_app"})
2273
+ monkeypatch.setattr(module, "read_platform", lambda *args, **kwargs: pytest.fail("unexpected platform read"))
2274
+ monkeypatch.setattr(sys, "argv", ["prepare_plugin_handoff.py", "--app-root", str(app), "--form-number", "sample_form", "--type", "form", "--intent-json", json.dumps({"coreLogic": ["test"]})])
2275
+ with pytest.raises(SystemExit):
2276
+ module.main()
2277
+ result = json.loads(capsys.readouterr().out)
2278
+ assert result["errorCode"] == "PLUGIN_WORKSPACE_UNRESOLVED"
2279
+ assert "ask the user" in result["message"]
2280
+
2281
+
2282
+ def test_explicit_source_directory_is_used_exactly_and_cannot_escape_module(tmp_path):
2283
+ project = _java_project(tmp_path / "module")
2284
+ chosen = project / "src" / "main" / "java" / "example" / "custom"
2285
+ result = resolve_output_path(str(tmp_path / "app"), "billform", str(project), source_directory=str(chosen))
2286
+ assert result["packagePath"] == "example/custom"
2287
+ assert result["outputDir"] == "src/main/java/example/custom"
2288
+ outside = resolve_output_path(str(tmp_path / "app"), "billform", str(project), source_directory=str(tmp_path / "outside"))
2289
+ assert outside["errorCode"] == "PLUGIN_WORKSPACE_UNRESOLVED"
2290
+
2291
+
2292
+ def test_user_selected_package_is_prepared_without_project_search(tmp_path, monkeypatch, capsys):
2293
+ module, app, calls, intent_file = _runtime_preparation_cli(tmp_path, monkeypatch, _runtime_responses())
2294
+ chosen = tmp_path / "src" / "main" / "java" / "example" / "chosen"
2295
+ intent = json.loads(intent_file.read_text())
2296
+ intent["sourceDirectory"] = str(chosen)
2297
+ _write(intent_file, intent)
2298
+ argv = sys.argv.copy()
2299
+ index = argv.index("--project-root")
2300
+ del argv[index:index + 2]
2301
+ monkeypatch.setattr(sys, "argv", argv)
2302
+ original_rglob = Path.rglob
2303
+ def check_search(path, *args, **kwargs):
2304
+ if path == tmp_path or tmp_path in path.parents:
2305
+ pytest.fail("unexpected project search")
2306
+ return original_rglob(path, *args, **kwargs)
2307
+ monkeypatch.setattr(Path, "rglob", check_search)
2308
+ module.main()
2309
+ outcome = json.loads(capsys.readouterr().out)
2310
+ assert outcome["status"] is True
2311
+ assert Path(outcome["handoff"]["sourcePath"]).parent == chosen
2312
+ assert outcome["handoff"]["packagePath"] == "example/chosen"
2313
+
2314
+
2315
+ def test_integer_field_context_contains_sdk_accessor_and_nullable_boundary():
2316
+ from prepare_plugin_handoff import _field_contract
2317
+ value = _field_contract({"fieldKey": "quantity", "fieldName": "Quantity", "fieldType": "IntegerProp"})
2318
+ assert value["valueAccessor"] == 'getInt("quantity")'
2319
+ assert value["javaType"] == "Integer"
2320
+ assert "raw value" in value["note"]
2321
+
2322
+
2323
+ def test_selected_java_source_root_uses_convention_without_project_search(tmp_path, monkeypatch):
2324
+ project = _java_project(tmp_path / "module")
2325
+ app = tmp_path / "app" / "sample_demo"
2326
+ _write(app / "archive.json", {"appCode": "sample_demo"})
2327
+ monkeypatch.setattr(Path, "rglob", lambda *args, **kwargs: pytest.fail("unexpected project search"))
2328
+ result = resolve_output_path(str(app), "operation", str(project), source_directory=str(project / "src/main/java"))
2329
+ assert result["errorCode"] == "0"
2330
+ assert result["packagePath"] == "kd/bos/app/sample/demo/operate"