hap-cli 0.8.7__tar.gz → 0.8.9__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 (215) hide show
  1. {hap_cli-0.8.7 → hap_cli-0.8.9}/PKG-INFO +1 -1
  2. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/__init__.py +1 -1
  3. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/chart_cmd.py +7 -5
  4. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/chatbot_cmd.py +2 -1
  5. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/icon_cmd.py +17 -11
  6. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/record_cmd.py +6 -3
  7. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/chart.py +19 -1
  8. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/chatbot.py +46 -9
  9. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/record.py +90 -14
  10. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/worksheet_templates.py +7 -1
  11. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_chart.py +78 -0
  12. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_core.py +195 -14
  13. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli.egg-info/PKG-INFO +1 -1
  14. {hap_cli-0.8.7 → hap_cli-0.8.9}/MANIFEST.in +0 -0
  15. {hap_cli-0.8.7 → hap_cli-0.8.9}/README.md +0 -0
  16. {hap_cli-0.8.7 → hap_cli-0.8.9}/app_live_tests/__init__.py +0 -0
  17. {hap_cli-0.8.7 → hap_cli-0.8.9}/app_live_tests/__main__.py +0 -0
  18. {hap_cli-0.8.7 → hap_cli-0.8.9}/app_live_tests/config.py +0 -0
  19. {hap_cli-0.8.7 → hap_cli-0.8.9}/app_live_tests/harness.py +0 -0
  20. {hap_cli-0.8.7 → hap_cli-0.8.9}/app_live_tests/smoke.py +0 -0
  21. {hap_cli-0.8.7 → hap_cli-0.8.9}/app_live_tests/state.py +0 -0
  22. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/README.md +0 -0
  23. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/README_CN.md +0 -0
  24. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/__main__.py +0 -0
  25. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/__init__.py +0 -0
  26. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/app_cmd.py +0 -0
  27. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/app_editor_cmd.py +0 -0
  28. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/auth_cmd.py +0 -0
  29. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/calendar_cmd.py +0 -0
  30. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/chat_cmd.py +0 -0
  31. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/config_cmd.py +0 -0
  32. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/contact_cmd.py +0 -0
  33. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/department_cmd.py +0 -0
  34. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/group_cmd.py +0 -0
  35. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/instance_cmd.py +0 -0
  36. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/node_cmd.py +0 -0
  37. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/optionset_cmd.py +0 -0
  38. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/page_cmd.py +0 -0
  39. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/post_cmd.py +0 -0
  40. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/region_cmd.py +0 -0
  41. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/role_cmd.py +0 -0
  42. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/update_cmd.py +0 -0
  43. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/upload_cmd.py +0 -0
  44. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/v3_registry.py +0 -0
  45. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/workflow_cmd.py +0 -0
  46. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/commands/worksheet_cmd.py +0 -0
  47. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/context.py +0 -0
  48. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/__init__.py +0 -0
  49. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/action_spec_adapter.py +0 -0
  50. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/app.py +0 -0
  51. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/auth.py +0 -0
  52. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/calendar_mod.py +0 -0
  53. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/chat.py +0 -0
  54. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/cli_access.py +0 -0
  55. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/contact.py +0 -0
  56. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/control_type_codes.py +0 -0
  57. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/department.py +0 -0
  58. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/editor/__init__.py +0 -0
  59. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
  60. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
  61. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/editor/apply.py +0 -0
  62. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/editor/componentlower.py +0 -0
  63. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/editor/editspec.py +0 -0
  64. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/editor/errors.py +0 -0
  65. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/editor/fieldlower.py +0 -0
  66. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/editor/jsonschema_mini.py +0 -0
  67. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/editor/models.py +0 -0
  68. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/editor/ops.py +0 -0
  69. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/editor/planner.py +0 -0
  70. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/editor/reader.py +0 -0
  71. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/field_normalizer.py +0 -0
  72. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/field_spec_adapter.py +0 -0
  73. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/filter_translator.py +0 -0
  74. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/flow_node.py +0 -0
  75. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/global_meta.py +0 -0
  76. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/group.py +0 -0
  77. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/icon_index.py +0 -0
  78. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/instance.py +0 -0
  79. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/logger.py +0 -0
  80. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/optionset.py +0 -0
  81. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/page.py +0 -0
  82. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/post.py +0 -0
  83. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/response_crypto.py +0 -0
  84. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/role.py +0 -0
  85. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/role_perm_builder.py +0 -0
  86. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/session.py +0 -0
  87. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/token_crypto.py +0 -0
  88. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/update.py +0 -0
  89. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/upload.py +0 -0
  90. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/v3/__init__.py +0 -0
  91. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/v3/dispatcher.py +0 -0
  92. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/v3/render.py +0 -0
  93. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/v3/schema.py +0 -0
  94. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/view_spec_adapter.py +0 -0
  95. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/workflow.py +0 -0
  96. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/workflow_node_dsl.py +0 -0
  97. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/core/worksheet.py +0 -0
  98. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/hap_cli.py +0 -0
  99. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/i18n.py +0 -0
  100. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/locale/__init__.py +0 -0
  101. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/locale/messages.json +0 -0
  102. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/locale/messages.schema.json +0 -0
  103. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/__init__.py +0 -0
  104. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/conftest.py +0 -0
  105. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_app_editor.py +0 -0
  106. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_app_export_usage.py +0 -0
  107. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_auth_prerelease.py +0 -0
  108. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_bug_036430.py +0 -0
  109. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_cli_access.py +0 -0
  110. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_config_cmd.py +0 -0
  111. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
  112. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_full_e2e.py +0 -0
  113. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_global_meta.py +0 -0
  114. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_i18n.py +0 -0
  115. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_instance_history_cli.py +0 -0
  116. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_integration.py +0 -0
  117. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_integration_approval.py +0 -0
  118. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_integration_approval_actions.py +0 -0
  119. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_integration_calendar.py +0 -0
  120. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_integration_destructive.py +0 -0
  121. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_integration_misc.py +0 -0
  122. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_integration_post.py +0 -0
  123. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_integration_social.py +0 -0
  124. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_integration_v3.py +0 -0
  125. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_integration_workflow.py +0 -0
  126. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
  127. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_multi_profile.py +0 -0
  128. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
  129. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_org_id_cli.py +0 -0
  130. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_org_id_docs.py +0 -0
  131. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_parameter_conventions.py +0 -0
  132. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
  133. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +0 -0
  134. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_record_field_alias.py +0 -0
  135. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_record_v3_semantic_cli.py +0 -0
  136. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_semantic_types_registry.py +0 -0
  137. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_update.py +0 -0
  138. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
  139. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_v3_dispatcher.py +0 -0
  140. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
  141. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_v3_session.py +0 -0
  142. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
  143. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_workflow_list_cli.py +0 -0
  144. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_workflow_v3_semantic_cli.py +0 -0
  145. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
  146. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +0 -0
  147. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/utils/__init__.py +0 -0
  148. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/utils/formatting.py +0 -0
  149. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/utils/options.py +0 -0
  150. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli/utils/parameter_mapping.py +0 -0
  151. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli.egg-info/SOURCES.txt +0 -0
  152. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli.egg-info/dependency_links.txt +0 -0
  153. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli.egg-info/entry_points.txt +0 -0
  154. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli.egg-info/requires.txt +0 -0
  155. {hap_cli-0.8.7 → hap_cli-0.8.9}/hap_cli.egg-info/top_level.txt +0 -0
  156. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/__init__.py +0 -0
  157. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/__main__.py +0 -0
  158. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/_wftest.py +0 -0
  159. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/charts.py +0 -0
  160. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/cleanup.py +0 -0
  161. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/compiler.py +0 -0
  162. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/config.py +0 -0
  163. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/errors.py +0 -0
  164. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/executor.py +0 -0
  165. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/fields.py +0 -0
  166. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/hap.py +0 -0
  167. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/recording/__init__.py +0 -0
  168. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/recording/console.py +0 -0
  169. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/recording/jsonl.py +0 -0
  170. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/recording/mirror.py +0 -0
  171. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/recording/report.py +0 -0
  172. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/schema.py +0 -0
  173. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/seed/__init__.py +0 -0
  174. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/seed/cli.py +0 -0
  175. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/seed/executor.py +0 -0
  176. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/seed/template.py +0 -0
  177. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/selftest.py +0 -0
  178. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/smoke.py +0 -0
  179. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/step.py +0 -0
  180. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/steps.py +0 -0
  181. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/store.py +0 -0
  182. {hap_cli-0.8.7 → hap_cli-0.8.9}/live_tests/workflow_dsl.py +0 -0
  183. {hap_cli-0.8.7 → hap_cli-0.8.9}/setup.cfg +0 -0
  184. {hap_cli-0.8.7 → hap_cli-0.8.9}/setup.py +0 -0
  185. {hap_cli-0.8.7 → hap_cli-0.8.9}/skills/hap-cli-app-editor/scripts/editspec/component.schema.json +0 -0
  186. {hap_cli-0.8.7 → hap_cli-0.8.9}/skills/hap-cli-app-editor/scripts/editspec/custom-action.schema.json +0 -0
  187. {hap_cli-0.8.7 → hap_cli-0.8.9}/skills/hap-cli-app-editor/scripts/editspec/envelope.schema.json +0 -0
  188. {hap_cli-0.8.7 → hap_cli-0.8.9}/skills/hap-cli-app-editor/scripts/editspec/field.schema.json +0 -0
  189. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/INDEX.json +0 -0
  190. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
  191. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
  192. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/create_optionset.yaml +0 -0
  193. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/create_role.yaml +0 -0
  194. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/delete_optionset.yaml +0 -0
  195. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/delete_record.yaml +0 -0
  196. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/delete_role.yaml +0 -0
  197. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
  198. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/get_app_info.yaml +0 -0
  199. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
  200. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
  201. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
  202. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/get_record_details.yaml +0 -0
  203. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
  204. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/get_record_list.yaml +0 -0
  205. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/get_record_logs.yaml +0 -0
  206. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
  207. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/get_record_relations.yaml +0 -0
  208. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
  209. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/get_regions.yaml +0 -0
  210. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/get_role_details.yaml +0 -0
  211. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/get_role_list.yaml +0 -0
  212. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/knowledge_search.yaml +0 -0
  213. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
  214. {hap_cli-0.8.7 → hap_cli-0.8.9}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
  215. {hap_cli-0.8.7 → hap_cli-0.8.9}/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.7
