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.
Files changed (258) hide show
  1. {hap_cli-0.8.19 → hap_cli-0.8.20}/PKG-INFO +1 -1
  2. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/__init__.py +1 -1
  3. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/record.py +49 -3
  4. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/session.py +24 -13
  5. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/worksheet.py +19 -4
  6. hap_cli-0.8.20/hap_cli/tests/test_control_lookup_guard.py +179 -0
  7. hap_cli-0.8.20/hap_cli/tests/test_workflow_envelope_errors.py +112 -0
  8. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli.egg-info/PKG-INFO +1 -1
  9. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli.egg-info/SOURCES.txt +2 -0
  10. {hap_cli-0.8.19 → hap_cli-0.8.20}/MANIFEST.in +0 -0
  11. {hap_cli-0.8.19 → hap_cli-0.8.20}/README.md +0 -0
  12. {hap_cli-0.8.19 → hap_cli-0.8.20}/app_live_tests/__init__.py +0 -0
  13. {hap_cli-0.8.19 → hap_cli-0.8.20}/app_live_tests/__main__.py +0 -0
  14. {hap_cli-0.8.19 → hap_cli-0.8.20}/app_live_tests/config.py +0 -0
  15. {hap_cli-0.8.19 → hap_cli-0.8.20}/app_live_tests/harness.py +0 -0
  16. {hap_cli-0.8.19 → hap_cli-0.8.20}/app_live_tests/smoke.py +0 -0
  17. {hap_cli-0.8.19 → hap_cli-0.8.20}/app_live_tests/state.py +0 -0
  18. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/README.md +0 -0
  19. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/README_CN.md +0 -0
  20. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/__main__.py +0 -0
  21. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/__init__.py +0 -0
  22. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/app_cmd.py +0 -0
  23. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/app_creator_cmd.py +0 -0
  24. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/app_editor_cmd.py +0 -0
  25. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/auth_cmd.py +0 -0
  26. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/calendar_cmd.py +0 -0
  27. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/chart_cmd.py +0 -0
  28. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/chat_cmd.py +0 -0
  29. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/chatbot_cmd.py +0 -0
  30. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/config_cmd.py +0 -0
  31. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/contact_cmd.py +0 -0
  32. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/department_cmd.py +0 -0
  33. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/group_cmd.py +0 -0
  34. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/icon_cmd.py +0 -0
  35. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/instance_cmd.py +0 -0
  36. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/node_cmd.py +0 -0
  37. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/optionset_cmd.py +0 -0
  38. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/page_cmd.py +0 -0
  39. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/post_cmd.py +0 -0
  40. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/record_cmd.py +0 -0
  41. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/region_cmd.py +0 -0
  42. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/role_cmd.py +0 -0
  43. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/update_cmd.py +0 -0
  44. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/upload_cmd.py +0 -0
  45. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/v3_registry.py +0 -0
  46. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/workflow_cmd.py +0 -0
  47. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/commands/worksheet_cmd.py +0 -0
  48. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/context.py +0 -0
  49. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/__init__.py +0 -0
  50. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/action_spec_adapter.py +0 -0
  51. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app.py +0 -0
  52. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/__init__.py +0 -0
  53. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/_hapmeta/__init__.py +0 -0
  54. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/_hapmeta/control_type_codes.py +0 -0
  55. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/_hapmeta/worksheet_templates.py +0 -0
  56. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/charts.py +0 -0
  57. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/cleanup.py +0 -0
  58. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/compiler.py +0 -0
  59. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/config.py +0 -0
  60. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/design/design.schema.json +0 -0
  61. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/errors.py +0 -0
  62. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/examples/minimal.design.json +0 -0
  63. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/executor.py +0 -0
  64. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/fields.py +0 -0
  65. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/hap.py +0 -0
  66. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/merge.py +0 -0
  67. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/recording/__init__.py +0 -0
  68. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/recording/console.py +0 -0
  69. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/recording/jsonl.py +0 -0
  70. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/recording/mirror.py +0 -0
  71. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/recording/report.py +0 -0
  72. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/schema.py +0 -0
  73. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/INSTRUCTIONS.md +0 -0
  74. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/__init__.py +0 -0
  75. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/cli.py +0 -0
  76. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/executor.py +0 -0
  77. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/resources/attachments.json +0 -0
  78. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/resources/sample.docx +0 -0
  79. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/resources/sample.pdf +0 -0
  80. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/resources/sample_images.json +0 -0
  81. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/seed/template.py +0 -0
  82. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/selftest.py +0 -0
  83. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/smoke.py +0 -0
  84. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/steps.py +0 -0
  85. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/store.py +0 -0
  86. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/validate.py +0 -0
  87. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/app_creator/workflow_dsl.py +0 -0
  88. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/auth.py +0 -0
  89. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/calendar_mod.py +0 -0
  90. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/chart.py +0 -0
  91. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/chat.py +0 -0
  92. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/chatbot.py +0 -0
  93. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/cli_access.py +0 -0
  94. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/contact.py +0 -0
  95. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/control_type_codes.py +0 -0
  96. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/department.py +0 -0
  97. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/__init__.py +0 -0
  98. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
  99. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
  100. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/apply.py +0 -0
  101. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/componentlower.py +0 -0
  102. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/editspec.py +0 -0
  103. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/errors.py +0 -0
  104. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/fieldlower.py +0 -0
  105. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/jsonschema_mini.py +0 -0
  106. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/models.py +0 -0
  107. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/ops.py +0 -0
  108. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/planner.py +0 -0
  109. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/editor/reader.py +0 -0
  110. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/field_normalizer.py +0 -0
  111. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/field_spec_adapter.py +0 -0
  112. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/filter_translator.py +0 -0
  113. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/flow_node.py +0 -0
  114. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/global_meta.py +0 -0
  115. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/group.py +0 -0
  116. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/icon_index.py +0 -0
  117. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/instance.py +0 -0
  118. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/logger.py +0 -0
  119. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/optionset.py +0 -0
  120. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/page.py +0 -0
  121. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/post.py +0 -0
  122. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/response_crypto.py +0 -0
  123. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/role.py +0 -0
  124. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/role_perm_builder.py +0 -0
  125. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/token_crypto.py +0 -0
  126. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/update.py +0 -0
  127. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/upload.py +0 -0
  128. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/v3/__init__.py +0 -0
  129. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/v3/dispatcher.py +0 -0
  130. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/v3/render.py +0 -0
  131. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/v3/schema.py +0 -0
  132. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/view_spec_adapter.py +0 -0
  133. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/workflow.py +0 -0
  134. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/workflow_node_dsl.py +0 -0
  135. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/core/worksheet_templates.py +0 -0
  136. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/hap_cli.py +0 -0
  137. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/i18n.py +0 -0
  138. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/locale/__init__.py +0 -0
  139. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/locale/messages.json +0 -0
  140. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/locale/messages.schema.json +0 -0
  141. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/__init__.py +0 -0
  142. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/conftest.py +0 -0
  143. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_app_creator.py +0 -0
  144. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_app_editor.py +0 -0
  145. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_app_export_usage.py +0 -0
  146. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_auth_prerelease.py +0 -0
  147. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_bug_036430.py +0 -0
  148. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_bug_036750.py +0 -0
  149. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_chart.py +0 -0
  150. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_cli_access.py +0 -0
  151. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_config_cmd.py +0 -0
  152. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_core.py +0 -0
  153. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
  154. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_full_e2e.py +0 -0
  155. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_global_meta.py +0 -0
  156. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_i18n.py +0 -0
  157. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_instance_history_cli.py +0 -0
  158. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration.py +0 -0
  159. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_approval.py +0 -0
  160. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_approval_actions.py +0 -0
  161. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_calendar.py +0 -0
  162. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_destructive.py +0 -0
  163. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_misc.py +0 -0
  164. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_post.py +0 -0
  165. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_social.py +0 -0
  166. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_v3.py +0 -0
  167. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_workflow.py +0 -0
  168. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
  169. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_member_relation_array_wrap.py +0 -0
  170. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_multi_profile.py +0 -0
  171. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
  172. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_org_id_cli.py +0 -0
  173. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_org_id_docs.py +0 -0
  174. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_parameter_conventions.py +0 -0
  175. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
  176. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +0 -0
  177. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_record_field_alias.py +0 -0
  178. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_record_v3_semantic_cli.py +0 -0
  179. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_select_rejects_relation_value.py +0 -0
  180. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_semantic_types_registry.py +0 -0
  181. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_update.py +0 -0
  182. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
  183. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_v3_dispatcher.py +0 -0
  184. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
  185. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_v3_session.py +0 -0
  186. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
  187. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_workflow_list_cli.py +0 -0
  188. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_workflow_v3_semantic_cli.py +0 -0
  189. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
  190. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +0 -0
  191. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/utils/__init__.py +0 -0
  192. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/utils/formatting.py +0 -0
  193. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/utils/options.py +0 -0
  194. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli/utils/parameter_mapping.py +0 -0
  195. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli.egg-info/dependency_links.txt +0 -0
  196. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli.egg-info/entry_points.txt +0 -0
  197. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli.egg-info/requires.txt +0 -0
  198. {hap_cli-0.8.19 → hap_cli-0.8.20}/hap_cli.egg-info/top_level.txt +0 -0
  199. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/__init__.py +0 -0
  200. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/__main__.py +0 -0
  201. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/_wftest.py +0 -0
  202. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/charts.py +0 -0
  203. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/cleanup.py +0 -0
  204. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/compiler.py +0 -0
  205. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/config.py +0 -0
  206. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/errors.py +0 -0
  207. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/executor.py +0 -0
  208. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/fields.py +0 -0
  209. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/hap.py +0 -0
  210. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/recording/__init__.py +0 -0
  211. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/recording/console.py +0 -0
  212. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/recording/jsonl.py +0 -0
  213. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/recording/mirror.py +0 -0
  214. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/recording/report.py +0 -0
  215. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/schema.py +0 -0
  216. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/seed/__init__.py +0 -0
  217. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/seed/cli.py +0 -0
  218. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/seed/executor.py +0 -0
  219. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/seed/template.py +0 -0
  220. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/selftest.py +0 -0
  221. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/smoke.py +0 -0
  222. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/step.py +0 -0
  223. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/steps.py +0 -0
  224. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/store.py +0 -0
  225. {hap_cli-0.8.19 → hap_cli-0.8.20}/live_tests/workflow_dsl.py +0 -0
  226. {hap_cli-0.8.19 → hap_cli-0.8.20}/setup.cfg +0 -0
  227. {hap_cli-0.8.19 → hap_cli-0.8.20}/setup.py +0 -0
  228. {hap_cli-0.8.19 → hap_cli-0.8.20}/skills/cli/hap-cli-app-editor/scripts/editspec/component.schema.json +0 -0
  229. {hap_cli-0.8.19 → hap_cli-0.8.20}/skills/cli/hap-cli-app-editor/scripts/editspec/custom-action.schema.json +0 -0
  230. {hap_cli-0.8.19 → hap_cli-0.8.20}/skills/cli/hap-cli-app-editor/scripts/editspec/envelope.schema.json +0 -0
  231. {hap_cli-0.8.19 → hap_cli-0.8.20}/skills/cli/hap-cli-app-editor/scripts/editspec/field.schema.json +0 -0
  232. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/INDEX.json +0 -0
  233. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
  234. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
  235. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/create_optionset.yaml +0 -0
  236. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/create_role.yaml +0 -0
  237. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/delete_optionset.yaml +0 -0
  238. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/delete_record.yaml +0 -0
  239. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/delete_role.yaml +0 -0
  240. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
  241. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_app_info.yaml +0 -0
  242. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
  243. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
  244. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
  245. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_details.yaml +0 -0
  246. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
  247. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_list.yaml +0 -0
  248. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_logs.yaml +0 -0
  249. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
  250. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_relations.yaml +0 -0
  251. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
  252. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_regions.yaml +0 -0
  253. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_role_details.yaml +0 -0
  254. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/get_role_list.yaml +0 -0
  255. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/knowledge_search.yaml +0 -0
  256. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
  257. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
  258. {hap_cli-0.8.19 → hap_cli-0.8.20}/sources/v3-api-schema/update_optionset.yaml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hap-cli
