hap-cli 0.8.8__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.8 → hap_cli-0.8.9}/PKG-INFO +1 -1
  2. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/__init__.py +1 -1
  3. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/chart_cmd.py +7 -5
  4. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/chatbot_cmd.py +2 -1
  5. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/record_cmd.py +6 -3
  6. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/chart.py +19 -1
  7. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/chatbot.py +46 -9
  8. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/record.py +90 -14
  9. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_chart.py +78 -0
  10. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_core.py +148 -14
  11. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli.egg-info/PKG-INFO +1 -1
  12. {hap_cli-0.8.8 → hap_cli-0.8.9}/MANIFEST.in +0 -0
  13. {hap_cli-0.8.8 → hap_cli-0.8.9}/README.md +0 -0
  14. {hap_cli-0.8.8 → hap_cli-0.8.9}/app_live_tests/__init__.py +0 -0
  15. {hap_cli-0.8.8 → hap_cli-0.8.9}/app_live_tests/__main__.py +0 -0
  16. {hap_cli-0.8.8 → hap_cli-0.8.9}/app_live_tests/config.py +0 -0
  17. {hap_cli-0.8.8 → hap_cli-0.8.9}/app_live_tests/harness.py +0 -0
  18. {hap_cli-0.8.8 → hap_cli-0.8.9}/app_live_tests/smoke.py +0 -0
  19. {hap_cli-0.8.8 → hap_cli-0.8.9}/app_live_tests/state.py +0 -0
  20. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/README.md +0 -0
  21. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/README_CN.md +0 -0
  22. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/__main__.py +0 -0
  23. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/__init__.py +0 -0
  24. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/app_cmd.py +0 -0
  25. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/app_editor_cmd.py +0 -0
  26. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/auth_cmd.py +0 -0
  27. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/calendar_cmd.py +0 -0
  28. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/chat_cmd.py +0 -0
  29. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/config_cmd.py +0 -0
  30. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/contact_cmd.py +0 -0
  31. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/department_cmd.py +0 -0
  32. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/group_cmd.py +0 -0
  33. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/icon_cmd.py +0 -0
  34. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/instance_cmd.py +0 -0
  35. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/node_cmd.py +0 -0
  36. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/optionset_cmd.py +0 -0
  37. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/page_cmd.py +0 -0
  38. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/post_cmd.py +0 -0
  39. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/region_cmd.py +0 -0
  40. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/role_cmd.py +0 -0
  41. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/update_cmd.py +0 -0
  42. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/upload_cmd.py +0 -0
  43. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/v3_registry.py +0 -0
  44. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/workflow_cmd.py +0 -0
  45. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/commands/worksheet_cmd.py +0 -0
  46. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/context.py +0 -0
  47. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/__init__.py +0 -0
  48. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/action_spec_adapter.py +0 -0
  49. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/app.py +0 -0
  50. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/auth.py +0 -0
  51. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/calendar_mod.py +0 -0
  52. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/chat.py +0 -0
  53. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/cli_access.py +0 -0
  54. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/contact.py +0 -0
  55. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/control_type_codes.py +0 -0
  56. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/department.py +0 -0
  57. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/editor/__init__.py +0 -0
  58. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
  59. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
  60. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/editor/apply.py +0 -0
  61. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/editor/componentlower.py +0 -0
  62. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/editor/editspec.py +0 -0
  63. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/editor/errors.py +0 -0
  64. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/editor/fieldlower.py +0 -0
  65. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/editor/jsonschema_mini.py +0 -0
  66. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/editor/models.py +0 -0
  67. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/editor/ops.py +0 -0
  68. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/editor/planner.py +0 -0
  69. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/editor/reader.py +0 -0
  70. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/field_normalizer.py +0 -0
  71. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/field_spec_adapter.py +0 -0
  72. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/filter_translator.py +0 -0
  73. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/flow_node.py +0 -0
  74. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/global_meta.py +0 -0
  75. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/group.py +0 -0
  76. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/icon_index.py +0 -0
  77. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/instance.py +0 -0
  78. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/logger.py +0 -0
  79. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/optionset.py +0 -0
  80. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/page.py +0 -0
  81. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/post.py +0 -0
  82. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/response_crypto.py +0 -0
  83. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/role.py +0 -0
  84. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/role_perm_builder.py +0 -0
  85. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/session.py +0 -0
  86. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/token_crypto.py +0 -0
  87. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/update.py +0 -0
  88. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/upload.py +0 -0
  89. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/v3/__init__.py +0 -0
  90. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/v3/dispatcher.py +0 -0
  91. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/v3/render.py +0 -0
  92. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/v3/schema.py +0 -0
  93. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/view_spec_adapter.py +0 -0
  94. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/workflow.py +0 -0
  95. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/workflow_node_dsl.py +0 -0
  96. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/worksheet.py +0 -0
  97. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/core/worksheet_templates.py +0 -0
  98. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/hap_cli.py +0 -0
  99. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/i18n.py +0 -0
  100. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/locale/__init__.py +0 -0
  101. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/locale/messages.json +0 -0
  102. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/locale/messages.schema.json +0 -0
  103. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/__init__.py +0 -0
  104. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/conftest.py +0 -0
  105. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_app_editor.py +0 -0
  106. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_app_export_usage.py +0 -0
  107. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_auth_prerelease.py +0 -0
  108. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_bug_036430.py +0 -0
  109. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_cli_access.py +0 -0
  110. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_config_cmd.py +0 -0
  111. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
  112. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_full_e2e.py +0 -0
  113. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_global_meta.py +0 -0
  114. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_i18n.py +0 -0
  115. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_instance_history_cli.py +0 -0
  116. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_integration.py +0 -0
  117. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_integration_approval.py +0 -0
  118. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_integration_approval_actions.py +0 -0
  119. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_integration_calendar.py +0 -0
  120. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_integration_destructive.py +0 -0
  121. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_integration_misc.py +0 -0
  122. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_integration_post.py +0 -0
  123. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_integration_social.py +0 -0
  124. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_integration_v3.py +0 -0
  125. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_integration_workflow.py +0 -0
  126. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
  127. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_multi_profile.py +0 -0
  128. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
  129. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_org_id_cli.py +0 -0
  130. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_org_id_docs.py +0 -0
  131. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_parameter_conventions.py +0 -0
  132. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
  133. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +0 -0
  134. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_record_field_alias.py +0 -0
  135. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_record_v3_semantic_cli.py +0 -0
  136. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_semantic_types_registry.py +0 -0
  137. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_update.py +0 -0
  138. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
  139. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_v3_dispatcher.py +0 -0
  140. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
  141. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_v3_session.py +0 -0
  142. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
  143. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_workflow_list_cli.py +0 -0
  144. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_workflow_v3_semantic_cli.py +0 -0
  145. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
  146. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +0 -0
  147. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/utils/__init__.py +0 -0
  148. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/utils/formatting.py +0 -0
  149. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/utils/options.py +0 -0
  150. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli/utils/parameter_mapping.py +0 -0
  151. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli.egg-info/SOURCES.txt +0 -0
  152. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli.egg-info/dependency_links.txt +0 -0
  153. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli.egg-info/entry_points.txt +0 -0
  154. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli.egg-info/requires.txt +0 -0
  155. {hap_cli-0.8.8 → hap_cli-0.8.9}/hap_cli.egg-info/top_level.txt +0 -0
  156. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/__init__.py +0 -0
  157. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/__main__.py +0 -0
  158. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/_wftest.py +0 -0
  159. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/charts.py +0 -0
  160. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/cleanup.py +0 -0
  161. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/compiler.py +0 -0
  162. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/config.py +0 -0
  163. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/errors.py +0 -0
  164. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/executor.py +0 -0
  165. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/fields.py +0 -0
  166. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/hap.py +0 -0
  167. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/recording/__init__.py +0 -0
  168. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/recording/console.py +0 -0
  169. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/recording/jsonl.py +0 -0
  170. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/recording/mirror.py +0 -0
  171. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/recording/report.py +0 -0
  172. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/schema.py +0 -0
  173. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/seed/__init__.py +0 -0
  174. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/seed/cli.py +0 -0
  175. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/seed/executor.py +0 -0
  176. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/seed/template.py +0 -0
  177. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/selftest.py +0 -0
  178. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/smoke.py +0 -0
  179. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/step.py +0 -0
  180. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/steps.py +0 -0
  181. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/store.py +0 -0
  182. {hap_cli-0.8.8 → hap_cli-0.8.9}/live_tests/workflow_dsl.py +0 -0
  183. {hap_cli-0.8.8 → hap_cli-0.8.9}/setup.cfg +0 -0
  184. {hap_cli-0.8.8 → hap_cli-0.8.9}/setup.py +0 -0
  185. {hap_cli-0.8.8 → hap_cli-0.8.9}/skills/hap-cli-app-editor/scripts/editspec/component.schema.json +0 -0
  186. {hap_cli-0.8.8 → hap_cli-0.8.9}/skills/hap-cli-app-editor/scripts/editspec/custom-action.schema.json +0 -0
  187. {hap_cli-0.8.8 → hap_cli-0.8.9}/skills/hap-cli-app-editor/scripts/editspec/envelope.schema.json +0 -0
  188. {hap_cli-0.8.8 → hap_cli-0.8.9}/skills/hap-cli-app-editor/scripts/editspec/field.schema.json +0 -0
  189. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/INDEX.json +0 -0
  190. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
  191. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
  192. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/create_optionset.yaml +0 -0
  193. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/create_role.yaml +0 -0
  194. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/delete_optionset.yaml +0 -0
  195. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/delete_record.yaml +0 -0
  196. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/delete_role.yaml +0 -0
  197. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
  198. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/get_app_info.yaml +0 -0
  199. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
  200. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
  201. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
  202. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/get_record_details.yaml +0 -0
  203. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
  204. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/get_record_list.yaml +0 -0
  205. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/get_record_logs.yaml +0 -0
  206. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
  207. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/get_record_relations.yaml +0 -0
  208. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
  209. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/get_regions.yaml +0 -0
  210. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/get_role_details.yaml +0 -0
  211. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/get_role_list.yaml +0 -0
  212. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/knowledge_search.yaml +0 -0
  213. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
  214. {hap_cli-0.8.8 → hap_cli-0.8.9}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
  215. {hap_cli-0.8.8 → 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.8
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.8"
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")
@@ -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)
@@ -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()
@@ -599,13 +599,14 @@ class TestRecord:
599
599
 
