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.
- {comate_cli-0.7.6 → comate_cli-0.7.7}/PKG-INFO +1 -1
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/main.py +10 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/app.py +33 -15
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/startup_profile.py +16 -3
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/update_check.py +98 -3
- {comate_cli-0.7.6 → comate_cli-0.7.7}/pyproject.toml +1 -1
- comate_cli-0.7.7/tests/conftest.py +33 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_app_startup_latency.py +24 -7
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_startup_profile.py +12 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_update_check.py +105 -7
- {comate_cli-0.7.6 → comate_cli-0.7.7}/uv.lock +2 -2
- comate_cli-0.7.6/tests/conftest.py +0 -14
- {comate_cli-0.7.6 → comate_cli-0.7.7}/.gitignore +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/CHANGELOG.md +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/README.md +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/__init__.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/__main__.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/mcp_cli.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/__init__.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/animations.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/assistant_render.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/codenames.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/config/__init__.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/config/model.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/config/picker.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/config/picker_state.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/config/store.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/custom_slash_commands.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/env_utils.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/error_display.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/event_renderer.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/figures.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/fragment_utils.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/goal_resume_view.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/history_printer.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/input_geometry.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/layout_coordinator.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/logging_adapter.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/logo.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/markdown_render.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/mention_completer.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/message_style.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/models.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/path_context_hint.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/__init__.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/components/__init__.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/components/detail_view.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/components/plugin_list.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/components/search_box.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/components/tab_bar.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/marketplace_install_view.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/plugin_picker.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/tabs/__init__.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/tabs/discover_tab.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/tabs/errors_tab.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/tabs/installed_tab.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/plugins/tabs/marketplaces_tab.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/preflight.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/question_view.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/resume_picker.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/resume_preview.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/resume_selector.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/rpc_protocol.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/rpc_stdio.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/selection_menu.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/slash_commands.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/startup.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/status_bar.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/statusline/__init__.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/statusline/model.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/statusline/picker.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/statusline/picker_state.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/statusline/store.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/text_effects.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tips.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tool_fold.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tool_result_formatters.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tool_result_store.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tool_result_viewer.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tool_view.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/transcript_viewer.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/__init__.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/btw_view.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/commands.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/history_sync.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/input_behavior.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/key_bindings.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/mcp_connecting_view.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/render_panels.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/slash_command_registry.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/comate_cli/terminal_agent/tui_parts/ui_mode.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/config/__init__.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/config/test_model.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/config/test_picker_state.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/config/test_picker_ui.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/config/test_roundtrip.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/config/test_store_load.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/config/test_store_save.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/fixtures/fake_mcp_misbehaving_stdout.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/statusline/__init__.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/statusline/test_model.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/statusline/test_picker_state.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/statusline/test_store.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_animator_shuffle.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_app_mcp_preload.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_app_preflight_gate.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_app_print_mode.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_app_shutdown.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_app_token_cost_config.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_app_usage_line.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_btw_slash_command.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_cli_project_root.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_compact_command_semantics.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_completion_context_activation.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_completion_status_panel.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_context_command.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_custom_slash_commands.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_discover_tab.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_errors_tab.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_event_renderer.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_event_renderer_boundary.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_event_renderer_e2e.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_event_renderer_log_boundary.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_event_renderer_log_queue.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_event_renderer_streaming.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_event_renderer_tool_fold.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_format_error.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_goal_resume_tui.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_goal_resume_view.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_goal_slash_command.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_handle_error.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_history_printer.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_history_printer_log.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_history_printer_subtitle_position.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_history_printer_tool_fold.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_history_sync.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_history_sync_tool_fold.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_input_behavior.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_input_history.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_installed_tab.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_interrupt_exit_semantics.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_layout_coordinator.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_logging_adapter.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_logo.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_main_args.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_markdown_render.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_marketplaces_tab.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_mcp_cli.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_mcp_slash_command.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_mention_completer.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_path_context_hint.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_plugin_slash_commands.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_plugin_tui_components.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_preflight.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_preflight_copilot.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_question_key_bindings.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_question_view.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_resume_picker.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_resume_preview.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_resume_selector.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_rewind_command_semantics.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_rpc_protocol.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_rpc_stdio_bridge.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_selection_menu.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_session_query_token_summary.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_shutdown_noise_guard.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_shutdown_noise_integration.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_skills_slash_command.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_slash_argument_hint.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_slash_clear.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_slash_completer.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_slash_registry.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_status_bar.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_status_bar_transient.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_task_panel_format.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_task_panel_key_bindings.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_task_panel_rendering.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_task_poll.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tool_fold.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tool_fold_panel.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tool_result_formatters.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tool_result_store.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tool_result_viewer.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tool_result_viewer_key_bindings.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tool_view.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_transcript_viewer.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_transcript_viewer_tool_fold.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_elapsed_status.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_esc_queue.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_mcp_init_gate.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_paste_newline_guard.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_paste_placeholder.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_queue_preview.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_queue_sdk_source.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_split_invariance.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_startup_latency.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_team_messages.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_thinking_display.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_tui_tool_result_registry_lifecycle.py +0 -0
- {comate_cli-0.7.6 → comate_cli-0.7.7}/tests/test_usage_command.py +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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(
|
|
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 = (
|
|
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
|
-
|
|
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,
|
|
531
|
+
values: dict[str, Any],
|
|
437
532
|
*,
|
|
438
533
|
settings_path: Path | None,
|
|
439
534
|
) -> None:
|
|
@@ -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
|
|
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
|
-
|
|
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(
|
|
255
|
-
self.
|
|
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
|
-
|
|
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(
|
|
281
|
-
self.
|
|
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
|
|
262
|
-
|
|
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
|
-
|
|
283
|
+
renderer=renderer,
|
|
282
284
|
profiler=app_module.StartupProfiler.from_env(logger=app_module.logger),
|
|
283
285
|
)
|
|
284
286
|
await task
|
|
285
287
|
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|