@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
|
@@ -12,7 +12,8 @@ from unittest import mock
|
|
|
12
12
|
import pytest
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
16
|
+
SCRIPT = (ROOT / ".locales" / "zh" if os.environ.get("APP_BUILD_CARD_TEST_LOCALE") == "zh" else ROOT) / "scripts" / "render_output_cards.py"
|
|
16
17
|
SPEC = importlib.util.spec_from_file_location("render_output_cards", SCRIPT)
|
|
17
18
|
assert SPEC and SPEC.loader
|
|
18
19
|
CARDS = importlib.util.module_from_spec(SPEC)
|
|
@@ -116,7 +117,7 @@ def test_cli_does_not_request_output_check_when_rendering_fails(capsys, tmp_path
|
|
|
116
117
|
|
|
117
118
|
@pytest.mark.parametrize("locale", ["", ".locales/zh"])
|
|
118
119
|
def test_loaded_skill_exposes_output_check_through_real_host_router(locale):
|
|
119
|
-
root =
|
|
120
|
+
root = ROOT
|
|
120
121
|
program = """
|
|
121
122
|
const { skillRelevantToolNames } = await import(process.argv[1] + "/src/tool/router.ts");
|
|
122
123
|
const output = await Bun.file(process.argv[2]).text();
|
|
@@ -518,8 +519,68 @@ def test_card_carries_list_preview_url_and_degrades_silently(tmp_path: Path) ->
|
|
|
518
519
|
assert "listUrl" not in handoff["forms"][0]
|
|
519
520
|
|
|
520
521
|
|
|
522
|
+
@pytest.mark.parametrize('failure,status,reuse', [
|
|
523
|
+
(502, 'archived', True), (503, 'archived', True), (504, 'archived', True),
|
|
524
|
+
(403, 'unavailable', False), (400, 'unavailable', False),
|
|
525
|
+
])
|
|
526
|
+
def test_preview_fallback_is_scoped_and_has_explicit_evidence(tmp_path, failure, status, reuse):
|
|
527
|
+
from urllib.error import HTTPError
|
|
528
|
+
previous = {'title': 'Target', 'url': 'https://example.test/ierp/?formId=target',
|
|
529
|
+
'metadata': 'https://example.test/ierp/meta?formNumber=target'}
|
|
530
|
+
archive = tmp_path / 'archive.json'
|
|
531
|
+
archive.write_text(json.dumps({'formNumbers': [{'number': 'target', 'name': 'Target'}],
|
|
532
|
+
'artifacts': [previous], 'previewEvidence': {'other': {'status': 'fresh'}}}))
|
|
533
|
+
with mock.patch('kd_env_loader.load_effective_skill_config', return_value={'server': {'baseUrl': 'https://example.test/ierp'}}), \
|
|
534
|
+
mock.patch.object(CARDS, 'get_preview_url', side_effect=HTTPError('https://example.test', failure, 'failure', {}, None)) as preview:
|
|
535
|
+
blocks = CARDS.render(str(tmp_path), target_form_number='target')
|
|
536
|
+
card = json.loads(blocks[0].splitlines()[1])
|
|
537
|
+
assert card['url'] == (previous['url'] if reuse else '')
|
|
538
|
+
preview.assert_called_once()
|
|
539
|
+
saved = json.loads(archive.read_text())
|
|
540
|
+
assert saved['previewEvidence']['target']['status'] == status
|
|
541
|
+
assert saved['previewEvidence']['target']['runtimeVerified'] is False
|
|
542
|
+
assert saved['previewEvidence']['target']['listStatus'] == 'not-run'
|
|
543
|
+
assert saved['previewEvidence']['other']['status'] == 'fresh'
|
|
544
|
+
assert 'status' not in card and 'runtimeVerified' not in card
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
@pytest.mark.parametrize('url', ['https://other.test/ierp/?formId=target',
|
|
548
|
+
'https://example.test/other/?formId=target', 'https://example.test/ierp-other/?formId=target',
|
|
549
|
+
'javascript:alert(1)', 'https://user:secret@example.test/ierp/?formId=target'])
|
|
550
|
+
def test_archived_preview_never_crosses_environment_or_accepts_credentials(url):
|
|
551
|
+
assert CARDS._archived_preview_urls([{'url': url, 'metadata': 'https://example.test/meta?formNumber=target'}],
|
|
552
|
+
'https://example.test/ierp') == {}
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def test_business_preview_error_does_not_restore_old_link(tmp_path):
|
|
556
|
+
archive = tmp_path / 'archive.json'
|
|
557
|
+
archive.write_text(json.dumps({'formNumbers': [{'number': 'target', 'name': 'Target'}],
|
|
558
|
+
'previewEvidence': [], 'artifacts': [{'url': 'https://example.test/?formId=target',
|
|
559
|
+
'metadata': 'https://example.test/meta?formNumber=target'}]}))
|
|
560
|
+
with mock.patch('kd_env_loader.load_effective_skill_config', return_value={'server': {'baseUrl': 'https://example.test'}}), \
|
|
561
|
+
mock.patch.object(CARDS, 'get_preview_url', side_effect=CARDS.PreviewUrlError('PREVIEW_URL_LOOKUP_FAILED: denied')):
|
|
562
|
+
blocks = CARDS.render(str(tmp_path), target_form_number='target')
|
|
563
|
+
assert json.loads(blocks[0].splitlines()[1])['url'] == ''
|
|
564
|
+
assert json.loads(archive.read_text())['previewEvidence']['target']['status'] == 'unavailable'
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def test_cli_publishes_preview_evidence_without_leaking_previous_call(capsys, tmp_path):
|
|
568
|
+
block = ':::render:kdform\n{"title":"Target","url":""}\n:::'
|
|
569
|
+
def render(*args, **kwargs):
|
|
570
|
+
CARDS._PREVIEW_EVIDENCE = {'target': {'status': 'archived', 'runtimeVerified': False}}
|
|
571
|
+
return [block]
|
|
572
|
+
with mock.patch.object(sys, 'argv', [str(SCRIPT), '--app-root', str(tmp_path)]), \
|
|
573
|
+
mock.patch.object(CARDS, 'render', side_effect=render), mock.patch.object(CARDS, '_DYM_BACKGROUND_NOTICE', ''):
|
|
574
|
+
assert CARDS.main() == 0
|
|
575
|
+
assert 'PREVIEW_EVIDENCE=' in capsys.readouterr().out
|
|
576
|
+
with mock.patch.object(sys, 'argv', [str(SCRIPT), '--app-root', str(tmp_path)]), \
|
|
577
|
+
mock.patch.object(CARDS, 'render', return_value=[block]), mock.patch.object(CARDS, '_DYM_BACKGROUND_NOTICE', ''):
|
|
578
|
+
assert CARDS.main() == 0
|
|
579
|
+
assert 'PREVIEW_EVIDENCE=' not in capsys.readouterr().out
|
|
580
|
+
|
|
581
|
+
|
|
521
582
|
def test_localized_card_cli_keeps_target_filter_contract() -> None:
|
|
522
|
-
localized =
|
|
583
|
+
localized = ROOT / ".locales" / "zh" / "scripts" / SCRIPT.name
|
|
523
584
|
result = subprocess.run(
|
|
524
585
|
[sys.executable, str(localized), "--help"],
|
|
525
586
|
check=True,
|
|
@@ -530,3 +591,14 @@ def test_localized_card_cli_keeps_target_filter_contract() -> None:
|
|
|
530
591
|
assert "--form-number" in result.stdout
|
|
531
592
|
assert "--form-name" in result.stdout
|
|
532
593
|
assert "repeat for" in result.stdout and "multiple forms" in result.stdout
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def test_localized_renderer_runs_regressions_in_its_own_process():
|
|
597
|
+
"""中文运行树在独立进程中跑同一组回归,避免复用 canonical 的模块缓存。"""
|
|
598
|
+
if os.environ.get("APP_BUILD_CARD_TEST_LOCALE") == "zh":
|
|
599
|
+
pytest.skip("已在中文独立进程中")
|
|
600
|
+
result = subprocess.run([sys.executable, "-m", "pytest", str(Path(__file__).resolve()),
|
|
601
|
+
"-q", "-k", "not test_localized_renderer_runs_regressions_in_its_own_process"],
|
|
602
|
+
cwd=ROOT, env={**os.environ, "APP_BUILD_CARD_TEST_LOCALE": "zh"},
|
|
603
|
+
capture_output=True, text=True, encoding="utf-8", timeout=60)
|
|
604
|
+
assert result.returncode == 0, result.stdout + result.stderr
|
|
@@ -99,6 +99,18 @@ class ReportBusinessContractTest(unittest.TestCase):
|
|
|
99
99
|
cosmic_api._execute_report_pipeline({}, "token", artifact, "APP-ID", "UNIT-ID")
|
|
100
100
|
create_page.assert_not_called()
|
|
101
101
|
|
|
102
|
+
def test_default_sort_preflight_fails_before_create_page(self):
|
|
103
|
+
for plugins, sort_key, error in (([], "a", "REPORT_DEFAULT_SORT_PLUGIN_REQUIRED"),
|
|
104
|
+
([{"className": "kd.test.P", "enabled": False}], "a", "REPORT_DEFAULT_SORT_PLUGIN_REQUIRED"),
|
|
105
|
+
([{"className": "kd.test.P"}], "missing", "sort columns not found")):
|
|
106
|
+
artifact = self._dym_artifact()
|
|
107
|
+
artifact["reportPlugins"] = plugins
|
|
108
|
+
artifact["reportSorts"][0]["fieldKey"] = sort_key
|
|
109
|
+
with self.subTest(plugins=plugins, key=sort_key), mock.patch.object(cosmic_api, "do_create_page") as create:
|
|
110
|
+
with self.assertRaisesRegex(ValueError, error):
|
|
111
|
+
cosmic_api._execute_report_pipeline({}, "token", artifact, "APP", "UNIT")
|
|
112
|
+
create.assert_not_called()
|
|
113
|
+
|
|
102
114
|
def test_data_source_write_surface_is_gone(self) -> None:
|
|
103
115
|
for name in ("normalize_data_source_input", "add_data_source"):
|
|
104
116
|
self.assertFalse(hasattr(report_ops, name), name)
|
|
@@ -251,23 +263,32 @@ class ReportBusinessContractTest(unittest.TestCase):
|
|
|
251
263
|
"reportSorts": [{"fieldKey": "a", "direction": "desc"}],
|
|
252
264
|
"reportPlugins": [{"className": "kd.uhyl.report.P", "enabled": True}],
|
|
253
265
|
}),
|
|
266
|
+
tempfile.TemporaryDirectory() as root,
|
|
254
267
|
):
|
|
255
268
|
result = cosmic_api._execute_report_pipeline(
|
|
256
|
-
{}, "token", self._dym_artifact(), "APP-ID", "UNIT-ID", app_root=
|
|
269
|
+
{}, "token", self._dym_artifact(), "APP-ID", "UNIT-ID", app_root=root)
|
|
270
|
+
persisted = json.loads(Path(result["reportPluginContract"]["path"]).read_text(encoding="utf-8"))
|
|
271
|
+
self.assertEqual(persisted["defaultSorts"], result["reportDefaultSorts"]["requirements"])
|
|
272
|
+
self.assertFalse(persisted["defaultSortPolicy"]["runtimeVerified"])
|
|
257
273
|
|
|
258
274
|
# The invariant is the order, not the exact call list: the export is synced after every
|
|
259
275
|
# write, so it cannot be older than what it is used to prove.
|
|
260
276
|
sync_at = calls.index("syncDym")
|
|
261
|
-
|
|
277
|
+
self.assertNotIn("sorts", calls)
|
|
278
|
+
for write in ("columns", "plugin"):
|
|
262
279
|
self.assertLess(calls.index(write), sync_at, f"{write} ran after the sync")
|
|
263
280
|
self.assertEqual("syncDym", calls[-1])
|
|
264
|
-
#
|
|
281
|
+
# 只证明元数据列和插件;默认排序要求被保留,但运行时仍未验证。
|
|
265
282
|
self.assertEqual("formLayout+dym", result["readBackSource"])
|
|
266
283
|
self.assertTrue(result["verification"]["verified"])
|
|
267
284
|
self.assertEqual(
|
|
268
|
-
{"reportColumns", "
|
|
285
|
+
{"reportColumns", "reportPlugins"},
|
|
269
286
|
{check["section"] for check in result["verification"]["checks"]},
|
|
270
287
|
)
|
|
288
|
+
self.assertEqual(result["reportDefaultSorts"]["requirements"], [
|
|
289
|
+
{"fieldKey": "a", "dateIndex": "a", "direction": "desc"}])
|
|
290
|
+
self.assertFalse(result["reportDefaultSorts"]["runtimeVerified"])
|
|
291
|
+
self.assertFalse(result["reportDefaultSorts"]["metadataWritten"])
|
|
271
292
|
|
|
272
293
|
def test_pipeline_survives_a_failed_design_state_sync(self) -> None:
|
|
273
294
|
"""Design-state evidence is additive; losing it must not fail a successful write."""
|
|
@@ -282,9 +303,10 @@ class ReportBusinessContractTest(unittest.TestCase):
|
|
|
282
303
|
return_value=self._derived_schema()),
|
|
283
304
|
mock.patch.object(cosmic_api, "_export_report_dym_evidence",
|
|
284
305
|
side_effect=RuntimeError("sync root not configured")),
|
|
306
|
+
tempfile.TemporaryDirectory() as root,
|
|
285
307
|
):
|
|
286
308
|
result = cosmic_api._execute_report_pipeline(
|
|
287
|
-
{}, "token", self._dym_artifact(), "APP-ID", "UNIT-ID", app_root=
|
|
309
|
+
{}, "token", self._dym_artifact(), "APP-ID", "UNIT-ID", app_root=root)
|
|
288
310
|
|
|
289
311
|
self.assertTrue(result["ok"])
|
|
290
312
|
self.assertEqual("formLayout", result["readBackSource"])
|
|
@@ -10,8 +10,13 @@ Every failure this file guards against has already happened at least once:
|
|
|
10
10
|
contract rejects;
|
|
11
11
|
- guidance to call `setFieldType(ReportColumn.TYPE_AMOUNT)`, a constant name that appears
|
|
12
12
|
nowhere in the knowledge base;
|
|
13
|
-
-
|
|
14
|
-
|
|
13
|
+
- an `id` / `pid` DataSet-column convention for tree reports, where the standard product's tree
|
|
14
|
+
reports carry `rowid` / `pid` / `isgroupnode`;
|
|
15
|
+
- a left-tree example registering its tree plugin as a second `reportPlugins` entry, which is
|
|
16
|
+
the report list's data-fetch slot, on a template that has no left tree;
|
|
17
|
+
- a left-tree example created from `bos_grouprpttpl` as if it were a left-tree template. It is
|
|
18
|
+
the grouped report, and no public template presets a left tree: one exists only after a user
|
|
19
|
+
adds the report tree control, so a creation Artifact cannot declare `reportProperties.tree`.
|
|
15
20
|
|
|
16
21
|
Prose is not tested here. What is tested is every place where documentation restates a value
|
|
17
22
|
the implementation owns, because that is where the two silently drift apart.
|
|
@@ -25,6 +30,8 @@ import unittest
|
|
|
25
30
|
from pathlib import Path
|
|
26
31
|
|
|
27
32
|
APP_BUILD_ROOT = Path(__file__).resolve().parents[2]
|
|
33
|
+
if APP_BUILD_ROOT.name == "zh" and APP_BUILD_ROOT.parent.name == ".locales":
|
|
34
|
+
APP_BUILD_ROOT = APP_BUILD_ROOT.parent.parent
|
|
28
35
|
ZH_ROOT = APP_BUILD_ROOT / ".locales" / "zh"
|
|
29
36
|
|
|
30
37
|
VALIDATOR = APP_BUILD_ROOT / "skills/cosmic-meta-modeling/scripts/report_validator.py"
|
|
@@ -66,8 +73,8 @@ EMITTED_RULE = re.compile(r'rule="(R-[a-z-]+)"')
|
|
|
66
73
|
# and in the sentence-boundary pattern below are matcher input, not prose.
|
|
67
74
|
_DENIAL_MARKERS = (
|
|
68
75
|
"no longer exists", "no documented", "not invent", "do not", "does not", "never",
|
|
69
|
-
"removed", "rejected", "refuse", "cannot",
|
|
70
|
-
"已不存在", "不存在", "没有", "不要", "禁止", "不再", "无法", "拒绝",
|
|
76
|
+
"removed", "rejected", "refuse", "cannot", "blocked",
|
|
77
|
+
"已不存在", "不存在", "没有", "不要", "禁止", "不再", "无法", "拒绝", "阻断",
|
|
71
78
|
)
|
|
72
79
|
|
|
73
80
|
|
|
@@ -171,12 +178,53 @@ class CommandTableTest(unittest.TestCase):
|
|
|
171
178
|
table = [line for line in _read(path).splitlines() if line.startswith("| ")]
|
|
172
179
|
referenced = {
|
|
173
180
|
name for line in table
|
|
174
|
-
for name in re.findall(r"`(set
|
|
181
|
+
for name in re.findall(r"`((?:set|add|remove|register|plan)[A-Za-z]+|deleteForm|run)`", line)
|
|
175
182
|
}
|
|
176
183
|
with self.subTest(doc=path.name):
|
|
177
184
|
self.assertTrue(referenced, f"{path}: no command found in the table")
|
|
178
185
|
unknown = sorted(referenced - published - routed)
|
|
179
186
|
self.assertEqual(unknown, [], f"{path} advertises unpublished commands")
|
|
187
|
+
self.assertIn("addReportColumns", referenced)
|
|
188
|
+
self.assertIn("removeReportColumn", referenced)
|
|
189
|
+
|
|
190
|
+
def test_unsafe_sort_writer_is_only_documented_as_blocked(self):
|
|
191
|
+
self.assertEqual(self.contracts["commands"]["setReportSorts"]["availability"], "blocked")
|
|
192
|
+
self.assertEqual(self.contracts["designStateEvidence"]["provenSections"], ["reportPlugins"])
|
|
193
|
+
checked = 0
|
|
194
|
+
for path in _report_doc_paths():
|
|
195
|
+
text = _read(path)
|
|
196
|
+
for match in re.finditer(r"`setReportSorts`", text):
|
|
197
|
+
checked += 1
|
|
198
|
+
with self.subTest(doc=str(path)):
|
|
199
|
+
self.assertTrue(_denied_near(text, match.start(), match.end()),
|
|
200
|
+
f"{path} advertises the unsafe sort writer")
|
|
201
|
+
self.assertGreater(checked, 0)
|
|
202
|
+
for relative in ("skills/cosmic-meta-modeling/modules/report/SKILL.md",
|
|
203
|
+
"skills/cosmic-meta-modifier/modules/report-op/SKILL.md",
|
|
204
|
+
"knowledge/cosmic/platform/reportlistdata-plugin.md"):
|
|
205
|
+
for root in (APP_BUILD_ROOT, ZH_ROOT):
|
|
206
|
+
with self.subTest(tree=root.name, doc=relative):
|
|
207
|
+
text = _read(root / relative)
|
|
208
|
+
self.assertIn("defaultSorts", text)
|
|
209
|
+
self.assertIn("defaultSortPolicy", text)
|
|
210
|
+
|
|
211
|
+
def test_column_removal_has_one_strict_payload_and_release_boundary(self):
|
|
212
|
+
removal = json.loads(_read(WRITE_SCHEMA))["$defs"]["columnRemoval"]
|
|
213
|
+
self.assertEqual(removal["required"], ["fieldKey"])
|
|
214
|
+
self.assertFalse(removal["additionalProperties"])
|
|
215
|
+
self.assertEqual(set(removal["properties"]), {"fieldKey"})
|
|
216
|
+
contract = self.contracts["commands"]["removeReportColumn"]
|
|
217
|
+
self.assertTrue(contract["payloadSchemaRef"].endswith("#/$defs/columnRemoval"))
|
|
218
|
+
self.assertIn("ReportColumns.remove", contract["writeSemantics"])
|
|
219
|
+
self.assertIn("REPORT_COLUMN_DELETE_API_UNAVAILABLE", contract["compatibility"])
|
|
220
|
+
self.assertIn("removeReportColumn", self.contracts["columnChangeRoute"]["remove"])
|
|
221
|
+
for root in (APP_BUILD_ROOT, ZH_ROOT):
|
|
222
|
+
for relative in ("skills/cosmic-meta-modifier/modules/report-op/SKILL.md",
|
|
223
|
+
"skills/cosmic-meta-modeling/modules/report/SKILL.md"):
|
|
224
|
+
with self.subTest(tree=root.name, doc=relative):
|
|
225
|
+
text = _read(root / relative)
|
|
226
|
+
self.assertIn("removeReportColumn", text)
|
|
227
|
+
self.assertIn("REPORT_COLUMN_DELETE_API_UNAVAILABLE", text)
|
|
180
228
|
|
|
181
229
|
def test_a_removed_command_is_only_ever_mentioned_to_warn_against_it(self):
|
|
182
230
|
removed = set(self.contracts.get("removedCommands") or {})
|
|
@@ -216,6 +264,7 @@ class CommandTableTest(unittest.TestCase):
|
|
|
216
264
|
"""
|
|
217
265
|
message = _block(_read(REPORT_OPS), "_REPORT_COLUMNS_ALREADY_SET = (")
|
|
218
266
|
self.assertIn("addReportColumns", message)
|
|
267
|
+
self.assertIn("removeReportColumn", message)
|
|
219
268
|
self.assertIn("planReportRecreate", message)
|
|
220
269
|
self.assertIn("designer", message)
|
|
221
270
|
self.assertTrue(_denied_near(message, message.index("designer"),
|
|
@@ -336,7 +385,12 @@ class PresentationContractTest(unittest.TestCase):
|
|
|
336
385
|
|
|
337
386
|
|
|
338
387
|
class TreeSemanticsTest(unittest.TestCase):
|
|
339
|
-
"""Two features are called tree, and
|
|
388
|
+
"""Two features are called tree, and each has its own plugin shape and slot.
|
|
389
|
+
|
|
390
|
+
Collapsible rows are one row set whose DataSet carries rowid / pid / isgroupnode, as the
|
|
391
|
+
standard product's tree reports build it; an id / pid convention is only ever named to warn
|
|
392
|
+
against it. Left-tree navigation is a second plugin on the left tree's own slot.
|
|
393
|
+
"""
|
|
340
394
|
|
|
341
395
|
def test_no_document_claims_an_id_pid_dataset_convention(self):
|
|
342
396
|
pattern = re.compile(r"`id`\s*(?:\+|/|and|与|和)\s*`pid`")
|
|
@@ -348,11 +402,49 @@ class TreeSemanticsTest(unittest.TestCase):
|
|
|
348
402
|
with self.subTest(doc=path.name):
|
|
349
403
|
self.assertTrue(
|
|
350
404
|
_denied_near(text, match.start(), match.end()),
|
|
351
|
-
f"{path} states an id/pid DataSet convention without denying it;
|
|
352
|
-
"
|
|
405
|
+
f"{path} states an id/pid DataSet convention without denying it; "
|
|
406
|
+
"collapsible tree rows carry rowid / pid / isgroupnode",
|
|
353
407
|
)
|
|
354
408
|
self.assertGreater(checked, 0, "the id/pid scan matched nothing; the pattern is stale")
|
|
355
409
|
|
|
410
|
+
def test_collapsible_rows_name_the_row_columns_in_both_trees(self):
|
|
411
|
+
for relative in ("references/plugin-runtime-routing.md",
|
|
412
|
+
"skills/cosmic-meta-knowledge/generation/report-modeling-guide.md",
|
|
413
|
+
"knowledge/cosmic/platform/reportlistdata-plugin.md"):
|
|
414
|
+
for root in (APP_BUILD_ROOT, ZH_ROOT):
|
|
415
|
+
text = _read(root / relative)
|
|
416
|
+
for column in ("`rowid`", "`pid`", "`isgroupnode`"):
|
|
417
|
+
with self.subTest(doc=relative, tree=root.name, column=column):
|
|
418
|
+
self.assertIn(column, text)
|
|
419
|
+
|
|
420
|
+
def test_the_left_tree_example_registers_the_tree_plugin_on_the_tree(self):
|
|
421
|
+
"""reportPlugins is the report list's data-fetch slot, and no template has a left tree."""
|
|
422
|
+
relative = "skills/cosmic-meta-modeling/modules/report/examples/golden-example-report-tree.md"
|
|
423
|
+
for root in (APP_BUILD_ROOT, ZH_ROOT):
|
|
424
|
+
text = _read(root / relative)
|
|
425
|
+
artifacts = [json.loads(block) for block in re.findall(
|
|
426
|
+
r"```json\s*\n(.*?)\n```", text, re.DOTALL)]
|
|
427
|
+
artifact = next(item for item in artifacts if item.get("reportColumns"))
|
|
428
|
+
with self.subTest(tree=root.name):
|
|
429
|
+
self.assertEqual(artifact["templateNumber"], "bos_rpttpl")
|
|
430
|
+
self.assertEqual(len(artifact["reportPlugins"]), 1)
|
|
431
|
+
self.assertNotIn("tree", artifact.get("reportProperties") or {})
|
|
432
|
+
# The tree plugin is written after the user adds the report tree control.
|
|
433
|
+
self.assertIn("--target tree", text)
|
|
434
|
+
self.assertIn('"ReportPlugin": "kd.kdtest.report.CostAnalysisTreePlugin"', text)
|
|
435
|
+
|
|
436
|
+
def test_no_document_calls_the_grouped_template_a_left_tree(self):
|
|
437
|
+
"""bos_grouprpttpl is the grouped report (group panel + group list), not a left tree."""
|
|
438
|
+
pattern = re.compile(r"left-tree template|左树模板")
|
|
439
|
+
for path in _report_doc_paths():
|
|
440
|
+
text = _read(path)
|
|
441
|
+
for match in pattern.finditer(text):
|
|
442
|
+
with self.subTest(doc=path.name):
|
|
443
|
+
self.assertTrue(
|
|
444
|
+
_denied_near(text, match.start(), match.end()),
|
|
445
|
+
f"{path} names a left-tree template; none exists",
|
|
446
|
+
)
|
|
447
|
+
|
|
356
448
|
def test_routing_uses_registered_report_types(self):
|
|
357
449
|
routing = _read(APP_BUILD_ROOT / "references/plugin-runtime-routing.md")
|
|
358
450
|
types = {entry["type"] for entry in json.loads(
|
|
@@ -432,7 +524,8 @@ class ReadBackTierTest(unittest.TestCase):
|
|
|
432
524
|
|
|
433
525
|
def test_contract_documents_every_tier_the_implementation_can_return(self):
|
|
434
526
|
documented = set(json.loads(_read(WRITE_CONTRACTS))["readBackSource"])
|
|
435
|
-
self.assertEqual(documented, {"canonical", "formLayout", "formLayout+dym",
|
|
527
|
+
self.assertEqual(documented, {"canonical", "formLayout", "formLayout+dym",
|
|
528
|
+
"reportFilterProjection", "unavailable"})
|
|
436
529
|
source = _read(REPORT_OPS)
|
|
437
530
|
for tier in documented:
|
|
438
531
|
with self.subTest(tier=tier):
|
|
@@ -454,8 +547,7 @@ class PipelineStepCountTest(unittest.TestCase):
|
|
|
454
547
|
steps = set(re.findall(r"Step (\d)/(\d): ", cli))
|
|
455
548
|
self.assertTrue(steps)
|
|
456
549
|
totals = {total for _index, total in steps}
|
|
457
|
-
#
|
|
458
|
-
# last so it settles ShowFilterFields after the filter write.
|
|
550
|
+
# 七步包含默认排序契约保留;属性仍最后结算过滤写入后的 ShowFilterFields。
|
|
459
551
|
self.assertEqual(totals, {"7"}, "the report pipeline no longer has exactly 7 steps")
|
|
460
552
|
skill = _read(APP_BUILD_ROOT / "skills/cosmic-meta-modeling/modules/report/SKILL.md")
|
|
461
553
|
ordered = re.search(r"`run` executes the fixed platform dependency order internally:(.+?)\n\n",
|
|
@@ -35,6 +35,7 @@ import render_output_cards as cards
|
|
|
35
35
|
|
|
36
36
|
REPORT = "kdtest_ljbb"
|
|
37
37
|
FQCN = "kd.test.guaner.report.LjbbReportPlugin"
|
|
38
|
+
TREE_FQCN = "kd.test.guaner.report.LjbbTreePlugin"
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
def _write(path: Path, payload) -> None:
|
|
@@ -46,7 +47,7 @@ class _Root:
|
|
|
46
47
|
"""An appRoot carrying the defect's exact shape: report archived, plugin registered."""
|
|
47
48
|
|
|
48
49
|
def __init__(self, stack, *, javas=None, contract=True, plugin_classes=(FQCN,),
|
|
49
|
-
source_on_disk=False):
|
|
50
|
+
source_on_disk=False, plugin_types=None):
|
|
50
51
|
self.path = Path(stack.enter_context(TemporaryDirectory()))
|
|
51
52
|
self.archive = {"formNumbers": [{"name": "lj report", "number": REPORT}]}
|
|
52
53
|
if javas is not None:
|
|
@@ -59,18 +60,18 @@ class _Root:
|
|
|
59
60
|
self.source = str(source)
|
|
60
61
|
_write(self.path / "archive.json", self.archive)
|
|
61
62
|
if contract:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
63
|
+
payload = {
|
|
64
|
+
"schemaVersion": "app-build-report-plugin-contract/v1",
|
|
65
|
+
"formNumber": REPORT,
|
|
66
|
+
"pluginClasses": list(plugin_classes),
|
|
67
|
+
"dataSetColumns": ["billno", "createtime", "creator_phone"],
|
|
68
|
+
"dataSetColumnTypes": [{"dateIndex": "billno", "javaType": "String"}],
|
|
69
|
+
"columnPresentation": [],
|
|
70
|
+
"filterEnforcement": [],
|
|
71
|
+
}
|
|
72
|
+
if plugin_types is not None:
|
|
73
|
+
payload["pluginTypes"] = plugin_types
|
|
74
|
+
_write(self.path / "_runtime" / f"{REPORT}.report-plugin-contract.json", payload)
|
|
74
75
|
|
|
75
76
|
def undelivered(self):
|
|
76
77
|
return cards.undelivered_report_plugins(str(self.path), [REPORT], self.archive)
|
|
@@ -130,6 +131,22 @@ class GateTest(unittest.TestCase):
|
|
|
130
131
|
self.assertEqual(
|
|
131
132
|
cards.undelivered_report_plugins(str(root.path), ["kdtest_other"], root.archive), [])
|
|
132
133
|
|
|
134
|
+
def test_each_missing_class_names_the_type_its_slot_runs(self):
|
|
135
|
+
"""A left-tree report registers two classes on two slots with two base classes.
|
|
136
|
+
|
|
137
|
+
The refusal used to send every missing class to type reportlist, which for the tree class
|
|
138
|
+
is the wrong base class and the wrong query signature.
|
|
139
|
+
"""
|
|
140
|
+
missing = _Root(self.stack, plugin_classes=(FQCN, TREE_FQCN), plugin_types={
|
|
141
|
+
FQCN: "reportlist", TREE_FQCN: "reporttreelist"}).undelivered()
|
|
142
|
+
self.assertEqual({item["className"]: item["type"] for item in missing},
|
|
143
|
+
{FQCN: "reportlist", TREE_FQCN: "reporttreelist"})
|
|
144
|
+
|
|
145
|
+
def test_a_contract_without_plugin_types_means_data_fetch_plugins(self):
|
|
146
|
+
"""Contracts written before pluginTypes existed registered reportPlugins entries only."""
|
|
147
|
+
missing = _Root(self.stack).undelivered()
|
|
148
|
+
self.assertEqual(missing[0]["type"], "reportlist")
|
|
149
|
+
|
|
133
150
|
|
|
134
151
|
class RenderRefusesTest(unittest.TestCase):
|
|
135
152
|
"""The gate has to stop the cards, not just be available to callers."""
|
|
@@ -148,6 +165,17 @@ class RenderRefusesTest(unittest.TestCase):
|
|
|
148
165
|
self.assertIn("java-plugin-delivery", message)
|
|
149
166
|
self.assertNotIn(":::render:", message)
|
|
150
167
|
|
|
168
|
+
def test_the_refusal_names_the_delivery_type_of_each_class(self):
|
|
169
|
+
root = _Root(self.stack, plugin_classes=(FQCN, TREE_FQCN), plugin_types={
|
|
170
|
+
FQCN: "reportlist", TREE_FQCN: "reporttreelist"})
|
|
171
|
+
with self.assertRaises(cards.ReportPluginSourceMissing) as caught:
|
|
172
|
+
cards.render(str(root.path))
|
|
173
|
+
lines = str(caught.exception).splitlines()
|
|
174
|
+
tree_line = next(line for line in lines if TREE_FQCN in line)
|
|
175
|
+
self.assertIn("as type reporttreelist", tree_line)
|
|
176
|
+
list_line = next(line for line in lines if FQCN in line and TREE_FQCN not in line)
|
|
177
|
+
self.assertIn("as type reportlist", list_line)
|
|
178
|
+
|
|
151
179
|
def test_refusal_happens_before_the_archive_is_rewritten(self):
|
|
152
180
|
"""Failing after writing artifacts back would record a delivery that did not happen."""
|
|
153
181
|
root = _Root(self.stack)
|
|
@@ -196,13 +224,15 @@ class MirrorParityTest(unittest.TestCase):
|
|
|
196
224
|
return ast.dump(tree)
|
|
197
225
|
|
|
198
226
|
def test_the_gate_is_identical_in_both_trees(self):
|
|
199
|
-
|
|
200
|
-
|
|
227
|
+
app_root = SCRIPTS.parents[2] if SCRIPTS.parent.parent.name == ".locales" else SCRIPTS.parent
|
|
228
|
+
main = app_root / "scripts/render_output_cards.py"
|
|
229
|
+
mirror = app_root / ".locales/zh/scripts/render_output_cards.py"
|
|
201
230
|
self.assertTrue(mirror.is_file(), f"{mirror} is missing")
|
|
202
231
|
self.assertEqual(self._skeleton(main), self._skeleton(mirror))
|
|
203
232
|
|
|
204
233
|
def test_the_mirror_exposes_the_same_gate_api(self):
|
|
205
|
-
|
|
234
|
+
app_root = SCRIPTS.parents[2] if SCRIPTS.parent.parent.name == ".locales" else SCRIPTS.parent
|
|
235
|
+
source = (app_root / ".locales/zh/scripts/render_output_cards.py").read_text(
|
|
206
236
|
encoding="utf-8")
|
|
207
237
|
for name in ("ReportPluginSourceMissing", "undelivered_report_plugins",
|
|
208
238
|
"_refuse_undelivered", "REPORT_PLUGIN_SOURCE_MISSING"):
|
|
@@ -13,8 +13,8 @@ guard below exists because the platform would not report the mistake:
|
|
|
13
13
|
persist, so "this source cannot see it" must never be reported as a failed write;
|
|
14
14
|
- the design-state export omits any value equal to the platform default, so an absent node
|
|
15
15
|
proves the default rather than a lost write;
|
|
16
|
-
-
|
|
17
|
-
|
|
16
|
+
- 旧部署的 setReportFilters 曾写入运行时无法解析的逗号字符串,当前附件已使用 JSON;
|
|
17
|
+
修复历史 ShowFilterFields 时,不能因 key 列表相同就把格式迁移判为 no-op;
|
|
18
18
|
- ReportPluginExt is a full replacement whose accessor carries a doubled r, so a merge composed
|
|
19
19
|
without reading rreportPluginExt would silently drop every plugin already registered.
|
|
20
20
|
"""
|
|
@@ -69,7 +69,6 @@ class TargetResolutionTest(unittest.TestCase):
|
|
|
69
69
|
"filterPanel": ("reportfilterap", "ReportFilterAp", "ReportFilterPanel"),
|
|
70
70
|
"moreFilterPanel": ("reportmorefilterpanelap", "ReportMoreFilterPanelAp",
|
|
71
71
|
"ReportMoreFilterPanel"),
|
|
72
|
-
"tree": ("reporttreeap", "ReportTreeAp", "ReportTree"),
|
|
73
72
|
}
|
|
74
73
|
for name, (element, host, complex_type) in expected.items():
|
|
75
74
|
spec = _target(name)
|
|
@@ -81,6 +80,17 @@ class TargetResolutionTest(unittest.TestCase):
|
|
|
81
80
|
def test_the_form_target_writes_the_root_control_whose_key_is_the_form_number(self) -> None:
|
|
82
81
|
self.assertEqual(_target("form")["elementKey"], FORM)
|
|
83
82
|
|
|
83
|
+
def test_the_tree_target_has_no_fixed_key_because_a_user_names_the_control(self) -> None:
|
|
84
|
+
spec = _target("tree")
|
|
85
|
+
self.assertIsNone(spec["elementKey"])
|
|
86
|
+
self.assertEqual(spec["hostType"], "ReportTreeAp")
|
|
87
|
+
self.assertEqual(spec["complexType"], "ReportTree")
|
|
88
|
+
self.assertEqual(_target("tree", "my_tree", "ReportTreeAp")["elementKey"], "my_tree")
|
|
89
|
+
|
|
90
|
+
def test_a_tree_element_of_another_type_is_refused(self) -> None:
|
|
91
|
+
with self.assertRaisesRegex(ValueError, "writes a ReportTreeAp"):
|
|
92
|
+
_target("tree", "reportlistap", "ReportListAp")
|
|
93
|
+
|
|
84
94
|
def test_a_column_target_without_an_element_is_refused(self) -> None:
|
|
85
95
|
with self.assertRaisesRegex(ValueError, "needs --element"):
|
|
86
96
|
_target("column")
|
|
@@ -164,7 +174,7 @@ class AllowListTest(unittest.TestCase):
|
|
|
164
174
|
"ReportPluginExt": "registerReportPluginExt",
|
|
165
175
|
"ReportQueryPlugin": "registerReportPlugin",
|
|
166
176
|
"ReportColumns": "addReportColumns",
|
|
167
|
-
"InitField": "
|
|
177
|
+
"InitField": "default column visibility, not sorting",
|
|
168
178
|
}
|
|
169
179
|
for key, owner in cases.items():
|
|
170
180
|
with self.assertRaisesRegex(ValueError, owner):
|
|
@@ -174,7 +184,7 @@ class AllowListTest(unittest.TestCase):
|
|
|
174
184
|
payload = _normalize("filterPanel", {"ShowFilterFields": ["flt_a", "flt_b"]})
|
|
175
185
|
self.assertEqual(json.loads(payload["ShowFilterFields"]), ["flt_a", "flt_b"])
|
|
176
186
|
|
|
177
|
-
def
|
|
187
|
+
def test_show_filter_fields_refuses_the_legacy_comma_string_as_write_input(self) -> None:
|
|
178
188
|
with self.assertRaisesRegex(ValueError, "must be a non-empty array"):
|
|
179
189
|
_normalize("filterPanel", {"ShowFilterFields": "flt_a,flt_b"})
|
|
180
190
|
|
|
@@ -192,7 +202,7 @@ class ValueComparisonTest(unittest.TestCase):
|
|
|
192
202
|
self.assertFalse(report_ops._values_match(report_ops._KIND_BOOL, True, "true"))
|
|
193
203
|
|
|
194
204
|
def test_both_persisted_forms_of_show_filter_fields_read_as_the_same_key_list(self) -> None:
|
|
195
|
-
"""
|
|
205
|
+
"""读回兼容旧逗号值和 JSON;兼容读取不代表当前 writer 仍写逗号。"""
|
|
196
206
|
self.assertTrue(report_ops._values_match(
|
|
197
207
|
report_ops._KIND_IDENTIFIER_LIST, '["a","b"]', "a,b"))
|
|
198
208
|
|
|
@@ -233,7 +243,13 @@ class ReadBackKeyTest(unittest.TestCase):
|
|
|
233
243
|
self.assertNotIn("Merge", observed)
|
|
234
244
|
|
|
235
245
|
|
|
236
|
-
|
|
246
|
+
_CONTROLS = [
|
|
247
|
+
{"key": "total_amount", "type": "EntryFieldAp", "category": "control"},
|
|
248
|
+
{"key": "amt_group", "type": "EntryFieldGroupAp", "category": "control"},
|
|
249
|
+
]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _write_backend(before, after, *, expect_complex_type=None, recorder=None, controls=()):
|
|
237
253
|
"""A backend that answers the pre-read, the write and the post-read in that order."""
|
|
238
254
|
state = {"written": False}
|
|
239
255
|
|
|
@@ -251,10 +267,7 @@ def _write_backend(before, after, *, expect_complex_type=None, recorder=None):
|
|
|
251
267
|
state["written"] = True
|
|
252
268
|
return {"status": True, "errorCode": "0"}
|
|
253
269
|
if endpoint == "listElements":
|
|
254
|
-
return {"status": True, "errorCode": "0", "data":
|
|
255
|
-
{"key": "total_amount", "type": "EntryFieldAp", "category": "control"},
|
|
256
|
-
{"key": "amt_group", "type": "EntryFieldGroupAp", "category": "control"},
|
|
257
|
-
]}
|
|
270
|
+
return {"status": True, "errorCode": "0", "data": _CONTROLS + list(controls)}
|
|
258
271
|
raise AssertionError(f"unexpected endpoint {endpoint}")
|
|
259
272
|
|
|
260
273
|
return call
|
|
@@ -299,17 +312,63 @@ class WriteVerdictTest(unittest.TestCase):
|
|
|
299
312
|
with self.assertRaisesRegex(RuntimeError, "REPORT_PROPERTY_MISMATCH"):
|
|
300
313
|
report_ops.set_report_properties({}, "t", FORM, "list", {"ShowSeq": False})
|
|
301
314
|
|
|
302
|
-
def
|
|
303
|
-
"""
|
|
315
|
+
def test_a_report_without_a_tree_control_is_refused_before_the_write(self) -> None:
|
|
316
|
+
"""No public template presets one; a write would otherwise report a successful no-op."""
|
|
304
317
|
recorder = []
|
|
305
318
|
call = _write_backend({}, {}, recorder=recorder)
|
|
306
319
|
with _platform(call):
|
|
307
|
-
with self.assertRaisesRegex(ValueError,
|
|
320
|
+
with self.assertRaisesRegex(ValueError,
|
|
321
|
+
"REPORT_PROPERTY_ELEMENT_MISMATCH: .* no ReportTreeAp"):
|
|
322
|
+
report_ops.set_report_properties(
|
|
323
|
+
{}, "t", FORM, "tree", {"LeafNodeQuery": False})
|
|
324
|
+
self.assertEqual([endpoint for endpoint, _ in recorder
|
|
325
|
+
if endpoint in ("getProperties", "setComplexProperty")], [])
|
|
326
|
+
|
|
327
|
+
def test_a_host_resolving_none_of_the_keys_is_refused_before_the_write(self) -> None:
|
|
328
|
+
recorder = []
|
|
329
|
+
call = _write_backend({}, {}, recorder=recorder,
|
|
330
|
+
controls=[{"key": "my_tree", "type": "ReportTreeAp"}])
|
|
331
|
+
with _platform(call):
|
|
332
|
+
with self.assertRaisesRegex(ValueError, "REPORT_PROPERTY_ELEMENT_MISMATCH: 'my_tree'"):
|
|
308
333
|
report_ops.set_report_properties(
|
|
309
334
|
{}, "t", FORM, "tree", {"LeafNodeQuery": False})
|
|
310
335
|
self.assertEqual([endpoint for endpoint, _ in recorder if endpoint == "setComplexProperty"],
|
|
311
336
|
[])
|
|
312
337
|
|
|
338
|
+
def test_a_tree_is_found_by_its_type_whatever_key_the_user_gave_it(self) -> None:
|
|
339
|
+
recorder = []
|
|
340
|
+
call = _write_backend({"LeafNodeQuery": True}, {"LeafNodeQuery": False},
|
|
341
|
+
expect_complex_type="ReportTree", recorder=recorder,
|
|
342
|
+
controls=[{"key": "my_tree", "type": "ReportTreeAp"}])
|
|
343
|
+
with _platform(call):
|
|
344
|
+
result = report_ops.set_report_properties(
|
|
345
|
+
{}, "t", FORM, "tree", {"LeafNodeQuery": False})
|
|
346
|
+
self.assertTrue(result["verified"])
|
|
347
|
+
self.assertEqual((result["elementKey"], result["hostType"]), ("my_tree", "ReportTreeAp"))
|
|
348
|
+
self.assertEqual({p["elementKey"] for e, p in recorder if e == "getProperties"},
|
|
349
|
+
{"my_tree"})
|
|
350
|
+
write = next(p for e, p in recorder if e == "setComplexProperty")
|
|
351
|
+
self.assertEqual(write["elementKey"], "my_tree")
|
|
352
|
+
|
|
353
|
+
def test_several_tree_controls_need_element_to_name_one(self) -> None:
|
|
354
|
+
trees = [{"key": "tree_a", "type": "ReportTreeAp"},
|
|
355
|
+
{"key": "tree_b", "type": "ReportTreeAp"}]
|
|
356
|
+
call = _write_backend({"LeafNodeQuery": True}, {"LeafNodeQuery": False}, controls=trees)
|
|
357
|
+
with _platform(call):
|
|
358
|
+
with self.assertRaisesRegex(ValueError, "several ReportTreeAp controls"):
|
|
359
|
+
report_ops.set_report_properties(
|
|
360
|
+
{}, "t", FORM, "tree", {"LeafNodeQuery": False})
|
|
361
|
+
result = report_ops.set_report_properties(
|
|
362
|
+
{}, "t", FORM, "tree", {"LeafNodeQuery": False}, element_key="tree_b")
|
|
363
|
+
self.assertEqual(result["elementKey"], "tree_b")
|
|
364
|
+
|
|
365
|
+
def test_a_tree_element_naming_another_control_is_refused(self) -> None:
|
|
366
|
+
call = _write_backend({}, {}, controls=[{"key": "my_tree", "type": "ReportTreeAp"}])
|
|
367
|
+
with _platform(call):
|
|
368
|
+
with self.assertRaisesRegex(ValueError, "is not a ReportTreeAp"):
|
|
369
|
+
report_ops.set_report_properties(
|
|
370
|
+
{}, "t", FORM, "tree", {"LeafNodeQuery": False}, element_key="total_amount")
|
|
371
|
+
|
|
313
372
|
def test_a_write_of_only_unobservable_keys_is_not_reported_as_verified(self) -> None:
|
|
314
373
|
"""An empty read-back is never a passing one."""
|
|
315
374
|
call = _write_backend({}, {})
|
|
@@ -333,7 +392,7 @@ class WriteVerdictTest(unittest.TestCase):
|
|
|
333
392
|
write = next(p for e, p in recorder if e == "setComplexProperty")
|
|
334
393
|
self.assertEqual(write["elementKey"], "total_amount")
|
|
335
394
|
|
|
336
|
-
def
|
|
395
|
+
def test_a_fixed_host_write_carries_no_element_key(self) -> None:
|
|
337
396
|
recorder = []
|
|
338
397
|
call = _write_backend({"ShowSeq": True}, {"ShowSeq": False}, recorder=recorder)
|
|
339
398
|
with _platform(call):
|