600
600
  @patch("hap_cli.core.worksheet.get_worksheet_controls")
601
601
  @patch("hap_cli.core.session.requests.post")
602
- def test_update_subtable_sends_edit_type_replace_shape(
602
+ def test_update_subtable_rows_without_rowid_are_adds(
603
603
  self, mock_post, mock_controls, mock_session
604
604
  ):
605
605
  # UpdateWorksheetRow must NOT receive the AddWorksheetRow 2-D
606
606
  # array — the server answers "服务异常". The update wire shape is
607
- # editType=9 on the control plus a delete-all + add-rows list
608
- # (pd-openweb src/components/Form/core/utils.js case 34).
607
+ # editType=9 plus an incremental op list. Child rows with no
608
+ # rowid are fresh adds (editType 0, no delete-all op, no parent
609
+ # rowid), leaving any existing rows untouched (patch semantics).
609
610
  mock_controls.return_value = self._SUBTABLE_CONTROLS
610
611
  mock_post.return_value = _mock_response(True)
611
612
  rec_mod.batch_update_by_name(
@@ -619,19 +620,97 @@ class TestRecord:
619
620
  cell = rc["cDetail"]
620
621
  assert cell["editType"] == 9
621
622
  ops = json.loads(cell["value"])
622
- assert ops[0] == {"rowid": "all", "editType": 2}
623
- adds = ops[1:]
624
- assert len(adds) == 2
625
- for add in adds:
623
+ assert len(ops) == 2
624
+ for add in ops:
626
625
  assert add["editType"] == 0
626
+ assert "rowid" not in add
627
627
  cells = {c["controlId"]: c["value"]
628
628
  for c in add["newOldControl"]}
629
629
  assert cells["tempRowId"].startswith("temp-")
630
630
  row0 = {c["controlId"]: c["value"]
631
- for c in adds[0]["newOldControl"]}
631
+ for c in ops[0]["newOldControl"]}
632
632
  assert row0["ch1"] == "苹果"
633
633
  assert row0["ch2"] == "10"
634
634
 
635
+ @patch("hap_cli.core.worksheet.get_worksheet_controls")
636
+ @patch("hap_cli.core.session.requests.post")
637
+ def test_update_subtable_existing_row_edited_in_place(
638
+ self, mock_post, mock_controls, mock_session
639
+ ):
640
+ # A child row carrying an existing rowid is edited in place: the
641
+ # op keeps that rowid (editType 0) and the tempRowId cell repeats
642
+ # it, so the server preserves the row id instead of minting a new
643
+ # one. Only the supplied field is sent (bug feedback: in-place
644
+ # edits previously dropped the child rowid by full-replace).
645
+ mock_controls.return_value = self._SUBTABLE_CONTROLS
646
+ mock_post.return_value = _mock_response(True)
647
+ rec_mod.batch_update_by_name(
648
+ mock_session, "ws1",
649
+ [{"rowid": "row-1",
650
+ "明细": [{"rowid": "child-1", "数量": 46}]}],
651
+ )
652
+ rc = {c["controlId"]: c
653
+ for c in mock_post.call_args[1]["json"]["newOldControl"]}
654
+ cell = rc["cDetail"]
655
+ assert cell["editType"] == 9
656
+ ops = json.loads(cell["value"])
657
+ assert len(ops) == 1
658
+ op = ops[0]
659
+ assert op["rowid"] == "child-1"
660
+ assert op["editType"] == 0
661
+ cells = {c["controlId"]: c["value"] for c in op["newOldControl"]}
662
+ assert cells["tempRowId"] == "child-1"
663
+ assert cells["ch2"] == "46"
664
+ # Only the changed field is sent — the untouched 品名 is absent.
665
+ assert "ch1" not in cells
666
+
667
+ @patch("hap_cli.core.worksheet.get_worksheet_controls")
668
+ @patch("hap_cli.core.session.requests.post")
669
+ def test_update_subtable_mixed_edit_add_delete(
670
+ self, mock_post, mock_controls, mock_session
671
+ ):
672
+ # One op list may mix an in-place edit, a fresh add, and a delete
673
+ # marker. Untouched rows are simply omitted.
674
+ mock_controls.return_value = self._SUBTABLE_CONTROLS
675
+ mock_post.return_value = _mock_response(True)
676
+ rec_mod.batch_update_by_name(
677
+ mock_session, "ws1",
678
+ [{"rowid": "row-1",
679
+ "明细": [
680
+ {"rowid": "child-1", "数量": 5},
681
+ {"品名": "新行"},
682
+ {"rowid": "child-2", "_delete": True},
683
+ ]}],
684
+ )
685
+ rc = {c["controlId"]: c
686
+ for c in mock_post.call_args[1]["json"]["newOldControl"]}
687
+ ops = json.loads(rc["cDetail"]["value"])
688
+ assert ops[0]["rowid"] == "child-1" and ops[0]["editType"] == 0
689
+ assert "rowid" not in ops[1] and ops[1]["editType"] == 0
690
+ assert ops[2] == {"rowid": "child-2", "editType": 2}
691
+
692
+ @patch("hap_cli.core.worksheet.get_worksheet_controls")
693
+ @patch("hap_cli.core.session.requests.post")
694
+ def test_update_subtable_temp_rowid_treated_as_add(
695
+ self, mock_post, mock_controls, mock_session
696
+ ):
697
+ # A temp-/default- prefixed rowid names a not-yet-persisted row,
698
+ # so it is a fresh add, not an in-place edit.
699
+ mock_controls.return_value = self._SUBTABLE_CONTROLS
700
+ mock_post.return_value = _mock_response(True)
701
+ rec_mod.batch_update_by_name(
702
+ mock_session, "ws1",
703
+ [{"rowid": "row-1",
704
+ "明细": [{"rowid": "temp-abc", "品名": "梨"}]}],
705
+ )
706
+ rc = {c["controlId"]: c
707
+ for c in mock_post.call_args[1]["json"]["newOldControl"]}
708
+ op = json.loads(rc["cDetail"]["value"])[0]
709
+ assert "rowid" not in op
710
+ assert op["editType"] == 0
711
+ cells = {c["controlId"]: c["value"] for c in op["newOldControl"]}
712
+ assert cells["tempRowId"].startswith("temp-")
713
+
635
714
  @patch("hap_cli.core.worksheet.get_worksheet_controls")
636
715
  @patch("hap_cli.core.session.requests.post")
637
716
  def test_update_subtable_empty_list_clears_rows(
@@ -666,9 +745,12 @@ class TestRecord:
666
745
  cell = out[0]
667
746
  assert cell["editType"] == 9
668
747
  ops = json.loads(cell["value"])
669
- assert ops[0] == {"rowid": "all", "editType": 2}
748
+ # No rowid given -> a fresh add, no delete-all op.
749
+ assert len(ops) == 1
750
+ assert ops[0]["editType"] == 0
751
+ assert "rowid" not in ops[0]
670
752
  row0 = {c["controlId"]: c["value"]
671
- for c in ops[1]["newOldControl"]}
753
+ for c in ops[0]["newOldControl"]}
672
754
  assert row0["ch1"] == "梨"
673
755
 
674
756
  @patch("hap_cli.core.worksheet.get_worksheet_controls")
@@ -721,8 +803,11 @@ class TestRecord:
721
803
  rc = {c["controlId"]: c
722
804
  for c in mock_post.call_args[1]["json"]["newOldControl"]}
723
805
  assert rc["cDetail"]["editType"] == 9
724
- assert json.loads(rc["cDetail"]["value"])[0] == {
725
- "rowid": "all", "editType": 2}
806
+ ops = json.loads(rc["cDetail"]["value"])
807
+ # No rowid -> fresh add (patch semantics), no delete-all op.
808
+ assert len(ops) == 1
809
+ assert ops[0]["editType"] == 0
810
+ assert "rowid" not in ops[0]
726
811
 
727
812
  @patch("hap_cli.core.worksheet.get_worksheet_controls")
728
813
  @patch("hap_cli.core.session.requests.post")
@@ -744,8 +829,10 @@ class TestRecord:
744
829
  for c in mock_post.call_args[1]["json"]["newOldControl"]}