3
+ Version: 0.8.9
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.7"
3
+ __version__ = "0.8.9"
@@ -22,12 +22,14 @@ def chart():
22
22
 
23
23
 
24
24
  # Report-type enum values used by the MingDAO statistics module. The CLI
25
- # accepts the integer directly so users can pass uncommon types (sankey,
26
- # treemap, etc.) without us having to enumerate every one.
25
+ # accepts the integer directly so users can pass uncommon types without us
26
+ # having to enumerate every one. Note: there is NO type 4 — passing 4 is
27
+ # coerced to 10 (number) by the server, so it is omitted here (bug #036584).
27
28
  _REPORT_TYPE_HINT = (
28
- "Chart type (integer). Common values: "
29
- "1=column, 2=line, 3=pie, 4=number, 5=bar, 6=radar, 7=funnel, "
30
- "8=dual-axis, 9=pivot, 10=heatmap."
29
+ "Chart type (integer). Values: "
30
+ "1=column, 2=line, 3=pie, 5=radar, 6=funnel, 7=dual-axis, 8=pivot, "
31
+ "9=region-map, 10=number, 11=bidirectional-bar, 12=scatter, "
32
+ "13=word-cloud, 14=gauge, 15=progress, 16=ranking, 17=world-map."
31
33
  )
