@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
|
@@ -6,7 +6,35 @@ from pathlib import Path
|
|
|
6
6
|
SCRIPTS = Path(__file__).resolve().parents[1]
|
|
7
7
|
sys.path.insert(0, str(SCRIPTS))
|
|
8
8
|
|
|
9
|
-
from plugin_source_gate import gate_path, read_state, write_state # noqa: E402
|
|
9
|
+
from plugin_source_gate import gate_path, read_state, write_state, validate_registration_entry # noqa: E402
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_registration_rechecks_legacy_operation_receipts(tmp_path):
|
|
13
|
+
import hashlib
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
source = tmp_path / "DeleteFlock.java"
|
|
17
|
+
for call, status, error in (
|
|
18
|
+
('"demo_flock", "delete", ids, option', "passed", "PLUGIN_SOURCE_OPERATION_ARGUMENT_ORDER"),
|
|
19
|
+
('"demo_flock", "delete", ids, option', "skipped-by-config", "PLUGIN_SOURCE_OPERATION_ARGUMENT_ORDER"),
|
|
20
|
+
('"delete", "demo_flock", ids', "skipped-by-config", "PLUGIN_SOURCE_OPERATION_OVERLOAD_UNVERIFIED"),
|
|
21
|
+
('"delete", "demo_flock", ids, option', "skipped-by-config", None),
|
|
22
|
+
# 未收录重载但已有实际编译证据,不凭文档缺失否认它。
|
|
23
|
+
('"delete", "demo_flock", ids', "passed", None),
|
|
24
|
+
):
|
|
25
|
+
source.write_text('''import kd.bos.entity.plugin.AbstractOperationServicePlugIn;
|
|
26
|
+
import kd.bos.servicehelper.operation.OperationServiceHelper;
|
|
27
|
+
class DeleteFlock extends AbstractOperationServicePlugIn {
|
|
28
|
+
void run() { OperationServiceHelper.executeOperate(''' + call + '''); }
|
|
29
|
+
}''', encoding="utf-8")
|
|
30
|
+
entry = {"className": "DeleteFlock", "type": "operation", "formNumber": "demo_flock",
|
|
31
|
+
"filePath": str(source.resolve()), "compileStatus": status,
|
|
32
|
+
"sourceHash": hashlib.sha256(source.read_bytes()).hexdigest()}
|
|
33
|
+
if error:
|
|
34
|
+
with pytest.raises(ValueError, match=error):
|
|
35
|
+
validate_registration_entry(entry, "OPERATION")
|
|
36
|
+
continue
|
|
37
|
+
assert validate_registration_entry(entry, "OPERATION")["compileStatus"] == status
|
|
10
38
|
|
|
11
39
|
|
|
12
40
|
def test_gate_advances_from_coding_to_source_ready_without_consuming_proof(tmp_path, capsys):
|
|
@@ -21,7 +49,7 @@ def test_gate_advances_from_coding_to_source_ready_without_consuming_proof(tmp_p
|
|
|
21
49
|
read_state(project_root, "CouponPlugin")
|
|
22
50
|
result = json.loads(capsys.readouterr().out.splitlines()[-1])
|
|
23
51
|
|
|
24
|
-
assert result == {"state": "source_ready", "verdict": "
|
|
52
|
+
assert result == {"state": "source_ready", "verdict": "source_ready_only"}
|
|
25
53
|
assert Path(gate_path(project_root, "CouponPlugin")).exists()
|
|
26
54
|
|
|
27
55
|
|
|
@@ -68,3 +68,24 @@ def test_unmapped_module_and_explicit_source_only_are_distinct(target):
|
|
|
68
68
|
result = verify_target_sdk(str(project), {})
|
|
69
69
|
assert result['status'] == 'not-run'
|
|
70
70
|
assert 'unverified' in result['evidenceScope']
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_source_only_still_validates_sdk_usage_shape(tmp_path, monkeypatch):
|
|
74
|
+
config = tmp_path / 'appconfig.json'
|
|
75
|
+
config.write_text('{"compile":false}')
|
|
76
|
+
monkeypatch.setenv('KD_APPCONFIG', str(config))
|
|
77
|
+
with pytest.raises(TargetSdkError) as error:
|
|
78
|
+
verify_target_sdk(str(tmp_path), {}, [{'class': 'kd.bos.entity.report.ReportColumn', 'members': 'getFieldKey'}])
|
|
79
|
+
assert error.value.error_code == 'PLUGIN_SDK_USAGE_INVALID'
|
|
80
|
+
result = verify_target_sdk(str(tmp_path), {}, [{'class': 'kd.bos.entity.report.ReportColumn'}])
|
|
81
|
+
assert result['status'] == 'not-run' and result['compile'] is False
|
|
82
|
+
assert 'classes' not in result
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_unselected_guide_parameter_does_not_block_target_sdk(target):
|
|
86
|
+
result = verify_target_sdk(str(target[0]), {
|
|
87
|
+
"baseContract": {"class": "kd.bos.probe.Probe"},
|
|
88
|
+
"signatureTypes": [{"class": "kd.bos.probe.UnusedLifecycleEvent"}],
|
|
89
|
+
})
|
|
90
|
+
assert result["status"] == "verified"
|
|
91
|
+
assert [item["class"] for item in result["classes"]] == ["kd.bos.probe.Probe"]
|
|
@@ -124,3 +124,168 @@ def test_contract_load_failure_is_loud() -> None:
|
|
|
124
124
|
raise AssertionError("契约缺失时应当抛错,而不是返回空表")
|
|
125
125
|
finally:
|
|
126
126
|
pr._PROPERTY_NAMES, pr._CONTRACT = saved_names, saved_contract
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_known_and_unknown_properties_do_not_partially_reroute():
|
|
130
|
+
op = _entity_modify(value={"mustInput": True, "notAProperty": 1})
|
|
131
|
+
structural, calls, unresolved = plan_property_reroute([op])
|
|
132
|
+
assert structural == [op] and calls == [] and unresolved
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_empty_property_map_is_unresolved():
|
|
136
|
+
assert plan_property_reroute([_entity_modify(value={})])[2]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_modify_preflight_never_writes_a_partial_request(monkeypatch):
|
|
140
|
+
import pytest
|
|
141
|
+
from _core import cmd_metaops
|
|
142
|
+
writes = []
|
|
143
|
+
monkeypatch.setattr(cmd_metaops, "_api_call", lambda *a, **k: writes.append(a))
|
|
144
|
+
monkeypatch.setattr(cmd_metaops, "do_set_properties_verified", lambda *a, **k: writes.append(a))
|
|
145
|
+
for ops, code in [
|
|
146
|
+
([_entity_modify(value={"mustInput": True, "unknown": 1})], "PROPERTY_REROUTE_UNRESOLVED"),
|
|
147
|
+
([_entity_modify(path="mustInput", value=True),
|
|
148
|
+
{"op": "add", "target": {"treeType": "form", "elementType": "control", "value": "p"},
|
|
149
|
+
"value": {"name": "Panel", "containerType": "FlexPanelAp"}}], "validation failed"),
|
|
150
|
+
([_entity_modify(path="mustInput", value=True),
|
|
151
|
+
{"op": "remove", "target": {"treeType": "entity", "elementType": "field", "value": "f1"}}],
|
|
152
|
+
"NON_ATOMIC_PROPERTY_OP"),
|
|
153
|
+
]:
|
|
154
|
+
with pytest.raises(ValueError, match=code):
|
|
155
|
+
cmd_metaops.do_modify_meta({}, "token", "FID", ops, "demo")
|
|
156
|
+
with pytest.raises(ValueError, match="PROPERTY_REROUTE_FORM_REQUIRED"):
|
|
157
|
+
cmd_metaops.do_modify_meta({}, "token", "FID", [_entity_modify(path="mustInput", value=True)])
|
|
158
|
+
assert writes == []
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_modify_property_failure_stops_and_is_not_reported_as_success(monkeypatch):
|
|
162
|
+
from _core import cmd_metaops
|
|
163
|
+
writes = []
|
|
164
|
+
def write(*args):
|
|
165
|
+
writes.append(args)
|
|
166
|
+
return {"status": False, "errorCode": "FAIL", "verification": {"verified": False}}
|
|
167
|
+
monkeypatch.setattr(cmd_metaops, "do_set_properties_verified", write)
|
|
168
|
+
result = cmd_metaops.do_modify_meta({}, "token", "FID", [
|
|
169
|
+
_entity_modify(path="mustInput", value=True),
|
|
170
|
+
_entity_modify(key="f2", path="name", value="名称"),
|
|
171
|
+
], "demo")
|
|
172
|
+
assert result["status"] is False and result["errorCode"] == "PROPERTY_REROUTE_INCOMPLETE"
|
|
173
|
+
assert len(writes) == 1 and result["data"]["pending"][0]["elementKey"] == "f2"
|
|
174
|
+
assert result["retryAdvice"] == "reconcile_before_retry"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_property_exception_retains_prior_receipts_and_stops(monkeypatch):
|
|
178
|
+
from _core import cmd_metaops
|
|
179
|
+
writes = []
|
|
180
|
+
def write(*args):
|
|
181
|
+
writes.append(args)
|
|
182
|
+
if len(writes) == 2:
|
|
183
|
+
raise RuntimeError("readback unavailable after dispatch")
|
|
184
|
+
return {"status": True, "errorCode": "0", "verification": {"verified": True}}
|
|
185
|
+
monkeypatch.setattr(cmd_metaops, "do_set_properties_verified", write)
|
|
186
|
+
result = cmd_metaops.do_modify_meta({}, "token", "FID", [
|
|
187
|
+
_entity_modify(key=key, path="mustInput", value=True) for key in ("f1", "f2", "f3")], "demo")
|
|
188
|
+
assert result["status"] is False and len(writes) == 2
|
|
189
|
+
assert result["data"]["rerouted"][0]["result"]["verification"]["verified"]
|
|
190
|
+
assert result["data"]["rerouted"][1]["result"]["errorCode"] == "PROPERTY_REROUTE_EXCEPTION"
|
|
191
|
+
assert result["data"]["pending"][0]["elementKey"] == "f3"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_property_success_requires_independent_verification(monkeypatch):
|
|
195
|
+
from _core import cmd_metaops
|
|
196
|
+
for verified in (False, True):
|
|
197
|
+
monkeypatch.setattr(cmd_metaops, "do_set_properties_verified", lambda *a, **k: {
|
|
198
|
+
"status": True, "errorCode": "0", "verification": {"verified": verified}})
|
|
199
|
+
result = cmd_metaops.do_modify_meta({}, "token", "FID", [
|
|
200
|
+
_entity_modify(path="mustInput", value=True)], "demo")
|
|
201
|
+
assert result["status"] is verified
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_combo_null_values_and_boolean_add_contract():
|
|
205
|
+
from _core.validators import _validate_ops
|
|
206
|
+
for value in (None, [None], [{"value": None, "caption": "选项"}], [{"caption": "选项"}]):
|
|
207
|
+
assert _validate_ops([_entity_modify(path="comboItems.add", value=value)])
|
|
208
|
+
assert _validate_ops([_entity_modify(path="comboItems.remove", value=[None])])
|
|
209
|
+
assert _validate_ops([_entity_modify(path="comboItems.add", value=[
|
|
210
|
+
{"value": 0, "caption": "零"}, {"value": "01", "caption": "一"}])]) == []
|
|
211
|
+
op = {"op": "add", "target": {"treeType": "entity", "elementType": "field", "value": "amount"},
|
|
212
|
+
"value": {"fieldType": "DecimalField", "bizWord": "amount", "name": "金额", "mustInput": True}}
|
|
213
|
+
assert _validate_ops([op]) == []
|
|
214
|
+
assert _validate_ops([{**op, "value": {**op["value"], "mustInput": "true"}}])
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_combo_create_validates_every_supplied_alias_and_effective_label():
|
|
218
|
+
from _core.validators import _validate_ops
|
|
219
|
+
op = {"op": "add", "target": {"treeType": "entity", "elementType": "field", "value": "mode"},
|
|
220
|
+
"value": {"fieldType": "ComboField", "bizWord": "mode", "name": "模式"}}
|
|
221
|
+
rows = [{"value": 0, "caption": "零"}, {"value": "01", "label": "一"}]
|
|
222
|
+
for options in ({"items": rows}, {"comboOptions": rows}, {"items": rows, "comboOptions": rows}):
|
|
223
|
+
assert _validate_ops([{**op, "value": {**op["value"], **options}}]) == []
|
|
224
|
+
for options in (
|
|
225
|
+
{}, {"items": None}, {"items": []}, {"items": {"value": "a"}},
|
|
226
|
+
{"items": [None]}, {"items": [{"value": None, "caption": "空"}]},
|
|
227
|
+
{"items": rows, "comboOptions": [{"value": "a", "caption": "甲"}]},
|
|
228
|
+
{"items": None, "comboOptions": rows}, {"items": rows, "comboOptions": None},
|
|
229
|
+
{"items": [{"value": "a", "label": " ", "caption": "不会被使用"}]},
|
|
230
|
+
{"items": [{"value": "a", "label": 1, "caption": "不会被使用"}]},
|
|
231
|
+
{"items": [{"value": 0, "caption": "零"}, {"value": "0", "caption": "重复"}]},
|
|
232
|
+
):
|
|
233
|
+
assert _validate_ops([{**op, "value": {**op["value"], **options}}]), options
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_combo_modify_aliases_use_only_backend_supported_shapes():
|
|
237
|
+
from _core.validators import _validate_ops
|
|
238
|
+
for prefix in ("comboItems", "comboOptions"):
|
|
239
|
+
for path in (prefix, prefix + ".add"):
|
|
240
|
+
for rows in ([{"value": "01", "caption": "一"}], [{"value": 0, "label": None, "caption": "零"}]):
|
|
241
|
+
assert _validate_ops([_entity_modify(path=path, value=rows)]) == []
|
|
242
|
+
for rows in (None, "a", [None], [0], [{"value": None, "caption": "空"}],
|
|
243
|
+
[{"value": "a", "label": "", "caption": "不会被使用"}],
|
|
244
|
+
[{"value": "a", "caption": "甲"}, {"value": "a", "caption": "重复"}]):
|
|
245
|
+
assert _validate_ops([_entity_modify(path=path, value=rows)]), (path, rows)
|
|
246
|
+
for rows in ("01", ["01", "0"], [{"value": 0}, {"value": "01"}]):
|
|
247
|
+
assert _validate_ops([_entity_modify(path=prefix + ".remove", value=rows)]) == []
|
|
248
|
+
for rows in (None, [None], [0], [False], [{"value": None}], [{"value": ""}], ["a", "a"]):
|
|
249
|
+
assert _validate_ops([_entity_modify(path=prefix + ".remove", value=rows)]), rows
|
|
250
|
+
for path in (prefix + ".unknown", prefix + ".add.extra", prefix + "."):
|
|
251
|
+
assert _validate_ops([_entity_modify(path=path, value=[])]), path
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_invalid_modify_paths_and_combo_inputs_never_dispatch(monkeypatch):
|
|
255
|
+
import pytest
|
|
256
|
+
from _core import cmd_metaops
|
|
257
|
+
from _core.validators import _validate_ops
|
|
258
|
+
writes = []
|
|
259
|
+
monkeypatch.setattr(cmd_metaops, "_api_call", lambda *a, **k: writes.append(a))
|
|
260
|
+
monkeypatch.setattr(cmd_metaops, "do_set_properties_verified", lambda *a, **k: writes.append(a))
|
|
261
|
+
for path in (False, 0, 1, [], {}, "", " "):
|
|
262
|
+
op = _entity_modify(path=path, value={"mustInput": True})
|
|
263
|
+
structural, calls, unresolved = plan_property_reroute([op])
|
|
264
|
+
assert structural == [op] and calls == [] and unresolved
|
|
265
|
+
form_op = {**op, "target": {**op["target"], "treeType": "form"}}
|
|
266
|
+
assert _validate_ops([form_op])
|
|
267
|
+
with pytest.raises(ValueError, match="validation failed"):
|
|
268
|
+
cmd_metaops.do_modify_meta({}, "token", "FID", [form_op], "demo")
|
|
269
|
+
with pytest.raises(ValueError, match="PROPERTY_REROUTE_UNRESOLVED"):
|
|
270
|
+
cmd_metaops.do_modify_meta({}, "token", "FID", [op], "demo")
|
|
271
|
+
for prefix in ("comboItems", "comboOptions"):
|
|
272
|
+
for suffix, value in (("", None), (".add", [None]), (".remove", [0]), (".unknown", [])):
|
|
273
|
+
with pytest.raises(ValueError, match="validation failed"):
|
|
274
|
+
cmd_metaops.do_modify_meta({}, "token", "FID", [
|
|
275
|
+
_entity_modify(path=prefix + suffix, value=value)], "demo")
|
|
276
|
+
op = {"op": "add", "target": {"treeType": "entity", "elementType": "field", "value": "mode"},
|
|
277
|
+
"value": {"fieldType": "ComboField", "name": "模式", "bizWord": "mode",
|
|
278
|
+
"items": None, "comboOptions": [{"value": "a", "caption": "甲"}]}}
|
|
279
|
+
with pytest.raises(ValueError, match="validation failed"):
|
|
280
|
+
cmd_metaops.do_modify_meta({}, "token", "FID", [op], "demo")
|
|
281
|
+
assert writes == []
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def test_verify_guidance_requires_reconciliation_not_write_replay():
|
|
285
|
+
guide = (APP_BUILD / "skills/cosmic-meta-api/modules/verify/SKILL.md").read_text(encoding="utf-8")
|
|
286
|
+
assert "reconcile_before_retry" in guide
|
|
287
|
+
assert "只重试验证读取,禁止重放写入" in guide
|
|
288
|
+
assert "不得重放整个 artifact / ops 数组" in guide
|
|
289
|
+
assert "WARN 不能证明本次写入未发生" in guide
|
|
290
|
+
assert "自动切换到建议的替代 API 重新执行" not in guide
|
|
291
|
+
assert "automatically switch to the suggested alternative API, re-execute" not in guide
|
|
@@ -997,3 +997,38 @@ def test_element_resolver_collapses_field_and_fieldap_with_same_key() -> None:
|
|
|
997
997
|
{"key": "uhyl_text_field", "name": "文本", "type": "FieldAp"},
|
|
998
998
|
]
|
|
999
999
|
assert MODULE.resolve_element_rows(rows, "文本", "demo") == "uhyl_text_field"
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
|
|
1003
|
+
def test_currency_binding_requires_live_candidates_before_write() -> None:
|
|
1004
|
+
schema = {"status": True, "data": [{"propertyName": "CurrencyFieldId", "editorType": "ecombo", "complexity": "simple", "editable": True}]}
|
|
1005
|
+
with (patch.object(MODULE, "do_get_property_schema", return_value=schema),
|
|
1006
|
+
patch.object(MODULE, "do_set_properties") as setter):
|
|
1007
|
+
result = MODULE.do_set_properties_verified({}, "token", "demo", "amount", {"CurrencyFieldId": "币别"})
|
|
1008
|
+
assert result["status"] is False
|
|
1009
|
+
assert result["errorCode"] == "PROPERTY_VALUE_INVALID"
|
|
1010
|
+
assert "candidates are unavailable" in result["data"]["errors"][0]["message"]
|
|
1011
|
+
setter.assert_not_called()
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
def test_currency_binding_rejects_ambiguous_live_option_names() -> None:
|
|
1015
|
+
declaration = {"propertyName": "CurrencyFieldId", "editorType": "ecombo", "complexity": "simple", "options": [
|
|
1016
|
+
{"id": "a", "name": "币别"}, {"id": "b", "name": "币别"},
|
|
1017
|
+
]}
|
|
1018
|
+
compiled, _, rejection = MODULE._compile_simple_property_map([declaration], {"CurrencyFieldId": "币别"})
|
|
1019
|
+
assert compiled is None
|
|
1020
|
+
assert rejection["errorCode"] == "PROPERTY_VALUE_INVALID"
|
|
1021
|
+
assert "ambiguous" in rejection["data"]["errors"][0]["message"]
|
|
1022
|
+
assert MODULE._normalize_simple_property_value(declaration, "a") == "a"
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def test_field_selector_resolves_declared_child_scope_without_options() -> None:
|
|
1026
|
+
schema = {"status": True, "data": [{"propertyName": "CurrencyFieldId", "editorType": "ecombo", "complexity": "simple", "editable": True, "constraints": {"scope": "child", "types": ["CurrencyField"]}}]}
|
|
1027
|
+
rows = {"status": True, "data": [{"category": "field", "key": "currency", "id": "currency-id", "type": "CurrencyField", "parentKey": "amount"}]}
|
|
1028
|
+
with (patch.object(MODULE, "do_get_property_schema", return_value=schema),
|
|
1029
|
+
patch.object(MODULE, "do_list_elements", return_value=rows),
|
|
1030
|
+
patch.object(MODULE, "do_set_properties", return_value={"status": True}) as setter,
|
|
1031
|
+
patch.object(MODULE, "do_get_properties", return_value={"status": True, "data": {"CurrencyFieldId": "currency-id"}})):
|
|
1032
|
+
result = MODULE.do_set_properties_verified({}, "token", "demo", "amount", {"CurrencyFieldId": "currency"})
|
|
1033
|
+
assert result["verification"]["verified"]
|
|
1034
|
+
assert setter.call_args.args[-1] == {"CurrencyFieldId": "currency-id"}
|
|
@@ -6,6 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
SCRIPT = Path(__file__).resolve().parents[1] / "query_plugin_handoff.py"
|
|
9
|
+
sys.path.insert(0, str(SCRIPT.parent))
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def handoff_file(tmp_path):
|
|
@@ -59,6 +60,44 @@ def test_target_readiness_is_bounded_and_missing_evidence_is_not_claimed(tmp_pat
|
|
|
59
60
|
assert 'classes' not in context['targetSdk'] and 'large-evidence-' not in output
|
|
60
61
|
|
|
61
62
|
|
|
63
|
+
def test_coding_projection_reads_gate_without_repreparing_or_mutating_snapshot(tmp_path):
|
|
64
|
+
file, document = handoff_file(tmp_path)
|
|
65
|
+
gate = tmp_path / ".temp" / ".BookingPlugin-gate"
|
|
66
|
+
document["handoff"].update({"projectRoot": str(tmp_path), "className": "BookingPlugin",
|
|
67
|
+
"sourceGate": {"requestedState": "coding", "path": str(gate)}})
|
|
68
|
+
file.write_text(json.dumps(document), encoding="utf-8")
|
|
69
|
+
original = file.read_bytes()
|
|
70
|
+
for content, expected, verified in [(None, "missing", False), ("coding:session-123", "coding", True),
|
|
71
|
+
("source_ready", "source_ready", True), ("invalid", "invalid", False)]:
|
|
72
|
+
if content is not None:
|
|
73
|
+
gate.parent.mkdir(exist_ok=True)
|
|
74
|
+
gate.write_text(content, encoding="utf-8")
|
|
75
|
+
for operation in ("coding-start", "coding-context"):
|
|
76
|
+
code, context, _ = query(file, operation)
|
|
77
|
+
assert code == 0
|
|
78
|
+
assert context["sourceGate"]["requestedState"] == "coding"
|
|
79
|
+
assert context["sourceGate"]["path"] == str(gate)
|
|
80
|
+
assert context["sourceGate"]["state"] == expected
|
|
81
|
+
assert context["sourceGate"]["verified"] == verified
|
|
82
|
+
assert context["sourceGate"]["sessionId"] == ("session-123" if content == "coding:session-123" else None)
|
|
83
|
+
assert "not a compile receipt" in context["sourceGate"]["evidenceScope"]
|
|
84
|
+
assert file.read_bytes() == original
|
|
85
|
+
assert (gate.read_text() if gate.exists() else None) == content
|
|
86
|
+
gate.write_bytes(b"\xff")
|
|
87
|
+
code, context, _ = query(file, "coding-start")
|
|
88
|
+
assert code == 2 and context["errorCode"] == "HANDOFF_SOURCE_GATE_UNAVAILABLE"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_coding_projection_rejects_gate_for_another_class(tmp_path):
|
|
92
|
+
file, document = handoff_file(tmp_path)
|
|
93
|
+
document["handoff"].update({"projectRoot": str(tmp_path), "className": "BookingPlugin",
|
|
94
|
+
"sourceGate": {"requestedState": "coding", "path": str(tmp_path / ".temp" / ".OtherPlugin-gate")}})
|
|
95
|
+
file.write_text(json.dumps(document), encoding="utf-8")
|
|
96
|
+
for operation in ("coding-start", "coding-context"):
|
|
97
|
+
code, context, _ = query(file, operation)
|
|
98
|
+
assert code == 2 and context["errorCode"] == "HANDOFF_SOURCE_GATE_INVALID"
|
|
99
|
+
|
|
100
|
+
|
|
62
101
|
def test_index_find_and_get_return_only_requested_native_context(tmp_path):
|
|
63
102
|
file, document = handoff_file(tmp_path)
|
|
64
103
|
original = hashlib.sha256(file.read_bytes()).hexdigest()
|
|
@@ -158,3 +197,185 @@ def test_large_startup_inventory_is_explicitly_referenced_instead_of_dumped(tmp_
|
|
|
158
197
|
assert result["entities"][0]["fieldCount"] == 1000
|
|
159
198
|
assert result["entities"][0]["fieldsPath"] == "/handoff/formGraph/entities/0/fields"
|
|
160
199
|
assert "fields" not in result["entities"][0]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_report_context_uses_real_contract_without_fabricating_form_graph(tmp_path):
|
|
203
|
+
from prepare_plugin_handoff import _compile_report_contract
|
|
204
|
+
file, document = handoff_file(tmp_path)
|
|
205
|
+
document['handoff'].pop('formGraph')
|
|
206
|
+
document['handoff'].update(type='reportlist', formNumber='sample_rpt',
|
|
207
|
+
reportContract=_compile_report_contract({'reportColumns': [
|
|
208
|
+
{'kind': 'column', 'fieldKey': 'person_id', 'dateIndex': 'person_id', 'fieldType': 'bigint',
|
|
209
|
+
'caption': '人员 / Person', 'presentation': {'kind': 'basedata', 'entityId': 'bos_user', 'displayProp': 'name'}}],
|
|
210
|
+
'reportFilters': [{'fieldPath': 'flt_period', 'displayName': '期间 / Period', 'compare': '='}],
|
|
211
|
+
'reportSorts': [{'fieldKey': 'person_id', 'direction': 'desc'}],
|
|
212
|
+
'reportPlugins': [{'className': 'kd.test.report.P', 'type': 'Java'}]}, 'sample_rpt'))
|
|
213
|
+
file.write_text(json.dumps(document, ensure_ascii=False))
|
|
214
|
+
for operation in ('coding-start', 'coding-context'):
|
|
215
|
+
code, context, stdout = query(file, operation)
|
|
216
|
+
assert code == 0 and context['domain'] == 'report'
|
|
217
|
+
assert 'formGraph' not in context and 'large-evidence-' not in stdout
|
|
218
|
+
assert context['reportContract']['dataSetColumnTypes'][0]['algoDataType'] == 'DataType.LongType'
|
|
219
|
+
assert context['reportContract']['columnPresentation'][0]['entityId'] == 'bos_user'
|
|
220
|
+
assert context['reportContract']['filterValueContracts'][0]['filterKey'] == 'flt_period'
|
|
221
|
+
assert context['reportContract']['defaultSorts'] == [
|
|
222
|
+
{'fieldKey': 'person_id', 'dateIndex': 'person_id', 'direction': 'desc'}]
|
|
223
|
+
assert context['reportContract']['defaultSortPolicy']['priority'] == 'interactiveThenDefault'
|
|
224
|
+
assert context['reportContract']['sections']['defaultSorts']['path'] == '/handoff/reportContract/defaultSorts'
|
|
225
|
+
assert context['codingEvidence']['apiContracts'][0]['path'].endswith('/0')
|
|
226
|
+
assert 'runtime' in context['evidenceScope']
|
|
227
|
+
document['handoff']['reportContract']['formNumber'] = 'other_rpt'
|
|
228
|
+
file.write_text(json.dumps(document))
|
|
229
|
+
assert query(file, 'coding-start')[1]['errorCode'] == 'HANDOFF_CODING_CONTEXT_INVALID'
|
|
230
|
+
document['handoff']['reportContract']['formNumber'] = 'sample_rpt'
|
|
231
|
+
document['handoff']['targetSdk'] = ['invalid']
|
|
232
|
+
file.write_text(json.dumps(document))
|
|
233
|
+
assert query(file, 'coding-start')[1]['errorCode'] == 'HANDOFF_CODING_CONTEXT_INVALID'
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_large_report_contract_uses_exact_section_pointers(tmp_path):
|
|
237
|
+
file, document = handoff_file(tmp_path)
|
|
238
|
+
document['handoff'].update(type='reportform', formNumber='sample_rpt', reportContract={
|
|
239
|
+
'formNumber': 'sample_rpt', 'dataSetColumns': [f'col_{i}' for i in range(1200)],
|
|
240
|
+
'dataSetColumnTypes': [{'fieldKey': f'col_{i}', 'algoDataType': 'LongType'} for i in range(1200)],
|
|
241
|
+
'columnPresentation': [], 'filterEnforcement': [], 'filterValueContracts': []})
|
|
242
|
+
file.write_text(json.dumps(document))
|
|
243
|
+
code, context, output = query(file, 'coding-start')
|
|
244
|
+
assert code == 0 and len(output.encode()) <= 16 * 1024
|
|
245
|
+
assert context['reportContract']['detailsOmitted'] is True
|
|
246
|
+
assert context['reportContract']['sections']['dataSetColumnTypes'] == {
|
|
247
|
+
'path': '/handoff/reportContract/dataSetColumnTypes', 'count': 1200}
|
|
248
|
+
assert 'formGraph' not in context
|
|
249
|
+
document['handoff']['reportContract']['dataSetColumnTypes'] = [False]
|
|
250
|
+
file.write_text(json.dumps(document))
|
|
251
|
+
assert query(file, 'coding-start')[1]['errorCode'] == 'HANDOFF_CODING_CONTEXT_INVALID'
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_changed_or_missing_prepared_input_rejects_old_success_snapshot(tmp_path):
|
|
255
|
+
file, document = handoff_file(tmp_path)
|
|
256
|
+
intent = tmp_path / 'intent.json'
|
|
257
|
+
intent.write_text('{"coreLogic":["old"]}')
|
|
258
|
+
document['inputFiles'] = [{'path': str(intent), 'sha256': hashlib.sha256(intent.read_bytes()).hexdigest()}]
|
|
259
|
+
file.write_text(json.dumps(document))
|
|
260
|
+
assert query(file, 'coding-start')[0] == 0
|
|
261
|
+
intent.write_text('{"coreLogic":["new"]}')
|
|
262
|
+
assert query(file, 'coding-start')[1]['errorCode'] == 'HANDOFF_INPUT_STALE'
|
|
263
|
+
document['inputFiles'][0]['path'] = str(tmp_path / 'missing.json')
|
|
264
|
+
file.write_text(json.dumps(document))
|
|
265
|
+
assert query(file, 'index')[1]['errorCode'] == 'HANDOFF_INPUT_STALE'
|
|
266
|
+
document['inputFiles'][0]['path'] = 'relative.json'
|
|
267
|
+
file.write_text(json.dumps(document))
|
|
268
|
+
assert query(file, 'index')[1]['errorCode'] == 'HANDOFF_INPUT_INVALID'
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_startup_keeps_main_form_enums_without_loading_all_related_form_values(tmp_path):
|
|
272
|
+
file, document = handoff_file(tmp_path)
|
|
273
|
+
source = {"fieldKey": "source", "fieldName": "来源", "entityKey": "booking_head",
|
|
274
|
+
"comboOptions": [{"label": "ERP", "value": "ERP"}, {"label": "BPM 转嫁数据", "value": "opt_2"}]}
|
|
275
|
+
related = [{"fieldKey": "category_" + str(index), "entityKey": "related_form",
|
|
276
|
+
"comboOptions": [{"label": "关联枚举" * 100, "value": str(index)}]} for index in range(50)]
|
|
277
|
+
document["handoff"]["stateFields"] = [source, *related]
|
|
278
|
+
file.write_text(json.dumps(document, ensure_ascii=False), encoding="utf-8")
|
|
279
|
+
before = file.read_bytes()
|
|
280
|
+
code, start, output = query(file, "coding-start")
|
|
281
|
+
assert code == 0 and len(output.encode("utf-8")) < 16384
|
|
282
|
+
assert start["stateFields"] == [source]
|
|
283
|
+
assert start["remainingContext"]["stateFields"] == "/handoff/stateFields"
|
|
284
|
+
assert "Main form only" in start["stateFieldsScope"]
|
|
285
|
+
code, value, _ = query(file, "get", "--path", "/handoff/stateFields/50")
|
|
286
|
+
assert code == 0 and value["value"] == related[-1]
|
|
287
|
+
assert file.read_bytes() == before
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_runtime_context_cli_reads_the_same_bound_target_and_never_renders_cards(tmp_path):
|
|
291
|
+
file, document = handoff_file(tmp_path)
|
|
292
|
+
app = tmp_path / "app/demo"
|
|
293
|
+
(app / "_runtime").mkdir(parents=True)
|
|
294
|
+
(app / "_runtime/context.json").write_text(json.dumps({"environment": {"baseUrl": "http://127.0.0.1:8080/ierp"}}))
|
|
295
|
+
url = "http://127.0.0.1:8080/ierp/?formId=bos_list&type=list&billFormId=booking"
|
|
296
|
+
archive = app / "archive.json"
|
|
297
|
+
archive.write_text(json.dumps({"artifacts": [{"formNumber": "booking", "listUrl": url}]}))
|
|
298
|
+
document["handoff"].update({"appRoot": str(app), "formNumber": "booking"})
|
|
299
|
+
file.write_text(json.dumps(document))
|
|
300
|
+
before = archive.read_bytes()
|
|
301
|
+
code, result, _ = query(file, "runtime-context")
|
|
302
|
+
assert code == 0 and result["runtime"]["listUrl"] == url
|
|
303
|
+
assert result["runtime"]["runtimeVerified"] is False
|
|
304
|
+
assert archive.read_bytes() == before
|
|
305
|
+
code, result, _ = query(file, "coding-start", "--resolve-entry")
|
|
306
|
+
assert code == 2 and result["errorCode"] == "HANDOFF_RUNTIME_ARGUMENT_INVALID"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_startup_supplies_selected_signatures_without_changing_member_inventory(tmp_path):
|
|
310
|
+
file, document = handoff_file(tmp_path)
|
|
311
|
+
document["handoff"]["codingEvidence"]["guideSignatures"] = ["public void onPreparePropertys(PreparePropertysEventArgs e)"]
|
|
312
|
+
file.write_text(json.dumps(document))
|
|
313
|
+
code, result, output = query(file, "coding-start")
|
|
314
|
+
assert code == 0
|
|
315
|
+
contract = result["codingEvidence"]["apiContracts"][0]
|
|
316
|
+
assert contract["members"] == ["query"]
|
|
317
|
+
assert contract["signatures"][0]["signature"] == document["handoff"]["codingEvidence"]["apiContracts"][0]["members"][0]["signature"]
|
|
318
|
+
assert contract["import"] == "import kd.bos.servicehelper.QueryServiceHelper;"
|
|
319
|
+
assert result["codingEvidence"]["guideSignatures"] == document["handoff"]["codingEvidence"]["guideSignatures"]
|
|
320
|
+
assert result["entities"][1]["fields"][0]["timeValueUnit"] == "seconds-after-midnight"
|
|
321
|
+
assert result["runtimeContextCommand"][-1] == str(file)
|
|
322
|
+
assert len(output.encode()) <= 16 * 1024
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def test_time_empty_and_zero_contract_reaches_startup_without_another_lookup(tmp_path):
|
|
326
|
+
from prepare_plugin_handoff import _field_contract
|
|
327
|
+
file, document = handoff_file(tmp_path)
|
|
328
|
+
field = _field_contract({"fieldKey": "start_time", "fieldType": "TimeProp"})
|
|
329
|
+
assert "-1" in field["note"] and "Zero is valid midnight" in field["note"]
|
|
330
|
+
document["handoff"]["formGraph"]["entities"][1]["fields"] = [field]
|
|
331
|
+
file.write_text(json.dumps(document))
|
|
332
|
+
code, start, output = query(file, "coding-start")
|
|
333
|
+
assert code == 0
|
|
334
|
+
assert start["entities"][1]["fields"][0]["note"] == field["note"]
|
|
335
|
+
assert start["compileCommand"][-2:] == ["--handoff", str(file)]
|
|
336
|
+
assert len(output.encode()) <= 16 * 1024
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def test_operation_execution_locales_share_the_real_implementation():
|
|
340
|
+
root = SCRIPT.parents[1]
|
|
341
|
+
if root.name == "zh" and root.parent.name == ".locales":
|
|
342
|
+
root = root.parent.parent
|
|
343
|
+
for name in ["query_plugin_handoff.py", "compile_plugin_source.py", "prepare_plugin_handoff.py", "test_environment.py"]:
|
|
344
|
+
assert (root / "scripts" / name).read_bytes() == (root / ".locales/zh/scripts" / name).read_bytes()
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_bound_startup_returns_the_actual_target_test_command(tmp_path):
|
|
348
|
+
file, document = handoff_file(tmp_path)
|
|
349
|
+
document['handoff']['projectRoot'] = str(tmp_path)
|
|
350
|
+
file.write_text(json.dumps(document))
|
|
351
|
+
code, start, _ = query(file, 'coding-start')
|
|
352
|
+
assert code == 0
|
|
353
|
+
assert start['testCommand'] == [sys.executable, str(SCRIPT.with_name('run_java_tests.py')), '--project-root', str(tmp_path)]
|
|
354
|
+
assert Path(start['testCommand'][1]).is_file()
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def test_large_page_startup_exposes_exact_control_pointer(tmp_path):
|
|
358
|
+
file, document = handoff_file(tmp_path)
|
|
359
|
+
handoff = document["handoff"]
|
|
360
|
+
handoff.pop("formGraph")
|
|
361
|
+
handoff["formNumber"] = "sample_page"
|
|
362
|
+
handoff["uiContext"] = {"formNumber": "sample_page", "source": "getFormMetadata", "plugins": [],
|
|
363
|
+
"controls": [{"key": f"control_{index}", "type": "TextField"} for index in range(2000)]}
|
|
364
|
+
file.write_text(json.dumps(document), encoding="utf-8")
|
|
365
|
+
for operation in ("coding-start", "coding-context"):
|
|
366
|
+
code, result, output = query(file, operation)
|
|
367
|
+
assert code == 0 and len(output.encode()) < (16 if operation == "coding-start" else 48) * 1024
|
|
368
|
+
assert result["uiContext"]["controlCount"] == 2000
|
|
369
|
+
assert result["uiContext"]["controlsPath"] == "/handoff/uiContext/controls"
|
|
370
|
+
code, result, _ = query(file, "get", "--path", "/handoff/uiContext/controls/1999")
|
|
371
|
+
assert code == 0 and result["value"]["key"] == "control_1999"
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def test_selected_entity_metadata_cannot_be_replaced_by_page_context(tmp_path):
|
|
375
|
+
file, document = handoff_file(tmp_path)
|
|
376
|
+
document["handoff"].pop("formGraph")
|
|
377
|
+
document["handoff"].update({"formNumber": "sample_page", "metadataNeeds": ["form", "entity"],
|
|
378
|
+
"uiContext": {"formNumber": "sample_page", "source": "getFormMetadata", "controls": [], "plugins": []}})
|
|
379
|
+
file.write_text(json.dumps(document))
|
|
380
|
+
code, result, _ = query(file, "coding-start")
|
|
381
|
+
assert code == 2 and result["errorCode"] == "HANDOFF_CODING_CONTEXT_INVALID"
|
|
@@ -262,8 +262,9 @@ class MainTest(unittest.TestCase):
|
|
|
262
262
|
mock_config.return_value = {"baseUrl": "https://api.example.com"}
|
|
263
263
|
|
|
264
264
|
output = self._run()
|
|
265
|
-
self.assertEqual(output["status"], "
|
|
266
|
-
self.assertEqual(output["summary"]
|
|
265
|
+
self.assertEqual(output["status"], "routed")
|
|
266
|
+
self.assertEqual(output["summary"], {"total": 1, "success": 0, "failed": 0, "deferred": 1})
|
|
267
|
+
mock_config.assert_not_called()
|
|
267
268
|
self.assertIn("needs no registration call", output["results"][0]["message"])
|
|
268
269
|
self.assertEqual(output["results"][0]["registeredBy"], "platform class discovery")
|
|
269
270
|
self.assertFalse(output["results"][0]["registeredHere"])
|
|
@@ -610,10 +611,81 @@ class ReportPluginNotOwnedHereTest(unittest.TestCase):
|
|
|
610
611
|
self.assertEqual(output["verification"]["status"], "not_run")
|
|
611
612
|
self.assertEqual(output["verification"]["checks"], [])
|
|
612
613
|
self.assertIn("nothing to verify", output["verification"]["reason"])
|
|
613
|
-
#
|
|
614
|
-
self.assertEqual(output["status"], "
|
|
614
|
+
# 转交不证明其他注册器已经执行,不能计入实际注册成功。
|
|
615
|
+
self.assertEqual(output["status"], "routed")
|
|
616
|
+
self.assertEqual(output["summary"], {"total": 1, "success": 0, "failed": 0, "deferred": 1})
|
|
617
|
+
self.assertFalse(output["results"][0]["success"])
|
|
618
|
+
mock_config.assert_not_called()
|
|
615
619
|
mock_get.assert_not_called()
|
|
616
620
|
|
|
617
621
|
|
|
622
|
+
class ReportFormPluginRegisteredHereTest(unittest.TestCase):
|
|
623
|
+
"""A reportform plugin is the report form's own form plugin, registered and verified here.
|
|
624
|
+
|
|
625
|
+
The registry used to name the report pipeline as its registrar, and the pipeline writes only the
|
|
626
|
+
data-fetch slot and the left tree: the class was reported as owned elsewhere and never
|
|
627
|
+
registered, so the report's hyperlink, toolbar and header-filter handlers did nothing. The
|
|
628
|
+
plugin service registers a BILL_FORM target on the form's root plugin list, which for a report
|
|
629
|
+
is the report form, and queryEditable reads that list back as location BILL_FORM. Uses the real
|
|
630
|
+
registry on purpose: the registry entry is what this test pins.
|
|
631
|
+
"""
|
|
632
|
+
|
|
633
|
+
def _run(self, extra_args=None):
|
|
634
|
+
argv = ["register_plugins.py", "--app-root", "/tmp"] + list(extra_args or [])
|
|
635
|
+
with mock.patch("builtins.print") as mock_print, mock.patch("sys.argv", argv):
|
|
636
|
+
register_plugins.main()
|
|
637
|
+
return json.loads(mock_print.call_args[0][0])
|
|
638
|
+
|
|
639
|
+
def test_the_registry_routes_report_form_plugins_to_the_form_plugin_list(self):
|
|
640
|
+
root = register_plugins._find_root_dir()
|
|
641
|
+
self.assertEqual(register_plugins._load_type_registry(root)["reportform"], "BILL_FORM")
|
|
642
|
+
self.assertEqual(register_plugins._load_type_owners(root)["reportform"], "plugin-register-api")
|
|
643
|
+
# The data-fetch and left-tree types stay with the report pipeline's own slots.
|
|
644
|
+
self.assertNotIn("reportlist", register_plugins._load_type_registry(root))
|
|
645
|
+
self.assertNotIn("reporttreelist", register_plugins._load_type_registry(root))
|
|
646
|
+
|
|
647
|
+
@mock.patch("register_plugins.kd_api.get")
|
|
648
|
+
@mock.patch("register_plugins.kd_api.post")
|
|
649
|
+
@mock.patch("register_plugins._load_config")
|
|
650
|
+
@mock.patch("register_plugins._load_classes_from_archive")
|
|
651
|
+
def test_report_form_plugin_registers_on_the_report_and_reads_back(
|
|
652
|
+
self, mock_load_classes, mock_config, mock_post, mock_get,
|
|
653
|
+
):
|
|
654
|
+
mock_load_classes.return_value = [{
|
|
655
|
+
"className": "kd.uhyl.report.KpiDrillReportPlugin", "type": "reportform",
|
|
656
|
+
"formNumber": "uhyl_kpi_rpt", "description": "drill"}]
|
|
657
|
+
mock_config.return_value = {"baseUrl": "https://api.example.com"}
|
|
658
|
+
mock_post.return_value = {"status": True, "data": {"success": True, "message": ""}}
|
|
659
|
+
mock_get.return_value = {"status": True, "data": [
|
|
660
|
+
{"className": "kd.uhyl.report.KpiReportPlugin", "location": "REPORT_QUERY_PLUGIN"},
|
|
661
|
+
{"className": "kd.uhyl.report.KpiDrillReportPlugin", "location": "BILL_FORM"},
|
|
662
|
+
]}
|
|
663
|
+
|
|
664
|
+
output = self._run(["--verify"])
|
|
665
|
+
|
|
666
|
+
self.assertEqual(output["status"], "success")
|
|
667
|
+
body = mock_post.call_args[0][3]
|
|
668
|
+
self.assertEqual(body["targetType"], "BILL_FORM")
|
|
669
|
+
self.assertEqual(body["formNumber"], "uhyl_kpi_rpt")
|
|
670
|
+
self.assertEqual(output["verification"]["status"], "pass")
|
|
671
|
+
self.assertTrue(output['results'][0]['registeredHere'])
|
|
672
|
+
self.assertTrue(output['results'][0]['verifiedHere'])
|
|
673
|
+
self.assertEqual(output["activation"]["registeredClasses"], ["kd.uhyl.report.KpiDrillReportPlugin"])
|
|
674
|
+
|
|
675
|
+
def test_mixed_report_delivery_distinguishes_registered_page_from_routed_query(self):
|
|
676
|
+
candidates = [{"className": "kd.sample.report.Query", "type": "reportlist", "formNumber": "sample_rpt"},
|
|
677
|
+
{"className": "kd.sample.report.Page", "type": "reportform", "formNumber": "sample_rpt"}]
|
|
678
|
+
with mock.patch.object(register_plugins, '_load_classes_from_archive', return_value=candidates), \
|
|
679
|
+
mock.patch.object(register_plugins, '_load_config', return_value={'baseUrl': 'https://example.test'}), \
|
|
680
|
+
mock.patch.object(register_plugins.kd_api, 'post', return_value={'status': True, 'data': {'success': True}}), \
|
|
681
|
+
mock.patch.object(register_plugins.kd_api, 'get', return_value={'status': True, 'data': [
|
|
682
|
+
{'className': 'kd.sample.report.Page', 'location': 'BILL_FORM'}]}):
|
|
683
|
+
output = self._run(['--verify'])
|
|
684
|
+
self.assertEqual(output['status'], 'partial')
|
|
685
|
+
self.assertEqual(output['summary'], {'total': 2, 'success': 1, 'failed': 0, 'deferred': 1})
|
|
686
|
+
self.assertFalse(output['results'][0]['verifiedHere'])
|
|
687
|
+
self.assertTrue(output['results'][1]['verifiedHere'])
|
|
688
|
+
|
|
689
|
+
|
|
618
690
|
if __name__ == "__main__":
|
|
619
691
|
unittest.main()
|