3
- Version: 0.8.19
3
+ Version: 0.8.20
4
4
  Summary: CLI harness for Mingdao HAP - Enterprise no-code platform
5
5
  Author: hap-cli
6
6
  License: Apache-2.0
@@ -1,3 +1,3 @@
1
1
  """CLI harness for Mingdao HAP (hap-cli)."""
2
2
 
3
- __version__ = "0.8.19"
3
+ __version__ = "0.8.20"
@@ -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 = ws_mod.get_worksheet_controls(session, worksheet_id)
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 = ws_mod.get_worksheet_controls(session, worksheet_id)
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 = ws_mod.get_worksheet_controls(session, worksheet_id)
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. status==0 is "ok" here historically; we
534
- # don't have a clean way to detect failures via status alone,
535
- # so we just unwrap data and trust HTTP status for errors.
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
- # Some HAP deployments tunnel auth failures through state=0 with
543
- # specific codes/messages instead of HTTP 401. Surface those as
544
- # AuthenticationError too so the CLI shows the login hint.
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
- exception_msg or "Authentication failed (upstream reported login required).",
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
- return inner.get("controls", [])
72
- if isinstance(inner, list):
73
- return inner
74
- return result.get("controls", [])
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"}]}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hap-cli
3
- Version: 0.8.19
3
+ Version: 0.8.20
4
4
  Summary: CLI harness for Mingdao HAP - Enterprise no-code platform
5
5
  Author: hap-cli
6
6
  License: Apache-2.0
@@ -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