comate-cli 0.7.6__tar.gz → 0.7.7__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 (201) hide show
  1. {comate_cli-0.7.6 → comate_cli-0.7.7}/PKG-INFO +1 -1
  2. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/main.py +10 -0
  3. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/app.py +33 -15
  4. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/startup_profile.py +16 -3
  5. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/update_check.py +98 -3
  6. {comate_cli-0.7.6 → comate_cli-0.7.7}/pyproject.toml +1 -1
  7. comate_cli-0.7.7/tests/conftest.py +33 -0
  8. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_app_startup_latency.py +24 -7
  9. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_startup_profile.py +12 -0
  10. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_update_check.py +105 -7
  11. {comate_cli-0.7.6 → comate_cli-0.7.7}/uv.lock +2 -2
  12. comate_cli-0.7.6/tests/conftest.py +0 -14
  13. {comate_cli-0.7.6 → comate_cli-0.7.7}/.gitignore +0 -0
  14. {comate_cli-0.7.6 → comate_cli-0.7.7}/CHANGELOG.md +0 -0
  15. {comate_cli-0.7.6 → comate_cli-0.7.7}/README.md +0 -0
  16. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/__init__.py +0 -0
  17. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/__main__.py +0 -0
  18. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/mcp_cli.py +0 -0
  19. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/__init__.py +0 -0
  20. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/animations.py +0 -0
  21. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/assistant_render.py +0 -0
  22. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/codenames.py +0 -0
  23. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/config/__init__.py +0 -0
  24. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/config/model.py +0 -0
  25. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/config/picker.py +0 -0
  26. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/config/picker_state.py +0 -0
  27. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/config/store.py +0 -0
  28. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
  29. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/env_utils.py +0 -0
  30. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/error_display.py +0 -0
  31. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/event_renderer.py +0 -0
  32. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/figures.py +0 -0
  33. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/fragment_utils.py +0 -0
  34. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/goal_resume_view.py +0 -0
  35. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/history_printer.py +0 -0
  36. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/input_geometry.py +0 -0
  37. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
  38. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/logging_adapter.py +0 -0
  39. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/logo.py +0 -0
  40. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/markdown_render.py +0 -0
  41. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/mention_completer.py +0 -0
  42. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/message_style.py +0 -0
  43. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/models.py +0 -0
  44. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/path_context_hint.py +0 -0
  45. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
  46. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
  47. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
  48. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
  49. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
  50. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
  51. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
  52. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
  53. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
  54. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
  55. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
  56. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
  57. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
  58. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/preflight.py +0 -0
  59. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/question_view.py +0 -0
  60. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/resume_picker.py +0 -0
  61. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/resume_preview.py +0 -0
  62. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/resume_selector.py +0 -0
  63. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
  64. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
  65. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/selection_menu.py +0 -0
  66. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/slash_commands.py +0 -0
  67. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/startup.py +0 -0
  68. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/status_bar.py +0 -0
  69. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/statusline/__init__.py +0 -0
  70. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/statusline/model.py +0 -0
  71. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/statusline/picker.py +0 -0
  72. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/statusline/picker_state.py +0 -0
  73. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/statusline/store.py +0 -0
  74. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/text_effects.py +0 -0
  75. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tips.py +0 -0
  76. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tool_fold.py +0 -0
  77. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tool_result_formatters.py +0 -0
  78. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tool_result_store.py +0 -0
  79. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
  80. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tool_view.py +0 -0
  81. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
  82. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui.py +0 -0
  83. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
  84. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
  85. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
  86. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
  87. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
  88. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
  89. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
  90. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
  91. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
  92. {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
  93. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/config/__init__.py +0 -0
  94. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/config/test_model.py +0 -0
  95. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/config/test_picker_state.py +0 -0
  96. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/config/test_picker_ui.py +0 -0
  97. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/config/test_roundtrip.py +0 -0
  98. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/config/test_store_load.py +0 -0
  99. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/config/test_store_save.py +0 -0
  100. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/fixtures/fake_mcp_misbehaving_stdout.py +0 -0
  101. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/statusline/__init__.py +0 -0
  102. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/statusline/test_model.py +0 -0
  103. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/statusline/test_picker_state.py +0 -0
  104. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/statusline/test_store.py +0 -0
  105. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_animator_shuffle.py +0 -0
  106. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_app_mcp_preload.py +0 -0
  107. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_app_preflight_gate.py +0 -0
  108. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_app_print_mode.py +0 -0
  109. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_app_shutdown.py +0 -0
  110. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_app_token_cost_config.py +0 -0
  111. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_app_usage_line.py +0 -0
  112. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_btw_slash_command.py +0 -0
  113. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_cli_project_root.py +0 -0
  114. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_compact_command_semantics.py +0 -0
  115. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_completion_context_activation.py +0 -0
  116. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_completion_status_panel.py +0 -0
  117. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_context_command.py +0 -0
  118. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_custom_slash_commands.py +0 -0
  119. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_discover_tab.py +0 -0
  120. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_errors_tab.py +0 -0
  121. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_event_renderer.py +0 -0
  122. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_event_renderer_boundary.py +0 -0
  123. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_event_renderer_e2e.py +0 -0
  124. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_event_renderer_log_boundary.py +0 -0
  125. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_event_renderer_log_queue.py +0 -0
  126. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_event_renderer_streaming.py +0 -0
  127. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_event_renderer_tool_fold.py +0 -0
  128. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_format_error.py +0 -0
  129. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_goal_resume_tui.py +0 -0
  130. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_goal_resume_view.py +0 -0
  131. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_goal_slash_command.py +0 -0
  132. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_handle_error.py +0 -0
  133. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_history_printer.py +0 -0
  134. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_history_printer_log.py +0 -0
  135. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_history_printer_subtitle_position.py +0 -0
  136. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_history_printer_tool_fold.py +0 -0
  137. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_history_sync.py +0 -0
  138. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_history_sync_tool_fold.py +0 -0
  139. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_input_behavior.py +0 -0
  140. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_input_history.py +0 -0
  141. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_installed_tab.py +0 -0
  142. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_interrupt_exit_semantics.py +0 -0
  143. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_layout_coordinator.py +0 -0
  144. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_logging_adapter.py +0 -0
  145. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_logo.py +0 -0
  146. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_main_args.py +0 -0
  147. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_markdown_render.py +0 -0
  148. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_marketplaces_tab.py +0 -0
  149. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_mcp_cli.py +0 -0
  150. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_mcp_slash_command.py +0 -0
  151. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_mention_completer.py +0 -0
  152. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_path_context_hint.py +0 -0
  153. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_plugin_slash_commands.py +0 -0
  154. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_plugin_tui_components.py +0 -0
  155. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_preflight.py +0 -0
  156. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_preflight_copilot.py +0 -0
  157. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_question_key_bindings.py +0 -0
  158. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_question_view.py +0 -0
  159. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_resume_picker.py +0 -0
  160. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_resume_preview.py +0 -0
  161. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_resume_selector.py +0 -0
  162. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_rewind_command_semantics.py +0 -0
  163. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_rpc_protocol.py +0 -0
  164. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_rpc_stdio_bridge.py +0 -0
  165. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_selection_menu.py +0 -0
  166. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_session_query_token_summary.py +0 -0
  167. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_shutdown_noise_guard.py +0 -0
  168. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_shutdown_noise_integration.py +0 -0
  169. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_skills_slash_command.py +0 -0
  170. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_slash_argument_hint.py +0 -0
  171. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_slash_clear.py +0 -0
  172. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_slash_completer.py +0 -0
  173. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_slash_registry.py +0 -0
  174. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_status_bar.py +0 -0
  175. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_status_bar_transient.py +0 -0
  176. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_task_panel_format.py +0 -0
  177. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_task_panel_key_bindings.py +0 -0
  178. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_task_panel_rendering.py +0 -0
  179. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_task_poll.py +0 -0
  180. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tool_fold.py +0 -0
  181. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tool_fold_panel.py +0 -0
  182. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tool_result_formatters.py +0 -0
  183. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tool_result_store.py +0 -0
  184. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tool_result_viewer.py +0 -0
  185. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tool_result_viewer_key_bindings.py +0 -0
  186. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tool_view.py +0 -0
  187. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_transcript_viewer.py +0 -0
  188. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_transcript_viewer_tool_fold.py +0 -0
  189. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_elapsed_status.py +0 -0
  190. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_esc_queue.py +0 -0
  191. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_mcp_init_gate.py +0 -0
  192. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_paste_newline_guard.py +0 -0
  193. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_paste_placeholder.py +0 -0
  194. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_queue_preview.py +0 -0
  195. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_queue_sdk_source.py +0 -0
  196. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_split_invariance.py +0 -0
  197. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_startup_latency.py +0 -0
  198. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_team_messages.py +0 -0
  199. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_thinking_display.py +0 -0
  200. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
  201. {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_usage_command.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comate-cli
3
- Version: 0.7.6
3
+ Version: 0.7.7
4
4
  Summary: Comate terminal CLI built on comate-agent-sdk
5
5
  Project-URL: Homepage, https://github.com/AndyLee1024/agent-sdk
6
6
  Project-URL: Repository, https://github.com/AndyLee1024/agent-sdk
@@ -7,7 +7,12 @@ import os
7
7
  import signal
8
8
  import subprocess
9
9
  import sys
10
+ import time
10
11
  import warnings
12
+
13
+ # 进程启动锚点:在任何重三方库 import(anthropic/prompt_toolkit/rich)之前捕获,
14
+ # 交给 StartupProfiler 测量「run() 之前的 import」这段以往不可见的启动成本。
15
+ _PROCESS_START_PERF = time.perf_counter()
11
16
  try:
12
17
  import termios
13
18
  except ImportError:
@@ -268,7 +273,9 @@ def main(argv: list[str] | None = None) -> None:
268
273
  atexit.register(term_guard.restore, reason="atexit")
269
274
  atexit.register(noise_guard.begin_shutdown)
270
275
 
276
+ _app_import_start_perf = time.perf_counter()
271
277
  from comate_cli.terminal_agent.app import run
278
+ _app_import_done_perf = time.perf_counter()
272
279
 
273
280
  try:
274
281
  rpc_stdio, resume_session_id, resume_select, print_prompt = _parse_args(run_argv)
@@ -295,6 +302,9 @@ def main(argv: list[str] | None = None) -> None:
295
302
  resume_session_id=resume_session_id,
296
303
  resume_select=resume_select,
297
304
  print_message=print_message,
305
+ process_start_perf=_PROCESS_START_PERF,
306
+ app_import_start_perf=_app_import_start_perf,
307
+ app_import_done_perf=_app_import_done_perf,
298
308
  )
299
309
  )