32
34
 
33
35
 
@@ -120,7 +120,8 @@ def chatbot_rename(ctx, app_id, chatbot_id, section_id, name, icon):
120
120
  @click.option("--app-id", "-a", required=True, help="Application ID")
121
121
  @org_id_option()
122
122
  @click.option("--section-id", default="",
123
- help="Section/group the assistant lives under. Optional.")
123
+ help="Section/group the assistant lives under. Auto-detected "
124
+ "when omitted.")
124
125
  @click.option("--permanent", is_flag=True, default=False,
125
126
  help="Delete permanently instead of moving to the recycle bin.")
126
127
  @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
@@ -31,9 +31,10 @@ def _icon_table(rows):
31
31
  @click.option(
32
32
  "--limit",
33
33
  type=int,
34
- default=20,
35
- show_default=True,
36
- help="Maximum number of icons to return.",
34
+ default=None,
35
+ help="Maximum number of icons to return. Caps the result exactly — "
36
+ "--limit 5 returns at most 5. Defaults to 20 for a keyword search, "
37
+ "or the whole catalogue when browsing with no keyword.",
37
38
  )
38
39
  @pass_context
39
40
  def icon_search(ctx, query, limit):
@@ -41,19 +42,24 @@ def icon_search(ctx, query, limit):
41
42
 