745
830
  assert rc["cDetail"]["editType"] == 9
746
831
  ops = json.loads(rc["cDetail"]["value"])
747
- assert ops[0] == {"rowid": "all", "editType": 2}
748
- assert len(ops) == 2
832
+ # No rowid -> fresh add (patch semantics), no delete-all op.
833
+ assert len(ops) == 1
834
+ assert ops[0]["editType"] == 0
835
+ assert "rowid" not in ops[0]
749
836
 
750
837
  @patch("hap_cli.core.worksheet.get_worksheet_controls")
751
838
  @patch("hap_cli.core.session.requests.post")
@@ -8968,6 +9055,53 @@ class TestChatbot:
8968
9055
  assert body["appSectionId"] == "sec1"
8969
9056
  assert body["type"] == 1
8970
9057
  assert body["isPermanentlyDelete"] is False
9058
+ # explicit section -> no GetApp lookup, single call
9059
+ assert mock_post.call_count == 1
9060
+
9061
+ @patch("hap_cli.core.session.requests.post")
9062
+ def test_delete_chatbot_auto_resolves_section(self, mock_post, mock_session):
9063
+ # appSectionId is required in practice (the UI always sends
9064
+ # parentGroupId||groupId; the server returns 服务异常 without it). When
9065
+ # the caller omits section_id we resolve it from the app structure
9066
+ # (GetApp) and include the discovered appSectionId on the delete.
9067
+ mock_post.side_effect = [
9068
+ _mock_response({"sections": [
9069
+ {"appSectionId": "secA",
9070
+ "workSheetInfo": [{"workSheetId": "other"}]},
9071
+ {"appSectionId": "secB",
9072
+ "workSheetInfo": [{"workSheetId": "cb1"}]},
9073
+ ]}),
9074
+ _mock_response(True),
9075
+ ]
9076
+ chatbot_mod.delete_chatbot(
9077
+ mock_session, chatbot_id="cb1", app_id="a1", project_id="p1",
9078
+ )
9079
+ assert mock_post.call_count == 2
9080
+ assert mock_post.call_args_list[0].args[0].endswith("/HomeApp/GetApp")
9081
+ del_body = mock_post.call_args_list[1].kwargs["json"]
9082
+ assert mock_post.call_args_list[1].args[0].endswith(
9083
+ "/AppManagement/RemoveWorkSheetForApp")
9084
+ assert del_body["appSectionId"] == "secB"
9085
+ assert del_body["workSheetId"] == "cb1"
9086
+ assert del_body["type"] == 1
9087
+
9088
+ @patch("hap_cli.core.session.requests.post")
9089
+ def test_delete_chatbot_auto_resolve_nested_section(self, mock_post, mock_session):
9090
+ # The chatbot may live in a nested childSection.
9091
+ mock_post.side_effect = [
9092
+ _mock_response({"sections": [
9093
+ {"appSectionId": "top", "workSheetInfo": [],
9094
+ "childSections": [
9095
+ {"appSectionId": "child1",
9096
+ "workSheetInfo": [{"workSheetId": "cb1"}]}]},
9097
+ ]}),
9098
+ _mock_response(True),
9099
+ ]
9100
+ chatbot_mod.delete_chatbot(
9101
+ mock_session, chatbot_id="cb1", app_id="a1", project_id="p1",
9102
+ )
9103
+ del_body = mock_post.call_args_list[1].kwargs["json"]
9104
+ assert del_body["appSectionId"] == "child1"
8971
9105
 
8972
9106
  def test_cli_delete_routes(self, tmp_config):
8973
9107
  runner = CliRunner()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hap-cli
3
- Version: 0.8.8
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes