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