hap-cli 0.8.16__tar.gz → 0.8.18__tar.gz
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.
- {hap_cli-0.8.16 → hap_cli-0.8.18}/PKG-INFO +1 -1
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/__init__.py +1 -1
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/_hapmeta/worksheet_templates.py +55 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/record.py +48 -3
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/workflow_node_dsl.py +14 -7
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/worksheet_templates.py +55 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_core.py +6 -3
- hap_cli-0.8.18/hap_cli/tests/test_select_rejects_relation_value.py +178 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_workflow_v3_semantic_cli.py +1 -1
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli.egg-info/PKG-INFO +1 -1
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli.egg-info/SOURCES.txt +1 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/MANIFEST.in +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/README.md +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/app_live_tests/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/app_live_tests/__main__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/app_live_tests/config.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/app_live_tests/harness.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/app_live_tests/smoke.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/app_live_tests/state.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/README.md +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/README_CN.md +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/__main__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/app_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/app_creator_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/app_editor_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/auth_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/calendar_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/chart_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/chat_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/chatbot_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/config_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/contact_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/department_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/group_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/icon_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/instance_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/node_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/optionset_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/page_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/post_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/record_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/region_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/role_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/update_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/upload_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/v3_registry.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/workflow_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/commands/worksheet_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/context.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/action_spec_adapter.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/_hapmeta/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/_hapmeta/control_type_codes.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/charts.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/cleanup.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/compiler.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/config.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/design/design.schema.json +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/errors.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/examples/minimal.design.json +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/executor.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/fields.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/hap.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/merge.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/recording/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/recording/console.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/recording/jsonl.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/recording/mirror.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/recording/report.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/schema.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/seed/INSTRUCTIONS.md +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/seed/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/seed/cli.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/seed/executor.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/seed/resources/attachments.json +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/seed/resources/sample.docx +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/seed/resources/sample.pdf +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/seed/resources/sample_images.json +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/seed/template.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/selftest.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/smoke.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/steps.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/store.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/validate.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/app_creator/workflow_dsl.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/auth.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/calendar_mod.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/chart.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/chat.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/chatbot.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/cli_access.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/contact.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/control_type_codes.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/department.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/editor/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/editor/apply.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/editor/componentlower.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/editor/editspec.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/editor/errors.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/editor/fieldlower.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/editor/jsonschema_mini.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/editor/models.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/editor/ops.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/editor/planner.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/editor/reader.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/field_normalizer.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/field_spec_adapter.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/filter_translator.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/flow_node.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/global_meta.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/group.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/icon_index.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/instance.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/logger.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/optionset.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/page.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/post.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/response_crypto.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/role.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/role_perm_builder.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/session.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/token_crypto.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/update.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/upload.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/v3/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/v3/dispatcher.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/v3/render.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/v3/schema.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/view_spec_adapter.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/workflow.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/core/worksheet.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/hap_cli.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/i18n.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/locale/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/locale/messages.json +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/locale/messages.schema.json +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/conftest.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_app_creator.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_app_editor.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_app_export_usage.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_auth_prerelease.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_bug_036430.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_bug_036750.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_chart.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_cli_access.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_config_cmd.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_full_e2e.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_global_meta.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_i18n.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_instance_history_cli.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_integration.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_integration_approval.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_integration_approval_actions.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_integration_calendar.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_integration_destructive.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_integration_misc.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_integration_post.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_integration_social.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_integration_v3.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_integration_workflow.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_multi_profile.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_org_id_cli.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_org_id_docs.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_parameter_conventions.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_record_field_alias.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_record_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_semantic_types_registry.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_update.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_v3_dispatcher.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_v3_session.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_workflow_list_cli.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/utils/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/utils/formatting.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/utils/options.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli/utils/parameter_mapping.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli.egg-info/dependency_links.txt +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli.egg-info/entry_points.txt +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli.egg-info/requires.txt +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/hap_cli.egg-info/top_level.txt +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/__main__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/_wftest.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/charts.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/cleanup.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/compiler.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/config.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/errors.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/executor.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/fields.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/hap.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/recording/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/recording/console.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/recording/jsonl.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/recording/mirror.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/recording/report.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/schema.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/seed/__init__.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/seed/cli.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/seed/executor.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/seed/template.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/selftest.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/smoke.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/step.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/steps.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/store.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/live_tests/workflow_dsl.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/setup.cfg +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/setup.py +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/skills/cli/hap-cli-app-editor/scripts/editspec/component.schema.json +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/skills/cli/hap-cli-app-editor/scripts/editspec/custom-action.schema.json +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/skills/cli/hap-cli-app-editor/scripts/editspec/envelope.schema.json +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/skills/cli/hap-cli-app-editor/scripts/editspec/field.schema.json +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/INDEX.json +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/create_optionset.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/create_role.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/delete_optionset.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/delete_record.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/delete_role.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/get_app_info.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/get_record_details.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/get_record_list.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/get_record_logs.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/get_record_relations.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/get_regions.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/get_role_details.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/get_role_list.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/knowledge_search.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
- {hap_cli-0.8.16 → hap_cli-0.8.18}/sources/v3-api-schema/update_optionset.yaml +0 -0
|
@@ -486,6 +486,34 @@ def build_control(
|
|
|
486
486
|
return ctrl
|
|
487
487
|
|
|
488
488
|
|
|
489
|
+
# The fields a HAP select option may legitimately carry. The base shape
|
|
490
|
+
# is {key, value, isDeleted, index, checked, color} (pd-openweb
|
|
491
|
+
# src/pages/widgetConfig/util/setting.js getDefaultOptions); ``score`` is
|
|
492
|
+
# the optional NPS/scoring weight (OptionList/AssignValue.jsx). There is
|
|
493
|
+
# no ``type`` field — an option carrying one is malformed input, not a HAP
|
|
494
|
+
# shape, so it is rejected rather than silently dropped.
|
|
495
|
+
_ALLOWED_OPTION_FIELDS = frozenset(
|
|
496
|
+
{"key", "value", "isDeleted", "index", "checked", "color", "score"}
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _option_value_is_object_shaped(v: Any) -> bool:
|
|
501
|
+
"""True when an option key/value is a relation/member/object shape
|
|
502
|
+
(``[{"sid": …}]`` / ``{...}``) instead of plain text — which would
|
|
503
|
+
only surface server-side as a ``ControlOptions`` deserialization
|
|
504
|
+
failure on the batch save."""
|
|
505
|
+
if isinstance(v, (list, tuple, dict)):
|
|
506
|
+
return True
|
|
507
|
+
if isinstance(v, str):
|
|
508
|
+
s = v.strip()
|
|
509
|
+
if s[:1] in ("[", "{"):
|
|
510
|
+
try:
|
|
511
|
+
return isinstance(json.loads(s), (list, dict))
|
|
512
|
+
except (ValueError, TypeError):
|
|
513
|
+
return False
|
|
514
|
+
return False
|
|
515
|
+
|
|
516
|
+
|
|
489
517
|
def _normalize_options(
|
|
490
518
|
opts: list[str] | list[dict[str, Any]],
|
|
491
519
|
) -> list[dict[str, Any]]:
|
|
@@ -493,6 +521,10 @@ def _normalize_options(
|
|
|
493
521
|
|
|
494
522
|
First item is marked as the default selection (checked=True) unless
|
|
495
523
|
any caller-supplied dict already sets it.
|
|
524
|
+
|
|
525
|
+
Option ``value``/``key`` must be plain text; a relation/object shape
|
|
526
|
+
or an unsupported field (e.g. ``type``) is rejected up front rather
|
|
527
|
+
than silently mangled or dropped.
|
|
496
528
|
"""
|
|
497
529
|
colors = [
|
|
498
530
|
"#C9E6FC", "#C3F2F2", "#C2F1D2", "#FFE7B1", "#FBD2BF",
|
|
@@ -502,6 +534,27 @@ def _normalize_options(
|
|
|
502
534
|
any_default = any(isinstance(o, dict) and o.get("checked") for o in opts)
|
|
503
535
|
for i, o in enumerate(opts):
|
|
504
536
|
if isinstance(o, dict):
|
|
537
|
+
unknown = set(o) - _ALLOWED_OPTION_FIELDS
|
|
538
|
+
if unknown:
|
|
539
|
+
raise ValueError(
|
|
540
|
+
f"Select option {o!r} has unsupported field(s) "
|
|
541
|
+
f"{sorted(unknown)}; an option only accepts "
|
|
542
|
+
f"{sorted(_ALLOWED_OPTION_FIELDS)}."
|
|
543
|
+
)
|
|
544
|
+
if o.get("value") in (None, ""):
|
|
545
|
+
raise ValueError(
|
|
546
|
+
f"Select option {o!r} is missing a non-empty 'value'."
|
|
547
|
+
)
|
|
548
|
+
if _option_value_is_object_shaped(o.get("value")):
|
|
549
|
+
raise ValueError(
|
|
550
|
+
f"Select option 'value' must be plain text, got a "
|
|
551
|
+
f"relation/object-shaped value: {o.get('value')!r}."
|
|
552
|
+
)
|
|
553
|
+
if o.get("key") is not None and _option_value_is_object_shaped(o.get("key")):
|
|
554
|
+
raise ValueError(
|
|
555
|
+
f"Select option 'key' must be a string, got a "
|
|
556
|
+
f"relation/object-shaped value: {o.get('key')!r}."
|
|
557
|
+
)
|
|
505
558
|
entry = {
|
|
506
559
|
"key": o.get("key") or str(uuid.uuid4()),
|
|
507
560
|
"value": o["value"],
|
|
@@ -510,6 +563,8 @@ def _normalize_options(
|
|
|
510
563
|
"checked": o.get("checked", False),
|
|
511
564
|
"color": o.get("color", colors[i % len(colors)]),
|
|
512
565
|
}
|
|
566
|
+
if "score" in o:
|
|
567
|
+
entry["score"] = o["score"]
|
|
513
568
|
else:
|
|
514
569
|
entry = {
|
|
515
570
|
"key": str(uuid.uuid4()),
|
|
@@ -22,13 +22,56 @@ DISCUSSION_SOURCE_WORKSHEET_ROW = 8
|
|
|
22
22
|
_LEGACY_SKIP_TYPES = {30, 31, 32, 33, 37, 38, 53}
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def _option_field_label(ctrl: dict[str, Any]) -> str:
|
|
26
|
+
return str(ctrl.get("controlName") or ctrl.get("controlId") or "option field")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _reject_relation_shaped_option(ctrl: dict[str, Any], item: Any) -> None:
|
|
30
|
+
"""Fail loudly when a select cell is handed a relation/member/sub-table
|
|
31
|
+
*object* shape instead of an option value.
|
|
32
|
+
|
|
33
|
+
A single/multi-select cell legitimately carries only an option key, a
|
|
34
|
+
known label, or an ``add_<text>`` new-option directive — never a
|
|
35
|
+
``[{"sid": rowId}]`` / ``{...}`` structure. When such a value slips
|
|
36
|
+
through (e.g. row data maps a relation value to the wrong child field),
|
|
37
|
+
the old code passed it straight to the server, which then failed
|
|
38
|
+
deserializing ``ControlOptions`` on the batch save. Catching it here
|
|
39
|
+
points the blame at the offending field instead of a generic 500.
|
|
40
|
+
"""
|
|
41
|
+
looks_object = isinstance(item, (list, tuple, dict))
|
|
42
|
+
if not looks_object and isinstance(item, str):
|
|
43
|
+
stripped = item.strip()
|
|
44
|
+
if stripped[:1] in ("[", "{"):
|
|
45
|
+
try:
|
|
46
|
+
parsed = json.loads(stripped)
|
|
47
|
+
except (ValueError, TypeError):
|
|
48
|
+
parsed = None
|
|
49
|
+
looks_object = isinstance(parsed, (list, dict))
|
|
50
|
+
if looks_object:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
f"Field '{_option_field_label(ctrl)}' is a select field and "
|
|
53
|
+
f"expects an option key, a label, or an 'add_<text>' new-option "
|
|
54
|
+
f"value, but got a relation/object-shaped value: {item!r}. "
|
|
55
|
+
f"This value belongs to a relation or member field, not this "
|
|
56
|
+
f"select field — check the field mapping."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
25
60
|
def _resolve_option_keys(ctrl: dict[str, Any], raw: Any) -> list[str]:
|
|
26
61
|
"""Map option labels (or already-keys) to the option `key` values
|
|
27
|
-
the legacy AddWorksheetRow expects (it takes keys, not labels).
|
|
62
|
+
the legacy AddWorksheetRow expects (it takes keys, not labels).
|
|
63
|
+
|
|
64
|
+
Relation/member/sub-table *object* shapes (``[{"sid": …}]`` etc.) are
|
|
65
|
+
rejected up front — they are never valid option values and would only
|
|
66
|
+
surface server-side as a ``ControlOptions`` deserialization failure on
|
|
67
|
+
the batch save.
|
|
68
|
+
"""
|
|
28
69
|
if raw is None or raw == "":
|
|
29
70
|
return []
|
|
30
71
|
if isinstance(raw, (list, tuple)):
|
|
31
|
-
items =
|
|
72
|
+
items: list[Any] = list(raw)
|
|
73
|
+
elif isinstance(raw, dict):
|
|
74
|
+
items = [raw]
|
|
32
75
|
else:
|
|
33
76
|
items = [s for s in str(raw).split(",") if s != ""]
|
|
34
77
|
by_label: dict[str, str] = {}
|
|
@@ -40,7 +83,9 @@ def _resolve_option_keys(ctrl: dict[str, Any], raw: Any) -> list[str]:
|
|
|
40
83
|
by_label[str(opt.get("value", ""))] = key
|
|
41
84
|
out: list[str] = []
|
|
42
85
|
for it in items:
|
|
43
|
-
|
|
86
|
+
_reject_relation_shaped_option(ctrl, it)
|
|
87
|
+
s = str(it)
|
|
88
|
+
out.append(s if s in valid_keys else by_label.get(s, s))
|
|
44
89
|
return out
|
|
45
90
|
|
|
46
91
|
|
|
@@ -684,17 +684,24 @@ def get_trigger_ref(session, process_id: str) -> dict[str, str]:
|
|
|
684
684
|
|
|
685
685
|
|
|
686
686
|
# Worksheet-event trigger event codes (triggerId on the flowNodeType-0
|
|
687
|
-
# node).
|
|
688
|
-
#
|
|
687
|
+
# node). Authoritative values from pd-openweb TRIGGER_ID enum
|
|
688
|
+
# (src/pages/workflow/WorkflowSettings/enum.js) and the trigger radio
|
|
689
|
+
# options in Detail/Start/WorksheetContent.jsx:
|
|
690
|
+
# ADD='1' (仅新增记录), EDIT='2' (当新增或更新记录), DELETE='3' (删除记录),
|
|
691
|
+
# ONLY_EDIT='4' (仅更新记录).
|
|
692
|
+
# Note the non-sequential mapping: create_or_update is '2' and delete is
|
|
693
|
+
# '3' — do NOT "tidy" these into 1/2/3/4 order. (Earlier code had
|
|
694
|
+
# create_or_update='3', which the backend interprets as 删除记录 — that was
|
|
695
|
+
# the cause of bug: "创建或更新" workflows showing up as 记录删除时触发.)
|
|
689
696
|
WORKSHEET_TRIGGER_EVENTS = {
|
|
690
|
-
"create": "1",
|
|
691
|
-
"update": "
|
|
692
|
-
"create_or_update": "
|
|
693
|
-
"delete": "
|
|
697
|
+
"create": "1", # 仅新增记录 (TRIGGER_ID.ADD)
|
|
698
|
+
"update": "4", # 仅更新记录 (TRIGGER_ID.ONLY_EDIT)
|
|
699
|
+
"create_or_update": "2", # 当新增或更新记录 (TRIGGER_ID.EDIT)
|
|
700
|
+
"delete": "3", # 删除记录 (TRIGGER_ID.DELETE)
|
|
694
701
|
# Design-vocabulary aliases (mcp_workflow_build_tool.json#create_process
|
|
695
702
|
# trigger.config.event uses the backend database values).
|
|
696
703
|
"add": "1",
|
|
697
|
-
"add_or_update": "
|
|
704
|
+
"add_or_update": "2",
|
|
698
705
|
}
|
|
699
706
|
|
|
700
707
|
|
|
@@ -471,6 +471,34 @@ def build_control(
|
|
|
471
471
|
return ctrl
|
|
472
472
|
|
|
473
473
|
|
|
474
|
+
# The fields a HAP select option may legitimately carry. The base shape
|
|
475
|
+
# is {key, value, isDeleted, index, checked, color} (pd-openweb
|
|
476
|
+
# src/pages/widgetConfig/util/setting.js getDefaultOptions); ``score`` is
|
|
477
|
+
# the optional NPS/scoring weight (OptionList/AssignValue.jsx). There is
|
|
478
|
+
# no ``type`` field — an option carrying one is malformed input, not a HAP
|
|
479
|
+
# shape, so it is rejected rather than silently dropped.
|
|
480
|
+
_ALLOWED_OPTION_FIELDS = frozenset(
|
|
481
|
+
{"key", "value", "isDeleted", "index", "checked", "color", "score"}
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _option_value_is_object_shaped(v: Any) -> bool:
|
|
486
|
+
"""True when an option key/value is a relation/member/object shape
|
|
487
|
+
(``[{"sid": …}]`` / ``{...}``) instead of plain text — which would
|
|
488
|
+
only surface server-side as a ``ControlOptions`` deserialization
|
|
489
|
+
failure on the batch save."""
|
|
490
|
+
if isinstance(v, (list, tuple, dict)):
|
|
491
|
+
return True
|
|
492
|
+
if isinstance(v, str):
|
|
493
|
+
s = v.strip()
|
|
494
|
+
if s[:1] in ("[", "{"):
|
|
495
|
+
try:
|
|
496
|
+
return isinstance(json.loads(s), (list, dict))
|
|
497
|
+
except (ValueError, TypeError):
|
|
498
|
+
return False
|
|
499
|
+
return False
|
|
500
|
+
|
|
501
|
+
|
|
474
502
|
def _normalize_options(
|
|
475
503
|
opts: list[str] | list[dict[str, Any]],
|
|
476
504
|
) -> list[dict[str, Any]]:
|
|
@@ -478,6 +506,10 @@ def _normalize_options(
|
|
|
478
506
|
|
|
479
507
|
First item is marked as the default selection (checked=True) unless
|
|
480
508
|
any caller-supplied dict already sets it.
|
|
509
|
+
|
|
510
|
+
Option ``value``/``key`` must be plain text; a relation/object shape
|
|
511
|
+
or an unsupported field (e.g. ``type``) is rejected up front rather
|
|
512
|
+
than silently mangled or dropped.
|
|
481
513
|
"""
|
|
482
514
|
colors = [
|
|
483
515
|
"#C9E6FC", "#C3F2F2", "#C2F1D2", "#FFE7B1", "#FBD2BF",
|
|
@@ -487,6 +519,27 @@ def _normalize_options(
|
|
|
487
519
|
any_default = any(isinstance(o, dict) and o.get("checked") for o in opts)
|
|
488
520
|
for i, o in enumerate(opts):
|
|
489
521
|
if isinstance(o, dict):
|
|
522
|
+
unknown = set(o) - _ALLOWED_OPTION_FIELDS
|
|
523
|
+
if unknown:
|
|
524
|
+
raise ValueError(
|
|
525
|
+
f"Select option {o!r} has unsupported field(s) "
|
|
526
|
+
f"{sorted(unknown)}; an option only accepts "
|
|
527
|
+
f"{sorted(_ALLOWED_OPTION_FIELDS)}."
|
|
528
|
+
)
|
|
529
|
+
if o.get("value") in (None, ""):
|
|
530
|
+
raise ValueError(
|
|
531
|
+
f"Select option {o!r} is missing a non-empty 'value'."
|
|
532
|
+
)
|
|
533
|
+
if _option_value_is_object_shaped(o.get("value")):
|
|
534
|
+
raise ValueError(
|
|
535
|
+
f"Select option 'value' must be plain text, got a "
|
|
536
|
+
f"relation/object-shaped value: {o.get('value')!r}."
|
|
537
|
+
)
|
|
538
|
+
if o.get("key") is not None and _option_value_is_object_shaped(o.get("key")):
|
|
539
|
+
raise ValueError(
|
|
540
|
+
f"Select option 'key' must be a string, got a "
|
|
541
|
+
f"relation/object-shaped value: {o.get('key')!r}."
|
|
542
|
+
)
|
|
490
543
|
entry = {
|
|
491
544
|
"key": o.get("key") or str(uuid.uuid4()),
|
|
492
545
|
"value": o["value"],
|
|
@@ -495,6 +548,8 @@ def _normalize_options(
|
|
|
495
548
|
"checked": o.get("checked", False),
|
|
496
549
|
"color": o.get("color", colors[i % len(colors)]),
|
|
497
550
|
}
|
|
551
|
+
if "score" in o:
|
|
552
|
+
entry["score"] = o["score"]
|
|
498
553
|
else:
|
|
499
554
|
entry = {
|
|
500
555
|
"key": str(uuid.uuid4()),
|
|
@@ -8820,10 +8820,13 @@ class TestLiveWireFixes:
|
|
|
8820
8820
|
|
|
8821
8821
|
def test_worksheet_trigger_event_codes(self):
|
|
8822
8822
|
from hap_cli.core import workflow_node_dsl as dsl
|
|
8823
|
+
# Authoritative TRIGGER_ID enum: ADD=1, EDIT(create_or_update)=2,
|
|
8824
|
+
# DELETE=3, ONLY_EDIT(update)=4. The mapping is intentionally
|
|
8825
|
+
# non-sequential — see WORKSHEET_TRIGGER_EVENTS comment.
|
|
8823
8826
|
assert dsl.WORKSHEET_TRIGGER_EVENTS["create"] == "1"
|
|
8824
|
-
assert dsl.WORKSHEET_TRIGGER_EVENTS["update"] == "
|
|
8825
|
-
assert dsl.WORKSHEET_TRIGGER_EVENTS["create_or_update"] == "
|
|
8826
|
-
assert dsl.WORKSHEET_TRIGGER_EVENTS["delete"] == "
|
|
8827
|
+
assert dsl.WORKSHEET_TRIGGER_EVENTS["update"] == "4"
|
|
8828
|
+
assert dsl.WORKSHEET_TRIGGER_EVENTS["create_or_update"] == "2"
|
|
8829
|
+
assert dsl.WORKSHEET_TRIGGER_EVENTS["delete"] == "3"
|
|
8827
8830
|
|
|
8828
8831
|
|
|
8829
8832
|
class TestRolePermBuilder:
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""A single/multi-select cell must reject relation/object-shaped values.
|
|
2
|
+
|
|
3
|
+
Traced from a live backend error on ``WorksheetService/AddWSRowsBatch``
|
|
4
|
+
(the batch service the main-site ``Worksheet/AddWorksheetRow`` dispatches
|
|
5
|
+
sub-table child rows to). A single-select child field (Type 11) had been
|
|
6
|
+
handed a RELATE_SHEET-shaped value ``[{"sid": "<rowId>"}]`` instead of an
|
|
7
|
+
option key, and the server failed deserialization:
|
|
8
|
+
|
|
9
|
+
Cannot deserialize the current JSON array (e.g. [1,2,3]) into type
|
|
10
|
+
'MD.Worksheet.Core.Entities.ControlOptions' ...
|
|
11
|
+
|
|
12
|
+
Before this fix, ``_resolve_option_keys`` passed any value it could not
|
|
13
|
+
map to a known key/label straight through to the server (the same soft
|
|
14
|
+
spot as bug #036430). A select cell legitimately carries only an option
|
|
15
|
+
key, a known label, or an ``add_<text>`` new-option directive (evidence:
|
|
16
|
+
pd-openweb src/components/Form/core/utils.js, formatControlToServer
|
|
17
|
+
case 10/11). An object / array-of-objects value is structurally a
|
|
18
|
+
relation/member/sub-table shape and is always wrong here, so it is now
|
|
19
|
+
rejected loudly at the CLI boundary instead of poisoning the server.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
from hap_cli.core import record as rec_mod
|
|
27
|
+
from hap_cli.core import worksheet_templates as wt
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_SELECT_CTRL = {
|
|
31
|
+
"controlId": "c_status",
|
|
32
|
+
"controlName": "状态",
|
|
33
|
+
"type": 11,
|
|
34
|
+
"options": [
|
|
35
|
+
{"key": "k_pass", "value": "已通过"},
|
|
36
|
+
{"key": "k_reject", "value": "已驳回"},
|
|
37
|
+
],
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# A sub-table child whose option metadata is NOT carried on the child
|
|
41
|
+
# control (relationControls are often slim) — the structural rejection
|
|
42
|
+
# must still fire without an option set to validate against.
|
|
43
|
+
_SELECT_CTRL_NO_OPTS = {
|
|
44
|
+
"controlId": "cc_status",
|
|
45
|
+
"controlName": "子状态",
|
|
46
|
+
"type": 11,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_RELATION_VALUE = [{"sid": "3587620f-f900-4f9c-9803-43827e69ff21"}]
|
|
50
|
+
_RELATION_JSON = json.dumps(_RELATION_VALUE) # '[{"sid": "..."}]'
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestResolveOptionKeysRejectsRelationShape:
|
|
54
|
+
def test_rejects_list_of_dicts(self):
|
|
55
|
+
with pytest.raises(ValueError, match="状态"):
|
|
56
|
+
rec_mod._resolve_option_keys(_SELECT_CTRL, _RELATION_VALUE)
|
|
57
|
+
|
|
58
|
+
def test_rejects_relation_json_string(self):
|
|
59
|
+
with pytest.raises(ValueError):
|
|
60
|
+
rec_mod._resolve_option_keys(_SELECT_CTRL, _RELATION_JSON)
|
|
61
|
+
|
|
62
|
+
def test_rejects_single_dict(self):
|
|
63
|
+
with pytest.raises(ValueError):
|
|
64
|
+
rec_mod._resolve_option_keys(
|
|
65
|
+
_SELECT_CTRL, {"sid": "3587620f-f900-4f9c-9803-43827e69ff21"})
|
|
66
|
+
|
|
67
|
+
def test_rejects_even_without_option_metadata(self):
|
|
68
|
+
# No options to validate against, but the shape is still wrong.
|
|
69
|
+
with pytest.raises(ValueError):
|
|
70
|
+
rec_mod._resolve_option_keys(_SELECT_CTRL_NO_OPTS, _RELATION_VALUE)
|
|
71
|
+
|
|
72
|
+
def test_rejects_json_object_string(self):
|
|
73
|
+
with pytest.raises(ValueError):
|
|
74
|
+
rec_mod._resolve_option_keys(_SELECT_CTRL, '{"sid": "abc"}')
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TestResolveOptionKeysStillAcceptsLegitValues:
|
|
78
|
+
def test_valid_key(self):
|
|
79
|
+
assert rec_mod._resolve_option_keys(_SELECT_CTRL, "k_pass") == ["k_pass"]
|
|
80
|
+
|
|
81
|
+
def test_label_maps_to_key(self):
|
|
82
|
+
assert rec_mod._resolve_option_keys(_SELECT_CTRL, "已通过") == ["k_pass"]
|
|
83
|
+
|
|
84
|
+
def test_add_new_option_directive_passes_through(self):
|
|
85
|
+
# `add_<text>` is how the form encodes "create this option on save".
|
|
86
|
+
assert rec_mod._resolve_option_keys(
|
|
87
|
+
_SELECT_CTRL, "add_新选项") == ["add_新选项"]
|
|
88
|
+
|
|
89
|
+
def test_unknown_plain_text_passes_through(self):
|
|
90
|
+
# Plain scalar text is not an object shape — left to the server
|
|
91
|
+
# (may be a key we cannot see, or a new option). Not our bug.
|
|
92
|
+
assert rec_mod._resolve_option_keys(
|
|
93
|
+
_SELECT_CTRL_NO_OPTS, "甲") == ["甲"]
|
|
94
|
+
|
|
95
|
+
def test_comma_list_of_keys(self):
|
|
96
|
+
assert rec_mod._resolve_option_keys(
|
|
97
|
+
_SELECT_CTRL, "k_pass,k_reject") == ["k_pass", "k_reject"]
|
|
98
|
+
|
|
99
|
+
def test_empty_is_cleared(self):
|
|
100
|
+
assert rec_mod._resolve_option_keys(_SELECT_CTRL, "") == []
|
|
101
|
+
assert rec_mod._resolve_option_keys(_SELECT_CTRL, None) == []
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TestSubTableChildSelectRejectsRelationValue:
|
|
105
|
+
"""The reported scenario: a sub-table (34) child single-select gets a
|
|
106
|
+
relation-shaped value while the parent record is created — the child
|
|
107
|
+
row is what the backend batch-saves."""
|
|
108
|
+
|
|
109
|
+
def test_sublist_child_select_relation_value_is_rejected(self):
|
|
110
|
+
sublist_ctrl = {
|
|
111
|
+
"controlId": "c_sub",
|
|
112
|
+
"controlName": "明细",
|
|
113
|
+
"type": 34,
|
|
114
|
+
"relationControls": [_SELECT_CTRL_NO_OPTS],
|
|
115
|
+
}
|
|
116
|
+
rows = [{"子状态": _RELATION_VALUE}]
|
|
117
|
+
with pytest.raises(ValueError):
|
|
118
|
+
rec_mod._legacy_cell_value(sublist_ctrl, rows)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestOptionDefinitionRejectsBadShapes:
|
|
122
|
+
"""The other injection point: defining a select field's *options*.
|
|
123
|
+
|
|
124
|
+
A select option is only ``{key, value, isDeleted, index, checked,
|
|
125
|
+
color}`` (evidence: pd-openweb widgetConfig/util/setting.js). Option
|
|
126
|
+
``value`` / ``key`` must be plain text — never a relation/object shape
|
|
127
|
+
— and an option carries no ``type`` field. Before this fix,
|
|
128
|
+
``_normalize_options`` passed any value/key straight through and
|
|
129
|
+
silently dropped unknown keys like ``type``.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def test_option_value_object_shape_rejected(self):
|
|
133
|
+
with pytest.raises(ValueError):
|
|
134
|
+
wt.build_control("DROP_DOWN", "状态",
|
|
135
|
+
options=[{"value": [{"sid": "abc"}]}])
|
|
136
|
+
|
|
137
|
+
def test_option_value_relation_json_string_rejected(self):
|
|
138
|
+
with pytest.raises(ValueError):
|
|
139
|
+
wt.build_control("DROP_DOWN", "状态",
|
|
140
|
+
options=[{"value": _RELATION_JSON}])
|
|
141
|
+
|
|
142
|
+
def test_option_key_object_shape_rejected(self):
|
|
143
|
+
with pytest.raises(ValueError):
|
|
144
|
+
wt.build_control("DROP_DOWN", "状态",
|
|
145
|
+
options=[{"key": [{"sid": "abc"}], "value": "甲"}])
|
|
146
|
+
|
|
147
|
+
def test_option_unknown_type_field_rejected(self):
|
|
148
|
+
with pytest.raises(ValueError, match="type"):
|
|
149
|
+
wt.build_control("DROP_DOWN", "状态",
|
|
150
|
+
options=[{"value": "甲", "type": 0}])
|
|
151
|
+
|
|
152
|
+
def test_option_missing_value_rejected(self):
|
|
153
|
+
with pytest.raises(ValueError):
|
|
154
|
+
wt.build_control("DROP_DOWN", "状态", options=[{"key": "k1"}])
|
|
155
|
+
|
|
156
|
+
# --- legit option definitions still build ---
|
|
157
|
+
|
|
158
|
+
def test_string_options_ok(self):
|
|
159
|
+
out = wt.build_control("DROP_DOWN", "状态", options=["甲", "乙"])
|
|
160
|
+
assert [o["value"] for o in out["options"]] == ["甲", "乙"]
|
|
161
|
+
|
|
162
|
+
def test_dict_option_with_known_fields_ok(self):
|
|
163
|
+
out = wt.build_control(
|
|
164
|
+
"DROP_DOWN", "状态",
|
|
165
|
+
options=[{"value": "甲", "key": "k1", "checked": True}])
|
|
166
|
+
assert out["options"][0]["key"] == "k1"
|
|
167
|
+
assert out["options"][0]["value"] == "甲"
|
|
168
|
+
assert out["options"][0]["checked"] is True
|
|
169
|
+
|
|
170
|
+
def test_scored_option_is_allowed_and_preserved(self):
|
|
171
|
+
# `score` is a legitimate NPS/scoring field — it must not be
|
|
172
|
+
# rejected (it is not a relation shape) and must survive to the
|
|
173
|
+
# wire instead of being silently dropped.
|
|
174
|
+
out = wt.build_control(
|
|
175
|
+
"DROP_DOWN", "满意度",
|
|
176
|
+
options=[{"value": "满意", "score": 5}])
|
|
177
|
+
assert out["options"][0]["value"] == "满意"
|
|
178
|
+
assert out["options"][0]["score"] == 5
|
|
@@ -452,7 +452,7 @@ class TestTriggerSpecLowering:
|
|
|
452
452
|
assert args["trigger_fields"] == ["f1"]
|
|
453
453
|
assert args["trigger_filter"] == {"logic": "and", "items": []}
|
|
454
454
|
assert dsl.WORKSHEET_TRIGGER_EVENTS["add"] == "1"
|
|
455
|
-
assert dsl.WORKSHEET_TRIGGER_EVENTS["add_or_update"] == "
|
|
455
|
+
assert dsl.WORKSHEET_TRIGGER_EVENTS["add_or_update"] == "2"
|
|
456
456
|
|
|
457
457
|
def test_schedule(self):
|
|
458
458
|
args = dsl.v3_trigger_to_batch_args({
|
|
@@ -177,6 +177,7 @@ hap_cli/tests/test_parameter_mapping_registry.py
|
|
|
177
177
|
hap_cli/tests/test_record_batch_v3_semantic_cli.py
|
|
178
178
|
hap_cli/tests/test_record_field_alias.py
|
|
179
179
|
hap_cli/tests/test_record_v3_semantic_cli.py
|
|
180
|
+
hap_cli/tests/test_select_rejects_relation_value.py
|
|
180
181
|
hap_cli/tests/test_semantic_types_registry.py
|
|
181
182
|
hap_cli/tests/test_update.py
|
|
182
183
|
hap_cli/tests/test_v3_api_schema_loader.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|