@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.
- package/package.json +1 -1
- package/skills/cosmic-build/LOCAL-PATCHES.md +43 -14
- package/skills/cosmic-build/SKILL.md +24 -9
- package/skills/cosmic-build/examples/java-rules/CosmicReportAggregation.java +26 -17
- package/skills/cosmic-build/examples/java-rules/CosmicReportFilters.java +96 -2
- package/skills/cosmic-build/knowledge/cosmic/arch/security/security_standard.md +4 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/allimport.md +2 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/core/dynamic-object-collection.md +6 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/core/model.md +9 -9
- package/skills/cosmic-build/knowledge/cosmic/platform/core/view.md +5 -5
- package/skills/cosmic-build/knowledge/cosmic/platform/form-plugin.md +8 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/list-plugin.md +5 -3
- package/skills/cosmic-build/knowledge/cosmic/platform/print-plugin-guide.md +1 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/report-plugin.md +269 -57
- package/skills/cosmic-build/knowledge/cosmic/platform/reportlistdata-plugin.md +130 -23
- package/skills/cosmic-build/knowledge/cosmic/platform/reporttreedata-plugin.md +48 -28
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/_index.md +12 -3
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/common/attachment-name-process.md +17 -13
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/common/dynamic-update-field.md +4 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/filter-grid/filter-grid-set-value.md +1 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-basedatafield.md +2 -3
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-container-init.md +0 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-popup.md +0 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/filter-value-parse.md +5 -6
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/list-show-form.md +1 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/control/list/query-field-dynamic.md +0 -7
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/business-data/delete-service.md +24 -29
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/ksql/db-batch.md +4 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/ksql/db-direct-access.md +4 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/qfilter/entity-object-access.md +8 -3
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/data-access/qfilter/qfilter-compose.md +6 -5
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/after-bind-data-set-visible.md +5 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/click-return-data.md +2 -2
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/closed-call-back-set-value.md +2 -2
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/register-listener.md +1 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/event/set-filter-tab-org.md +3 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/dispatch-service/microservice-dev.md +18 -11
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/dist-lock/dist-lock-basic.md +3 -6
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/mq/mq-subscribe.md +5 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/thread-pool/async-operation.md +10 -6
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/thread-pool/big-task-async.md +4 -2
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/infra/thread-pool/schedule-task.md +9 -7
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/operation-interactive.md +33 -34
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/operation/operation-plugin-events.md +30 -22
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-async-query.md +133 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-batch-query.md +26 -22
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-chart.md +112 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-dataset-assembly.md +200 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-head-filter.md +177 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-hyperlink-drill.md +216 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-list-data-query.md +12 -12
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-merge-column.md +44 -6
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-print-data.md +163 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-row-data-toolbar.md +188 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-subtotal-total.md +139 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-tree-data-query.md +108 -12
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/report/report-tree-list.md +181 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/system-service/report-plugin-interface.md +3 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-basedata-cascade-confirm.md +14 -11
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-before-item-validate.md +1 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-build-filter.md +1 -1
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-multi-level-filter.md +11 -11
- package/skills/cosmic-build/knowledge/cosmic/platform/scenes/tree/tree-list-refresh-node.md +22 -15
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-algo.md +2 -2
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-entity-model.md +3 -4
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-exception.md +9 -5
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-orm-access.md +4 -0
- package/skills/cosmic-build/knowledge/cosmic/platform/sdk/sdk-request-context.md +7 -3
- package/skills/cosmic-build/knowledge/cosmic/platform/standards.md +28 -3
- package/skills/cosmic-build/references/compile-guide.md +36 -1
- package/skills/cosmic-build/references/dcs-code-rules.md +180 -0
- package/skills/cosmic-build/references/delivery.md +16 -2
- package/skills/cosmic-build/references/execution-contract.md +15 -3
- package/skills/cosmic-build/references/java-plugin-delivery.md +21 -11
- package/skills/cosmic-build/references/plugin-handoff-input.md +82 -24
- package/skills/cosmic-build/references/plugin-runtime-routing.md +23 -6
- package/skills/cosmic-build/references/report-filter-contract.md +53 -15
- package/skills/cosmic-build/references/sdk-inspection.md +4 -1
- package/skills/cosmic-build/references/type-registry.json +26 -17
- package/skills/cosmic-build/schemas/data-model-authoring.schema.json +1 -1
- package/skills/cosmic-build/scripts/_http_client.py +3 -0
- package/skills/cosmic-build/scripts/compile_plugin_source.py +251 -42
- package/skills/cosmic-build/scripts/configure_list.py +112 -0
- package/skills/cosmic-build/scripts/dcs-rule-catalog.json +799 -0
- package/skills/cosmic-build/scripts/dcs_code_gate.py +1976 -0
- package/skills/cosmic-build/scripts/execution_binding.py +25 -0
- package/skills/cosmic-build/scripts/platform_api_contract.py +352 -7
- package/skills/cosmic-build/scripts/plugin_source_gate.py +9 -1
- package/skills/cosmic-build/scripts/plugin_target_sdk.py +12 -8
- package/skills/cosmic-build/scripts/prepare_plugin_handoff.py +488 -88
- package/skills/cosmic-build/scripts/query_plugin_handoff.py +207 -20
- package/skills/cosmic-build/scripts/register_plugins.py +27 -10
- package/skills/cosmic-build/scripts/render_output_cards.py +57 -20
- package/skills/cosmic-build/scripts/resolve_output_path.py +60 -26
- package/skills/cosmic-build/scripts/run_java_tests.py +162 -0
- package/skills/cosmic-build/scripts/sync_generated_locale_assets.py +28 -0
- package/skills/cosmic-build/scripts/test/fixtures/ReportSdkCheck.java +1 -22
- package/skills/cosmic-build/scripts/test/fixtures/list_column_property_declarations.json +887 -0
- package/skills/cosmic-build/scripts/test/fixtures/operation-entry-field-prepare.java +78 -0
- package/skills/cosmic-build/scripts/test/test_add_field_required_contract.py +12 -0
- package/skills/cosmic-build/scripts/test/test_batch5_drift_governance.py +23 -2
- package/skills/cosmic-build/scripts/test/test_compile_plugin_source.py +287 -4
- package/skills/cosmic-build/scripts/test/test_configure_list_delivery.py +132 -0
- package/skills/cosmic-build/scripts/test/test_cosmic_sdk_examples.py +58 -1
- package/skills/cosmic-build/scripts/test/test_data_model_contract.py +19 -0
- package/skills/cosmic-build/scripts/test/test_dcs_code_gate.py +901 -0
- package/skills/cosmic-build/scripts/test/test_delivery_stage_boundary.py +1 -1
- package/skills/cosmic-build/scripts/test/test_direct_cli_architecture.py +2 -0
- package/skills/cosmic-build/scripts/test/test_http_client.py +6 -0
- package/skills/cosmic-build/scripts/test/test_java_toolchain_feedback.py +206 -0
- package/skills/cosmic-build/scripts/test/test_kd_api.py +21 -0
- package/skills/cosmic-build/scripts/test/test_list_configuration_closure.py +184 -0
- package/skills/cosmic-build/scripts/test/test_mcp_cli.py +139 -2
- package/skills/cosmic-build/scripts/test/test_menu_creation_closure.py +16 -0
- package/skills/cosmic-build/scripts/test/test_modeling_input_recovery.py +55 -1
- package/skills/cosmic-build/scripts/test/test_open_world_cli_catalog.py +1 -1
- package/skills/cosmic-build/scripts/test/test_operation_field_preparation.py +84 -0
- package/skills/cosmic-build/scripts/test/test_platform_api_contract.py +322 -1
- package/skills/cosmic-build/scripts/test/test_plugin_handoff.py +951 -4
- package/skills/cosmic-build/scripts/test/test_plugin_source_gate.py +30 -2
- package/skills/cosmic-build/scripts/test/test_plugin_target_sdk.py +21 -0
- package/skills/cosmic-build/scripts/test/test_property_reroute.py +165 -0
- package/skills/cosmic-build/scripts/test/test_property_schema_first.py +35 -0
- package/skills/cosmic-build/scripts/test/test_query_plugin_handoff.py +221 -0
- package/skills/cosmic-build/scripts/test/test_register_plugins.py +76 -4
- package/skills/cosmic-build/scripts/test/test_render_output_cards.py +75 -3
- package/skills/cosmic-build/scripts/test/test_report_business_contract.py +27 -5
- package/skills/cosmic-build/scripts/test/test_report_doc_contract_sync.py +103 -11
- package/skills/cosmic-build/scripts/test/test_report_plugin_source_gate.py +46 -16
- package/skills/cosmic-build/scripts/test/test_report_properties.py +74 -15
- package/skills/cosmic-build/scripts/test/test_report_query_data_source.py +3 -1
- package/skills/cosmic-build/scripts/test/test_report_sdk_optimization.py +1 -2
- package/skills/cosmic-build/scripts/test/test_test_environment.py +76 -1
- package/skills/cosmic-build/scripts/test_environment.py +67 -1
- package/skills/cosmic-build/skills/cosmic-coding-impl/SKILL.md +52 -10
- package/skills/cosmic-build/skills/cosmic-meta-api/apis/ai_meta.md +16 -3
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/report-write-contracts.json +59 -26
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/cli/rule-domain.json +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/contracts/schema/report-write.schema.json +75 -18
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/scripts/pdm_auto.py +39 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/pdm/scripts/pdm_client.py +97 -10
- package/skills/cosmic-build/skills/cosmic-meta-api/modules/verify/SKILL.md +10 -12
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_apps.py +3 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_formactions.py +38 -29
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_listui.py +400 -233
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_metaops.py +87 -45
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_properties.py +42 -15
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/cmd_rules.py +135 -5
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/config.py +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/execution_errors.py +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/formatters.py +17 -11
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/property_reroute.py +15 -5
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/report_ops.py +1429 -238
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/_core/validators.py +56 -28
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/cosmic_api.py +380 -134
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/field_decorator.py +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/meta_export.py +12 -11
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/meta_verifier.py +42 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_add_field_placement_closure.py +8 -8
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_build_configuration_readback.py +94 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_build_receipt_resume.py +47 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_metadata_unchanged_fail_closed.py +87 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_dym_evidence.py +23 -23
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_entity_integrity.py +279 -4
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_field_guard.py +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_plugin_contract.py +509 -32
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_readback_derivation.py +178 -1
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_recreate_plan.py +125 -10
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_serve_contract.py +208 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_report_typed_filters.py +259 -50
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_rule_inventory_perf.py +29 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_rule_write_closure.py +20 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_post_build_recovery.py +18 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_batch_verification_evidence.py +35 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_run_identity_guard.py +25 -0
- package/skills/cosmic-build/skills/cosmic-meta-api/scripts/test/test_time_range_rule.py +98 -0
- package/skills/cosmic-build/skills/cosmic-meta-knowledge/domain-model/report-model.md +20 -9
- package/skills/cosmic-build/skills/cosmic-meta-knowledge/generation/report-modeling-guide.md +55 -25
- package/skills/cosmic-build/skills/cosmic-meta-knowledge/property-guides/report-column-properties.md +15 -39
- package/skills/cosmic-build/skills/cosmic-meta-modeling/SKILL.md +19 -17
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/SKILL.md +82 -38
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-batch.md +64 -19
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-plugin.md +32 -20
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-tree.md +78 -31
- package/skills/cosmic-build/skills/cosmic-meta-modeling/modules/report/report-artifact-schema.json +2 -1
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-companion-fields.md +3 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/references/authoring-list-configuration.md +2 -0
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_contract.py +3 -1
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/data_model_emit.py +27 -4
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/report_validator.py +99 -44
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/test_report_examples.py +31 -4
- package/skills/cosmic-build/skills/cosmic-meta-modeling/scripts/test_report_validator.py +129 -29
- package/skills/cosmic-build/skills/cosmic-meta-modifier/SKILL.md +6 -2
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/operation/validation.md +2 -0
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/property/SKILL.md +1 -1
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/report-op/SKILL.md +104 -25
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/rule/SKILL.md +9 -0
- package/skills/cosmic-build/skills/cosmic-meta-modifier/modules/verify/usage-guide.md +2 -2
- package/skills/cosmic-build/skills/cosmic-meta-modifier/references/list-filter-modification.md +39 -12
- package/skills/cosmic-build/skills/mcp-server/SKILL.md +39 -24
- package/skills/cosmic-build/skills/mcp-server/scripts/mcp_cli.py +20 -6
- package/skills/cosmic-dev-best-practices/SKILL.md +1 -1
- package/skills/cosmic-dev-best-practices/references/README.md +2 -2
- package/skills/cosmic-dev-best-practices/references/basedata-dev-convention.md +2 -2
- package/skills/cosmic-dev-best-practices/references/bill-dev-convention.md +4 -4
- package/skills/cosmic-dev-best-practices/references/design-document-convention.md +2 -2
- package/skills/cosmic-dev-best-practices/references/design-examples/basedata-design-example.md +4 -4
- package/skills/cosmic-dev-best-practices/references/design-examples/bill-design-example.md +13 -13
- package/skills/cosmic-dev-best-practices/references/elements/CustomerField.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/README.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/SupplierField.md +1 -1
- package/skills/cosmic-dev-best-practices/references/elements/common-field-property-conventions.md +109 -13
- package/skills/cosmic-dev-best-practices/references/layout-control-convention.md +1 -1
- package/skills/erp-analysis/references/delivery-contract.md +2 -1
- package/skills/erp-analysis/templates/AGENTS.template.md +3 -1
- package/skills/cosmic-build/assets/platform-report-filters/TypedReportFilterFields.java +0 -169
- package/skills/cosmic-build/assets/platform-report-filters/platform.patch +0 -98
- 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"
|