hap-cli 0.8.19__tar.gz → 0.8.20__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.19 → hap_cli-0.8.20}/PKG-INFO +1 -1
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/__init__.py +1 -1
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/record.py +49 -3
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/session.py +24 -13
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/worksheet.py +19 -4
- hap_cli-0.8.20/hap_cli/tests/test_control_lookup_guard.py +179 -0
- hap_cli-0.8.20/hap_cli/tests/test_workflow_envelope_errors.py +112 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli.egg-info/PKG-INFO +1 -1
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli.egg-info/SOURCES.txt +2 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/MANIFEST.in +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/README.md +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/app_live_tests/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/app_live_tests/__main__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/app_live_tests/config.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/app_live_tests/harness.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/app_live_tests/smoke.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/app_live_tests/state.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/README.md +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/README_CN.md +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/__main__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/app_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/app_creator_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/app_editor_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/auth_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/calendar_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/chart_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/chat_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/chatbot_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/config_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/contact_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/department_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/group_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/icon_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/instance_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/node_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/optionset_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/page_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/post_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/record_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/region_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/role_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/update_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/upload_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/v3_registry.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/workflow_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/worksheet_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/context.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/action_spec_adapter.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/_hapmeta/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/_hapmeta/control_type_codes.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/_hapmeta/worksheet_templates.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/charts.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/cleanup.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/compiler.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/config.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/design/design.schema.json +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/errors.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/examples/minimal.design.json +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/executor.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/fields.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/hap.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/merge.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/recording/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/recording/console.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/recording/jsonl.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/recording/mirror.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/recording/report.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/schema.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/INSTRUCTIONS.md +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/cli.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/executor.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/resources/attachments.json +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/resources/sample.docx +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/resources/sample.pdf +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/resources/sample_images.json +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/template.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/selftest.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/smoke.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/steps.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/store.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/validate.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/workflow_dsl.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/auth.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/calendar_mod.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/chart.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/chat.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/chatbot.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/cli_access.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/contact.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/control_type_codes.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/department.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/apply.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/componentlower.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/editspec.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/errors.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/fieldlower.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/jsonschema_mini.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/models.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/ops.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/planner.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/reader.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/field_normalizer.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/field_spec_adapter.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/filter_translator.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/flow_node.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/global_meta.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/group.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/icon_index.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/instance.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/logger.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/optionset.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/page.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/post.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/response_crypto.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/role.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/role_perm_builder.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/token_crypto.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/update.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/upload.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/v3/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/v3/dispatcher.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/v3/render.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/v3/schema.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/view_spec_adapter.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/workflow.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/workflow_node_dsl.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/worksheet_templates.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/hap_cli.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/i18n.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/locale/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/locale/messages.json +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/locale/messages.schema.json +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/conftest.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_app_creator.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_app_editor.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_app_export_usage.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_auth_prerelease.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_bug_036430.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_bug_036750.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_chart.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_cli_access.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_config_cmd.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_core.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_full_e2e.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_global_meta.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_i18n.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_instance_history_cli.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_approval.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_approval_actions.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_calendar.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_destructive.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_misc.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_post.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_social.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_v3.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_workflow.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_member_relation_array_wrap.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_multi_profile.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_org_id_cli.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_org_id_docs.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_parameter_conventions.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_record_field_alias.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_record_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_select_rejects_relation_value.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_semantic_types_registry.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_update.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_v3_dispatcher.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_v3_session.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_workflow_list_cli.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_workflow_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/utils/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/utils/formatting.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/utils/options.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/utils/parameter_mapping.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli.egg-info/dependency_links.txt +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli.egg-info/entry_points.txt +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli.egg-info/requires.txt +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli.egg-info/top_level.txt +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/__main__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/_wftest.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/charts.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/cleanup.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/compiler.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/config.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/errors.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/executor.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/fields.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/hap.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/recording/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/recording/console.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/recording/jsonl.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/recording/mirror.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/recording/report.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/schema.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/seed/__init__.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/seed/cli.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/seed/executor.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/seed/template.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/selftest.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/smoke.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/step.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/steps.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/store.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/workflow_dsl.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/setup.cfg +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/setup.py +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/skills/cli/hap-cli-app-editor/scripts/editspec/component.schema.json +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/skills/cli/hap-cli-app-editor/scripts/editspec/custom-action.schema.json +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/skills/cli/hap-cli-app-editor/scripts/editspec/envelope.schema.json +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/skills/cli/hap-cli-app-editor/scripts/editspec/field.schema.json +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/INDEX.json +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/create_optionset.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/create_role.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/delete_optionset.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/delete_record.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/delete_role.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_app_info.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_details.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_list.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_logs.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_relations.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_regions.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_role_details.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_role_list.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/knowledge_search.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
- {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/update_optionset.yaml +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Record CRUD module for Mingdao HAP."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
import uuid
|
|
5
6
|
from typing import Any, Optional
|
|
6
7
|
|
|
@@ -8,6 +9,8 @@ from hap_cli.core.session import Session, APIError
|
|
|
8
9
|
from hap_cli.core import worksheet as ws_mod
|
|
9
10
|
from hap_cli.core import global_meta as gm
|
|
10
11
|
|
|
12
|
+
logger = logging.getLogger("hap_cli.record")
|
|
13
|
+
|
|
11
14
|
# Discussion source types — pd-openweb src/components/comment/config.js
|
|
12
15
|
# WORKSHEET = 7 whole-worksheet discussion
|
|
13
16
|
# WORKSHEETROW = 8 single record/row discussion
|
|
@@ -383,6 +386,41 @@ def _legacy_cell_value(ctrl: dict[str, Any], value: Any) -> str:
|
|
|
383
386
|
return "" if value is None else str(value)
|
|
384
387
|
|
|
385
388
|
|
|
389
|
+
def _controls_for_write(
|
|
390
|
+
session: Session, worksheet_id: str,
|
|
391
|
+
) -> list[dict[str, Any]]:
|
|
392
|
+
"""Fetch the worksheet's controls for a write, refusing an empty list.
|
|
393
|
+
|
|
394
|
+
Every write path type-encodes its cells against this metadata, so an
|
|
395
|
+
empty list silently disables the encoder: option/member/relation/
|
|
396
|
+
sub-table values would all take the "unknown field -> send the raw
|
|
397
|
+
value" fallback and reach the server in whatever shape the caller
|
|
398
|
+
happened to type. That is how a hand-written 1-D sub-table value
|
|
399
|
+
(``[{childControlId: value}, ...]``) escaped to the server and failed
|
|
400
|
+
deserialization into ``List<Cell>`` — the encoder would otherwise have
|
|
401
|
+
rewritten it to the 2-D form.
|
|
402
|
+
|
|
403
|
+
``get_worksheet_controls`` answers ``[]`` for any response that does
|
|
404
|
+
not carry a ``controls`` list — a soft failure, ``data: null``, or a
|
|
405
|
+
permission-degraded envelope — none of which raise. A real worksheet
|
|
406
|
+
always has controls, so an empty list means "we could not read the
|
|
407
|
+
fields", never "this worksheet has none". Abort before writing.
|
|
408
|
+
"""
|
|
409
|
+
controls = ws_mod.get_worksheet_controls(session, worksheet_id)
|
|
410
|
+
if not controls:
|
|
411
|
+
logger.warning(
|
|
412
|
+
"GetWorksheetControls returned no fields for worksheet %s; "
|
|
413
|
+
"aborting the write instead of sending unencoded cells",
|
|
414
|
+
worksheet_id,
|
|
415
|
+
)
|
|
416
|
+
raise ValueError(
|
|
417
|
+
f"Could not read the fields of worksheet {worksheet_id}, so "
|
|
418
|
+
"nothing was written. Check the worksheet id and that your "
|
|
419
|
+
"account can open it, then try again."
|
|
420
|
+
)
|
|
421
|
+
return controls
|
|
422
|
+
|
|
423
|
+
|
|
386
424
|
def encode_controls_by_id(
|
|
387
425
|
session: Session,
|
|
388
426
|
worksheet_id: str,
|
|
@@ -419,7 +457,7 @@ def encode_controls_by_id(
|
|
|
419
457
|
(in-place edits keyed by ``rowid``, fresh adds, and deletes — see
|
|
420
458
|
:func:`_sublist_update_value`).
|
|
421
459
|
"""
|
|
422
|
-
controls =
|
|
460
|
+
controls = _controls_for_write(session, worksheet_id)
|
|
423
461
|
by_id: dict[str, dict[str, Any]] = {
|
|
424
462
|
c.get("controlId"): c for c in controls if c.get("controlId")
|
|
425
463
|
}
|
|
@@ -434,6 +472,14 @@ def encode_controls_by_id(
|
|
|
434
472
|
raw = rc.get("value")
|
|
435
473
|
ctrl = by_id.get(cid) or by_ref.get(cid)
|
|
436
474
|
if ctrl is None:
|
|
475
|
+
# Deliberate escape hatch (see docstring), but a value that
|
|
476
|
+
# skips the type encoder is exactly how a malformed cell
|
|
477
|
+
# reaches the server, so leave a trace: this is the only
|
|
478
|
+
# place a write can go out unencoded.
|
|
479
|
+
logger.warning(
|
|
480
|
+
"Field %r is not on worksheet %s; its value is sent "
|
|
481
|
+
"unencoded (no type conversion applied)", cid, worksheet_id,
|
|
482
|
+
)
|
|
437
483
|
out.append({"controlId": cid, "value": raw})
|
|
438
484
|
continue
|
|
439
485
|
out.append({
|
|
@@ -603,7 +649,7 @@ def batch_create_by_name(
|
|
|
603
649
|
by child controlId; single/multi select -> option keys; etc.).
|
|
604
650
|
Evidence: hap_cli/tests/answer_create_record.md.
|
|
605
651
|
"""
|
|
606
|
-
controls =
|
|
652
|
+
controls = _controls_for_write(session, worksheet_id)
|
|
607
653
|
by_name: dict[str, dict[str, Any]] = {}
|
|
608
654
|
for c in controls:
|
|
609
655
|
for key in (c.get("controlName"), c.get("alias"),
|
|
@@ -662,7 +708,7 @@ def batch_update_by_name(
|
|
|
662
708
|
:func:`_sublist_update_value`). Record updates never use the V3
|
|
663
709
|
rows endpoint — V3 silently drops relation / sub-table cells.
|
|
664
710
|
"""
|
|
665
|
-
controls =
|
|
711
|
+
controls = _controls_for_write(session, worksheet_id)
|
|
666
712
|
by_name: dict[str, dict[str, Any]] = {}
|
|
667
713
|
for c in controls:
|
|
668
714
|
for key in (c.get("controlName"), c.get("alias"),
|
|
@@ -529,28 +529,39 @@ class Session:
|
|
|
529
529
|
# Detect each and unwrap accordingly.
|
|
530
530
|
if not isinstance(result, dict):
|
|
531
531
|
return result
|
|
532
|
+
# A non-empty ``exception`` means the call failed, whatever envelope
|
|
533
|
+
# it arrived in. This is the frontend's own and only test — mdyAPI
|
|
534
|
+
# rejects on ``if (responseData.exception)`` alone and never looks at
|
|
535
|
+
# ``status`` (pd-openweb src/common/global.js:846-849). Checking it
|
|
536
|
+
# before any unwrapping is what makes workflow failures visible: the
|
|
537
|
+
# workflow envelope has no ``state``, so it used to skip the error
|
|
538
|
+
# branch entirely and hand the caller a "successful" empty result.
|
|
539
|
+
exception_msg = str(result.get("exception", "") or "")
|
|
540
|
+
if exception_msg:
|
|
541
|
+
code = result.get("code", result.get("state"))
|
|
542
|
+
# Some HAP deployments tunnel auth failures through the exception
|
|
543
|
+
# text/code instead of HTTP 401. Surface those as
|
|
544
|
+
# AuthenticationError so the CLI shows the login hint.
|
|
545
|
+
if _looks_like_auth_failure(code, exception_msg):
|
|
546
|
+
raise AuthenticationError(exception_msg, status_code=None)
|
|
547
|
+
raise APIError(exception_msg, code=code)
|
|
532
548
|
if "status" in result and "data" in result and "state" not in result:
|
|
533
|
-
# Workflow envelope
|
|
534
|
-
#
|
|
535
|
-
#
|
|
549
|
+
# Workflow envelope: ``status`` carries no success/failure
|
|
550
|
+
# semantics (the frontend ignores it), and a failure would have
|
|
551
|
+
# been caught by the exception check above.
|
|
536
552
|
return result.get("data", result)
|
|
537
553
|
if "state" not in result:
|
|
538
554
|
return result
|
|
539
555
|
if result.get("state") == 0:
|
|
540
|
-
exception_msg = str(result.get("exception", "") or "")
|
|
541
556
|
code = result.get("code")
|
|
542
|
-
#
|
|
543
|
-
#
|
|
544
|
-
|
|
545
|
-
if _looks_like_auth_failure(code, exception_msg):
|
|
557
|
+
# state=0 with no exception text — still a failure, just an
|
|
558
|
+
# unlabelled one.
|
|
559
|
+
if _looks_like_auth_failure(code, ""):
|
|
546
560
|
raise AuthenticationError(
|
|
547
|
-
|
|
561
|
+
"Authentication failed (upstream reported login required).",
|
|
548
562
|
status_code=None,
|
|
549
563
|
)
|
|
550
|
-
raise APIError(
|
|
551
|
-
exception_msg or "Unknown API error",
|
|
552
|
-
code=code,
|
|
553
|
-
)
|
|
564
|
+
raise APIError("Unknown API error", code=code)
|
|
554
565
|
return result.get("data", result)
|
|
555
566
|
|
|
556
567
|
# ── API Call Methods ──────────────────────────────────────────────────
|
|
@@ -68,10 +68,25 @@ def get_worksheet_controls(
|
|
|
68
68
|
if isinstance(result, dict):
|
|
69
69
|
inner = result.get("data")
|
|
70
70
|
if isinstance(inner, dict):
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
controls = inner.get("controls", [])
|
|
72
|
+
elif isinstance(inner, list):
|
|
73
|
+
controls = inner
|
|
74
|
+
else:
|
|
75
|
+
controls = result.get("controls", [])
|
|
76
|
+
else:
|
|
77
|
+
controls = []
|
|
78
|
+
if not controls:
|
|
79
|
+
# A real worksheet always has controls, so an empty result means
|
|
80
|
+
# the envelope was not the expected shape (soft failure, data:
|
|
81
|
+
# null, permission-degraded response). Callers that type-encode
|
|
82
|
+
# cells against this metadata silently lose their encoding when
|
|
83
|
+
# it comes back empty, so leave a trace at the source.
|
|
84
|
+
logger.warning(
|
|
85
|
+
"GetWorksheetControls returned no fields for worksheet %s "
|
|
86
|
+
"(response keys: %s)", worksheet_id,
|
|
87
|
+
sorted(result) if isinstance(result, dict) else type(result).__name__,
|
|
88
|
+
)
|
|
89
|
+
return controls
|
|
75
90
|
|
|
76
91
|
|
|
77
92
|
def get_worksheet_views(
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""A record write must abort when the worksheet's fields can't be read.
|
|
2
|
+
|
|
3
|
+
Traced from a live backend error on ``Worksheet.AddWSRowData.DetailTable
|
|
4
|
+
.Details``:
|
|
5
|
+
|
|
6
|
+
Cannot deserialize the current JSON object (e.g. {"name":"value"})
|
|
7
|
+
into type 'System.Collections.Generic.List`1[MD.Entity.Worksheet.Cell]'
|
|
8
|
+
... Path '[0].66961f256a6cbc06cce98326'
|
|
9
|
+
|
|
10
|
+
The server had received a sub-table cell whose value was a *1-D* array of
|
|
11
|
+
``{childControlId: value}`` maps instead of the 2-D
|
|
12
|
+
``[[{controlId, value}, ...], ...]`` array it deserializes into
|
|
13
|
+
``List<Cell>``.
|
|
14
|
+
|
|
15
|
+
``encode_controls_by_id`` normally rewrites exactly that mistake: it looks
|
|
16
|
+
the sub-table control up, runs ``_legacy_cell_value`` (type 34) over the
|
|
17
|
+
value and re-emits the 2-D form with a ``tempRowId`` cell per row. The one
|
|
18
|
+
way the wrong shape can escape is the "control not found -> send the raw
|
|
19
|
+
value" fallback. And because ``get_worksheet_controls`` returns ``[]``
|
|
20
|
+
whenever the response doesn't carry a ``controls`` list (soft failure,
|
|
21
|
+
``data: null``, permission-degraded envelope), *every* cell of that write
|
|
22
|
+
silently took the fallback — the type encoder was effectively switched
|
|
23
|
+
off with no signal to the user.
|
|
24
|
+
|
|
25
|
+
Two guards, both exercised here:
|
|
26
|
+
* an empty control list on a write path fails loudly, and
|
|
27
|
+
* an individual unresolved field reference is logged before its value
|
|
28
|
+
is passed through.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import logging
|
|
33
|
+
|
|
34
|
+
import pytest
|
|
35
|
+
|
|
36
|
+
from hap_cli.core import record as rec_mod
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_SUB_CTRL = {
|
|
40
|
+
"controlId": "66962b546c73406b9268582d",
|
|
41
|
+
"controlName": "明细",
|
|
42
|
+
"type": 34,
|
|
43
|
+
"relationControls": [
|
|
44
|
+
{"controlId": "66961f256a6cbc06cce98326", "controlName": "编号",
|
|
45
|
+
"type": 2},
|
|
46
|
+
{"controlId": "66962af95957d709b346d1ad", "controlName": "数量",
|
|
47
|
+
"type": 6},
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# The shape an LLM / hand-written payload typically produces: one map per
|
|
52
|
+
# child row, keyed by child controlId, values left in their native JSON
|
|
53
|
+
# types. This is what the server choked on.
|
|
54
|
+
_MAP_SHAPED_ROWS = json.dumps([
|
|
55
|
+
{"66961f256a6cbc06cce98326": "WL001238",
|
|
56
|
+
"66962af95957d709b346d1ad": 2},
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture
|
|
61
|
+
def controls(monkeypatch):
|
|
62
|
+
"""Stub the worksheet control fetch; assign to the list to change it."""
|
|
63
|
+
holder = [_SUB_CTRL]
|
|
64
|
+
|
|
65
|
+
def _fake(session, worksheet_id, app_id=""):
|
|
66
|
+
return holder[0] if isinstance(holder[0], list) else holder
|
|
67
|
+
|
|
68
|
+
monkeypatch.setattr(rec_mod.ws_mod, "get_worksheet_controls", _fake)
|
|
69
|
+
return holder
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _set(holder, value):
|
|
73
|
+
holder[0] = value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@pytest.fixture
|
|
77
|
+
def warnings():
|
|
78
|
+
"""Collect this module's warnings off its own logger.
|
|
79
|
+
|
|
80
|
+
``caplog`` listens on the root logger, and ``hap config log on``
|
|
81
|
+
detaches ``hap_cli`` from root (``propagate = False``), so a test that
|
|
82
|
+
happens to run after one is deaf to these records. Attaching here is
|
|
83
|
+
independent of that.
|
|
84
|
+
"""
|
|
85
|
+
records: list[logging.LogRecord] = []
|
|
86
|
+
|
|
87
|
+
class _Collect(logging.Handler):
|
|
88
|
+
def emit(self, record):
|
|
89
|
+
records.append(record)
|
|
90
|
+
|
|
91
|
+
handler = _Collect(level=logging.WARNING)
|
|
92
|
+
log = logging.getLogger("hap_cli.record")
|
|
93
|
+
log.addHandler(handler)
|
|
94
|
+
try:
|
|
95
|
+
yield records
|
|
96
|
+
finally:
|
|
97
|
+
log.removeHandler(handler)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestEmptyControlListAborts:
|
|
101
|
+
def test_encode_controls_by_id_raises(self, controls):
|
|
102
|
+
_set(controls, [])
|
|
103
|
+
with pytest.raises(ValueError, match="ws_1"):
|
|
104
|
+
rec_mod.encode_controls_by_id(
|
|
105
|
+
None, "ws_1",
|
|
106
|
+
[{"controlId": _SUB_CTRL["controlId"],
|
|
107
|
+
"value": _MAP_SHAPED_ROWS}],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def test_update_path_raises_too(self, controls):
|
|
111
|
+
_set(controls, [])
|
|
112
|
+
with pytest.raises(ValueError):
|
|
113
|
+
rec_mod.encode_controls_by_id(
|
|
114
|
+
None, "ws_1",
|
|
115
|
+
[{"controlId": _SUB_CTRL["controlId"], "value": "x"}],
|
|
116
|
+
for_update=True,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def test_batch_create_by_name_raises(self, controls):
|
|
120
|
+
_set(controls, [])
|
|
121
|
+
with pytest.raises(ValueError, match="ws_1"):
|
|
122
|
+
rec_mod.batch_create_by_name(None, "ws_1", [{"明细": []}])
|
|
123
|
+
|
|
124
|
+
def test_batch_update_by_name_raises(self, controls):
|
|
125
|
+
_set(controls, [])
|
|
126
|
+
with pytest.raises(ValueError, match="ws_1"):
|
|
127
|
+
rec_mod.batch_update_by_name(
|
|
128
|
+
None, "ws_1", [{"rowid": "r1", "明细": []}])
|
|
129
|
+
|
|
130
|
+
def test_error_names_the_worksheet_not_the_field(self, controls):
|
|
131
|
+
"""The old message blamed the field ("Field '明细' not found").
|
|
132
|
+
|
|
133
|
+
With no controls at all the field is not the problem — say so, or
|
|
134
|
+
the user goes hunting for a field that exists.
|
|
135
|
+
"""
|
|
136
|
+
_set(controls, [])
|
|
137
|
+
with pytest.raises(ValueError) as exc:
|
|
138
|
+
rec_mod.batch_create_by_name(None, "ws_1", [{"明细": []}])
|
|
139
|
+
assert "not found" not in str(exc.value)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class TestUnresolvedFieldIsLogged:
|
|
143
|
+
def test_warning_names_the_field(self, controls, warnings):
|
|
144
|
+
out = rec_mod.encode_controls_by_id(
|
|
145
|
+
None, "ws_1", [{"controlId": "c_nope", "value": "v"}])
|
|
146
|
+
# Still passed through — explicit ids must keep reaching the server.
|
|
147
|
+
assert out == [{"controlId": "c_nope", "value": "v"}]
|
|
148
|
+
assert any("c_nope" in r.getMessage() for r in warnings)
|
|
149
|
+
|
|
150
|
+
def test_no_warning_when_resolved(self, controls, warnings):
|
|
151
|
+
rec_mod.encode_controls_by_id(
|
|
152
|
+
None, "ws_1",
|
|
153
|
+
[{"controlId": _SUB_CTRL["controlId"], "value": "[]"}])
|
|
154
|
+
assert warnings == []
|
|
155
|
+
|
|
156
|
+
def test_empty_control_list_is_logged_before_the_abort(
|
|
157
|
+
self, controls, warnings):
|
|
158
|
+
_set(controls, [])
|
|
159
|
+
with pytest.raises(ValueError):
|
|
160
|
+
rec_mod.encode_controls_by_id(
|
|
161
|
+
None, "ws_1", [{"controlId": "c_any", "value": "v"}])
|
|
162
|
+
assert any("ws_1" in r.getMessage() for r in warnings)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class TestMapShapedSubTableIsStillNormalized:
|
|
166
|
+
"""The guards must not weaken the repair that already works."""
|
|
167
|
+
|
|
168
|
+
def test_map_rows_become_2d_cell_lists(self, controls):
|
|
169
|
+
out = rec_mod.encode_controls_by_id(
|
|
170
|
+
None, "ws_1",
|
|
171
|
+
[{"controlId": _SUB_CTRL["controlId"],
|
|
172
|
+
"value": _MAP_SHAPED_ROWS}],
|
|
173
|
+
)
|
|
174
|
+
rows = json.loads(out[0]["value"])
|
|
175
|
+
assert isinstance(rows, list) and isinstance(rows[0], list)
|
|
176
|
+
cells = {c["controlId"]: c["value"] for c in rows[0]}
|
|
177
|
+
assert cells["66961f256a6cbc06cce98326"] == "WL001238"
|
|
178
|
+
assert cells["66962af95957d709b346d1ad"] == "2" # stringified
|
|
179
|
+
assert "tempRowId" in cells
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""A workflow response carrying an ``exception`` must raise, not unwrap.
|
|
2
|
+
|
|
3
|
+
Reported from a month of building applications end-to-end: workflow node
|
|
4
|
+
saves and publishes come back green while the node does not exist / the
|
|
5
|
+
field value was never written. Two of the reported symptoms ("the node is
|
|
6
|
+
reported saved but isn't there", "save and publish are all green but the
|
|
7
|
+
value is never written") share one cause in the transport, not in the
|
|
8
|
+
node builders.
|
|
9
|
+
|
|
10
|
+
``Session._post`` special-cased the workflow envelope
|
|
11
|
+
(``{status, data, msg}``) and unwrapped ``data`` unconditionally, on the
|
|
12
|
+
reasoning that ``status`` alone can't tell success from failure. But
|
|
13
|
+
``status`` is not how the frontend decides either: ``mdyAPI``
|
|
14
|
+
(pd-openweb src/common/global.js:846-849) rejects on one condition —
|
|
15
|
+
|
|
16
|
+
if (responseData.exception) { ...; reject({...}) }
|
|
17
|
+
|
|
18
|
+
— for every envelope it handles, workflow included. ``exception`` is the
|
|
19
|
+
failure marker; ``status`` is not consulted at all. So an errored
|
|
20
|
+
workflow response was the one shape the CLI swallowed.
|
|
21
|
+
|
|
22
|
+
The check is applied before any unwrapping so it covers the workflow
|
|
23
|
+
envelope, an envelope with no ``data`` key, and a main-site response that
|
|
24
|
+
carries an exception without ``state: 0``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from unittest.mock import MagicMock, patch
|
|
28
|
+
|
|
29
|
+
import pytest
|
|
30
|
+
|
|
31
|
+
from hap_cli.core.session import Session, APIError
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _mock_response(payload, status_code=200):
|
|
35
|
+
resp = MagicMock()
|
|
36
|
+
resp.status_code = status_code
|
|
37
|
+
resp.json.return_value = payload
|
|
38
|
+
resp.text = str(payload)
|
|
39
|
+
return resp
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture
|
|
43
|
+
def session():
|
|
44
|
+
return Session(server_url="https://test.mingdao.com", auth_token="tok123")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestWorkflowEnvelopeRaises:
|
|
48
|
+
def test_exception_raises_instead_of_unwrapping(self, session):
|
|
49
|
+
with patch("requests.post") as post:
|
|
50
|
+
post.return_value = _mock_response(
|
|
51
|
+
{"status": 1, "data": None, "exception": "节点不存在"})
|
|
52
|
+
with pytest.raises(APIError, match="节点不存在"):
|
|
53
|
+
session.workflow_call("flowNode/saveNode", {"nodeId": "n1"})
|
|
54
|
+
|
|
55
|
+
def test_exception_raises_even_with_data_present(self, session):
|
|
56
|
+
"""A failed save can still echo a data payload — the exception wins."""
|
|
57
|
+
with patch("requests.post") as post:
|
|
58
|
+
post.return_value = _mock_response(
|
|
59
|
+
{"status": 1, "data": {"id": "n1"}, "exception": "参数错误"})
|
|
60
|
+
with pytest.raises(APIError, match="参数错误"):
|
|
61
|
+
session.workflow_call("flowNode/saveNode", {"nodeId": "n1"})
|
|
62
|
+
|
|
63
|
+
def test_envelope_without_data_key_still_raises(self, session):
|
|
64
|
+
with patch("requests.post") as post:
|
|
65
|
+
post.return_value = _mock_response(
|
|
66
|
+
{"status": 0, "exception": "流程已发布"})
|
|
67
|
+
with pytest.raises(APIError, match="流程已发布"):
|
|
68
|
+
session.workflow_call("process/publish", {"processId": "p1"})
|
|
69
|
+
|
|
70
|
+
def test_success_still_unwraps_data(self, session):
|
|
71
|
+
with patch("requests.post") as post:
|
|
72
|
+
post.return_value = _mock_response(
|
|
73
|
+
{"status": 1, "data": {"nodeId": "n1"}})
|
|
74
|
+
assert session.workflow_call(
|
|
75
|
+
"flowNode/saveNode", {"nodeId": "n1"}) == {"nodeId": "n1"}
|
|
76
|
+
|
|
77
|
+
def test_empty_exception_is_not_a_failure(self, session):
|
|
78
|
+
"""The server sends exception: null / "" on success — don't raise."""
|
|
79
|
+
for empty in (None, "", False):
|
|
80
|
+
with patch("requests.post") as post:
|
|
81
|
+
post.return_value = _mock_response(
|
|
82
|
+
{"status": 1, "data": {"ok": True}, "exception": empty})
|
|
83
|
+
assert session.workflow_call("flowNode/saveNode", {}) == {
|
|
84
|
+
"ok": True}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestMainSiteEnvelopeUnaffected:
|
|
88
|
+
def test_exception_without_state_zero_still_raises(self, session):
|
|
89
|
+
with patch("requests.post") as post:
|
|
90
|
+
post.return_value = _mock_response(
|
|
91
|
+
{"state": 1, "data": None, "exception": "服务异常"})
|
|
92
|
+
with pytest.raises(APIError, match="服务异常"):
|
|
93
|
+
session.api_call("Worksheet", "AddWorksheetRow", {})
|
|
94
|
+
|
|
95
|
+
def test_state_zero_path_unchanged(self, session):
|
|
96
|
+
with patch("requests.post") as post:
|
|
97
|
+
post.return_value = _mock_response(
|
|
98
|
+
{"state": 0, "exception": "无权限", "code": 5})
|
|
99
|
+
with pytest.raises(APIError, match="无权限"):
|
|
100
|
+
session.api_call("Worksheet", "GetWorksheetControls", {})
|
|
101
|
+
|
|
102
|
+
def test_plain_success_unchanged(self, session):
|
|
103
|
+
with patch("requests.post") as post:
|
|
104
|
+
post.return_value = _mock_response(
|
|
105
|
+
{"state": 1, "data": {"controls": []}})
|
|
106
|
+
assert session.api_call("Worksheet", "GetWorksheetControls", {}) \
|
|
107
|
+
== {"controls": []}
|
|
108
|
+
|
|
109
|
+
def test_v3_payload_without_envelope_unchanged(self, session):
|
|
110
|
+
with patch("requests.post") as post:
|
|
111
|
+
post.return_value = _mock_response({"rows": [{"rowid": "r1"}]})
|
|
112
|
+
assert session.api_call("X", "Y", {}) == {"rows": [{"rowid": "r1"}]}
|
|
@@ -151,6 +151,7 @@ hap_cli/tests/test_bug_036750.py
|
|
|
151
151
|
hap_cli/tests/test_chart.py
|
|
152
152
|
hap_cli/tests/test_cli_access.py
|
|
153
153
|
hap_cli/tests/test_config_cmd.py
|
|
154
|
+
hap_cli/tests/test_control_lookup_guard.py
|
|
154
155
|
hap_cli/tests/test_core.py
|
|
155
156
|
hap_cli/tests/test_destructive_confirm_cli.py
|
|
156
157
|
hap_cli/tests/test_full_e2e.py
|
|
@@ -186,6 +187,7 @@ hap_cli/tests/test_v3_dispatcher.py
|
|
|
186
187
|
hap_cli/tests/test_v3_registry_coverage.py
|
|
187
188
|
hap_cli/tests/test_v3_session.py
|
|
188
189
|
hap_cli/tests/test_v3_yaml_translation.py
|
|
190
|
+
hap_cli/tests/test_workflow_envelope_errors.py
|
|
189
191
|
hap_cli/tests/test_workflow_list_cli.py
|
|
190
192
|
hap_cli/tests/test_workflow_v3_semantic_cli.py
|
|
191
193
|
hap_cli/tests/test_worksheet_crud_cli.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
|