42
43
  Pass one or more keywords; an icon must relate to every keyword to
43
44
  match. With no keyword, lists the whole catalogue. When fewer than
44
- 10 icons match, random suggestions top the list up to 10 so you
45
- always have something to pick from. Use the returned icon name with
46
- the --icon option of app/worksheet creation.
45
+ 10 icons match, random suggestions top the list up to 10 (never more
46
+ than --limit) so you always have something to pick from. Use the
47
+ returned icon name with the --icon option of app/worksheet creation.
47
48
  """
48
49
  try:
49
- # With no keyword the command acts as "browse all"; the limit
50
- # only narrows an actual keyword search.
51
- effective_limit = limit if query else None
52
- rows = ic_mod.search_icons(list(query), limit=effective_limit)
53
50
  if query:
51
+ # Keyword search: default to 20 matches; an explicit --limit
52
+ # is the exact upper bound, and the random-suggestion floor
53
+ # never exceeds it either.
54
+ effective_limit = limit if limit is not None else 20
55
+ rows = ic_mod.search_icons(list(query), limit=effective_limit)
54
56
  rows = ic_mod.pad_with_random_icons(
55
- rows, target=min(10, max(0, limit))
57
+ rows, target=min(10, max(0, effective_limit))
56
58
  )
59
+ else:
60
+ # Browse all by default; honor an explicit --limit if given.
61
+ # search_icons treats None as "no truncation".
62
+ rows = ic_mod.search_icons(list(query), limit=limit)
57
63
  ctx.output(rows, _icon_table)
58
64
  except Exception as e:
59
65
  ctx.handle_error(e)
@@ -156,9 +156,12 @@ def record_update(ctx, worksheet_id, row_id, field, fields_json, no_workflow):
156
156
  Pass values either as repeated -f CONTROL_ID=VALUE pairs or as one
157
157
  --fields-json array.
158
158
 
159
- A sub-table value is the full list of child rows (each a dict keyed
160
- by child field name) and replaces the existing rows; an empty list
161
- clears the sub-table.
159
+ A sub-table value is a list of child rows, each a dict keyed by child
160
+ field name. Updates are incremental: a child row with a "rowid" key
161
+ edits that existing row in place (its id is kept and only the listed
162
+ fields change), a child row without a "rowid" adds a new row, and a
163
+ child row with "_delete": true removes that row. Rows you don't list
164
+ are left untouched. An empty list (or null) clears the sub-table.
162
165
  """
163
166
  if field and fields_json:
164
167
  raise click.UsageError("Pass exactly one of -f or --fields-json.")
@@ -17,7 +17,14 @@ Save body shape (verified against pd-openweb @param docs):
17
17
  - ``name`` — chart display name
18
18
  - ``appId`` — worksheet id the chart attaches to
19
19
  - ``appType`` — 1: worksheet, 2: aggregate table (default 1)