300
310
  except KeyboardInterrupt:
@@ -36,7 +36,9 @@ from comate_cli.terminal_agent.update_check import (
36
36
  get_pending_update_prompt_info,
37
37
  mark_update_seen,
38
38
  record_skip_until_next_version,
39
+ record_update_check_attempt,
39
40
  run_update_command,
41
+ should_check_for_update,
40
42
  show_update_prompt,
41
43
  )
42
44
 
@@ -87,7 +89,7 @@ async def _handle_update_on_launch(info: UpdateInfo) -> bool:
87
89
  async def _handle_background_update_on_launch(
88
90
  info: UpdateInfo,
89
91
  *,
90
- status_bar: StatusBar,
92
+ renderer: EventRenderer,
91
93
  profiler: StartupProfiler,
92
94
  ) -> None:
93
95
  try:
@@ -96,12 +98,12 @@ async def _handle_background_update_on_launch(
96
98
  profiler.mark("update_check.skip")
97
99
  return
98
100
  if decision == UpdatePromptDecision.SHOW_HINT:
99
- status_bar.show_transient(format_update_hint(info), duration_s=8.0)
101
+ renderer.append_system_message(format_update_hint(info))
100
102
  mark_update_seen(info)
101
103
  profiler.mark("update_check.hint")
102
104
  return
103
105
 
104
- status_bar.show_transient(format_update_hint(info), duration_s=8.0)
106
+ renderer.append_system_message(format_update_hint(info))
105
107
  profiler.mark("update_check.prompt_deferred")
106
108
  except Exception:
107
109
  logger.debug("startup background update handling failed", exc_info=True)
@@ -110,7 +112,7 @@ async def _handle_background_update_on_launch(
110
112
 
111
113
  def _schedule_update_check_on_launch(
112
114
  *,
113
- status_bar: StatusBar,
115
+ renderer: EventRenderer,
114
116
  profiler: StartupProfiler,
115
117
  ) -> asyncio.Task[None]:
116
118
  async def _run() -> None:
@@ -124,7 +126,7 @@ def _schedule_update_check_on_launch(
124
126
  if update_info is not None:
125
127
  await _handle_background_update_on_launch(
126
128
  update_info,
127
- status_bar=status_bar,
129
+ renderer=renderer,
128
130
  profiler=profiler,
129
131
  )
130
132
  except asyncio.CancelledError:
@@ -440,9 +442,18 @@ async def run(
440
442
  resume_session_id: str | None = None,
441
443
  resume_select: bool = False,
442
444
  print_message: str | None = None,
445
+ process_start_perf: float | None = None,
446
+ app_import_start_perf: float | None = None,
447
+ app_import_done_perf: float | None = None,
443
448
  ) -> None:
444
449
  _install_event_loop_exception_handler()
445
- profiler = StartupProfiler.from_env(logger=logger)
450
+ # process_start_perf 由入口 main() 在模块 import 之前捕获,使 profiler 能测到
451
+ # 「run() 之前的重三方库 import」这段以往不可见的启动成本(柱子 A)。
452
+ profiler = StartupProfiler.from_env(logger=logger, started_at=process_start_perf)
453
+ if app_import_start_perf is not None:
454
+ profiler.mark_at("main.app_import.start", app_import_start_perf)
455
+ if app_import_done_perf is not None:
456
+ profiler.mark_at("main.app_import.done", app_import_done_perf)
446
457
  profiler.mark("cli_project_root.start")
447
458
  project_root = _resolve_cli_project_root()
448
459
  profiler.mark("cli_project_root.done")
@@ -538,10 +549,16 @@ async def run(
538
549
  tui = TerminalAgentTUI(session, status_bar, renderer)
539
550
  profiler.mark("tui.init.done")
540
551
  tui.add_resume_history(mode)
541
- update_check_task = _schedule_update_check_on_launch(
542
- status_bar=status_bar,
543
- profiler=profiler,
544
- )
552
+ update_check_task: asyncio.Task[None] | None = None
553
+ if should_check_for_update():
554
+ # attempt 语义:先记录尝试时间,确保 interval 内最多发起一次网络检查
555
+ record_update_check_attempt()
556
+ update_check_task = _schedule_update_check_on_launch(
557
+ renderer=renderer,
558
+ profiler=profiler,
559
+ )
560
+ else:
561
+ profiler.mark("update_check.throttled")
545
562
 
546
563
  async def _mcp_loader() -> None:
547
564
  await _preload_mcp_in_tui(session, profiler=profiler.child("mcp"))
@@ -569,11 +586,12 @@ async def run(
569
586
  )
570
587
  finally:
571
588
  try:
572
- await _cancel_background_task(
573
- update_check_task,
574
- timeout_s=0.1,
575
- task_name="terminal-update-check",
576
- )
589
+ if update_check_task is not None:
590
+ await _cancel_background_task(
591
+ update_check_task,
592
+ timeout_s=0.1,
593
+ task_name="terminal-update-check",
594
+ )
577
595
  if active_session is session:
578
596
  await _graceful_shutdown(active_session)
579
597
  else:
@@ -28,18 +28,31 @@ class StartupProfiler:
28
28
  self._lock = lock if lock is not None else threading.Lock()
29
29
 
30
30
  @classmethod
31
- def from_env(cls, *, logger: logging.Logger) -> "StartupProfiler":
31
+ def from_env(
32
+ cls,
33
+ *,
34
+ logger: logging.Logger,
35
+ started_at: float | None = None,
36
+ ) -> "StartupProfiler":
32
37
  raw_value = os.environ.get("COMATE_STARTUP_PROFILE", "")
33
38
  enabled = raw_value.strip().lower() in _TRUTHY_VALUES
34
- return cls(logger=logger, enabled=enabled)
39
+ return cls(logger=logger, enabled=enabled, started_at=started_at)
35
40
 
36
41
  def mark(self, phase: str) -> None:
42
+ self.mark_at(phase, time.perf_counter())
43
+
44
+ def mark_at(self, phase: str, perf_value: float) -> None:
45
+ """记录一个发生在指定 perf_counter() 时刻的阶段。
46
+
47
+ 用于补记 profiler 创建之前(如 main 入口的模块 import)已经测好的时间点,
48
+ elapsed_ms 仍以 started_at 为锚点,保持与 mark() 同一坐标系。
49
+ """
37
50
  if not self._enabled:
38
51
  return
39
52
  normalized_phase = phase.strip(".")
40
53
  if self._prefix:
41
54
  normalized_phase = f"{self._prefix}.{normalized_phase}"
42
- elapsed_ms = (time.perf_counter() - self._started_at) * 1000
55
+ elapsed_ms = (perf_value - self._started_at) * 1000
43
56
  with self._lock:
44
57
  self._pending_messages.append(
45
58
  f"startup_profile phase={normalized_phase} elapsed_ms={elapsed_ms:.3f}"
@@ -5,6 +5,9 @@ import importlib.metadata
5
5
  import locale
6
6
  import logging
7
7
  import os
8
+ import ssl
9
+ import threading
10
+ import time
8
11
  from dataclasses import dataclass
9
12
  from enum import Enum
10
13
  from pathlib import Path
@@ -27,6 +30,91 @@ _SETTINGS_SECTION = "updates"
27
30
  _UPDATE_CHECK_TRUST_ENV = "COMATE_CLI_UPDATE_CHECK_TRUST_ENV"
28
31
  _TRUTHY_ENV_VALUES = {"1", "true", "yes", "on"}
29
32
 
33
+ # 启动后台更新检查限频:默认 24h 内只查一次,避免每次启动都付网络 + httpx 构造成本。
34
+ # 设为 0(或负数)表示禁用限频,每次启动都检查。
35
+ _LAST_CHECK_AT_KEY = "last_check_at"
36
+ _UPDATE_CHECK_INTERVAL_ENV = "COMATE_CLI_UPDATE_CHECK_INTERVAL_S"
37
+ _DEFAULT_UPDATE_CHECK_INTERVAL_S = 24 * 60 * 60
38
+
39
+ # certifi 预建的 SSL context(进程级复用)。默认 verify=True 会让 httpx 走
40
+ # ssl.create_default_context() → 在 Windows 上枚举系统证书库,实测可达数秒;
41
+ # 显式传 cafile 则不触发 load_default_certs(),构造降到几十毫秒。
42
+ _update_check_ssl_context: ssl.SSLContext | None = None
43
+ _ssl_context_lock = threading.Lock()
44
+
45
+
46
+ def _get_update_check_ssl_context() -> ssl.SSLContext | None:
47
+ """惰性构建并复用 certifi SSL context;失败返回 None(回退 httpx 默认行为)。"""
48
+ global _update_check_ssl_context
49
+ if _update_check_ssl_context is not None:
50
+ return _update_check_ssl_context
51
+ with _ssl_context_lock:
52
+ if _update_check_ssl_context is None:
53
+ try:
54
+ import certifi
55
+
56
+ _update_check_ssl_context = ssl.create_default_context(
57
+ cafile=certifi.where()
58
+ )
59
+ except Exception:
60
+ logger.debug(
61
+ "failed to build certifi SSL context for update check",
62
+ exc_info=True,
63
+ )
64
+ return None
65
+ return _update_check_ssl_context
66
+
67
+
68
+ def _resolve_update_check_interval_s() -> float:
69
+ raw = os.environ.get(_UPDATE_CHECK_INTERVAL_ENV, "").strip()
70
+ if not raw:
71
+ return float(_DEFAULT_UPDATE_CHECK_INTERVAL_S)
72
+ try:
73
+ value = float(raw)
74
+ except ValueError:
75
+ return float(_DEFAULT_UPDATE_CHECK_INTERVAL_S)
76
+ return value if value >= 0 else float(_DEFAULT_UPDATE_CHECK_INTERVAL_S)
77
+
78
+
79
+ def should_check_for_update(
80
+ *,
81
+ package: str = PACKAGE_NAME,
82
+ settings_path: Path | None = None,
83
+ now: float | None = None,
84
+ ) -> bool:
85
+ """启动时是否应执行网络版本检查(限频判断,纯本地 IO)。"""
86
+ interval_s = _resolve_update_check_interval_s()
87
+ if interval_s <= 0:
88
+ return True
89
+ state = _load_package_update_state(package, settings_path=settings_path)
90
+ try:
91
+ last_check_at = float(state.get(_LAST_CHECK_AT_KEY)) # type: ignore[arg-type]
92
+ except (TypeError, ValueError):
93
+ return True
94
+ current = now if now is not None else time.time()
95
+ if current < last_check_at:
96
+ # 时钟回拨:视为需要重新检查
97
+ return True
98
+ return (current - last_check_at) >= interval_s
99
+
100
+
101
+ def record_update_check_attempt(
102
+ *,
103
+ package: str = PACKAGE_NAME,
104
+ settings_path: Path | None = None,
105
+ now: float | None = None,
106
+ ) -> None:
107
+ """记录一次更新检查尝试时间(attempt 语义:每个 interval 内最多一次网络请求)。"""
108
+ current = now if now is not None else time.time()
109
+ try:
110
+ _write_package_update_state(
111
+ package,
112
+ {_LAST_CHECK_AT_KEY: current},
113
+ settings_path=settings_path,
114
+ )
115
+ except Exception:
116
+ logger.debug("failed to record update check attempt", exc_info=True)
117
+
30
118
 
31
119
  class UpdatePromptDecision(Enum):
32
120
  SKIP = "skip"
@@ -154,7 +242,14 @@ def check_update_blocking(
154
242
  else "http_client.trust_env.disabled"
155
243
  )
156
244
  profiler.mark("http_client.construct.start")
157
- client = httpx.Client(timeout=3.0, trust_env=trust_env)
245
+ client_kwargs: dict[str, Any] = {"timeout": 3.0, "trust_env": trust_env}
246
+ if not trust_env:
247
+ # 默认路径用 certifi context 绕开 Windows 证书库枚举(见 _get_update_check_ssl_context)。
248
+ # trust_env 逃生通道(企业代理/自签根证书)保持 httpx 默认 verify=True 行为不变。
249
+ ssl_context = _get_update_check_ssl_context()
250
+ if ssl_context is not None:
251
+ client_kwargs["verify"] = ssl_context
252
+ client = httpx.Client(**client_kwargs)
158
253
  if profiler is not None:
159
254
  profiler.mark("http_client.construct.done")
160
255
 
@@ -207,7 +302,7 @@ def format_update_hint(info: UpdateInfo) -> str:
207
302
  # Returns plain text only. Callers are responsible for adding markup
208
303
  # appropriate to their render target (Rich Console vs prompt_toolkit).
209
304
  return (
210
- f"New version available: {info.latest_version} "
305
+ f"New version available: {info.latest_version} "
211
306
  f"(current: {info.current_version}) "
212
307
  f"Run `uv tool update {info.package}` to update."
213
308
  )
@@ -433,7 +528,7 @@ def _load_package_update_state(
433
528
 
434
529
  def _write_package_update_state(
435
530
  package: str,
436
- values: dict[str, str],
531
+ values: dict[str, Any],
437
532
  *,
438
533
  settings_path: Path | None,
439
534
  ) -> None:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "comate-cli"
7
- version = "0.7.6"
7
+ version = "0.7.7"
8
8
  description = "Comate terminal CLI built on comate-agent-sdk"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11,<3.14"
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+
9
+ def _ensure_cli_package_on_path() -> None:
10
+ cli_project_root = Path(__file__).resolve().parents[1]
11
+ cli_project_root_str = str(cli_project_root)
12
+ if cli_project_root_str not in sys.path:
13
+ sys.path.insert(0, cli_project_root_str)
14
+
15
+
16
+ _ensure_cli_package_on_path()
17
+
18
+
19
+ @pytest.fixture(autouse=True)
20
+ def _isolate_update_check_settings(tmp_path, monkeypatch):
21
+ """每个测试用独立临时 settings.json,隔离启动更新检查的限频状态。
22
+
23
+ update_check 的限频(last_check_at)与 last_seen_version 都落在 USER_SETTINGS_PATH。
24
+ 驱动 run()/scheduler 的集成测试若写到真实用户配置,会造成跨用例顺序相关的脏状态。
25
+ 在这里把模块级 USER_SETTINGS_PATH 指向临时目录,统一隔离。
26
+ 显式传 settings_path 的单测不受影响。
27
+ """
28
+ from comate_cli.terminal_agent import update_check
29
+
30
+ monkeypatch.setattr(
31
+ update_check, "USER_SETTINGS_PATH", tmp_path / "settings.json"
32
+ )
33
+ yield
@@ -28,6 +28,17 @@ class _FakeStatusBar:
28
28
  self.transients.append(message)
29
29
 
30
30
 
31
+ class _FakeRenderer:
32
+ def __init__(self) -> None:
33
+ self.system_messages: list[str] = []
34
+
35
+ def flush_pending_logs(self) -> None:
36
+ pass
37
+
38
+ def append_system_message(self, message: str) -> None:
39
+ self.system_messages.append(message)
40
+
41
+
31
42
  class _FakeTUI:
32
43
  initialized_session_id = None
33
44
 
@@ -230,8 +241,9 @@ class TestAppStartupLatency(unittest.IsolatedAsyncioTestCase):
230
241
  self.assertEqual(len(created_tuis), 1)
231
242
  self.assertTrue(created_tuis[0].run_called.is_set())
232
243
 
233
- async def test_background_update_hint_marks_seen_and_uses_status_bar(self) -> None:
244
+ async def test_background_update_hint_marks_seen_and_uses_scrollback(self) -> None:
234
245
  status_bar = _FakeStatusBar(_fake_session())
246
+ renderer = _FakeRenderer()
235
247
  info = UpdateInfo(
236
248
  package="comate-cli",
237
249
  current_version="0.3.3",
@@ -246,17 +258,20 @@ class TestAppStartupLatency(unittest.IsolatedAsyncioTestCase):
246
258
  patch.object(app_module, "mark_update_seen") as mark_seen,
247
259
  ):
248
260
  task = app_module._schedule_update_check_on_launch(
249
- status_bar=status_bar,
261
+ renderer=renderer,
250
262
  profiler=app_module.StartupProfiler.from_env(logger=app_module.logger),
251
263
  )
252
264
  await task
253
265
 
254
- self.assertEqual(len(status_bar.transients), 1)
255
- self.assertIn("0.3.6", status_bar.transients[0])
266
+ self.assertEqual(status_bar.transients, [])
267
+ self.assertEqual(len(renderer.system_messages), 1)
268
+ self.assertTrue(renderer.system_messages[0].startswith("✨ New version available"))
269
+ self.assertIn("0.3.6", renderer.system_messages[0])
256
270
  mark_seen.assert_called_once_with(info)
257
271
 
258
272
  async def test_background_update_prompt_is_deferred_without_running_update(self) -> None:
259
273
  status_bar = _FakeStatusBar(_fake_session())
274
+ renderer = _FakeRenderer()
260
275
  info = UpdateInfo(
261
276
  package="comate-cli",
262
277
  current_version="0.3.3",
@@ -272,13 +287,15 @@ class TestAppStartupLatency(unittest.IsolatedAsyncioTestCase):
272
287
  patch.object(app_module, "run_update_command", AsyncMock()) as run_update,
273
288
  ):
274
289
  task = app_module._schedule_update_check_on_launch(
275
- status_bar=status_bar,
290
+ renderer=renderer,
276
291
  profiler=app_module.StartupProfiler.from_env(logger=app_module.logger),
277
292
  )
278
293
  await task
279
294
 
280
- self.assertEqual(len(status_bar.transients), 1)
281
- self.assertIn("0.3.6", status_bar.transients[0])
295
+ self.assertEqual(status_bar.transients, [])
296
+ self.assertEqual(len(renderer.system_messages), 1)
297
+ self.assertTrue(renderer.system_messages[0].startswith("✨ New version available"))
298
+ self.assertIn("0.3.6", renderer.system_messages[0])
282
299
  show_prompt.assert_not_awaited()
283
300
  run_update.assert_not_awaited()
284
301
 
@@ -78,6 +78,18 @@ class TestStartupProfiler(unittest.TestCase):
78
78
  self.assertIn("phase=agent.build.done", message)
79
79
  self.assertIn("elapsed_ms=125.000", message)
80
80
 
81
+ def test_mark_at_backfills_phase_relative_to_started_at(self) -> None:
82
+ """mark_at 用于补记 profiler 创建前已测好的时刻(如 main 入口的 app import)。"""
83
+ logger = MagicMock(spec=logging.Logger)
84
+
85
+ with patch.dict("os.environ", {"COMATE_STARTUP_PROFILE": "1"}, clear=False):
86
+ profiler = StartupProfiler.from_env(logger=logger, started_at=100.0)
87
+ profiler.mark_at("main.app_import.done", 102.5)
88
+
89
+ message = logger.info.call_args.args[0]
90
+ self.assertIn("phase=main.app_import.done", message)
91
+ self.assertIn("elapsed_ms=2500.000", message)
92
+
81
93
  def test_child_prefixes_phase_names(self) -> None:
82
94
  logger = MagicMock(spec=logging.Logger)
83
95
 
@@ -21,6 +21,8 @@ from comate_cli.terminal_agent.update_check import (
21
21
  get_pending_update_prompt_info,
22
22
  mark_update_seen,
23
23
  record_skip_until_next_version,
24
+ record_update_check_attempt,
25
+ should_check_for_update,
24
26
  )
25
27
 
26
28
 
@@ -258,8 +260,8 @@ class TestCheckUpdate(unittest.IsolatedAsyncioTestCase):
258
260
 
259
261
  self.assertTrue(await asyncio.to_thread(worker_finished.wait, 0.2))
260
262
 
261
- async def test_background_update_hint_uses_status_bar_on_first_detection(self) -> None:
262
- status_bar = MagicMock()
263
+ async def test_background_update_hint_goes_to_scrollback_not_status_bar_on_first_detection(self) -> None:
264
+ renderer = MagicMock()
263
265
  update_info = UpdateInfo(
264
266
  package="comate-cli",
265
267
  current_version="0.3.3",
@@ -278,17 +280,18 @@ class TestCheckUpdate(unittest.IsolatedAsyncioTestCase):
278
280
  patch.object(app_module, "mark_update_seen") as mark_seen,
279
281
  ):
280
282
  task = app_module._schedule_update_check_on_launch(
281
- status_bar=status_bar,
283
+ renderer=renderer,
282
284
  profiler=app_module.StartupProfiler.from_env(logger=app_module.logger),
283
285
  )
284
286
  await task
285
287
 
286
- status_bar.show_transient.assert_called_once()
287
- self.assertIn("New version available", status_bar.show_transient.call_args.args[0])
288
+ renderer.append_system_message.assert_called_once()
289
+ message = renderer.append_system_message.call_args.args[0]
290
+ self.assertTrue(message.startswith("✨ New version available"), message)
291
+ self.assertIn("0.3.6", message)
288
292
  mark_seen.assert_called_once_with(update_info)
289
293
 
290
294
  async def test_background_update_check_waits_before_network_check(self) -> None:
291
- status_bar = MagicMock()
292
295
  profiler = _RecordingProfiler()
293
296
  check_started = False
294
297
 
@@ -303,7 +306,7 @@ class TestCheckUpdate(unittest.IsolatedAsyncioTestCase):
303
306
  patch.object(app_module, "_check_update", side_effect=_check) as check_update,
304
307
  ):
305
308
  task = app_module._schedule_update_check_on_launch(
306
- status_bar=status_bar,
309
+ renderer=MagicMock(),
307
310
  profiler=profiler,
308
311
  )
309
312
  await asyncio.sleep(0)
@@ -708,5 +711,100 @@ class TestPendingPromptIntegration(unittest.IsolatedAsyncioTestCase):
708
711
  mock_tui_cls.assert_not_called()
709
712
 
710
713
 
714
+ class TestUpdateCheckThrottle(unittest.TestCase):
715
+ """限频:interval 内最多发起一次网络检查,避免每次启动都付 httpx + 网络成本。"""
716
+
717
+ def _settings_path(self) -> Path:
718
+ tmp = tempfile.mkdtemp()
719
+ return Path(tmp) / "settings.json"
720
+
721
+ def test_fresh_state_should_check(self) -> None:
722
+ path = self._settings_path()
723
+ self.assertTrue(should_check_for_update(settings_path=path, now=1000.0))
724
+
725
+ def test_within_interval_is_throttled(self) -> None:
726
+ path = self._settings_path()
727
+ record_update_check_attempt(settings_path=path, now=1000.0)
728
+ # 1 小时后仍在默认 24h 窗口内 → 跳过
729
+ self.assertFalse(should_check_for_update(settings_path=path, now=1000.0 + 3600))
730
+
731
+ def test_after_interval_should_check_again(self) -> None:
732
+ path = self._settings_path()
733
+ record_update_check_attempt(settings_path=path, now=1000.0)
734
+ later = 1000.0 + 24 * 60 * 60 + 1
735
+ self.assertTrue(should_check_for_update(settings_path=path, now=later))
736
+
737
+ def test_clock_rollback_should_check(self) -> None:
738
+ path = self._settings_path()
739
+ record_update_check_attempt(settings_path=path, now=10_000.0)
740
+ # 系统时钟回拨到尝试时间之前 → 不应被卡住
741
+ self.assertTrue(should_check_for_update(settings_path=path, now=5_000.0))
742
+
743
+ def test_interval_zero_disables_throttle(self) -> None:
744
+ path = self._settings_path()
745
+ record_update_check_attempt(settings_path=path, now=1000.0)
746
+ with patch.dict(
747
+ "os.environ", {"COMATE_CLI_UPDATE_CHECK_INTERVAL_S": "0"}, clear=False
748
+ ):
749
+ self.assertTrue(should_check_for_update(settings_path=path, now=1000.5))
750
+
751
+ def test_attempt_preserves_existing_state(self) -> None:
752
+ path = self._settings_path()
753
+ info = UpdateInfo(
754
+ package="comate-cli",
755
+ current_version="0.3.3",
756
+ latest_version="0.3.6",
757
+ release_notes_url="https://example.invalid",
758
+ )
759
+ mark_update_seen(info, settings_path=path)
760
+ record_update_check_attempt(settings_path=path, now=1234.0)
761
+ # last_seen_version 不应被 last_check_at 写入覆盖(两者共存于同一 state dict)
762
+ from comate_cli.terminal_agent.update_check import _load_package_update_state
763
+
764
+ state = _load_package_update_state("comate-cli", settings_path=path)
765
+ self.assertEqual(state.get("last_seen_version"), "0.3.6")
766
+ self.assertEqual(float(state.get("last_check_at")), 1234.0)
767
+
768
+
769
+ class TestUpdateCheckSSLContext(unittest.IsolatedAsyncioTestCase):
770
+ """默认路径用 certifi context 绕开 Windows 证书库枚举;trust_env 逃生通道保持默认。"""
771
+
772
+ async def test_default_path_passes_certifi_ssl_context(self) -> None:
773
+ import ssl as _ssl
774
+
775
+ captured: list[dict[str, object]] = []
776
+
777
+ def _factory(**kwargs):
778
+ captured.append(dict(kwargs))
779
+ return _FakeClient("0.3.6")
780
+
781
+ with (
782
+ patch.dict("os.environ", {"COMATE_CLI_UPDATE_CHECK_TRUST_ENV": ""}, clear=False),
783
+ patch("comate_cli.terminal_agent.update_check._is_chinese_locale", return_value=False),
784
+ patch("comate_cli.terminal_agent.update_check.importlib.metadata.version", return_value="0.3.3"),
785
+ patch("httpx.Client", side_effect=_factory),
786
+ ):
787
+ await app_module._check_update()
788
+
789
+ self.assertIsInstance(captured[-1].get("verify"), _ssl.SSLContext)
790
+
791
+ async def test_trust_env_path_keeps_httpx_default_verify(self) -> None:
792
+ captured: list[dict[str, object]] = []
793
+
794
+ def _factory(**kwargs):
795
+ captured.append(dict(kwargs))
796
+ return _FakeClient("0.3.6")
797
+
798
+ with (
799
+ patch.dict("os.environ", {"COMATE_CLI_UPDATE_CHECK_TRUST_ENV": "1"}, clear=False),
800
+ patch("comate_cli.terminal_agent.update_check._is_chinese_locale", return_value=False),
801
+ patch("comate_cli.terminal_agent.update_check.importlib.metadata.version", return_value="0.3.3"),
802
+ patch("httpx.Client", side_effect=_factory),
803
+ ):
804
+ await app_module._check_update()
805
+
806
+ self.assertNotIn("verify", captured[-1])
807
+
808
+
711
809
  if __name__ == "__main__":
712
810
  unittest.main(verbosity=2)
@@ -301,7 +301,7 @@ wheels = [
301
301
 
302
302
  [[package]]
303
303
  name = "comate-agent-sdk"
304
- version = "0.8.6"
304
+ version = "0.8.7"
305
305
  source = { editable = "../../" }
306
306
  dependencies = [
307
307
  { name = "aiohttp" },
@@ -375,7 +375,7 @@ dev = [
375
375
 
376
376
  [[package]]
377
377
  name = "comate-cli"
378
- version = "0.7.5"
378
+ version = "0.7.6"
379
379
  source = { editable = "." }
380
380
  dependencies = [
381
381
  { name = "charset-normalizer" },
@@ -1,14 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import sys
4
- from pathlib import Path
5
-
6
-
7
- def _ensure_cli_package_on_path() -> None:
8
- cli_project_root = Path(__file__).resolve().parents[1]
9
- cli_project_root_str = str(cli_project_root)
10
- if cli_project_root_str not in sys.path:
11
- sys.path.insert(0, cli_project_root_str)
12
-
13
-
14
- _ensure_cli_package_on_path()
File without changes
File without changes
File without changes