20
- - ``reportType`` — 1:柱图 2:折线图 3:饼图 4:数值图 (plus many more in the wild)
20
+ - ``reportType`` — chart type. The statistics module's canonical
21
+ enum (``pd-openweb/src/pages/Statistics/Charts/common.js`` →
22
+ ``reportTypes``) is: 1:柱图 2:折线图 3:饼图 5:雷达图 6:漏斗图
23
+ 7:双轴图 8:透视表 9:行政区划 10:数值图 11:双向条形图 12:散点图
24
+ 13:词云 14:仪表图 15:进度图 16:数值排名 17:世界地图. There is
25
+ **no reportType 4** — an old API doc comment labels 4 as 数值图,
26
+ but the live backend coerces a sent ``4`` to ``10`` (the real
27
+ 数值图). Pass 10 for a number chart, never 4 (bug #036584).
21
28
  - ``filter`` / ``filters`` / ``config`` / ``displaySetup`` / ``pivotTable``
22
29
  / ``summary`` / ``xaxes`` / ``yaxisList`` / ``sorts`` / ``formulas`` —
23
30
  optional nested config objects; CLI accepts a full ``--spec-json`` so the
@@ -200,6 +207,17 @@ def save_chart(
200
207
  charts) are needed for that path.
201
208
  """
202
209
  body: dict[str, Any] = dict(extra or {})
210
+ # The statistics backend reads the split (拆分维度/分组) control from
211
+ # the TOP-LEVEL `splitId` field, not from the nested `split.controlId`.
212
+ # A spec that only sets `split.controlId` (the natural shape, and what
213
+ # the UI's drag-into-split slot produces internally) gets its split
214
+ # silently cleared on save unless `splitId` mirrors it. Auto-populate
215
+ # `splitId` from `split.controlId` when the caller hasn't pinned one
216
+ # (bug #036584). An explicit splitId always wins.
217
+ if not body.get("splitId") and isinstance(body.get("split"), dict):
218
+ split_control_id = body["split"].get("controlId")
219
+ if split_control_id:
220
+ body["splitId"] = split_control_id
203
221
  # Resolve the chart's filter block before sending saveReportConfig.
204
222
  if isinstance(body.get("filter"), dict):
205
223
  body["filter"] = _ensure_filter_id(
@@ -196,6 +196,37 @@ def update_chatbot_info(
196
196
  return session.api_call("AppManagement", "EditWorkSheetInfoForApp", data)
197
197
 
198
198
 
199
+ def _resolve_chatbot_section(
200
+ session: Session, app_id: str, chatbot_id: str
201
+ ) -> str:
202
+ """Find the appSectionId of the section holding ``chatbot_id``.
203
+
204
+ Walks the app's sections (and nested childSections) for the item whose
205
+ ``workSheetId`` is the chatbot, returning its section id. Empty string
206
+ if not found (caller proceeds without it).
207
+ """
208
+ from hap_cli.core.app import get_app_detail
209
+
210
+ detail = get_app_detail(session, app_id)
211
+
212
+ def _walk(sections: list[dict[str, Any]]) -> str:
213
+ for sec in sections or []:
214
+ for item in sec.get("workSheetInfo", []) or []:
215
+ if item.get("workSheetId") == chatbot_id:
216
+ return sec.get("appSectionId", "") or ""
217
+ found = _walk(sec.get("childSections") or [])
218
+ if found:
219
+ return found
220
+ return ""
221
+
222
+ try:
223
+ return _walk(detail.get("sections", []) or [])
224
+ except Exception as e: # never block a delete on a lookup hiccup
225
+ logger.warning("could not resolve chatbot %s section: %s",
226
+ chatbot_id, e)
227
+ return ""
228
+
229
+
199
230
  def delete_chatbot(
200
231
  session: Session,
201
232
  chatbot_id: str,
@@ -207,17 +238,23 @@ def delete_chatbot(
207
238
  ) -> dict[str, Any]:
208
239
  """Delete a chatbot (AI assistant).
209
240
 
210
- Evidence: pd-openweb ``src/pages/chatBot/MoreMenu.jsx:48-58``. Even
211
- though a chatbot is *created* as a ``type:3`` worksheet
212
- (``AddWorkSheet``), it is *deleted* through the shared deleteSheet
213
- flow with ``type: 1`` (``AppManagement/RemoveWorkSheetForApp``,
214
- ``workSheetId`` = the chatbot id) — the same call shape as a custom
215
- page (see ``core/page.delete_page``). ``isPermanentlyDelete=False``
216
- sends it to the recycle bin.
241
+ Evidence: pd-openweb ``src/pages/Chatbot/MoreMenu.jsx`` ->
242
+ ``sheetList.deleteSheet`` -> ``RemoveWorkSheetForApp``. Even though a
243
+ chatbot is *created* as a ``type:3`` worksheet (``AddWorkSheet``), it
244
+ is *deleted* through the shared deleteSheet flow with ``type: 1``
245
+ (``AppManagement/RemoveWorkSheetForApp``, ``workSheetId`` = the chatbot
246
+ id) — the same call shape as a custom page (see
247
+ ``core/page.delete_page``). ``isPermanentlyDelete=False`` sends it to
248
+ the recycle bin.
217
249
 
218
- ``appSectionId`` (the group the assistant lives under) is optional
219
- per the swagger ``DeleteItemRequest`` and sent only when provided.
250
+ ``appSectionId`` is REQUIRED in practice: the UI always sends
251
+ ``parentGroupId || groupId`` and the server returns ``服务异常`` without
252
+ it. When the caller omits ``section_id`` we resolve the chatbot's
253
+ section from the app structure so a bare ``chatbot delete <id> -a
254
+ <app>`` just works.
220
255
  """
256
+ if not section_id:
257
+ section_id = _resolve_chatbot_section(session, app_id, chatbot_id)
221
258
  data: dict[str, Any] = {
222
259
  "appId": app_id,
223
260
  "projectId": project_id,
@@ -76,12 +76,54 @@ def _sublist_parse_rows(ctrl: dict[str, Any], value: Any) -> list:
76
76
  return list(value)
77
77
 
78
78
 
79
+ # Reserved keys a child-row dict may carry that are NOT child fields.
80
+ # ``rowid`` (or ``_rowid``) pins an existing child row so an update edits
81
+ # it in place and preserves its id; ``_delete`` (or ``_deleted``) marks
82
+ # that row for removal. They never map to a cell.
83
+ _SUBLIST_ROWID_KEYS = ("rowid", "_rowid")
84
+ _SUBLIST_DELETE_KEYS = ("_delete", "_deleted")
85
+ _SUBLIST_RESERVED_KEYS = frozenset(_SUBLIST_ROWID_KEYS + _SUBLIST_DELETE_KEYS)
86
+
87
+
88
+ def _sublist_child_rowid(row: dict[str, Any]) -> Optional[str]:
89
+ """Return the existing child-row id a row dict pins, or None.
90
+
91
+ A ``temp-``/``default-`` prefixed value names a row that does not yet
92
+ exist server-side, so it is treated as "no id" (a fresh add).
93
+ """
94
+ for k in _SUBLIST_ROWID_KEYS:
95
+ v = (row or {}).get(k)
96
+ if v not in (None, ""):
97
+ s = str(v)
98
+ if s.startswith("temp-") or s.startswith("default-"):
99
+ return None
100
+ return s
101
+ return None
102
+
103
+
104
+ def _sublist_row_is_delete(row: dict[str, Any]) -> bool:
105
+ return any((row or {}).get(k) for k in _SUBLIST_DELETE_KEYS)
106
+
107
+
79
108
  def _sublist_row_cells(
80
109
  ctrl: dict[str, Any], row: dict[str, Any],
110
+ *, temp_row_id: Optional[str] = None,
81
111
  ) -> list[dict[str, Any]]:
82
112
  """Serialize one child-row dict (keyed by child field name/alias/id)
83
113
  into the ``[{controlId, value}, ...]`` cell list the server expects,
84
- prefixed with a fresh ``tempRowId`` cell.
114
+ prefixed with a ``tempRowId`` cell.
115
+
116
+ For a brand-new row ``temp_row_id`` is left None and a fresh
117
+ ``temp-<uuid>`` id is minted. For an in-place update of an existing
118
+ row, pass that row's real id as ``temp_row_id`` — the UI sends the
119
+ existing rowid in the ``tempRowId`` cell on edit (evidence: a
120
+ captured UpdateWorksheetRow body where an in-place child edit carried
121
+ ``{"controlId":"tempRowId","value":"<existing rowid>"}``).
122
+
123
+ Reserved keys (``rowid`` / ``_delete`` …) are skipped — they steer
124
+ the op, they are not child-field cells. Only the keys the caller
125
+ supplies become cells, so an in-place update touches just the
126
+ changed fields.
85
127
  """
86
128
  child_by_name: dict[str, dict[str, Any]] = {}
87
129
  for cc in ctrl.get("relationControls") or []:
@@ -91,9 +133,11 @@ def _sublist_row_cells(
91
133
  child_by_name[k] = cc
92
134
  cells = [{
93
135
  "controlId": "tempRowId",
94
- "value": "temp-" + str(uuid.uuid4()),
136
+ "value": temp_row_id or ("temp-" + str(uuid.uuid4())),
95
137
  }]
96
138
  for cname, cval in (row or {}).items():
139
+ if cname in _SUBLIST_RESERVED_KEYS:
140
+ continue
97
141
  cc = child_by_name.get(cname)
98
142
  if cc is None or cc.get("type") in _LEGACY_SKIP_TYPES:
99
143
  continue
@@ -109,20 +153,51 @@ def _sublist_update_value(ctrl: dict[str, Any], value: Any) -> str:
109
153
 
110
154
  Updates must NOT reuse the AddWorksheetRow 2-D array — the server
111
155
  answers "服务异常". The UI sends ``editType: 9`` on the control and a
112
- value of per-row operations: ``{"rowid": "all", "editType": 2}``
113
- (delete every existing child row) followed by one
114
- ``{"editType": 0, "newOldControl": [cells...]}`` entry per new row —
115
- i.e. full replace. Evidence: pd-openweb
116
- src/components/Form/core/utils.js, formatControlToServer case 34
117
- (the non-isNewRecord branch builds exactly this list).
156
+ value of per-row operations. We apply **incremental / patch**
157
+ semantics, matching what the form actually sends (it only emits the
158
+ rows that changed and leaves the rest untouched):
159
+
160
+ - a child row carrying an existing ``rowid`` →
161
+ ``{"rowid": id, "editType": 0, "newOldControl": [changed cells…]}``
162
+ — edits that row in place, preserving its id; only the supplied
163
+ fields are sent.
164
+ - a child row with no ``rowid`` →
165
+ ``{"editType": 0, "newOldControl": [cells…]}`` — a fresh add.
166
+ - a child row flagged ``_delete`` → ``{"rowid": id, "editType": 2}``.
167
+ - rows not listed are left as-is (server keeps them).
168
+ - ``None`` / ``[]`` clears the whole sub-table:
169
+ ``[{"rowid": "all", "editType": 2}]``.
170
+
171
+ Evidence: pd-openweb src/components/Form/core/utils.js,
172
+ formatControlToServer case 34 (the changes.updated branch builds
173
+ ``{rowid, editType:0, newOldControl}`` for existing rows), plus a
174
+ captured UpdateWorksheetRow body editing one field of one child row
175
+ while leaving its sibling row untouched.
118
176
  """
119
177
  rows = _sublist_parse_rows(ctrl, value)
120
- ops: list[dict[str, Any]] = [{"rowid": "all", "editType": 2}]
178
+ # None or an explicit empty list means "clear every child row".
179
+ if not rows:
180
+ return json.dumps([{"rowid": "all", "editType": 2}],
181
+ ensure_ascii=False)
182
+ ops: list[dict[str, Any]] = []
121
183
  for r in rows:
122
- ops.append({
123
- "editType": 0,
124
- "newOldControl": _sublist_row_cells(ctrl, r),
125
- })
184
+ rowid = _sublist_child_rowid(r)
185
+ if _sublist_row_is_delete(r):
186
+ if rowid:
187
+ ops.append({"rowid": rowid, "editType": 2})
188
+ continue
189
+ if rowid:
190
+ ops.append({
191
+ "rowid": rowid,
192
+ "editType": 0,
193
+ "newOldControl": _sublist_row_cells(
194
+ ctrl, r, temp_row_id=rowid),
195
+ })
196
+ else:
197
+ ops.append({
198
+ "editType": 0,
199
+ "newOldControl": _sublist_row_cells(ctrl, r),
200
+ })
126
201
  return json.dumps(ops, ensure_ascii=False)
127
202
 
128
203
 
@@ -232,7 +307,8 @@ def encode_controls_by_id(
232
307
 
233
308
  ``for_update`` selects the UpdateWorksheetRow wire shapes where they
234
309
  differ from AddWorksheetRow — today that is SUB_LIST (34), which on
235
- update needs ``editType: 9`` plus a delete-all/add-rows op list (see
310
+ update needs ``editType: 9`` plus an incremental per-row op list
311
+ (in-place edits keyed by ``rowid``, fresh adds, and deletes — see
236
312
  :func:`_sublist_update_value`).
237
313
  """
238
314
  controls = ws_mod.get_worksheet_controls(session, worksheet_id)
@@ -407,8 +407,14 @@ def build_control(
407
407
  spec = dict(spec)
408
408
  child_type = spec.pop("type")
409
409
  child_name = spec.pop("name")
410
+ # row/col default to the child's position in the list, but a
411
+ # child spec may pin its own grid coordinates (the skill emits
412
+ # row/size per child). Seed defaults then let the spec win, so
413
+ # an explicit `row`/`col` in spec does not collide with a
414
+ # positional kwarg ("multiple values for 'row'").
415
+ child_kwargs = {"row": i, "col": 0, **spec}
410
416
  built.append(build_control(
411
- child_type, child_name, row=i, col=0, **spec,
417
+ child_type, child_name, **child_kwargs,
412
418
  ))
413
419
  relation_controls = built
414
420
 
@@ -138,6 +138,67 @@ class TestSaveChart:
138
138
  assert body["appType"] == 2
139
139
  assert body["reportType"] == 2
140
140
 
141
+ @patch("hap_cli.core.session.requests.post")
142
+ def test_split_control_id_auto_populates_top_level_split_id(self, mock_post):
143
+ # bug #036584: the statistics backend reads the split (拆分维度)
144
+ # control from the TOP-LEVEL `splitId` field, not from the nested
145
+ # `split.controlId`. A spec that only sets `split.controlId` (the
146
+ # natural shape) gets its split silently cleared on save. The core
147
+ # must mirror the controlId up to `splitId` so the split persists.
148
+ sess = Session(server_url="https://t.com", auth_token="tok")
149
+ mock_post.return_value = _resp({"reportId": "r1"})
150
+
151
+ chart_mod.save_chart(
152
+ sess, app_id="ws1", name="Grouped", report_type=1,
153
+ extra={"split": {"controlId": "ctrlX", "controlType": 9}},
154
+ )
155
+
156
+ body = mock_post.call_args[1]["json"]
157
+ assert body["splitId"] == "ctrlX"
158
+ assert body["split"]["controlId"] == "ctrlX"
159
+
160
+ @patch("hap_cli.core.session.requests.post")
161
+ def test_explicit_split_id_is_not_overwritten(self, mock_post):
162
+ # If the caller already pinned a top-level splitId, honor it.
163
+ sess = Session(server_url="https://t.com", auth_token="tok")
164
+ mock_post.return_value = _resp({"reportId": "r1"})
165
+
166
+ chart_mod.save_chart(
167
+ sess, app_id="ws1", name="Grouped", report_type=1,
168
+ extra={
169
+ "splitId": "pinned",
170
+ "split": {"controlId": "ctrlX", "controlType": 9},
171
+ },
172
+ )
173
+
174
+ assert mock_post.call_args[1]["json"]["splitId"] == "pinned"
175
+
176
+ @patch("hap_cli.core.session.requests.post")
177
+ def test_no_split_means_no_split_id_injected(self, mock_post):
178
+ # A chart without a split block must not gain an empty splitId.
179
+ sess = Session(server_url="https://t.com", auth_token="tok")
180
+ mock_post.return_value = _resp({"reportId": "r1"})
181
+
182
+ chart_mod.save_chart(
183
+ sess, app_id="ws1", name="Plain", report_type=1,
184
+ )
185
+
186
+ assert "splitId" not in mock_post.call_args[1]["json"]
187
+
188
+ @patch("hap_cli.core.session.requests.post")
189
+ def test_empty_split_control_id_does_not_inject_split_id(self, mock_post):
190
+ # A split block with a blank controlId carries no dimension —
191
+ # don't fabricate a splitId from it.
192
+ sess = Session(server_url="https://t.com", auth_token="tok")
193
+ mock_post.return_value = _resp({"reportId": "r1"})
194
+
195
+ chart_mod.save_chart(
196
+ sess, app_id="ws1", name="Plain", report_type=1,
197
+ extra={"split": {"controlId": ""}},
198
+ )
199
+
200
+ assert "splitId" not in mock_post.call_args[1]["json"]
201
+
141
202
  @patch("hap_cli.core.session.requests.post")
142
203
  def test_extra_dict_is_merged_under_required_fields(self, mock_post):
143
204
  sess = Session(server_url="https://t.com", auth_token="tok")
@@ -248,6 +309,23 @@ class TestChartCLI:
248
309
  body = mock_post.call_args[1]["json"]
249
310
  assert body["id"] == "r1"
250
311
 
312
+ def test_report_type_help_maps_number_to_10_not_4(self, tmp_config):
313
+ # bug #036584: the old help text claimed "4=number" and shifted
314
+ # every type above it by one. The statistics module has no
315
+ # reportType 4 (the backend coerces a legacy 4 to 10), and the
316
+ # number chart (数值图) is 10. The help must not mislead users
317
+ # into passing 4.
318
+ result = CliRunner().invoke(cli, [
319
+ "worksheet", "chart", "create", "--help",
320
+ ])
321
+ assert result.exit_code == 0, result.output
322
+ help_text = result.output
323
+ assert "4=number" not in help_text
324
+ assert "10=number" in help_text
325
+ # The genuinely correct anchors for the shifted-by-one values.
326
+ assert "5=radar" in help_text
327
+ assert "8=pivot" in help_text
328
+
251
329
  @patch("hap_cli.core.session.requests.post")
252
330
  def test_chart_delete_cli(self, mock_post, tmp_config):
253
331
  Session(server_url="https://t.com", auth_token="tok").save()